From e71fb2829546eb53c96b87abec0580e87aae7ff7 Mon Sep 17 00:00:00 2001 From: Benjamin Renard Date: Mon, 13 Mar 2023 18:58:20 +0100 Subject: [PATCH] Email: add possibility to easily load templates from a directory --- mylib/email.py | 49 ++++++++++++++++++++-- mylib/scripts/email_templates/test.html | 1 + mylib/scripts/email_templates/test.subject | 1 + mylib/scripts/email_templates/test.txt | 1 + mylib/scripts/email_test.py | 38 +++++++---------- mylib/scripts/email_test_with_config.py | 35 +++++++--------- mylib/scripts/helpers.py | 18 +++++++- 7 files changed, 96 insertions(+), 47 deletions(-) create mode 100644 mylib/scripts/email_templates/test.html create mode 100644 mylib/scripts/email_templates/test.subject create mode 100644 mylib/scripts/email_templates/test.txt diff --git a/mylib/email.py b/mylib/email.py index 1cca112..e90f1d7 100644 --- a/mylib/email.py +++ b/mylib/email.py @@ -46,15 +46,18 @@ class EmailClient( "encoding": "utf-8", "catch_all_addr": None, "just_try": False, + "templates_path": None, } templates = {} - def __init__(self, templates=None, **kwargs): + def __init__(self, templates=None, initialize=False, **kwargs): super().__init__(**kwargs) assert templates is None or isinstance(templates, dict) self.templates = templates if templates else {} + if initialize: + self.initialize() # pylint: disable=arguments-differ,arguments-renamed def configure(self, use_smtp=True, just_try=True, **kwargs): @@ -137,8 +140,40 @@ class EmailClient( comment="Just-try mode: do not really send emails", ) + section.add_option( + StringOption, + "templates_path", + comment="Path to templates directory", + ) + return section + def initialize(self, *args, **kwargs): # pylint: disable=arguments-differ + """Configuration initialized hook""" + super().initialize(*args, **kwargs) + self.load_templates_directory() + + def load_templates_directory(self, templates_path=None): + """Load templates from specified directory""" + if templates_path is None: + templates_path = self._get_option("templates_path") + if not templates_path: + return + log.debug("Load email templates from %s directory", templates_path) + for filename in os.listdir(templates_path): + filepath = os.path.join(templates_path, filename) + if not os.path.isfile(filepath): + continue + template_name, template_type = os.path.splitext(filename) + if template_type not in [".html", ".txt", ".subject"]: + continue + template_type = "text" if template_type == ".txt" else template_type[1:] + if template_name not in self.templates: + self.templates[template_name] = {} + log.debug("Load email template %s %s from %s", template_name, template_type, filepath) + with open(filepath, encoding="utf8") as file_desc: + self.templates[template_name][template_type] = MakoTemplate(file_desc.read()) + def forge_message( self, rcpt_to, @@ -179,7 +214,11 @@ class EmailClient( ) ) if subject: - msg["Subject"] = subject.format(**template_vars) + msg["Subject"] = ( + subject.render(**template_vars) + if isinstance(subject, MakoTemplate) + else subject.format(**template_vars) + ) msg["Date"] = email.utils.formatdate(None, True) encoding = encoding if encoding else self._get_option("encoding") if template: @@ -189,7 +228,11 @@ class EmailClient( assert self.templates[template].get( "subject" ), f"No subject defined in template {template}" - msg["Subject"] = self.templates[template]["subject"].format(**template_vars) + msg["Subject"] = ( + self.templates[template]["subject"].render(**template_vars) + if isinstance(self.templates[template]["subject"], MakoTemplate) + else self.templates[template]["subject"].format(**template_vars) + ) # Put HTML part in last one to prefered it parts = [] diff --git a/mylib/scripts/email_templates/test.html b/mylib/scripts/email_templates/test.html new file mode 100644 index 0000000..f9ba261 --- /dev/null +++ b/mylib/scripts/email_templates/test.html @@ -0,0 +1 @@ +Just a test email. (sent at ${sent_date}) diff --git a/mylib/scripts/email_templates/test.subject b/mylib/scripts/email_templates/test.subject new file mode 100644 index 0000000..4e9552a --- /dev/null +++ b/mylib/scripts/email_templates/test.subject @@ -0,0 +1 @@ +Test email diff --git a/mylib/scripts/email_templates/test.txt b/mylib/scripts/email_templates/test.txt new file mode 100644 index 0000000..b48fd30 --- /dev/null +++ b/mylib/scripts/email_templates/test.txt @@ -0,0 +1 @@ +Just a test email sent at ${sent_date}. diff --git a/mylib/scripts/email_test.py b/mylib/scripts/email_test.py index 44bef4f..542170a 100644 --- a/mylib/scripts/email_test.py +++ b/mylib/scripts/email_test.py @@ -2,10 +2,9 @@ import datetime import getpass import logging +import os import sys -from mako.template import Template as MakoTemplate - from mylib.scripts.helpers import add_email_opts, get_opts_parser, init_email_client, init_logging log = logging.getLogger("mylib.scripts.email_test") @@ -18,7 +17,10 @@ def main(argv=None): # pylint: disable=too-many-locals,too-many-statements # Options parser parser = get_opts_parser(just_try=True) - add_email_opts(parser) + add_email_opts( + parser, + templates_path=os.path.join(os.path.dirname(os.path.realpath(__file__)), "email_templates"), + ) test_opts = parser.add_argument_group("Test email options") @@ -31,6 +33,15 @@ def main(argv=None): # pylint: disable=too-many-locals,too-many-statements help="Test email recipient", ) + test_opts.add_argument( + "-T", + "--template", + action="store_true", + dest="template", + help="Template name to send (default: test)", + default="test", + ) + test_opts.add_argument( "-m", "--mako", @@ -51,26 +62,7 @@ def main(argv=None): # pylint: disable=too-many-locals,too-many-statements if options.email_smtp_user and not options.email_smtp_password: options.email_smtp_password = getpass.getpass("Please enter SMTP password: ") - email_client = init_email_client( - options, - templates=dict( - test=dict( - subject="Test email", - text=( - "Just a test email sent at {sent_date}." - if not options.test_mako - else MakoTemplate("Just a test email sent at ${sent_date}.") - ), - html=( - "Just a test email. (sent at {sent_date})" - if not options.test_mako - else MakoTemplate( - "Just a test email. (sent at ${sent_date})" - ) - ), - ) - ), - ) + 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()): diff --git a/mylib/scripts/email_test_with_config.py b/mylib/scripts/email_test_with_config.py index 9e966d3..b20b336 100644 --- a/mylib/scripts/email_test_with_config.py +++ b/mylib/scripts/email_test_with_config.py @@ -1,10 +1,9 @@ """ Test Email client using mylib.config.Config for configuration """ import datetime import logging +import os import sys -from mako.template import Template as MakoTemplate - from mylib.config import Config from mylib.email import EmailClient @@ -20,6 +19,11 @@ def main(argv=None): # pylint: disable=too-many-locals,too-many-statements email_client = EmailClient(config=config) email_client.configure() + config.set_default( + "email", + "templates_path", + os.path.join(os.path.dirname(os.path.realpath(__file__)), "email_templates"), + ) # Options parser parser = config.get_arguments_parser(description=__doc__) @@ -35,6 +39,15 @@ def main(argv=None): # pylint: disable=too-many-locals,too-many-statements help="Test email recipient", ) + test_opts.add_argument( + "-T", + "--template", + action="store_true", + dest="template", + help="Template name to send (default: test)", + default="test", + ) + test_opts.add_argument( "-m", "--mako", @@ -49,24 +62,6 @@ def main(argv=None): # pylint: disable=too-many-locals,too-many-statements parser.error("You must specify test email recipient using -t/--to parameter") sys.exit(1) - email_client.templates = dict( - test=dict( - subject="Test email", - text=( - "Just a test email sent at {sent_date}." - if not options.test_mako - else MakoTemplate("Just a test email sent at ${sent_date}.") - ), - html=( - "Just a test email. (sent at {sent_date})" - if not options.test_mako - else MakoTemplate( - "Just a test email. (sent at ${sent_date})" - ) - ), - ) - ) - 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()): logging.info("Test email sent") diff --git a/mylib/scripts/helpers.py b/mylib/scripts/helpers.py index e12a00d..e02db2e 100644 --- a/mylib/scripts/helpers.py +++ b/mylib/scripts/helpers.py @@ -87,7 +87,7 @@ def get_opts_parser(desc=None, just_try=False, just_one=False, progress=False, c return parser -def add_email_opts(parser, config=None): +def add_email_opts(parser, config=None, **defaults): """Add email options""" email_opts = parser.add_argument_group("Email options") @@ -103,7 +103,9 @@ def add_email_opts(parser, config=None): sender_name=getpass.getuser(), sender_email=f"{getpass.getuser()}@{socket.gethostname()}", catch_all=False, + templates_path=None, ) + default_config.update(defaults) email_opts.add_argument( "--smtp-host", @@ -220,6 +222,18 @@ def add_email_opts(parser, config=None): default=get_default_opt_value(config, default_config, "catch_all"), ) + email_opts.add_argument( + "--templates-path", + action="store", + type=str, + dest="email_templates_path", + help=( + "Load templates from specify directory " + f'(default: {get_default_opt_value(config, default_config, "templates_path")})' + ), + default=get_default_opt_value(config, default_config, "templates_path"), + ) + def init_email_client(options, **kwargs): """Initialize email client from calling script options""" @@ -239,6 +253,8 @@ def init_email_client(options, **kwargs): catch_all_addr=options.email_catch_all, just_try=options.just_try if hasattr(options, "just_try") else False, encoding=options.email_encoding, + templates_path=options.email_templates_path, + initialize=True, **kwargs, )