Compare commits

...

2 commits

Author SHA1 Message Date
Benjamin Renard
845af9c367
Email: add support for CC & BCC recipients 2023-03-14 17:00:31 +01:00
Benjamin Renard
c93b3508ed
Email: add possibility to specify more than one recipient 2023-03-14 16:55:36 +01:00
4 changed files with 130 additions and 25 deletions

View file

@ -8,6 +8,8 @@ disable=invalid-name,
too-many-nested-blocks,
too-many-instance-attributes,
too-many-lines,
too-many-statements,
logging-too-many-args,
duplicate-code,
[FORMAT]

View file

@ -177,7 +177,7 @@ class EmailClient(
def forge_message(
self,
rcpt_to,
recipients,
subject=None,
html_body=None,
text_body=None, # pylint: disable=too-many-arguments,too-many-locals
@ -187,13 +187,14 @@ class EmailClient(
sender_email=None,
encoding=None,
template=None,
cc=None,
**template_vars,
):
"""
Forge a message
:param rcpt_to: The recipient of the email. Could be a tuple(name, email) or
just the email of the recipient.
:param recipients: The recipient(s) of the email. List of tuple(name, email) or
just the email of the recipients.
:param subject: The subject of the email.
:param html_body: The HTML body of the email
:param text_body: The plain text body of the email
@ -203,11 +204,29 @@ class EmailClient(
:param sender_email: Custom sender email (default: as defined on initialization)
:param encoding: Email content encoding (default: as defined on initialization)
:param template: The name of a template to use to forge this email
:param cc: Optional list of CC recipient addresses.
List of tuple(name, email) or just the email of the recipients.
All other parameters will be consider as template variables.
"""
recipients = [recipients] if not isinstance(recipients, list) else recipients
msg = MIMEMultipart("alternative")
msg["To"] = email.utils.formataddr(rcpt_to) if isinstance(rcpt_to, tuple) else rcpt_to
msg["To"] = ", ".join(
[
email.utils.formataddr(recipient) if isinstance(recipient, tuple) else recipient
for recipient in recipients
]
)
if cc:
cc = [cc] if not isinstance(cc, list) else cc
msg["Cc"] = ", ".join(
[
email.utils.formataddr(recipient) if isinstance(recipient, tuple) else recipient
for recipient in cc
]
)
msg["From"] = email.utils.formataddr(
(
sender_name or self._get_option("sender_name"),
@ -280,36 +299,63 @@ class EmailClient(
msg.attach(part)
return msg
def send(self, rcpt_to, msg=None, subject=None, just_try=False, **forge_args):
def send(
self, recipients, msg=None, subject=None, just_try=False, cc=None, bcc=None, **forge_args
):
"""
Send an email
:param rcpt_to: The recipient of the email. Could be a tuple(name, email)
or just the email of the recipient.
:param recipients: The recipient(s) of the email. List of tuple(name, email) or
just the email of the recipients.
:param msg: The message of this email (as MIMEBase or derivated classes)
:param subject: The subject of the email (only if the message is not provided
using msg parameter)
:param just_try: Enable just try mode (do not really send email, default: as defined on
initialization)
:param cc: Optional list of CC recipient addresses. List of tuple(name, email) or
just the email of the recipients.
:param bcc: Optional list of BCC recipient addresses. List of tuple(name, email) or
just the email of the recipients.
All other parameters will be consider as parameters to forge the message
(only if the message is not provided using msg parameter).
"""
msg = msg if msg else self.forge_message(rcpt_to, subject, **forge_args)
recipients = [recipients] if not isinstance(recipients, list) else recipients
msg = msg if msg else self.forge_message(recipients, subject, cc=cc, **forge_args)
catch_addr = self._get_option("catch_all_addr")
if catch_addr:
log.debug(
"Catch email originaly send to %s (CC:%s, BCC:%s) to %s",
", ".join(recipients),
", ".join(cc) if isinstance(cc, list) else cc,
", ".join(bcc) if isinstance(cc, list) else bcc,
catch_addr,
)
recipients = catch_addr if isinstance(catch_addr, list) else [catch_addr]
else:
if cc:
recipients.extend(
[
recipient[1] if isinstance(recipient, tuple) else recipient
for recipient in (cc if isinstance(cc, list) else [cc])
]
)
if bcc:
recipients.extend(
[
recipient[1] if isinstance(recipient, tuple) else recipient
for recipient in (bcc if isinstance(bcc, list) else [bcc])
]
)
if just_try or self._get_option("just_try"):
log.debug(
'Just-try mode: do not really send this email to %s (subject="%s")',
rcpt_to,
", ".join(recipients),
subject or msg.get("subject", "No subject"),
)
return True
catch_addr = self._get_option("catch_all_addr")
if catch_addr:
log.debug("Catch email originaly send to %s to %s", rcpt_to, catch_addr)
rcpt_to = catch_addr
smtp_host = self._get_option("smtp_host")
smtp_port = self._get_option("smtp_port")
try:
@ -347,15 +393,18 @@ class EmailClient(
error = False
try:
log.info("Sending email to %s", rcpt_to)
log.info("Sending email to %s", ", ".join(recipients))
server.sendmail(
self._get_option("sender_email"),
[rcpt_to[1] if isinstance(rcpt_to, tuple) else rcpt_to],
[
recipient[1] if isinstance(recipient, tuple) else recipient
for recipient in recipients
],
msg.as_string(),
)
except smtplib.SMTPException:
error = True
log.error("Error sending email to %s", rcpt_to, exc_info=True)
log.error("Error sending email to %s", ", ".join(recipients), exc_info=True)
finally:
server.quit()

View file

@ -30,7 +30,8 @@ def main(argv=None): # pylint: disable=too-many-locals,too-many-statements
action="store",
type=str,
dest="test_to",
help="Test email recipient",
help="Test email recipient(s)",
nargs="+",
)
test_opts.add_argument(
@ -50,10 +51,28 @@ def main(argv=None): # pylint: disable=too-many-locals,too-many-statements
help="Test mako templating",
)
test_opts.add_argument(
"--cc",
action="store",
type=str,
dest="test_cc",
help="Test CC email recipient(s)",
nargs="+",
)
test_opts.add_argument(
"--bcc",
action="store",
type=str,
dest="test_bcc",
help="Test BCC email recipient(s)",
nargs="+",
)
options = parser.parse_args()
if not options.test_to:
parser.error("You must specify test email recipient using -t/--to parameter")
parser.error("You must specify at least one test email recipient using -t/--to parameter")
sys.exit(1)
# Initialize logs
@ -64,8 +83,19 @@ def main(argv=None): # pylint: disable=too-many-locals,too-many-statements
email_client = init_email_client(options)
log.info("Send a test email to %s", options.test_to)
if email_client.send(options.test_to, template="test", sent_date=datetime.datetime.now()):
log.info(
"Send a test email to %s (CC: %s / BCC: %s)",
", ".join(options.test_to),
", ".join(options.test_cc) if options.test_cc else None,
", ".join(options.test_bcc) if options.test_bcc else None,
)
if email_client.send(
options.test_to,
cc=options.test_cc,
bcc=options.test_bcc,
template="test",
sent_date=datetime.datetime.now(),
):
log.info("Test email sent")
sys.exit(0)
log.error("Fail to send test email")

View file

@ -35,7 +35,8 @@ def main(argv=None): # pylint: disable=too-many-locals,too-many-statements
action="store",
type=str,
dest="test_to",
help="Test email recipient",
help="Test email recipient(s)",
nargs="+",
)
test_opts.add_argument(
@ -55,14 +56,37 @@ def main(argv=None): # pylint: disable=too-many-locals,too-many-statements
help="Test mako templating",
)
test_opts.add_argument(
"--cc",
action="store",
type=str,
dest="test_cc",
help="Test CC email recipient(s)",
nargs="+",
)
test_opts.add_argument(
"--bcc",
action="store",
type=str,
dest="test_bcc",
help="Test BCC email recipient(s)",
nargs="+",
)
options = config.parse_arguments_options()
if not options.test_to:
parser.error("You must specify test email recipient using -t/--to parameter")
parser.error("You must specify at least one test email recipient using -t/--to parameter")
sys.exit(1)
logging.info("Send a test email to %s", options.test_to)
if email_client.send(options.test_to, template="test", sent_date=datetime.datetime.now()):
if email_client.send(
options.test_to,
cc=options.test_cc,
bcc=options.test_bcc,
template="test",
sent_date=datetime.datetime.now(),
):
logging.info("Test email sent")
sys.exit(0)
logging.error("Fail to send test email")