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:

+ +

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:

- -

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:

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:

- -

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.

""" From 264474682b051282ed925bc1e5ece6a619e0a16a Mon Sep 17 00:00:00 2001 From: Ben Zhang Date: Sun, 18 Aug 2024 18:37:18 +0000 Subject: [PATCH 9/9] Revert text decoration none --- src/main.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main.py b/src/main.py index 54dd15b..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.

""" @@ -148,7 +148,6 @@ def sign_up(req: SignUpRequest, request: Request): }} .monospace-text {{ font-family: 'Courier New', monospace; - text-decoration: none; }}