From 11789559f14a047451c9e9bdf4ef93b3d3bc468f Mon Sep 17 00:00:00 2001 From: Son NK <> Date: Wed, 17 Mar 2021 10:09:02 +0100 Subject: [PATCH] move spf_pass(), sl_sendmail() to email_utils.py --- app/email_utils.py | 124 ++++++++++++++++++++++++++++++++++++++++++- email_handler.py | 129 ++------------------------------------------- 2 files changed, 126 insertions(+), 127 deletions(-) diff --git a/app/email_utils.py b/app/email_utils.py index 6f74e98d..23863ad1 100644 --- a/app/email_utils.py +++ b/app/email_utils.py @@ -5,15 +5,17 @@ import os import quopri import random import re +import time from email.header import decode_header from email.message import Message from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.utils import make_msgid, formatdate, parseaddr -from smtplib import SMTP +from smtplib import SMTP, SMTPServerDisconnected import arrow import dkim +import spf from jinja2 import Environment, FileSystemLoader from validate_email import validate_email @@ -38,6 +40,8 @@ from app.config import ( ALERT_DIRECTORY_DISABLED_ALIAS_CREATION, TRANSACTIONAL_BOUNCE_EMAIL, REDDIT_URL, + ALERT_SPF, + POSTFIX_PORT_FORWARD, ) from app.dns_utils import get_mx_domains from app.extensions import db @@ -59,6 +63,7 @@ from app.utils import ( convert_to_alphanumeric, sanitize_email, ) +from email_handler import _IP_HEADER def render(template_name, **kwargs) -> str: @@ -990,3 +995,120 @@ def should_disable(alias: Alias) -> bool: def parse_id_from_bounce(email_address: str) -> int: return int(email_address[email_address.find("+") : email_address.rfind("+")]) + + +def spf_pass( + ip: str, + envelope, + mailbox: Mailbox, + user: User, + alias: Alias, + contact_email: str, + msg: Message, +) -> bool: + if ip: + LOG.d("Enforce SPF on %s %s", ip, envelope.mail_from) + try: + r = spf.check2(i=ip, s=envelope.mail_from, h=None) + except Exception: + LOG.exception("SPF error, mailbox %s, ip %s", mailbox.email, ip) + else: + # TODO: Handle temperr case (e.g. dns timeout) + # only an absolute pass, or no SPF policy at all is 'valid' + if r[0] not in ["pass", "none"]: + LOG.w( + "SPF fail for mailbox %s, reason %s, failed IP %s", + mailbox.email, + r[0], + ip, + ) + subject = get_header_unicode(msg["Subject"]) + send_email_with_rate_control( + user, + ALERT_SPF, + mailbox.email, + f"SimpleLogin Alert: attempt to send emails from your alias {alias.email} from unknown IP Address", + render( + "transactional/spf-fail.txt", + alias=alias.email, + ip=ip, + mailbox_url=URL + f"/dashboard/mailbox/{mailbox.id}#spf", + to_email=contact_email, + subject=subject, + time=arrow.now(), + ), + render( + "transactional/spf-fail.html", + ip=ip, + mailbox_url=URL + f"/dashboard/mailbox/{mailbox.id}#spf", + to_email=contact_email, + subject=subject, + time=arrow.now(), + ), + ) + return False + + else: + LOG.w( + "Could not find %s header %s -> %s", + _IP_HEADER, + mailbox.email, + contact_email, + ) + + return True + + +def sl_sendmail( + from_addr, + to_addr, + msg: Message, + mail_options, + rcpt_options, + is_forward: bool, + can_retry=True, +): + """replace smtp.sendmail""" + if NOT_SEND_EMAIL: + LOG.d( + "send email with subject '%s', from '%s' to '%s'", + msg["Subject"], + msg["From"], + msg["To"], + ) + return + + try: + if POSTFIX_SUBMISSION_TLS: + smtp = SMTP(POSTFIX_SERVER, 587) + smtp.starttls() + else: + if is_forward: + smtp = SMTP(POSTFIX_SERVER, POSTFIX_PORT_FORWARD) + else: + smtp = SMTP(POSTFIX_SERVER, POSTFIX_PORT) + + # smtp.send_message has UnicodeEncodeError + # encode message raw directly instead + smtp.sendmail( + from_addr, + to_addr, + to_bytes(msg), + mail_options, + rcpt_options, + ) + except SMTPServerDisconnected: + if can_retry: + LOG.w("SMTPServerDisconnected error, retry") + time.sleep(3) + sl_sendmail( + from_addr, + to_addr, + msg, + mail_options, + rcpt_options, + is_forward, + can_retry=False, + ) + else: + raise diff --git a/email_handler.py b/email_handler.py index 52af49d0..3850107c 100644 --- a/email_handler.py +++ b/email_handler.py @@ -41,11 +41,9 @@ from email.mime.application import MIMEApplication from email.mime.multipart import MIMEMultipart from email.utils import formataddr, make_msgid, formatdate, getaddresses from io import BytesIO -from smtplib import SMTP, SMTPRecipientsRefused, SMTPServerDisconnected +from smtplib import SMTPRecipientsRefused from typing import List, Tuple, Optional -import arrow -import spf from aiosmtpd.controller import Controller from aiosmtpd.smtp import Envelope from sqlalchemy.exc import IntegrityError @@ -54,17 +52,13 @@ from app import pgp_utils, s3 from app.alias_utils import try_auto_create from app.config import ( EMAIL_DOMAIN, - POSTFIX_SERVER, URL, - POSTFIX_SUBMISSION_TLS, UNSUBSCRIBER, LOAD_PGP_EMAIL_HANDLER, ENFORCE_SPF, ALERT_REVERSE_ALIAS_UNKNOWN_MAILBOX, ALERT_BOUNCE_EMAIL, ALERT_SPAM_EMAIL, - ALERT_SPF, - POSTFIX_PORT, SPAMASSASSIN_HOST, MAX_SPAM_SCORE, MAX_REPLY_PHASE_SPAM_SCORE, @@ -78,8 +72,6 @@ from app.config import ( BOUNCE_SUFFIX, TRANSACTIONAL_BOUNCE_PREFIX, TRANSACTIONAL_BOUNCE_SUFFIX, - POSTFIX_PORT_FORWARD, - NOT_SEND_EMAIL, ) from app.email.spam import get_spam_score from app.email_utils import ( @@ -109,6 +101,8 @@ from app.email_utils import ( replace, should_disable, parse_id_from_bounce, + spf_pass, + sl_sendmail, ) from app.extensions import db from app.greylisting import greylisting_needed @@ -1000,68 +994,6 @@ def get_mailbox_from_mail_from(mail_from: str, alias) -> Optional[Mailbox]: return None -def spf_pass( - ip: str, - envelope, - mailbox: Mailbox, - user: User, - alias: Alias, - contact_email: str, - msg: Message, -) -> bool: - if ip: - LOG.d("Enforce SPF on %s %s", ip, envelope.mail_from) - try: - r = spf.check2(i=ip, s=envelope.mail_from, h=None) - except Exception: - LOG.exception("SPF error, mailbox %s, ip %s", mailbox.email, ip) - else: - # TODO: Handle temperr case (e.g. dns timeout) - # only an absolute pass, or no SPF policy at all is 'valid' - if r[0] not in ["pass", "none"]: - LOG.w( - "SPF fail for mailbox %s, reason %s, failed IP %s", - mailbox.email, - r[0], - ip, - ) - subject = get_header_unicode(msg["Subject"]) - send_email_with_rate_control( - user, - ALERT_SPF, - mailbox.email, - f"SimpleLogin Alert: attempt to send emails from your alias {alias.email} from unknown IP Address", - render( - "transactional/spf-fail.txt", - alias=alias.email, - ip=ip, - mailbox_url=URL + f"/dashboard/mailbox/{mailbox.id}#spf", - to_email=contact_email, - subject=subject, - time=arrow.now(), - ), - render( - "transactional/spf-fail.html", - ip=ip, - mailbox_url=URL + f"/dashboard/mailbox/{mailbox.id}#spf", - to_email=contact_email, - subject=subject, - time=arrow.now(), - ), - ) - return False - - else: - LOG.w( - "Could not find %s header %s -> %s", - _IP_HEADER, - mailbox.email, - contact_email, - ) - - return True - - def handle_unknown_mailbox( envelope, msg, reply_email: str, user: User, alias: Alias, contact: Contact ): @@ -1670,61 +1602,6 @@ def handle_bounce(envelope, rcpt_to) -> str: return "550 SL E26 Email cannot be forwarded to mailbox" -def sl_sendmail( - from_addr, - to_addr, - msg: Message, - mail_options, - rcpt_options, - is_forward: bool, - can_retry=True, -): - """replace smtp.sendmail""" - if NOT_SEND_EMAIL: - LOG.d( - "send email with subject '%s', from '%s' to '%s'", - msg["Subject"], - msg["From"], - msg["To"], - ) - return - - try: - if POSTFIX_SUBMISSION_TLS: - smtp = SMTP(POSTFIX_SERVER, 587) - smtp.starttls() - else: - if is_forward: - smtp = SMTP(POSTFIX_SERVER, POSTFIX_PORT_FORWARD) - else: - smtp = SMTP(POSTFIX_SERVER, POSTFIX_PORT) - - # smtp.send_message has UnicodeEncodeError - # encode message raw directly instead - smtp.sendmail( - from_addr, - to_addr, - to_bytes(msg), - mail_options, - rcpt_options, - ) - except SMTPServerDisconnected: - if can_retry: - LOG.w("SMTPServerDisconnected error, retry") - time.sleep(3) - sl_sendmail( - from_addr, - to_addr, - msg, - mail_options, - rcpt_options, - is_forward, - can_retry=False, - ) - else: - raise - - class MailHandler: async def handle_DATA(self, server, session, envelope: Envelope): try: