// The IPC contract. Every type here has a matching declaration in src/ipc.ts, and both sides are // frozen once written: implementation modules add bodies, not fields. // // Two rules run through the whole file. Nothing that crosses this boundary carries a provider // identifier the frontend could act on, because a view is the mirror joined to the state and the // frontend is not entitled to know which of the two a value came from. And every list the frontend // renders arrives already ordered and already grouped, because the grouping is part of the view // and computing it twice in two languages is how the two drift. use serde::{Deserialize, Serialize}; // ------------------------------------------------------------------------------------------- // People, accounts and places // ------------------------------------------------------------------------------------------- /// One correspondent. `name` is whatever the header carried, which is often nothing. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Person { pub name: Option, pub address: String, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Account { pub id: String, pub email: String, /// Which mailbox is behind this account, and therefore which `Provider` drives it. Read by the /// frontend to know whether a scope list means anything here: an IMAP account has none. pub kind: AccountKind, /// The display name from the provider's profile, editable in settings. pub name: String, /// One of the eight hues, as a token name (`hue-1`), not a hex. The stylesheet owns the value. pub color: String, pub connected: bool, /// What Google actually granted. Granular consent means a user can untick a scope on the /// consent screen, so every feature that needs one checks this rather than assuming. pub granted_scopes: Vec, /// How far back the mirror keeps this account's mail, in days. Zero means everything. pub window_days: u32, } /// The two kinds of mailbox, which is the one thing above the `Provider` trait that has to know. /// /// Everything else in the app is written against the trait. This exists because a few screens /// genuinely differ: an IMAP account has no scopes to grant, no Google account page to revoke /// from, and a server and port to show in settings that a Google account does not have. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum AccountKind { Google, Imap, } impl Default for AccountKind { /// Every account that existed before there was a second kind is a Google one. fn default() -> Self { AccountKind::Google } } // ------------------------------------------------------------------------------------------- // IMAP and SMTP configuration // ------------------------------------------------------------------------------------------- /// How a socket is protected. /// /// The three names Thunderbird's autoconfig format uses, because every published configuration in /// the world is written in that vocabulary and translating it twice is how the meanings drift. /// `Tls` is what the format calls SSL: TLS from the first byte, on 993 or 465. `StartTls` is a /// plaintext connection upgraded by a command, on 143 or 587. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum Security { Plain, StartTls, Tls, } /// Which family of SASL mechanism to authenticate with. The exact mechanism is chosen from what /// the server advertises; this only says which list to choose from. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum AuthKind { Password, /// Reachable from a discovered configuration, which is why it can be represented. Nothing /// builds one yet: OAuth over IMAP needs a client registered with that provider. OAuth2, } /// One end of a mail account: a host, a port, how the socket is protected and who to log in as. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ServerConfig { pub host: String, pub port: u16, pub security: Security, pub auth: AuthKind, /// Already expanded. `%EMAILADDRESS%` and `%EMAILLOCALPART%` are substituted inside discovery, /// so nothing downstream has to know that the format has placeholders in it. pub username: String, } /// Both ends, and where they came from. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct MailConfig { pub imap: ServerConfig, pub smtp: ServerConfig, /// Which rung of the ladder answered: `autoconfig`, `ispdb`, `mx`, `probe` or `manual`. The /// connect screen says where the settings came from, because "we found these" and "we guessed /// these" are different promises and a person about to type a password should be told which. pub source: String, /// The provider's own name for itself, when the configuration carried one. pub display_name: Option, } /// A certificate the user has to decide about before a connection can be made. /// /// Only ever raised for a host that is not the loopback. Proton Bridge listens on 127.0.0.1 with a /// certificate it generated itself, and there is nothing between this process and that socket to /// impersonate anybody, so loopback is trusted without asking. Every other host is a question. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CertQuestion { pub host: String, pub port: u16, /// SHA-256 of the DER, uppercase hex in colon-separated pairs, which is the form every other /// tool prints so it can be compared against one. pub fingerprint: String, pub subject: String, pub issuer: String, pub expires_ms: i64, /// `self-signed`, `expired` or `unknown-issuer`. A hostname mismatch is never a question: it /// is the one failure that looks exactly like an interception, so it is refused outright. pub reason: String, } /// What a connection test found. Not a `Result`, because "the password was wrong" and "the /// certificate needs a decision" are answers the screen renders rather than errors it reports. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ConnectReport { pub ok: bool, /// What kind of refusal it was: `unreachable`, `auth`, `certificate`, `wrong-host` or `other`. /// /// A separate field rather than something the screen reads back out of `message`, because the /// screen has real decisions hanging off it (a loopback that is unreachable means the bridge /// is not running, and that is a different sentence from a wrong password) and reading them /// out of prose means a regex over whatever the server happened to say that day. pub kind: Option, /// `imap` or `smtp`, when one leg failed and the other did not. pub failed: Option, /// One sentence, already fit to read. The server's own words when they are usable. pub message: Option, pub cert: Option, /// Set when the server refused the password but named a reason a person can act on, such as /// an app password being required. pub advice: Option, } /// Every view the app can show. `Label` and `Search` carry an argument in `ThreadQuery`. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum Place { Inbox, Feed, PaperTrail, ReplyLater, SetAside, Screener, Snoozed, Everything, Sent, Drafts, Starred, ScreenedOut, Spam, Trash, Label, Search, } /// Where a sender's mail goes. Exactly one per sender, which is the whole of the routing model. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum Destination { Inbox, Feed, PaperTrail, ScreenedOut, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum Pile { ReplyLater, SetAside, } /// The surface a message body renders on, decided from what the sender painted rather than from /// how the bytes arrived. `sanitize::surface` is where it is worked out and why. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum Surface { /// The sender painted nothing, so the body takes the app's own paper and follows the theme. #[default] Theme, /// The sender painted an opaque light page across the layout, so the body keeps it in both /// palettes and the pane's chrome goes dark around it. Paper, } /// What a list asks for. `account_id` of None means every account, which is the All accounts view. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ThreadQuery { #[serde(default)] pub account_id: Option, pub place: Place, /// The provider's label id, for `Place::Label`. #[serde(default)] pub label_id: Option, /// The query text, for `Place::Search`. #[serde(default)] pub query: Option, pub limit: u32, /// Opaque, from the previous page's `next_cursor`. None starts at the top. #[serde(default)] pub cursor: Option, } // ------------------------------------------------------------------------------------------- // Threads // ------------------------------------------------------------------------------------------- /// One row of a list, already grouped and already ordered. /// /// `key` is the portable thread key: the first entry of the message's `References` header, else its /// `In-Reply-To`, else its own `Message-ID`. It does not depend on which messages happen to be /// mirrored, so it survives a storage window changing, a second device, and a move to another /// provider. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ThreadSummary { pub key: String, pub account_id: String, /// The account's hue token, for the coloured edge in All accounts. pub account_color: String, /// What to print. The rename when there is one, the real subject otherwise. pub subject: String, /// Set only when the thread was renamed, so the row can say "renamed · was …". pub original_subject: Option, /// Whose name goes on the row: the latest correspondent who is not the account. pub from: Person, /// Everyone in the thread, for the stacked avatars in the pane. pub participants: Vec, pub snippet: String, /// Epoch milliseconds of the latest message. pub date_ms: i64, pub message_count: u32, /// At least one message the account has not seen. There is no count anywhere in this app. pub unseen: bool, pub starred: bool, /// In the provider's trash, and in its spam. Flags rather than places: a search result carries /// them into a list that is not Trash or Spam, and neither the row nor the pane can work them /// out from the place it is being shown in. pub trashed: bool, pub spam: bool, pub has_attachment: bool, pub has_draft: bool, pub pile: Option, /// Epoch milliseconds: the moment this thread was due back. /// /// In the future while it is still waiting, in the past once it has come back and is sitting in /// the Back group, which is how a thread that returned late can say "Due yesterday" rather than /// pretending. The group tells the two apart, so a row never has to guess. pub snoozed_until: Option, pub ignored: bool, pub notify: bool, pub merged: bool, /// The one line under the row. The whole note is in the pane. pub note: Option, /// Which group head this row sits under: "back", "new", "seen", "this-week", "earlier" or /// "sent-to". The view decides both the grouping and the order the groups come in, and the /// frontend renders a head whenever this changes and never sorts again. Sorting twice is how /// Back ends up in the middle of the Inbox. pub group: String, /// Waiting in the outbox, so the row can say "Waiting to send". pub sending: bool, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ThreadPage { pub threads: Vec, /// Feed the next call. None when this is the end of the list. pub next_cursor: Option, /// The quiet line at the foot: "Showing the last month. Older mail is on Gmail." or the /// search's "Search older mail on Gmail". None when there is nothing to say. pub footer: Option, } /// A thread this one was merged from, for the banner that offers Unmerge. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct MergedSource { pub key: String, pub subject: String, } /// The whole thread, for the reading pane. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ThreadView { pub key: String, pub account_id: String, pub subject: String, pub original_subject: Option, pub participants: Vec, pub messages: Vec, pub notes: Vec, pub merged_from: Vec, pub pile: Option, pub snoozed_until: Option, pub ignored: bool, pub notify: bool, pub starred: bool, /// The same two flags the row carries, so the pane can offer the way back out. pub trashed: bool, pub spam: bool, /// The provider's labels on this thread, for the label list. Never rendered in a row. pub labels: Vec, } // ------------------------------------------------------------------------------------------- // Messages // ------------------------------------------------------------------------------------------- #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Attachment { pub id: String, pub message_id: String, pub filename: String, pub mime_type: String, pub size: u64, /// Referenced from the body by `cid:` rather than listed as a chip. pub inline: bool, pub content_id: Option, /// Already in the local cache, so opening it costs nothing. pub cached: bool, } /// A remote image that was removed before the body was rendered, and who it belonged to. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Tracker { /// The vendor from the shipped list, or the bare host when it is not a known one. pub vendor: String, pub url: String, } /// `List-Unsubscribe`, in the three forms it arrives in. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Unsubscribe { /// RFC 8058: the app POSTs and the sender is required to honour it without a round trip. pub one_click: bool, pub mailto: Option, pub url: Option, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum InviteResponse { Accepted, Tentative, Declined, NeedsAction, } /// A `text/calendar` part with `METHOD:REQUEST`, rendered as a card. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Invite { pub uid: String, pub summary: String, pub start_ms: i64, pub end_ms: i64, pub all_day: bool, pub location: Option, pub organizer: Option, pub description: Option, /// What this account has already answered, when it has. pub my_response: InviteResponse, /// A deep link into Margin Calendar, when the event can be addressed there. pub calendar_link: Option, } /// One message, sanitised and ready to render. /// /// `html` is what goes into the iframe's `srcdoc`. It has been through the sanitiser, so `cid:` /// images are already `data:` URIs and every remote image is either removed or, once the user has /// asked for them, fetched by Rust and inlined the same way. The frontend never fetches anything. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct MessageView { /// The provider's message id. Every command argument named `message_id` is this one, never the /// header below it: the RFC `Message-ID` is what the state database keys on and it never /// crosses this boundary as an argument. pub id: String, /// The RFC `Message-ID`, which is what the state database keys on. pub message_id: String, pub thread_key: String, pub from: Person, pub to: Vec, pub cc: Vec, pub bcc: Vec, pub reply_to: Vec, pub date_ms: i64, pub subject: String, pub html: String, /// True when this message's body has not been fetched yet, so `html` is empty because there is /// nothing to show rather than because the message was. /// /// Opening a thread never waits on the network. The rows come back from the mirror at once and /// anything still missing a body arrives on a later `store-changed`, which is the difference /// between a thread that opens and a thread that loads. pub body_pending: bool, /// The trailing quoted conversation, split off so it can sit behind a pill. pub quoted_html: Option, /// True when the source was HTML. A plain text message has been converted, and the pane sets /// it on a 46em measure in the text face rather than leaving it to the sender's markup. pub is_html: bool, /// Which surface the body reads on. Not the same question as `is_html`: what decides it is /// whether the sender painted a page, and most HTML mail paints nothing. pub surface: Surface, pub attachments: Vec, pub trackers: Vec, /// Ordinary remote images that were blocked, which is a different count from the trackers. pub blocked_images: u32, #[serde(default)] pub images_allowed: bool, /// Whether the body currently rendered has remote images loaded. pub images_loaded: bool, pub seen: bool, pub draft: bool, pub sent_by_me: bool, pub invite: Option, pub unsubscribe: Option, pub list_id: Option, } // ------------------------------------------------------------------------------------------- // The decisions: rules, piles, snoozes, notes, clips // ------------------------------------------------------------------------------------------- #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SenderRule { pub account_id: String, /// An address, or a domain when `is_domain`. pub subject: String, pub is_domain: bool, pub destination: Destination, pub decided_at_ms: i64, /// The suggestion rule that fired when this was set automatically, for the contact card. pub reason: Option, } /// One waiting sender in the Screener. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ScreenerCard { pub account_id: String, pub sender: Person, pub thread_key: String, pub subject: String, pub snippet: String, pub date_ms: i64, pub suggestion: Destination, /// The sentence the card prints, which is the row of the rules table that matched. pub reason: String, /// How many messages this sender has waiting, when it is more than one. pub waiting: u32, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum SnoozeKind { LaterToday, Tomorrow, Weekend, NextWeek, Date, /// Comes back only if nobody but the account has written since. IfNoReply, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Snooze { pub thread_key: String, pub return_at_ms: i64, pub kind: SnoozeKind, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Note { pub id: String, pub thread_key: String, pub body: String, pub created_at_ms: i64, /// The message that was latest when the note was written, so the pane can place it. pub after_message_id: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Clip { pub id: String, pub account_id: String, pub thread_key: String, pub message_id: String, pub text: String, pub sender: Person, pub subject: String, pub created_at_ms: i64, } /// Everything the app knows about one correspondent, for the popover and the Contacts place. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ContactCard { pub person: Person, pub account_id: String, pub destination: Destination, /// The rule that decides them is on their whole domain rather than their address. pub domain_rule: bool, /// A consumer domain cannot carry a domain rule, so the toggle is not offered. pub domain_rule_allowed: bool, pub notify: bool, pub screened_at_ms: Option, pub note: Option, pub allow_remote_images: bool, /// Trash this sender's Feed mail after so many days. None is never. pub auto_trash_days: Option, /// Bundle this sender's Paper Trail rows into one. pub bundle: bool, pub recent_threads: Vec, pub files: Vec, pub unsubscribe: Option, } /// The patch the contact card writes back. An absent field means unchanged. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ContactPatch { #[serde(default)] pub destination: Option, #[serde(default)] pub domain_rule: Option, #[serde(default)] pub notify: Option, #[serde(default)] pub note: Option, #[serde(default)] pub allow_remote_images: Option, #[serde(default)] pub auto_trash_days: Option>, #[serde(default)] pub bundle: Option, } // ------------------------------------------------------------------------------------------- // Writing // ------------------------------------------------------------------------------------------- /// A file on the way out, before it has been encoded into a message. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct DraftAttachment { /// Absent for one that is already in the mirror, which is what a forward carries. #[serde(default)] pub path: Option, /// The mirror's attachment id, for a forward. #[serde(default)] pub attachment_id: Option, pub filename: String, pub mime_type: String, pub size: u64, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Draft { /// The app's own draft id. Absent on the first save. #[serde(default)] pub id: Option, pub account_id: String, /// The thread being replied to, which is what sets the threading headers. #[serde(default)] pub thread_key: Option, /// The message being replied to or forwarded. #[serde(default)] pub in_reply_to: Option, /// A verified alias to send as, when it is not the account's own address. #[serde(default)] pub from_alias: Option, pub to: Vec, #[serde(default)] pub cc: Vec, #[serde(default)] pub bcc: Vec, pub subject: String, /// The editor's HTML. Rust inlines the stylesheet and builds the plain text alternative. pub body_html: String, #[serde(default)] pub attachments: Vec, /// Set a reminder on the thread if nobody replies by then. #[serde(default)] pub remind_at_ms: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct DraftSaved { pub id: String, pub updated_at_ms: i64, /// The encoded size, so the composer can refuse an attachment before the send fails. pub encoded_size: u64, pub over_limit: bool, } /// A send that is waiting out its undo delay, or waiting for the network. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Outgoing { pub id: String, pub account_id: String, pub thread_key: Option, /// Who the toast names. pub to: Vec, pub subject: String, /// Epoch milliseconds. Until then the send can still be taken back. pub hold_until_ms: i64, pub attempts: u32, pub last_error: Option, } // ------------------------------------------------------------------------------------------- // Undo // ------------------------------------------------------------------------------------------- /// What a mutating command hands back so `z` can take it back. /// /// The token is a handle on the state before the change, held in a bounded stack in Rust. It is /// there rather than in the frontend because a bulk archive of forty threads with mixed prior /// state cannot be reversed from what the frontend knew, and a reversal that guesses is worse than /// no reversal at all. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Undo { pub token: String, /// What the toast says: "Archived 3 threads", "Sent to Ana". pub label: String, /// Zero for anything but a send. A send's toast counts down. pub undo_ms: u32, } // ------------------------------------------------------------------------------------------- // Search, labels, files // ------------------------------------------------------------------------------------------- #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SearchResult { pub page: ThreadPage, /// The query reached past the storage window, so the provider was asked as a second pass. pub provider_searched: bool, /// A note appended under the results when the two sources differ. pub note: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct LabelInfo { pub id: String, pub account_id: String, pub name: String, /// system or user. The system ones are places already and are not offered for applying. pub kind: String, } /// One card in the All files place. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct FileCard { pub attachment: Attachment, pub thread_key: String, pub subject: String, pub sender: Person, pub date_ms: i64, /// images, pdfs, documents, spreadsheets, presentations, invites, archives, other pub category: String, } // ------------------------------------------------------------------------------------------- // Settings // ------------------------------------------------------------------------------------------- /// margin-shared's `FontRef`, which is either one of the six bundled faces or a family off the /// machine. Stored rather than a bare family name, because a system font can be called "Literata" /// and must not come back as the bundled one. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "kind", rename_all = "camelCase")] pub enum FontRef { #[serde(rename_all = "camelCase")] Bundled { id: String }, #[serde(rename_all = "camelCase")] System { family: String }, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AccountSettings { pub account_id: String, pub name: String, pub color: String, /// 30, 90, 180, 365, or 0 for everything. pub window_days: u32, pub signature: String, pub aliases: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SnoozeTimes { /// Hours from now for Later today. pub later_today_hours: u32, /// Local time of day, in minutes from midnight. pub tomorrow_at: u32, pub weekend_at: u32, pub next_week_at: u32, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct BackupSettings { /// none, drive or r2 pub store: String, pub configured: bool, pub last_backup_ms: Option, /// Shown once, at setup. Never returned again. pub has_phrase: bool, /// R2 only. The secret never crosses this boundary in the reading direction. pub r2_bucket: Option, pub r2_endpoint: Option, } /// Everything the settings screen edits. Device settings live in `settings.json` in the app data /// directory, written atomically; the per account decisions that should roam live in the state /// database and its journal instead, and are edited through the contact card and the account /// sections rather than through this blob. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Settings { /// light, dark or system pub theme: String, pub font_ui: FontRef, pub font_text: FontRef, pub text_size: u32, pub reading_pane: bool, /// comfortable or compact, on the phone pub density: String, pub accounts: Vec, pub attachment_cache_mb: u32, pub prefetch_bodies: bool, /// never, ask or always. The per sender allowances are not here: they live on the contact, /// because they are a decision about a person and they roam with the rest of those. pub remote_images: String, pub link_cleaning: bool, #[serde(default)] pub otp_autofill_enabled: bool, #[serde(default)] pub app_lock_enabled: bool, #[serde(default = "on")] pub spelling_enabled: bool, #[serde(default)] pub grammar_enabled: bool, #[serde(default)] pub writing_tools_enabled: bool, pub screener_enabled: bool, /// A reply to a thread you are in is never held. Turning this off holds it anyway. pub hold_replies: bool, pub suggestions: bool, pub snooze_times: SnoozeTimes, /// The pile a swipe reaches on the phone: reply-later, set-aside, archive, trash or none. pub swipe_right: String, pub swipe_left: String, pub feed_auto_trash_days: u32, pub undo_delay_secs: u32, pub reply_all_default: bool, pub instant_intro: String, pub badge: bool, /// The switch over everything below: off, and nothing is posted whatever a thread, a person or /// a place says. On by default, and absent from files written before it existed. #[serde(default = "on")] pub notifications: bool, /// The places that notify without being asked thread by thread. Empty is the default, which is /// the product's position: nothing tells you anything until you say so. pub notify_places: Vec, pub backup: BackupSettings, } fn on() -> bool { true } /// Whether the system will show this app's notifications: what System Settings says on macOS, and /// Prompt until the app has asked once. Everywhere else the answer is Granted. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum NotifyPermission { Granted, Denied, Prompt, } /// Where a click on a notification goes: the account it was about, the place its thread shows in, /// and the thread itself when the notification was about one. Several messages in one pass are /// one notification, and a click on that goes to the place alone. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct NotifyTarget { pub account_id: String, pub place: Place, pub thread_key: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct StorageUsed { pub account_id: String, pub messages: u64, pub threads: u64, pub mirror_bytes: u64, pub bodies_bytes: u64, pub attachments_bytes: u64, pub state_bytes: u64, /// The date of the oldest message on the device, which is what the window setting shows. pub oldest_ms: Option, } // ------------------------------------------------------------------------------------------- // Sync, auth and events // ------------------------------------------------------------------------------------------- /// Per account, because one account being signed out must not be reported as the app being broken. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SyncStatus { pub account_id: String, /// idle | syncing | hydrating | caching | backfilling | offline | error | paused pub phase: String, pub last_sync_ms: Option, pub error: Option, pub pending_writes: u32, /// The line the progress screen and the account chip show, when there is one. pub message: Option, /// Whatever fill is running: the first sync's metadata crawl, and then the body cache behind /// it. Both zero when there is nothing left to bring in. pub hydrated: u32, pub total: u32, /// How far back the mirror actually reaches, which is not the same as the window setting until /// the first sync has finished. pub oldest_ms: Option, } impl SyncStatus { pub fn idle(account_id: &str) -> Self { SyncStatus { account_id: account_id.to_string(), phase: "idle".to_string(), last_sync_ms: None, error: None, pending_writes: 0, message: None, hydrated: 0, total: 0, oldest_ms: None, } } } /// Payload of the `auth` event. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AuthEvent { pub ok: bool, pub error: Option, pub account_id: Option, pub email: Option, /// The user closed the consent browser rather than anything going wrong. Still not `ok`, /// because no account arrived, but changing your mind is not a failure. pub cancelled: bool, /// What was granted, which may be less than what was asked for. pub granted_scopes: Vec, /// Set when the account was refused because a scope the app cannot work without was withheld. pub missing_required: Vec, } /// The flags a triage action sets, and the only thing in this file the provider ever hears about. /// An absent field means unchanged. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct FlagPatch { #[serde(default)] pub seen: Option, #[serde(default)] pub starred: Option, #[serde(default)] pub archived: Option, #[serde(default)] pub trashed: Option, #[serde(default)] pub spam: Option, }