mirror of
https://github.com/priyanshujain/margin-mail.git
synced 2026-10-02 11:07:06 +00:00
370 lines
19 KiB
Markdown
370 lines
19 KiB
Markdown
# Features
|
|
|
|
The specification for version one. Each feature says what it does, how it is invoked, the rules it
|
|
follows, where its state lives, and what the provider (Gmail for now) sees. "Local" means the
|
|
portable state database described in [architecture.md](architecture.md), which roams through the
|
|
backup store and never depends on the provider. "Provider" means a change made to the mailbox
|
|
itself. The reasoning behind the choices is in [design.md](design.md); the screens are in
|
|
[ui.md](ui.md); every key is in [keyboard.md](keyboard.md).
|
|
|
|
## 1. Places
|
|
|
|
A place is a view. Numbers reach the seven daily ones, `0` reaches Everything, and the palette
|
|
reaches all of them.
|
|
|
|
| Key | Place | What it holds |
|
|
|---|---|---|
|
|
| `1` | Inbox | Threads from senders routed to Inbox, plus any reply to a thread you are in |
|
|
| `2` | Feed | Threads from senders routed to Feed, rendered open |
|
|
| `3` | Paper Trail | Threads from senders routed to Paper Trail |
|
|
| `4` | Reply later | The Reply later pile |
|
|
| `5` | Set aside | The Set aside pile |
|
|
| `6` | Screener | First messages from senders with no decision yet |
|
|
| `7` | Snoozed | Threads waiting to return, with their return time |
|
|
| `0` | Everything | Every thread in the account, including archived, in date order |
|
|
| palette | Sent, Drafts, Starred, Screened out, Spam, Trash | The usual folders |
|
|
| palette | All files, Clips, Contacts | The libraries |
|
|
| palette | Labels | The provider's labels or folders, one place each |
|
|
|
|
Every place except Feed, Screener and Focus & Reply is a list column beside the reading pane.
|
|
Feed and Screener take the whole stage because their content is inline. A place remembers its
|
|
scroll position and selection while the app is open.
|
|
|
|
## 2. Routing and the Screener
|
|
|
|
### Destinations
|
|
|
|
Every sender has exactly one destination: Inbox, Feed, Paper Trail, or Screened out. The rule is
|
|
keyed on the sender's address, or on the domain when the user chose "everyone at this domain".
|
|
Address rules beat domain rules. Consumer domains (gmail.com, outlook.com, yahoo.com, icloud.com,
|
|
proton.me, hey.com and a maintained list) cannot carry a domain rule.
|
|
|
|
Two overrides apply before the sender rule:
|
|
|
|
- A message whose `In-Reply-To` or `References` points at a thread the account is already in goes
|
|
where that thread is, or to the Inbox if the thread was screened. A reply is never held.
|
|
- A message from an address in the account's contacts, or one the account has ever sent to, is
|
|
screened in on first run and routed by the suggestion rules, never held.
|
|
|
|
### The Screener
|
|
|
|
A message from a sender with no rule is held: it is not shown in any box, and the Inbox shows a
|
|
pill "Screen N new senders". The Screener place lists one card per sender: avatar, name, address,
|
|
subject, snippet, and a one-line reason with the suggested destination. Keys:
|
|
|
|
- `y` accepts the suggestion and sets the rule for the address.
|
|
- `v` opens the destination picker: Inbox, Feed, Paper Trail, and a toggle for "everyone at
|
|
this domain".
|
|
- `n` screens the sender out. Nothing is sent. Their mail is routed to Screened out from then on.
|
|
- `Enter` expands the card to show the whole message, with a Reply button that screens the
|
|
sender into the Inbox and opens a reply.
|
|
- Clear all screens out every sender currently waiting, after a confirmation.
|
|
|
|
Screened out mail is kept for 90 days in the Screened out place, then trashed. Reversing a
|
|
decision is done from the sender's contact card, and re-screening someone in brings back whatever
|
|
they sent in the last 90 days.
|
|
|
|
### Suggestions
|
|
|
|
The suggestion is a deterministic function of the first message, and the reason shown is the rule
|
|
that fired. In order:
|
|
|
|
1. Written by a person: no `List-Id`, no `List-Unsubscribe`, no `Precedence: bulk` or `list`, no
|
|
`Auto-Submitted`, a `From` local part that is not `noreply`, `no-reply`, `donotreply`,
|
|
`notifications`, `mailer`, `bounce` or similar. Suggest Inbox.
|
|
2. Carries `List-Unsubscribe`, `List-Id` or `Precedence: bulk`, or Gmail put it in
|
|
`CATEGORY_PROMOTIONS`. Suggest Feed.
|
|
3. Sent by a service for a person, or transactional: a no-reply local part, Gmail's
|
|
`CATEGORY_UPDATES`, or a subject matching receipt, order, confirmation, invoice, shipped,
|
|
payment, verify, code, ticket, itinerary, reservation. Suggest Paper Trail.
|
|
4. Anything else: Inbox.
|
|
|
|
Rule 3 wins over rule 2 when both match, because a receipt with an unsubscribe footer is still a
|
|
receipt. The rules are a table in the source and the reason strings are the table's rows, so a
|
|
wrong suggestion is a one-line fix.
|
|
|
|
### First run
|
|
|
|
When an account is added, every sender in the mirror is screened in with a rule set by the same
|
|
suggestion function, silently. The user can move any sender from the contact card, and the move
|
|
applies to that sender's existing threads immediately. Only senders whose first message arrives
|
|
after the account was added are held in the Screener.
|
|
|
|
Alongside this, a first-run panel offers "Start fresh": mark everything older than a chosen age
|
|
(default one week) as seen, so New for you holds only what is recent. This is the only bulk
|
|
write to the provider the app ever proposes, it is optional, and it is reversible for seven days
|
|
from the palette.
|
|
|
|
State: sender rules are local, keyed on address or domain. Provider: nothing. A held or
|
|
screened-out message keeps whatever labels Gmail gave it.
|
|
|
|
## 3. The Inbox
|
|
|
|
Two groups, fixed order, newest first within each: New for you (threads with at least one message
|
|
the account has not seen) and Previously seen (everything else routed to Inbox, including sent
|
|
threads). A third group, Back, appears above New for you when a snoozed thread has returned, and
|
|
holds it until it is opened.
|
|
|
|
- Opening a thread marks it seen. `u` toggles seen on the selected thread. Mark all as seen is a
|
|
link on the New for you heading and a palette command.
|
|
- A new message in a Previously seen thread moves the thread to New for you.
|
|
- `e` archives: the thread leaves the Inbox and lives in Everything. A new message in an archived
|
|
thread brings it back to New for you. Archive is a provider change (Gmail: remove `INBOX`).
|
|
- There are no counts on the groups, on the place, or on the app icon. A dock badge for New for
|
|
you exists as a setting and is off.
|
|
- A note on a thread shows as a single line under its row.
|
|
|
|
Seen state is the provider's read state (Gmail: `UNREAD`), so it is not app state. Everything
|
|
else about the Inbox is a view over the mirror plus the sender rules.
|
|
|
|
## 4. The Feed
|
|
|
|
The Feed renders every thread routed to it as an open card: brand avatar, sender and address,
|
|
time, the subject as a title, and the message body. A card longer than one screen truncates with
|
|
a fade and "Read more"; `Enter` on the focused card expands it in place, `Enter` again collapses.
|
|
A hairline reading "You left off here" marks the newest card that was on screen at the end of the
|
|
last visit.
|
|
|
|
- No read state, no counts, no New for you. Time is the only order.
|
|
- Card actions: Read more, Save clip, Unsubscribe, Move (to Inbox or Paper Trail, for this
|
|
sender), Set aside, Reply later, Archive, Trash. All with keys.
|
|
- `j` and `k` move between cards and scroll the focused card into view.
|
|
- Remote images obey the same rules as the reading pane. Trackers are stripped and the top of the
|
|
Feed says how many today.
|
|
- Feed threads older than a configurable age (default never; HEY defaults to 90 days) can be
|
|
trashed automatically per sender from the contact card.
|
|
|
|
## 5. The Paper Trail
|
|
|
|
A flat list, newest first, of threads routed to it. No read state. A sender with more than one
|
|
thread in the last seven days is bundled into one row showing the count and the latest subject;
|
|
`Enter` expands the bundle in place, and bundling can be turned off per sender from the contact
|
|
card and off entirely in settings. The reading pane works as in the Inbox. Verbs are the same as
|
|
the Inbox minus the seen toggle, plus Move to Inbox.
|
|
|
|
## 6. The piles
|
|
|
|
### Reply later
|
|
|
|
`l` on a thread, or on a selection, moves it out of its list into the Reply later pile. The pile
|
|
is a stack of cards at the foot of the list column showing the top thread's subject and sender;
|
|
`4` or a click opens the Reply later place, which lists the pile with the reading pane. `l` again
|
|
returns the thread to where it came from. Sending a reply on a Reply later thread clears it from
|
|
the pile with an undo toast.
|
|
|
|
### Set aside
|
|
|
|
`s` does the same into the Set aside pile at the bottom right. `5` opens the place. `s` again
|
|
returns the thread. Set aside is for reference, so a thread can sit there indefinitely; nothing
|
|
nags.
|
|
|
|
### Focus & Reply
|
|
|
|
`Shift+F`, the palette, or the button in the Reply later place. A page listing every Reply later
|
|
thread, each with its latest message on the left and a reply box on the right. `Tab` moves to the
|
|
next item, `Cmd+Enter` sends and collapses the item to a "Sent to" line, `Esc` leaves the page.
|
|
Items you skip stay in the pile.
|
|
|
|
State: pile membership and order are local, keyed on the thread key. Provider: nothing. A piled
|
|
thread keeps its Gmail labels; it simply does not render in the Inbox list.
|
|
|
|
## 7. Snooze and reminders
|
|
|
|
`b` opens the snooze picker on a thread or selection: Later today (in three hours), Tomorrow
|
|
(8:00), This weekend (Saturday 9:00), Next week (Monday 8:00), Pick a date and time, and If no
|
|
reply by (a date; default tomorrow). The thread leaves its list and appears in the Snoozed place
|
|
with its return time.
|
|
|
|
When a device opens the app, comes to the foreground, or wakes from sleep, it evaluates every
|
|
snooze whose time has passed and returns those threads to the Back group at the top of the Inbox
|
|
(or the top of the Feed or Paper Trail if that is where they live). If no reply by returns the
|
|
thread only if nobody but the account has written to it since; a reply cancels the reminder and
|
|
the reply lands as normal. A returned thread stays in Back until opened.
|
|
|
|
Nothing runs in the background and nothing fires at an exact time. The Snoozed place shows the
|
|
return time so the user can see what is pending, and a thread that returns late says "Due
|
|
yesterday" rather than pretending.
|
|
|
|
In compose and inline reply, Remind me if no reply is a toggle in the footer with the same date
|
|
picker. It applies to the sent thread after the send completes.
|
|
|
|
State: snooze entries are local (thread key, return time, kind). Provider: nothing.
|
|
|
|
## 8. Reading
|
|
|
|
The reading pane shows the selected thread. Subject in the text face, participants and message
|
|
count beneath, then messages separated by hairlines. Older messages collapse to a one-line
|
|
preview; the latest is open. `n` and `p` move between messages, `o` expands or collapses the
|
|
focused message, `Shift+O` expands all. Quoted text is collapsed behind a pill.
|
|
|
|
- Message bodies render in a sandboxed webview with scripts, forms and external styles removed.
|
|
Remote images are blocked by default; a banner says how many trackers were stripped and names
|
|
the vendor; Show images loads them for this message, and the contact card can allow them for a
|
|
sender always. Attachments are chips; images and PDFs preview inline on demand.
|
|
- Attachments are fetched when the thread is opened, not during sync, and cached.
|
|
- A calendar invite (`text/calendar` with `METHOD:REQUEST`) renders as a card: date, title,
|
|
time, location, organiser, and Accept (`y`), Maybe (`m`), Decline (`n`), plus Open in Margin
|
|
Calendar. RSVP goes through the Calendar API on the invited calendar; when the event is not
|
|
there yet it is imported first. Without the Calendar scope the card still renders read-only.
|
|
- Links show their real destination on hover and open with known tracking parameters removed.
|
|
- Read together: select several threads with `x` and press `Enter`; the pane shows them one
|
|
after another with a heading each.
|
|
- The pane can be hidden (`Cmd+\`); the list then takes the width and `Enter` opens a thread in
|
|
place, HEY style, with `Esc` returning to the list.
|
|
|
|
## 9. Writing
|
|
|
|
### Reply and new mail
|
|
|
|
`r` replies to the sender, `a` replies to all, `f` forwards; each opens a box under the last
|
|
message with the recipients shown as chips and reply-all as a one-key switch. `c` opens the
|
|
compose card floating over the list, bottom right; the expand button or `Cmd+Shift+P` makes it
|
|
the whole window. Drafts save locally as you type and to the provider every few seconds, so a
|
|
draft roams the way Gmail drafts always have.
|
|
|
|
The editor is TipTap, as in margin: paragraphs, bold, italic, links, lists, quotes, code. No
|
|
colours, no fonts. Plain-text mail is sent as plain text. The signature comes from the provider's
|
|
settings for the sending address and is editable in settings.
|
|
|
|
### Sending
|
|
|
|
`Cmd+Enter` sends. Every send is held for ten seconds (five, twenty or thirty in settings) with
|
|
a toast "Sent to X · Undo"; `z` or the toast cancels and reopens the draft. `Cmd+Shift+Enter`
|
|
sends immediately. Sends that fail or happen offline wait in the outbox and retry, and the thread
|
|
shows a "Waiting to send" line until they go. Threading headers (`In-Reply-To`, `References`,
|
|
the provider's thread id, a matching subject) are always set so replies land in the thread on
|
|
both ends.
|
|
|
|
### Remind me if no reply
|
|
|
|
A toggle in the send footer with a date. See section 7.
|
|
|
|
### Instant intro
|
|
|
|
`Cmd+Shift+I` in a reply to an introduction moves the introducer to Bcc and inserts a thank-you
|
|
line from a template that can be edited in settings. Pressing it again reverts.
|
|
|
|
### Attachments
|
|
|
|
Drag and drop, paste, or `Cmd+Shift+A`. The composer refuses anything that would push the
|
|
encoded message over the provider's limit (Gmail: 35 MB total) and says so before you try to
|
|
send.
|
|
|
|
### Not in this version
|
|
|
|
Snippets, send later, and any AI assistance. The compose footer leaves room for them.
|
|
|
|
## 10. The things HEY owns, kept beside the mail
|
|
|
|
### Notes
|
|
|
|
`y` on a thread adds a private note. In the pane it is a block after the message that was latest
|
|
when it was written, dated; in the list it is one line under the row. Text only in this version.
|
|
Local, keyed on the thread key.
|
|
|
|
### Rename
|
|
|
|
Click the subject or run Rename from the palette. The pane shows the new name with "renamed ·
|
|
was …" beside it, the list shows the new name, and replies still carry the real subject so
|
|
threading holds on both ends. Local.
|
|
|
|
### Merge
|
|
|
|
Select two or more threads and press `g`. They become one thread in every list and in the pane,
|
|
named after the longest one or a name you type. A banner on the merged thread says where it came
|
|
from and offers Unmerge. Replies and new messages in any underlying thread appear in the merged
|
|
one. Local: a mapping from the underlying thread keys to a merged key.
|
|
|
|
### Clips
|
|
|
|
Select text in any message and the Save clip button appears; `Cmd+Shift+C` also works. The Clips
|
|
place lists every clip with its sender, thread and date, and each links back. Local.
|
|
|
|
### All files
|
|
|
|
The All files place lists every attachment in the mirror as a card with name, type, size, sender
|
|
and thread, newest first, with filters by type (images, PDFs, documents, spreadsheets,
|
|
presentations, calendar invites, archives, other) and by sender. Signature junk (images under
|
|
10 KB referenced inline) is excluded. Opening a card opens the thread with the attachment
|
|
focused. Built from the local index; nothing is fetched until you open one.
|
|
|
|
### Ignore
|
|
|
|
`m` on a thread. New messages still arrive and append, but the thread never returns to New for
|
|
you and never notifies. A banner on the thread says "You are ignoring this thread" with Stop
|
|
ignoring. Local.
|
|
|
|
### Notifications
|
|
|
|
Off by default everywhere. `Shift+N` on a thread turns them on for that thread; the contact card
|
|
turns them on for a person. A notification shows the sender and subject, and opening it opens the
|
|
thread. There is no badge unless the setting is turned on. Local.
|
|
|
|
## 11. Contacts and the contact card
|
|
|
|
`i` on a thread, or clicking a name or avatar anywhere, opens the contact card as a popover:
|
|
avatar, name, address, then Delivers to (the destination, changeable), Notify, Screened (in or
|
|
out, with the date), Note, Recent threads, Files, and Unsubscribe when the sender's mail carries
|
|
`List-Unsubscribe`. The Contacts place lists every sender with a rule and lets you search them.
|
|
|
|
Autocomplete in compose draws first on the addresses in the mirror (everyone you have written to
|
|
or received from, ranked by recency and frequency) and second on the provider's contacts
|
|
(Google: People API `otherContacts` and `connections`).
|
|
|
|
State: notes, delivery, notify are local. Provider: nothing.
|
|
|
|
## 12. Unsubscribe, block, spam, trash
|
|
|
|
- Unsubscribe (`Cmd+U`, the Feed card, or the contact card): with an RFC 8058 one-click header
|
|
the app POSTs and confirms; with a `mailto:` header it sends the message; otherwise it opens the
|
|
link. Either way it offers "and trash everything from them" and "and screen them out".
|
|
- Screen out from the contact card is the block: future mail goes to Screened out. Nothing is sent.
|
|
- `!` marks spam (provider), `#` trashes (provider), both with undo. Trash empties after 30 days
|
|
on the provider's schedule; the Trash place has an Empty button.
|
|
|
|
## 13. Selection and bulk actions
|
|
|
|
`x` selects the focused row, `Shift+J` and `Shift+K` extend, `Cmd+A` selects all from here,
|
|
`Esc` clears. While a selection exists the piles are replaced by an action bar with the same verbs
|
|
and keys: Reply later, Set aside, Snooze, Mark seen, Archive, Move, Merge, Ignore, Trash, and
|
|
Enter for Read together. Every bulk action is one undo.
|
|
|
|
## 14. Search
|
|
|
|
`/` focuses search. Results replace the list column and the reading pane works as usual. Search
|
|
is local over the full mirror: subject, participants, snippet and body text, with operators
|
|
`from:`, `to:`, `subject:`, `has:attachment`, `filename:`, `in:` (any place), `before:` and
|
|
`after:`, `label:`. When the query touches mail that is not yet hydrated, the provider's search
|
|
runs as a second pass and its results append with a note. Results open in place and `Esc`
|
|
returns to the previous place.
|
|
|
|
## 15. Accounts
|
|
|
|
Add as many Gmail accounts as you like. Each has its own places, sender rules, piles and
|
|
Screener. The account chip in the title bar switches (`Ctrl+1` to `Ctrl+9`) and offers All
|
|
accounts (`Ctrl+0`), which merges every account's version of the current place into one list with
|
|
a coloured edge on each row. Compose picks the account from the thread you are replying to, or
|
|
the account you are looking at, and the From field switches it. Sent mail goes through the
|
|
sending account; an alias verified on the provider can be chosen in From.
|
|
|
|
## 16. Labels
|
|
|
|
The provider's labels or folders are places, listed under Labels in the palette. `Shift+L`
|
|
applies or removes one on the selected threads, `v` moves (applies a label and archives). They
|
|
are the provider's, they roam with the mailbox, and they are not how Margin organises anything.
|
|
|
|
## 17. Settings and export
|
|
|
|
`Cmd+,` opens settings as a panel: Accounts (add, remove, signature, aliases), Backup (Google
|
|
Drive or Cloudflare R2, and the recovery phrase), Appearance (theme, reading pane, row density
|
|
for the phone), Sending (undo delay, reply-all default, instant intro text), Privacy (remote
|
|
images, link cleaning, per-sender allowances), Notifications (badge, sound), Keyboard (the keymap
|
|
file), Data (export mail as mbox per account, export app state as JSON, import app state).
|
|
|
|
## 18. Not in version one
|
|
|
|
Send later. Snippets. Any AI. Shared threads, team comments, read statuses. Workflows, collections,
|
|
cover art. A calendar sidebar. Unified search across accounts (search is per account until the
|
|
unified view is proven). IMAP and JMAP providers (the trait is there; the implementations come
|
|
after Gmail is solid).
|