Skip to content

Commit

Permalink
Merge pull request #73 from TeskaLabs/Enhancement/Add-retry-SMTP
Browse files Browse the repository at this point in the history
Enhancement: SMTP retry mechanism introduction
  • Loading branch information
mithunbharadwaj authored May 21, 2024
2 parents aba4394 + db8e436 commit 3a87f4b
Showing 1 changed file with 108 additions and 83 deletions.
191 changes: 108 additions & 83 deletions asabiris/output/smtp/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import re

import asab
import asyncio
import aiosmtplib

from ...output_abc import OutputABC
Expand Down Expand Up @@ -56,7 +57,6 @@ def __init__(self, app, service_name="SmtpService", config_section_name='smtp'):
self.SenderNameEmailRegex = re.compile(r"<([^<>]*)>\s*([^<>]*)")
self.AngelBracketRegex = re.compile(r".*<.*>.*")


async def send(
self, *,
email_to,
Expand All @@ -81,9 +81,12 @@ async def send(
:param cc: A list of Cc email addresses.
:param bcc: A list of Bcc email addresses.
"""
if email_cc is None:
email_cc = []
if email_bcc is None:
email_bcc = []

# Prepare Message

msg = email.message.EmailMessage()
msg.set_content(body, subtype='html')

Expand All @@ -96,14 +99,13 @@ async def send(
msg['To'] = ', '.join(formatted_email_to)

if email_cc is not None:
assert isinstance(email_to, list)
assert isinstance(email_cc, list)
formatted_email_cc = []
for email_address in email_cc:
formatted_sender, sender_email = self.format_sender_info(email_address)
formatted_email_cc.append(sender_email)
msg['Cc'] = ', '.join(formatted_email_cc)


if email_subject is not None and len(email_subject) > 0:
msg['Subject'] = email_subject
else:
Expand All @@ -127,85 +129,108 @@ async def send(
filename=attachment.FileName
)

# Send the result over SMTP
try:
result = await aiosmtplib.send(
msg,
sender=sender,
recipients=email_to + email_cc + email_bcc,
hostname=self.Host,
port=int(self.Port) if self.Port != "" else None,
username=self.User,
password=self.Password,
use_tls=self.SSL,
start_tls=self.StartTLS
)
except aiosmtplib.errors.SMTPConnectError as e:
L.warning("Connection failed: {}".format(e), struct_data={"host": self.Host, "port": self.Port})
raise ASABIrisError(
ErrorCode.SMTP_CONNECTION_ERROR,
tech_message="SMTP connection failed: {}.".format(str(e)),
error_i18n_key="Could not connect to SMTP host '{{host}}'.",
error_dict={
"host": self.Host,
}
)
except aiosmtplib.errors.SMTPAuthenticationError as e:
L.warning("SMTP error: {}".format(e), struct_data={"host": self.Host})
raise ASABIrisError(
ErrorCode.SMTP_AUTHENTICATION_ERROR,
tech_message="SMTP authentication error: {}.".format(str(e)),
error_i18n_key="SMTP authentication failed for host '{{host}}'.",
error_dict={
"host": self.Host
}
)
except aiosmtplib.errors.SMTPResponseException as e:
L.warning("SMTP Error", struct_data={"message": e.message, "code": e.code, "host": self.Host})
raise ASABIrisError(
ErrorCode.SMTP_RESPONSE_ERROR,
tech_message="SMTP response exception: Code {}, Message '{}'.".format(e.code, e.message),
error_i18n_key="SMTP response issue encountered for '{{host}}': Code '{{code}}', Message '{{message}}'.",
error_dict={
"message": e.message,
"code": e.code,
"host": self.Host
}
)
except aiosmtplib.errors.SMTPServerDisconnected as e:
L.warning("Server disconnected: {}; check the SMTP credentials".format(e), struct_data={"host": self.Host})
raise ASABIrisError(
ErrorCode.SMTP_SERVER_DISCONNECTED,
tech_message="SMTP server disconnected: {}.".format(str(e)),
error_i18n_key="The SMTP server '{{host}}' disconnected unexpectedly.",
error_dict={
"host": self.Host
}
)
except aiosmtplib.errors.SMTPTimeoutError as e:
L.warning("SMTP timeout encountered: {}; check network connectivity or SMTP server status".format(e), struct_data={"host": self.Host})
raise ASABIrisError(
ErrorCode.SMTP_TIMEOUT, # Make sure to define this error code appropriately
tech_message="SMTP timeout encountered: {}.".format(str(e)),
error_i18n_key="The SMTP server for '{{host}}' timed out unexpectedly.",
error_dict={
"host": self.Host
}
)

except Exception as e:
L.warning("SMTP error: {}; check credentials".format(e), struct_data={"host": self.Host})
raise ASABIrisError(
ErrorCode.SMTP_GENERIC_ERROR,
tech_message="Generic error occurred: {}.".format(str(e)),
error_i18n_key="A generic SMTP error occurred for host '{{host}}'.",
error_dict={
"host": self.Host
}
)

L.log(asab.LOG_NOTICE, "Email sent", struct_data={'result': result[1], "host": self.Host})

# Send the email with retry logic
retry_attempts = 3
delay = 5 # seconds

for attempt in range(retry_attempts):
try:
result = await aiosmtplib.send(
msg,
sender=sender,
recipients=email_to + email_cc + email_bcc,
hostname=self.Host,
port=int(self.Port) if self.Port != "" else None,
username=self.User,
password=self.Password,
use_tls=self.SSL,
start_tls=self.StartTLS
)
L.log(asab.LOG_NOTICE, "Email sent", struct_data={'result': result[1], "host": self.Host})
break # Email sent successfully, exit the retry loop

except aiosmtplib.errors.SMTPConnectError as e:
L.warning("Connection failed: {}".format(e), struct_data={"host": self.Host, "port": self.Port})
if attempt < retry_attempts - 1:
L.info("Retrying email send after connection failure, attempt {}".format(attempt + 1))
await asyncio.sleep(delay)
continue # Retry the email sending
raise ASABIrisError(
ErrorCode.SMTP_CONNECTION_ERROR,
tech_message="SMTP connection failed: {}.".format(str(e)),
error_i18n_key="Could not connect to SMTP for host '{{host}}'.",
error_dict={
"host": self.Host,
}
)
except aiosmtplib.errors.SMTPAuthenticationError as e:
L.warning("SMTP error: {}".format(e), struct_data={"host": self.Host})
raise ASABIrisError(
ErrorCode.SMTP_AUTHENTICATION_ERROR,
tech_message="SMTP authentication error: {}.".format(str(e)),
error_i18n_key="SMTP authentication failed for host '{{host}}'.",
error_dict={
"host": self.Host
}
)
except aiosmtplib.errors.SMTPResponseException as e:
L.warning("SMTP Error", struct_data={"message": e.message, "code": e.code, "host": self.Host})
if attempt < retry_attempts - 1:
L.info("Retrying email send after connection failure, attempt {}".format(attempt + 1))
await asyncio.sleep(delay)
continue # Retry the email sending
raise ASABIrisError(
ErrorCode.SMTP_RESPONSE_ERROR,
tech_message="SMTP response exception: Code {}, Message '{}'.".format(e.code, e.message),
error_i18n_key="SMTP response issue encountered for '{{host}}': Code '{{code}}', Message '{{message}}'.",
error_dict={
"message": e.message,
"code": e.code,
"host": self.Host
}
)
except aiosmtplib.errors.SMTPServerDisconnected as e:
L.warning("Server disconnected: {}; check the SMTP credentials".format(e), struct_data={"host": self.Host})
if attempt < retry_attempts - 1:
L.info("Retrying email send after connection failure, attempt {}".format(attempt + 1))
await asyncio.sleep(delay)
continue # Retry the email sending
raise ASABIrisError(
ErrorCode.SMTP_SERVER_DISCONNECTED,
tech_message="SMTP server disconnected: {}.".format(str(e)),
error_i18n_key="The SMTP server for '{{host}}' disconnected unexpectedly.",
error_dict={
"host": self.Host
}
)
except aiosmtplib.errors.SMTPTimeoutError as e:
L.warning("SMTP timeout encountered: {}; check network connectivity or SMTP server status".format(e), struct_data={"host": self.Host})
if attempt < retry_attempts - 1:
L.info("Retrying email send after connection failure, attempt {}".format(attempt + 1))
await asyncio.sleep(delay)
continue # Retry the email sending
raise ASABIrisError(
ErrorCode.SMTP_TIMEOUT,
tech_message="SMTP timeout encountered: {}.".format(str(e)),
error_i18n_key="The SMTP server for '{{host}}' timed out unexpectedly.",
error_dict={
"host": self.Host
}
)
except Exception as e:
L.warning("SMTP error: {}; check credentials".format(e), struct_data={"host": self.Host})
if attempt < retry_attempts - 1:
L.info("Retrying email send after connection failure, attempt {}".format(attempt + 1))
await asyncio.sleep(delay)
continue # Retry the email sending
raise ASABIrisError(
ErrorCode.SMTP_GENERIC_ERROR,
tech_message="Generic error occurred: {}.".format(str(e)),
error_i18n_key="A generic SMTP error occurred for host '{{host}}'.",
error_dict={
"host": self.Host
}
)

def format_sender_info(self, email_info):
"""
Expand Down

0 comments on commit 3a87f4b

Please sign in to comment.