From 17ce4b5029729eea1c4d6d4f253e2d27101cdfda Mon Sep 17 00:00:00 2001 From: Russell Ballestrini Date: Thu, 9 Jan 2025 09:02:00 -0500 Subject: [PATCH] when in development log SMTP errors & email modified: Makefile modified: README.rst modified: remarkbox/__init__.py modified: remarkbox/lib/mail.py --- Makefile | 4 -- README.rst | 13 ++---- remarkbox/__init__.py | 7 +++ remarkbox/lib/mail.py | 104 +++++++++++++++++++++++++++++------------- 4 files changed, 84 insertions(+), 44 deletions(-) diff --git a/Makefile b/Makefile index 3d40460..29e6c53 100644 --- a/Makefile +++ b/Makefile @@ -29,7 +29,3 @@ serve: dev http: dev # In the second shell, run a "mock" simple HTTP webserver to serve index.html: 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 diff --git a/README.rst b/README.rst index 5674e6c..4938486 100644 --- a/README.rst +++ b/README.rst @@ -70,7 +70,7 @@ We utilize a ``Makefile`` to capture targets for building a local Remarkbox envi 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: @@ -84,14 +84,11 @@ In the second shell, run a "mock" simple HTTP web server to serve index.html: 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 - - 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! +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. New Environments diff --git a/remarkbox/__init__.py b/remarkbox/__init__.py index 26b6de8..6551a7e 100644 --- a/remarkbox/__init__.py +++ b/remarkbox/__init__.py @@ -192,6 +192,12 @@ def main(global_config, **settings): # compile the email validator regex outside of the functions. _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): """Return Redis Connection""" @@ -490,6 +496,7 @@ def main(global_config, **settings): # each request instance will run these functions and attach results. # cache result with `reify=True` to prevent multiple db lookups. # 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_csrf_token, "csrf_token", reify=True) config.add_request_method(add_email, "email", reify=True) diff --git a/remarkbox/lib/mail.py b/remarkbox/lib/mail.py index 0caf6a6..fe033ec 100644 --- a/remarkbox/lib/mail.py +++ b/remarkbox/lib/mail.py @@ -26,6 +26,10 @@ from email.mime.text import MIMEText # catch socket errors when postfix isn't running... from socket import error as socket_error +import logging + +log = logging.getLogger(__name__) + from jinja2 import Environment, PackageLoader, select_autoescape jinja2_env = Environment( @@ -44,12 +48,11 @@ def send_email( dkim_private_key_path="", dkim_selector="", dkim_signature_algorithm="ed25519-sha256", + debug_mode=False, ): - # the `email` library assumes it is working with string objects. # the `dkim` library assumes it is working with byte objects. # this function performs the acrobatics to make them both happy. - if isinstance(message_text, bytes): # needed for Python 3. message_text = message_text.decode() @@ -74,47 +77,83 @@ def send_email( msg_data = msg.as_string() 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: - # Python 3 libraries expect bytes. - msg_data = msg.as_bytes() - except: - # Python 2 libraries expect strings. - msg_data = msg.as_string() + # 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() - # TODO: react if connecting to relay (localhost postfix) is a socket error. - s = smtplib.SMTP(relay) - s.sendmail(sender_email, [to_email], msg_data) - s.quit() - return msg + try: + # Python 3 libraries expect bytes. + msg_data = msg.as_bytes() + except: + # 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): """Thin wrapper around `send_email` to customise settings using request object.""" default_sender = "no-reply@{}".format(request.domain) 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") dkim_private_key_path = request.app.get("email.dkim_private_key_path", "") 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( to_email, @@ -126,6 +165,7 @@ def send_pyramid_email(request, to_email, subject, message_text, message_html): dkim_private_key_path, dkim_selector, dkim_signature_algorithm, + request.debug_mode, )