Invite a friend; handle field shows the domain; system mail fixes

- Invitations: each user can invite two friends by email (sign-up works
  through the link even while registration is closed); admin-editable
  template, subject and sender.
- Invitation sender defaults to the system-mail From address, which the
  SMTP account is allowed to send as (was postmaster@APP_DOMAIN, refused
  with 553 when the two differ).
- Sign-in, sign-up and forgot-password fields show the fixed
  @domain after the typed name; the registration status reports it.
- System mail gets a Date header; the invitation's plain-text part keeps
  its paragraphs and line breaks.
- SMTP EHLO uses SMTP_HELO_NAME / APP_DOMAIN, not the container name.
- DNS recommendations for the mail domains.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-15 19:34:31 +02:00
co-authored by Claude Opus 5.5
parent addb2232a1
commit 463f96ad57
30 changed files with 1870 additions and 42 deletions
+28
View File
@@ -87,6 +87,13 @@ class AppSettings(Base):
# take it (base64url, uncompressed P-256 point).
vapid_public: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
vapid_private_ct: Mapped[bytes | None] = mapped_column(BYTEA)
# Users may invite friends (app/identity/invitations.py); the invitation
# email's From address (empty: postmaster@APP_DOMAIN), subject and HTML
# template (empty: the built-in ones).
invitations_enabled: Mapped[bool] = mapped_column(nullable=False, server_default=text("false"))
invite_from: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
invite_subject: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
invite_template: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
updated_at: Mapped[dt.datetime] = _now()
@@ -376,6 +383,27 @@ class UserAppPassword(Base):
server_id: Mapped[str | None] = mapped_column(Text)
class Invitation(Base):
"""A single-use link that lets one person sign up, sent by a user."""
__tablename__ = "invitations"
id: Mapped[uuid.UUID] = _uuid_pk()
inviter_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True
)
# sha256 of the token in the link; the token itself is only in the email.
token_hash: Mapped[str] = mapped_column(Text, nullable=False, unique=True)
invitee_name: Mapped[str] = mapped_column(Text, nullable=False)
invitee_email: Mapped[str] = mapped_column(Text, nullable=False)
created_at: Mapped[dt.datetime] = _now()
expires_at: Mapped[dt.datetime] = mapped_column(TIMESTAMP(timezone=True), nullable=False)
used_at: Mapped[dt.datetime | None] = mapped_column(TIMESTAMP(timezone=True))
used_by: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL")
)
class PushSubscription(Base):
"""A browser or installed app to tell about new mail (Web Push)."""