Compare commits

..

No commits in common. "601857a2d4d8e14c60aef9ee5e19ed7f509dd178" and "384e8a441d4db350d9d28a6f92ccaaaa7bf052af" have entirely different histories.

5 changed files with 198 additions and 94 deletions

View file

@ -1,6 +1,5 @@
""" Email client to forge and send emails """
import base64
import email.utils
import logging
import os
@ -10,7 +9,6 @@ from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import magic
from mako.template import Template as MakoTemplate
from mylib.config import (
@ -24,14 +22,6 @@ from mylib.config import (
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(
ConfigurableObject
): # pylint: disable=useless-object-inheritance,too-many-instance-attributes
@ -175,7 +165,7 @@ class EmailClient(
continue
template_type = "text" if template_type == ".txt" else template_type[1:]
if template_name not in self.templates:
self.templates[template_name] = {"path": templates_path}
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(
@ -249,7 +239,6 @@ class EmailClient(
msg["Date"] = email.utils.formatdate(None, True)
encoding = encoding if encoding else self._get_option("encoding")
if template:
log.debug("Forge email from template %s", template)
assert template in self.templates, f"Unknown template {template}"
# Handle subject from template
if not subject:
@ -275,9 +264,6 @@ class EmailClient(
)
if self.templates[template].get("html"):
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"))
else:
parts.append((self.templates[template]["html"].format(**template_vars), "html"))
@ -310,19 +296,6 @@ class EmailClient(
msg.attach(part)
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(
self, recipients, msg=None, subject=None, just_try=None, cc=None, bcc=None, **forge_args
):
@ -433,3 +406,189 @@ class EmailClient(
server.quit()
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

@ -1,62 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -1,2 +1 @@
<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>
<strong>Just a test email.</strong> <small>(sent at ${sent_date})</small>

View file

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

View file

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