Compare commits

..

2 commits

Author SHA1 Message Date
Benjamin Renard
601857a2d4
email: remove duplicated in file tester entrypoint
Some checks failed
Run tests / tests (push) Failing after 1m30s
2024-06-12 14:04:11 +02:00
Benjamin Renard
4c51d0086f
email: add load_image_as_base64 helper function 2024-06-12 14:03:09 +02:00
5 changed files with 94 additions and 198 deletions

View file

@ -1,5 +1,6 @@
""" Email client to forge and send emails """ """ Email client to forge and send emails """
import base64
import email.utils import email.utils
import logging import logging
import os import os
@ -9,6 +10,7 @@ from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText from email.mime.text import MIMEText
import magic
from mako.template import Template as MakoTemplate from mako.template import Template as MakoTemplate
from mylib.config import ( from mylib.config import (
@ -22,6 +24,14 @@ from mylib.config import (
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def load_image_as_base64(path):
"""Load image file as base64"""
log.debug("Load image file '%s'", path)
with open(path, "rb") as file_desc:
data = file_desc.read()
return f"data:{magic.from_buffer(data, mime=True)};base64, {base64.b64encode(data).decode()}"
class EmailClient( class EmailClient(
ConfigurableObject ConfigurableObject
): # pylint: disable=useless-object-inheritance,too-many-instance-attributes ): # pylint: disable=useless-object-inheritance,too-many-instance-attributes
@ -165,7 +175,7 @@ class EmailClient(
continue continue
template_type = "text" if template_type == ".txt" else template_type[1:] template_type = "text" if template_type == ".txt" else template_type[1:]
if template_name not in self.templates: if template_name not in self.templates:
self.templates[template_name] = {} self.templates[template_name] = {"path": templates_path}
log.debug("Load email template %s %s from %s", template_name, template_type, filepath) log.debug("Load email template %s %s from %s", template_name, template_type, filepath)
with open(filepath, encoding="utf8") as file_desc: with open(filepath, encoding="utf8") as file_desc:
self.templates[template_name][template_type] = MakoTemplate( self.templates[template_name][template_type] = MakoTemplate(
@ -239,6 +249,7 @@ class EmailClient(
msg["Date"] = email.utils.formatdate(None, True) msg["Date"] = email.utils.formatdate(None, True)
encoding = encoding if encoding else self._get_option("encoding") encoding = encoding if encoding else self._get_option("encoding")
if template: if template:
log.debug("Forge email from template %s", template)
assert template in self.templates, f"Unknown template {template}" assert template in self.templates, f"Unknown template {template}"
# Handle subject from template # Handle subject from template
if not subject: if not subject:
@ -264,6 +275,9 @@ class EmailClient(
) )
if self.templates[template].get("html"): if self.templates[template].get("html"):
if isinstance(self.templates[template]["html"], MakoTemplate): if isinstance(self.templates[template]["html"], MakoTemplate):
template_vars["load_image_as_base64"] = self.template_image_loader(
self.templates[template].get("path")
)
parts.append((self.templates[template]["html"].render(**template_vars), "html")) parts.append((self.templates[template]["html"].render(**template_vars), "html"))
else: else:
parts.append((self.templates[template]["html"].format(**template_vars), "html")) parts.append((self.templates[template]["html"].format(**template_vars), "html"))
@ -296,6 +310,19 @@ class EmailClient(
msg.attach(part) msg.attach(part)
return msg return msg
@staticmethod
def template_image_loader(directory_path):
"""Return wrapper for the load_image_as_base64 function bind on the template directory"""
def _load_image_as_base64(path):
return load_image_as_base64(
os.path.join(directory_path, path)
if directory_path and not os.path.isabs(path)
else path
)
return _load_image_as_base64
def send( def send(
self, recipients, msg=None, subject=None, just_try=None, cc=None, bcc=None, **forge_args self, recipients, msg=None, subject=None, just_try=None, cc=None, bcc=None, **forge_args
): ):
@ -406,189 +433,3 @@ class EmailClient(
server.quit() server.quit()
return not error return not error
if __name__ == "__main__":
# Run tests
import argparse
import datetime
import sys
# Options parser
parser = argparse.ArgumentParser()
parser.add_argument(
"-v", "--verbose", action="store_true", dest="verbose", help="Enable verbose mode"
)
parser.add_argument(
"-d", "--debug", action="store_true", dest="debug", help="Enable debug mode"
)
parser.add_argument(
"-l", "--log-file", action="store", type=str, dest="logfile", help="Log file path"
)
parser.add_argument(
"-j", "--just-try", action="store_true", dest="just_try", help="Enable just-try mode"
)
email_opts = parser.add_argument_group("Email options")
email_opts.add_argument(
"-H", "--smtp-host", action="store", type=str, dest="email_smtp_host", help="SMTP host"
)
email_opts.add_argument(
"-P", "--smtp-port", action="store", type=int, dest="email_smtp_port", help="SMTP port"
)
email_opts.add_argument(
"-S", "--smtp-ssl", action="store_true", dest="email_smtp_ssl", help="Use SSL"
)
email_opts.add_argument(
"-T", "--smtp-tls", action="store_true", dest="email_smtp_tls", help="Use TLS"
)
email_opts.add_argument(
"-u", "--smtp-user", action="store", type=str, dest="email_smtp_user", help="SMTP username"
)
email_opts.add_argument(
"-p",
"--smtp-password",
action="store",
type=str,
dest="email_smtp_password",
help="SMTP password",
)
email_opts.add_argument(
"-D",
"--smtp-debug",
action="store_true",
dest="email_smtp_debug",
help="Debug SMTP connection",
)
email_opts.add_argument(
"-e",
"--email-encoding",
action="store",
type=str,
dest="email_encoding",
help="SMTP encoding",
)
email_opts.add_argument(
"-f",
"--sender-name",
action="store",
type=str,
dest="email_sender_name",
help="Sender name",
)
email_opts.add_argument(
"-F",
"--sender-email",
action="store",
type=str,
dest="email_sender_email",
help="Sender email",
)
email_opts.add_argument(
"-C",
"--catch-all",
action="store",
type=str,
dest="email_catch_all",
help="Catch all sent email: specify catch recipient email address",
)
test_opts = parser.add_argument_group("Test email options")
test_opts.add_argument(
"-t",
"--to",
action="store",
type=str,
dest="test_to",
help="Test email recipient",
)
test_opts.add_argument(
"-m",
"--mako",
action="store_true",
dest="test_mako",
help="Test mako templating",
)
options = parser.parse_args()
if not options.test_to:
parser.error("You must specify test email recipient using -t/--to parameter")
sys.exit(1)
# Initialize logs
logformat = "%(asctime)s - Test EmailClient - %(levelname)s - %(message)s"
if options.debug:
loglevel = logging.DEBUG
elif options.verbose:
loglevel = logging.INFO
else:
loglevel = logging.WARNING
if options.logfile:
logging.basicConfig(filename=options.logfile, level=loglevel, format=logformat)
else:
logging.basicConfig(level=loglevel, format=logformat)
if options.email_smtp_user and not options.email_smtp_password:
import getpass
options.email_smtp_password = getpass.getpass("Please enter SMTP password: ")
logging.info("Initialize Email client")
email_client = EmailClient(
smtp_host=options.email_smtp_host,
smtp_port=options.email_smtp_port,
smtp_ssl=options.email_smtp_ssl,
smtp_tls=options.email_smtp_tls,
smtp_user=options.email_smtp_user,
smtp_password=options.email_smtp_password,
smtp_debug=options.email_smtp_debug,
sender_name=options.email_sender_name,
sender_email=options.email_sender_email,
catch_all_addr=options.email_catch_all,
just_try=options.just_try,
encoding=options.email_encoding,
templates={
"test": {
"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 | h}.") # nosec
),
"html": (
"<strong>Just a test email.</strong> <small>(sent at {sent_date | h})</small>"
if not options.test_mako
else MakoTemplate( # nosec
"<strong>Just a test email.</strong> "
"<small>(sent at ${sent_date | h})</small>"
)
),
}
},
)
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")
sys.exit(0)
logging.error("Fail to send test email")
sys.exit(1)

View file

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="92.738403mm"
height="17.141003mm"
viewBox="0 0 92.738403 17.141003"
version="1.1"
id="svg5"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="header.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="0.84096521"
inkscape:cx="92.156012"
inkscape:cy="315.11411"
inkscape:window-width="1920"
inkscape:window-height="1011"
inkscape:window-x="0"
inkscape:window-y="32"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs2" />
<g
inkscape:label="Calque 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-44.730637,-68.858589)">
<rect
style="fill:none;stroke:#004787;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect234"
width="90.738403"
height="15.141003"
x="45.730637"
y="69.858589" />
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:5.29167px;line-height:125%;font-family:'DejaVu Sans Mono';-inkscape-font-specification:'DejaVu Sans Mono';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="82.664459"
y="79.623909"
id="text2144"><tspan
sodipodi:role="line"
id="tspan2142"
style="fill:#004787;fill-opacity:1;stroke-width:0.264583px"
x="82.664459"
y="79.623909">Header</tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -1 +1,2 @@
<strong>Just a test email.</strong> <small>(sent at ${sent_date})</small> <img src="${load_image_as_base64('header.svg')}" style="display: block" />
<p><strong>Just a test email.</strong> <small>(sent at ${sent_date})</small></p>

View file

@ -38,19 +38,10 @@ def main(argv=None): # pylint: disable=too-many-locals,too-many-statements
"-T", "-T",
"--template", "--template",
action="store_true", action="store_true",
dest="template",
help="Template name to send (default: test)", help="Template name to send (default: test)",
default="test", default="test",
) )
test_opts.add_argument(
"-m",
"--mako",
action="store_true",
dest="test_mako",
help="Test mako templating",
)
test_opts.add_argument( test_opts.add_argument(
"--cc", "--cc",
action="store", action="store",
@ -93,7 +84,7 @@ def main(argv=None): # pylint: disable=too-many-locals,too-many-statements
options.test_to, options.test_to,
cc=options.test_cc, cc=options.test_cc,
bcc=options.test_bcc, bcc=options.test_bcc,
template="test", template=options.template,
sent_date=datetime.datetime.now(), sent_date=datetime.datetime.now(),
): ):
log.info("Test email sent") log.info("Test email sent")

View file

@ -22,6 +22,7 @@ extras_require = {
"pytz", "pytz",
], ],
"email": [ "email": [
"python-magic",
"mako", "mako",
], ],
"pgsql": [ "pgsql": [