"""Outbound email via Purelymail, and template rendering.
Preconfigured for Purelymail — only the account's login and password are needed:
MAIL_USER=you@yourdomain.com (or you@purelymail.com)
MAIL_PASSWORD=... (use an App Password if 2FA is enabled)
Every message is sent through SMTP (smtp.purelymail.com:465, SSL/TLS) and a
copy is saved to the account's Sent folder over IMAP
(imap.purelymail.com:993, SSL/TLS). Every attempt — success or failure — is
recorded in the mail_log table shown on the admin "Mail status" page.
"""
import html as html_mod
import imaplib
import os
import smtplib
import time
from email.message import EmailMessage
from email.utils import make_msgid
import db
SMTP_HOST = "smtp.purelymail.com"
SMTP_PORT = 465 # SSL/TLS
IMAP_HOST = "imap.purelymail.com"
IMAP_PORT = 993 # SSL/TLS
SENT_FOLDER_CANDIDATES = ("Sent", "INBOX.Sent", "Sent Messages", "Sent Items")
def render(template: str, **vars) -> str:
"""Safe {placeholder} substitution — unknown placeholders are left as-is."""
out = template
for k, v in vars.items():
out = out.replace("{" + k + "}", str(v))
return out
def _save_to_sent(user, password, msg) -> str:
"""Append a copy of the message to the account's Sent folder. Returns a status string."""
try:
with imaplib.IMAP4_SSL(IMAP_HOST, IMAP_PORT) as imap:
imap.login(user, password)
raw = msg.as_bytes()
stamp = imaplib.Time2Internaldate(time.time())
last_err = ""
for folder in SENT_FOLDER_CANDIDATES:
status, resp = imap.append(folder, r"(\Seen)", stamp, raw)
if status == "OK":
return f"saved to '{folder}'"
last_err = (resp[0] or b"").decode("utf-8", "replace") if resp else ""
# No known folder worked — create "Sent" and try once more.
imap.create("Sent")
status, resp = imap.append("Sent", r"(\Seen)", stamp, raw)
if status == "OK":
return "saved to 'Sent' (folder created)"
return f"failed: {last_err or 'could not append to any Sent folder'}"
except Exception as e:
return f"failed: {e}"
def _record(request_id, recipients, subject, status, detail, sent_copy):
try:
con = db.get_db()
con.execute(
"INSERT INTO mail_log (at, request_id, recipients, subject, status, detail, sent_copy)"
" VALUES (?,?,?,?,?,?,?)",
(db.now(), request_id, ", ".join(recipients), subject, status, detail, sent_copy))
con.commit()
con.close()
except Exception as e:
print(f"[mail] Could not record mail log entry: {e}")
def send_mail(to_addrs, subject, body, request_id=None, attachments=None, inline_images=None):
"""Send an email through Purelymail, keep a copy in the Sent folder, and
record the attempt in the mail log. Returns (ok, detail).
attachments: optional list of (filename, bytes, maintype, subtype).
inline_images: optional dict {marker_name: (bytes, subtype)}. When given,
an HTML alternative is built where each "{image:marker_name}"
in the body becomes an embedded ; the plain-text part
shows "[image: marker_name]" instead.
"""
if isinstance(to_addrs, str):
to_addrs = [to_addrs]
to_addrs = [a for a in to_addrs if a]
if not to_addrs:
return False, "no recipients"
user = os.environ.get("MAIL_USER", "")
password = os.environ.get("MAIL_PASSWORD", "")
msg = EmailMessage()
msg["From"] = user or "approvalflow@localhost"
msg["To"] = ", ".join(to_addrs)
msg["Subject"] = subject
if inline_images:
text = body
for name in inline_images:
text = text.replace("{image:" + name + "}", f"[image: {name}]")
msg.set_content(text)
html = html_mod.escape(body).replace("\n", "
\n")
cids = {}
for name in inline_images:
cid = make_msgid(domain="martinhal.approvalflow")
cids[name] = cid
html = html.replace("{image:" + name + "}",
f'')
msg.add_alternative(
f'