Compare commits
No commits in common. "601857a2d4d8e14c60aef9ee5e19ed7f509dd178" and "384e8a441d4db350d9d28a6f92ccaaaa7bf052af" have entirely different histories.
601857a2d4
...
384e8a441d
5 changed files with 198 additions and 94 deletions
215
mylib/email.py
215
mylib/email.py
|
@ -1,6 +1,5 @@
|
||||||
""" 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
|
||||||
|
@ -10,7 +9,6 @@ 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 (
|
||||||
|
@ -24,14 +22,6 @@ 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
|
||||||
|
@ -175,7 +165,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] = {"path": templates_path}
|
self.templates[template_name] = {}
|
||||||
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(
|
||||||
|
@ -249,7 +239,6 @@ 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:
|
||||||
|
@ -275,9 +264,6 @@ 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"))
|
||||||
|
@ -310,19 +296,6 @@ 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
|
||||||
):
|
):
|
||||||
|
@ -433,3 +406,189 @@ 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)
|
||||||
|
|
|
@ -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 |
|
@ -1,2 +1 @@
|
||||||
<img src="${load_image_as_base64('header.svg')}" style="display: block" />
|
<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></p>
|
|
||||||
|
|
|
@ -38,10 +38,19 @@ 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",
|
||||||
|
@ -84,7 +93,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=options.template,
|
template="test",
|
||||||
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,7 +22,6 @@ extras_require = {
|
||||||
"pytz",
|
"pytz",
|
||||||
],
|
],
|
||||||
"email": [
|
"email": [
|
||||||
"python-magic",
|
|
||||||
"mako",
|
"mako",
|
||||||
],
|
],
|
||||||
"pgsql": [
|
"pgsql": [
|
||||||
|
|
Loading…
Reference in a new issue