move spf_pass(), sl_sendmail() to email_utils.py

This commit is contained in:
Son NK 2021-03-17 10:09:02 +01:00
parent d1d81e6a6d
commit 11789559f1
2 changed files with 126 additions and 127 deletions

View File

@ -5,15 +5,17 @@ import os
import quopri import quopri
import random import random
import re import re
import time
from email.header import decode_header from email.header import decode_header
from email.message import Message from email.message import Message
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText from email.mime.text import MIMEText
from email.utils import make_msgid, formatdate, parseaddr from email.utils import make_msgid, formatdate, parseaddr
from smtplib import SMTP from smtplib import SMTP, SMTPServerDisconnected
import arrow import arrow
import dkim import dkim
import spf
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
from validate_email import validate_email from validate_email import validate_email
@ -38,6 +40,8 @@ from app.config import (
ALERT_DIRECTORY_DISABLED_ALIAS_CREATION, ALERT_DIRECTORY_DISABLED_ALIAS_CREATION,
TRANSACTIONAL_BOUNCE_EMAIL, TRANSACTIONAL_BOUNCE_EMAIL,
REDDIT_URL, REDDIT_URL,
ALERT_SPF,
POSTFIX_PORT_FORWARD,
) )
from app.dns_utils import get_mx_domains from app.dns_utils import get_mx_domains
from app.extensions import db from app.extensions import db
@ -59,6 +63,7 @@ from app.utils import (
convert_to_alphanumeric, convert_to_alphanumeric,
sanitize_email, sanitize_email,
) )
from email_handler import _IP_HEADER
def render(template_name, **kwargs) -> str: 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: def parse_id_from_bounce(email_address: str) -> int:
return int(email_address[email_address.find("+") : email_address.rfind("+")]) 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

View File

@ -41,11 +41,9 @@ from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from email.utils import formataddr, make_msgid, formatdate, getaddresses from email.utils import formataddr, make_msgid, formatdate, getaddresses
from io import BytesIO from io import BytesIO
from smtplib import SMTP, SMTPRecipientsRefused, SMTPServerDisconnected from smtplib import SMTPRecipientsRefused
from typing import List, Tuple, Optional from typing import List, Tuple, Optional
import arrow
import spf
from aiosmtpd.controller import Controller from aiosmtpd.controller import Controller
from aiosmtpd.smtp import Envelope from aiosmtpd.smtp import Envelope
from sqlalchemy.exc import IntegrityError 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.alias_utils import try_auto_create
from app.config import ( from app.config import (
EMAIL_DOMAIN, EMAIL_DOMAIN,
POSTFIX_SERVER,
URL, URL,
POSTFIX_SUBMISSION_TLS,
UNSUBSCRIBER, UNSUBSCRIBER,
LOAD_PGP_EMAIL_HANDLER, LOAD_PGP_EMAIL_HANDLER,
ENFORCE_SPF, ENFORCE_SPF,
ALERT_REVERSE_ALIAS_UNKNOWN_MAILBOX, ALERT_REVERSE_ALIAS_UNKNOWN_MAILBOX,
ALERT_BOUNCE_EMAIL, ALERT_BOUNCE_EMAIL,
ALERT_SPAM_EMAIL, ALERT_SPAM_EMAIL,
ALERT_SPF,
POSTFIX_PORT,
SPAMASSASSIN_HOST, SPAMASSASSIN_HOST,
MAX_SPAM_SCORE, MAX_SPAM_SCORE,
MAX_REPLY_PHASE_SPAM_SCORE, MAX_REPLY_PHASE_SPAM_SCORE,
@ -78,8 +72,6 @@ from app.config import (
BOUNCE_SUFFIX, BOUNCE_SUFFIX,
TRANSACTIONAL_BOUNCE_PREFIX, TRANSACTIONAL_BOUNCE_PREFIX,
TRANSACTIONAL_BOUNCE_SUFFIX, TRANSACTIONAL_BOUNCE_SUFFIX,
POSTFIX_PORT_FORWARD,
NOT_SEND_EMAIL,
) )
from app.email.spam import get_spam_score from app.email.spam import get_spam_score
from app.email_utils import ( from app.email_utils import (
@ -109,6 +101,8 @@ from app.email_utils import (
replace, replace,
should_disable, should_disable,
parse_id_from_bounce, parse_id_from_bounce,
spf_pass,
sl_sendmail,
) )
from app.extensions import db from app.extensions import db
from app.greylisting import greylisting_needed from app.greylisting import greylisting_needed
@ -1000,68 +994,6 @@ def get_mailbox_from_mail_from(mail_from: str, alias) -> Optional[Mailbox]:
return None 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( def handle_unknown_mailbox(
envelope, msg, reply_email: str, user: User, alias: Alias, contact: Contact 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" 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: class MailHandler:
async def handle_DATA(self, server, session, envelope: Envelope): async def handle_DATA(self, server, session, envelope: Envelope):
try: try: