Compare commits

..

No commits in common. "master" and "v1.0.8" have entirely different histories.

22 changed files with 178 additions and 13099 deletions

View File

@ -4,16 +4,13 @@ WORKDIR /usr/src/app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
RUN apk add python3-dev build-base linux-headers pcre-dev spamassassin
RUN apk add python3-dev build-base linux-headers pcre-dev
RUN pip install uwsgi
#spamassassin_client
ENV UID=0
ENV MOUNT=/
# Since the package maintainer doesnt merge PR
# https://github.com/petermat/spamassassin_client/pull/2
COPY ./main.py ./spamassassin/spamassasin_client.py ./
COPY ./main.py ./list.tpl ./
# I juste wanted to change the socket owner but it turned out I needed to change thu uwsgi user
#CMD uwsgi --exec-asap 'chown $UID:$UID /tmp/uwsgi/ ; mkdir -p $BASE_PATH && chown $UID:$UID $BASE_PATH' -s /tmp/uwsgi/uwsgi.sock --uid $UID --manage-script-name --mount /=server:app

View File

@ -1,81 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" src="style.css" />
<title>Contact mailer admin interface</title>
<meta charset="utf-8" />
</head>
<body>
<main id="app">
<section>
<h3>Athentification</h3>
<div v-if="!loggedin" class="loginform">
<form v-on:submit.prevent="login">
<select v-model="type">
<option value="token">Utilisateur</option>
<option value="admin_pass">Administrateur</option>
</select>
<input type="password" v-model="password" />
<input type="submit" value="connect" />
</form>
</div>
<div v-else="">
<p>Connecté en tant que {{ type }}</p>
<button v-on:click="logout">Se déconnecter</button>
</div>
</section>
<section>
<div v-if="loggedin && type=='admin_pass'">
<h3>Utilisateurices</h3>
<form v-on:submit.prevent="addUser">
<input type="text" v-model="newUser" />
<input type="submit" />
</form>
<button v-on:click="getUsers">Rafraichir les utilisateurs</button>
<ul>
<li v-for="user in users">{{user.token}} — {{user.username}}</li>
</ul>
</div>
</section>
<section>
<h3>Formulaires</h3>
<button v-on:click="getForms">Rafraichir les formulaires</button>
<ul>
<li v-for="form in forms">
<div>À {{form.mail}}</div>
<div>Objet {{form.subject}}</div>
<div>{{form.content}}</div>
<div>{{form.token}} — {{form.honeypotfield}} — {{form.timerdelay}}</div>
<button v-on:click="deleteForm(form.token)">Supprimer</button>
</li>
</ul>
<div v-if="page=='new_user'">
<form v-on:submit.prevent="addForm">
<label for="mail">Mail :</label>
<input v-model="newForm.mail" type="text" name="mail" id="mail" />
<br />
<label for="content">Contenu :</label>
<textarea v-model="newForm.content" name="content" id="content">
</textarea>
<br />
<label for="subject">Objet :</label>
<input v-model="newForm.subject" type="text" name="subject" id="subject" />
<br />
<label for="honeypot">Honeypot (ne pas toucher) :</label>
<input v-model="newForm.honeypotfield" type="text" name="honeypot" id="honeypot" />
<br />
<label for="timerdelay">Timer delay :</label>
<input v-model="newForm.timerdelay" type="number" name="timerdelay" id="timerdelay" />
<br />
<input type="submit" />
</form>
</div>
</section>
</main>
<script src="./vue.js"></script>
<script src="./index.js"></script>
</body>
</html>

View File

@ -1,118 +0,0 @@
var app = new Vue({
el: '#app',
data: {
type: 'admin_pass', /* admin_pass or token */
password: 'test',
loggedin: false,
mailerHost: 'https://mailer.jean-cloud.net',
//mailerHost: 'http://localhost:8080',
//mailerHost: '/api',
forms: [],
users: [],
newUser: '',
page:'new_user',
newForm: {
'content': '{{message}}',
'subject': '[contact jean-cloud.net] {{nom|annonyme}} — {{objet}}',
'mail': 'contact@jean-cloud.org',
'honeypotfield': 'prenom',
'timerdelay': 5,
}
},
methods: {
login: function () {
if (!this.type) {
console.log('missing type')
return
}
if (!this.password) {
console.log('missing password')
return
}
this.loggedin = true
this.getForms()
if ( this.type == 'admin_pass' )
this.getUsers()
},
logout: function () {
this.type = 'token'
this.password = null
this.loggedin = false
},
getForms: function () {
fetch(this.mailerHost + '/form/list', {
method: 'POST',
body: this.type + '=' + this.password
})
.then(response => response.json())
.then(data => {
this.forms = data.data
})
.catch((error) => {
console.error(error)
})
},
getUsers: function () {
fetch(this.mailerHost + '/user/list', {
method: 'POST',
body: this.type + '=' + this.password
})
.then(response => response.json())
.then(data => {
this.users = data.data
})
.catch((error) => {
console.error(error)
})
},
addForm: function () {
fetch(this.mailerHost + '/form', {
method: 'post',
body: this.type + '=' + this.password
+ '&subject=' + this.newForm.subject
+ '&mail=' + this.newForm.mail
+ '&content=' + this.newForm.content
+ '&honeypotfield=' + this.newForm.honeypotfield
+ '&timerdelay=' + this.newForm.timerdelay
})
.then(data => {
console.log(data)
this.getForms()
})
.catch(error => {
console.log(error)
})
},
addUser: function () {
if (!this.newUser) {
console.log('need username')
return
}
fetch(this.mailerHost + '/user/' + this.newUser, {
method: 'put',
body: this.type + '=' + this.password
})
.then(data => {
this.newUser = ''
this.getUsers()
})
.catch((error) => {
console.error(error)
})
},
deleteForm: function (formId) {
fetch(this.mailerHost + '/form/' + formId, {
method: 'delete',
body: this.type + '=' + this.password
})
.then(data => {
console.log(data)
this.getForms()
})
.catch((error) => {
console.error(error)
})
},
},
})

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
set -e
version=2.1.0
version=2.0.1
docker build -t jeancloud/contact-mailer:latest -t jeancloud/contact-mailer:$version .
docker push jeancloud/contact-mailer:latest
docker push jeancloud/contact-mailer:$version

1
client/.gitignore vendored
View File

@ -1 +0,0 @@
dist

View File

@ -1,141 +1,48 @@
/* Executed after page loading */
(function () {
class JeanCloudContactFormNotifier {
constructor (theme, messageContainer) {
/* Choose the theme */
this.theme = theme ? theme : 'light'
/* Create or get the message container */
if (messageContainer) {
this.messageContainer = messageContainer
} else {
this.messageContainer = document.createElement('div')
this.messageContainer.classList.add('contact-mailer-message-container')
document.body.appendChild(this.messageContainer)
}
}
message(level, text) {
/* This function displays a message */
const messageElement = document.createElement('p')
const messageId = 'contact-mailer-' + this.theme + '-message-' + level
this.messageContainer.appendChild(messageElement)
messageElement.textContent = text
messageElement.classList.add('contact-mailer-message')
messageElement.classList.add(messageId)
messageElement.id = messageId
/*add close button to the alert message*/
const closeButtonElement = document.createElement('span')
messageElement.appendChild(closeButtonElement)
closeButtonElement.textContent = "×"
closeButtonElement.classList.add('contact-mailer-message-close-button')
closeButtonElement.title = 'Cliquer pour fermer'
closeButtonElement.onclick = () => {
messageElement.parentNode.removeChild(messageElement)
}
//setTimeout(() => {
// try {
// messageElement.parentNode.removeChild(messageElement)
// } catch (e) {} /* Silently fail if message was already deleted by click */
//}, 10000)
}
success (text) {
this.message('success', text)
}
error (text) {
this.message('error', text)
}
function message (messageContainer, level, text) {
const messageElement = document.createElement('p')
messageContainer.appendChild(messageElement)
messageElement.textContent = text
messageElement.classList.add('contact-mailer-message')
messageElement.classList.add('contact-mailer-message-'+level)
setTimeout(() => {
messageContainer.removeChild(messageElement)
}, 10000)
}
function jeanCloudContactFormIntercept (formId, notifier) {
function interceptForm (formId) {
/*
* This function intercepts a form submission and send it via XHR.
* Param formId is the HTML id of the form
*/
const formElem = document.getElementById(formId)
const formElem = document.getElementById(formId)
if (!formElem) {
console.error('You tried to intercept form with id:"' + formId + '" but it was not found.')
console.error('You tried to intercept form id:"' + formId + '" but it was not found.')
return
}
if (!notifier)
console.log('No notifier given, no message can be displayed')
/* Create the message container */
const messageBox = document.createElement('div')
messageBox.classList.add('contact-mailer-message-container')
formElem.parentNode.insertBefore(messageBox, formElem)
/* Intercept the submit event */
formElem.onsubmit = async (e) => {
e.preventDefault()
/* Add loading text */
const submitButton = formElem.querySelector('[type="submit"]')
const loadingText = document.createElement('span')
loadingText.classList.add("contact-mailer-sending");
loadingText.textContent = 'Envoi en cours…'
submitButton.after(loadingText)
/* Add the filling timer in seconds */
const timerField = document.createElement('input')
timerField.value = Math.round((Date.now() - contactMailerPageLoadedTime) / 1000)
timerField.name = 'timerfield'
timerField.hidden = 'hidden'
formElem.appendChild(timerField)
/* XHR */
fetch(formElem.action, {
method: formElem.method,
body: new FormData(formElem)
})
.then(data => {
loadingText.parentNode.removeChild(loadingText)
if (data.ok && data.status == 200) {
notifier.success('Le message a bien été envoyé !')
if (!data.ok || data.status == 500) {
message(messageBox, 'error', 'Erreur du service denvoi. Réessayez plus tard ou contactez https://jean-cloud.net')
} else if (data.ok || data.status == 200) {
message(messageBox, 'success', 'Le message a bien été envoyé !')
formElem.reset()
} else if (!data.ok && data.status == 500) {
notifier.error('Erreur du service denvoi. Réessayez plus tard ou contactez https://jean-cloud.net')
} else if (!data.ok && data.status == 400) {
notifier.error('Une erreur est survenue dans la requête que vous avez effectué. Réessayez plus tard ou contactez le webmaster par un autre moyen.')
// TODO display servers error message
}
})
.catch((error) => {
console.error(error)
loadingText.parentNode.removeChild(loadingText)
notifier.error('Impossible denvoyer le formulaire. Vérifiez votre connexion internet ou réessayez plus tard.')
message(messageBox, 'error', 'Impossible denvoyer le formulaire. Vérifiez votre connexion internet ou réessayez plus tard.')
})
/* Remove timer field after xhr. So we can try again. */
formElem.removeChild(timerField)
}
}
/* Get the current js file location */
const path = (document.currentScript.src[-1] == '/' ? document.currentScript.src : document.currentScript.src.replace(/\/[^\/]*$/, ''))
/* Adding a css file */
var link = document.createElement("link");
link.type = "text/css";
link.rel = "stylesheet";
link.crossOrigin = 'anonymous';
link.href = path + "/style.css";
link.integrity = 'sha384-D12RSMaIURTgZZljhdQqYlQzgEfXvOFwtiqzkWnNcDbKFwMWXcmsCRFO5BNii0MB'
// cat style.css | openssl dgst -sha384 -binary | openssl base64 -A
document.head.appendChild(link);
/* Load the targeted forms */
var configs = document.getElementsByClassName('contact-form-config')
for (var i=0; i<configs.length; i++) {
var formId = configs[i].getAttribute('form-id')
var theme = configs[i].getAttribute('notify-theme')
jeanCloudContactFormIntercept(formId, new JeanCloudContactFormNotifier(theme))
}
var contactMailerPageLoadedTime = Date.now()
})()
interceptForm ('contact-mailer-form')

View File

@ -1,21 +0,0 @@
{
"name": "jean-cloud-contact-mailer-client",
"version": "1.1.6",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"npm-auto-version": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/npm-auto-version/-/npm-auto-version-1.0.0.tgz",
"integrity": "sha1-b21s8b2JEL5U71DnOMCfgSWE3jI=",
"requires": {
"semver": "^5.0.3"
}
},
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
}
}
}

View File

@ -1,18 +1,11 @@
{
"name": "jean-cloud-contact-mailer-client",
"version": "1.1.6",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"prepublishOnly": "npm-auto-version",
"postpublish": "git push origin --tags",
"build": "mkdir -p dist && cp index.js style.css dist"
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"npm-auto-version": "^1.0.0"
},
"devDependencies": {}
"license": "ISC"
}

View File

@ -2,64 +2,20 @@
}
.contact-mailer-message-container {
position: fixed;
right: 0;
bottom: 0;
width: 20%;
min-width: 320px;
z-index: 10;
}
.contact-mailer-message {
border: 1px solid;
padding: 15px 70px 15px 10px;
padding: 3px;
border-radius: 2px;
position: relative;
}
.contact-mailer-plain-message-error {
.contact-mailer-message-error {
color: white;
background-color: #ef4757;
border-color: #ef4757;
}
.contact-mailer-plain-message-success {
.contact-mailer-message-success {
color: white;
background-color: #00B06A;
border-color: #00B06A;
background-color: #06d6a0;
border-color: #06d6a0;
}
.contact-mailer-light-message-error {
color: #721c24;
background-color: #f8d7da;
border-color: #f5c6cb;
}
.contact-mailer-light-message-success {
color: #155724;
background-color: #d4edda;
border-color: #c3e6cb;
}
.contact-mailer-sending {
padding: 5px 10px;
}
.contact-mailer-message-close-button {
position: absolute;
right: 0;
top: 0;
font-size: 60px;
height: calc(100% - 10px);
width: 60px;
border-left: 1px solid currentColor;
padding: 5px 5px;
text-align: center;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-sizing: unset;
}

6
list.tpl Normal file
View File

@ -0,0 +1,6 @@
<h2>Liste</h2>
<ul>
% for item in data:
<li>{{item}}</li>
% end
</ul>

View File

@ -4,7 +4,6 @@ response = bottle.response
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.message import EmailMessage
import os # for environ vars
import sys # to print ot stderr
import re # to match our template system
@ -12,9 +11,7 @@ import pymongo # database
from dotenv import load_dotenv
import random, string # for tokens
import html # for sanitization
from bson.json_util import dumps
import datetime # For email date
from spamassasin_client import SpamAssassin
import datetime # to name unsent mails
##################################################### Bottle stuff ############################################
@ -29,8 +26,10 @@ class StripPathMiddleware(object):
e['PATH_INFO'] = e['PATH_INFO'].rstrip('/')
return self.a(e, h)
app = application = bottle.Bottle(catchall=False)
##################################################### Configuration ############################################
# The exception that is thrown when an argument is missing
@ -69,16 +68,13 @@ smtp_server_password = get_env('SMTP_SERVER_PASSWORD')
smtp_server_sender = get_env('SMTP_SERVER_SENDER')
# Get mongodb connection
mongodb_host = get_env('MONGODB_HOST')
mongodb_host = get_env('MONGODB_HOST')
mongodb_port = get_env('MONGODB_PORT', '27017')
mongodb_dbname = get_env('MONGODB_DBNAME', 'contact_mailer')
# Security
admin_password = get_env('ADMIN_PASSWORD')
# Test purpose, do not send mail
do_not_send = get_env('do_not_send', 'false') == 'true'
if 'SMTP_SSL' in os.environ and os.environ['SMTP_SSL'] == 'true':
security = 'ssl'
elif 'SMTP_STARTTLS' in os.environ and os.onviron['SMTP_STARTTLS'] == 'true':
@ -89,7 +85,6 @@ else:
# mongodb initialization
mongodb_client = pymongo.MongoClient("mongodb://{}:{}/".format(mongodb_host, mongodb_port), connect=False, serverSelectionTimeoutMS=10000, connectTimeoutMS=10000)
mongodb_database = mongodb_client[mongodb_dbname]
print(mongodb_database)
##################################################### main route: mail submission ############################################
@ -100,7 +95,8 @@ def submission ():
if 'token' in request.forms:
token = request.forms.getunicode('token')
else:
return resp(400, 'Le jeton dautentification est requis')
response.status = 400
return 'Le jeton dautentification est requis'
# Getting mail address
if 'mail' in request.forms:
@ -113,52 +109,52 @@ def submission ():
try:
form = mongodb_database['forms'].find({'token': token})[0]
except IndexError as e:
return resp(400, 'Le formulaire demandé est introuvable, merci de vérifier que le token utilisé est le bon')
response.status = 400
return 'Le formulaire demandé est introuvable, merci de vérifier que le token utilisé est le bon'
except pymongo.errors.ServerSelectionTimeoutError as e:
return resp(500, 'La base de donnée nest pas accessible.')
# Did the bot filled the honeypot field?
if 'honeypotfield' in form and form['honeypotfield'] in request.forms and request.forms.get(form['honeypotfield']) != '':
return resp(400, 'We identified you as a bot. If this is an error, try to contact us via another way.')
# Is the js timer enabled?
if 'timerdelay' in form:
# Did it work?
if 'timerfield' not in request.forms or int(request.forms.get('timerfield')) < int(form['timerdelay']):
print('timer : {}/{}'.format(request.forms.get('timerfield'), form['timerdelay']))
return resp(400, 'We identified you as a bot. If this is an error, try to contact us via another way.')
response.status = 500
return 'La base de donnée nest pas accessible.'
try:
subject_fields = fill_fields(request, get_fields(form['subject']))
content_fields = fill_fields(request, get_fields(form['content']))
except MissingParameterException as e:
return resp(400, str(e))
response.status = 404
return str(e)
subject = re.sub(form_regex, r'{\1}', form['subject']).format(**subject_fields)
content = re.sub(form_regex, r'{\1}', form['content']).format(**content_fields)
try:
msg = build_mail(from_address, form['mail'], subject, content)
if is_spam(msg):
return resp(400, 'Votre message semble être du spam !')
if not send_mail(form['mail'], msg):
return resp(500, 'Le mail na pas pu être envoyé.')
if not send_mail(from_address, form['mail'], subject, content):
response.status = 500
return 'Le mail na pas pu être envoyé.'
except smtplib.SMTPDataError as e:
save_mail (token, form['mail'], from_address, subject, content)
response.status = 500
error = 'Le mail a été refusé. Merci de réessayer plus tard.'
error = 'Le mail a été refusé. Votre message a été enregistré, il sera remis manuellement à son destinataire.'
except smtplib.SMTPRecipientsRefused as e:
save_mail (token, form['mail'], from_address, subject, content)
response.status = 500
error = 'Impossible de trouver le destinataire du mail. Merci de réessayer plus tard'
error = 'Impossible de trouver le destinataire du mail. Votre message a été enregistré, il sera remis manuellement à son destinataire.'
except Exception as e:
save_mail (token, form['mail'], from_address, subject, content)
raise
# Redirection
#bottle.redirect(success_redirect_default)
origin = request.headers.get('origin')
return resp(200, 'Mail envoyé !')
return '<p>Mail envoyé !</p>' + ('<p>Retour au <a href="{}">formulaire de contact</a></p>'.format(origin) if origin else '')
##################################################### Helpers ############################################
def resp (status, msg, data='{}'):
response.status = status
return '{{"status": "{}", "msg": "{}", "data": {}}}'.format(status, msg, data)
def save_mail (token, to, from_address, subject, content):
with open('unsent/unsent_{}_{}_{}.txt'.format(str(datetime.datetime.now()), token, to), 'w') as f:
f.write("Unsent mail\nSubject: {}\nFrom: {}Content:\n{}".format(
subject,
from_address,
content
))
def get_fields (string):
""" Parse the string looking for template elements and create an array with template to fill and their default values. None if mandatory. """
@ -171,40 +167,19 @@ def fill_fields(request, fields):
"""Look for fields in request and fill fields dict with values or let default ones. If the value is required, throw exception."""
for field in fields:
if field in request.forms:
if request.forms.get(field).strip() == '' and fields[field] is None: # If empty and mandatory
raise MissingParameterException("Le champs {} doit être rempli".format(field))
fields[field] = request.forms.getunicode(field)
if fields[field] is None: # if unicode failed
fields[field] = request.forms.get(field)
if fields[field] is None: # if get failed too
raise Exception("Error, field '{}' not gettable".format(field))
elif fields[field] is None:
elif fields[field] == None:
raise MissingParameterException("Le champs {} est obligatoire".format(field))
return fields
def build_mail(from_address, to, subject, content):
msg = EmailMessage()
def send_mail(from_address, to, subject, content):
"""Actually connect to smtp server, build a message object and send it as a mail"""
msg = MIMEMultipart()
msg['From'] = smtp_server_sender
msg.add_header('reply-to', from_address)
msg['To'] = to
msg['Subject'] = subject
msg['Date'] = datetime.datetime.now()
msg.set_content(MIMEText(content, 'plain', "utf-8"))
#or
#msg.set_content(content)
return msg
def is_spam(msg):
assassin = SpamAssassin(msg.as_string().encode(), 'spamassassin')
return assassin.is_spam()
def send_mail (to, msg):
"""Actually connect to smtp server and send the mail"""
if do_not_send:
print('-------------------------- Following message sent. But only to stdout ----------------------------------')
print(msg.as_string())
print('--------------------------------------------------------------------------------------------------------')
return True
msg.attach(MIMEText(content, 'plain', "utf-8"))
# SMTP preambles
if security == 'ssl':
@ -231,7 +206,7 @@ def login(request):
Privileges : 0=admin 1=loggedIn 1000=guest
"""
if 'admin_pass' in request.forms and request.forms['admin_pass'] == admin_password:
return {'_privilege':0, '_id':'-1'}
return {'_privilege':0}
if 'token' in request.forms:
token = request.forms.getunicode('token')
try:
@ -240,9 +215,9 @@ def login(request):
return user
except IndexError as e:
pass
#except pymongo.errors.ServerSelectionTimeoutError as e:
# response.status = 500
# return {'_error': True} # anonymous
except pymongo.errors.ServerSelectionTimeoutError as e:
response.status = 500
return 'La base de donnée nest pas accessible'
return {'_privilege': 1000} # anonymous
@ -257,45 +232,43 @@ def create_form ():
elif mail_default_subject != '':
subject = mail_default_subject
else:
return resp(400, 'Le champs « sujet » est requis')
response.status = 400
return 'Le champs « sujet » est requis'
# Getting mail content
if 'content' in request.forms:
content = request.forms.getunicode('content')
else:
return resp(400, 'Le champs « contenu » est requis')
response.status = 400
return 'Le champs « contenu » est requis'
# Getting from address
if 'mail' in request.forms:
mail = request.forms.getunicode('mail')
else:
return resp(400, 'Le champs « adresse » est requis')
response.status = 400
return 'Le champs « adresse » est requis'
user = login(request)
if user['_privilege'] > 1:
return resp(400, 'Privilèges insufisants')
response.status = 400
return 'Privilèges insufisants'
# TODO limit the insertion rate
token = ''.join(random.sample(token_chars, token_len))
try:
newEntry = {
inserted = mongodb_database['forms'].insert_one({
'mail': mail,
'content': content,
'subject': subject,
'user_id': user['_id'],
'token': token,
}
if 'honeypotfield' in request.forms:
newEntry['honeypotfield'] = request.forms.getunicode('honeypotfield')
if 'timerdelay' in request.forms:
newEntry['timerdelay'] = request.forms.getunicode('timerdelay')
inserted = mongodb_database['forms'].insert_one(newEntry)
})
except pymongo.errors.ServerSelectionTimeoutError as e:
return resp(500, 'La base de donnée nest pas accessible')
response.status = 500
return 'La base de donnée nest pas accessible'
return resp(200, 'Créé : ' + token)
return 'Créé : ' + token
@app.post('/form/list')
def list_forms ():
@ -306,28 +279,33 @@ def list_forms ():
elif user['_privilege'] == 1:
filt = {'user_id': user['_id']}
else:
return resp(400, 'Privilèges insufisants')
response.status = 400
return 'Privilèges insufisants'
data = mongodb_database['forms'].find(filt)
return resp(200,'', dumps(list(data)))
return bottle.template("list.tpl", data=data)
except pymongo.errors.ServerSelectionTimeoutError as e:
return resp(500,'La base de donnée nest pas accessible')
response.status = 500
return 'La base de donnée nest pas accessible'
@app.delete('/form/<token>')
def delete_form(token):
# TODO If admin or form owner
# If admin or form owner
user = login(request)
if user['_privilege'] > 1:
return resp(400, 'Privilèges insufisants')
response.status = 400
return 'Privilèges insufisants'
# Actually delete
try:
form = mongodb_database['forms'].find({'token':token })[0]
except IndexError as e:
return resp(400, 'Le token nest pas valide')
response.status = 400
return 'Le token nest pas valide'
except pymongo.errors.ServerSelectionTimeoutError as e:
return resp(500, 'La base de donnée nest pas accessible')
response.status = 500
return 'La base de donnée nest pas accessible'
if user['_privilege'] == 0 or (form['user_id'] == user['_id']):
try:
@ -335,9 +313,11 @@ def delete_form(token):
'token': token,
})
except pymongo.errors.ServerSelectionTimeoutError as e:
return resp(500, 'La base de donnée nest pas accessible')
return resp(200, 'Supprimé ' + token)
return resp(400, 'Privilèges insufisants')
response.status = 500
return 'La base de donnée nest pas accessible'
return 'Supprimé ' + token
response.status = 400
return 'Privilèges insufisants'
##################################################### Users ############################################
@ -346,53 +326,63 @@ def delete_form(token):
def list_users ():
user = login(request)
if user['_privilege'] > 0:
return resp(400, 'Privilèges insufisants')
response.status = 400
return 'Privilèges insufisants'
try:
data = mongodb_database['users'].find()
return resp(200, '', dumps(list(data)))
return bottle.template("list.tpl", data=data)
except pymongo.errors.ServerSelectionTimeoutError as e:
return resp(500, 'La base de donnée nest pas accessible')
response.status = 500
return 'La base de donnée nest pas accessible'
@app.route('/user/<username>', method=['OPTIONS', 'PUT'])
@app.put('/user/<username>')
def create_user (username):
user = login(request)
if user['_privilege'] > 0:
return resp(400, 'Privilèges insufisants')
response.status = 400
return 'Privilèges insufisants'
try:
mongodb_database['users'].find({'username': username})[0]
return resp(400, 'Lutilisateur existe déjà')
return 'Lutilisateur existe déjà'
except IndexError as e:
try:
inserted = mongodb_database['users'].insert_one({
'username': username,
'token': ''.join(random.sample(token_chars, token_len))
})
return resp(200, 'Créé : ' + username)
return 'Créé : ' + username
except pymongo.errors.ServerSelectionTimeoutError as e:
return resp(500, 'La base de donnée nest pas accessible')
response.status = 500
return 'La base de donnée nest pas accessible'
except pymongo.errors.ServerSelectionTimeoutError as e:
return resp(500,'La base de donnée nest pas accessible')
response.status = 500
return 'La base de donnée nest pas accessible'
@app.delete('/user/<username>')
def delete_user (username):
user = login(request)
if user['_privilege'] > 0:
return resp(400, 'Privilèges insufisants')
response.status = 400
return 'Privilèges insufisants'
try:
mongodb_database['users'].find({'username': username})[0]
mongodb_database['users'].delete_one({
'username': username,
})
return resp(200, 'Supprimé ' + username)
return 'Supprimé ' + username
except IndexError as e:
return resp(400, 'Lutilisateur nexiste pas')
response.status = 400
return 'Lutilisateur nexiste pas'
except pymongo.errors.ServerSelectionTimeoutError as e:
return resp(500, 'La base de donnée nest pas accessible')
response.status = 500
return 'La base de donnée nest pas accessible'
##################################################### app startup ############################################
prod_app = StripPathMiddleware(app)
if __name__ == '__main__':
bottle.run(app=prod_app, host=listen_address, port=listen_port, debug=True)
bottle.run(app=StripPathMiddleware(app), host=listen_address, port=listen_port, debug=True)
else:
prod_app = StripPathMiddleware(app)

View File

@ -1,6 +1,3 @@
THIS REPO IS DISCONTINUED
- Too much spam through our smtp server
- It is simpler and more friendly to juste put your mail address on your website
# Contact Mailer
A minimal python app to send mail when people fills in your contact form!
@ -62,9 +59,9 @@ The app needs a lot of env vars to run :
SMTP_SERVER_ADDRESS=mail.gandi.net
SMTP_SERVER_PORT=465
SMTP_SSL=true
SMTP_SERVER_USERNAME=noreply@example.net
SMTP_SERVER_PASSWORD=bigpass
SMTP_SERVER_SENDER=noreply@example.net
SMTP_SERVER_USERNAME=nepasrepondre@jean-cloud.org
SMTP_SERVER_PASSWORD=B9UZtOnIlJcRzx8mh2jCsPTQujwTr9I6XyiA
SMTP_SERVER_SENDER=nepasrepondre@jean-cloud.org
MONGODB_HOST=mongodb
ADMIN_PASSWORD=test
UID=1000
@ -78,20 +75,15 @@ You can store them in a `.env` file. The python app will read it or you can pass
- `ADMIN_PASSWORD` password used to manage users
- `UID` used to set the uwsg socket ownership in production
## Client
plain or light theme.
## Roadmap
### Near future
- go on docker hub
- use a standart logger (used by bottle and uwsgi) to log error on mail fail
- [unit tests](https://bottlepy.org/docs/dev/recipes.html#unit-testing-bottle-applications)
- add redirection urls to form config
- Include some [capcha](https://alternativeto.net/software/recaptcha/) support
- Correctly escape html entities
- Sign mails with the server key
- Use a dedicated SMTP server
### Ameliorations
- Use real user/passwords accounts
- Create a gui client
- Créate a gui client

View File

@ -1,13 +0,0 @@
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true
[dev-packages]
[packages]
email = "*"
bottle = "*"
[requires]
python_version = "3.6"

View File

@ -1,2 +0,0 @@
Since the package maintainer doesnt merge PR
https://github.com/petermat/spamassassin_client/pull/2

View File

@ -1,115 +0,0 @@
import socket, select, re, logging
from io import BytesIO
divider_pattern = re.compile(br'^(.*?)\r?\n(.*?)\r?\n\r?\n', re.DOTALL)
first_line_pattern = re.compile(br'^SPAMD/[^ ]+ 0 EX_OK$')
class SpamAssassin(object):
def __init__(self, message, host='127.0.0.1', port=783, timeout=20):
self.score = None
self.symbols = None
# Connecting
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.settimeout(timeout)
client.connect((host, port))
# Sending
client.sendall(self._build_message(message))
client.shutdown(socket.SHUT_WR)
# Reading
resfp = BytesIO()
while True:
ready = select.select([client], [], [], timeout)
if ready[0] is None:
# Kill with Timeout!
logging.info('[SpamAssassin] - Timeout ({0}s)!'.format(str(timeout)))
break
data = client.recv(4096)
if data == b'':
break
resfp.write(data)
# Closing
client.close()
client = None
self._parse_response(resfp.getvalue())
def _build_message(self, message):
reqfp = BytesIO()
data_len = str(len(message)).encode()
reqfp.write(b'REPORT SPAMC/1.2\r\n')
reqfp.write(b'Content-Length: ' + data_len + b'\r\n')
reqfp.write(b'User: cx42\r\n\r\n')
reqfp.write(message)
return reqfp.getvalue()
def _parse_response(self, response):
if response == b'':
logging.info("[SPAM ASSASSIN] Empty response")
return None
match = divider_pattern.match(response)
if not match:
logging.error("[SPAM ASSASSIN] Response error:")
logging.error(response)
return None
first_line = match.group(1)
headers = match.group(2)
body = response[match.end(0):]
# Checking response is good
match = first_line_pattern.match(first_line)
if not match:
logging.error("[SPAM ASSASSIN] invalid response:")
logging.error(first_line)
return None
report_list = [s.strip() for s in body.decode('utf-8').strip().split('\n')]
linebreak_num = report_list.index([s for s in report_list if "---" in s][0])
tablelists = [s for s in report_list[linebreak_num + 1:]]
self.report_fulltext = '\n'.join(report_list)
# join line when current one is only wrap of previous
tablelists_temp = []
if tablelists:
for counter, tablelist in enumerate(tablelists):
if len(tablelist)>1:
if (tablelist[0].isnumeric() or tablelist[0] == '-') and (tablelist[1].isnumeric() or tablelist[1] == '.'):
tablelists_temp.append(tablelist)
else:
if tablelists_temp:
tablelists_temp[-1] += " " + tablelist
tablelists = tablelists_temp
# create final json
self.report_json = dict()
for tablelist in tablelists:
wordlist = re.split('\s+', tablelist)
self.report_json[wordlist[1]] = {'partscore': float(wordlist[0]), 'description': ' '.join(wordlist[1:])}
headers = headers.decode('utf-8').replace(' ', '').replace(':', ';').replace('/', ';').split(';')
self.score = float(headers[2])
def get_report_json(self):
return self.report_json
def get_score(self):
return self.score
def is_spam(self, level=5):
return self.score is None or self.score > level
def get_fulltext(self):
return self.report_fulltext

View File

@ -1,383 +0,0 @@
%!PS-Adobe-3.0
%%Creator: (ImageMagick)
%%Title: (spamassassin_client)
%%CreationDate: (2021-05-17T23:08:26+00:00)
%%BoundingBox: 428 345 508 391
%%HiResBoundingBox: 428 345 508 391
%%DocumentData: Clean7Bit
%%LanguageLevel: 1
%%Orientation: Portrait
%%PageOrder: Ascend
%%Pages: 1
%%EndComments
%%BeginDefaults
%%EndDefaults
%%BeginProlog
%
% Display a color image. The image is displayed in color on
% Postscript viewers or printers that support color, otherwise
% it is displayed as grayscale.
%
/DirectClassPacket
{
%
% Get a DirectClass packet.
%
% Parameters:
% red.
% green.
% blue.
% length: number of pixels minus one of this color (optional).
%
currentfile color_packet readhexstring pop pop
compression 0 eq
{
/number_pixels 3 def
}
{
currentfile byte readhexstring pop 0 get
/number_pixels exch 1 add 3 mul def
} ifelse
0 3 number_pixels 1 sub
{
pixels exch color_packet putinterval
} for
pixels 0 number_pixels getinterval
} bind def
/DirectClassImage
{
%
% Display a DirectClass image.
%
systemdict /colorimage known
{
columns rows 8
[
columns 0 0
rows neg 0 rows
]
{ DirectClassPacket } false 3 colorimage
}
{
%
% No colorimage operator; convert to grayscale.
%
columns rows 8
[
columns 0 0
rows neg 0 rows
]
{ GrayDirectClassPacket } image
} ifelse
} bind def
/GrayDirectClassPacket
{
%
% Get a DirectClass packet; convert to grayscale.
%
% Parameters:
% red
% green
% blue
% length: number of pixels minus one of this color (optional).
%
currentfile color_packet readhexstring pop pop
color_packet 0 get 0.299 mul
color_packet 1 get 0.587 mul add
color_packet 2 get 0.114 mul add
cvi
/gray_packet exch def
compression 0 eq
{
/number_pixels 1 def
}
{
currentfile byte readhexstring pop 0 get
/number_pixels exch 1 add def
} ifelse
0 1 number_pixels 1 sub
{
pixels exch gray_packet put
} for
pixels 0 number_pixels getinterval
} bind def
/GrayPseudoClassPacket
{
%
% Get a PseudoClass packet; convert to grayscale.
%
% Parameters:
% index: index into the colormap.
% length: number of pixels minus one of this color (optional).
%
currentfile byte readhexstring pop 0 get
/offset exch 3 mul def
/color_packet colormap offset 3 getinterval def
color_packet 0 get 0.299 mul
color_packet 1 get 0.587 mul add
color_packet 2 get 0.114 mul add
cvi
/gray_packet exch def
compression 0 eq
{
/number_pixels 1 def
}
{
currentfile byte readhexstring pop 0 get
/number_pixels exch 1 add def
} ifelse
0 1 number_pixels 1 sub
{
pixels exch gray_packet put
} for
pixels 0 number_pixels getinterval
} bind def
/PseudoClassPacket
{
%
% Get a PseudoClass packet.
%
% Parameters:
% index: index into the colormap.
% length: number of pixels minus one of this color (optional).
%
currentfile byte readhexstring pop 0 get
/offset exch 3 mul def
/color_packet colormap offset 3 getinterval def
compression 0 eq
{
/number_pixels 3 def
}
{
currentfile byte readhexstring pop 0 get
/number_pixels exch 1 add 3 mul def
} ifelse
0 3 number_pixels 1 sub
{
pixels exch color_packet putinterval
} for
pixels 0 number_pixels getinterval
} bind def
/PseudoClassImage
{
%
% Display a PseudoClass image.
%
% Parameters:
% class: 0-PseudoClass or 1-Grayscale.
%
currentfile buffer readline pop
token pop /class exch def pop
class 0 gt
{
currentfile buffer readline pop
token pop /depth exch def pop
/grays columns 8 add depth sub depth mul 8 idiv string def
columns rows depth
[
columns 0 0
rows neg 0 rows
]
{ currentfile grays readhexstring pop } image
}
{
%
% Parameters:
% colors: number of colors in the colormap.
% colormap: red, green, blue color packets.
%
currentfile buffer readline pop
token pop /colors exch def pop
/colors colors 3 mul def
/colormap colors string def
currentfile colormap readhexstring pop pop
systemdict /colorimage known
{
columns rows 8
[
columns 0 0
rows neg 0 rows
]
{ PseudoClassPacket } false 3 colorimage
}
{
%
% No colorimage operator; convert to grayscale.
%
columns rows 8
[
columns 0 0
rows neg 0 rows
]
{ GrayPseudoClassPacket } image
} ifelse
} ifelse
} bind def
/DisplayImage
{
%
% Display a DirectClass or PseudoClass image.
%
% Parameters:
% x & y translation.
% x & y scale.
% label pointsize.
% image label.
% image columns & rows.
% class: 0-DirectClass or 1-PseudoClass.
% compression: 0-none or 1-RunlengthEncoded.
% hex color packets.
%
gsave
/buffer 512 string def
/byte 1 string def
/color_packet 3 string def
/pixels 768 string def
currentfile buffer readline pop
token pop /x exch def
token pop /y exch def pop
x y translate
currentfile buffer readline pop
token pop /x exch def
token pop /y exch def pop
currentfile buffer readline pop
token pop /pointsize exch def pop
x y scale
currentfile buffer readline pop
token pop /columns exch def
token pop /rows exch def pop
currentfile buffer readline pop
token pop /class exch def pop
currentfile buffer readline pop
token pop /compression exch def pop
class 0 gt { PseudoClassImage } { DirectClassImage } ifelse
grestore
showpage
} bind def
%%EndProlog
%%Page: 1 1
%%PageBoundingBox: 428 345 508 391
DisplayImage
428 345
80 46
12
80 46
1
1
1
8
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
1C1C1C1C1C1C1C1C
%%PageTrailer
%%Trailer
%%EOF

31
test.html Normal file
View File

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<link rel="stylesheet" href="./client/style.css" />
</head>
<body>
<div id="contact-mailer-message"></div>
<form action="http://localhost:8080/submit" method="POST" id="contact-mailer-form">
<input type="hidden" name="token" value="s0y6WANzU1XnYERoJxMwekP9pqilSVLK5Gbf3hmZadHB2rQ4u8" />
<div>
<label for="nom">Votre nom&nbsp;:</label>
<input type="text" name="nom" required="required"/>
</div>
<div>
<label for="mail">Adresse mail&nbsp;:</label>
<input type="email" name="mail" required="required"/>
</div>
<div>
<label for="objet">Objet&nbsp;:</label>
<input type="text" name="objet" />
</div>
<div>
<label for="objet">Votre message&nbsp;:</label>
<textarea name="message" required="required"></textarea>
</div>
<input type="submit" />
</form>
<script src="./client/index.js"></script>
</body>
</html>

View File

@ -2,42 +2,24 @@ version: '3'
services:
db:
image: mongo
mailer:
build: ../server
build: ..
volumes:
- ../server/main.py:/usr/src/app/main.py
- ../main.py:/usr/src/app/main.py
- ./uwsgi:/tmp/uwsgi
depends_on:
- db
- spamassassin
environment:
MONGODB_HOST: db
SMTP_SERVER_ADDRESS: toto.mail
SMTP_SERVER_ADDRESS: 'lol'
SMTP_SERVER_PORT: 994
SMTP_SERVER_USERNAME: toto@toto.mail
SMTP_SERVER_PASSWORD: password
SMTP_SERVER_SENDER: toto@toto.mail
ADMIN_PASSWORD: test
SMTP_SERVER_USERNAME: toto
SMTP_SERVER_PASSWORD: lol
SMTP_SERVER_SENDER: moi
ADMIN_PASSWORD: admin
SMTP_SSL: 'true'
UID: 101
MOUNT: /api
do_not_send: 'true'
proxy:
image: nginx
ports:
- 8080:8080
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- ../:/usr/app
- ./uwsgi:/tmp/uwsgi
environment:
nginx_uid: 1000
depends_on:
- mailer
spamassassin:
image: dinkel/spamassassin
restart: unless-stopped

View File

@ -1,39 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<link rel="stylesheet" href="./client/style.css" />
</head>
<body>
<div id="contact-mailer-message"></div>
<form action="/api/submit" method="POST" id="contact-mailer-form">
<noscript>Les protections anti-spam, nécéssitent lutilisation de javascript. Rien dintrusif normalement.</noscript>
<div>
<label for="token">Token&nbsp;:</label>
<input type="text" name="token"/>
</div>
<div>
<label for="nom">Votre nom&nbsp;:</label>
<input type="text" name="nom" required="required"/>
</div>
<div>
<label for="prenom">Votre prénom&nbsp;:</label>
<input type="text" name="prenom"/>
</div>
<div>
<label for="mail">Adresse mail&nbsp;:</label>
<input type="email" name="mail" required="required"/>
</div>
<div>
<label for="objet">Objet&nbsp;:</label>
<input type="text" name="objet" />
</div>
<div>
<label for="message">Votre message&nbsp;:</label>
<textarea name="message"></textarea>
</div>
<input type="submit" />
</form>
<script class="contact-form-config" form-id="contact-mailer-form" notify-theme="plain" src="../client/index.js"></script>
</body>
</html>

View File

@ -1,37 +0,0 @@
worker_processes auto;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 768;
# multi_accept on;
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
error_log stderr;
access_log /dev/stdout;
include /etc/nginx/mime.types;
default_type application/octet-stream;
types_hash_max_size 2048;
types_hash_bucket_size 128;
gzip on;
server {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, DELETE, OPTIONS';
listen 8080;
location / {
root /usr/app/;
index index.html;
}
location /api/ {
include uwsgi_params;
uwsgi_pass unix:/tmp/uwsgi/uwsgi.sock;
#uwsgi_param PATH_INFO "$1";
#uwsgi_param SCRIPT_NAME /;
}
}
}