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:
@@ -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)."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user