import bottle request = bottle.request response = bottle.response import smtplib from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart from email.message import EmailMessage import os # for environ vars import sys # to print ot stderr import re # to match our template system import pymongo # database from dotenv import load_dotenv import random, string # for tokens import html # for sanitization from bson.json_util import dumps import datetime # For email date from spamassasin_client import SpamAssassin ##################################################### Bottle stuff ############################################ class StripPathMiddleware(object): ''' Get that slash out of the request ''' def __init__(self, a): self.a = a def __call__(self, e, h): e['PATH_INFO'] = e['PATH_INFO'].rstrip('/') return self.a(e, h) app = application = bottle.Bottle(catchall=False) ##################################################### Configuration ############################################ # The exception that is thrown when an argument is missing class MissingParameterException (Exception): pass def get_env(var, default=None): """var is an env var name, default is the value to return if var does not exist. If no default and no value, an exception is raised.""" if var in os.environ: return os.environ[var] elif default is not None: return default else: raise MissingParameterException("Environment variable {} is missing".format(var)) # Token generation token_chars = string.ascii_lowercase+string.ascii_uppercase+string.digits token_len = 50 # form template regex form_regex = '\{\{(\w+)(\|[\w\s,\.\?\-\'\"\!\[\]]+)?\}\}' # Load file from .env file. load_dotenv(os.path.dirname(__file__) + '.env') # Get address and port from env listen_address = get_env('LISTEN_ADDRESS', '0.0.0.0') listen_port = get_env('LISTEN_PORT', 8080) # Get SMTP infos from env smtp_server_address = get_env('SMTP_SERVER_ADDRESS') smtp_server_port = get_env('SMTP_SERVER_PORT') smtp_server_username = get_env('SMTP_SERVER_USERNAME') smtp_server_password = get_env('SMTP_SERVER_PASSWORD') smtp_server_sender = get_env('SMTP_SERVER_SENDER') # Get mongodb connection mongodb_host = get_env('MONGODB_HOST') mongodb_port = get_env('MONGODB_PORT', '27017') mongodb_dbname = get_env('MONGODB_DBNAME', 'contact_mailer') # Security admin_password = get_env('ADMIN_PASSWORD') # Test purpose, do not send mail do_not_send = get_env('do_not_send', 'false') == 'true' if 'SMTP_SSL' in os.environ and os.environ['SMTP_SSL'] == 'true': security = 'ssl' elif 'SMTP_STARTTLS' in os.environ and os.onviron['SMTP_STARTTLS'] == 'true': security = 'starttls' else: raise MissingParameterException('No security env var (SMTP_SSL or SMTP_STARTTLS) have been defined. (Expected true or false)') # mongodb initialization mongodb_client = pymongo.MongoClient("mongodb://{}:{}/".format(mongodb_host, mongodb_port), connect=False, serverSelectionTimeoutMS=10000, connectTimeoutMS=10000) mongodb_database = mongodb_client[mongodb_dbname] print(mongodb_database) ##################################################### main route: mail submission ############################################ @app.post('/submit') def submission (): # Getting token if 'token' in request.forms: token = request.forms.getunicode('token') else: return resp(400, 'Le jeton d’autentification est requis') # Getting mail address if 'mail' in request.forms: from_address = request.forms.getunicode('mail') else: #response.status = 400 #return 'Le mail est requis' from_address = '' try: form = mongodb_database['forms'].find({'token': token})[0] except IndexError as e: return resp(400, 'Le formulaire demandé est introuvable, merci de vérifier que le token utilisé est le bon') except pymongo.errors.ServerSelectionTimeoutError as e: return resp(500, '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: subject_fields = fill_fields(request, get_fields(form['subject'])) content_fields = fill_fields(request, get_fields(form['content'])) except MissingParameterException as e: return resp(400, str(e)) subject = re.sub(form_regex, r'{\1}', form['subject']).format(**subject_fields) content = re.sub(form_regex, r'{\1}', form['content']).format(**content_fields) try: msg = build_mail(from_address, form['mail'], subject, content) if is_spam(msg): return resp(400, 'Votre message semble être du spam !') if not send_mail(form['mail'], msg): return resp(500, 'Le mail n’a pas pu être envoyé.') except smtplib.SMTPDataError as e: response.status = 500 error = 'Le mail a été refusé. Merci de réessayer plus tard.' except smtplib.SMTPRecipientsRefused as e: response.status = 500 error = 'Impossible de trouver le destinataire du mail. Merci de réessayer plus tard' except Exception as e: raise origin = request.headers.get('origin') return resp(200, 'Mail envoyé !') ##################################################### Helpers ############################################ def resp (status, msg, data='{}'): response.status = status return '{{"status": "{}", "msg": "{}", "data": {}}}'.format(status, msg, data) 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. """ result = {} for match in re.findall(form_regex, string): result[match[0]] = None if match[1] == '' else match[1][1:] return result def fill_fields(request, fields): """Look for fields in request and fill fields dict with values or let default ones. If the value is required, throw exception.""" for field in fields: if field in request.forms: if request.forms.get(field).strip() == '' and fields[field] is None: # If empty and mandatory raise MissingParameterException("Le champs {} doit être rempli".format(field)) fields[field] = request.forms.getunicode(field) if fields[field] is None: # if unicode failed fields[field] = request.forms.get(field) if fields[field] is None: # if get failed too raise Exception("Error, field '{}' not gettable".format(field)) elif fields[field] is None: raise MissingParameterException("Le champs {} est obligatoire".format(field)) return fields def build_mail(from_address, to, subject, content): msg = EmailMessage() msg['From'] = smtp_server_sender msg.add_header('reply-to', from_address) msg['To'] = to msg['Subject'] = subject msg['Date'] = datetime.datetime.now() msg.set_content(MIMEText(content, 'plain', "utf-8")) #or #msg.set_content(content) return msg def is_spam(msg): assassin = SpamAssassin(msg.as_string().encode(), 'spamassassin') return assassin.is_spam() def send_mail (to, msg): """Actually connect to smtp server and send the mail""" if do_not_send: print('-------------------------- Following message sent. But only to stdout ----------------------------------') print(msg.as_string()) print('--------------------------------------------------------------------------------------------------------') return True # SMTP preambles if security == 'ssl': smtp = smtplib.SMTP_SSL(smtp_server_address, smtp_server_port) elif security == 'starttls': smtp = smtplib.SMTP(smtp_server_address, smtp_server_port) smtp.ehlo() if security == 'starttls': smtp.starttls() smtp.ehlo() # SMTP connection smtp.login(smtp_server_username, smtp_server_password) refused = smtp.sendmail(smtp_server_sender, to, msg.as_string()) smtp.close() if refused: print('Message was not send to ' + str(refused)) return False return True def login(request): """ Check if user is admin or simple user. Return a disct with _privilege key. dict is also a user if _privilege == 1 Privileges : 0=admin 1=loggedIn 1000=guest """ if 'admin_pass' in request.forms and request.forms['admin_pass'] == admin_password: return {'_privilege':0, '_id':'-1'} if 'token' in request.forms: token = request.forms.getunicode('token') try: user = mongodb_database['users'].find({'token': token})[0] user['_privilege'] = 1 return user except IndexError as e: pass #except pymongo.errors.ServerSelectionTimeoutError as e: # response.status = 500 # return {'_error': True} # anonymous return {'_privilege': 1000} # anonymous ##################################################### Forms ############################################ @app.post('/form') def create_form (): # Getting subject template if 'subject' in request.forms: subject = request.forms.getunicode('subject') elif mail_default_subject != '': subject = mail_default_subject else: return resp(400, 'Le champs « sujet » est requis') # Getting mail content if 'content' in request.forms: content = request.forms.getunicode('content') else: return resp(400, 'Le champs « contenu » est requis') # Getting from address if 'mail' in request.forms: mail = request.forms.getunicode('mail') else: return resp(400, 'Le champs « adresse » est requis') user = login(request) if user['_privilege'] > 1: return resp(400, 'Privilèges insufisants') # TODO limit the insertion rate token = ''.join(random.sample(token_chars, token_len)) try: newEntry = { 'mail': mail, 'content': content, 'subject': subject, 'user_id': user['_id'], 'token': token, } if 'honeypotfield' in request.forms: newEntry['honeypotfield'] = request.forms.getunicode('honeypotfield') if 'timerdelay' in request.forms: newEntry['timerdelay'] = request.forms.getunicode('timerdelay') inserted = mongodb_database['forms'].insert_one(newEntry) except pymongo.errors.ServerSelectionTimeoutError as e: return resp(500, 'La base de donnée n’est pas accessible') return resp(200, 'Créé : ' + token) @app.post('/form/list') def list_forms (): try: user = login(request) if user['_privilege'] == 0: filt = {} elif user['_privilege'] == 1: filt = {'user_id': user['_id']} else: return resp(400, 'Privilèges insufisants') data = mongodb_database['forms'].find(filt) return resp(200,'', dumps(list(data))) except pymongo.errors.ServerSelectionTimeoutError as e: return resp(500,'La base de donnée n’est pas accessible') @app.delete('/form/') def delete_form(token): # TODO If admin or form owner user = login(request) if user['_privilege'] > 1: return resp(400, 'Privilèges insufisants') # Actually delete try: form = mongodb_database['forms'].find({'token':token })[0] except IndexError as e: return resp(400, 'Le token n’est pas valide') except pymongo.errors.ServerSelectionTimeoutError as e: return resp(500, 'La base de donnée n’est pas accessible') if user['_privilege'] == 0 or (form['user_id'] == user['_id']): try: mongodb_database['forms'].delete_one({ 'token': token, }) except pymongo.errors.ServerSelectionTimeoutError as e: return resp(500, 'La base de donnée n’est pas accessible') return resp(200, 'Supprimé ' + token) return resp(400, 'Privilèges insufisants') ##################################################### Users ############################################ @app.post('/user/list') def list_users (): user = login(request) if user['_privilege'] > 0: return resp(400, 'Privilèges insufisants') try: data = mongodb_database['users'].find() return resp(200, '', dumps(list(data))) except pymongo.errors.ServerSelectionTimeoutError as e: return resp(500, 'La base de donnée n’est pas accessible') @app.route('/user/', method=['OPTIONS', 'PUT']) def create_user (username): user = login(request) if user['_privilege'] > 0: return resp(400, 'Privilèges insufisants') try: mongodb_database['users'].find({'username': username})[0] return resp(400, 'L’utilisateur existe déjà') except IndexError as e: try: inserted = mongodb_database['users'].insert_one({ 'username': username, 'token': ''.join(random.sample(token_chars, token_len)) }) return resp(200, 'Créé : ' + username) except pymongo.errors.ServerSelectionTimeoutError as e: return resp(500, 'La base de donnée n’est pas accessible') except pymongo.errors.ServerSelectionTimeoutError as e: return resp(500,'La base de donnée n’est pas accessible') @app.delete('/user/') def delete_user (username): user = login(request) if user['_privilege'] > 0: return resp(400, 'Privilèges insufisants') try: mongodb_database['users'].find({'username': username})[0] mongodb_database['users'].delete_one({ 'username': username, }) return resp(200, 'Supprimé ' + username) except IndexError as e: return resp(400, 'L’utilisateur n’existe pas') except pymongo.errors.ServerSelectionTimeoutError as e: return resp(500, 'La base de donnée n’est pas accessible') ##################################################### app startup ############################################ prod_app = StripPathMiddleware(app) if __name__ == '__main__': bottle.run(app=prod_app, host=listen_address, port=listen_port, debug=True)