Productivity features, UI translations, local email translation

- Snooze (with a Snoozed folder), keyboard shortcuts with a Settings
  section, mail templates (Compose > More > Templates), a Subscriptions
  folder with bulk unsubscribe/delete, and tracker blocking with a
  Privacy toggle (on by default)
- Own dialogs instead of browser popups; emoji picker in compose; compose
  windows survive a visit to Settings
- Sentence completion learned from the user's sent mail (no AI)
- Interface in English and Polish (one file per language, English as the
  fallback), picked from the browser on first run, then kept
- Email translation on the server via LibreTranslate: Translate button
  and "Translate to...", messages in several languages translated part
  by part, names/addresses/emoji/file names/phone numbers kept, an
  animated line while translating
- Antispam "Mark as spam" loads with the message, not when the menu opens

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-18 20:00:31 +02:00
co-authored by Claude Opus 5.5
parent 463f96ad57
commit 8a1b9979e9
83 changed files with 8677 additions and 293 deletions
+91
View File
@@ -199,6 +199,12 @@ class Thread(Base):
has_attachments: Mapped[bool] = mapped_column(nullable=False, server_default=text("false"))
pinned: Mapped[bool] = mapped_column(nullable=False, server_default=text("false"))
last_message_at: Mapped[dt.datetime] = mapped_column(TIMESTAMP(timezone=True), nullable=False)
# Snooze: out of the Inbox until this time (NULL when not snoozed). A message
# that arrives after ``snoozed_at`` wakes it early; ``unsnoozed_at`` (when it
# last woke) puts it back at the top of the Inbox.
snoozed_until: Mapped[dt.datetime | None] = mapped_column(TIMESTAMP(timezone=True))
snoozed_at: Mapped[dt.datetime | None] = mapped_column(TIMESTAMP(timezone=True))
unsnoozed_at: Mapped[dt.datetime | None] = mapped_column(TIMESTAMP(timezone=True))
class Message(Base):
@@ -263,6 +269,17 @@ class Message(Base):
snippet: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
blob_sha256: Mapped[str] = mapped_column(Text, nullable=False)
body_indexed: Mapped[str | None] = mapped_column(TSVECTOR)
# Mailing-list headers, for the Subscriptions page. ``list_unsubscribe`` is
# NULL until read (a backfill job fills older mail), '' when there is none.
list_id: Mapped[str | None] = mapped_column(Text)
list_unsubscribe: Mapped[str | None] = mapped_column(Text)
# RFC 8058: the sender takes a one-click unsubscribe POST.
list_unsubscribe_post: Mapped[bool] = mapped_column(
nullable=False, server_default=text("false")
)
# The language of its text (app/translation.py), found the first time it is
# shown: NULL not looked at yet, '' could not tell.
content_language: Mapped[str | None] = mapped_column(Text)
class BodyPart(Base):
@@ -276,6 +293,8 @@ class BodyPart(Base):
content: Mapped[str] = mapped_column(Text, nullable=False)
# SPEC §10.4: re-sanitize on render if the sanitizer version changed.
sanitizer_v: Mapped[int] = mapped_column(Integer, nullable=False, server_default=text("0"))
# Tracking pixels taken out of this HTML (shown as "N trackers blocked").
trackers_blocked: Mapped[int] = mapped_column(Integer, nullable=False, server_default=text("0"))
class Attachment(Base):
@@ -404,6 +423,78 @@ class Invitation(Base):
)
class SubscriptionState(Base):
"""What a user did about a bulk sender on the Subscriptions page."""
__tablename__ = "subscription_states"
user_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), primary_key=True
)
# The sender group: "list:<List-Id>" or "from:<address>".
key: Mapped[str] = mapped_column(Text, primary_key=True)
unsubscribed_at: Mapped[dt.datetime] = _now()
method: Mapped[str] = mapped_column(Text, nullable=False) # one_click | mailto | web
class MailTemplate(Base):
"""A saved reply, inserted from the compose window."""
__tablename__ = "mail_templates"
id: Mapped[uuid.UUID] = _uuid_pk()
user_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True
)
name: Mapped[str] = mapped_column(Text, nullable=False)
subject: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
body_html: Mapped[str] = mapped_column(Text, nullable=False, server_default=text("''"))
created_at: Mapped[dt.datetime] = _now()
updated_at: Mapped[dt.datetime] = _now()
class MessageTranslation(Base):
"""A message's HTML translated into a language (app/translation.py), kept so
it is translated once. ``images``: with remote images shown or not."""
__tablename__ = "message_translations"
message_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("messages.id", ondelete="CASCADE"), primary_key=True
)
target: Mapped[str] = mapped_column(Text, primary_key=True)
images: Mapped[bool] = mapped_column(primary_key=True)
source: Mapped[str] = mapped_column(Text, nullable=False)
html: Mapped[str] = mapped_column(Text, nullable=False)
sanitizer_v: Mapped[int] = mapped_column(Integer, nullable=False)
created_at: Mapped[dt.datetime] = _now()
class CompletionPhrase(Base):
"""Sentence completion (app/completion.py): after these words, the user
usually writes this — seen ``count`` times in their sent mail."""
__tablename__ = "completion_phrases"
user_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), primary_key=True
)
context: Mapped[str] = mapped_column(Text, primary_key=True) # normalized words
continuation: Mapped[str] = mapped_column(Text, primary_key=True) # as written
count: Mapped[int] = mapped_column(Integer, nullable=False)
class CompletionIndexState(Base):
"""When a user's completion index was last built."""
__tablename__ = "completion_index_state"
user_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), primary_key=True
)
built_at: Mapped[dt.datetime] = mapped_column(TIMESTAMP(timezone=True), nullable=False)
class PushSubscription(Base):
"""A browser or installed app to tell about new mail (Web Push)."""