Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use multipart to send text and HTML emails #3

Merged
merged 9 commits into from
Aug 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
136 changes: 80 additions & 56 deletions src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -17,6 +20,18 @@
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("* * * * *"))
Expand Down Expand Up @@ -85,67 +100,76 @@ 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_html_body = f"""
<body>
<h1>Confirm Your Subscription</h1>
<p>Thanks for signing up for updates from "{req.mailing_list}"!</p>
<p>Please confirm your subscription by clicking the button below. This confirmation email will expire in {CODE_TTL_SEC // 60} minutes.</p>
<a class="confirmation-button" href="{confirmation_url}">Confirm Email</a>
<p>If the button above does not work, please copy and paste the following URL into your browser:</p>
<pre class="monospace-text">{confirmation_url}</pre>
<p> 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.</p>
</body>
"""
msg_html = f"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Email Confirmation</title>
<style>
body {{
font-family: Arial, sans-serif;
line-height: 1.6;
margin: 20px;
color: #333;
background-color: #f4f4f4;
padding: 20px;
}}
.confirmation-button {{
background-color: #007BFF;
color: white;
padding: 10px 20px;
text-decoration: none;
border-radius: 5px;
font-size: 18px;
}}
.confirmation-button:hover {{
background-color: #0056b3;
}}
.monospace-text {{
font-family: 'Courier New', monospace;
}}
</style>
</head>
{msg_html_body}
</html>
"""
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"

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Email Confirmation</title>
<style>
body {{
font-family: Arial, sans-serif;
line-height: 1.6;
margin: 20px;
color: #333;
background-color: #f4f4f4;
padding: 20px;
}}
a {{
background-color: #007BFF;
color: white;
padding: 10px 20px;
text-decoration: none;
border-radius: 5px;
font-size: 18px;
}}
a:hover {{
background-color: #0056b3;
}}
.link-text {{
font-family: 'Courier New', monospace;
}}
</style>
</head>
<body>
<h1>Confirm Your Email</h1>
<p>Please confirm your email address by clicking the button or the link below to receiving updates from "{req.mailing_list}":</p>
<a href="{confirmation_url}">Confirm Email</a>
<p>If the button above does not work, please copy and paste the following URL into your browser:</p>
<p class="link-text">{confirmation_url}</p>
<p>If you did not request this subscription, no further action is required.</p>
</body>
</html>
"""
),
)
smtp.send_message(msg)

app.runtime_info["num_signups"] += 1

Expand Down
Loading