move spf_pass(), sl_sendmail() to email_utils.py
This commit is contained in:
parent
d1d81e6a6d
commit
11789559f1
|
@ -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
|
||||||
|
|
129
email_handler.py
129
email_handler.py
|
@ -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:
|
||||||
|
|
Loading…
Reference in New Issue