Improve delivery error-handling
- Introduce exceptions to be raised upon transient and permanent delivery failures, as specified by SMTP RFC. Depending on type of failure, return either 451 or 554 reply code. - When serialising a message, treat ValueError as a serialisation issue (and try again to deliver in cleartext).
This commit is contained in:
parent
8955cf6c9d
commit
09d7a498df
5 changed files with 62 additions and 16 deletions
|
@ -6,6 +6,7 @@ configuration.
|
|||
|
||||
from enum import Enum, auto
|
||||
from configparser import RawConfigParser
|
||||
from collections import namedtuple
|
||||
|
||||
import os
|
||||
|
||||
|
@ -128,9 +129,11 @@ def validate_config(*, additional=None):
|
|||
# High level access to configuration.
|
||||
#
|
||||
|
||||
def relay_params():
|
||||
"""Return a (HOST, PORT) tuple identifying the mail relay."""
|
||||
return (cfg["relay"]["host"], int(cfg["relay"]["port"]))
|
||||
Host = namedtuple('Host', ['name', 'port'])
|
||||
|
||||
def relay_params() -> Host:
|
||||
"""Return a Host named tuple identifying the mail relay."""
|
||||
return Host(name = cfg["relay"]["host"], port = int(cfg["relay"]["port"]))
|
||||
|
||||
|
||||
def daemon_params():
|
||||
|
|
|
@ -55,11 +55,19 @@ class MailEncryptionProxy:
|
|||
LOG.error('Unable to encrypt message, delivering in cleartext: %s', e)
|
||||
self._send_unencrypted(operation, envelope, send)
|
||||
|
||||
except:
|
||||
LOG.exception('Unexpected exception caught, bouncing message')
|
||||
except xport.TransientFailure:
|
||||
LOG.info('Bouncing message')
|
||||
return xport.RESULT_ABORT
|
||||
|
||||
except xport.PermanentFailure:
|
||||
LOG.exception('Permanent failure')
|
||||
return xport.RESULT_PERM_FAIL
|
||||
|
||||
except:
|
||||
if conf.should_log_headers():
|
||||
LOG.error('Erroneous message headers: %s', self._beginning(envelope))
|
||||
LOG.exception('Unexpected exception caught, bouncing message. Erroneous message headers: %s', self._beginning(envelope))
|
||||
else:
|
||||
LOG.exception('Unexpected exception caught, bouncing message')
|
||||
|
||||
return xport.RESULT_ABORT
|
||||
|
||||
|
|
|
@ -131,7 +131,7 @@ class KeepIntact(MailOperation):
|
|||
"""Return MESSAGE unmodified."""
|
||||
try:
|
||||
return message.as_bytes(policy=SMTPUTF8)
|
||||
except (IndexError, UnicodeEncodeError) as e:
|
||||
except (IndexError, UnicodeEncodeError, ValueError) as e:
|
||||
raise MailSerialisationException(e)
|
||||
|
||||
def __repr__(self):
|
||||
|
|
|
@ -45,8 +45,8 @@ def notify(mailsubject, messagefile, recipients = None):
|
|||
msg.attach(MIMEText(markdown.markdown(mailbody), 'html'))
|
||||
|
||||
if conf.config_item_set('relay', 'host') and conf.config_item_set('relay', 'enc_port'):
|
||||
(host, port) = conf.relay_params()
|
||||
smtp = smtplib.SMTP(host, port)
|
||||
host = conf.relay_params()
|
||||
smtp = smtplib.SMTP(host.name, host.port)
|
||||
_authenticate_maybe(smtp)
|
||||
LOG.info('Delivering notification: %s', recipients)
|
||||
smtp.sendmail(conf.get_item('cron', 'notification_email'), recipients, msg.as_string())
|
||||
|
|
|
@ -5,13 +5,19 @@ import logging
|
|||
from typing import AnyStr, List
|
||||
|
||||
import lacre.config as conf
|
||||
from lacre.mailop import MailSerialisationException
|
||||
|
||||
# Mail status constants.
|
||||
#
|
||||
# These are the only values that our mail handler is allowed to return.
|
||||
RESULT_OK = '250 OK' # delivered
|
||||
RESULT_ABORT = '451 Aborted: error in processing' # aborted, retry later
|
||||
RESULT_FAILED = '554 Transaction failed' # failed
|
||||
RESULT_OK = '250 OK'
|
||||
RESULT_TRANS_FAIL = '451 Aborted: error in processing'
|
||||
RESULT_PERM_FAIL = '554 Transaction failed'
|
||||
|
||||
# See RFC 5321, section 4.2.1 "Reply Code Severities and Theory" for more
|
||||
# information on SMTP reply codes.
|
||||
RESP_TRANSIENT_NEG = 4
|
||||
RESP_PERMANENT_NEG = 5
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
@ -35,7 +41,7 @@ def send_msg(message: AnyStr, recipients: List[str]):
|
|||
if recipients:
|
||||
LOG.info(f"Sending email to: {recipients!r}")
|
||||
relay = conf.relay_params()
|
||||
smtp = smtplib.SMTP(relay[0], relay[1])
|
||||
smtp = smtplib.SMTP(relay.name, relay.port)
|
||||
if conf.flag_enabled('relay', 'starttls'):
|
||||
smtp.starttls()
|
||||
smtp.sendmail(from_addr, recipients, message)
|
||||
|
@ -43,6 +49,19 @@ def send_msg(message: AnyStr, recipients: List[str]):
|
|||
LOG.info("No recipient found")
|
||||
|
||||
|
||||
class TransientFailure(BaseException):
|
||||
"""Signals a transient delivery failure (4xx SMTP reply).
|
||||
|
||||
Message should be bounced and re-sent later.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class PermanentFailure(BaseException):
|
||||
"""Signals a permanent delivery failure (5xx SMTP reply)."""
|
||||
pass
|
||||
|
||||
|
||||
class SendFrom:
|
||||
"""A class wrapping the transport process."""
|
||||
|
||||
|
@ -64,12 +83,28 @@ class SendFrom:
|
|||
|
||||
LOG.info("Sending email to: %s", recipients)
|
||||
relay = conf.relay_params()
|
||||
smtp = smtplib.SMTP(relay[0], relay[1])
|
||||
smtp = smtplib.SMTP(relay.name, relay.port)
|
||||
|
||||
if conf.flag_enabled('relay', 'starttls'):
|
||||
smtp.starttls()
|
||||
|
||||
try:
|
||||
smtp.sendmail(self._from_addr, recipients, message)
|
||||
except smtplib.SMTPException:
|
||||
LOG.exception('Failed to deliver message')
|
||||
except smtplib.SMTPResponseException as re:
|
||||
resp_class = self._get_class(re.smtp_code)
|
||||
|
||||
if resp_class == RESP_TRANSIENT_NEG:
|
||||
LOG.warning('Transient delivery failure: %s', re)
|
||||
raise TransientFailure()
|
||||
elif resp_class == RESP_PERMANENT_NEG:
|
||||
LOG.error('Permanent delivery failure: %s', re)
|
||||
raise PermanentFailure()
|
||||
except smtplib.SMTPException as err:
|
||||
LOG.error('Failed to deliver message: %s', err)
|
||||
raise PermanentFailure()
|
||||
except UnicodeEncodeError as uee:
|
||||
LOG.error('Failed to deliver for non-SMTP reason', uee)
|
||||
raise MailSerialisationException(uee)
|
||||
|
||||
def _get_class(self, resp_code):
|
||||
return int(resp_code / 100)
|
||||
|
|
Loading…
Reference in a new issue