Skip to content

Commit

Permalink
attachmentsystem overhaul and cid replacement
Browse files Browse the repository at this point in the history
  • Loading branch information
geek-at committed Nov 19, 2023
1 parent 2e55c2b commit b821441
Show file tree
Hide file tree
Showing 5 changed files with 62 additions and 24 deletions.
8 changes: 8 additions & 0 deletions 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
Expand Down
4 changes: 2 additions & 2 deletions README.md
Expand Up @@ -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
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions docker/rootfs/start.sh
Expand Up @@ -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}"
Expand All @@ -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}"
Expand Down
60 changes: 43 additions & 17 deletions python/mailserver3.py
Expand Up @@ -19,6 +19,7 @@
DELETE_OLDER_THAN_DAYS = False
DOMAINS = []
LAST_CLEANUP = 0
URL = ""

class CustomHandler:
async def handle_DATA(self, server, session, envelope):
Expand All @@ -31,6 +32,8 @@ async def handle_DATA(self, server, session, envelope):
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')

Expand All @@ -50,25 +53,11 @@ async def handle_DATA(self, server, session, envelope):
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()
Expand All @@ -89,6 +78,22 @@ async def handle_DATA(self, server, session, envelope):

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:
Expand All @@ -99,12 +104,32 @@ async def handle_DATA(self, server, session, envelope):
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):
Expand Down Expand Up @@ -150,6 +175,7 @@ async def run(port):
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")
Expand Down
10 changes: 7 additions & 3 deletions tools/testmail.txt
Expand Up @@ -21,17 +21,21 @@ Content-Type: text/html

<html>
<body>
<p><img src="cid:part1.wUxVdgTp.JyT3JNov@localhost.dev"
moz-do-not-send="false"></p>
<p><br>
<p>This is the HTML part of the email.</p>
</body>
</html>

--alternative-boundary--

--boundary-string
Content-Type: application/octet-stream
Content-Disposition: attachment; filename="example.txt"
Content-Type: image/svg+xml
Content-Id: <part1.wUxVdgTp.JyT3JNov@localhost.dev>
Content-Disposition: attachment; filename="42.svg"

(Attach the contents of your file here)
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>42</title><path d="M24 12.42l-4.428 4.415H24zm-4.428-4.417l-4.414 4.418v4.414h4.414V12.42L24 8.003V3.575h-4.428zm-4.414 0l4.414-4.428h-4.414zM0 15.996h8.842v4.43h4.412V12.42H4.428l8.826-8.846H8.842L0 12.421z"/></svg>

--boundary-string--
.
Expand Down

0 comments on commit b821441

Please sign in to comment.