Compare commits
25 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
e55c45e7fa | ||
7e646d2ae9 | |||
f06d2e7219 | |||
b71d9e2ded | |||
77724afccd | |||
6e821c6ee9 | |||
416960ac6c | |||
5ebd46de77 | |||
69dd13af4d | |||
69fd92dc77 | |||
d39405a7e4 | |||
ad4c187475 | |||
c947acf8bb | |||
22a21da722 | |||
be61601950 | |||
5b98424bc6 | |||
f43d5a93de | |||
e2465e2874 | |||
8f88cd6d2c | |||
e66ac2e8bd | |||
967b4bf4f5 | |||
48333e8040 | |||
4fe3ce5652 | |||
3e4e3854d4 | |||
cfb0b52ec2 |
81
adminer/index.html
Normal file
81
adminer/index.html
Normal 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
118
adminer/index.js
Normal 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
11965
adminer/vue.js
Normal file
File diff suppressed because it is too large
Load Diff
1
client/.gitignore
vendored
Normal file
1
client/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
dist
|
@ -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 d’envoyer le formulaire. Vérifiez votre connexion internet ou réessayez plus tard.')
|
notifier.error('Impossible d’envoyer 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-xiBc0umeH/V8hcvxoq+N7lcex1UYBINwjBXXbBh+MSRZveX+RAJts/bGXYzD6BhI'
|
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()
|
||||||
})()
|
})()
|
||||||
|
|
||||||
|
2
client/package-lock.json
generated
2
client/package-lock.json
generated
@ -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": {
|
||||||
|
@ -1,16 +1,18 @@
|
|||||||
{
|
{
|
||||||
"name": "jean-cloud-contact-mailer-client",
|
"name": "jean-cloud-contact-mailer-client",
|
||||||
"version": "1.1.4",
|
"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": {}
|
||||||
|
}
|
||||||
|
6
list.tpl
6
list.tpl
@ -1,6 +0,0 @@
|
|||||||
<h2>Liste</h2>
|
|
||||||
<ul>
|
|
||||||
% for item in data:
|
|
||||||
<li>{{item}}</li>
|
|
||||||
% end
|
|
||||||
</ul>
|
|
14
readme.md
14
readme.md
@ -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
|
||||||
|
@ -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
13
server/Pipfile
Executable 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"
|
@ -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
|
@ -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
|
||||||
@ -68,13 +69,16 @@ smtp_server_password = get_env('SMTP_SERVER_PASSWORD')
|
|||||||
smtp_server_sender = get_env('SMTP_SERVER_SENDER')
|
smtp_server_sender = get_env('SMTP_SERVER_SENDER')
|
||||||
|
|
||||||
# Get mongodb connection
|
# Get mongodb connection
|
||||||
mongodb_host = get_env('MONGODB_HOST')
|
mongodb_host = get_env('MONGODB_HOST')
|
||||||
mongodb_port = get_env('MONGODB_PORT', '27017')
|
mongodb_port = get_env('MONGODB_PORT', '27017')
|
||||||
mongodb_dbname = get_env('MONGODB_DBNAME', 'contact_mailer')
|
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 d’autentification est requis')
|
||||||
return 'Le jeton d’autentification 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 n’est pas accessible.')
|
||||||
return 'La base de donnée n’est 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 n’a 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 n’a 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 n’est 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 n’est 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 n’est 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 n’est pas accessible')
|
||||||
return 'La base de donnée n’est 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 n’est pas valide')
|
||||||
return 'Le token n’est 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 n’est pas accessible')
|
||||||
return 'La base de donnée n’est 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 n’est pas accessible')
|
||||||
return 'La base de donnée n’est 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 n’est pas accessible')
|
||||||
return 'La base de donnée n’est 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 'L’utilisateur existe déjà'
|
return resp(400, 'L’utilisateur 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 n’est pas accessible')
|
||||||
return 'La base de donnée n’est 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 n’est pas accessible')
|
||||||
return 'La base de donnée n’est 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, 'L’utilisateur n’existe pas')
|
||||||
return 'L’utilisateur n’existe pas'
|
|
||||||
except pymongo.errors.ServerSelectionTimeoutError as e:
|
except pymongo.errors.ServerSelectionTimeoutError as e:
|
||||||
response.status = 500
|
return resp(500, 'La base de donnée n’est pas accessible')
|
||||||
return 'La base de donnée n’est pas accessible'
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
##################################################### app startup ############################################
|
##################################################### app startup ############################################
|
||||||
|
prod_app = StripPathMiddleware(app)
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
bottle.run(app=StripPathMiddleware(app), host=listen_address, port=listen_port, debug=True)
|
bottle.run(app=prod_app, host=listen_address, port=listen_port, debug=True)
|
||||||
else:
|
|
||||||
prod_app = StripPathMiddleware(app)
|
|
2
server/spamassassin/readme
Normal file
2
server/spamassassin/readme
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
Since the package maintainer doesnt merge PR
|
||||||
|
https://github.com/petermat/spamassassin_client/pull/2
|
115
server/spamassassin/spamassasin_client.py
Normal file
115
server/spamassassin/spamassasin_client.py
Normal 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
|
||||||
|
|
||||||
|
|
||||||
|
|
383
server/spamassassin/spamassassin_client
Normal file
383
server/spamassassin/spamassassin_client
Normal 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
|
32
test.html
32
test.html
@ -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 :</label>
|
|
||||||
<input type="text" name="nom" required="required"/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="mail">Adresse mail :</label>
|
|
||||||
<input type="email" name="mail" required="required"/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="objet">Objet :</label>
|
|
||||||
<input type="text" name="objet" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label for="objet">Votre message :</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>
|
|
@ -2,24 +2,42 @@ version: '3'
|
|||||||
services:
|
services:
|
||||||
db:
|
db:
|
||||||
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
39
test/index.html
Normal 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 l’utilisation de javascript. Rien d’intrusif normalement.</noscript>
|
||||||
|
<div>
|
||||||
|
<label for="token">Token :</label>
|
||||||
|
<input type="text" name="token"/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="nom">Votre nom :</label>
|
||||||
|
<input type="text" name="nom" required="required"/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="prenom">Votre prénom :</label>
|
||||||
|
<input type="text" name="prenom"/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="mail">Adresse mail :</label>
|
||||||
|
<input type="email" name="mail" required="required"/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="objet">Objet :</label>
|
||||||
|
<input type="text" name="objet" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="message">Votre message :</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
37
test/nginx.conf
Normal 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 /;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user