From a61b90067570cd356592f9162a5eff1b42bc0669 Mon Sep 17 00:00:00 2001 From: Son NK Date: Tue, 17 Dec 2019 18:48:06 +0200 Subject: [PATCH] Sign DKIM at app level - add DKIM_PRIVATE_KEY_PATH param - create email_utils.add_dkim_signature - add DKIM signature for transactional emails - add DKIM signature for forward & reply emails. In reply phase, only non-custom-domain emails have DKIM added. --- .env.example | 5 ++++- app/config.py | 8 ++++++++ app/email_utils.py | 37 +++++++++++++++++++++++++++++++++++-- email_handler.py | 13 ++++++++++++- local_data/dkim.key | 15 +++++++++++++++ tests/env.test | 1 + 6 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 local_data/dkim.key diff --git a/.env.example b/.env.example index c69002a2..0cbc8cfd 100644 --- a/.env.example +++ b/.env.example @@ -28,12 +28,15 @@ EMAIL_SERVERS_WITH_PRIORITY=[(10, "email.hostname.")] # these emails are ignored when computing stats # IGNORED_EMAILS = ["my_email@domain.com"] + +# the DKIM private key used to compute DKIM-Signature +DKIM_PRIVATE_KEY_PATH=local_data/dkim.key # <<< END Email related settings >>> # <<< Database >>> # delete and recreate sqlite database, for local development -RESET_DB=true +# RESET_DB=true # DB Connection DB_URI=sqlite:///db.sqlite diff --git a/app/config.py b/app/config.py index 09fa3854..e1684eaf 100644 --- a/app/config.py +++ b/app/config.py @@ -57,6 +57,14 @@ if os.environ.get("IGNORED_EMAILS"): else: IGNORED_EMAILS = [] +DKIM_PRIVATE_KEY_PATH = get_abs_path(os.environ["DKIM_PRIVATE_KEY_PATH"]) +DKIM_SELECTOR = b"dkim" + +with open(DKIM_PRIVATE_KEY_PATH) as f: + DKIM_PRIVATE_KEY = f.read() + +DKIM_HEADERS = [b'from', b'to', b'subject'] + # Database DB_URI = os.environ["DB_URI"] diff --git a/app/email_utils.py b/app/email_utils.py index 54fe9808..cc7059a0 100644 --- a/app/email_utils.py +++ b/app/email_utils.py @@ -3,9 +3,18 @@ from email.message import EmailMessage from email.utils import make_msgid, formatdate from smtplib import SMTP +import dkim from jinja2 import Environment, FileSystemLoader -from app.config import SUPPORT_EMAIL, ROOT_DIR, POSTFIX_SERVER, NOT_SEND_EMAIL +from app.config import ( + SUPPORT_EMAIL, + ROOT_DIR, + POSTFIX_SERVER, + NOT_SEND_EMAIL, + DKIM_SELECTOR, + DKIM_PRIVATE_KEY, + DKIM_HEADERS, +) from app.log import LOG @@ -123,7 +132,12 @@ def send_email(to_email, subject, plaintext, html): LOG.d("Date header: %s", date_header) msg["Date"] = date_header - smtp.send_message(msg, from_addr=SUPPORT_EMAIL, to_addrs=[to_email]) + # add DKIM + email_domain = SUPPORT_EMAIL[SUPPORT_EMAIL.find("@") + 1 :] + add_dkim_signature(msg, email_domain) + + msg_raw = msg.as_string().encode() + smtp.sendmail(SUPPORT_EMAIL, to_email, msg_raw) def get_email_name(email_from): @@ -146,3 +160,22 @@ def get_email_part(email_from): return email_from[email_from.find("<") + 1 : email_from.find(">")].strip() return email_from + + +def add_dkim_signature(msg: EmailMessage, email_domain: str): + # Specify headers in "byte" form + # Generate message signature + sig = dkim.sign( + msg.as_string().encode(), + DKIM_SELECTOR, + email_domain.encode(), + DKIM_PRIVATE_KEY.encode(), + include_headers=DKIM_HEADERS, + ) + sig = sig.decode() + + # remove linebreaks from sig + sig = sig.replace("\n", " ").replace("\r", "") + + # Add the DKIM-Signature + msg.add_header("DKIM-Signature", sig[len("DKIM-Signature: ") :]) diff --git a/email_handler.py b/email_handler.py index 9fe7f71d..01294e85 100644 --- a/email_handler.py +++ b/email_handler.py @@ -39,7 +39,12 @@ from smtplib import SMTP from aiosmtpd.controller import Controller from app.config import EMAIL_DOMAIN, POSTFIX_SERVER, URL -from app.email_utils import get_email_name, get_email_part, send_email +from app.email_utils import ( + get_email_name, + get_email_part, + send_email, + add_dkim_signature, +) from app.extensions import db from app.log import LOG from app.models import GenEmail, ForwardEmail, ForwardEmailLog @@ -179,6 +184,8 @@ class MailHandler: envelope.rcpt_options, ) + add_dkim_signature(msg, EMAIL_DOMAIN) + # smtp.send_message has UnicodeEncodeErroremail issue # encode message raw directly instead msg_raw = msg.as_string().encode() @@ -242,6 +249,10 @@ class MailHandler: ) del msg["DKIM-Signature"] + # add DKIM-Signature for non-custom-domain alias + if alias.endswith(EMAIL_DOMAIN): + add_dkim_signature(msg, EMAIL_DOMAIN) + # email seems to come from alias msg.replace_header("From", alias) msg.replace_header("To", forward_email.website_email) diff --git a/local_data/dkim.key b/local_data/dkim.key new file mode 100644 index 00000000..afa7ba21 --- /dev/null +++ b/local_data/dkim.key @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQCxhcKgFHz+HbZiuUhH7iGCVsaZYQ7xzf64ui+09QFlSYzl7d28 +LVlr7nvM0+xDbwwsgu2D1vweklroWM5FjbfVtJX3HvSnNbwceX5du/m8RHelmX0/ +vLSfsEcnvdNjBmwl/gSIUb660pEp2yo6dUBDTzTDUBNoL6qmnnTNhriRoQIDAQAB +AoGAdhGEtHtr9odEerzIei7DUrDsPa70BZcAR1Rtzmj1mKwmbfaad0GiK8rdxAlf +JiqBaklaN0mRPbQRil8mMdRj4z8gBYbiHWIL7q6zEjjo8f6CUnNqKgs2trTApqLq +L4l110fFSHCmIava5Ly9hJhdOWuJ+PUbcbp0l3j2yoz7RhECQQDa8IMEB/qqeM+e +FTz2+F3HhPI3tGALKWYyCbqcip9UUePfPQ/m547YXsdc1ATzb8OsI7emqTcZLu7H +joX+8WN/AkEAz5J9uFnp2+fWvmkNV1imoys38OwOq7yYUBSfgDymuzWrf8D2L5mt +gSK2LToIjfMRwdJ1RFLGv6oCy6ge3aga3wJAaEKKkZvfIdkgPY6tloqV1hKYajCK +YCZZ1VBOvodA8p2An2lrrjDtFFqmI62PogHCM7JanZINe/+elAdqBgsbrwJBAIN1 +wY2Z1FRjlkttePeSu6anXnyE5B28CbLd/M5YmzgBm6YDbWdkKtCYTUyDbpuID/zy +7zXgPuNwJukYhsPXDX0CQGD3laRUSRZiVSD/rJwsJTG2o1FZcsv13CO/0jY7sYxk +IjBK29XMHhTB/dip+beU0RCLFjB3nNK8VyMWmmn1WJ0= +-----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/tests/env.test b/tests/env.test index 34a5731c..87246925 100644 --- a/tests/env.test +++ b/tests/env.test @@ -10,6 +10,7 @@ ADMIN_EMAIL=to_fill # Max number emails user can generate for free plan MAX_NB_EMAIL_FREE_PLAN=3 EMAIL_SERVERS_WITH_PRIORITY=[(10, "email.hostname.")] +DKIM_PRIVATE_KEY_PATH=local_data/dkim.key # Database RESET_DB=true