From 820f75e21aab30fd6892667690565806ff9c5600 Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 13 Nov 2023 21:04:58 +0100 Subject: [PATCH] testing with python3 smtp library --- python/mailserver3.py | 189 ++++++++++++++++++++++++++++++++++++++++++ tools/send.py | 11 +++ 2 files changed, 200 insertions(+) create mode 100644 python/mailserver3.py create mode 100644 tools/send.py diff --git a/python/mailserver3.py b/python/mailserver3.py new file mode 100644 index 0000000..506fe19 --- /dev/null +++ b/python/mailserver3.py @@ -0,0 +1,189 @@ +import asyncio +from aiosmtpd.controller import Controller +import email +from email.header import decode_header +from email.utils import parseaddr +import configparser +import json +import re +import logging +import os, sys +import time + +logger = logging.getLogger(__name__) + +# globals for settings +DISCARD_UNKNOWN = False +DELETE_OLDER_THAN_DAYS = False +DOMAINS = [] +LAST_CLEANUP = 0 + +class TrashmailHandler: + async def handle_DATA(self, server, session, envelope): + peer = session.peer + mailfrom = envelope.mail_from + rcpttos = envelope.rcpt_tos + data = envelope.content.decode('utf8', errors='replace') + + try: + mailfrom = parseaddr(mailfrom)[1] + logger.debug('Receiving message from: %s:%d' % peer) + logger.debug('Message addressed from: %s' % mailfrom) + logger.debug('Message addressed to: %s' % str(rcpttos)) + + msg = email.message_from_bytes(envelope.content) + #print("head -> ",msg) + subject = '' + for encoded_string, charset in decode_header(msg.get('Subject')): + try: + if charset is not None: + subject += encoded_string.decode(charset) + else: + subject += encoded_string + except: + logger.exception('Error reading part of subject: %s charset %s' % + (encoded_string, charset)) + return '500 Could not process your message' + + logger.debug('Subject: %s' % subject) + + text_parts = [] + html_parts = [] + attachments = {} + + for part in msg.walk(): + if part.get_content_maintype() == 'multipart': + continue + + c_type = part.get_content_type() + c_disp = part.get('Content-Disposition') + + # text parts will be appended to text_parts + if c_type == 'text/plain' and c_disp == None: + text_parts.append(str(part.get_payload(decode=True).strip().decode('utf8', errors='replace'))) + # ignore html part + elif c_type == 'text/html': + html_parts.append(str(part.get_payload(decode=True).strip().decode('utf8', errors='replace'))) + # attachments will be sent as files in the POST request + else: + filename = part.get_filename() + filecontent = part.get_payload(decode=True) + if filecontent is not None: + if filename is None: + filename = 'untitled' + attachments['file%d' % len(attachments)] = (filename, + filecontent) + + body = '\n'.join(text_parts) + htmlbody = '\n'.join(html_parts) + + except: + logger.exception('Error reading incoming email') + return '500 Could not process your message' + + else: + # this data will be sent as POST data + edata = { + 'subject': subject, + 'body': body, + 'htmlbody': htmlbody, + 'from': mailfrom, + 'attachments':[] + } + savedata = {'sender_ip':peer[0],'from':mailfrom,'rcpts':rcpttos,'raw':data,'parsed':edata} + + filenamebase = str(int(round(time.time() * 1000))) + + for em in rcpttos: + em = em.lower() + if not re.match(r"[^@\s]+@[^@\s]+\.[a-zA-Z0-9]+$", em): + logger.exception('Invalid recipient: %s' % em) + continue + + domain = em.split('@')[1] + found = False + for x in DOMAINS: + if "*" in x and domain.endswith(x.replace('*', '')): + found = True + elif domain == x: + found = True + if(DISCARD_UNKNOWN and found==False): + logger.info('Discarding email for unknown domain: %s' % domain) + continue + + if not os.path.exists("../data/"+em): + os.mkdir( "../data/"+em, 0o755 ) + + #same attachments if any + for att in attachments: + if not os.path.exists("../data/"+em+"/attachments"): + os.mkdir( "../data/"+em+"/attachments", 0o755 ) + attd = attachments[att] + file = open("../data/"+em+"/attachments/"+filenamebase+"-"+attd[0], 'wb') + file.write(attd[1]) + file.close() + edata["attachments"].append(filenamebase+"-"+attd[0]) + + # save actual json data + with open("../data/"+em+"/"+filenamebase+".json", "w") as outfile: + json.dump(savedata, outfile) + + # if error_occurred: + # return '500 Could not process your message' + return '250 OK' + +def cleanup(): + if(DELETE_OLDER_THAN_DAYS == False or time.time() - LAST_CLEANUP < 86400): + return + logger.info("Cleaning up") + rootdir = '../data/' + for subdir, dirs, files in os.walk(rootdir): + for file in files: + if(file.endswith(".json")): + filepath = os.path.join(subdir, file) + file_modified = os.path.getmtime(filepath) + if(time.time() - file_modified > (DELETE_OLDER_THAN_DAYS * 86400)): + os.remove(filepath) + logger.info("Deleted file: " + filepath) + +if __name__ == '__main__': + ch = logging.StreamHandler() + ch.setLevel(logging.DEBUG) + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + ch.setFormatter(formatter) + logger.setLevel(logging.DEBUG) + logger.addHandler(ch) + + if not os.path.isfile("../config.ini"): + print("[ERR] Config.ini not found. Rename example.config.ini to config.ini. Defaulting to port 25") + port = 25 + else : + Config = configparser.ConfigParser(allow_no_value=True) + Config.read("../config.ini") + port = int(Config.get("MAILSERVER","MAILPORT")) + if("discard_unknown" in Config.options("MAILSERVER")): + DISCARD_UNKNOWN = (Config.get("MAILSERVER","DISCARD_UNKNOWN").lower() == "true") + DOMAINS = Config.get("GENERAL","DOMAINS").lower().split(",") + + if("CLEANUP" in Config.sections() and "delete_older_than_days" in Config.options("CLEANUP")): + DELETE_OLDER_THAN_DAYS = (Config.get("CLEANUP","DELETE_OLDER_THAN_DAYS").lower() == "true") + + print("[i] Starting Mailserver on port",port) + print("[i] Discard unknown domains:",DISCARD_UNKNOWN) + print("[i] Listening for domains:",DOMAINS) + + + handler = TrashmailHandler() + controller = Controller(handler, hostname='0.0.0.0', port=port) + # Run the event loop in a separate thread. + controller.start() + print("[i] Ready to receive Emails") + print("") + + while True: + try: + time.sleep(3600) + cleanup() + except KeyboardInterrupt: + break + \ No newline at end of file diff --git a/tools/send.py b/tools/send.py new file mode 100644 index 0000000..d647b1e --- /dev/null +++ b/tools/send.py @@ -0,0 +1,11 @@ +from smtplib import SMTP as Client + +client = Client("localhost", "2525") +r = client.sendmail('a@example.com', ['b@domain.tld'], """\ +From: Anne Person +To: Bart Person +Subject: A test +Message-ID: + +Hi Bart, this is Anne. +""") \ No newline at end of file