Compare commits

...

27 Commits

Author SHA1 Message Date
Adrian Amaglio
e55c45e7fa discontinuation 2024-01-23 10:22:32 +01:00
7e646d2ae9 spamassassin test 2021-05-18 01:25:56 +02:00
f06d2e7219 spamassassin integration 2021-05-18 01:25:37 +02:00
b71d9e2ded update 2021-05-15 10:03:09 +02:00
77724afccd build script 2021-03-13 12:15:29 +01:00
6e821c6ee9 test env seems to work 2021-01-14 00:27:26 +01:00
416960ac6c setup local test env 2021-01-14 00:00:40 +01:00
5ebd46de77 :fix: test path 2020-12-26 16:42:08 +01:00
69dd13af4d :fix: typos 2020-12-26 16:32:36 +01:00
69fd92dc77 🔥 rm venv 2020-12-26 14:36:55 +01:00
d39405a7e4 File restructuration 2020-12-26 14:36:31 +01:00
ad4c187475 updated hash 2020-12-26 14:07:03 +01:00
c947acf8bb administration tool 2020-12-26 14:03:56 +01:00
22a21da722 nginx test conf file 2020-12-26 14:03:36 +01:00
be61601950 Refactoring. Client has no global names anymore, and no inline scripts 2020-12-26 14:03:13 +01:00
5b98424bc6 Removed cors headers 2020-12-26 13:17:46 +01:00
f43d5a93de test env setup 2020-12-25 13:02:57 +01:00
e2465e2874 fix field control 2020-10-08 20:17:48 +02:00
8f88cd6d2c tested timer field 2020-09-17 16:19:39 +02:00
e66ac2e8bd First work on timer field against bots 2020-09-17 16:08:46 +02:00
967b4bf4f5 removed dependancy to old template 2020-09-16 15:15:10 +02:00
48333e8040 Date header added 2020-09-16 15:14:26 +02:00
4fe3ce5652 All responses are now json. Added a honeypot field 2020-09-16 14:49:15 +02:00
3e4e3854d4 getunicode returns None time to time. Catched that. 2020-09-16 11:19:31 +02:00
cfb0b52ec2 fixed z-index issue 2020-08-27 16:42:37 +02:00
d570d5b865 update 2020-08-27 16:38:04 +02:00
0535151b65 Update style.css 2020-08-27 16:33:06 +02:00
22 changed files with 12935 additions and 157 deletions

81
adminer/index.html Normal file
View File

@ -0,0 +1,81 @@
<!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>

118
adminer/index.js Normal file
View File

@ -0,0 +1,118 @@
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)
})
},
},
})

11965
adminer/vue.js Normal file

File diff suppressed because it is too large Load Diff

1
client/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
dist

View File

@ -1,3 +1,6 @@
/* Executed after page loading */
(function () {
class JeanCloudContactFormNotifier { class JeanCloudContactFormNotifier {
constructor (theme, messageContainer) { constructor (theme, messageContainer) {
/* Choose the theme */ /* Choose the theme */
@ -74,6 +77,14 @@ function jeanCloudContactFormIntercept (formId, notifier) {
loadingText.classList.add("contact-mailer-sending"); loadingText.classList.add("contact-mailer-sending");
loadingText.textContent = 'Envoi en cours…' loadingText.textContent = 'Envoi en cours…'
submitButton.after(loadingText) 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 */ /* XHR */
fetch(formElem.action, { fetch(formElem.action, {
method: formElem.method, method: formElem.method,
@ -98,10 +109,12 @@ function jeanCloudContactFormIntercept (formId, notifier) {
loadingText.parentNode.removeChild(loadingText) loadingText.parentNode.removeChild(loadingText)
notifier.error('Impossible denvoyer le formulaire. Vérifiez votre connexion internet ou réessayez plus tard.') notifier.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)
} }
} }
(function () {
/* Get the current js file location */ /* Get the current js file location */
const path = (document.currentScript.src[-1] == '/' ? document.currentScript.src : document.currentScript.src.replace(/\/[^\/]*$/, '')) const path = (document.currentScript.src[-1] == '/' ? document.currentScript.src : document.currentScript.src.replace(/\/[^\/]*$/, ''))
@ -111,7 +124,18 @@ function jeanCloudContactFormIntercept (formId, notifier) {
link.rel = "stylesheet"; link.rel = "stylesheet";
link.crossOrigin = 'anonymous'; link.crossOrigin = 'anonymous';
link.href = path + "/style.css"; link.href = path + "/style.css";
link.integrity = 'sha384-WJutysMMrOQpV0KGvocEEo29oFWTmU2iw0ZSgPqi53R8YElWq3qnSLd2vRCXAhjz' link.integrity = 'sha384-D12RSMaIURTgZZljhdQqYlQzgEfXvOFwtiqzkWnNcDbKFwMWXcmsCRFO5BNii0MB'
// cat style.css | openssl dgst -sha384 -binary | openssl base64 -A // cat style.css | openssl dgst -sha384 -binary | openssl base64 -A
document.head.appendChild(link); 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()
})() })()

View File

@ -1,6 +1,6 @@
{ {
"name": "jean-cloud-contact-mailer-client", "name": "jean-cloud-contact-mailer-client",
"version": "1.0.0", "version": "1.1.6",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

View File

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

View File

@ -8,6 +8,7 @@
bottom: 0; bottom: 0;
width: 20%; width: 20%;
min-width: 320px; min-width: 320px;
z-index: 10;
} }
.contact-mailer-message { .contact-mailer-message {

View File

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

View File

@ -1,3 +1,6 @@
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 # Contact Mailer
A minimal python app to send mail when people fills in your contact form! A minimal python app to send mail when people fills in your contact form!
@ -59,9 +62,9 @@ The app needs a lot of env vars to run :
SMTP_SERVER_ADDRESS=mail.gandi.net SMTP_SERVER_ADDRESS=mail.gandi.net
SMTP_SERVER_PORT=465 SMTP_SERVER_PORT=465
SMTP_SSL=true SMTP_SSL=true
SMTP_SERVER_USERNAME=nepasrepondre@jean-cloud.org SMTP_SERVER_USERNAME=noreply@example.net
SMTP_SERVER_PASSWORD=B9UZtOnIlJcRzx8mh2jCsPTQujwTr9I6XyiA SMTP_SERVER_PASSWORD=bigpass
SMTP_SERVER_SENDER=nepasrepondre@jean-cloud.org SMTP_SERVER_SENDER=noreply@example.net
MONGODB_HOST=mongodb MONGODB_HOST=mongodb
ADMIN_PASSWORD=test ADMIN_PASSWORD=test
UID=1000 UID=1000
@ -81,13 +84,14 @@ plain or light theme.
## Roadmap ## Roadmap
### Near future ### Near future
- go on docker hub
- use a standart logger (used by bottle and uwsgi) to log error on mail fail - 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) - [unit tests](https://bottlepy.org/docs/dev/recipes.html#unit-testing-bottle-applications)
- add redirection urls to form config - add redirection urls to form config
- Include some [capcha](https://alternativeto.net/software/recaptcha/) support - Include some [capcha](https://alternativeto.net/software/recaptcha/) support
- Correctly escape html entities - Correctly escape html entities
- Sign mails with the server key
- Use a dedicated SMTP server
### Ameliorations ### Ameliorations
- Use real user/passwords accounts - Use real user/passwords accounts
- Créate a gui client - Create a gui client

View File

@ -4,13 +4,16 @@ WORKDIR /usr/src/app
COPY requirements.txt ./ COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
RUN apk add python3-dev build-base linux-headers pcre-dev RUN apk add python3-dev build-base linux-headers pcre-dev spamassassin
RUN pip install uwsgi RUN pip install uwsgi
#spamassassin_client
ENV UID=0 ENV UID=0
ENV MOUNT=/ ENV MOUNT=/
COPY ./main.py ./list.tpl ./ # Since the package maintainer doesnt merge PR
# https://github.com/petermat/spamassassin_client/pull/2
COPY ./main.py ./spamassassin/spamassasin_client.py ./
# I juste wanted to change the socket owner but it turned out I needed to change thu uwsgi user # 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 #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

13
server/Pipfile Executable file
View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,115 @@
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

@ -0,0 +1,383 @@
%!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

View File

@ -1,32 +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="https://mailer.jean-cloud.net/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>
<script> jeanCloudContactFormIntercept ('contact-mailer-form', new JeanCloudContactFormNotifier()) </script>
</body>
</html>

View File

@ -4,22 +4,40 @@ services:
image: mongo image: mongo
mailer: mailer:
build: .. build: ../server
volumes: volumes:
- ../main.py:/usr/src/app/main.py - ../server/main.py:/usr/src/app/main.py
- ./uwsgi:/tmp/uwsgi - ./uwsgi:/tmp/uwsgi
depends_on: depends_on:
- db - db
- spamassassin
environment: environment:
MONGODB_HOST: db MONGODB_HOST: db
SMTP_SERVER_ADDRESS: 'lol' SMTP_SERVER_ADDRESS: toto.mail
SMTP_SERVER_PORT: 994 SMTP_SERVER_PORT: 994
SMTP_SERVER_USERNAME: toto SMTP_SERVER_USERNAME: toto@toto.mail
SMTP_SERVER_PASSWORD: lol SMTP_SERVER_PASSWORD: password
SMTP_SERVER_SENDER: moi SMTP_SERVER_SENDER: toto@toto.mail
ADMIN_PASSWORD: admin ADMIN_PASSWORD: test
SMTP_SSL: 'true' SMTP_SSL: 'true'
UID: 101
MOUNT: /api
do_not_send: 'true'
proxy: proxy:
image: nginx image: nginx
ports: ports:
- 8080:8080 - 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

39
test/index.html Normal file
View File

@ -0,0 +1,39 @@
<!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>

37
test/nginx.conf Normal file
View File

@ -0,0 +1,37 @@
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 /;
}
}
}