From b82144158037bf9141ecb81562f093065c9a94ae Mon Sep 17 00:00:00 2001 From: Chris Date: Sun, 19 Nov 2023 22:13:19 +0100 Subject: [PATCH] attachmentsystem overhaul and cid replacement --- CHANGELOG.md | 8 ++++++ README.md | 4 +-- docker/rootfs/start.sh | 4 +-- python/mailserver3.py | 60 ++++++++++++++++++++++++++++++------------ tools/testmail.txt | 10 ++++--- 5 files changed, 62 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa10896..06c466b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## Nov 19th 2023 - V1.1.3 +- Switched SMTP server to Python3 and aiosmptd +- Switched PHP backend to PHP8.1 +- Implemented content-id replacement with smart link to API so embedded images will now work +- Updated JSON to include details about attachments (filename,size in bytes,id,cid and a download URL) +- Removed quotes from ini settings +- Made docker start script more neat + ## Nov 13th 2023 - V1.0.0 - Launch of V1.0.0 - Complete rewrite of the GUI diff --git a/README.md b/README.md index f2905c2..6e6c3e4 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ In Docker you can use the following environment variables: - [x] Storing received mails in JSON - [x] Storing file attachments - [x] Docker files and configs -- [ ] Web interface +- [x] Web interface - [x] Choose email - [x] Get random email address - [x] Download attachments safely @@ -96,7 +96,7 @@ In Docker you can use the following environment variables: - [x] Delete messages - [x] Make better theme - [x] Secure HTML, so no malicious things can be loaded - - [ ] Display embedded images inline using Content-ID + - [x] Display embedded images inline using Content-ID - [ ] Configurable settings - [x] Choose domains for random generation - [x] Choose if out-of-scope emails are discarded diff --git a/docker/rootfs/start.sh b/docker/rootfs/start.sh index 300360f..c2c9cc0 100644 --- a/docker/rootfs/start.sh +++ b/docker/rootfs/start.sh @@ -29,7 +29,7 @@ echo ' [+] Setting up config.ini' _buildConfig() { echo "[GENERAL]" echo "DOMAINS=${DOMAINS:-localhost}" - echo "URL='${URL:-http://localhost:8080}'" + echo "URL=${URL:-http://localhost:8080}" echo "SHOW_ACCOUNT_LIST=${SHOW_ACCOUNT_LIST:-false}" echo "ADMIN=${ADMIN:-}" echo "SHOW_LOGS=${SHOW_LOGS:-false}" @@ -39,7 +39,7 @@ _buildConfig() { echo "DISCARD_UNKNOWN=${DISCARD_UNKNOWN:-true}" echo "[DATETIME]" - echo "DATEFORMAT='${DATEFORMAT:-D.M.YYYY HH:mm}'" + echo "DATEFORMAT=${DATEFORMAT:-D.M.YYYY HH:mm}" echo "[CLEANUP]" echo "DELETE_OLDER_THAN_DAYS=${DELETE_OLDER_THAN_DAYS:-false}" diff --git a/python/mailserver3.py b/python/mailserver3.py index 730da70..acddc29 100644 --- a/python/mailserver3.py +++ b/python/mailserver3.py @@ -19,6 +19,7 @@ DISCARD_UNKNOWN = False DELETE_OLDER_THAN_DAYS = False DOMAINS = [] LAST_CLEANUP = 0 +URL = "" class CustomHandler: async def handle_DATA(self, server, session, envelope): @@ -31,6 +32,8 @@ class CustomHandler: logger.debug('Message addressed from: %s' % envelope.mail_from) logger.debug('Message addressed to: %s' % str(rcpts)) + filenamebase = str(int(round(time.time() * 1000))) + # Get the raw email data raw_email = envelope.content.decode('utf-8') @@ -50,25 +53,11 @@ class CustomHandler: html += part.get_payload() else: filename = part.get_filename() + cid = part.get('Content-ID') + logger.debug('Handling attachment: "%s" of type "%s" with CID "%s"',filename, part.get_content_type(), cid) if filename is None: filename = 'untitled' - attachments['file%d' % len(attachments)] = (filename,part.get_payload(decode=True)) - - edata = { - 'subject': message['subject'], - 'body': plaintext, - 'htmlbody': html, - 'from': message['from'], - 'attachments':[] - } - savedata = {'sender_ip':peer[0], - 'from':message['from'], - 'rcpts':rcpts, - 'raw':raw_email, - 'parsed':edata - } - - filenamebase = str(int(round(time.time() * 1000))) + attachments['file%d' % len(attachments)] = (filename,part.get_payload(decode=True),cid) for em in rcpts: em = em.lower() @@ -89,6 +78,22 @@ class CustomHandler: if not os.path.exists("../data/"+em): os.mkdir( "../data/"+em, 0o755 ) + + + edata = { + 'subject': message['subject'], + 'body': plaintext, + 'htmlbody': self.replace_cid_with_attachment_id(html, attachments,filenamebase,em), + 'from': message['from'], + 'attachments':[], + 'attachments_details':[] + } + savedata = {'sender_ip':peer[0], + 'from':message['from'], + 'rcpts':rcpts, + 'raw':raw_email, + 'parsed':edata + } #same attachments if any for att in attachments: @@ -99,12 +104,32 @@ class CustomHandler: file.write(attd[1]) file.close() edata["attachments"].append(filenamebase+"-"+attd[0]) + edata["attachments_details"].append({ + "filename":attd[0], + "cid":attd[2][1:-1], + "id":filenamebase+"-"+attd[0], + "download_url":URL+"/api/attachment/"+em+"/"+filenamebase+"-"+attd[0], + "size":len(attd[1]) + }) # save actual json data with open("../data/"+em+"/"+filenamebase+".json", "w") as outfile: json.dump(savedata, outfile) return '250 OK' + + def replace_cid_with_attachment_id(self, html_content, attachments,filenamebase,email): + # Replace cid references with attachment filename + for attachment_id in attachments: + attachment = attachments[attachment_id] + filename = attachment[0] + cid = attachment[2] + if cid is None: + continue + cid = cid[1:-1] + if cid is not None: + html_content = html_content.replace('cid:' + cid, "/api/attachment/"+email+"/"+filenamebase+"-"+filename) + return html_content def cleanup(): if(DELETE_OLDER_THAN_DAYS == False or time.time() - LAST_CLEANUP < 86400): @@ -150,6 +175,7 @@ if __name__ == '__main__': if("discard_unknown" in Config.options("MAILSERVER")): DISCARD_UNKNOWN = (Config.get("MAILSERVER", "DISCARD_UNKNOWN").lower() == "true") DOMAINS = Config.get("GENERAL", "DOMAINS").lower().split(",") + URL = Config.get("GENERAL", "URL") 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") diff --git a/tools/testmail.txt b/tools/testmail.txt index 3889102..09f3bd1 100644 --- a/tools/testmail.txt +++ b/tools/testmail.txt @@ -21,6 +21,9 @@ Content-Type: text/html +

+


This is the HTML part of the email.

@@ -28,10 +31,11 @@ Content-Type: text/html --alternative-boundary-- --boundary-string -Content-Type: application/octet-stream -Content-Disposition: attachment; filename="example.txt" +Content-Type: image/svg+xml +Content-Id: +Content-Disposition: attachment; filename="42.svg" -(Attach the contents of your file here) +42 --boundary-string-- .