Logo in system mail; "don't ask for the 2FA code on this device"
System mail: the password-reset email (and the admin panel's test email) now opens with the Ultimo logo, centered. It is a PNG rendered from icons/ultimo-logo.svg and embedded in the message (multipart/related, cid:), so it shows where remote images are blocked and in clients that do not render SVG. The PNG is listed in package-data so the Docker image carries it. Trusted devices: a 2FA sign-in offers "Don't ask for the code on this device in future". Ticked with a correct code, the browser gets its own HttpOnly trusted-device cookie (token in Redis, 90 days, renewed on use); later sign-ins to the same account from that browser need only the password. Other accounts and browsers are unaffected. The trust is revoked on a password reset, when 2FA is turned off, and when an admin blocks or deletes the user; signing out keeps it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,8 @@ import logging
|
||||
from dataclasses import dataclass
|
||||
from email.message import EmailMessage
|
||||
from email.utils import formataddr, make_msgid
|
||||
from functools import cache
|
||||
from pathlib import Path
|
||||
|
||||
import aiosmtplib
|
||||
|
||||
@@ -26,6 +28,22 @@ logger = logging.getLogger(__name__)
|
||||
SENDER_NAME = "Ultimo Email"
|
||||
_TIMEOUT = 20.0
|
||||
|
||||
# The logo travels inside the message (multipart/related, referenced by cid:),
|
||||
# not as a link: it shows even where remote images are blocked, and it is a PNG
|
||||
# because mail clients (Gmail first) do not display SVG. Rendered at twice the
|
||||
# size it is shown at, for sharp high-DPI screens.
|
||||
_LOGO_FILE = Path(__file__).parent / "assets" / "ultimo-logo.png"
|
||||
_LOGO_CID = "ultimo-logo@ultimo.email"
|
||||
LOGO_HTML = (
|
||||
f'<p style="text-align:center;margin:0 0 24px"><img src="cid:{_LOGO_CID}" '
|
||||
'width="180" height="47" alt="Ultimo Email" style="display:inline-block;border:0"></p>'
|
||||
)
|
||||
|
||||
|
||||
@cache
|
||||
def _logo() -> bytes:
|
||||
return _LOGO_FILE.read_bytes()
|
||||
|
||||
|
||||
class MailerError(Exception):
|
||||
"""System mail is not configured, or the SMTP server refused the message."""
|
||||
@@ -67,6 +85,10 @@ async def send_mail(cfg: MailerConfig, to: str, subject: str, text: str, html: s
|
||||
msg["Auto-Submitted"] = "auto-generated" # RFC 3834: do not auto-reply to this
|
||||
msg.set_content(text)
|
||||
msg.add_alternative(html, subtype="html")
|
||||
if f"cid:{_LOGO_CID}" in html:
|
||||
html_part = msg.get_body(("html",))
|
||||
if isinstance(html_part, EmailMessage):
|
||||
html_part.add_related(_logo(), maintype="image", subtype="png", cid=f"<{_LOGO_CID}>")
|
||||
try:
|
||||
await aiosmtplib.send(
|
||||
msg,
|
||||
@@ -99,7 +121,7 @@ def reset_mail(link: str, handle: str) -> tuple[str, str, str]:
|
||||
)
|
||||
body = "font-family:Arial,Helvetica,sans-serif;font-size:15px;color:#1f2937;line-height:1.5"
|
||||
html = (
|
||||
f'<!doctype html>\n<html><body style="{body}">\n'
|
||||
f'<!doctype html>\n<html><body style="{body}">\n{LOGO_HTML}\n'
|
||||
f"<p>Someone asked to change the password of the Ultimo Email account <b>{h}</b>.</p>\n"
|
||||
"<p>To choose a new password, open this link within 30 minutes:</p>\n"
|
||||
f'<p><a href="{u}" style="{button}">Change password</a></p>\n'
|
||||
|
||||
Reference in New Issue
Block a user