From c9d462ab8c3e4dabf36cd790ec4085f8038af2b9 Mon Sep 17 00:00:00 2001
From: Ben Zhang
Date: Sun, 18 Aug 2024 17:52:52 +0000
Subject: [PATCH 1/9] Support both text and HTML emails
---
.env.example | 2 +-
src/main.py | 134 ++++++++++++++++++++++++++++++---------------------
2 files changed, 79 insertions(+), 57 deletions(-)
diff --git a/.env.example b/.env.example
index 51b8cbd..e47e066 100644
--- a/.env.example
+++ b/.env.example
@@ -5,7 +5,7 @@ AZURE_STORAGE_CONNECTION_STRING="DefaultEndpointsProtocol=http;AccountName=devst
SMTP_HOST=smtp
SMTP_PORT=1025
-SMTP_USERNAME=testuser
+SMTP_USERNAME=testuser@example.com
SMTP_PASSWORD=testpass
# Comma-separated list of Google Group keys (group's email address, group alias, or the unique group ID)
diff --git a/src/main.py b/src/main.py
index 03f473f..41541fe 100644
--- a/src/main.py
+++ b/src/main.py
@@ -3,6 +3,9 @@
import time
import urllib.parse
from contextlib import asynccontextmanager
+from email.mime.multipart import MIMEMultipart
+from email.mime.text import MIMEText
+from html.parser import HTMLParser
from smtplib import SMTP, SMTPNotSupportedError
from textwrap import dedent
@@ -16,6 +19,14 @@
from google_admin_sdk_utils import DirectoryService
from utils import get_azure_table_client, random_str
+class HTMLTextFilter(HTMLParser):
+ """
+ Converts HTML to plain text.
+ Derived from https://stackoverflow.com/a/55825140/4527337
+ """
+ text = ""
+ def handle_data(self, data):
+ self.text += data
@asynccontextmanager
async def lifespan(app: FastAPI):
@@ -85,67 +96,78 @@ def sign_up(req: SignUpRequest, request: Request):
)
confirmation_url = f"{app_url}/confirm/{req.mailing_list}/{urllib.parse.quote_plus(req.email)}/{code}"
+ # Support both HTML and plain text emails
+ # https://stackoverflow.com/a/882770/4527337
+ msg = MIMEMultipart('alternative')
+ msg["Subject"] = f"Confirm Your Email Subscription for '{req.mailing_list}'"
+ msg["From"] = os.getenv("SMTP_SEND_AS", os.environ["SMTP_USERNAME"])
+ msg["To"] = req.email
+ msg["Reply-To"] = os.getenv("SMTP_REPLY_TO", os.environ["SMTP_USERNAME"])
+
+ # msg["MIME-Version"] = "1.0"
+ # msg["Content-Type"] = "text/html; charset=utf-8"
+
+ msg_html_body = f"""
+
+ Confirm Your Email
+ Please confirm your email address by clicking the button or the link below to receiving updates from "{req.mailing_list}". This confirmation link will expire in {CODE_TTL_SEC // 60} minutes.
+ Confirm Email
+ If the button above does not work, please copy and paste the following URL into your browser:
+ {confirmation_url}
+ If you did not request this subscription, no further action is required.
+
+ """
+ msg_html = f"""
+
+
+
+
+
+ Email Confirmation
+
+
+ {msg_html_body}
+
+ """
+ msg_html_parser = HTMLTextFilter()
+ msg_html_parser.feed(msg_html_body)
+ msg_text = msg_html_parser.text
+
+ msg.attach(MIMEText(msg_text, "plain"))
+ msg.attach(MIMEText(msg_html, "html"))
+
with SMTP(os.environ["SMTP_HOST"], port=os.environ["SMTP_PORT"]) as smtp:
try:
smtp.starttls()
except SMTPNotSupportedError as e:
- logger.warning(f"SMTP server does not support STARTTLS: {e}. Attempting to send email without encryption.")
+ logger.warning(
+ f"SMTP server does not support STARTTLS: {e}. Attempting to send email without encryption."
+ )
smtp.login(os.environ["SMTP_USERNAME"], os.environ["SMTP_PASSWORD"])
- smtp.sendmail(
- os.environ["SMTP_USERNAME"],
- req.email,
- dedent(
- f"""
- Subject: Confirm Your Email Subscription for '{req.mailing_list}'
- From: {os.getenv("SMTP_SEND_AS", os.environ["SMTP_USERNAME"])}
- To: {req.email}
- Reply-To: {os.getenv("SMTP_REPLY_TO", os.environ["SMTP_USERNAME"])}
- MIME-Version: 1.0
- Content-Type: text/html; charset="utf-8"
-
-
-
-
-
-
- Email Confirmation
-
-
-
- Confirm Your Email
- Please confirm your email address by clicking the button or the link below to receiving updates from "{req.mailing_list}":
- Confirm Email
- If the button above does not work, please copy and paste the following URL into your browser:
- {confirmation_url}
- If you did not request this subscription, no further action is required.
-
-
- """
- ),
- )
+ smtp.send_message(msg)
app.runtime_info["num_signups"] += 1
From 66dea9e69ec1554fe2aa107cc814b398529667b2 Mon Sep 17 00:00:00 2001
From: Ben Zhang
Date: Sun, 18 Aug 2024 17:55:22 +0000
Subject: [PATCH 2/9] Formatting and remove comments
---
src/main.py | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/src/main.py b/src/main.py
index 41541fe..0aefa36 100644
--- a/src/main.py
+++ b/src/main.py
@@ -19,15 +19,19 @@
from google_admin_sdk_utils import DirectoryService
from utils import get_azure_table_client, random_str
+
class HTMLTextFilter(HTMLParser):
"""
Converts HTML to plain text.
Derived from https://stackoverflow.com/a/55825140/4527337
"""
+
text = ""
+
def handle_data(self, data):
self.text += data
+
@asynccontextmanager
async def lifespan(app: FastAPI):
scheduler.add_job(cleanup, trigger=CronTrigger.from_crontab("* * * * *"))
@@ -98,15 +102,12 @@ def sign_up(req: SignUpRequest, request: Request):
# Support both HTML and plain text emails
# https://stackoverflow.com/a/882770/4527337
- msg = MIMEMultipart('alternative')
+ msg = MIMEMultipart("alternative")
msg["Subject"] = f"Confirm Your Email Subscription for '{req.mailing_list}'"
msg["From"] = os.getenv("SMTP_SEND_AS", os.environ["SMTP_USERNAME"])
msg["To"] = req.email
msg["Reply-To"] = os.getenv("SMTP_REPLY_TO", os.environ["SMTP_USERNAME"])
- # msg["MIME-Version"] = "1.0"
- # msg["Content-Type"] = "text/html; charset=utf-8"
-
msg_html_body = f"""
Confirm Your Email
From e143d81b62691dba11ec3014ffba0a217a0afaa3 Mon Sep 17 00:00:00 2001
From: Ben Zhang
Date: Sun, 18 Aug 2024 18:02:13 +0000
Subject: [PATCH 3/9] Fix styling
---
src/main.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/main.py b/src/main.py
index 0aefa36..c2962d9 100644
--- a/src/main.py
+++ b/src/main.py
@@ -112,7 +112,7 @@ def sign_up(req: SignUpRequest, request: Request):
Confirm Your Email
Please confirm your email address by clicking the button or the link below to receiving updates from "{req.mailing_list}". This confirmation link will expire in {CODE_TTL_SEC // 60} minutes.
- Confirm Email
+ Confirm Email
If the button above does not work, please copy and paste the following URL into your browser:
{confirmation_url}
If you did not request this subscription, no further action is required.
@@ -134,7 +134,7 @@ def sign_up(req: SignUpRequest, request: Request):
background-color: #f4f4f4;
padding: 20px;
}}
- a {{
+ .confirmation-button {{
background-color: #007BFF;
color: white;
padding: 10px 20px;
@@ -142,7 +142,7 @@ def sign_up(req: SignUpRequest, request: Request):
border-radius: 5px;
font-size: 18px;
}}
- a:hover {{
+ .confirmation-button:hover {{
background-color: #0056b3;
}}
.link-text {{
From 9afcb9966ea9d52c792aeae4f41fac0fab780074 Mon Sep 17 00:00:00 2001
From: Ben Zhang
Date: Sun, 18 Aug 2024 18:10:30 +0000
Subject: [PATCH 4/9] improve email
---
src/main.py | 11 ++++++-----
1 file changed, 6 insertions(+), 5 deletions(-)
diff --git a/src/main.py b/src/main.py
index c2962d9..1f0bbad 100644
--- a/src/main.py
+++ b/src/main.py
@@ -110,12 +110,13 @@ def sign_up(req: SignUpRequest, request: Request):
msg_html_body = f"""
- Confirm Your Email
- Please confirm your email address by clicking the button or the link below to receiving updates from "{req.mailing_list}". This confirmation link will expire in {CODE_TTL_SEC // 60} minutes.
+ Confirm Your Subscription
+ Thanks for signing up for updates from "{req.mailing_list}"!
+ Please confirm your subscription by clicking the button below. This confirmation email will expire in {CODE_TTL_SEC // 60} minutes.
Confirm Email
If the button above does not work, please copy and paste the following URL into your browser:
- {confirmation_url}
- If you did not request this subscription, no further action is required.
+ {confirmation_url}
+ This email was sent to {req.email}. If you did not request this subscription, no further action is required. You won't be subscribed if you don't click the confirmation link.
"""
msg_html = f"""
@@ -145,7 +146,7 @@ def sign_up(req: SignUpRequest, request: Request):
.confirmation-button:hover {{
background-color: #0056b3;
}}
- .link-text {{
+ .monospace-text {{
font-family: 'Courier New', monospace;
}}
From 2a7dc8872948ba0d6b69858769f46927d0ccad0d Mon Sep 17 00:00:00 2001
From: Ben Zhang
Date: Sun, 18 Aug 2024 18:11:36 +0000
Subject: [PATCH 5/9] Wrap confirmation URL in pre
---
src/main.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/main.py b/src/main.py
index 1f0bbad..55a9548 100644
--- a/src/main.py
+++ b/src/main.py
@@ -115,7 +115,7 @@ def sign_up(req: SignUpRequest, request: Request):
Please confirm your subscription by clicking the button below. This confirmation email will expire in {CODE_TTL_SEC // 60} minutes.
Confirm Email
If the button above does not work, please copy and paste the following URL into your browser:
- {confirmation_url}
+ {confirmation_url}
This email was sent to {req.email}. If you did not request this subscription, no further action is required. You won't be subscribed if you don't click the confirmation link.
"""
From 34529526ccc201579f89d69832b6305cb1fdbd50 Mon Sep 17 00:00:00 2001
From: Ben Zhang
Date: Sun, 18 Aug 2024 18:14:40 +0000
Subject: [PATCH 6/9] Use top-level pre
---
src/main.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/main.py b/src/main.py
index 55a9548..159fb3f 100644
--- a/src/main.py
+++ b/src/main.py
@@ -115,7 +115,7 @@ def sign_up(req: SignUpRequest, request: Request):
Please confirm your subscription by clicking the button below. This confirmation email will expire in {CODE_TTL_SEC // 60} minutes.
Confirm Email
If the button above does not work, please copy and paste the following URL into your browser:
- {confirmation_url}
+ {confirmation_url}
This email was sent to {req.email}. If you did not request this subscription, no further action is required. You won't be subscribed if you don't click the confirmation link.
"""
From 1c14288344d26d8e8e3cc59ee24e9f4cd720c937 Mon Sep 17 00:00:00 2001
From: Ben Zhang
Date: Sun, 18 Aug 2024 18:22:02 +0000
Subject: [PATCH 7/9] Set text decoration to none in link
---
src/main.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/main.py b/src/main.py
index 159fb3f..a447e17 100644
--- a/src/main.py
+++ b/src/main.py
@@ -148,6 +148,7 @@ def sign_up(req: SignUpRequest, request: Request):
}}
.monospace-text {{
font-family: 'Courier New', monospace;
+ text-decoration: none;
}}
From 1199265481535b5032af1d708d0d9985d14d8cf4 Mon Sep 17 00:00:00 2001
From: Ben Zhang
Date: Sun, 18 Aug 2024 18:30:09 +0000
Subject: [PATCH 8/9] Use inline text-decoration style
---
src/main.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/main.py b/src/main.py
index a447e17..54dd15b 100644
--- a/src/main.py
+++ b/src/main.py
@@ -115,7 +115,7 @@ def sign_up(req: SignUpRequest, request: Request):
Please confirm your subscription by clicking the button below. This confirmation email will expire in {CODE_TTL_SEC // 60} minutes.
Confirm Email
If the button above does not work, please copy and paste the following URL into your browser:
- {confirmation_url}
+ {confirmation_url}
This email was sent to {req.email}. If you did not request this subscription, no further action is required. You won't be subscribed if you don't click the confirmation link.