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 """
|
""" 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)
|
|
||||||
|
|
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",
|
"-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")
|
||||||
|
|
1
setup.py
1
setup.py
|
@ -22,6 +22,7 @@ extras_require = {
|
||||||
"pytz",
|
"pytz",
|
||||||
],
|
],
|
||||||
"email": [
|
"email": [
|
||||||
|
"python-magic",
|
||||||
"mako",
|
"mako",
|
||||||
],
|
],
|
||||||
"pgsql": [
|
"pgsql": [
|
||||||
|
|
Loading…
Reference in a new issue