Merge branch 'development-smtp-solution' into 'main'
when in development log SMTP errors & email Closes #3 See merge request engineering/remarkbox/remarkbox!4
This commit is contained in:
commit
84fabcd1c0
4 changed files with 84 additions and 44 deletions
4
Makefile
4
Makefile
|
|
@ -29,7 +29,3 @@ serve: dev
|
||||||
http: dev
|
http: dev
|
||||||
# In the second shell, run a "mock" simple HTTP webserver to serve index.html:
|
# In the second shell, run a "mock" simple HTTP webserver to serve index.html:
|
||||||
env/bin/python -m http.server 8000
|
env/bin/python -m http.server 8000
|
||||||
|
|
||||||
smtp: dev
|
|
||||||
# In the third shell, run a "mock" SMTP server to catch log in emails:
|
|
||||||
sudo env/bin/python -m smtpd -n -c DebuggingServer localhost:25
|
|
||||||
|
|
|
||||||
13
README.rst
13
README.rst
|
|
@ -70,7 +70,7 @@ We utilize a ``Makefile`` to capture targets for building a local Remarkbox envi
|
||||||
Functional testing environment
|
Functional testing environment
|
||||||
-------------------------------
|
-------------------------------
|
||||||
|
|
||||||
To setup a "functional testing" environment on your personal workstation, open three terminal shells.
|
To setup a "functional testing" environment on your personal workstation, open two terminal shells.
|
||||||
|
|
||||||
In the first shell, run a copy of Remarkbox using:
|
In the first shell, run a copy of Remarkbox using:
|
||||||
|
|
||||||
|
|
@ -84,14 +84,11 @@ In the second shell, run a "mock" simple HTTP web server to serve index.html:
|
||||||
|
|
||||||
make http
|
make http
|
||||||
|
|
||||||
In the third shell, run a "mock" SMTP server to catch log in emails:
|
Now browse to http://127.0.0.1:8000 and index.html will load.
|
||||||
|
This has an embedded copy of Remarkbox which is also running on localhost.
|
||||||
|
|
||||||
.. code-block:: bash
|
If you attempt to log in, a verification one-time-password code will be sent over SMTP to log in!
|
||||||
|
If you do not have an SMTP server the socket error will log email to console when in development.
|
||||||
make smtp
|
|
||||||
|
|
||||||
Now browse to http://127.0.0.1:8000 and index.html will load and will have an embedded copy of Remarkbox which running on localhost.
|
|
||||||
If you attempt to log in, your third shell will capture the email and you can copy / paste the verification link to log in!
|
|
||||||
|
|
||||||
|
|
||||||
New Environments
|
New Environments
|
||||||
|
|
|
||||||
|
|
@ -192,6 +192,12 @@ def main(global_config, **settings):
|
||||||
# compile the email validator regex outside of the functions.
|
# compile the email validator regex outside of the functions.
|
||||||
_email_regex = re.compile("^[^@]+@[^@]+\.[^.@]+$")
|
_email_regex = re.compile("^[^@]+@[^@]+\.[^.@]+$")
|
||||||
|
|
||||||
|
def add_debug_mode(request):
|
||||||
|
"""Return True if debug toolbar is enabled."""
|
||||||
|
return "pyramid_debugtoolbar" in request.registry.settings.get(
|
||||||
|
"pyramid.includes", ""
|
||||||
|
)
|
||||||
|
|
||||||
'''
|
'''
|
||||||
def add_redis(request):
|
def add_redis(request):
|
||||||
"""Return Redis Connection"""
|
"""Return Redis Connection"""
|
||||||
|
|
@ -490,6 +496,7 @@ def main(global_config, **settings):
|
||||||
# each request instance will run these functions and attach results.
|
# each request instance will run these functions and attach results.
|
||||||
# cache result with `reify=True` to prevent multiple db lookups.
|
# cache result with `reify=True` to prevent multiple db lookups.
|
||||||
# config.add_request_method(add_redis, "redis", reify=True)
|
# config.add_request_method(add_redis, "redis", reify=True)
|
||||||
|
config.add_request_method(add_debug_mode, "debug_mode", reify=True)
|
||||||
config.add_request_method(add_user, "user", reify=True)
|
config.add_request_method(add_user, "user", reify=True)
|
||||||
config.add_request_method(add_csrf_token, "csrf_token", reify=True)
|
config.add_request_method(add_csrf_token, "csrf_token", reify=True)
|
||||||
config.add_request_method(add_email, "email", reify=True)
|
config.add_request_method(add_email, "email", reify=True)
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,10 @@ from email.mime.text import MIMEText
|
||||||
# catch socket errors when postfix isn't running...
|
# catch socket errors when postfix isn't running...
|
||||||
from socket import error as socket_error
|
from socket import error as socket_error
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
from jinja2 import Environment, PackageLoader, select_autoescape
|
from jinja2 import Environment, PackageLoader, select_autoescape
|
||||||
|
|
||||||
jinja2_env = Environment(
|
jinja2_env = Environment(
|
||||||
|
|
@ -44,12 +48,11 @@ def send_email(
|
||||||
dkim_private_key_path="",
|
dkim_private_key_path="",
|
||||||
dkim_selector="",
|
dkim_selector="",
|
||||||
dkim_signature_algorithm="ed25519-sha256",
|
dkim_signature_algorithm="ed25519-sha256",
|
||||||
|
debug_mode=False,
|
||||||
):
|
):
|
||||||
|
|
||||||
# the `email` library assumes it is working with string objects.
|
# the `email` library assumes it is working with string objects.
|
||||||
# the `dkim` library assumes it is working with byte objects.
|
# the `dkim` library assumes it is working with byte objects.
|
||||||
# this function performs the acrobatics to make them both happy.
|
# this function performs the acrobatics to make them both happy.
|
||||||
|
|
||||||
if isinstance(message_text, bytes):
|
if isinstance(message_text, bytes):
|
||||||
# needed for Python 3.
|
# needed for Python 3.
|
||||||
message_text = message_text.decode()
|
message_text = message_text.decode()
|
||||||
|
|
@ -74,47 +77,83 @@ def send_email(
|
||||||
msg_data = msg.as_string()
|
msg_data = msg.as_string()
|
||||||
|
|
||||||
if dkim_private_key_path and dkim_selector:
|
if dkim_private_key_path and dkim_selector:
|
||||||
# the dkim library uses regex on byte strings so everything
|
|
||||||
# needs to be encoded from strings to bytes.
|
|
||||||
with open(dkim_private_key_path) as fh:
|
|
||||||
dkim_private_key = fh.read()
|
|
||||||
headers = [b"To", b"From", b"Subject"]
|
|
||||||
sig = dkim.sign(
|
|
||||||
message=msg_data,
|
|
||||||
selector=str(dkim_selector).encode(),
|
|
||||||
domain=sender_domain.encode(),
|
|
||||||
privkey=dkim_private_key.encode(),
|
|
||||||
include_headers=headers,
|
|
||||||
signature_algorithm=dkim_signature_algorithm.encode(),
|
|
||||||
)
|
|
||||||
# add the dkim signature to the email message headers.
|
|
||||||
# decode the signature back to string_type because later on
|
|
||||||
# the call to msg.as_string() performs it's own bytes encoding...
|
|
||||||
msg["DKIM-Signature"] = sig[len("DKIM-Signature: ") :].decode()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Python 3 libraries expect bytes.
|
# the dkim library uses regex on byte strings so everything
|
||||||
msg_data = msg.as_bytes()
|
# needs to be encoded from strings to bytes.
|
||||||
except:
|
with open(dkim_private_key_path) as fh:
|
||||||
# Python 2 libraries expect strings.
|
dkim_private_key = fh.read()
|
||||||
msg_data = msg.as_string()
|
headers = [b"To", b"From", b"Subject"]
|
||||||
|
sig = dkim.sign(
|
||||||
|
message=msg_data,
|
||||||
|
selector=str(dkim_selector).encode(),
|
||||||
|
domain=sender_domain.encode(),
|
||||||
|
privkey=dkim_private_key.encode(),
|
||||||
|
include_headers=headers,
|
||||||
|
signature_algorithm=dkim_signature_algorithm.encode(),
|
||||||
|
)
|
||||||
|
# add the dkim signature to the email message headers.
|
||||||
|
# decode the signature back to string_type because later on
|
||||||
|
# the call to msg.as_string() performs it's own bytes encoding...
|
||||||
|
msg["DKIM-Signature"] = sig[len("DKIM-Signature: ") :].decode()
|
||||||
|
|
||||||
# TODO: react if connecting to relay (localhost postfix) is a socket error.
|
try:
|
||||||
s = smtplib.SMTP(relay)
|
# Python 3 libraries expect bytes.
|
||||||
s.sendmail(sender_email, [to_email], msg_data)
|
msg_data = msg.as_bytes()
|
||||||
s.quit()
|
except:
|
||||||
return msg
|
# Python 2 libraries expect strings.
|
||||||
|
msg_data = msg.as_string()
|
||||||
|
except Exception as e:
|
||||||
|
if debug_mode:
|
||||||
|
log.error(f"DKIM signing failed: {str(e)}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
try:
|
||||||
|
s = smtplib.SMTP(relay)
|
||||||
|
s.sendmail(sender_email, [to_email], msg_data)
|
||||||
|
s.quit()
|
||||||
|
return msg
|
||||||
|
|
||||||
|
except (socket_error, smtplib.SMTPException) as e:
|
||||||
|
error_msg = f"Failed to send email: {str(e)}"
|
||||||
|
|
||||||
|
if debug_mode:
|
||||||
|
# Log the error first for quick scanning
|
||||||
|
log.error(error_msg)
|
||||||
|
# Then log the email details
|
||||||
|
log.info(
|
||||||
|
f"""
|
||||||
|
|
||||||
|
Email Contents:
|
||||||
|
To: {to_email}
|
||||||
|
From: {sender_email}
|
||||||
|
Subject: {subject}
|
||||||
|
|
||||||
|
Text Content:
|
||||||
|
{message_text}
|
||||||
|
|
||||||
|
HTML Content:
|
||||||
|
{message_html}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
if not debug_mode:
|
||||||
|
raise
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def send_pyramid_email(request, to_email, subject, message_text, message_html):
|
def send_pyramid_email(request, to_email, subject, message_text, message_html):
|
||||||
"""Thin wrapper around `send_email` to customise settings using request object."""
|
"""Thin wrapper around `send_email` to customise settings using request object."""
|
||||||
default_sender = "no-reply@{}".format(request.domain)
|
default_sender = "no-reply@{}".format(request.domain)
|
||||||
sender_email = request.app.get("email.sender", default_sender)
|
sender_email = request.app.get("email.sender", default_sender)
|
||||||
subject = "{} | {}".format(subject, request.app.get("email.subject_postfix", request.domain))
|
subject = "{} | {}".format(
|
||||||
|
subject, request.app.get("email.subject_postfix", request.domain)
|
||||||
|
)
|
||||||
relay = request.app.get("email.relay", "localhost")
|
relay = request.app.get("email.relay", "localhost")
|
||||||
dkim_private_key_path = request.app.get("email.dkim_private_key_path", "")
|
dkim_private_key_path = request.app.get("email.dkim_private_key_path", "")
|
||||||
dkim_selector = request.app.get("email.dkim_selector", "")
|
dkim_selector = request.app.get("email.dkim_selector", "")
|
||||||
dkim_signature_algorithm = request.app.get("email.dkim_signature_algorithm", "ed25519-sha256")
|
dkim_signature_algorithm = request.app.get(
|
||||||
|
"email.dkim_signature_algorithm", "ed25519-sha256"
|
||||||
|
)
|
||||||
|
|
||||||
send_email(
|
send_email(
|
||||||
to_email,
|
to_email,
|
||||||
|
|
@ -126,6 +165,7 @@ def send_pyramid_email(request, to_email, subject, message_text, message_html):
|
||||||
dkim_private_key_path,
|
dkim_private_key_path,
|
||||||
dkim_selector,
|
dkim_selector,
|
||||||
dkim_signature_algorithm,
|
dkim_signature_algorithm,
|
||||||
|
request.debug_mode,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue