fix: Date/Message-ID/From-name on ticket emails, richer body (1.6.1-aio.9)
A tester's ticket email landed in spam. A mail-tester run against demo scored 8.3/10 with SPF, DKIM and DMARC all passing through the VPS relay, so the deductions were all in the message: MISSING_DATE (1.4), HTML_IMAGE_ONLY_04 (0.3), MISSING_MID (0.1) — and Gmail/Outlook weigh a missing Date/Message-ID as "machine-generated" far more than that. - `build_ticket_email` sets Date, a Message-ID under the sender domain, and a From display name from `lnbits_site_title`. - The body now carries the event name, dates, location, name on ticket, ticket id and the door instruction, so the HTML part is no longer a QR with a handful of words. Upstream candidate: lnbits core `send_email` has the same omissions. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYwoAkZZmXMMmaBp4WGUBo
This commit is contained in:
parent
8b423f185a
commit
3a8e0ff591
4 changed files with 137 additions and 26 deletions
69
services.py
69
services.py
|
|
@ -5,6 +5,7 @@ import smtplib
|
|||
from asyncio.tasks import create_task
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from email.utils import formataddr, formatdate, make_msgid
|
||||
from html import escape
|
||||
|
||||
from lnbits.core.models.users import UserNotifications
|
||||
|
|
@ -114,12 +115,34 @@ def _ticket_notification_message(ticket: Ticket, event: Event) -> tuple[str, str
|
|||
return subject, f"{body}\n\nOpen it here: {ticket_url}"
|
||||
|
||||
|
||||
def _ticket_delivery_message(ticket: Ticket, base_message: str) -> str:
|
||||
return f"{base_message}\n\nTicket image: {_ticket_image_url(ticket)}"
|
||||
def _ticket_details(ticket: Ticket, event: Event) -> str:
|
||||
"""Human-readable ticket facts for the email body. Also what keeps the
|
||||
mail from being an image with no words (SpamAssassin HTML_IMAGE_ONLY)."""
|
||||
when = event.event_start_date
|
||||
if event.event_end_date and event.event_end_date != event.event_start_date:
|
||||
when = f"{when} to {event.event_end_date}"
|
||||
lines = [f"Event: {event.name}", f"When: {when}"]
|
||||
if event.location:
|
||||
lines.append(f"Where: {event.location}")
|
||||
if ticket.name:
|
||||
lines.append(f"Name on ticket: {ticket.name}")
|
||||
lines.append(f"Ticket ID: {ticket.id}")
|
||||
lines.append(
|
||||
"Show the QR code below (or open the link above on your phone) at the "
|
||||
"door to be scanned in."
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _ticket_email_html_message(ticket: Ticket, base_message: str) -> str:
|
||||
text_message = _ticket_delivery_message(ticket, base_message)
|
||||
def _ticket_delivery_message(ticket: Ticket, event: Event, base_message: str) -> str:
|
||||
return (
|
||||
f"{base_message}\n\n{_ticket_details(ticket, event)}"
|
||||
f"\n\nTicket image: {_ticket_image_url(ticket)}"
|
||||
)
|
||||
|
||||
|
||||
def _ticket_email_html_message(ticket: Ticket, event: Event, base_message: str) -> str:
|
||||
text_message = _ticket_delivery_message(ticket, event, base_message)
|
||||
html_message = f"<p>{escape(text_message).replace(chr(10), '<br />')}</p>"
|
||||
image_url = escape(_ticket_image_url(ticket), quote=True)
|
||||
return (
|
||||
|
|
@ -131,8 +154,8 @@ def _ticket_email_html_message(ticket: Ticket, base_message: str) -> str:
|
|||
|
||||
def _ticket_notification_payload(ticket: Ticket, event: Event) -> tuple[str, str, str]:
|
||||
subject, base_message = _ticket_notification_message(ticket, event)
|
||||
text_message = _ticket_delivery_message(ticket, base_message)
|
||||
html_message = _ticket_email_html_message(ticket, base_message)
|
||||
text_message = _ticket_delivery_message(ticket, event, base_message)
|
||||
html_message = _ticket_email_html_message(ticket, event, base_message)
|
||||
return subject, text_message, html_message
|
||||
|
||||
|
||||
|
|
@ -221,14 +244,7 @@ async def _send_ticket_email_notification(
|
|||
if not is_valid_email_address(address):
|
||||
raise ValueError(f"Invalid email address: {address}")
|
||||
|
||||
msg = MIMEMultipart("alternative")
|
||||
msg["From"] = from_email
|
||||
msg["To"] = ", ".join(to_emails)
|
||||
msg["Subject"] = subject
|
||||
msg.attach(MIMEText(message, "plain"))
|
||||
if html_message:
|
||||
msg.attach(MIMEText(html_message, "html"))
|
||||
|
||||
msg = build_ticket_email(from_email, to_emails, subject, message, html_message)
|
||||
username = settings.lnbits_email_notifications_username or from_email
|
||||
await asyncio.to_thread(
|
||||
_smtp_send,
|
||||
|
|
@ -242,6 +258,31 @@ async def _send_ticket_email_notification(
|
|||
)
|
||||
|
||||
|
||||
def build_ticket_email(
|
||||
from_email: str,
|
||||
to_emails: list[str],
|
||||
subject: str,
|
||||
message: str,
|
||||
html_message: str | None = None,
|
||||
) -> MIMEMultipart:
|
||||
"""Assemble the multipart ticket email with the headers receivers score
|
||||
on: a Date and a Message-ID (their absence is what SpamAssassin's
|
||||
MISSING_DATE / MISSING_MID flag, and what Gmail/Outlook read as
|
||||
machine-generated), and a display name on From so the sender is not a
|
||||
bare address."""
|
||||
msg = MIMEMultipart("alternative")
|
||||
sender_name = (settings.lnbits_site_title or "").strip() or "Tickets"
|
||||
msg["From"] = formataddr((sender_name, from_email))
|
||||
msg["To"] = ", ".join(to_emails)
|
||||
msg["Subject"] = subject
|
||||
msg["Date"] = formatdate(localtime=True)
|
||||
msg["Message-ID"] = make_msgid(domain=from_email.rsplit("@", 1)[-1])
|
||||
msg.attach(MIMEText(message, "plain"))
|
||||
if html_message:
|
||||
msg.attach(MIMEText(html_message, "html"))
|
||||
return msg
|
||||
|
||||
|
||||
def _smtp_send(
|
||||
server: str,
|
||||
port: int,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue