refactor: put all SMTP statuses into status.py

This commit is contained in:
Son NK 2021-06-23 19:47:06 +02:00
parent 58a1d6e783
commit 6fa267e92b
2 changed files with 80 additions and 40 deletions

39
app/email/status.py Normal file
View File

@ -0,0 +1,39 @@
# 2** status
E200 = "250 Message accepted for delivery"
E201 = "250 SL E201"
E202 = "250 Unsubscribe request accepted"
E203 = "250 SL E203 email can't be sent from a reverse-alias"
E204 = "250 SL E204 ignore"
E205 = "250 SL E205 bounce handled"
# out of office status
E206 = "250 SL E206 Out of office"
# 4** errors
E401 = "421 SL E401 Retry later"
E402 = "421 SL E402 Retry later"
E403 = "421 SL E403 Retry later"
E404 = "421 SL E404 Retry later"
E405 = "421 SL E405 Retry later"
E406 = "421 SL E406 Retry later"
# 5** errors
E501 = "550 SL E501"
E502 = "550 SL E502 Email not exist"
E503 = "550 SL E503"
E504 = "550 SL E504 Account disabled"
E505 = "550 SL E505"
E506 = "550 SL E506 Email detected as spam"
E507 = "550 SL E507 Wrongly formatted subject"
E508 = "550 SL E508 Email not exist"
E509 = "550 SL E509 unauthorized"
E510 = "550 SL E510 so such user"
E511 = "550 SL E511 unsubscribe error"
E512 = "550 SL E512 No such email log"
E513 = "550 SL E513 Email cannot be forwarded to mailbox"
E514 = "550 SL E514 Email sent to noreply address"
E515 = "550 SL E515 Email not exist"
E516 = "550 SL E516 invalid mailbox"
E517 = "550 SL E517 unverified mailbox"
E518 = "550 SL E518 Disabled mailbox"
E519 = "550 SL E519 Email detected as spam"
E520 = "550 SL E24 Email cannot be sent to contact"

View File

@ -75,6 +75,7 @@ from app.config import (
ENABLE_SPAM_ASSASSIN, ENABLE_SPAM_ASSASSIN,
BOUNCE_PREFIX_FOR_REPLY_PHASE, BOUNCE_PREFIX_FOR_REPLY_PHASE,
) )
from app.email import status
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 (
send_email, send_email,
@ -485,13 +486,13 @@ def handle_forward(envelope, msg: Message, rcpt_to: str) -> List[Tuple[bool, str
alias = try_auto_create(address) alias = try_auto_create(address)
if not alias: if not alias:
LOG.d("alias %s cannot be created on-the-fly, return 550", address) LOG.d("alias %s cannot be created on-the-fly, return 550", address)
return [(False, "550 SL E3 Email not exist")] return [(False, status.E515)]
user = alias.user user = alias.user
if user.disabled: if user.disabled:
LOG.w("User %s disabled, disable forwarding emails for %s", user, alias) LOG.w("User %s disabled, disable forwarding emails for %s", user, alias)
return [(False, "550 SL E20 Account disabled")] return [(False, status.E504)]
# mail_from = envelope.mail_from # mail_from = envelope.mail_from
# for mb in alias.mailboxes: # for mb in alias.mailboxes:
@ -518,20 +519,20 @@ def handle_forward(envelope, msg: Message, rcpt_to: str) -> List[Tuple[bool, str
) )
db.session.commit() db.session.commit()
# do not return 5** to allow user to receive emails later when alias is enabled # do not return 5** to allow user to receive emails later when alias is enabled
return [(True, "250 Message accepted for delivery")] return [(True, status.E200)]
ret = [] ret = []
mailboxes = alias.mailboxes mailboxes = alias.mailboxes
# no valid mailbox # no valid mailbox
if not mailboxes: if not mailboxes:
return [(False, "550 SL E16 invalid mailbox")] return [(False, status.E516)]
# no need to create a copy of message # no need to create a copy of message
for mailbox in mailboxes: for mailbox in mailboxes:
if not mailbox.verified: if not mailbox.verified:
LOG.d("%s unverified, do not forward", mailbox) LOG.d("%s unverified, do not forward", mailbox)
ret.append((False, "550 SL E19 unverified mailbox")) ret.append((False, status.E517))
else: else:
# create a copy of message for each forward # create a copy of message for each forward
ret.append( ret.append(
@ -560,7 +561,7 @@ def forward_email_to_mailbox(
if mailbox.disabled: if mailbox.disabled:
LOG.debug("%s disabled, do not forward") LOG.debug("%s disabled, do not forward")
return False, "550 SL E21 Disabled mailbox" return False, status.E518
# sanity check: make sure mailbox is not actually an alias # sanity check: make sure mailbox is not actually an alias
if get_email_domain_part(alias.email) == get_email_domain_part(mailbox.email): if get_email_domain_part(alias.email) == get_email_domain_part(mailbox.email):
@ -593,7 +594,7 @@ def forward_email_to_mailbox(
# retry later # retry later
# so when user fixes the mailbox, the email can be delivered # so when user fixes the mailbox, the email can be delivered
return False, "421 SL E14" return False, status.E405
email_log = EmailLog.create( email_log = EmailLog.create(
contact_id=contact.id, user_id=user.id, mailbox_id=mailbox.id, commit=True contact_id=contact.id, user_id=user.id, mailbox_id=mailbox.id, commit=True
@ -641,7 +642,7 @@ def forward_email_to_mailbox(
db.session.commit() db.session.commit()
handle_spam(contact, alias, msg, user, mailbox, email_log) handle_spam(contact, alias, msg, user, mailbox, email_log)
return False, "550 SL E1 Email detected as spam" return False, status.E519
if contact.invalid_email: if contact.invalid_email:
LOG.d("add noreply information %s %s", alias, mailbox) LOG.d("add noreply information %s %s", alias, mailbox)
@ -684,11 +685,11 @@ def forward_email_to_mailbox(
msg, mailbox.pgp_finger_print, mailbox.pgp_public_key, can_sign=True msg, mailbox.pgp_finger_print, mailbox.pgp_public_key, can_sign=True
) )
except PGPException: except PGPException:
LOG.exception( LOG.e(
"Cannot encrypt message %s -> %s. %s %s", contact, alias, mailbox, user "Cannot encrypt message %s -> %s. %s %s", contact, alias, mailbox, user
) )
# so the client can retry later # so the client can retry later
return False, "421 SL E12 Retry later" return False, status.E406
# add custom header # add custom header
add_or_replace_header(msg, _DIRECTION, "Forward") add_or_replace_header(msg, _DIRECTION, "Forward")
@ -750,10 +751,10 @@ def forward_email_to_mailbox(
alias, alias,
mailbox, mailbox,
) )
return False, "421 SL E17 Retry later" return False, status.E401
else: else:
db.session.commit() db.session.commit()
return True, "250 Message accepted for delivery" return True, status.E200
def handle_reply(envelope, msg: Message, rcpt_to: str) -> (bool, str): def handle_reply(envelope, msg: Message, rcpt_to: str) -> (bool, str):
@ -766,7 +767,7 @@ def handle_reply(envelope, msg: Message, rcpt_to: str) -> (bool, str):
# reply_email must end with EMAIL_DOMAIN # reply_email must end with EMAIL_DOMAIN
if not reply_email.endswith(EMAIL_DOMAIN): if not reply_email.endswith(EMAIL_DOMAIN):
LOG.w(f"Reply email {reply_email} has wrong domain") LOG.w(f"Reply email {reply_email} has wrong domain")
return False, "550 SL E2" return False, status.E501
# handle case where reply email is generated with non-allowed char # handle case where reply email is generated with non-allowed char
reply_email = normalize_reply_email(reply_email) reply_email = normalize_reply_email(reply_email)
@ -774,7 +775,7 @@ def handle_reply(envelope, msg: Message, rcpt_to: str) -> (bool, str):
contact = Contact.get_by(reply_email=reply_email) contact = Contact.get_by(reply_email=reply_email)
if not contact: if not contact:
LOG.w(f"No such forward-email with {reply_email} as reply-email") LOG.w(f"No such forward-email with {reply_email} as reply-email")
return False, "550 SL E4 Email not exist" return False, status.E502
alias = contact.alias alias = contact.alias
address: str = contact.alias.email address: str = contact.alias.email
@ -784,7 +785,7 @@ def handle_reply(envelope, msg: Message, rcpt_to: str) -> (bool, str):
# scenario: a user have removed a domain but due to a bug, the aliases are still there # scenario: a user have removed a domain but due to a bug, the aliases are still there
if not is_valid_alias_address_domain(alias.email): if not is_valid_alias_address_domain(alias.email):
LOG.exception("%s domain isn't known", alias) LOG.exception("%s domain isn't known", alias)
return False, "550 SL E5" return False, status.E503
user = alias.user user = alias.user
mail_from = envelope.mail_from mail_from = envelope.mail_from
@ -796,7 +797,7 @@ def handle_reply(envelope, msg: Message, rcpt_to: str) -> (bool, str):
alias, alias,
contact, contact,
) )
return [(False, "550 SL E20 Account disabled")] return [(False, status.E504)]
# Anti-spoofing # Anti-spoofing
mailbox = get_mailbox_from_mail_from(mail_from, alias) mailbox = get_mailbox_from_mail_from(mail_from, alias)
@ -813,12 +814,13 @@ def handle_reply(envelope, msg: Message, rcpt_to: str) -> (bool, str):
else: else:
# only mailbox can send email to the reply-email # only mailbox can send email to the reply-email
handle_unknown_mailbox(envelope, msg, reply_email, user, alias, contact) handle_unknown_mailbox(envelope, msg, reply_email, user, alias, contact)
return False, "550 SL E7" return False, status.E505
if ENFORCE_SPF and mailbox.force_spf and not alias.disable_email_spoofing_check: if ENFORCE_SPF and mailbox.force_spf and not alias.disable_email_spoofing_check:
if not spf_pass(envelope, mailbox, user, alias, contact.website_email, msg): if not spf_pass(envelope, mailbox, user, alias, contact.website_email, msg):
# cannot use 4** here as sender will retry. 5** because that generates bounce report # cannot use 4** here as sender will retry.
return True, "250 SL E11" # cannot use 5** because that generates bounce report
return True, status.E201
email_log = EmailLog.create( email_log = EmailLog.create(
contact_id=contact.id, contact_id=contact.id,
@ -870,7 +872,7 @@ def handle_reply(envelope, msg: Message, rcpt_to: str) -> (bool, str):
db.session.commit() db.session.commit()
handle_spam(contact, alias, msg, user, mailbox, email_log, is_reply=True) handle_spam(contact, alias, msg, user, mailbox, email_log, is_reply=True)
return False, "550 SL E15 Email detected as spam" return False, status.E506
delete_all_headers_except( delete_all_headers_except(
msg, msg,
@ -907,7 +909,7 @@ def handle_reply(envelope, msg: Message, rcpt_to: str) -> (bool, str):
EmailLog.delete(email_log.id) EmailLog.delete(email_log.id)
db.session.commit() db.session.commit()
# return 421 so the client can retry later # return 421 so the client can retry later
return False, "421 SL E13 Retry later" return False, status.E402
db.session.commit() db.session.commit()
@ -991,8 +993,7 @@ def handle_reply(envelope, msg: Message, rcpt_to: str) -> (bool, str):
) )
# return 250 even if error as user is already informed of the incident and can retry sending the email # return 250 even if error as user is already informed of the incident and can retry sending the email
return True, status.E200
return True, "250 Message accepted for delivery"
def get_mailbox_from_mail_from(mail_from: str, alias) -> Optional[Mailbox]: def get_mailbox_from_mail_from(mail_from: str, alias) -> Optional[Mailbox]:
@ -1262,7 +1263,7 @@ def handle_bounce_reply_phase(envelope, msg: Message, email_log: EmailLog):
refused_email_url=refused_email_url, refused_email_url=refused_email_url,
), ),
) )
return "550 SL E24 Email cannot be sent to contact" return status.E520
def handle_spam( def handle_spam(
@ -1381,11 +1382,11 @@ def handle_unsubscribe(envelope: Envelope, msg: Message) -> str:
alias = Alias.get(alias_id) alias = Alias.get(alias_id)
except Exception: except Exception:
LOG.w("Cannot parse alias from subject %s", msg["Subject"]) LOG.w("Cannot parse alias from subject %s", msg["Subject"])
return "550 SL E8 Wrongly formatted subject" return status.E507
if not alias: if not alias:
LOG.w("No such alias %s", alias_id) LOG.w("No such alias %s", alias_id)
return "550 SL E9 Email not exist" return status.E508
# This sender cannot unsubscribe # This sender cannot unsubscribe
mail_from = envelope.mail_from mail_from = envelope.mail_from
@ -1393,7 +1394,7 @@ def handle_unsubscribe(envelope: Envelope, msg: Message) -> str:
mailbox = get_mailbox_from_mail_from(mail_from, alias) mailbox = get_mailbox_from_mail_from(mail_from, alias)
if not mailbox: if not mailbox:
LOG.d("%s cannot disable alias %s", envelope.mail_from, alias) LOG.d("%s cannot disable alias %s", envelope.mail_from, alias)
return "550 SL E10 unauthorized" return status.E509
# Sender is owner of this alias # Sender is owner of this alias
alias.enabled = False alias.enabled = False
@ -1419,7 +1420,7 @@ def handle_unsubscribe(envelope: Envelope, msg: Message) -> str:
), ),
) )
return "250 Unsubscribe request accepted" return status.E202
def handle_unsubscribe_user(user_id: int, mail_from: str) -> str: def handle_unsubscribe_user(user_id: int, mail_from: str) -> str:
@ -1427,11 +1428,11 @@ def handle_unsubscribe_user(user_id: int, mail_from: str) -> str:
user = User.get(user_id) user = User.get(user_id)
if not user: if not user:
LOG.exception("No such user %s %s", user_id, mail_from) LOG.exception("No such user %s %s", user_id, mail_from)
return "550 SL E22 so such user" return status.E510
if mail_from != user.email: if mail_from != user.email:
LOG.exception("Unauthorized mail_from %s %s", user, mail_from) LOG.exception("Unauthorized mail_from %s %s", user, mail_from)
return "550 SL E23 unsubscribe error" return status.E511
user.notification = False user.notification = False
db.session.commit() db.session.commit()
@ -1449,7 +1450,7 @@ def handle_unsubscribe_user(user_id: int, mail_from: str) -> str:
), ),
) )
return "250 Unsubscribe request accepted" return status.E202
def handle_transactional_bounce(envelope: Envelope, rcpt_to): def handle_transactional_bounce(envelope: Envelope, rcpt_to):
@ -1472,7 +1473,7 @@ def handle_bounce(envelope, email_log: EmailLog, msg: Message) -> str:
if not email_log: if not email_log:
LOG.w("No such email log") LOG.w("No such email log")
return "550 SL E27 No such email log" return status.E512
contact: Contact = email_log.contact contact: Contact = email_log.contact
alias = contact.alias alias = contact.alias
@ -1524,7 +1525,7 @@ def handle_bounce(envelope, email_log: EmailLog, msg: Message) -> str:
return handle_bounce_reply_phase(envelope, msg, email_log) return handle_bounce_reply_phase(envelope, msg, email_log)
else: # forward phase else: # forward phase
handle_bounce_forward_phase(msg, email_log) handle_bounce_forward_phase(msg, email_log)
return "550 SL E26 Email cannot be forwarded to mailbox" return status.E513
def should_ignore(mail_from: str, rcpt_tos: List[str]) -> bool: def should_ignore(mail_from: str, rcpt_tos: List[str]) -> bool:
@ -1556,7 +1557,7 @@ def handle(envelope: Envelope) -> str:
if should_ignore(mail_from, rcpt_tos): if should_ignore(mail_from, rcpt_tos):
LOG.e("Ignore email mail_from=%s rcpt_to=%s", mail_from, rcpt_tos) LOG.e("Ignore email mail_from=%s rcpt_to=%s", mail_from, rcpt_tos)
return "250 email can't be sent from a reverse-alias" return status.E204
# sanitize email headers # sanitize email headers
sanitize_header(msg, "from") sanitize_header(msg, "from")
@ -1584,7 +1585,7 @@ def handle(envelope: Envelope) -> str:
contact.alias, contact.alias,
contact.website_email, contact.website_email,
) )
return "250 email can't be sent from a reverse-alias" return status.E203
# unsubscribe request # unsubscribe request
if UNSUBSCRIBER and rcpt_tos == [UNSUBSCRIBER]: if UNSUBSCRIBER and rcpt_tos == [UNSUBSCRIBER]:
@ -1599,7 +1600,7 @@ def handle(envelope: Envelope) -> str:
): ):
LOG.d("Handle email sent to sender from %s", mail_from) LOG.d("Handle email sent to sender from %s", mail_from)
handle_transactional_bounce(envelope, rcpt_tos[0]) handle_transactional_bounce(envelope, rcpt_tos[0])
return "250 bounce handled" return status.E205
# Handle bounce # Handle bounce
if ( if (
@ -1638,7 +1639,7 @@ def handle(envelope: Envelope) -> str:
# Whether it's necessary to apply greylisting # Whether it's necessary to apply greylisting
if greylisting_needed(mail_from, rcpt_tos): if greylisting_needed(mail_from, rcpt_tos):
LOG.w("Grey listing applied for mail_from:%s rcpt_tos:%s", mail_from, rcpt_tos) LOG.w("Grey listing applied for mail_from:%s rcpt_tos:%s", mail_from, rcpt_tos)
return "421 SL Retry later" return status.E403
# Handle "out of office" auto notice. An automatic response is sent for every forwarded email # Handle "out of office" auto notice. An automatic response is sent for every forwarded email
# todo: remove logging # todo: remove logging
@ -1646,7 +1647,7 @@ def handle(envelope: Envelope) -> str:
LOG.w( LOG.w(
"out-of-office email to reverse alias %s. %s", rcpt_tos[0], msg.as_string() "out-of-office email to reverse alias %s. %s", rcpt_tos[0], msg.as_string()
) )
return "250 SL E28" return status.E206
# result of all deliveries # result of all deliveries
# each element is a couple of whether the delivery is successful and the smtp status # each element is a couple of whether the delivery is successful and the smtp status
@ -1656,7 +1657,7 @@ def handle(envelope: Envelope) -> str:
for rcpt_index, rcpt_to in enumerate(rcpt_tos): for rcpt_index, rcpt_to in enumerate(rcpt_tos):
if rcpt_to == NOREPLY: if rcpt_to == NOREPLY:
LOG.e("email sent to noreply address from %s", mail_from) LOG.e("email sent to noreply address from %s", mail_from)
return "550 SL E25 Email sent to noreply address" return status.E514
# create a copy of msg for each recipient except the last one # create a copy of msg for each recipient except the last one
# as copy() is a slow function # as copy() is a slow function
@ -1704,7 +1705,7 @@ class MailHandler:
envelope.mail_from, envelope.mail_from,
envelope.rcpt_tos, envelope.rcpt_tos,
) )
return "421 SL Retry later" return status.E404
def _handle(self, envelope: Envelope): def _handle(self, envelope: Envelope):
start = time.time() start = time.time()