Compare commits
No commits in common. "master" and "v1.1.1" have entirely different histories.
@ -4,16 +4,13 @@ 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 spamassassin
|
RUN apk add python3-dev build-base linux-headers pcre-dev
|
||||||
RUN pip install uwsgi
|
RUN pip install uwsgi
|
||||||
#spamassassin_client
|
|
||||||
|
|
||||||
ENV UID=0
|
ENV UID=0
|
||||||
ENV MOUNT=/
|
ENV MOUNT=/
|
||||||
|
|
||||||
# Since the package maintainer doesnt merge PR
|
COPY ./main.py ./list.tpl ./
|
||||||
# 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
|
@ -1,81 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<link rel="stylesheet" src="style.css" />
|
|
||||||
<title>Contact mailer admin interface</title>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<main id="app">
|
|
||||||
<section>
|
|
||||||
<h3>Athentification</h3>
|
|
||||||
<div v-if="!loggedin" class="loginform">
|
|
||||||
<form v-on:submit.prevent="login">
|
|
||||||
<select v-model="type">
|
|
||||||
<option value="token">Utilisateur</option>
|
|
||||||
<option value="admin_pass">Administrateur</option>
|
|
||||||
</select>
|
|
||||||
<input type="password" v-model="password" />
|
|
||||||
<input type="submit" value="connect" />
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div v-else="">
|
|
||||||
<p>Connecté en tant que {{ type }}</p>
|
|
||||||
<button v-on:click="logout">Se déconnecter</button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<div v-if="loggedin && type=='admin_pass'">
|
|
||||||
<h3>Utilisateurices</h3>
|
|
||||||
<form v-on:submit.prevent="addUser">
|
|
||||||
<input type="text" v-model="newUser" />
|
|
||||||
<input type="submit" />
|
|
||||||
</form>
|
|
||||||
<button v-on:click="getUsers">Rafraichir les utilisateurs</button>
|
|
||||||
<ul>
|
|
||||||
<li v-for="user in users">{{user.token}} — {{user.username}}</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<h3>Formulaires</h3>
|
|
||||||
<button v-on:click="getForms">Rafraichir les formulaires</button>
|
|
||||||
<ul>
|
|
||||||
<li v-for="form in forms">
|
|
||||||
<div>À {{form.mail}}</div>
|
|
||||||
<div>Objet {{form.subject}}</div>
|
|
||||||
<div>{{form.content}}</div>
|
|
||||||
<div>{{form.token}} — {{form.honeypotfield}} — {{form.timerdelay}}</div>
|
|
||||||
<button v-on:click="deleteForm(form.token)">Supprimer</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<div v-if="page=='new_user'">
|
|
||||||
<form v-on:submit.prevent="addForm">
|
|
||||||
<label for="mail">Mail :</label>
|
|
||||||
<input v-model="newForm.mail" type="text" name="mail" id="mail" />
|
|
||||||
<br />
|
|
||||||
<label for="content">Contenu :</label>
|
|
||||||
<textarea v-model="newForm.content" name="content" id="content">
|
|
||||||
</textarea>
|
|
||||||
<br />
|
|
||||||
<label for="subject">Objet :</label>
|
|
||||||
<input v-model="newForm.subject" type="text" name="subject" id="subject" />
|
|
||||||
<br />
|
|
||||||
<label for="honeypot">Honeypot (ne pas toucher) :</label>
|
|
||||||
<input v-model="newForm.honeypotfield" type="text" name="honeypot" id="honeypot" />
|
|
||||||
<br />
|
|
||||||
<label for="timerdelay">Timer delay :</label>
|
|
||||||
<input v-model="newForm.timerdelay" type="number" name="timerdelay" id="timerdelay" />
|
|
||||||
<br />
|
|
||||||
<input type="submit" />
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
<script src="./vue.js"></script>
|
|
||||||
<script src="./index.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
118
adminer/index.js
118
adminer/index.js
@ -1,118 +0,0 @@
|
|||||||
var app = new Vue({
|
|
||||||
el: '#app',
|
|
||||||
data: {
|
|
||||||
type: 'admin_pass', /* admin_pass or token */
|
|
||||||
password: 'test',
|
|
||||||
loggedin: false,
|
|
||||||
mailerHost: 'https://mailer.jean-cloud.net',
|
|
||||||
//mailerHost: 'http://localhost:8080',
|
|
||||||
//mailerHost: '/api',
|
|
||||||
forms: [],
|
|
||||||
users: [],
|
|
||||||
newUser: '',
|
|
||||||
page:'new_user',
|
|
||||||
newForm: {
|
|
||||||
'content': '{{message}}',
|
|
||||||
'subject': '[contact jean-cloud.net] {{nom|annonyme}} — {{objet}}',
|
|
||||||
'mail': 'contact@jean-cloud.org',
|
|
||||||
'honeypotfield': 'prenom',
|
|
||||||
'timerdelay': 5,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
login: function () {
|
|
||||||
if (!this.type) {
|
|
||||||
console.log('missing type')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!this.password) {
|
|
||||||
console.log('missing password')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.loggedin = true
|
|
||||||
this.getForms()
|
|
||||||
if ( this.type == 'admin_pass' )
|
|
||||||
this.getUsers()
|
|
||||||
},
|
|
||||||
logout: function () {
|
|
||||||
this.type = 'token'
|
|
||||||
this.password = null
|
|
||||||
this.loggedin = false
|
|
||||||
},
|
|
||||||
getForms: function () {
|
|
||||||
fetch(this.mailerHost + '/form/list', {
|
|
||||||
method: 'POST',
|
|
||||||
body: this.type + '=' + this.password
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
this.forms = data.data
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error(error)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
getUsers: function () {
|
|
||||||
fetch(this.mailerHost + '/user/list', {
|
|
||||||
method: 'POST',
|
|
||||||
body: this.type + '=' + this.password
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
this.users = data.data
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error(error)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
addForm: function () {
|
|
||||||
fetch(this.mailerHost + '/form', {
|
|
||||||
method: 'post',
|
|
||||||
body: this.type + '=' + this.password
|
|
||||||
+ '&subject=' + this.newForm.subject
|
|
||||||
+ '&mail=' + this.newForm.mail
|
|
||||||
+ '&content=' + this.newForm.content
|
|
||||||
+ '&honeypotfield=' + this.newForm.honeypotfield
|
|
||||||
+ '&timerdelay=' + this.newForm.timerdelay
|
|
||||||
})
|
|
||||||
.then(data => {
|
|
||||||
console.log(data)
|
|
||||||
this.getForms()
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.log(error)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
addUser: function () {
|
|
||||||
if (!this.newUser) {
|
|
||||||
console.log('need username')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fetch(this.mailerHost + '/user/' + this.newUser, {
|
|
||||||
method: 'put',
|
|
||||||
body: this.type + '=' + this.password
|
|
||||||
})
|
|
||||||
.then(data => {
|
|
||||||
this.newUser = ''
|
|
||||||
this.getUsers()
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error(error)
|
|
||||||
})
|
|
||||||
|
|
||||||
},
|
|
||||||
deleteForm: function (formId) {
|
|
||||||
fetch(this.mailerHost + '/form/' + formId, {
|
|
||||||
method: 'delete',
|
|
||||||
body: this.type + '=' + this.password
|
|
||||||
})
|
|
||||||
.then(data => {
|
|
||||||
console.log(data)
|
|
||||||
this.getForms()
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error(error)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
11965
adminer/vue.js
11965
adminer/vue.js
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,5 @@
|
|||||||
set -e
|
set -e
|
||||||
version=2.1.0
|
version=2.0.1
|
||||||
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
|
1
client/.gitignore
vendored
1
client/.gitignore
vendored
@ -1 +0,0 @@
|
|||||||
dist
|
|
@ -1,6 +1,3 @@
|
|||||||
/* Executed after page loading */
|
|
||||||
(function () {
|
|
||||||
|
|
||||||
class JeanCloudContactFormNotifier {
|
class JeanCloudContactFormNotifier {
|
||||||
constructor (theme, messageContainer) {
|
constructor (theme, messageContainer) {
|
||||||
/* Choose the theme */
|
/* Choose the theme */
|
||||||
@ -61,13 +58,10 @@ function jeanCloudContactFormIntercept (formId, notifier) {
|
|||||||
*/
|
*/
|
||||||
const formElem = document.getElementById(formId)
|
const formElem = document.getElementById(formId)
|
||||||
if (!formElem) {
|
if (!formElem) {
|
||||||
console.error('You tried to intercept form with id:"' + formId + '" but it was not found.')
|
console.error('You tried to intercept form id:"' + formId + '" but it was not found.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!notifier)
|
|
||||||
console.log('No notifier given, no message can be displayed')
|
|
||||||
|
|
||||||
/* Intercept the submit event */
|
/* Intercept the submit event */
|
||||||
formElem.onsubmit = async (e) => {
|
formElem.onsubmit = async (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@ -77,14 +71,6 @@ 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,
|
||||||
@ -105,16 +91,13 @@ function jeanCloudContactFormIntercept (formId, notifier) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error(error)
|
|
||||||
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(/\/[^\/]*$/, ''))
|
||||||
|
|
||||||
@ -124,18 +107,7 @@ 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-D12RSMaIURTgZZljhdQqYlQzgEfXvOFwtiqzkWnNcDbKFwMWXcmsCRFO5BNii0MB'
|
link.integrity = 'sha384-8PWvFCRowSxssUyiGirvpq/Nh6TTzYrsbAmpC0cw/OUKkZibNdI5L1gFiHxfrTZT'
|
||||||
// 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.1.6",
|
"version": "1.0.0",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -1,18 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "jean-cloud-contact-mailer-client",
|
"name": "jean-cloud-contact-mailer-client",
|
||||||
"version": "1.1.6",
|
"version": "1.0.16",
|
||||||
"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": {}
|
|
||||||
}
|
}
|
@ -8,7 +8,6 @@
|
|||||||
bottom: 0;
|
bottom: 0;
|
||||||
width: 20%;
|
width: 20%;
|
||||||
min-width: 320px;
|
min-width: 320px;
|
||||||
z-index: 10;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.contact-mailer-message {
|
.contact-mailer-message {
|
||||||
@ -60,6 +59,5 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
box-sizing: unset;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
6
list.tpl
Normal file
6
list.tpl
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<h2>Liste</h2>
|
||||||
|
<ul>
|
||||||
|
% for item in data:
|
||||||
|
<li>{{item}}</li>
|
||||||
|
% end
|
||||||
|
</ul>
|
@ -4,7 +4,6 @@ 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
|
||||||
@ -12,9 +11,7 @@ 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
|
||||||
from bson.json_util import dumps
|
import datetime # to name unsent mails
|
||||||
import datetime # For email date
|
|
||||||
from spamassasin_client import SpamAssassin
|
|
||||||
|
|
||||||
|
|
||||||
##################################################### Bottle stuff ############################################
|
##################################################### Bottle stuff ############################################
|
||||||
@ -29,8 +26,10 @@ 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
|
||||||
@ -76,9 +75,6 @@ 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':
|
||||||
@ -89,7 +85,6 @@ 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 ############################################
|
||||||
@ -100,7 +95,8 @@ def submission ():
|
|||||||
if 'token' in request.forms:
|
if 'token' in request.forms:
|
||||||
token = request.forms.getunicode('token')
|
token = request.forms.getunicode('token')
|
||||||
else:
|
else:
|
||||||
return resp(400, 'Le jeton d’autentification est requis')
|
response.status = 400
|
||||||
|
return 'Le jeton d’autentification est requis'
|
||||||
|
|
||||||
# Getting mail address
|
# Getting mail address
|
||||||
if 'mail' in request.forms:
|
if 'mail' in request.forms:
|
||||||
@ -113,52 +109,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:
|
||||||
return resp(400, 'Le formulaire demandé est introuvable, merci de vérifier que le token utilisé est le bon')
|
response.status = 400
|
||||||
|
return 'Le formulaire demandé est introuvable, merci de vérifier que le token utilisé est le bon'
|
||||||
except pymongo.errors.ServerSelectionTimeoutError as e:
|
except pymongo.errors.ServerSelectionTimeoutError as e:
|
||||||
return resp(500, 'La base de donnée n’est pas accessible.')
|
response.status = 500
|
||||||
|
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:
|
||||||
return resp(400, str(e))
|
response.status = 404
|
||||||
|
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:
|
||||||
msg = build_mail(from_address, form['mail'], subject, content)
|
if not send_mail(from_address, form['mail'], subject, content):
|
||||||
if is_spam(msg):
|
response.status = 500
|
||||||
return resp(400, 'Votre message semble être du spam !')
|
return 'Le mail n’a pas pu être envoyé.'
|
||||||
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é. Merci de réessayer plus tard.'
|
error = 'Le mail a été refusé. Votre message a été enregistré, il sera remis manuellement à son destinataire.'
|
||||||
except smtplib.SMTPRecipientsRefused as e:
|
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. Merci de réessayer plus tard'
|
error = 'Impossible de trouver le destinataire du mail. Votre message a été enregistré, il sera remis manuellement à son destinataire.'
|
||||||
except Exception as e:
|
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 resp(200, 'Mail envoyé !')
|
return '<p>Mail envoyé !</p>' + ('<p>Retour au <a href="{}">formulaire de contact</a></p>'.format(origin) if origin else '')
|
||||||
|
|
||||||
##################################################### Helpers ############################################
|
##################################################### Helpers ############################################
|
||||||
|
def save_mail (token, to, from_address, subject, content):
|
||||||
def resp (status, msg, data='{}'):
|
with open('unsent/unsent_{}_{}_{}.txt'.format(str(datetime.datetime.now()), token, to), 'w') as f:
|
||||||
response.status = status
|
f.write("Unsent mail\nSubject: {}\nFrom: {}Content:\n{}".format(
|
||||||
return '{{"status": "{}", "msg": "{}", "data": {}}}'.format(status, msg, data)
|
subject,
|
||||||
|
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. """
|
||||||
@ -171,40 +167,19 @@ def fill_fields(request, fields):
|
|||||||
"""Look for fields in request and fill fields dict with values or let default ones. If the value is required, throw exception."""
|
"""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)
|
||||||
if fields[field] is None: # if unicode failed
|
elif fields[field] == None:
|
||||||
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 build_mail(from_address, to, subject, content):
|
def send_mail(from_address, to, subject, content):
|
||||||
msg = EmailMessage()
|
"""Actually connect to smtp server, build a message object and send it as a mail"""
|
||||||
|
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['Date'] = datetime.datetime.now()
|
msg.attach(MIMEText(content, 'plain', "utf-8"))
|
||||||
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':
|
||||||
@ -231,7 +206,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, '_id':'-1'}
|
return {'_privilege':0}
|
||||||
if 'token' in request.forms:
|
if 'token' in request.forms:
|
||||||
token = request.forms.getunicode('token')
|
token = request.forms.getunicode('token')
|
||||||
try:
|
try:
|
||||||
@ -240,9 +215,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 {'_error': True} # anonymous
|
return 'La base de donnée n’est pas accessible'
|
||||||
|
|
||||||
return {'_privilege': 1000} # anonymous
|
return {'_privilege': 1000} # anonymous
|
||||||
|
|
||||||
@ -257,45 +232,43 @@ def create_form ():
|
|||||||
elif mail_default_subject != '':
|
elif mail_default_subject != '':
|
||||||
subject = mail_default_subject
|
subject = mail_default_subject
|
||||||
else:
|
else:
|
||||||
return resp(400, 'Le champs « sujet » est requis')
|
response.status = 400
|
||||||
|
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:
|
||||||
return resp(400, 'Le champs « contenu » est requis')
|
response.status = 400
|
||||||
|
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:
|
||||||
return resp(400, 'Le champs « adresse » est requis')
|
response.status = 400
|
||||||
|
return 'Le champs « adresse » est requis'
|
||||||
|
|
||||||
user = login(request)
|
user = login(request)
|
||||||
if user['_privilege'] > 1:
|
if user['_privilege'] > 1:
|
||||||
return resp(400, 'Privilèges insufisants')
|
response.status = 400
|
||||||
|
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:
|
||||||
newEntry = {
|
inserted = mongodb_database['forms'].insert_one({
|
||||||
'mail': mail,
|
'mail': mail,
|
||||||
'content': content,
|
'content': content,
|
||||||
'subject': subject,
|
'subject': subject,
|
||||||
'user_id': user['_id'],
|
'user_id': user['_id'],
|
||||||
'token': token,
|
'token': token,
|
||||||
}
|
})
|
||||||
if 'honeypotfield' in request.forms:
|
|
||||||
newEntry['honeypotfield'] = request.forms.getunicode('honeypotfield')
|
|
||||||
if 'timerdelay' in request.forms:
|
|
||||||
newEntry['timerdelay'] = request.forms.getunicode('timerdelay')
|
|
||||||
|
|
||||||
inserted = mongodb_database['forms'].insert_one(newEntry)
|
|
||||||
except pymongo.errors.ServerSelectionTimeoutError as e:
|
except pymongo.errors.ServerSelectionTimeoutError as e:
|
||||||
return resp(500, 'La base de donnée n’est pas accessible')
|
response.status = 500
|
||||||
|
return 'La base de donnée n’est pas accessible'
|
||||||
|
|
||||||
return resp(200, 'Créé : ' + token)
|
return 'Créé : ' + token
|
||||||
|
|
||||||
@app.post('/form/list')
|
@app.post('/form/list')
|
||||||
def list_forms ():
|
def list_forms ():
|
||||||
@ -306,28 +279,33 @@ def list_forms ():
|
|||||||
elif user['_privilege'] == 1:
|
elif user['_privilege'] == 1:
|
||||||
filt = {'user_id': user['_id']}
|
filt = {'user_id': user['_id']}
|
||||||
else:
|
else:
|
||||||
return resp(400, 'Privilèges insufisants')
|
response.status = 400
|
||||||
|
return 'Privilèges insufisants'
|
||||||
data = mongodb_database['forms'].find(filt)
|
data = mongodb_database['forms'].find(filt)
|
||||||
return resp(200,'', dumps(list(data)))
|
return bottle.template("list.tpl", data=data)
|
||||||
except pymongo.errors.ServerSelectionTimeoutError as e:
|
except pymongo.errors.ServerSelectionTimeoutError as e:
|
||||||
return resp(500,'La base de donnée n’est pas accessible')
|
response.status = 500
|
||||||
|
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):
|
||||||
# TODO If admin or form owner
|
# If admin or form owner
|
||||||
user = login(request)
|
user = login(request)
|
||||||
if user['_privilege'] > 1:
|
if user['_privilege'] > 1:
|
||||||
return resp(400, 'Privilèges insufisants')
|
response.status = 400
|
||||||
|
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:
|
||||||
return resp(400, 'Le token n’est pas valide')
|
response.status = 400
|
||||||
|
return 'Le token n’est pas valide'
|
||||||
except pymongo.errors.ServerSelectionTimeoutError as e:
|
except pymongo.errors.ServerSelectionTimeoutError as e:
|
||||||
return resp(500, 'La base de donnée n’est pas accessible')
|
response.status = 500
|
||||||
|
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:
|
||||||
@ -335,9 +313,11 @@ def delete_form(token):
|
|||||||
'token': token,
|
'token': token,
|
||||||
})
|
})
|
||||||
except pymongo.errors.ServerSelectionTimeoutError as e:
|
except pymongo.errors.ServerSelectionTimeoutError as e:
|
||||||
return resp(500, 'La base de donnée n’est pas accessible')
|
response.status = 500
|
||||||
return resp(200, 'Supprimé ' + token)
|
return 'La base de donnée n’est pas accessible'
|
||||||
return resp(400, 'Privilèges insufisants')
|
return 'Supprimé ' + token
|
||||||
|
response.status = 400
|
||||||
|
return 'Privilèges insufisants'
|
||||||
|
|
||||||
|
|
||||||
##################################################### Users ############################################
|
##################################################### Users ############################################
|
||||||
@ -346,53 +326,63 @@ def delete_form(token):
|
|||||||
def list_users ():
|
def list_users ():
|
||||||
user = login(request)
|
user = login(request)
|
||||||
if user['_privilege'] > 0:
|
if user['_privilege'] > 0:
|
||||||
return resp(400, 'Privilèges insufisants')
|
response.status = 400
|
||||||
|
return 'Privilèges insufisants'
|
||||||
try:
|
try:
|
||||||
data = mongodb_database['users'].find()
|
data = mongodb_database['users'].find()
|
||||||
return resp(200, '', dumps(list(data)))
|
return bottle.template("list.tpl", data=data)
|
||||||
except pymongo.errors.ServerSelectionTimeoutError as e:
|
except pymongo.errors.ServerSelectionTimeoutError as e:
|
||||||
return resp(500, 'La base de donnée n’est pas accessible')
|
response.status = 500
|
||||||
|
return 'La base de donnée n’est pas accessible'
|
||||||
|
|
||||||
|
|
||||||
@app.route('/user/<username>', method=['OPTIONS', 'PUT'])
|
@app.put('/user/<username>')
|
||||||
def create_user (username):
|
def create_user (username):
|
||||||
user = login(request)
|
user = login(request)
|
||||||
if user['_privilege'] > 0:
|
if user['_privilege'] > 0:
|
||||||
return resp(400, 'Privilèges insufisants')
|
response.status = 400
|
||||||
|
return 'Privilèges insufisants'
|
||||||
try:
|
try:
|
||||||
mongodb_database['users'].find({'username': username})[0]
|
mongodb_database['users'].find({'username': username})[0]
|
||||||
return resp(400, 'L’utilisateur existe déjà')
|
return '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 resp(200, 'Créé : ' + username)
|
return 'Créé : ' + username
|
||||||
except pymongo.errors.ServerSelectionTimeoutError as e:
|
except pymongo.errors.ServerSelectionTimeoutError as e:
|
||||||
return resp(500, 'La base de donnée n’est pas accessible')
|
response.status = 500
|
||||||
|
return 'La base de donnée n’est pas accessible'
|
||||||
except pymongo.errors.ServerSelectionTimeoutError as e:
|
except pymongo.errors.ServerSelectionTimeoutError as e:
|
||||||
return resp(500,'La base de donnée n’est pas accessible')
|
response.status = 500
|
||||||
|
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:
|
||||||
return resp(400, 'Privilèges insufisants')
|
response.status = 400
|
||||||
|
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 resp(200, 'Supprimé ' + username)
|
return 'Supprimé ' + username
|
||||||
except IndexError as e:
|
except IndexError as e:
|
||||||
return resp(400, 'L’utilisateur n’existe pas')
|
response.status = 400
|
||||||
|
return 'L’utilisateur n’existe pas'
|
||||||
except pymongo.errors.ServerSelectionTimeoutError as e:
|
except pymongo.errors.ServerSelectionTimeoutError as e:
|
||||||
return resp(500, 'La base de donnée n’est pas accessible')
|
response.status = 500
|
||||||
|
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=prod_app, host=listen_address, port=listen_port, debug=True)
|
bottle.run(app=StripPathMiddleware(app), host=listen_address, port=listen_port, debug=True)
|
||||||
|
else:
|
||||||
|
prod_app = StripPathMiddleware(app)
|
14
readme.md
14
readme.md
@ -1,6 +1,3 @@
|
|||||||
THIS REPO IS DISCONTINUED
|
|
||||||
- Too much spam through our smtp server
|
|
||||||
- It is simpler and more friendly to juste put your mail address on your website
|
|
||||||
# Contact Mailer
|
# 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!
|
||||||
|
|
||||||
@ -62,9 +59,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=noreply@example.net
|
SMTP_SERVER_USERNAME=nepasrepondre@jean-cloud.org
|
||||||
SMTP_SERVER_PASSWORD=bigpass
|
SMTP_SERVER_PASSWORD=B9UZtOnIlJcRzx8mh2jCsPTQujwTr9I6XyiA
|
||||||
SMTP_SERVER_SENDER=noreply@example.net
|
SMTP_SERVER_SENDER=nepasrepondre@jean-cloud.org
|
||||||
MONGODB_HOST=mongodb
|
MONGODB_HOST=mongodb
|
||||||
ADMIN_PASSWORD=test
|
ADMIN_PASSWORD=test
|
||||||
UID=1000
|
UID=1000
|
||||||
@ -84,14 +81,13 @@ 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
|
||||||
- Create a gui client
|
- Créate a gui client
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
[[source]]
|
|
||||||
name = "pypi"
|
|
||||||
url = "https://pypi.org/simple"
|
|
||||||
verify_ssl = true
|
|
||||||
|
|
||||||
[dev-packages]
|
|
||||||
|
|
||||||
[packages]
|
|
||||||
email = "*"
|
|
||||||
bottle = "*"
|
|
||||||
|
|
||||||
[requires]
|
|
||||||
python_version = "3.6"
|
|
@ -1,2 +0,0 @@
|
|||||||
Since the package maintainer doesnt merge PR
|
|
||||||
https://github.com/petermat/spamassassin_client/pull/2
|
|
@ -1,115 +0,0 @@
|
|||||||
import socket, select, re, logging
|
|
||||||
from io import BytesIO
|
|
||||||
|
|
||||||
divider_pattern = re.compile(br'^(.*?)\r?\n(.*?)\r?\n\r?\n', re.DOTALL)
|
|
||||||
first_line_pattern = re.compile(br'^SPAMD/[^ ]+ 0 EX_OK$')
|
|
||||||
|
|
||||||
|
|
||||||
class SpamAssassin(object):
|
|
||||||
def __init__(self, message, host='127.0.0.1', port=783, timeout=20):
|
|
||||||
self.score = None
|
|
||||||
self.symbols = None
|
|
||||||
|
|
||||||
# Connecting
|
|
||||||
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
||||||
client.settimeout(timeout)
|
|
||||||
client.connect((host, port))
|
|
||||||
|
|
||||||
# Sending
|
|
||||||
client.sendall(self._build_message(message))
|
|
||||||
client.shutdown(socket.SHUT_WR)
|
|
||||||
|
|
||||||
# Reading
|
|
||||||
resfp = BytesIO()
|
|
||||||
while True:
|
|
||||||
ready = select.select([client], [], [], timeout)
|
|
||||||
if ready[0] is None:
|
|
||||||
# Kill with Timeout!
|
|
||||||
logging.info('[SpamAssassin] - Timeout ({0}s)!'.format(str(timeout)))
|
|
||||||
break
|
|
||||||
|
|
||||||
data = client.recv(4096)
|
|
||||||
if data == b'':
|
|
||||||
break
|
|
||||||
|
|
||||||
resfp.write(data)
|
|
||||||
|
|
||||||
# Closing
|
|
||||||
client.close()
|
|
||||||
client = None
|
|
||||||
|
|
||||||
self._parse_response(resfp.getvalue())
|
|
||||||
|
|
||||||
def _build_message(self, message):
|
|
||||||
reqfp = BytesIO()
|
|
||||||
data_len = str(len(message)).encode()
|
|
||||||
reqfp.write(b'REPORT SPAMC/1.2\r\n')
|
|
||||||
reqfp.write(b'Content-Length: ' + data_len + b'\r\n')
|
|
||||||
reqfp.write(b'User: cx42\r\n\r\n')
|
|
||||||
reqfp.write(message)
|
|
||||||
return reqfp.getvalue()
|
|
||||||
|
|
||||||
def _parse_response(self, response):
|
|
||||||
if response == b'':
|
|
||||||
logging.info("[SPAM ASSASSIN] Empty response")
|
|
||||||
return None
|
|
||||||
|
|
||||||
match = divider_pattern.match(response)
|
|
||||||
if not match:
|
|
||||||
logging.error("[SPAM ASSASSIN] Response error:")
|
|
||||||
logging.error(response)
|
|
||||||
return None
|
|
||||||
|
|
||||||
first_line = match.group(1)
|
|
||||||
headers = match.group(2)
|
|
||||||
body = response[match.end(0):]
|
|
||||||
|
|
||||||
# Checking response is good
|
|
||||||
match = first_line_pattern.match(first_line)
|
|
||||||
if not match:
|
|
||||||
logging.error("[SPAM ASSASSIN] invalid response:")
|
|
||||||
logging.error(first_line)
|
|
||||||
return None
|
|
||||||
|
|
||||||
report_list = [s.strip() for s in body.decode('utf-8').strip().split('\n')]
|
|
||||||
linebreak_num = report_list.index([s for s in report_list if "---" in s][0])
|
|
||||||
tablelists = [s for s in report_list[linebreak_num + 1:]]
|
|
||||||
|
|
||||||
self.report_fulltext = '\n'.join(report_list)
|
|
||||||
|
|
||||||
|
|
||||||
# join line when current one is only wrap of previous
|
|
||||||
tablelists_temp = []
|
|
||||||
if tablelists:
|
|
||||||
for counter, tablelist in enumerate(tablelists):
|
|
||||||
if len(tablelist)>1:
|
|
||||||
if (tablelist[0].isnumeric() or tablelist[0] == '-') and (tablelist[1].isnumeric() or tablelist[1] == '.'):
|
|
||||||
tablelists_temp.append(tablelist)
|
|
||||||
else:
|
|
||||||
if tablelists_temp:
|
|
||||||
tablelists_temp[-1] += " " + tablelist
|
|
||||||
tablelists = tablelists_temp
|
|
||||||
|
|
||||||
# create final json
|
|
||||||
self.report_json = dict()
|
|
||||||
for tablelist in tablelists:
|
|
||||||
wordlist = re.split('\s+', tablelist)
|
|
||||||
self.report_json[wordlist[1]] = {'partscore': float(wordlist[0]), 'description': ' '.join(wordlist[1:])}
|
|
||||||
|
|
||||||
headers = headers.decode('utf-8').replace(' ', '').replace(':', ';').replace('/', ';').split(';')
|
|
||||||
self.score = float(headers[2])
|
|
||||||
|
|
||||||
def get_report_json(self):
|
|
||||||
return self.report_json
|
|
||||||
|
|
||||||
def get_score(self):
|
|
||||||
return self.score
|
|
||||||
|
|
||||||
def is_spam(self, level=5):
|
|
||||||
return self.score is None or self.score > level
|
|
||||||
|
|
||||||
def get_fulltext(self):
|
|
||||||
return self.report_fulltext
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,383 +0,0 @@
|
|||||||
%!PS-Adobe-3.0
|
|
||||||
%%Creator: (ImageMagick)
|
|
||||||
%%Title: (spamassassin_client)
|
|
||||||
%%CreationDate: (2021-05-17T23:08:26+00:00)
|
|
||||||
%%BoundingBox: 428 345 508 391
|
|
||||||
%%HiResBoundingBox: 428 345 508 391
|
|
||||||
%%DocumentData: Clean7Bit
|
|
||||||
%%LanguageLevel: 1
|
|
||||||
%%Orientation: Portrait
|
|
||||||
%%PageOrder: Ascend
|
|
||||||
%%Pages: 1
|
|
||||||
%%EndComments
|
|
||||||
|
|
||||||
%%BeginDefaults
|
|
||||||
%%EndDefaults
|
|
||||||
|
|
||||||
%%BeginProlog
|
|
||||||
%
|
|
||||||
% Display a color image. The image is displayed in color on
|
|
||||||
% Postscript viewers or printers that support color, otherwise
|
|
||||||
% it is displayed as grayscale.
|
|
||||||
%
|
|
||||||
/DirectClassPacket
|
|
||||||
{
|
|
||||||
%
|
|
||||||
% Get a DirectClass packet.
|
|
||||||
%
|
|
||||||
% Parameters:
|
|
||||||
% red.
|
|
||||||
% green.
|
|
||||||
% blue.
|
|
||||||
% length: number of pixels minus one of this color (optional).
|
|
||||||
%
|
|
||||||
currentfile color_packet readhexstring pop pop
|
|
||||||
compression 0 eq
|
|
||||||
{
|
|
||||||
/number_pixels 3 def
|
|
||||||
}
|
|
||||||
{
|
|
||||||
currentfile byte readhexstring pop 0 get
|
|
||||||
/number_pixels exch 1 add 3 mul def
|
|
||||||
} ifelse
|
|
||||||
0 3 number_pixels 1 sub
|
|
||||||
{
|
|
||||||
pixels exch color_packet putinterval
|
|
||||||
} for
|
|
||||||
pixels 0 number_pixels getinterval
|
|
||||||
} bind def
|
|
||||||
|
|
||||||
/DirectClassImage
|
|
||||||
{
|
|
||||||
%
|
|
||||||
% Display a DirectClass image.
|
|
||||||
%
|
|
||||||
systemdict /colorimage known
|
|
||||||
{
|
|
||||||
columns rows 8
|
|
||||||
[
|
|
||||||
columns 0 0
|
|
||||||
rows neg 0 rows
|
|
||||||
]
|
|
||||||
{ DirectClassPacket } false 3 colorimage
|
|
||||||
}
|
|
||||||
{
|
|
||||||
%
|
|
||||||
% No colorimage operator; convert to grayscale.
|
|
||||||
%
|
|
||||||
columns rows 8
|
|
||||||
[
|
|
||||||
columns 0 0
|
|
||||||
rows neg 0 rows
|
|
||||||
]
|
|
||||||
{ GrayDirectClassPacket } image
|
|
||||||
} ifelse
|
|
||||||
} bind def
|
|
||||||
|
|
||||||
/GrayDirectClassPacket
|
|
||||||
{
|
|
||||||
%
|
|
||||||
% Get a DirectClass packet; convert to grayscale.
|
|
||||||
%
|
|
||||||
% Parameters:
|
|
||||||
% red
|
|
||||||
% green
|
|
||||||
% blue
|
|
||||||
% length: number of pixels minus one of this color (optional).
|
|
||||||
%
|
|
||||||
currentfile color_packet readhexstring pop pop
|
|
||||||
color_packet 0 get 0.299 mul
|
|
||||||
color_packet 1 get 0.587 mul add
|
|
||||||
color_packet 2 get 0.114 mul add
|
|
||||||
cvi
|
|
||||||
/gray_packet exch def
|
|
||||||
compression 0 eq
|
|
||||||
{
|
|
||||||
/number_pixels 1 def
|
|
||||||
}
|
|
||||||
{
|
|
||||||
currentfile byte readhexstring pop 0 get
|
|
||||||
/number_pixels exch 1 add def
|
|
||||||
} ifelse
|
|
||||||
0 1 number_pixels 1 sub
|
|
||||||
{
|
|
||||||
pixels exch gray_packet put
|
|
||||||
} for
|
|
||||||
pixels 0 number_pixels getinterval
|
|
||||||
} bind def
|
|
||||||
|
|
||||||
/GrayPseudoClassPacket
|
|
||||||
{
|
|
||||||
%
|
|
||||||
% Get a PseudoClass packet; convert to grayscale.
|
|
||||||
%
|
|
||||||
% Parameters:
|
|
||||||
% index: index into the colormap.
|
|
||||||
% length: number of pixels minus one of this color (optional).
|
|
||||||
%
|
|
||||||
currentfile byte readhexstring pop 0 get
|
|
||||||
/offset exch 3 mul def
|
|
||||||
/color_packet colormap offset 3 getinterval def
|
|
||||||
color_packet 0 get 0.299 mul
|
|
||||||
color_packet 1 get 0.587 mul add
|
|
||||||
color_packet 2 get 0.114 mul add
|
|
||||||
cvi
|
|
||||||
/gray_packet exch def
|
|
||||||
compression 0 eq
|
|
||||||
{
|
|
||||||
/number_pixels 1 def
|
|
||||||
}
|
|
||||||
{
|
|
||||||
currentfile byte readhexstring pop 0 get
|
|
||||||
/number_pixels exch 1 add def
|
|
||||||
} ifelse
|
|
||||||
0 1 number_pixels 1 sub
|
|
||||||
{
|
|
||||||
pixels exch gray_packet put
|
|
||||||
} for
|
|
||||||
pixels 0 number_pixels getinterval
|
|
||||||
} bind def
|
|
||||||
|
|
||||||
/PseudoClassPacket
|
|
||||||
{
|
|
||||||
%
|
|
||||||
% Get a PseudoClass packet.
|
|
||||||
%
|
|
||||||
% Parameters:
|
|
||||||
% index: index into the colormap.
|
|
||||||
% length: number of pixels minus one of this color (optional).
|
|
||||||
%
|
|
||||||
currentfile byte readhexstring pop 0 get
|
|
||||||
/offset exch 3 mul def
|
|
||||||
/color_packet colormap offset 3 getinterval def
|
|
||||||
compression 0 eq
|
|
||||||
{
|
|
||||||
/number_pixels 3 def
|
|
||||||
}
|
|
||||||
{
|
|
||||||
currentfile byte readhexstring pop 0 get
|
|
||||||
/number_pixels exch 1 add 3 mul def
|
|
||||||
} ifelse
|
|
||||||
0 3 number_pixels 1 sub
|
|
||||||
{
|
|
||||||
pixels exch color_packet putinterval
|
|
||||||
} for
|
|
||||||
pixels 0 number_pixels getinterval
|
|
||||||
} bind def
|
|
||||||
|
|
||||||
/PseudoClassImage
|
|
||||||
{
|
|
||||||
%
|
|
||||||
% Display a PseudoClass image.
|
|
||||||
%
|
|
||||||
% Parameters:
|
|
||||||
% class: 0-PseudoClass or 1-Grayscale.
|
|
||||||
%
|
|
||||||
currentfile buffer readline pop
|
|
||||||
token pop /class exch def pop
|
|
||||||
class 0 gt
|
|
||||||
{
|
|
||||||
currentfile buffer readline pop
|
|
||||||
token pop /depth exch def pop
|
|
||||||
/grays columns 8 add depth sub depth mul 8 idiv string def
|
|
||||||
columns rows depth
|
|
||||||
[
|
|
||||||
columns 0 0
|
|
||||||
rows neg 0 rows
|
|
||||||
]
|
|
||||||
{ currentfile grays readhexstring pop } image
|
|
||||||
}
|
|
||||||
{
|
|
||||||
%
|
|
||||||
% Parameters:
|
|
||||||
% colors: number of colors in the colormap.
|
|
||||||
% colormap: red, green, blue color packets.
|
|
||||||
%
|
|
||||||
currentfile buffer readline pop
|
|
||||||
token pop /colors exch def pop
|
|
||||||
/colors colors 3 mul def
|
|
||||||
/colormap colors string def
|
|
||||||
currentfile colormap readhexstring pop pop
|
|
||||||
systemdict /colorimage known
|
|
||||||
{
|
|
||||||
columns rows 8
|
|
||||||
[
|
|
||||||
columns 0 0
|
|
||||||
rows neg 0 rows
|
|
||||||
]
|
|
||||||
{ PseudoClassPacket } false 3 colorimage
|
|
||||||
}
|
|
||||||
{
|
|
||||||
%
|
|
||||||
% No colorimage operator; convert to grayscale.
|
|
||||||
%
|
|
||||||
columns rows 8
|
|
||||||
[
|
|
||||||
columns 0 0
|
|
||||||
rows neg 0 rows
|
|
||||||
]
|
|
||||||
{ GrayPseudoClassPacket } image
|
|
||||||
} ifelse
|
|
||||||
} ifelse
|
|
||||||
} bind def
|
|
||||||
|
|
||||||
/DisplayImage
|
|
||||||
{
|
|
||||||
%
|
|
||||||
% Display a DirectClass or PseudoClass image.
|
|
||||||
%
|
|
||||||
% Parameters:
|
|
||||||
% x & y translation.
|
|
||||||
% x & y scale.
|
|
||||||
% label pointsize.
|
|
||||||
% image label.
|
|
||||||
% image columns & rows.
|
|
||||||
% class: 0-DirectClass or 1-PseudoClass.
|
|
||||||
% compression: 0-none or 1-RunlengthEncoded.
|
|
||||||
% hex color packets.
|
|
||||||
%
|
|
||||||
gsave
|
|
||||||
/buffer 512 string def
|
|
||||||
/byte 1 string def
|
|
||||||
/color_packet 3 string def
|
|
||||||
/pixels 768 string def
|
|
||||||
|
|
||||||
currentfile buffer readline pop
|
|
||||||
token pop /x exch def
|
|
||||||
token pop /y exch def pop
|
|
||||||
x y translate
|
|
||||||
currentfile buffer readline pop
|
|
||||||
token pop /x exch def
|
|
||||||
token pop /y exch def pop
|
|
||||||
currentfile buffer readline pop
|
|
||||||
token pop /pointsize exch def pop
|
|
||||||
x y scale
|
|
||||||
currentfile buffer readline pop
|
|
||||||
token pop /columns exch def
|
|
||||||
token pop /rows exch def pop
|
|
||||||
currentfile buffer readline pop
|
|
||||||
token pop /class exch def pop
|
|
||||||
currentfile buffer readline pop
|
|
||||||
token pop /compression exch def pop
|
|
||||||
class 0 gt { PseudoClassImage } { DirectClassImage } ifelse
|
|
||||||
grestore
|
|
||||||
showpage
|
|
||||||
} bind def
|
|
||||||
%%EndProlog
|
|
||||||
%%Page: 1 1
|
|
||||||
%%PageBoundingBox: 428 345 508 391
|
|
||||||
DisplayImage
|
|
||||||
428 345
|
|
||||||
80 46
|
|
||||||
12
|
|
||||||
80 46
|
|
||||||
1
|
|
||||||
1
|
|
||||||
1
|
|
||||||
8
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C
|
|
||||||
1C1C1C1C1C1C1C1C
|
|
||||||
%%PageTrailer
|
|
||||||
%%Trailer
|
|
||||||
%%EOF
|
|
32
test.html
Normal file
32
test.html
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<!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>
|
@ -4,40 +4,22 @@ services:
|
|||||||
image: mongo
|
image: mongo
|
||||||
|
|
||||||
mailer:
|
mailer:
|
||||||
build: ../server
|
build: ..
|
||||||
volumes:
|
volumes:
|
||||||
- ../server/main.py:/usr/src/app/main.py
|
- ../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: toto.mail
|
SMTP_SERVER_ADDRESS: 'lol'
|
||||||
SMTP_SERVER_PORT: 994
|
SMTP_SERVER_PORT: 994
|
||||||
SMTP_SERVER_USERNAME: toto@toto.mail
|
SMTP_SERVER_USERNAME: toto
|
||||||
SMTP_SERVER_PASSWORD: password
|
SMTP_SERVER_PASSWORD: lol
|
||||||
SMTP_SERVER_SENDER: toto@toto.mail
|
SMTP_SERVER_SENDER: moi
|
||||||
ADMIN_PASSWORD: test
|
ADMIN_PASSWORD: admin
|
||||||
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
|
|
||||||
|
@ -1,39 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<link rel="stylesheet" href="./client/style.css" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="contact-mailer-message"></div>
|
|
||||||
<form action="/api/submit" method="POST" id="contact-mailer-form">
|
|
||||||
<noscript>Les protections anti-spam, nécéssitent 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>
|
|
@ -1,37 +0,0 @@
|
|||||||
worker_processes auto;
|
|
||||||
include /etc/nginx/modules-enabled/*.conf;
|
|
||||||
|
|
||||||
events {
|
|
||||||
worker_connections 768;
|
|
||||||
# multi_accept on;
|
|
||||||
}
|
|
||||||
|
|
||||||
http {
|
|
||||||
sendfile on;
|
|
||||||
tcp_nopush on;
|
|
||||||
tcp_nodelay on;
|
|
||||||
keepalive_timeout 65;
|
|
||||||
error_log stderr;
|
|
||||||
access_log /dev/stdout;
|
|
||||||
include /etc/nginx/mime.types;
|
|
||||||
default_type application/octet-stream;
|
|
||||||
types_hash_max_size 2048;
|
|
||||||
types_hash_bucket_size 128;
|
|
||||||
gzip on;
|
|
||||||
|
|
||||||
server {
|
|
||||||
add_header 'Access-Control-Allow-Origin' '*';
|
|
||||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, DELETE, OPTIONS';
|
|
||||||
listen 8080;
|
|
||||||
location / {
|
|
||||||
root /usr/app/;
|
|
||||||
index index.html;
|
|
||||||
}
|
|
||||||
location /api/ {
|
|
||||||
include uwsgi_params;
|
|
||||||
uwsgi_pass unix:/tmp/uwsgi/uwsgi.sock;
|
|
||||||
#uwsgi_param PATH_INFO "$1";
|
|
||||||
#uwsgi_param SCRIPT_NAME /;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user