Compare commits
2 commits
384e8a441d
...
601857a2d4
Author | SHA1 | Date | |
---|---|---|---|
|
601857a2d4 | ||
|
4c51d0086f |
5 changed files with 94 additions and 198 deletions
215
mylib/email.py
215
mylib/email.py
|
@ -1,5 +1,6 @@
|
|||
""" Email client to forge and send emails """
|
||||
|
||||
import base64
|
||||
import email.utils
|
||||
import logging
|
||||
import os
|
||||
|
@ -9,6 +10,7 @@ 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 (
|
||||
|
@ -22,6 +24,14 @@ 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
|
||||
|
@ -165,7 +175,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] = {}
|
||||
self.templates[template_name] = {"path": templates_path}
|
||||
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(
|
||||
|
@ -239,6 +249,7 @@ 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:
|
||||
|
@ -264,6 +275,9 @@ 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"))
|
||||
|
@ -296,6 +310,19 @@ 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
|
||||
):
|
||||
|
@ -406,189 +433,3 @@ 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)
|
||||
|
|
62
mylib/scripts/email_templates/header.svg
Normal file
62
mylib/scripts/email_templates/header.svg
Normal 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 |
|
@ -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>
|
||||
|
|
|
@ -38,19 +38,10 @@ 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",
|
||||
|
@ -93,7 +84,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="test",
|
||||
template=options.template,
|
||||
sent_date=datetime.datetime.now(),
|
||||
):
|
||||
log.info("Test email sent")
|
||||
|
|
1
setup.py
1
setup.py
|
@ -22,6 +22,7 @@ extras_require = {
|
|||
"pytz",
|
||||
],
|
||||
"email": [
|
||||
"python-magic",
|
||||
"mako",
|
||||
],
|
||||
"pgsql": [
|
||||
|
|
Loading…
Reference in a new issue