import time from concurrent.futures import ThreadPoolExecutor from mailbox import Message from smtplib import SMTP, SMTPServerDisconnected, SMTPRecipientsRefused from typing import Optional, Dict import newrelic.agent from attr import dataclass from app.config import ( NOT_SEND_EMAIL, POSTFIX_SUBMISSION_TLS, POSTFIX_PORT, POSTFIX_SERVER, ) from app.email import headers from app.log import LOG from app.message_utils import message_to_bytes @dataclass class SendRequest: envelope_from: str envelope_to: str msg: Message mail_options: Dict = {} rcpt_options: Dict = {} is_forward: bool = False ignore_smtp_errors: bool = False class MailSender: def __init__(self): self._pool: Optional[ThreadPoolExecutor] = None def enable_background_pool(self, max_workers=10): self._pool = ThreadPoolExecutor(max_workers=max_workers) def send(self, send_request: SendRequest, retries: int = 2): """replace smtp.sendmail""" if NOT_SEND_EMAIL: LOG.d( "send email with subject '%s', from '%s' to '%s'", send_request.msg[headers.SUBJECT], send_request.msg[headers.FROM], send_request.msg[headers.TO], ) return if not self._pool: self._send_to_smtp(send_request, retries) else: self._pool.submit(self._send_to_smtp, (send_request, retries)) def _send_to_smtp(self, send_request: SendRequest, retries: int): try: start = time.time() if POSTFIX_SUBMISSION_TLS: smtp_port = 587 else: smtp_port = POSTFIX_PORT with SMTP(POSTFIX_SERVER, smtp_port) as smtp: if POSTFIX_SUBMISSION_TLS: smtp.starttls() elapsed = time.time() - start LOG.d("getting a smtp connection takes seconds %s", elapsed) newrelic.agent.record_custom_metric( "Custom/smtp_connection_time", elapsed ) # smtp.send_message has UnicodeEncodeError # encode message raw directly instead LOG.d( "Sendmail mail_from:%s, rcpt_to:%s, header_from:%s, header_to:%s, header_cc:%s", send_request.envelope_from, send_request.envelope_to, send_request.msg[headers.FROM], send_request.msg[headers.TO], send_request.msg[headers.CC], ) smtp.sendmail( send_request.envelope_from, send_request.envelope_to, message_to_bytes(send_request.msg), send_request.mail_options, send_request.rcpt_options, ) newrelic.agent.record_custom_metric( "Custom/smtp_sending_time", time.time() - start ) except (SMTPServerDisconnected, SMTPRecipientsRefused) as e: if retries > 0: LOG.w( "SMTPServerDisconnected or SMTPRecipientsRefused error %s, retry", e, exc_info=True, ) time.sleep(0.3 * send_request.retries) self._send_to_smtp(send_request, retries - 1) else: if send_request.ignore_smtp_error: LOG.w("Ignore smtp error %s", e) else: raise mail_sender = MailSender() def sl_sendmail( envelope_from: str, envelope_to: str, msg: Message, mail_options=(), rcpt_options=(), is_forward: bool = False, retries=2, ignore_smtp_error=False, ): send_request = SendRequest( envelope_from, envelope_to, msg, mail_options, rcpt_options, is_forward, ignore_smtp_error, ) mail_sender.send(send_request, retries)