remove SMTP server from dev and use console logs instead.

This commit is contained in:
Russell Ballestrini 2025-01-10 13:10:37 +00:00
parent e101026d98
commit 906e4a93e1
5 changed files with 119 additions and 56 deletions

View file

@ -43,5 +43,10 @@ build:
deploy:
stage: deploy
needs: [build]
rules:
- if: '$CI_COMMIT_BRANCH == "develop"'
when: on_success
- if: '$CI_COMMIT_BRANCH == "master"'
when: on_success
trigger:
include: .gitlab-ci.deploy.yml

View file

@ -29,8 +29,3 @@ test: dev
serve: dev
# In the first shell, run a copy of make_post_sell using:
env/bin/pserve development.ini --reload
smtp: env
# 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
sudo env/bin/python -m aiosmtpd -l 127.0.0.1:8025

View file

@ -28,12 +28,6 @@ Run a web application server with the project loaded:
make serve
Open a 2nd shell and run a test SMTP server for intercepting OTP login uri's:
.. code-block:: bash
make smtp
In a web browser navagate to http://127.0.0.1:6501/ and you should see the app.
@ -128,3 +122,36 @@ For example, we needed to migrate production data using this script:
# commit/close the database transaction to really make changes.
request.tm.commit()
Contributing
===================
* Establish communication with Russell or another admin to bless your git.unturf.com gitlab account & put you into the proper roles.
* Russell should see your account request but due to spam you have to ask him directly for approval via email or some other means of comms.
* Clone repo & make commits
* Create merge requests, we automatically run the unit & headless functional tests on each commit
* On merge we release to the production site & see the change across users.
Optionally, format your code.
This is not set in stone, but if you want to use a formatter this is the path for now!
**Python**
black (manual)
**Jinja2**
None (not needed, neither is an HTML formatter)
**JavaScript**
Prettier or biome (manual)
**CSS**
Prettier or biome (manual)
Licence
=====================
All code contributed goes into the public domain.

View file

@ -7,7 +7,6 @@ except ImportError:
# Python 3.
from urllib.parse import quote_plus
from make_post_sell.lib.mail_messages import (
WELCOME_1_TEXT,
WELCOME_1_HTML,
@ -19,17 +18,15 @@ from make_post_sell.lib.mail_messages import (
INVITE_1_HTML,
)
import dkim
import smtplib
from email.mime.multipart import MIMEMultipart
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
import logging
log = logging.getLogger(__name__)
def send_email(
@ -42,17 +39,18 @@ 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.
# 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.
# Needed for Python 3.
message_text = message_text.decode()
if isinstance(message_html, bytes):
# needed for Python 3.
# Needed for Python 3.
message_html = message_html.decode()
sender_domain = sender_email.split("@")[-1]
@ -71,40 +69,71 @@ 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 its 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 AttributeError: # For Python 2 compatibility
# 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."""
"""Thin wrapper around `send_email` to customize settings using request object."""
default_sender = f"no-reply@{request.domain}"
sender_email = request.app.get("email.sender", default_sender)
subject = f"{subject} | {request.app.get('email.subject_postfix', request.domain)}"
@ -125,6 +154,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,
)
@ -225,17 +255,17 @@ def send_sale_email(request, shop, products, total_cost):
subject, shop.absolute_sales_url(request), product_text, f"{total_cost:.2f}"
)
# send a separate email for each shop owner.
# Send a separate email for each shop owner.
for user in shop.users:
send_pyramid_email(request, user.email, subject, message_text, message_html)
def send_invite_email(request, to_email, user, shop):
"""
Send an email to all shop owners regarding the sale.
Send an email to invite a user to join a shop.
request
the request (of the successful log in attempt)
the request (of the invitation)
to_email
the email address to send the shop invitation to.

View file

@ -18,6 +18,12 @@ def includeme(config):
# get the app_settings from the config file.
app_settings = get_children_settings(config.get_settings(), "app")
def add_debug_mode(request):
"""Return True if debug toolbar is enabled."""
return "pyramid_debugtoolbar" in request.registry.settings.get(
"pyramid.includes", ""
)
def add_user(request):
"""Return User object or None. User.authenticated may be True or False."""
user = None
@ -164,7 +170,7 @@ def includeme(config):
# Register functions to app config as request methods.
# To prevent multiple DB lookups, cache result with `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_active_cart, "active_cart", reify=True)
config.add_request_method(add_session_cart, "session_cart", reify=True)