scaffold the app: tauri crate, vite front end, icons, ci
No files matched your search
@@ -0,0 +1,92 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
# The two checkouts reproduce the layout on a working machine: this repository sits at
|
||||
# rust/margin-mail and the public margin repository beside it at python/margin, because
|
||||
# package.json depends on margin-shared through a relative path. A token edit there is meant to
|
||||
# show up in every Margin app at once, and a copy vendored here would defeat that.
|
||||
jobs:
|
||||
frontend:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: rust/margin-mail
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
path: rust/margin-mail
|
||||
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
repository: priyanshujain/margin
|
||||
path: python/margin
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 26
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
# The vendored copy under public/fonts is what the bundle serves, and it drifting from the
|
||||
# package is a face that falls back to Georgia in one app and not the other.
|
||||
- run: pnpm fonts:check
|
||||
|
||||
# `pnpm build` is tsc then vite, so this is the typecheck and the bundle in one step.
|
||||
- run: pnpm build
|
||||
|
||||
- run: pnpm test
|
||||
|
||||
- run: node scripts/docs-check.mjs
|
||||
|
||||
rust:
|
||||
runs-on: ubuntu-22.04
|
||||
defaults:
|
||||
run:
|
||||
working-directory: rust/margin-mail
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
path: rust/margin-mail
|
||||
|
||||
- name: Install Linux dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
libwebkit2gtk-4.1-dev \
|
||||
libgtk-3-dev \
|
||||
libayatana-appindicator3-dev \
|
||||
librsvg2-dev \
|
||||
patchelf \
|
||||
libxdo-dev \
|
||||
libssl-dev \
|
||||
build-essential
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- uses: swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: rust/margin-mail/src-tauri -> target
|
||||
|
||||
# tauri_build::build() wants a frontendDist that exists, and build.rs wants credentials to
|
||||
# embed. The example file is what a fresh clone compiles against, so that is what CI uses.
|
||||
- name: Stub the build inputs
|
||||
run: |
|
||||
mkdir -p dist && touch dist/index.html
|
||||
cp google-credentials.example.json google-credentials.json
|
||||
|
||||
- name: Test
|
||||
working-directory: rust/margin-mail/src-tauri
|
||||
run: cargo test
|
||||
@@ -0,0 +1,193 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Release version, e.g. 0.2.0. Leave empty to bump the patch number."
|
||||
required: false
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
tag: ${{ steps.version.outputs.tag }}
|
||||
release_id: ${{ steps.release.outputs.release_id }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Determine version
|
||||
id: version
|
||||
run: |
|
||||
if [ -n "${{ inputs.version }}" ]; then
|
||||
VERSION="${{ inputs.version }}"
|
||||
VERSION="${VERSION#v}"
|
||||
else
|
||||
CURRENT=$(jq -r .version src-tauri/tauri.conf.json)
|
||||
IFS=. read -r MAJOR MINOR PATCH <<< "$CURRENT"
|
||||
VERSION="$MAJOR.$MINOR.$((PATCH + 1))"
|
||||
fi
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "tag=v$VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "Releasing v$VERSION"
|
||||
|
||||
- name: Bump version in manifests
|
||||
env:
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
tmp=$(mktemp)
|
||||
jq --arg v "$VERSION" '.version = $v' src-tauri/tauri.conf.json > "$tmp" && mv "$tmp" src-tauri/tauri.conf.json
|
||||
jq --arg v "$VERSION" '.version = $v' package.json > "$tmp" && mv "$tmp" package.json
|
||||
sed -i "0,/^version = \".*\"/s//version = \"$VERSION\"/" src-tauri/Cargo.toml
|
||||
|
||||
- name: Commit and tag
|
||||
env:
|
||||
TAG: ${{ steps.version.outputs.tag }}
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add src-tauri/tauri.conf.json package.json src-tauri/Cargo.toml
|
||||
git commit -m "chore(release): $TAG"
|
||||
for attempt in 1 2 3 4 5; do
|
||||
git fetch origin main
|
||||
git rebase origin/main
|
||||
if git push origin HEAD; then
|
||||
break
|
||||
fi
|
||||
if [ "$attempt" = "5" ]; then
|
||||
echo "::error::main kept advancing; could not push release bump after 5 attempts."
|
||||
exit 1
|
||||
fi
|
||||
echo "main advanced during release; rebasing and retrying ($attempt)…"
|
||||
sleep 3
|
||||
done
|
||||
git tag "$TAG"
|
||||
git push origin "$TAG"
|
||||
|
||||
- name: Create draft release
|
||||
id: release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAG: ${{ steps.version.outputs.tag }}
|
||||
run: |
|
||||
gh release create "$TAG" --draft --title "Margin Mail $TAG" --notes "Release $TAG"
|
||||
ID=$(gh release view "$TAG" --json databaseId --jq .databaseId)
|
||||
echo "release_id=$ID" >> "$GITHUB_OUTPUT"
|
||||
|
||||
build:
|
||||
needs: prepare
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: macos-26
|
||||
args: "--target universal-apple-darwin --config src-tauri/tauri.release.conf.json"
|
||||
rust-targets: "aarch64-apple-darwin,x86_64-apple-darwin"
|
||||
# Ubuntu 22.04 is the glibc baseline: the bundle will not run on anything older than the
|
||||
# glibc it was linked against, so build on the oldest supported.
|
||||
- os: ubuntu-22.04
|
||||
args: "--config src-tauri/tauri.release.conf.json"
|
||||
rust-targets: ""
|
||||
runs-on: ${{ matrix.os }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: rust/margin-mail
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ needs.prepare.outputs.tag }}
|
||||
path: rust/margin-mail
|
||||
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
repository: priyanshujain/margin
|
||||
path: python/margin
|
||||
|
||||
- name: Install Linux dependencies
|
||||
if: startsWith(matrix.os, 'ubuntu')
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
libwebkit2gtk-4.1-dev \
|
||||
libgtk-3-dev \
|
||||
libayatana-appindicator3-dev \
|
||||
librsvg2-dev \
|
||||
patchelf \
|
||||
libxdo-dev \
|
||||
libssl-dev \
|
||||
build-essential \
|
||||
curl \
|
||||
wget \
|
||||
file
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 26
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.rust-targets }}
|
||||
|
||||
- uses: swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: rust/margin-mail/src-tauri -> target
|
||||
|
||||
- name: Install frontend dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
# The OAuth client is the whole suite's, so this secret is the same value in every Margin
|
||||
# repository. A build without it still runs; it just cannot connect to Google.
|
||||
- name: Provision Google credentials
|
||||
shell: bash
|
||||
env:
|
||||
GOOGLE_CREDENTIALS: ${{ secrets.GOOGLE_CREDENTIALS }}
|
||||
run: |
|
||||
if [ -n "$GOOGLE_CREDENTIALS" ]; then
|
||||
printf '%s' "$GOOGLE_CREDENTIALS" > google-credentials.json
|
||||
echo "Wrote google-credentials.json from GOOGLE_CREDENTIALS secret."
|
||||
else
|
||||
cp google-credentials.example.json google-credentials.json
|
||||
echo "::warning::GOOGLE_CREDENTIALS secret not set, embedding placeholder credentials; this build cannot connect to Gmail."
|
||||
fi
|
||||
|
||||
- name: Build and upload
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
with:
|
||||
projectPath: rust/margin-mail
|
||||
releaseId: ${{ needs.prepare.outputs.release_id }}
|
||||
args: ${{ matrix.args }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||
|
||||
publish:
|
||||
needs: [prepare, build]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Verify manifest is complete, then publish
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
REPO: ${{ github.repository }}
|
||||
TAG: ${{ needs.prepare.outputs.tag }}
|
||||
run: |
|
||||
gh release download "$TAG" --repo "$REPO" --pattern latest.json --output latest.json --clobber
|
||||
echo "Platforms in latest.json:"
|
||||
jq '.platforms | keys' latest.json
|
||||
for key in darwin-aarch64 darwin-x86_64 linux-x86_64; do
|
||||
if ! jq -e ".platforms[\"$key\"].url" latest.json > /dev/null; then
|
||||
echo "::error::latest.json is missing platform '$key', refusing to publish a partial update manifest. Re-run the release."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
gh release edit "$TAG" --repo "$REPO" --draft=false --latest
|
||||
@@ -0,0 +1,41 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.sw?
|
||||
|
||||
# Tauri build output
|
||||
src-tauri/target/
|
||||
src-tauri/gen/schemas/
|
||||
|
||||
# Xcode build output. The project under src-tauri/gen/apple is generated but committed, the
|
||||
# products of building it are not.
|
||||
src-tauri/gen/apple/build/
|
||||
src-tauri/gen/apple/Externals/
|
||||
src-tauri/gen/apple/Pods/
|
||||
src-tauri/gen/apple/Podfile.lock
|
||||
xcuserdata/
|
||||
|
||||
# Google OAuth desktop client (real values, never commit). Shared with the other Margin apps: the
|
||||
# suite has one Cloud project and one installed client, so this file is a copy of margin's.
|
||||
/google-credentials.json
|
||||
/client_secret_*.json
|
||||
|
||||
# Screenshots & Playwright MCP artifacts
|
||||
.playwright-mcp/
|
||||
/*.png
|
||||
|
||||
# The Kit page and screen screenshots the UI suite writes for review by eye
|
||||
/screenshots/
|
||||
@@ -0,0 +1,110 @@
|
||||
# Functional Source License, Version 1.1, MIT Future License
|
||||
|
||||
## Abbreviation
|
||||
|
||||
FSL-1.1-MIT
|
||||
|
||||
## Notice
|
||||
|
||||
Copyright 2026 Priyanshu Jain
|
||||
|
||||
## Terms and Conditions
|
||||
|
||||
### Licensor ("We")
|
||||
|
||||
The party offering the Software under these Terms and Conditions.
|
||||
|
||||
### The Software
|
||||
|
||||
The "Software" is each version of the software that we make available under
|
||||
these Terms and Conditions, as indicated by our inclusion of these Terms and
|
||||
Conditions with the Software.
|
||||
|
||||
### License Grant
|
||||
|
||||
Subject to your compliance with this License Grant and the Patents,
|
||||
Redistribution and Trademark clauses below, we hereby grant you the right to
|
||||
use, copy, modify, create derivative works, publicly perform, publicly display
|
||||
and redistribute the Software for any Permitted Purpose identified below.
|
||||
|
||||
### Permitted Purpose
|
||||
|
||||
A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
|
||||
means making the Software available to others in a commercial product or
|
||||
service that:
|
||||
|
||||
1. substitutes for the Software;
|
||||
|
||||
2. substitutes for any other product or service we offer using the Software
|
||||
that exists as of the date we make the Software available; or
|
||||
|
||||
3. offers the same or substantially similar functionality as the Software.
|
||||
|
||||
Permitted Purposes specifically include using the Software:
|
||||
|
||||
1. for your internal use and access;
|
||||
|
||||
2. for non-commercial education;
|
||||
|
||||
3. for non-commercial research; and
|
||||
|
||||
4. in connection with professional services that you provide to a licensee
|
||||
using the Software in accordance with these Terms and Conditions.
|
||||
|
||||
### Patents
|
||||
|
||||
To the extent your use for a Permitted Purpose would necessarily infringe our
|
||||
patents, the license grant above includes a license under our patents. If you
|
||||
make a claim against any party that the Software infringes or contributes to
|
||||
the infringement of any patent, then your patent license to the Software ends
|
||||
immediately.
|
||||
|
||||
### Redistribution
|
||||
|
||||
The Terms and Conditions apply to all copies, modifications and derivatives of
|
||||
the Software.
|
||||
|
||||
If you redistribute any copies, modifications or derivatives of the Software,
|
||||
you must include a copy of or a link to these Terms and Conditions and not
|
||||
remove any copyright notices provided in or with the Software.
|
||||
|
||||
### Disclaimer
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
|
||||
PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
|
||||
|
||||
IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
|
||||
SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
|
||||
EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
|
||||
|
||||
### Trademarks
|
||||
|
||||
Except for displaying the License Details and identifying us as the origin of
|
||||
the Software, you have no right under these Terms and Conditions to use our
|
||||
trademarks, trade names, service marks or product names.
|
||||
|
||||
## Grant of Future License
|
||||
|
||||
We hereby irrevocably grant you an additional license to use the Software under
|
||||
the MIT license that is effective on the second anniversary of the date we make
|
||||
the Software available. On or after that date, you may use the Software under
|
||||
the MIT license, in which case the following will apply:
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,20 @@
|
||||
# Margin Mail
|
||||
|
||||
A calm, keyboard-first mail client for Gmail on macOS and iPhone, with the parts of HEY and
|
||||
Superhuman worth having and none of the tracking. New senders wait at the door until you let them
|
||||
in; people, newsletters and receipts live in three separate boxes; what you owe and what you need
|
||||
sit in two piles at the foot of the list. Every decision you make is yours, kept beside the mail
|
||||
rather than inside Gmail, and it comes with you if you leave.
|
||||
|
||||
It is a sibling to [margin](https://github.com/priyanshujain/margin) and
|
||||
[Margin Calendar](https://github.com/priyanshujain/margin-calendar) and shares their stack and
|
||||
visual language.
|
||||
|
||||
Nothing is built yet. The product is defined in [docs/design.md](docs/design.md), the features in
|
||||
[docs/features.md](docs/features.md), the screens in [docs/ui.md](docs/ui.md), the keys in
|
||||
[docs/keyboard.md](docs/keyboard.md), and how it will be built in
|
||||
[docs/architecture.md](docs/architecture.md). The research it rests on is in
|
||||
[docs/research/](docs/research/).
|
||||
|
||||
Licensed FSL-1.1-MIT: use it for anything except building a competing product, and every version
|
||||
turns MIT two years after its release.
|
||||
@@ -0,0 +1,188 @@
|
||||
# Architecture
|
||||
|
||||
Tauri 2, React 19, Vite, TypeScript and zustand on the front, Rust behind. The same stack as
|
||||
margin and Margin Calendar, so OAuth, token sealing, the build and bundle setup, the overlay
|
||||
title bar and the phone chrome carry over rather than being invented again. The conventions are
|
||||
the calendar's, in `../margin-caledar/docs/conventions.md`, and apply here unchanged.
|
||||
|
||||
The split is strict. Rust owns authentication, every byte to and from a mail provider, the local
|
||||
mirror, the sync loop, MIME parsing, HTML sanitising, the portable state database, the journal
|
||||
and the backup store. TypeScript owns rendering and interaction. The frontend never talks to a
|
||||
provider, which keeps the content security policy locked to `ipc:` as in the siblings.
|
||||
|
||||
The facts about the Gmail API that this design rests on are in
|
||||
[research/gmail-api.md](research/gmail-api.md), checked against Google's pages on 3 September
|
||||
2026.
|
||||
|
||||
## Two databases, one boundary
|
||||
|
||||
There are two SQLite databases per account and the boundary between them is the product.
|
||||
|
||||
The **mirror** is a copy of the mailbox: messages, threads, labels, bodies, attachment metadata,
|
||||
an FTS5 index, the provider's sync cursor, and an outbox. It is derived from the provider and can
|
||||
be thrown away and rebuilt. Its keys are the provider's ids.
|
||||
|
||||
The **state** database is everything the user decided: sender rules, pile membership, snoozes,
|
||||
notes, renames, merges, clips, ignore flags, notification opt-ins, contact notes. It is never
|
||||
derived, it is never sent to the provider, and its keys are portable: a thread is identified by
|
||||
the RFC `Message-ID` of its earliest message (the thread key), a message by its own `Message-ID`,
|
||||
a sender by their address or domain. Provider ids appear in the state database nowhere. When the
|
||||
user moves to another provider and the same mail arrives through IMAP with the same
|
||||
`Message-ID`s, every decision reattaches.
|
||||
|
||||
A view is the mirror joined to the state. The Inbox is "threads whose sender rule says Inbox, or
|
||||
that carry a reply to a thread we are in, minus piles, minus snoozes, minus archived", grouped by
|
||||
seen state. The join is computed in Rust and served to the frontend as a flat, ordered list of
|
||||
thread summaries; the frontend never sees a label id or a rule.
|
||||
|
||||
## The provider trait
|
||||
|
||||
```
|
||||
trait Provider {
|
||||
fn authenticate(...) // interactive, returns a sealed credential
|
||||
fn full_list(...) // ids and thread ids, newest first, paged
|
||||
fn changes_since(cursor) // the change log, or NeedsFullSync
|
||||
fn fetch_headers(ids) // batched metadata
|
||||
fn fetch_body(id) // raw RFC 2822 bytes
|
||||
fn fetch_attachment(id, part) // bytes
|
||||
fn set_flags(ids, seen, starred, archived, trashed, spam)
|
||||
fn labels() / apply_label / remove_label
|
||||
fn send(raw, thread_hint) // returns the provider's id and thread id
|
||||
fn drafts(): create / update / delete
|
||||
fn search(query, page) // provider-side search, ids only
|
||||
fn settings(): aliases, signature
|
||||
}
|
||||
```
|
||||
|
||||
Gmail implements it with the REST API. IMAP and SMTP will implement it next with folders mapped
|
||||
onto the flag set and labels onto `X-GM-LABELS` or keywords, and JMAP after that with its own
|
||||
change log. The trait is shaped by what every provider can do, and everything the trait cannot do
|
||||
is done in the state database instead, which is why the state database exists.
|
||||
|
||||
Gmail specifics that live only inside the Gmail implementation: `history.list` as the change log
|
||||
and its 404 recovery, the 6,000 units per minute per user budget, `format=metadata` with a fixed
|
||||
header list for hydration, `format=raw` for bodies, batch requests of 25 to 50, truncated
|
||||
exponential backoff on 429, threading rules on send, `CATEGORY_*` labels read as a hint for the
|
||||
suggestion function, and the People API for contact autocomplete.
|
||||
|
||||
## Authentication and distribution
|
||||
|
||||
Loopback OAuth with PKCE from the calendar, unchanged: the consent page opens in the system
|
||||
browser, never in a webview the app owns, and the code lands on `127.0.0.1`. Refresh tokens are
|
||||
sealed with XChaCha20-Poly1305 in the app data directory as the calendar does, for the same
|
||||
reasons (no `keyring` on Android, code-signature churn on macOS, no Secret Service on minimal
|
||||
Linux). Scopes: `gmail.modify`, `gmail.settings.basic`, `contacts.other.readonly`,
|
||||
`contacts.readonly`, and `calendar.events` for RSVP. No `mail.google.com` until IMAP is real.
|
||||
|
||||
Every Gmail scope that reads mail is restricted. The plan is the one in the research: push the
|
||||
consent screen to production unverified for the friends release (100 lifetime users, no weekly
|
||||
re-login), file restricted-scope verification at once with the statement that there is no server
|
||||
and all Google user data stays on the device, ask in writing whether the security assessment
|
||||
applies, and budget for it anyway. Bring-your-own OAuth client stays as an escape hatch in
|
||||
settings for the technical.
|
||||
|
||||
## Sync
|
||||
|
||||
Polling, no push. `users.watch` needs a Pub/Sub subscription, which needs a server or an IAM
|
||||
grant no desktop app should hold, and a relay is the thing that might drag the app into an
|
||||
annual security assessment. `history.list` costs 2 units; polling every 12 seconds in the
|
||||
foreground and 60 seconds in the background is a rounding error against the budget.
|
||||
|
||||
Initial sync is the expensive part after the May 2026 quota change: `messages.get` is 20 units,
|
||||
so hydration runs at about 300 messages a minute per account. A 20,000 message mailbox takes
|
||||
about an hour of background work; a 100,000 message mailbox most of a working day. The order is
|
||||
newest first, the app is usable as soon as the first page lands, and a thin bar in the account
|
||||
chip says how far back the mirror reaches. Bodies are fetched on open and prefetched for the
|
||||
last 90 days when idle. Attachments are fetched on open and cached with a size cap. The mirror
|
||||
is the whole mailbox by decision; a setting caps the age for people who want less on disk.
|
||||
|
||||
Every write is optimistic: it lands in the mirror, renders, and is pushed behind. Consecutive
|
||||
flag changes are coalesced into `batchModify`. Offline writes queue in the outbox and drain on
|
||||
reconnect, with the thread showing "Waiting to send" until a send goes.
|
||||
|
||||
Seen, starred, archived, trashed and spam are provider flags and go through the trait. Nothing in
|
||||
the piles, the Screener, snoozes or notes ever touches the provider, so a user who screens out
|
||||
two hundred senders makes zero API calls.
|
||||
|
||||
## The state journal and the backup store
|
||||
|
||||
The state database is written through an append-only journal: every change is an event with a
|
||||
device id, a per-device sequence number, a wall-clock timestamp, a kind, a portable key, and a
|
||||
payload. The tables are a materialised view of the journal. This is what makes roaming possible
|
||||
without a server and what makes the backup meaningful.
|
||||
|
||||
A **backup store** is a trait with three operations: put a blob at a name, get a blob by name,
|
||||
list names under a prefix. Two implementations ship: Google Drive's app-data folder, which every
|
||||
Gmail user already has and which margin's backup already uses, and Cloudflare R2 through S3
|
||||
credentials for people who run their own. Each device uploads its own journal segments under its
|
||||
device id and downloads every other device's. Merging is last-writer-wins per key by timestamp,
|
||||
which is correct for every kind of state here (a pile toggle, a note, a rule), and a device that
|
||||
has been offline for a month simply replays what it missed.
|
||||
|
||||
Everything uploaded is encrypted on the device with XChaCha20-Poly1305 under a key that is
|
||||
generated on first backup, stored sealed like the tokens, and shown once as a recovery phrase.
|
||||
Drive and R2 hold ciphertext and names; neither can read a note or a rule. The recovery phrase is
|
||||
the only way to attach a second device or restore after a lost one, and the settings panel says
|
||||
so in one sentence.
|
||||
|
||||
Version one on macOS alone does not need the merge. The journal shape is there from the first
|
||||
commit so that the iPhone can join without a migration.
|
||||
|
||||
## Security
|
||||
|
||||
Message bodies are parsed from raw RFC 2822 with a real MIME parser (`mail-parser`), never from
|
||||
the provider's pre-parsed payload beyond headers, because encoded words, parameter
|
||||
continuations and legacy charsets appear daily. HTML is sanitised in Rust before it reaches the
|
||||
webview: scripts, forms, event handlers, `<meta>` refreshes, external stylesheets, `javascript:`
|
||||
and `data:` navigation are removed; `cid:` references are rewritten to a local resource scheme;
|
||||
every remote `<img>` is replaced with a placeholder and its source recorded. The body renders in
|
||||
an iframe with a strict CSP inside the app's webview so the message can never touch the app.
|
||||
|
||||
Tracker stripping happens in the same pass: images with a known tracking host (a maintained
|
||||
list, shipped with the app and updated with it), images of one pixel or hidden by style, and
|
||||
images whose URL carries a recipient token are removed and counted, and the vendor is named in
|
||||
the banner. When the user asks to show images, Rust fetches them without cookies or referrer and
|
||||
serves them from cache; the user's IP is exposed to the image host at that moment and only then,
|
||||
which the privacy setting says plainly. Outgoing mail never contains a tracker and the app never
|
||||
requests a read receipt. Links are rewritten on click to drop known tracking parameters, with a
|
||||
setting to turn that off.
|
||||
|
||||
Refresh tokens and the backup key are sealed, never in SQLite. The mirror and the state database
|
||||
are files in the app data directory and inherit the OS's disk encryption; encrypting them again
|
||||
would cost search and buy nothing on a device that is already locked, and the export path is
|
||||
the answer for anyone who wants their mail in a form they control.
|
||||
|
||||
## Platforms
|
||||
|
||||
macOS: overlay title bar with the traffic lights on the header's centre line, closing the window
|
||||
hides it and Cmd-Q quits, all from the calendar. iOS second: the same code with the phone chrome
|
||||
from the calendar's `data-phone` and `data-touch` scheme, overlays as bottom sheets, the OAuth
|
||||
flow through `ASWebAuthenticationSession`, and background app refresh used only to run the
|
||||
snooze evaluation and a short sync. Linux afterwards: no traffic lights, closing quits, deb and
|
||||
AppImage.
|
||||
|
||||
## Order of work
|
||||
|
||||
The sync engine and the reading pane are the two hard things and neither proves the other, so the
|
||||
first milestone is one account, authentication, the mirror, and a read-only Inbox with a
|
||||
sanitised, tracker-stripped reading pane. Everything the app is for depends on those being
|
||||
right.
|
||||
|
||||
Second, triage on the mirror: seen, archive, star, trash, spam, selection, the keyboard, the
|
||||
palette, local search. At this point it is a fast Gmail client and nothing more.
|
||||
|
||||
Third, the state database and the four places: sender rules, the suggestion function, the
|
||||
Screener, the first-run pass, Feed and Paper Trail. This is the milestone where it stops being a
|
||||
Gmail client.
|
||||
|
||||
Fourth, the piles and their friends: Reply later, Set aside, Focus & Reply, snooze with lazy
|
||||
evaluation, notes, rename, merge, clips, All files, ignore, per-thread notifications, the contact
|
||||
card.
|
||||
|
||||
Fifth, writing: reply, compose, drafts, the outbox with undo send, attachments, remind me if no
|
||||
reply, instant intro, calendar RSVP.
|
||||
|
||||
Sixth, more than one account, the unified view, settings, export, and the backup store with the
|
||||
journal behind it.
|
||||
|
||||
Then the iPhone, then Linux, then the IMAP provider, in that order.
|
||||
@@ -0,0 +1,146 @@
|
||||
# Design
|
||||
|
||||
A mail client for Gmail, macOS first and then the iPhone, that borrows the two ideas worth
|
||||
borrowing from HEY and the two worth borrowing from Superhuman and refuses the rest of both.
|
||||
It exists because the email experience should belong to the person reading it, not to whoever
|
||||
stores the bytes, and because nobody has built a native, quiet, keyboard-driven Gmail client with
|
||||
sender screening. The research behind every claim here is in [research/](research/), and the
|
||||
screens are in [ui.md](ui.md).
|
||||
|
||||
## What we took and what we refused
|
||||
|
||||
From HEY: consent and separation. Nobody reaches your Inbox until you have said yes to them once,
|
||||
and mail from people is kept apart from newsletters and receipts by a decision you made, not by a
|
||||
classifier guessing. Reply Later and Set Aside are physical piles rather than flags. Read mail
|
||||
sinks rather than nagging. Notifications are off until you ask for one. Trackers are stripped and
|
||||
named. We refused HEY's layout (one column, a page per thread, a round trip for everything), its
|
||||
insistence that routing is per sender only, and its refusal to let you archive.
|
||||
|
||||
From Superhuman: speed and the keyboard. One key per verb, the key printed on every button, and a
|
||||
command palette that is the whole settings and discovery surface, so the app teaches itself. A
|
||||
list beside a reading pane, so you triage without leaving the list. Remind me if no reply, undo
|
||||
send, the contact card. We refused the tracking pixels, the AI surface that ships your mail to a
|
||||
vendor, the inbox-zero streak, the tiny fixed type, and the price.
|
||||
|
||||
From neither: the app is the only place your state lives, and that state is yours. Your piles,
|
||||
screening decisions, notes, renames and clips are keyed on the mail itself (the RFC Message-ID
|
||||
and the sender's address), not on Gmail's identifiers, and they are kept in a local database that
|
||||
backs up to a store you choose. Move to Fastmail or Proton next year and every decision comes
|
||||
with you. Gmail is storage and transport. It never sees a HEY-style label, a note, or a rule.
|
||||
|
||||
## The four places
|
||||
|
||||
There are three boxes and one gate, and every sender has exactly one destination among them.
|
||||
|
||||
**Inbox** is for people and for the few services you want to hear from as they arrive. It is a
|
||||
stream, not a queue: what you have not looked at sits under New for you at the top, and
|
||||
everything you have opened or sent sinks to Previously seen beneath it. Nothing counts anything.
|
||||
A reply pulls a thread back up. There is an archive key, because some people need an empty list
|
||||
to feel finished, but nothing in the design pushes you towards it.
|
||||
|
||||
**Feed** is for newsletters and long reads. Every item is already open, in one scrolling column,
|
||||
newest first, with a marker where you left off. There is no read state and no obligation. You
|
||||
scroll it when you want to read, and it never tells you how far behind you are.
|
||||
|
||||
**Paper Trail** is for receipts, confirmations and notifications: things you file and later
|
||||
search for. A flat list, no read state, senders who flood you bundled into one row.
|
||||
|
||||
**The Screener** holds the first message from anyone new until you decide. The card shows who they
|
||||
are, what they sent, and what Margin thinks the right box is and why: written by a person, so
|
||||
Inbox; carries an unsubscribe header, so Feed; sent by a service on someone's behalf, so Paper
|
||||
Trail. One key accepts the suggestion, one picks elsewhere, one says no. No is silent. The
|
||||
decision can be per address or per domain, which is the thing HEY's users ask for most and HEY
|
||||
will not give them. Replies to a thread you are already in bypass the whole system and land in
|
||||
the Inbox, because the sender rule is about first contact, not about conversations.
|
||||
|
||||
On first run there is no Screener avalanche. Everyone who has ever written to you is screened in,
|
||||
routed by the same suggestion rules, and movable later from their contact card. Only genuinely
|
||||
new senders from that point on are held.
|
||||
|
||||
## The two piles
|
||||
|
||||
Reply Later and Set Aside sit at the foot of the list as two stacks of cards, always visible,
|
||||
each showing what is on top. A thread you pile leaves the list. Reply Later is for what you owe;
|
||||
Set Aside is for what you need to hand (a ticket, an itinerary, a code). Focus & Reply lines up
|
||||
everything in Reply Later on one page with a reply box beside each, and sending a reply clears
|
||||
the thread from the pile.
|
||||
|
||||
Snooze covers the rest: later today, tomorrow, the weekend, a date, or if no reply by a date. A
|
||||
snoozed thread leaves the list and comes back to the top of the Inbox when it is due. There is no
|
||||
scheduler and no server. Whichever of your devices next opens the app checks what is due and
|
||||
brings it back. That is honest about what a client without a server can do, and it is enough.
|
||||
|
||||
## Keyboard first, mouse whole
|
||||
|
||||
Every verb is one unmodified key, the same key Gmail and Superhuman use where they agree
|
||||
(`j`, `k`, `e`, `r`, `a`, `f`, `c`, `/`, `x`, `u`, `z`) and HEY's letters for HEY's verbs (`l`
|
||||
Reply Later, `s` Set Aside, `b` snooze, `y` note, `m` ignore). Number keys go to places. There
|
||||
are no two-key chords, nothing is modal, and the key is printed on every button so the mouse
|
||||
teaches the keyboard. Cmd-K opens the palette, which also lists every place, every command and
|
||||
every setting. The full map and the reasoning for each conflict are in [keyboard.md](keyboard.md).
|
||||
|
||||
## Quiet by design
|
||||
|
||||
No badge, no unread count, no streak, no photograph when the list is empty. No notification
|
||||
unless you turned it on for that thread or that person. Remote images do not load until you ask,
|
||||
tracking pixels are removed before the message renders, and the banner tells you whose pixel it
|
||||
was. Nothing you send carries a tracker and nothing reports when it was opened. Links open with
|
||||
their tracking parameters removed.
|
||||
|
||||
There is no AI in this version. Classification is by headers and by your decisions, the way HEY
|
||||
does it, and it is explainable in a sentence on every Screener card. The design leaves a seat for
|
||||
a local model later; it does not leave one for a cloud one.
|
||||
|
||||
## Reading, writing, and the things HEY owns
|
||||
|
||||
Reading happens in a pane beside the list. Long threads collapse quoted text and older messages;
|
||||
`n` and `p` step through messages. A calendar invite renders as a card with accept, maybe and
|
||||
decline, and a link that opens the event in Margin Calendar. A sender's name opens their contact
|
||||
card: where their mail delivers, whether they notify you, your note about them, recent threads,
|
||||
files.
|
||||
|
||||
Replies are written under the last message. New mail is a card floating over the list that can
|
||||
expand to the whole window. Every send waits ten seconds with an undo, then leaves. Remind me if
|
||||
no reply is a toggle on the send button.
|
||||
|
||||
Notes, renames, merges and clips are the features HEY can offer only because it owns the data
|
||||
model. We can offer them because we keep our own, beside the mail rather than inside it. A note
|
||||
is a private block in the thread and a line under its row. A rename changes the subject for you
|
||||
alone and says so. A merge shows several threads as one and can be undone. A clip is a saved
|
||||
passage with a link back. All of it roams through the backup store, none of it touches Gmail.
|
||||
|
||||
## What it is not
|
||||
|
||||
Not a team tool: no shared threads, comments, or read statuses. Not a calendar: invites hand off
|
||||
to Margin Calendar. Not a scheduler: no send later in this version. Not an assistant: nothing is
|
||||
summarised or drafted for you. Not a Gmail skin: the Gmail web UI is not a consideration, since
|
||||
the whole point is never opening it.
|
||||
|
||||
## Visual language
|
||||
|
||||
Lifted from margin and the calendar unchanged: warm paper, ink and two softer inks, hairline
|
||||
borders, a four-step type scale, three radii, one easing curve, light and dark driven by
|
||||
`data-theme`. Hanken Grotesk for the interface, Literata for the subject line and for message
|
||||
bodies, because mail is reading and reading deserves a text face. The additions are mail-specific:
|
||||
a row hover and a row selection wash, a warmer band for Previously seen, a note surface, a dot
|
||||
colour for new mail, and the eight muted hues the calendar already uses, here for avatars and
|
||||
account edges. No CSS framework, no component library, hand-written CSS on tokens.
|
||||
|
||||
Rows are two lines: sender and time, then subject and snippet. New mail gets a dot and a heavier
|
||||
sender; nothing else is bold. No chips, no labels in the row, no icons appearing on hover. The
|
||||
reading pane has one bar of verbs with their keys, a serif subject, and messages separated by
|
||||
hairlines rather than cards.
|
||||
|
||||
## On a phone
|
||||
|
||||
The list is the screen, the piles sit above a tab bar with the three boxes and a write button, and
|
||||
a thread is a page with a floating action bar where the thumb is. Swipes are Reply Later to the
|
||||
right and Set Aside to the left, both changeable. Nothing that only appears on hover exists on a
|
||||
phone; there is no hover. Notifications remain opt-in per thread and per person, and there is no
|
||||
badge. The same premise, the same four places, the same two piles.
|
||||
|
||||
## Licence and ownership
|
||||
|
||||
FSL-1.1-MIT, as margin is: use it for anything except a competing product, and each version turns
|
||||
MIT two years after release. Ownership is a feature, not a slogan: mail exports as mbox, contacts
|
||||
as vCard, and every piece of app state as JSON, from settings, at any time.
|
||||
@@ -0,0 +1,369 @@
|
||||
# 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).
|
||||
@@ -0,0 +1,129 @@
|
||||
# Keyboard
|
||||
|
||||
One unmodified key per verb, no two-key chords, nothing modal. Where Gmail and Superhuman agree,
|
||||
we use their key so a refugee's hands keep working. Where HEY has a verb they do not, we use
|
||||
HEY's letter. The three conflicts that result are settled below and explained. The key is printed
|
||||
on every button, the palette lists every command with its key, and `?` shows this table in the
|
||||
app. Bindings live in a keymap file the user can edit; the defaults are what follows.
|
||||
|
||||
## Navigation
|
||||
|
||||
| Key | Action |
|
||||
|---|---|
|
||||
| `j` / `k` | Next / previous thread (or card in the Feed and Screener) |
|
||||
| `Enter` | Open the focused thread; with several selected, Read together |
|
||||
| `Esc` | Close what is open: a popover, then the palette, then the compose card, then the pane in one-column mode, then the selection |
|
||||
| `n` / `p` | Next / previous message inside a thread |
|
||||
| `o` / `Shift+O` | Expand or collapse the focused message / expand all |
|
||||
| `Space` / `Shift+Space` | Scroll the pane down / up |
|
||||
| `1` `2` `3` | Inbox, Feed, Paper Trail |
|
||||
| `4` `5` | Reply later, Set aside |
|
||||
| `6` `7` | Screener, Snoozed |
|
||||
| `0` | Everything |
|
||||
| `/` | Search |
|
||||
| `Cmd+K` | Palette: places, commands, people, settings |
|
||||
| `Ctrl+1` to `Ctrl+9`, `Ctrl+0` | Switch account, All accounts |
|
||||
| `Cmd+\` | Show or hide the reading pane |
|
||||
| `Cmd+,` | Settings |
|
||||
| `?` | This table |
|
||||
|
||||
## Triage
|
||||
|
||||
| Key | Action | Provider |
|
||||
|---|---|---|
|
||||
| `e` | Archive | yes |
|
||||
| `u` | Toggle seen | yes |
|
||||
| `Shift+S` | Toggle star | yes |
|
||||
| `#` | Trash | yes |
|
||||
| `!` | Spam | yes |
|
||||
| `l` | Reply later (toggle) | no |
|
||||
| `s` | Set aside (toggle) | no |
|
||||
| `b` | Snooze… | no |
|
||||
| `y` | Note | no |
|
||||
| `m` | Ignore (toggle) | no |
|
||||
| `Shift+N` | Notify me on this thread (toggle) | no |
|
||||
| `g` | Merge the selected threads | no |
|
||||
| `i` | Contact card for the sender | no |
|
||||
| `Shift+L` | Label… | yes |
|
||||
| `v` | Move to Inbox, Feed or Paper Trail (sets the sender's rule); in a label list, move to a label | rule: no, label: yes |
|
||||
| `Cmd+U` | Unsubscribe… | sends or opens |
|
||||
| `x` | Select the focused thread | |
|
||||
| `Shift+J` / `Shift+K` | Extend the selection down / up | |
|
||||
| `Cmd+A` | Select all from here | |
|
||||
| `z` | Undo the last action, including a send within its delay | |
|
||||
| `.` | More actions for the thread | |
|
||||
|
||||
## Screener
|
||||
|
||||
| Key | Action |
|
||||
|---|---|
|
||||
| `y` | Yes, to the suggested place |
|
||||
| `v` | Elsewhere: pick the place, optionally for the whole domain |
|
||||
| `n` | No, screen out |
|
||||
| `Enter` | Expand the card to read the message |
|
||||
| `r` | Screen in to Inbox and reply |
|
||||
|
||||
## Writing
|
||||
|
||||
| Key | Action |
|
||||
|---|---|
|
||||
| `c` | New message |
|
||||
| `r` / `a` / `f` | Reply / reply all / forward |
|
||||
| `Cmd+Enter` | Send (held for the undo delay) |
|
||||
| `Cmd+Shift+Enter` | Send now, no undo |
|
||||
| `Cmd+Shift+P` | Expand the compose card to the window, or back |
|
||||
| `Cmd+Shift+A` | Attach |
|
||||
| `Cmd+Shift+I` | Instant intro: move the introducer to Bcc and thank them |
|
||||
| `Cmd+Shift+H` | Remind me if no reply… |
|
||||
| `Cmd+Shift+C` | Save the selected text as a clip (also outside compose) |
|
||||
| `Cmd+B` / `Cmd+I` / `Cmd+K` | Bold / italic / link inside the editor |
|
||||
| `Cmd+Shift+7` / `8` / `9` | Numbered list / bulleted list / quote |
|
||||
| `Cmd+Shift+,` | Discard the draft |
|
||||
| `Esc` | Leave the editor, keeping the draft |
|
||||
|
||||
## Focus & Reply
|
||||
|
||||
| Key | Action |
|
||||
|---|---|
|
||||
| `Shift+F` | Open Focus & Reply from anywhere |
|
||||
| `Tab` / `Shift+Tab` | Next / previous item |
|
||||
| `Cmd+Enter` | Send this reply and move on |
|
||||
| `Esc` | Back to Reply later |
|
||||
|
||||
## Calendar invites
|
||||
|
||||
| Key | Action |
|
||||
|---|---|
|
||||
| `y` / `m` / `n` | Accept / maybe / decline, when an invite card is focused |
|
||||
|
||||
## The conflicts, and how they were settled
|
||||
|
||||
`a` is reply all in Gmail and Superhuman and Set aside in HEY. Reply all is used every day, so it
|
||||
keeps `a`; Set aside takes `s`, which is star in Gmail. Star is a vestige here (the piles do its
|
||||
job), so star moves to `Shift+S` and stays reachable.
|
||||
|
||||
`l` is label in Gmail and Superhuman and Reply later in HEY. Reply later is a primary verb in this
|
||||
app and labels are not, so `l` is Reply later and label is `Shift+L`.
|
||||
|
||||
`z` is undo in Gmail and Superhuman and Bubble Up in HEY. Undo is sacred; `z` stays undo. Snooze
|
||||
takes `b`, which is Gmail's own snooze key and also reads as Bubble Up for HEY hands.
|
||||
|
||||
`m` is mute in Gmail and Ignore in HEY, the same verb, so it is `m`. `y` is a sticky note in HEY
|
||||
and a rarely known "remove from view" in Gmail; it is a note here. `g` is HEY's merge; in Gmail it
|
||||
begins a chord we do not have, so on its own it merges a selection and does nothing otherwise.
|
||||
|
||||
Keys are never reused with a different meaning in a different context, with one exception: `y`,
|
||||
`v` and `n` on a Screener card and on an invite card, where the card is the only thing that can
|
||||
receive them and the buttons print the letters. HEY reuses `o`, `r` and `i` contextually and it
|
||||
is the one thing about its keyboard worth not copying.
|
||||
|
||||
## Rules
|
||||
|
||||
- Single keys act on the focused thread, or on the selection when there is one.
|
||||
- A key that would act on nothing does nothing and shows nothing.
|
||||
- Every destructive or hidden action (archive, trash, spam, snooze, pile, screen out, merge,
|
||||
send) produces a toast with Undo, and `z` undoes it.
|
||||
- On a phone there are no keys. The same verbs are buttons on the action bar and swipes on rows,
|
||||
and the printed letters disappear.
|
||||
- The keymap is a file in the app's data directory; the shortcuts sheet is generated from it, so
|
||||
a remapped key is what the buttons print.
|
||||
|
After Width: | Height: | Size: 331 KiB |
|
After Width: | Height: | Size: 412 KiB |
|
After Width: | Height: | Size: 291 KiB |
|
After Width: | Height: | Size: 463 KiB |
|
After Width: | Height: | Size: 470 KiB |
|
After Width: | Height: | Size: 600 KiB |
|
After Width: | Height: | Size: 341 KiB |
|
After Width: | Height: | Size: 210 KiB |
|
After Width: | Height: | Size: 203 KiB |
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env bash
|
||||
# Renders every mockup in src/ to a PNG beside this script, at 2x. The fonts come from the
|
||||
# sibling margin repo's shared package, so nothing binary is vendored here, and Playwright comes
|
||||
# from the calendar repo's node_modules, where chromium is already installed.
|
||||
set -euo pipefail
|
||||
|
||||
here="$(cd "$(dirname "$0")" && pwd)"
|
||||
fonts="$here/../../../../python/margin/shared/fonts"
|
||||
runner="$here/../../../../python/margin-caledar"
|
||||
work="$(mktemp -d)"
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
|
||||
cp "$here"/src/*.html "$here"/src/*.css "$work"/
|
||||
mkdir -p "$work/fonts"
|
||||
cp "$fonts"/HankenGrotesk-VF.ttf "$fonts"/HankenGrotesk-Italic-VF.ttf \
|
||||
"$fonts"/Literata-VF.ttf "$fonts"/Literata-Italic-VF.ttf "$work/fonts/"
|
||||
|
||||
only="${1:-}"
|
||||
|
||||
NODE_PATH="$runner/node_modules" node - "$work" "$here" "$only" <<'EOF'
|
||||
const { chromium } = require("@playwright/test");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const [work, out, only] = process.argv.slice(2);
|
||||
|
||||
(async () => {
|
||||
const browser = await chromium.launch();
|
||||
const files = fs.readdirSync(work).filter((f) => f.endsWith(".html"));
|
||||
for (const file of files) {
|
||||
const name = file.replace(/\.html$/, "");
|
||||
if (only && name !== only) continue;
|
||||
const phone = name.startsWith("phone-");
|
||||
const context = await browser.newContext({
|
||||
viewport: phone ? { width: 390, height: 844 } : { width: 1440, height: 900 },
|
||||
deviceScaleFactor: 2,
|
||||
});
|
||||
const page = await context.newPage();
|
||||
await page.goto("file://" + path.join(work, file));
|
||||
await page.evaluate(() => document.fonts.ready);
|
||||
await page.waitForTimeout(300);
|
||||
await page.screenshot({ path: path.join(out, name + ".png") });
|
||||
await context.close();
|
||||
console.log("rendered " + name + ".png");
|
||||
}
|
||||
await browser.close();
|
||||
})();
|
||||
EOF
|
||||
|
After Width: | Height: | Size: 223 KiB |
@@ -0,0 +1,211 @@
|
||||
<!doctype html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Margin Mail: Compose</title>
|
||||
<link rel="stylesheet" href="mail.css">
|
||||
</head>
|
||||
<body>
|
||||
<svg width="0" height="0" style="position:absolute">
|
||||
<defs>
|
||||
<symbol id="i-search" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></symbol>
|
||||
<symbol id="i-places" viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h10"/></symbol>
|
||||
<symbol id="i-pen" viewBox="0 0 24 24"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z"/></symbol>
|
||||
<symbol id="i-chev" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></symbol>
|
||||
<symbol id="i-reply" viewBox="0 0 24 24"><path d="M9 17 4 12l5-5"/><path d="M20 18v-2a4 4 0 0 0-4-4H4"/></symbol>
|
||||
<symbol id="i-later" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></symbol>
|
||||
<symbol id="i-aside" viewBox="0 0 24 24"><path d="M12 17v4"/><path d="M8 3h8l-1 7 3 3H6l3-3z"/></symbol>
|
||||
<symbol id="i-snooze" viewBox="0 0 24 24"><path d="M5 3 2 6"/><path d="m22 6-3-3"/><circle cx="12" cy="13" r="8"/><path d="M12 9v4l2 2"/></symbol>
|
||||
<symbol id="i-archive" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="4" rx="1"/><path d="M5 8v11a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V8"/><path d="M10 12h4"/></symbol>
|
||||
<symbol id="i-more" viewBox="0 0 24 24"><circle cx="5" cy="12" r="1.2"/><circle cx="12" cy="12" r="1.2"/><circle cx="19" cy="12" r="1.2"/></symbol>
|
||||
<symbol id="i-shield" viewBox="0 0 24 24"><path d="M12 3 4 6v6c0 5 3.5 8 8 9 4.5-1 8-4 8-9V6z"/><path d="m9 12 2 2 4-4"/></symbol>
|
||||
<symbol id="i-clip" viewBox="0 0 24 24"><path d="m21 11-8.5 8.5a5 5 0 0 1-7-7L14 4a3.3 3.3 0 0 1 4.7 4.7L10.5 17a1.7 1.7 0 0 1-2.4-2.4L16 6.7"/></symbol>
|
||||
<symbol id="i-note" viewBox="0 0 24 24"><path d="M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><path d="M14 3v6h6"/></symbol>
|
||||
<symbol id="i-bolt" viewBox="0 0 24 24"><path d="M13 2 4 14h7l-1 8 9-12h-7z"/></symbol>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
<div class="app">
|
||||
<header class="titlebar">
|
||||
<div class="traffic"><i></i><i></i><i></i></div>
|
||||
<div class="lead">
|
||||
<button class="account-chip">
|
||||
<span class="avatar" data-hue="4">PJ</span>
|
||||
[email protected]
|
||||
<svg class="chev" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#i-chev"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="view-switch">
|
||||
<button class="view-option" data-active="true"><span class="key">1</span>Inbox</button>
|
||||
<button class="view-option"><span class="key">2</span>Feed</button>
|
||||
<button class="view-option"><span class="key">3</span>Paper Trail</button>
|
||||
</div>
|
||||
<div class="trail">
|
||||
<button class="icon-button" title="Search /"><svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-search"/></svg></button>
|
||||
<button class="icon-button" title="Places ⌘K"><svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-places"/></svg></button>
|
||||
<button class="write-button"><svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#i-pen"/></svg>Write<span class="key">c</span></button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="stage">
|
||||
<section class="list-col">
|
||||
<div class="list-head">
|
||||
<h1 class="list-title">Inbox</h1>
|
||||
<button class="pill"><svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#i-shield"/></svg>Screen 3 new senders<span class="key">6</span></button>
|
||||
</div>
|
||||
<div class="list">
|
||||
<div class="section">New for you</div>
|
||||
|
||||
<div class="row" data-new>
|
||||
<div class="row-gutter"><span class="row-dot"></span></div>
|
||||
<span class="avatar" data-hue="2">MR</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">Maya Raghunathan</span><span class="row-count">3</span><span class="row-time">11:42</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Dinner on Thursday?</span><span class="row-snippet">Priya said the place on Church Street takes bookings now, want me to</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" data-new data-selected>
|
||||
<div class="row-gutter"><span class="row-dot"></span></div>
|
||||
<span class="avatar" data-hue="6">AK</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">Arun Kulkarni</span><span class="row-time">10:15</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Re: Lease renewal for the studio</span><span class="row-snippet">Attached the revised draft. The only change is clause 7, which now</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" data-new>
|
||||
<div class="row-gutter"><span class="row-dot"></span></div>
|
||||
<span class="avatar" data-brand>Ai</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">Airbnb</span><span class="row-time">09:03</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Your reservation in Lisbon is confirmed</span><span class="row-snippet">Check-in Friday 12 September after 15:00. Your host Inês will send</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" data-new>
|
||||
<div class="row-gutter"><span class="row-dot"></span></div>
|
||||
<span class="avatar" data-hue="7">SO</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">Sam Okafor</span><span class="row-time">Yesterday</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Piano lessons, the form you sent</span><span class="row-snippet">Got it, thank you. Wednesdays at five work for us. Is there a</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="seen">
|
||||
<div class="section">Previously seen</div>
|
||||
|
||||
<div class="row" data-noted>
|
||||
<div class="row-gutter"></div>
|
||||
<span class="avatar" data-hue="5">LB</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">Lena Brandt</span><span class="row-count">7</span><span class="row-time">Yesterday</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Kitchen bench quote</span><span class="row-snippet">Sounds good, Julie. Any afternoon next week works for me.</span></div>
|
||||
</div>
|
||||
<div class="row-note">Ask about the oak finish before confirming</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="row-gutter"></div>
|
||||
<span class="avatar" data-hue="3">DP</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">Dev Patel</span><span class="row-time">Mon</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Slides from the talk</span><span class="row-snippet">Here they are, plus the reading list I mentioned. The paper on</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="row-gutter"></div>
|
||||
<span class="avatar" data-brand>Ds</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">DocuSign</span><span class="row-time">Mon</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Completed: Studio lease 2026</span><span class="row-snippet">All parties have completed the envelope. You can access the</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="row-gutter"></div>
|
||||
<span class="avatar" data-hue="8">HW</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">Hannah Weiss</span><span class="row-count">2</span><span class="row-time">Sun</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Photos from the Hawaii trip</span><span class="row-snippet">Finally went through them all. The ones from the ridge walk are</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="row-gutter"></div>
|
||||
<span class="avatar" data-hue="1">RY</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">Russell Young</span><span class="row-time">30 Aug</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Pumpkin bread recipe</span><span class="row-snippet">From my mother's card, transcribed as best I could. Bake at 175</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="piles">
|
||||
<button class="pile">
|
||||
<div class="pile-card" data-depth="2"></div>
|
||||
<div class="pile-card" data-depth="1"></div>
|
||||
<div class="pile-card" data-depth="0">
|
||||
<div class="pile-top"><svg width="11" height="11" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#i-later"/></svg>Reply later<span class="key">4</span></div>
|
||||
<div class="pile-subject">Re: Cooper's parent-teacher conference</div>
|
||||
<div class="pile-from">Jeff Wolfe, and 2 more</div>
|
||||
</div>
|
||||
</button>
|
||||
<button class="pile">
|
||||
<div class="pile-card" data-depth="1"></div>
|
||||
<div class="pile-card" data-depth="0">
|
||||
<div class="pile-top"><svg width="11" height="11" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#i-aside"/></svg>Set aside<span class="key">5</span></div>
|
||||
<div class="pile-subject">Les Misérables tickets</div>
|
||||
<div class="pile-from">Caroline Bauhaus, and 1 more</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="pane">
|
||||
<div class="pane-empty">Nothing selected</div>
|
||||
</section>
|
||||
|
||||
<div class="compose">
|
||||
<div class="compose-head">
|
||||
<h2>New message</h2>
|
||||
<button class="icon-button" title="Expand"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6"/><path d="M9 21H3v-6"/><path d="m21 3-7 7"/><path d="m3 21 7-7"/></svg></button>
|
||||
<button class="icon-button" title="Close"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg></button>
|
||||
</div>
|
||||
<div class="compose-field">
|
||||
<span class="lab">From</span>
|
||||
<div class="chips">
|
||||
<span class="chip"><span class="avatar" data-hue="4">PJ</span>[email protected]<svg width="10" height="10" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#i-chev"/></svg></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="compose-field">
|
||||
<span class="lab">To</span>
|
||||
<div class="chips">
|
||||
<span class="chip"><span class="avatar" data-hue="2">MR</span>Maya Raghunathan</span>
|
||||
</div>
|
||||
<span class="more">Cc · Bcc</span>
|
||||
</div>
|
||||
<div class="compose-field">
|
||||
<span class="lab">Subject</span>
|
||||
<span>Thursday</span>
|
||||
</div>
|
||||
<div class="compose-body">
|
||||
<p>Maya, Thursday works. Church Street at eight?</p>
|
||||
<p>I will book it if you do not mind the walk from the station.<span class="caret"></span></p>
|
||||
</div>
|
||||
<div class="compose-foot">
|
||||
<button class="button" data-variant="primary">Send<span class="key">⌘↩</span></button>
|
||||
<button class="button" data-variant="ghost">Remind me if no reply</button>
|
||||
<span class="spacer"></span>
|
||||
<button class="icon-button" title="Attach"><svg width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-clip"/></svg></button>
|
||||
<span style="color:var(--ink-faint);font-size:var(--t-1)">Undo send: 10 s</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toast">Sent to Arun Kulkarni<button class="toast-action">Undo<span class="key">z</span></button></div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,124 @@
|
||||
<!doctype html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Margin Mail: Feed</title>
|
||||
<link rel="stylesheet" href="mail.css">
|
||||
</head>
|
||||
<body>
|
||||
<svg width="0" height="0" style="position:absolute">
|
||||
<defs>
|
||||
<symbol id="i-search" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></symbol>
|
||||
<symbol id="i-places" viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h10"/></symbol>
|
||||
<symbol id="i-pen" viewBox="0 0 24 24"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z"/></symbol>
|
||||
<symbol id="i-chev" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></symbol>
|
||||
<symbol id="i-reply" viewBox="0 0 24 24"><path d="M9 17 4 12l5-5"/><path d="M20 18v-2a4 4 0 0 0-4-4H4"/></symbol>
|
||||
<symbol id="i-later" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></symbol>
|
||||
<symbol id="i-aside" viewBox="0 0 24 24"><path d="M12 17v4"/><path d="M8 3h8l-1 7 3 3H6l3-3z"/></symbol>
|
||||
<symbol id="i-snooze" viewBox="0 0 24 24"><path d="M5 3 2 6"/><path d="m22 6-3-3"/><circle cx="12" cy="13" r="8"/><path d="M12 9v4l2 2"/></symbol>
|
||||
<symbol id="i-archive" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="4" rx="1"/><path d="M5 8v11a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V8"/><path d="M10 12h4"/></symbol>
|
||||
<symbol id="i-more" viewBox="0 0 24 24"><circle cx="5" cy="12" r="1.2"/><circle cx="12" cy="12" r="1.2"/><circle cx="19" cy="12" r="1.2"/></symbol>
|
||||
<symbol id="i-shield" viewBox="0 0 24 24"><path d="M12 3 4 6v6c0 5 3.5 8 8 9 4.5-1 8-4 8-9V6z"/><path d="m9 12 2 2 4-4"/></symbol>
|
||||
<symbol id="i-clip" viewBox="0 0 24 24"><path d="m21 11-8.5 8.5a5 5 0 0 1-7-7L14 4a3.3 3.3 0 0 1 4.7 4.7L10.5 17a1.7 1.7 0 0 1-2.4-2.4L16 6.7"/></symbol>
|
||||
<symbol id="i-note" viewBox="0 0 24 24"><path d="M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><path d="M14 3v6h6"/></symbol>
|
||||
<symbol id="i-bolt" viewBox="0 0 24 24"><path d="M13 2 4 14h7l-1 8 9-12h-7z"/></symbol>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
<div class="app">
|
||||
<header class="titlebar">
|
||||
<div class="traffic"><i></i><i></i><i></i></div>
|
||||
<div class="lead">
|
||||
<button class="account-chip">
|
||||
<span class="avatar" data-hue="4">PJ</span>
|
||||
[email protected]
|
||||
<svg class="chev" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#i-chev"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="view-switch">
|
||||
<button class="view-option"><span class="key">1</span>Inbox</button>
|
||||
<button class="view-option" data-active="true"><span class="key">2</span>Feed</button>
|
||||
<button class="view-option"><span class="key">3</span>Paper Trail</button>
|
||||
</div>
|
||||
<div class="trail">
|
||||
<button class="icon-button" title="Search /"><svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-search"/></svg></button>
|
||||
<button class="icon-button" title="Places ⌘K"><svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-places"/></svg></button>
|
||||
<button class="write-button"><svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#i-pen"/></svg>Write<span class="key">c</span></button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="stage">
|
||||
<div class="feed">
|
||||
<div class="feed-inner">
|
||||
<div class="banner">
|
||||
<svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-shield"/></svg>
|
||||
<span>Images are loaded through Margin, never from the sender. Trackers stripped from <b>2 items</b> today.</span>
|
||||
</div>
|
||||
|
||||
<article class="feed-card">
|
||||
<div class="feed-head">
|
||||
<span class="avatar" data-brand>Tb</span>
|
||||
<div class="msg-who"><div class="msg-name">The Browser<span class="addr">[email protected]</span></div></div>
|
||||
<div class="msg-time">Today 06:00</div>
|
||||
</div>
|
||||
<h2 class="feed-title">Five things worth reading this weekend</h2>
|
||||
<div class="feed-body">
|
||||
<div class="feed-hero"></div>
|
||||
<p>Good morning. It is a long weekend in some places and a wet one in most, so this edition leans toward pieces you can settle into. A long piece on the history of the pencil, a short one on why bridges hum, and three more.</p>
|
||||
<p><b>The pencil, at length.</b> Henry Petroski wrote a whole book about the pencil and this essay is the argument for why that was a reasonable thing to do. Graphite, cedar, the Napoleonic wars and a factory in Nuremberg that still runs. Forty minutes, worth every one.</p>
|
||||
<p><b>Why bridges hum.</b> A short explanation of vortex shedding from an engineer who spent a career listening to cables. Includes the story of a footbridge that was closed for two years because it sang in a particular wind.</p>
|
||||
<p><b>The last typewriter repairman in Mumbai.</b> A profile that is really about what it means to keep a trade going after the trade has gone.</p>
|
||||
<div class="feed-fade"></div>
|
||||
</div>
|
||||
<div class="feed-foot">
|
||||
<button class="button">Read more<span class="key">↩</span></button>
|
||||
<button class="button">Save clip</button>
|
||||
<span class="spacer"></span>
|
||||
<button class="button" data-variant="ghost">Unsubscribe</button>
|
||||
<button class="button" data-variant="ghost">Move<span class="key">v</span></button>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<div class="left-off">You left off here</div>
|
||||
|
||||
<article class="feed-card">
|
||||
<div class="feed-head">
|
||||
<span class="avatar" data-brand>Fn</span>
|
||||
<div class="msg-who"><div class="msg-name">Field Notes Dispatch<span class="addr">[email protected]</span></div></div>
|
||||
<div class="msg-time">Mon 09:30</div>
|
||||
</div>
|
||||
<h2 class="feed-title">September: the notebook that survived a washing machine</h2>
|
||||
<div class="feed-body" data-open>
|
||||
<p>A reader in Tromsø sent us a photo of a pocket notebook that went through a full cycle at forty degrees, in a coat, with the coat. Every page is still legible.</p>
|
||||
<p>Also this month: the autumn edition ships on the 15th. Three colours, the usual count.</p>
|
||||
</div>
|
||||
<div class="feed-foot">
|
||||
<button class="button">See less<span class="key">↩</span></button>
|
||||
<button class="button">Save clip</button>
|
||||
<span class="spacer"></span>
|
||||
<button class="button" data-variant="ghost">Unsubscribe</button>
|
||||
<button class="button" data-variant="ghost">Move<span class="key">v</span></button>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="feed-card" data-collapsed>
|
||||
<div class="feed-head">
|
||||
<span class="avatar" data-brand>Ll</span>
|
||||
<div class="msg-who"><div class="msg-name">Ledger Lines<span class="addr">[email protected]</span></div></div>
|
||||
<div class="msg-time">30 Aug</div>
|
||||
</div>
|
||||
<h2 class="feed-title">The bond market, again</h2>
|
||||
<div class="feed-foot">
|
||||
<button class="button">Read more<span class="key">↩</span></button>
|
||||
<button class="button">Save clip</button>
|
||||
<span class="spacer"></span>
|
||||
<button class="button" data-variant="ghost">Unsubscribe</button>
|
||||
<button class="button" data-variant="ghost">Move<span class="key">v</span></button>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,135 @@
|
||||
<!doctype html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Margin Mail: Focus & Reply</title>
|
||||
<link rel="stylesheet" href="mail.css">
|
||||
</head>
|
||||
<body>
|
||||
<svg width="0" height="0" style="position:absolute">
|
||||
<defs>
|
||||
<symbol id="i-search" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></symbol>
|
||||
<symbol id="i-places" viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h10"/></symbol>
|
||||
<symbol id="i-pen" viewBox="0 0 24 24"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z"/></symbol>
|
||||
<symbol id="i-chev" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></symbol>
|
||||
<symbol id="i-reply" viewBox="0 0 24 24"><path d="M9 17 4 12l5-5"/><path d="M20 18v-2a4 4 0 0 0-4-4H4"/></symbol>
|
||||
<symbol id="i-later" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></symbol>
|
||||
<symbol id="i-aside" viewBox="0 0 24 24"><path d="M12 17v4"/><path d="M8 3h8l-1 7 3 3H6l3-3z"/></symbol>
|
||||
<symbol id="i-snooze" viewBox="0 0 24 24"><path d="M5 3 2 6"/><path d="m22 6-3-3"/><circle cx="12" cy="13" r="8"/><path d="M12 9v4l2 2"/></symbol>
|
||||
<symbol id="i-archive" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="4" rx="1"/><path d="M5 8v11a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V8"/><path d="M10 12h4"/></symbol>
|
||||
<symbol id="i-more" viewBox="0 0 24 24"><circle cx="5" cy="12" r="1.2"/><circle cx="12" cy="12" r="1.2"/><circle cx="19" cy="12" r="1.2"/></symbol>
|
||||
<symbol id="i-shield" viewBox="0 0 24 24"><path d="M12 3 4 6v6c0 5 3.5 8 8 9 4.5-1 8-4 8-9V6z"/><path d="m9 12 2 2 4-4"/></symbol>
|
||||
<symbol id="i-clip" viewBox="0 0 24 24"><path d="m21 11-8.5 8.5a5 5 0 0 1-7-7L14 4a3.3 3.3 0 0 1 4.7 4.7L10.5 17a1.7 1.7 0 0 1-2.4-2.4L16 6.7"/></symbol>
|
||||
<symbol id="i-note" viewBox="0 0 24 24"><path d="M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><path d="M14 3v6h6"/></symbol>
|
||||
<symbol id="i-bolt" viewBox="0 0 24 24"><path d="M13 2 4 14h7l-1 8 9-12h-7z"/></symbol>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
<div class="app">
|
||||
<header class="titlebar">
|
||||
<div class="traffic"><i></i><i></i><i></i></div>
|
||||
<div class="lead">
|
||||
<button class="account-chip">
|
||||
<span class="avatar" data-hue="4">PJ</span>
|
||||
[email protected]
|
||||
<svg class="chev" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#i-chev"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="view-switch">
|
||||
<button class="view-option"><span class="key">1</span>Inbox</button>
|
||||
<button class="view-option"><span class="key">2</span>Feed</button>
|
||||
<button class="view-option"><span class="key">3</span>Paper Trail</button>
|
||||
</div>
|
||||
<div class="trail">
|
||||
<button class="icon-button" title="Search /"><svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-search"/></svg></button>
|
||||
<button class="icon-button" title="Places ⌘K"><svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-places"/></svg></button>
|
||||
<button class="write-button"><svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#i-pen"/></svg>Write<span class="key">c</span></button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="stage">
|
||||
<section class="focus">
|
||||
<div class="focus-inner">
|
||||
<div class="screen-intro">
|
||||
<h1>Focus & Reply</h1>
|
||||
<p>Every thread in Reply later, each with its own reply box. Send, or move on to the next.</p>
|
||||
</div>
|
||||
<div class="focus-hint">
|
||||
<span>2 left</span>
|
||||
<span>·</span>
|
||||
<span class="key">Tab</span><span>next</span>
|
||||
<span>·</span>
|
||||
<span class="key">⌘↩</span><span>send</span>
|
||||
<span>·</span>
|
||||
<span class="key">Esc</span><span>back</span>
|
||||
</div>
|
||||
|
||||
<div class="focus-item" data-sent>
|
||||
<div class="focus-sent">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>
|
||||
<span>Sent to <b>Robyn Madison</b> · You have an invitation from Robyn Madison</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="focus-item" data-active>
|
||||
<div class="focus-read">
|
||||
<h2 class="thread-subject">Re: Cooper's parent-teacher conference</h2>
|
||||
<div class="msg">
|
||||
<div class="msg-head">
|
||||
<span class="avatar" data-hue="7">JW</span>
|
||||
<div class="msg-who"><div class="msg-name">Jeff Wolfe<span class="addr">[email protected]</span></div><div class="msg-to">to you</div></div>
|
||||
<div class="msg-time">Yesterday 16:02</div>
|
||||
</div>
|
||||
<div class="msg-body">
|
||||
<p>Mr. and Mrs. Young, this is just a reminder to schedule your parent-teacher conference with me. You can feel free to do that here.</p>
|
||||
<p>If you would prefer a video call instead of meeting at school, I am happy to do that as well. Please let me know what date and time works best for you.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="focus-write">
|
||||
<div class="reply">
|
||||
<div class="reply-head">Reply to <b>Jeff Wolfe</b></div>
|
||||
<div class="reply-body">Thanks Jeff. Wednesday at 4pm in person would suit us, if that is still open.<span class="caret"></span></div>
|
||||
<div class="reply-foot">
|
||||
<button class="button" data-variant="primary">Send<span class="key">⌘↩</span></button>
|
||||
<button class="button" data-variant="ghost">Skip<span class="key">Tab</span></button>
|
||||
<span class="spacer"></span>
|
||||
<button class="icon-button" title="Attach"><svg width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-clip"/></svg></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="focus-item">
|
||||
<div class="focus-read">
|
||||
<h2 class="thread-subject">Re: Life insurance quote</h2>
|
||||
<div class="msg">
|
||||
<div class="msg-head">
|
||||
<span class="avatar" data-hue="5">TM</span>
|
||||
<div class="msg-who"><div class="msg-name">Todd Markham<span class="addr">[email protected]</span></div><div class="msg-to">to you</div></div>
|
||||
<div class="msg-time">Mon 09:48</div>
|
||||
</div>
|
||||
<div class="msg-body">
|
||||
<p>Following up on the quote I sent through last week for the twenty year term policy. If the premium looks right, I can start the paperwork whenever you are ready.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="focus-write">
|
||||
<div class="reply">
|
||||
<div class="reply-head">Reply to <b>Todd Markham</b></div>
|
||||
<div class="reply-body"><span class="placeholder">Write a reply</span></div>
|
||||
<div class="reply-foot">
|
||||
<button class="button" data-variant="primary">Send<span class="key">⌘↩</span></button>
|
||||
<button class="button" data-variant="ghost">Skip<span class="key">Tab</span></button>
|
||||
<span class="spacer"></span>
|
||||
<button class="icon-button" title="Attach"><svg width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-clip"/></svg></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,244 @@
|
||||
<!doctype html>
|
||||
<html lang="en" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Margin Mail: Inbox, dark</title>
|
||||
<link rel="stylesheet" href="mail.css">
|
||||
</head>
|
||||
<body>
|
||||
<svg width="0" height="0" style="position:absolute">
|
||||
<defs>
|
||||
<symbol id="i-search" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></symbol>
|
||||
<symbol id="i-places" viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h10"/></symbol>
|
||||
<symbol id="i-pen" viewBox="0 0 24 24"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z"/></symbol>
|
||||
<symbol id="i-chev" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></symbol>
|
||||
<symbol id="i-reply" viewBox="0 0 24 24"><path d="M9 17 4 12l5-5"/><path d="M20 18v-2a4 4 0 0 0-4-4H4"/></symbol>
|
||||
<symbol id="i-later" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></symbol>
|
||||
<symbol id="i-aside" viewBox="0 0 24 24"><path d="M12 17v4"/><path d="M8 3h8l-1 7 3 3H6l3-3z"/></symbol>
|
||||
<symbol id="i-snooze" viewBox="0 0 24 24"><path d="M5 3 2 6"/><path d="m22 6-3-3"/><circle cx="12" cy="13" r="8"/><path d="M12 9v4l2 2"/></symbol>
|
||||
<symbol id="i-archive" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="4" rx="1"/><path d="M5 8v11a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V8"/><path d="M10 12h4"/></symbol>
|
||||
<symbol id="i-more" viewBox="0 0 24 24"><circle cx="5" cy="12" r="1.2"/><circle cx="12" cy="12" r="1.2"/><circle cx="19" cy="12" r="1.2"/></symbol>
|
||||
<symbol id="i-shield" viewBox="0 0 24 24"><path d="M12 3 4 6v6c0 5 3.5 8 8 9 4.5-1 8-4 8-9V6z"/><path d="m9 12 2 2 4-4"/></symbol>
|
||||
<symbol id="i-clip" viewBox="0 0 24 24"><path d="m21 11-8.5 8.5a5 5 0 0 1-7-7L14 4a3.3 3.3 0 0 1 4.7 4.7L10.5 17a1.7 1.7 0 0 1-2.4-2.4L16 6.7"/></symbol>
|
||||
<symbol id="i-note" viewBox="0 0 24 24"><path d="M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><path d="M14 3v6h6"/></symbol>
|
||||
<symbol id="i-bolt" viewBox="0 0 24 24"><path d="M13 2 4 14h7l-1 8 9-12h-7z"/></symbol>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
<div class="app">
|
||||
<header class="titlebar">
|
||||
<div class="traffic"><i></i><i></i><i></i></div>
|
||||
<div class="lead">
|
||||
<button class="account-chip">
|
||||
<span class="avatar" data-hue="4">PJ</span>
|
||||
[email protected]
|
||||
<svg class="chev" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#i-chev"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="view-switch">
|
||||
<button class="view-option" data-active="true"><span class="key">1</span>Inbox</button>
|
||||
<button class="view-option"><span class="key">2</span>Feed</button>
|
||||
<button class="view-option"><span class="key">3</span>Paper Trail</button>
|
||||
</div>
|
||||
<div class="trail">
|
||||
<button class="icon-button" title="Search /"><svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-search"/></svg></button>
|
||||
<button class="icon-button" title="Places ⌘K"><svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-places"/></svg></button>
|
||||
<button class="write-button"><svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#i-pen"/></svg>Write<span class="key">c</span></button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="stage">
|
||||
<section class="list-col">
|
||||
<div class="list-head">
|
||||
<h1 class="list-title">Inbox</h1>
|
||||
<button class="pill"><svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#i-shield"/></svg>Screen 3 new senders<span class="key">6</span></button>
|
||||
</div>
|
||||
<div class="list">
|
||||
<div class="section">New for you</div>
|
||||
|
||||
<div class="row" data-new>
|
||||
<div class="row-gutter"><span class="row-dot"></span></div>
|
||||
<span class="avatar" data-hue="2">MR</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">Maya Raghunathan</span><span class="row-count">3</span><span class="row-time">11:42</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Dinner on Thursday?</span><span class="row-snippet">Priya said the place on Church Street takes bookings now, want me to</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" data-new data-selected>
|
||||
<div class="row-gutter"><span class="row-dot"></span></div>
|
||||
<span class="avatar" data-hue="6">AK</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">Arun Kulkarni</span><span class="row-time">10:15</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Re: Lease renewal for the studio</span><span class="row-snippet">Attached the revised draft. The only change is clause 7, which now</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" data-new>
|
||||
<div class="row-gutter"><span class="row-dot"></span></div>
|
||||
<span class="avatar" data-brand>Ai</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">Airbnb</span><span class="row-time">09:03</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Your reservation in Lisbon is confirmed</span><span class="row-snippet">Check-in Friday 12 September after 15:00. Your host Inês will send</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" data-new>
|
||||
<div class="row-gutter"><span class="row-dot"></span></div>
|
||||
<span class="avatar" data-hue="7">SO</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">Sam Okafor</span><span class="row-time">Yesterday</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Piano lessons, the form you sent</span><span class="row-snippet">Got it, thank you. Wednesdays at five work for us. Is there a</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="seen">
|
||||
<div class="section">Previously seen</div>
|
||||
|
||||
<div class="row" data-noted>
|
||||
<div class="row-gutter"></div>
|
||||
<span class="avatar" data-hue="5">LB</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">Lena Brandt</span><span class="row-count">7</span><span class="row-time">Yesterday</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Kitchen bench quote</span><span class="row-snippet">Sounds good, Julie. Any afternoon next week works for me.</span></div>
|
||||
</div>
|
||||
<div class="row-note">Ask about the oak finish before confirming</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="row-gutter"></div>
|
||||
<span class="avatar" data-hue="3">DP</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">Dev Patel</span><span class="row-time">Mon</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Slides from the talk</span><span class="row-snippet">Here they are, plus the reading list I mentioned. The paper on</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="row-gutter"></div>
|
||||
<span class="avatar" data-brand>Ds</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">DocuSign</span><span class="row-time">Mon</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Completed: Studio lease 2026</span><span class="row-snippet">All parties have completed the envelope. You can access the</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="row-gutter"></div>
|
||||
<span class="avatar" data-hue="8">HW</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">Hannah Weiss</span><span class="row-count">2</span><span class="row-time">Sun</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Photos from the Hawaii trip</span><span class="row-snippet">Finally went through them all. The ones from the ridge walk are</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="row-gutter"></div>
|
||||
<span class="avatar" data-hue="1">RY</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">Russell Young</span><span class="row-time">30 Aug</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Pumpkin bread recipe</span><span class="row-snippet">From my mother's card, transcribed as best I could. Bake at 175</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="piles">
|
||||
<button class="pile">
|
||||
<div class="pile-card" data-depth="2"></div>
|
||||
<div class="pile-card" data-depth="1"></div>
|
||||
<div class="pile-card" data-depth="0">
|
||||
<div class="pile-top"><svg width="11" height="11" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#i-later"/></svg>Reply later<span class="key">4</span></div>
|
||||
<div class="pile-subject">Re: Cooper's parent-teacher conference</div>
|
||||
<div class="pile-from">Jeff Wolfe, and 2 more</div>
|
||||
</div>
|
||||
</button>
|
||||
<button class="pile">
|
||||
<div class="pile-card" data-depth="1"></div>
|
||||
<div class="pile-card" data-depth="0">
|
||||
<div class="pile-top"><svg width="11" height="11" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#i-aside"/></svg>Set aside<span class="key">5</span></div>
|
||||
<div class="pile-subject">Les Misérables tickets</div>
|
||||
<div class="pile-from">Caroline Bauhaus, and 1 more</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="pane">
|
||||
<div class="pane-bar">
|
||||
<button class="verb"><svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-reply"/></svg>Reply<span class="key">r</span></button>
|
||||
<button class="verb"><svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-later"/></svg>Reply later<span class="key">l</span></button>
|
||||
<button class="verb"><svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-aside"/></svg>Set aside<span class="key">s</span></button>
|
||||
<button class="verb"><svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-snooze"/></svg>Snooze<span class="key">b</span></button>
|
||||
<span class="spacer"></span>
|
||||
<button class="verb"><svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-archive"/></svg>Archive<span class="key">e</span></button>
|
||||
<button class="icon-button" title="More ."><svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-more"/></svg></button>
|
||||
</div>
|
||||
|
||||
<div class="thread">
|
||||
<div class="thread-inner">
|
||||
<div class="thread-head">
|
||||
<h1 class="thread-subject">Lease renewal for the studio</h1>
|
||||
<div class="thread-meta">
|
||||
<span class="avatars"><span class="avatar" data-hue="6">AK</span><span class="avatar" data-hue="4">PJ</span></span>
|
||||
<span>Arun Kulkarni and you</span>
|
||||
<span>·</span>
|
||||
<span>2 messages</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="banner">
|
||||
<svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-shield"/></svg>
|
||||
<span>Blocked <b>1 tracker</b> from HubSpot. Remote images are off for this sender.</span>
|
||||
<button class="banner-action">Show images</button>
|
||||
</div>
|
||||
|
||||
<div class="msg" data-collapsed>
|
||||
<div class="msg-head">
|
||||
<span class="avatar" data-hue="4">PJ</span>
|
||||
<div class="msg-who"><div class="msg-name">You</div><div class="msg-preview">Hi Arun, thanks for sending the renewal over. Two things before I sign: the notice period in clause 7 and</div></div>
|
||||
<div class="msg-time">Mon 14:20</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="msg">
|
||||
<div class="msg-head">
|
||||
<span class="avatar" data-hue="6">AK</span>
|
||||
<div class="msg-who"><div class="msg-name">Arun Kulkarni<span class="addr">[email protected]</span></div><div class="msg-to">to you</div></div>
|
||||
<div class="msg-time">Today 10:15</div>
|
||||
</div>
|
||||
<div class="msg-body">
|
||||
<p>Hi Priyanshu,</p>
|
||||
<p>Attached the revised draft. The only change is clause 7, which now reads three months either side rather than six. The rent review in clause 12 stays as it was, tied to the index and capped at four percent.</p>
|
||||
<p>If that works, sign when you get a moment and I will countersign the same day. Happy to walk through anything on a call, Thursday afternoon is open.</p>
|
||||
<p>Best,<br>Arun</p>
|
||||
<span class="quoted">··· Show quoted text</span>
|
||||
<div class="attachments">
|
||||
<div class="attachment"><span class="ext">PDF</span>Studio-lease-2026-v2.pdf<span class="size">412 KB</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="note">
|
||||
<span class="note-label">Note to self</span>
|
||||
Check the four percent cap against last year's index before signing. Ask Maya if she still has the 2024 letter.
|
||||
</div>
|
||||
|
||||
<div class="reply">
|
||||
<div class="reply-head">Reply to <b>Arun Kulkarni</b><span style="margin-left:auto" class="key">a</span><span style="color:var(--ink-faint)">reply all</span></div>
|
||||
<div class="reply-body">Thanks Arun, three months works. One more question on clause 12<span class="caret"></span></div>
|
||||
<div class="reply-foot">
|
||||
<button class="button" data-variant="primary">Send<span class="key">⌘↩</span></button>
|
||||
<button class="button" data-variant="ghost">Remind me if no reply</button>
|
||||
<span class="spacer"></span>
|
||||
<button class="icon-button" title="Attach"><svg width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-clip"/></svg></button>
|
||||
<button class="icon-button" title="Note"><svg width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-note"/></svg></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,244 @@
|
||||
<!doctype html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Margin Mail: Inbox</title>
|
||||
<link rel="stylesheet" href="mail.css">
|
||||
</head>
|
||||
<body>
|
||||
<svg width="0" height="0" style="position:absolute">
|
||||
<defs>
|
||||
<symbol id="i-search" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></symbol>
|
||||
<symbol id="i-places" viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h10"/></symbol>
|
||||
<symbol id="i-pen" viewBox="0 0 24 24"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z"/></symbol>
|
||||
<symbol id="i-chev" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></symbol>
|
||||
<symbol id="i-reply" viewBox="0 0 24 24"><path d="M9 17 4 12l5-5"/><path d="M20 18v-2a4 4 0 0 0-4-4H4"/></symbol>
|
||||
<symbol id="i-later" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></symbol>
|
||||
<symbol id="i-aside" viewBox="0 0 24 24"><path d="M12 17v4"/><path d="M8 3h8l-1 7 3 3H6l3-3z"/></symbol>
|
||||
<symbol id="i-snooze" viewBox="0 0 24 24"><path d="M5 3 2 6"/><path d="m22 6-3-3"/><circle cx="12" cy="13" r="8"/><path d="M12 9v4l2 2"/></symbol>
|
||||
<symbol id="i-archive" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="4" rx="1"/><path d="M5 8v11a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V8"/><path d="M10 12h4"/></symbol>
|
||||
<symbol id="i-more" viewBox="0 0 24 24"><circle cx="5" cy="12" r="1.2"/><circle cx="12" cy="12" r="1.2"/><circle cx="19" cy="12" r="1.2"/></symbol>
|
||||
<symbol id="i-shield" viewBox="0 0 24 24"><path d="M12 3 4 6v6c0 5 3.5 8 8 9 4.5-1 8-4 8-9V6z"/><path d="m9 12 2 2 4-4"/></symbol>
|
||||
<symbol id="i-clip" viewBox="0 0 24 24"><path d="m21 11-8.5 8.5a5 5 0 0 1-7-7L14 4a3.3 3.3 0 0 1 4.7 4.7L10.5 17a1.7 1.7 0 0 1-2.4-2.4L16 6.7"/></symbol>
|
||||
<symbol id="i-note" viewBox="0 0 24 24"><path d="M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><path d="M14 3v6h6"/></symbol>
|
||||
<symbol id="i-bolt" viewBox="0 0 24 24"><path d="M13 2 4 14h7l-1 8 9-12h-7z"/></symbol>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
<div class="app">
|
||||
<header class="titlebar">
|
||||
<div class="traffic"><i></i><i></i><i></i></div>
|
||||
<div class="lead">
|
||||
<button class="account-chip">
|
||||
<span class="avatar" data-hue="4">PJ</span>
|
||||
[email protected]
|
||||
<svg class="chev" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#i-chev"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="view-switch">
|
||||
<button class="view-option" data-active="true"><span class="key">1</span>Inbox</button>
|
||||
<button class="view-option"><span class="key">2</span>Feed</button>
|
||||
<button class="view-option"><span class="key">3</span>Paper Trail</button>
|
||||
</div>
|
||||
<div class="trail">
|
||||
<button class="icon-button" title="Search /"><svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-search"/></svg></button>
|
||||
<button class="icon-button" title="Places ⌘K"><svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-places"/></svg></button>
|
||||
<button class="write-button"><svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#i-pen"/></svg>Write<span class="key">c</span></button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="stage">
|
||||
<section class="list-col">
|
||||
<div class="list-head">
|
||||
<h1 class="list-title">Inbox</h1>
|
||||
<button class="pill"><svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#i-shield"/></svg>Screen 3 new senders<span class="key">6</span></button>
|
||||
</div>
|
||||
<div class="list">
|
||||
<div class="section">New for you</div>
|
||||
|
||||
<div class="row" data-new>
|
||||
<div class="row-gutter"><span class="row-dot"></span></div>
|
||||
<span class="avatar" data-hue="2">MR</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">Maya Raghunathan</span><span class="row-count">3</span><span class="row-time">11:42</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Dinner on Thursday?</span><span class="row-snippet">Priya said the place on Church Street takes bookings now, want me to</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" data-new data-selected>
|
||||
<div class="row-gutter"><span class="row-dot"></span></div>
|
||||
<span class="avatar" data-hue="6">AK</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">Arun Kulkarni</span><span class="row-time">10:15</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Re: Lease renewal for the studio</span><span class="row-snippet">Attached the revised draft. The only change is clause 7, which now</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" data-new>
|
||||
<div class="row-gutter"><span class="row-dot"></span></div>
|
||||
<span class="avatar" data-brand>Ai</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">Airbnb</span><span class="row-time">09:03</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Your reservation in Lisbon is confirmed</span><span class="row-snippet">Check-in Friday 12 September after 15:00. Your host Inês will send</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" data-new>
|
||||
<div class="row-gutter"><span class="row-dot"></span></div>
|
||||
<span class="avatar" data-hue="7">SO</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">Sam Okafor</span><span class="row-time">Yesterday</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Piano lessons, the form you sent</span><span class="row-snippet">Got it, thank you. Wednesdays at five work for us. Is there a</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="seen">
|
||||
<div class="section">Previously seen</div>
|
||||
|
||||
<div class="row" data-noted>
|
||||
<div class="row-gutter"></div>
|
||||
<span class="avatar" data-hue="5">LB</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">Lena Brandt</span><span class="row-count">7</span><span class="row-time">Yesterday</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Kitchen bench quote</span><span class="row-snippet">Sounds good, Julie. Any afternoon next week works for me.</span></div>
|
||||
</div>
|
||||
<div class="row-note">Ask about the oak finish before confirming</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="row-gutter"></div>
|
||||
<span class="avatar" data-hue="3">DP</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">Dev Patel</span><span class="row-time">Mon</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Slides from the talk</span><span class="row-snippet">Here they are, plus the reading list I mentioned. The paper on</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="row-gutter"></div>
|
||||
<span class="avatar" data-brand>Ds</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">DocuSign</span><span class="row-time">Mon</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Completed: Studio lease 2026</span><span class="row-snippet">All parties have completed the envelope. You can access the</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="row-gutter"></div>
|
||||
<span class="avatar" data-hue="8">HW</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">Hannah Weiss</span><span class="row-count">2</span><span class="row-time">Sun</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Photos from the Hawaii trip</span><span class="row-snippet">Finally went through them all. The ones from the ridge walk are</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="row-gutter"></div>
|
||||
<span class="avatar" data-hue="1">RY</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">Russell Young</span><span class="row-time">30 Aug</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Pumpkin bread recipe</span><span class="row-snippet">From my mother's card, transcribed as best I could. Bake at 175</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="piles">
|
||||
<button class="pile">
|
||||
<div class="pile-card" data-depth="2"></div>
|
||||
<div class="pile-card" data-depth="1"></div>
|
||||
<div class="pile-card" data-depth="0">
|
||||
<div class="pile-top"><svg width="11" height="11" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#i-later"/></svg>Reply later<span class="key">4</span></div>
|
||||
<div class="pile-subject">Re: Cooper's parent-teacher conference</div>
|
||||
<div class="pile-from">Jeff Wolfe, and 2 more</div>
|
||||
</div>
|
||||
</button>
|
||||
<button class="pile">
|
||||
<div class="pile-card" data-depth="1"></div>
|
||||
<div class="pile-card" data-depth="0">
|
||||
<div class="pile-top"><svg width="11" height="11" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#i-aside"/></svg>Set aside<span class="key">5</span></div>
|
||||
<div class="pile-subject">Les Misérables tickets</div>
|
||||
<div class="pile-from">Caroline Bauhaus, and 1 more</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="pane">
|
||||
<div class="pane-bar">
|
||||
<button class="verb"><svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-reply"/></svg>Reply<span class="key">r</span></button>
|
||||
<button class="verb"><svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-later"/></svg>Reply later<span class="key">l</span></button>
|
||||
<button class="verb"><svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-aside"/></svg>Set aside<span class="key">s</span></button>
|
||||
<button class="verb"><svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-snooze"/></svg>Snooze<span class="key">b</span></button>
|
||||
<span class="spacer"></span>
|
||||
<button class="verb"><svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-archive"/></svg>Archive<span class="key">e</span></button>
|
||||
<button class="icon-button" title="More ."><svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-more"/></svg></button>
|
||||
</div>
|
||||
|
||||
<div class="thread">
|
||||
<div class="thread-inner">
|
||||
<div class="thread-head">
|
||||
<h1 class="thread-subject">Lease renewal for the studio</h1>
|
||||
<div class="thread-meta">
|
||||
<span class="avatars"><span class="avatar" data-hue="6">AK</span><span class="avatar" data-hue="4">PJ</span></span>
|
||||
<span>Arun Kulkarni and you</span>
|
||||
<span>·</span>
|
||||
<span>2 messages</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="banner">
|
||||
<svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-shield"/></svg>
|
||||
<span>Blocked <b>1 tracker</b> from HubSpot. Remote images are off for this sender.</span>
|
||||
<button class="banner-action">Show images</button>
|
||||
</div>
|
||||
|
||||
<div class="msg" data-collapsed>
|
||||
<div class="msg-head">
|
||||
<span class="avatar" data-hue="4">PJ</span>
|
||||
<div class="msg-who"><div class="msg-name">You</div><div class="msg-preview">Hi Arun, thanks for sending the renewal over. Two things before I sign: the notice period in clause 7 and</div></div>
|
||||
<div class="msg-time">Mon 14:20</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="msg">
|
||||
<div class="msg-head">
|
||||
<span class="avatar" data-hue="6">AK</span>
|
||||
<div class="msg-who"><div class="msg-name">Arun Kulkarni<span class="addr">[email protected]</span></div><div class="msg-to">to you</div></div>
|
||||
<div class="msg-time">Today 10:15</div>
|
||||
</div>
|
||||
<div class="msg-body">
|
||||
<p>Hi Priyanshu,</p>
|
||||
<p>Attached the revised draft. The only change is clause 7, which now reads three months either side rather than six. The rent review in clause 12 stays as it was, tied to the index and capped at four percent.</p>
|
||||
<p>If that works, sign when you get a moment and I will countersign the same day. Happy to walk through anything on a call, Thursday afternoon is open.</p>
|
||||
<p>Best,<br>Arun</p>
|
||||
<span class="quoted">··· Show quoted text</span>
|
||||
<div class="attachments">
|
||||
<div class="attachment"><span class="ext">PDF</span>Studio-lease-2026-v2.pdf<span class="size">412 KB</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="note">
|
||||
<span class="note-label">Note to self</span>
|
||||
Check the four percent cap against last year's index before signing. Ask Maya if she still has the 2024 letter.
|
||||
</div>
|
||||
|
||||
<div class="reply">
|
||||
<div class="reply-head">Reply to <b>Arun Kulkarni</b><span style="margin-left:auto" class="key">a</span><span style="color:var(--ink-faint)">reply all</span></div>
|
||||
<div class="reply-body">Thanks Arun, three months works. One more question on clause 12<span class="caret"></span></div>
|
||||
<div class="reply-foot">
|
||||
<button class="button" data-variant="primary">Send<span class="key">⌘↩</span></button>
|
||||
<button class="button" data-variant="ghost">Remind me if no reply</button>
|
||||
<span class="spacer"></span>
|
||||
<button class="icon-button" title="Attach"><svg width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-clip"/></svg></button>
|
||||
<button class="icon-button" title="Note"><svg width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-note"/></svg></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,278 @@
|
||||
<!doctype html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Margin Mail: Palette</title>
|
||||
<link rel="stylesheet" href="mail.css">
|
||||
</head>
|
||||
<body>
|
||||
<svg width="0" height="0" style="position:absolute">
|
||||
<defs>
|
||||
<symbol id="i-search" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></symbol>
|
||||
<symbol id="i-places" viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h10"/></symbol>
|
||||
<symbol id="i-pen" viewBox="0 0 24 24"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z"/></symbol>
|
||||
<symbol id="i-chev" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></symbol>
|
||||
<symbol id="i-reply" viewBox="0 0 24 24"><path d="M9 17 4 12l5-5"/><path d="M20 18v-2a4 4 0 0 0-4-4H4"/></symbol>
|
||||
<symbol id="i-later" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></symbol>
|
||||
<symbol id="i-aside" viewBox="0 0 24 24"><path d="M12 17v4"/><path d="M8 3h8l-1 7 3 3H6l3-3z"/></symbol>
|
||||
<symbol id="i-snooze" viewBox="0 0 24 24"><path d="M5 3 2 6"/><path d="m22 6-3-3"/><circle cx="12" cy="13" r="8"/><path d="M12 9v4l2 2"/></symbol>
|
||||
<symbol id="i-archive" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="4" rx="1"/><path d="M5 8v11a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V8"/><path d="M10 12h4"/></symbol>
|
||||
<symbol id="i-more" viewBox="0 0 24 24"><circle cx="5" cy="12" r="1.2"/><circle cx="12" cy="12" r="1.2"/><circle cx="19" cy="12" r="1.2"/></symbol>
|
||||
<symbol id="i-shield" viewBox="0 0 24 24"><path d="M12 3 4 6v6c0 5 3.5 8 8 9 4.5-1 8-4 8-9V6z"/><path d="m9 12 2 2 4-4"/></symbol>
|
||||
<symbol id="i-clip" viewBox="0 0 24 24"><path d="m21 11-8.5 8.5a5 5 0 0 1-7-7L14 4a3.3 3.3 0 0 1 4.7 4.7L10.5 17a1.7 1.7 0 0 1-2.4-2.4L16 6.7"/></symbol>
|
||||
<symbol id="i-note" viewBox="0 0 24 24"><path d="M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><path d="M14 3v6h6"/></symbol>
|
||||
<symbol id="i-bolt" viewBox="0 0 24 24"><path d="M13 2 4 14h7l-1 8 9-12h-7z"/></symbol>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
<div class="app">
|
||||
<header class="titlebar">
|
||||
<div class="traffic"><i></i><i></i><i></i></div>
|
||||
<div class="lead">
|
||||
<button class="account-chip">
|
||||
<span class="avatar" data-hue="4">PJ</span>
|
||||
[email protected]
|
||||
<svg class="chev" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#i-chev"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="view-switch">
|
||||
<button class="view-option" data-active="true"><span class="key">1</span>Inbox</button>
|
||||
<button class="view-option"><span class="key">2</span>Feed</button>
|
||||
<button class="view-option"><span class="key">3</span>Paper Trail</button>
|
||||
</div>
|
||||
<div class="trail">
|
||||
<button class="icon-button" title="Search /"><svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-search"/></svg></button>
|
||||
<button class="icon-button" title="Places ⌘K"><svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-places"/></svg></button>
|
||||
<button class="write-button"><svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#i-pen"/></svg>Write<span class="key">c</span></button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="stage">
|
||||
<section class="list-col">
|
||||
<div class="list-head">
|
||||
<h1 class="list-title">Inbox</h1>
|
||||
<button class="pill"><svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#i-shield"/></svg>Screen 3 new senders<span class="key">6</span></button>
|
||||
</div>
|
||||
<div class="list">
|
||||
<div class="section">New for you</div>
|
||||
|
||||
<div class="row" data-new>
|
||||
<div class="row-gutter"><span class="row-dot"></span></div>
|
||||
<span class="avatar" data-hue="2">MR</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">Maya Raghunathan</span><span class="row-count">3</span><span class="row-time">11:42</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Dinner on Thursday?</span><span class="row-snippet">Priya said the place on Church Street takes bookings now, want me to</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" data-new data-selected>
|
||||
<div class="row-gutter"><span class="row-dot"></span></div>
|
||||
<span class="avatar" data-hue="6">AK</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">Arun Kulkarni</span><span class="row-time">10:15</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Re: Lease renewal for the studio</span><span class="row-snippet">Attached the revised draft. The only change is clause 7, which now</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" data-new>
|
||||
<div class="row-gutter"><span class="row-dot"></span></div>
|
||||
<span class="avatar" data-brand>Ai</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">Airbnb</span><span class="row-time">09:03</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Your reservation in Lisbon is confirmed</span><span class="row-snippet">Check-in Friday 12 September after 15:00. Your host Inês will send</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" data-new>
|
||||
<div class="row-gutter"><span class="row-dot"></span></div>
|
||||
<span class="avatar" data-hue="7">SO</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">Sam Okafor</span><span class="row-time">Yesterday</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Piano lessons, the form you sent</span><span class="row-snippet">Got it, thank you. Wednesdays at five work for us. Is there a</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="seen">
|
||||
<div class="section">Previously seen</div>
|
||||
|
||||
<div class="row" data-noted>
|
||||
<div class="row-gutter"></div>
|
||||
<span class="avatar" data-hue="5">LB</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">Lena Brandt</span><span class="row-count">7</span><span class="row-time">Yesterday</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Kitchen bench quote</span><span class="row-snippet">Sounds good, Julie. Any afternoon next week works for me.</span></div>
|
||||
</div>
|
||||
<div class="row-note">Ask about the oak finish before confirming</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="row-gutter"></div>
|
||||
<span class="avatar" data-hue="3">DP</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">Dev Patel</span><span class="row-time">Mon</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Slides from the talk</span><span class="row-snippet">Here they are, plus the reading list I mentioned. The paper on</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="row-gutter"></div>
|
||||
<span class="avatar" data-brand>Ds</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">DocuSign</span><span class="row-time">Mon</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Completed: Studio lease 2026</span><span class="row-snippet">All parties have completed the envelope. You can access the</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="row-gutter"></div>
|
||||
<span class="avatar" data-hue="8">HW</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">Hannah Weiss</span><span class="row-count">2</span><span class="row-time">Sun</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Photos from the Hawaii trip</span><span class="row-snippet">Finally went through them all. The ones from the ridge walk are</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="row-gutter"></div>
|
||||
<span class="avatar" data-hue="1">RY</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">Russell Young</span><span class="row-time">30 Aug</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Pumpkin bread recipe</span><span class="row-snippet">From my mother's card, transcribed as best I could. Bake at 175</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="piles">
|
||||
<button class="pile">
|
||||
<div class="pile-card" data-depth="2"></div>
|
||||
<div class="pile-card" data-depth="1"></div>
|
||||
<div class="pile-card" data-depth="0">
|
||||
<div class="pile-top"><svg width="11" height="11" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#i-later"/></svg>Reply later<span class="key">4</span></div>
|
||||
<div class="pile-subject">Re: Cooper's parent-teacher conference</div>
|
||||
<div class="pile-from">Jeff Wolfe, and 2 more</div>
|
||||
</div>
|
||||
</button>
|
||||
<button class="pile">
|
||||
<div class="pile-card" data-depth="1"></div>
|
||||
<div class="pile-card" data-depth="0">
|
||||
<div class="pile-top"><svg width="11" height="11" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#i-aside"/></svg>Set aside<span class="key">5</span></div>
|
||||
<div class="pile-subject">Les Misérables tickets</div>
|
||||
<div class="pile-from">Caroline Bauhaus, and 1 more</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="pane">
|
||||
<div class="pane-bar">
|
||||
<button class="verb"><svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-reply"/></svg>Reply<span class="key">r</span></button>
|
||||
<button class="verb"><svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-later"/></svg>Reply later<span class="key">l</span></button>
|
||||
<button class="verb"><svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-aside"/></svg>Set aside<span class="key">s</span></button>
|
||||
<button class="verb"><svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-snooze"/></svg>Snooze<span class="key">b</span></button>
|
||||
<span class="spacer"></span>
|
||||
<button class="verb"><svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-archive"/></svg>Archive<span class="key">e</span></button>
|
||||
<button class="icon-button" title="More ."><svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-more"/></svg></button>
|
||||
</div>
|
||||
|
||||
<div class="thread">
|
||||
<div class="thread-inner">
|
||||
<div class="thread-head">
|
||||
<h1 class="thread-subject">Lease renewal for the studio</h1>
|
||||
<div class="thread-meta">
|
||||
<span class="avatars"><span class="avatar" data-hue="6">AK</span><span class="avatar" data-hue="4">PJ</span></span>
|
||||
<span>Arun Kulkarni and you</span>
|
||||
<span>·</span>
|
||||
<span>2 messages</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="banner">
|
||||
<svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-shield"/></svg>
|
||||
<span>Blocked <b>1 tracker</b> from HubSpot. Remote images are off for this sender.</span>
|
||||
<button class="banner-action">Show images</button>
|
||||
</div>
|
||||
|
||||
<div class="msg" data-collapsed>
|
||||
<div class="msg-head">
|
||||
<span class="avatar" data-hue="4">PJ</span>
|
||||
<div class="msg-who"><div class="msg-name">You</div><div class="msg-preview">Hi Arun, thanks for sending the renewal over. Two things before I sign: the notice period in clause 7 and</div></div>
|
||||
<div class="msg-time">Mon 14:20</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="msg">
|
||||
<div class="msg-head">
|
||||
<span class="avatar" data-hue="6">AK</span>
|
||||
<div class="msg-who"><div class="msg-name">Arun Kulkarni<span class="addr">[email protected]</span></div><div class="msg-to">to you</div></div>
|
||||
<div class="msg-time">Today 10:15</div>
|
||||
</div>
|
||||
<div class="msg-body">
|
||||
<p>Hi Priyanshu,</p>
|
||||
<p>Attached the revised draft. The only change is clause 7, which now reads three months either side rather than six. The rent review in clause 12 stays as it was, tied to the index and capped at four percent.</p>
|
||||
<p>If that works, sign when you get a moment and I will countersign the same day. Happy to walk through anything on a call, Thursday afternoon is open.</p>
|
||||
<p>Best,<br>Arun</p>
|
||||
<span class="quoted">··· Show quoted text</span>
|
||||
<div class="attachments">
|
||||
<div class="attachment"><span class="ext">PDF</span>Studio-lease-2026-v2.pdf<span class="size">412 KB</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="note">
|
||||
<span class="note-label">Note to self</span>
|
||||
Check the four percent cap against last year's index before signing. Ask Maya if she still has the 2024 letter.
|
||||
</div>
|
||||
|
||||
<div class="reply">
|
||||
<div class="reply-head">Reply to <b>Arun Kulkarni</b><span style="margin-left:auto" class="key">a</span><span style="color:var(--ink-faint)">reply all</span></div>
|
||||
<div class="reply-body">Thanks Arun, three months works. One more question on clause 12<span class="caret"></span></div>
|
||||
<div class="reply-foot">
|
||||
<button class="button" data-variant="primary">Send<span class="key">⌘↩</span></button>
|
||||
<button class="button" data-variant="ghost">Remind me if no reply</button>
|
||||
<span class="spacer"></span>
|
||||
<button class="icon-button" title="Attach"><svg width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-clip"/></svg></button>
|
||||
<button class="icon-button" title="Note"><svg width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-note"/></svg></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overlay" data-align="top">
|
||||
<div class="panel palette">
|
||||
<input class="palette-input" placeholder="Go to a place, run a command, or find a person" value="">
|
||||
<ul class="palette-list">
|
||||
<li class="palette-group">Places</li>
|
||||
<li class="palette-row"><span class="palette-label">Inbox</span><span class="palette-keys"><span class="key">1</span></span></li>
|
||||
<li class="palette-row"><span class="palette-label">Feed</span><span class="palette-keys"><span class="key">2</span></span></li>
|
||||
<li class="palette-row"><span class="palette-label">Paper Trail</span><span class="palette-keys"><span class="key">3</span></span></li>
|
||||
<li class="palette-row" data-active><span class="palette-label">Reply later</span><span class="palette-keys"><span class="key">4</span></span></li>
|
||||
<li class="palette-row"><span class="palette-label">Set aside</span><span class="palette-keys"><span class="key">5</span></span></li>
|
||||
<li class="palette-row"><span class="palette-label">Screener</span><span class="palette-keys"><span class="key">6</span></span></li>
|
||||
<li class="palette-row"><span class="palette-label">Snoozed</span><span class="palette-keys"><span class="key">7</span></span></li>
|
||||
<li class="palette-row"><span class="palette-label">Everything</span><span class="palette-keys"><span class="key">0</span></span></li>
|
||||
<li class="palette-row"><span class="palette-label">Sent</span><span class="palette-keys"></span></li>
|
||||
<li class="palette-row"><span class="palette-label">Drafts</span><span class="palette-keys"></span></li>
|
||||
<li class="palette-row"><span class="palette-label">All files</span><span class="palette-keys"></span></li>
|
||||
<li class="palette-row"><span class="palette-label">Clips</span><span class="palette-keys"></span></li>
|
||||
<li class="palette-row"><span class="palette-label">Contacts</span><span class="palette-keys"></span></li>
|
||||
<li class="palette-row"><span class="palette-label">Screened out</span><span class="palette-keys"></span></li>
|
||||
<li class="palette-row"><span class="palette-label">Trash</span><span class="palette-keys"></span></li>
|
||||
<li class="palette-group">Actions</li>
|
||||
<li class="palette-row"><span class="palette-label">Write</span><span class="palette-keys"><span class="key">c</span></span></li>
|
||||
<li class="palette-row"><span class="palette-label">Search</span><span class="palette-keys"><span class="key">/</span></span></li>
|
||||
<li class="palette-row"><span class="palette-label">Toggle reading pane</span><span class="palette-keys"><span class="key">⌘\</span></span></li>
|
||||
<li class="palette-row"><span class="palette-label">Focus & Reply</span><span class="palette-keys"><span class="key">⇧F</span></span></li>
|
||||
<li class="palette-row"><span class="palette-label">Start fresh…</span><span class="palette-keys"></span></li>
|
||||
<li class="palette-row"><span class="palette-label">Dark mode</span><span class="palette-keys"></span></li>
|
||||
<li class="palette-row"><span class="palette-label">Add account</span><span class="palette-keys"></span></li>
|
||||
<li class="palette-row"><span class="palette-label">Settings</span><span class="palette-keys"><span class="key">⌘,</span></span></li>
|
||||
<li class="palette-row"><span class="palette-label">Keyboard shortcuts</span><span class="palette-keys"><span class="key">?</span></span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,201 @@
|
||||
<!doctype html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Margin Mail: Paper Trail</title>
|
||||
<link rel="stylesheet" href="mail.css">
|
||||
</head>
|
||||
<body>
|
||||
<svg width="0" height="0" style="position:absolute">
|
||||
<defs>
|
||||
<symbol id="i-search" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></symbol>
|
||||
<symbol id="i-places" viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h10"/></symbol>
|
||||
<symbol id="i-pen" viewBox="0 0 24 24"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z"/></symbol>
|
||||
<symbol id="i-chev" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></symbol>
|
||||
<symbol id="i-reply" viewBox="0 0 24 24"><path d="M9 17 4 12l5-5"/><path d="M20 18v-2a4 4 0 0 0-4-4H4"/></symbol>
|
||||
<symbol id="i-later" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></symbol>
|
||||
<symbol id="i-aside" viewBox="0 0 24 24"><path d="M12 17v4"/><path d="M8 3h8l-1 7 3 3H6l3-3z"/></symbol>
|
||||
<symbol id="i-snooze" viewBox="0 0 24 24"><path d="M5 3 2 6"/><path d="m22 6-3-3"/><circle cx="12" cy="13" r="8"/><path d="M12 9v4l2 2"/></symbol>
|
||||
<symbol id="i-archive" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="4" rx="1"/><path d="M5 8v11a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V8"/><path d="M10 12h4"/></symbol>
|
||||
<symbol id="i-more" viewBox="0 0 24 24"><circle cx="5" cy="12" r="1.2"/><circle cx="12" cy="12" r="1.2"/><circle cx="19" cy="12" r="1.2"/></symbol>
|
||||
<symbol id="i-shield" viewBox="0 0 24 24"><path d="M12 3 4 6v6c0 5 3.5 8 8 9 4.5-1 8-4 8-9V6z"/><path d="m9 12 2 2 4-4"/></symbol>
|
||||
<symbol id="i-clip" viewBox="0 0 24 24"><path d="m21 11-8.5 8.5a5 5 0 0 1-7-7L14 4a3.3 3.3 0 0 1 4.7 4.7L10.5 17a1.7 1.7 0 0 1-2.4-2.4L16 6.7"/></symbol>
|
||||
<symbol id="i-note" viewBox="0 0 24 24"><path d="M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><path d="M14 3v6h6"/></symbol>
|
||||
<symbol id="i-bolt" viewBox="0 0 24 24"><path d="M13 2 4 14h7l-1 8 9-12h-7z"/></symbol>
|
||||
<symbol id="i-inbox" viewBox="0 0 24 24"><path d="M3 13h5l2 3h4l2-3h5"/><path d="M5 5h14l2 8v6H3v-6z"/></symbol>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
<div class="app">
|
||||
<header class="titlebar">
|
||||
<div class="traffic"><i></i><i></i><i></i></div>
|
||||
<div class="lead">
|
||||
<button class="account-chip">
|
||||
<span class="avatar" data-hue="4">PJ</span>
|
||||
[email protected]
|
||||
<svg class="chev" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#i-chev"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="view-switch">
|
||||
<button class="view-option"><span class="key">1</span>Inbox</button>
|
||||
<button class="view-option"><span class="key">2</span>Feed</button>
|
||||
<button class="view-option" data-active="true"><span class="key">3</span>Paper Trail</button>
|
||||
</div>
|
||||
<div class="trail">
|
||||
<button class="icon-button" title="Search /"><svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-search"/></svg></button>
|
||||
<button class="icon-button" title="Places ⌘K"><svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-places"/></svg></button>
|
||||
<button class="write-button"><svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#i-pen"/></svg>Write<span class="key">c</span></button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="stage">
|
||||
<section class="list-col">
|
||||
<div class="list-head">
|
||||
<h1 class="list-title">Paper Trail</h1>
|
||||
</div>
|
||||
<div class="list">
|
||||
<div class="section">This week</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="row-gutter"></div>
|
||||
<span class="avatar" data-brand>Sp</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">Spotify</span><span class="row-time">06:30</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Your receipt</span><span class="row-snippet">Thank you for purchasing Spotify Premium. Amount charged</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" data-selected>
|
||||
<div class="row-gutter"></div>
|
||||
<span class="avatar" data-brand>CP</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">City Power</span><span class="row-time">Yesterday</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Bill payment pending</span><span class="row-snippet">Your one-time payment of 98.26 has been received and is pending</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="row-gutter"></div>
|
||||
<span class="avatar" data-brand>Bo</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">Bookshop.org</span><span class="row-time">Yesterday</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Your order has shipped</span><span class="row-snippet">Two items are on their way. Track your parcel with the</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="row-gutter"></div>
|
||||
<span class="avatar" data-brand>Gh</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">GitHub</span><span class="row-count">12</span><span class="row-time">Mon</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Notifications, bundled</span><span class="row-snippet">margin-calendar: 3 new issues, 9 comments</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="row-gutter"></div>
|
||||
<span class="avatar" data-brand>Ds</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">DocuSign</span><span class="row-time">Mon</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Completed: Studio lease 2026</span><span class="row-snippet">All parties have completed the envelope. You can access the</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">Earlier</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="row-gutter"></div>
|
||||
<span class="avatar" data-brand>Nf</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">Netflix</span><span class="row-time">28 Aug</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Receipt for your payment</span><span class="row-snippet">We received your payment of 15.49 for your membership. Your next</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="row-gutter"></div>
|
||||
<span class="avatar" data-brand>Ng</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">National Geographic Kids</span><span class="row-time">27 Aug</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Gift order confirmation</span><span class="row-snippet">Thank you for your gift order. The first issue will arrive in</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="row-gutter"></div>
|
||||
<span class="avatar" data-brand>Dt</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">Discount Tire</span><span class="row-time">26 Aug</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Your air pressure reminder</span><span class="row-snippet">It has been 30 days since your last check. Stop by any store for a</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="piles">
|
||||
<button class="pile">
|
||||
<div class="pile-card" data-depth="2"></div>
|
||||
<div class="pile-card" data-depth="1"></div>
|
||||
<div class="pile-card" data-depth="0">
|
||||
<div class="pile-top"><svg width="11" height="11" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#i-later"/></svg>Reply later<span class="key">4</span></div>
|
||||
<div class="pile-subject">Re: Cooper's parent-teacher conference</div>
|
||||
<div class="pile-from">Jeff Wolfe, and 2 more</div>
|
||||
</div>
|
||||
</button>
|
||||
<button class="pile">
|
||||
<div class="pile-card" data-depth="1"></div>
|
||||
<div class="pile-card" data-depth="0">
|
||||
<div class="pile-top"><svg width="11" height="11" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#i-aside"/></svg>Set aside<span class="key">5</span></div>
|
||||
<div class="pile-subject">Les Misérables tickets</div>
|
||||
<div class="pile-from">Caroline Bauhaus, and 1 more</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="pane">
|
||||
<div class="pane-bar">
|
||||
<button class="verb"><svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-reply"/></svg>Reply<span class="key">r</span></button>
|
||||
<button class="verb"><svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-aside"/></svg>Set aside<span class="key">s</span></button>
|
||||
<button class="verb"><svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-snooze"/></svg>Snooze<span class="key">b</span></button>
|
||||
<span class="spacer"></span>
|
||||
<button class="verb"><svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-inbox"/></svg>Move to Inbox<span class="key">v</span></button>
|
||||
<button class="verb"><svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-archive"/></svg>Archive<span class="key">e</span></button>
|
||||
<button class="icon-button" title="More ."><svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-more"/></svg></button>
|
||||
</div>
|
||||
|
||||
<div class="thread">
|
||||
<div class="thread-inner">
|
||||
<div class="thread-head">
|
||||
<h1 class="thread-subject">Bill payment pending</h1>
|
||||
<div class="thread-meta">
|
||||
<span class="avatars"><span class="avatar" data-brand>CP</span></span>
|
||||
<span>City Power</span>
|
||||
<span>·</span>
|
||||
<span>1 message</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="msg">
|
||||
<div class="msg-head">
|
||||
<span class="avatar" data-brand>CP</span>
|
||||
<div class="msg-who"><div class="msg-name">City Power<span class="addr">[email protected]</span></div><div class="msg-to">to you</div></div>
|
||||
<div class="msg-time">Yesterday 16:04</div>
|
||||
</div>
|
||||
<div class="msg-body" data-plain>
|
||||
<p>Dear Customer,</p>
|
||||
<p>Your one-time electronic payment of 98.26 for your City Power bill has been received and is pending. Your payment will be posted within two business days.</p>
|
||||
<p><b>Confirmation number 116400046977232</b></p>
|
||||
<p>Please keep this message for your records. If you did not make this payment, call us on the number printed on your statement.</p>
|
||||
<p>Thank you,<br>City Power Customer Care</p>
|
||||
<div class="attachments">
|
||||
<div class="attachment"><span class="ext">PDF</span>Statement-August.pdf<span class="size">88 KB</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,140 @@
|
||||
<!doctype html>
|
||||
<html lang="en" data-theme="light" data-phone data-touch>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Margin Mail: Inbox (iPhone)</title>
|
||||
<link rel="stylesheet" href="mail.css">
|
||||
</head>
|
||||
<body>
|
||||
<svg width="0" height="0" style="position:absolute">
|
||||
<defs>
|
||||
<symbol id="i-search" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></symbol>
|
||||
<symbol id="i-places" viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h10"/></symbol>
|
||||
<symbol id="i-pen" viewBox="0 0 24 24"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z"/></symbol>
|
||||
<symbol id="i-later" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></symbol>
|
||||
<symbol id="i-aside" viewBox="0 0 24 24"><path d="M12 17v4"/><path d="M8 3h8l-1 7 3 3H6l3-3z"/></symbol>
|
||||
<symbol id="i-shield" viewBox="0 0 24 24"><path d="M12 3 4 6v6c0 5 3.5 8 8 9 4.5-1 8-4 8-9V6z"/><path d="m9 12 2 2 4-4"/></symbol>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
<div class="app">
|
||||
<header class="phonebar">
|
||||
<button class="icon-button" title="Search"><svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-search"/></svg></button>
|
||||
<div class="phonebar-title">Inbox</div>
|
||||
<button class="icon-button" title="Places"><svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-places"/></svg></button>
|
||||
</header>
|
||||
|
||||
<div class="list">
|
||||
<button class="pill screen-strip"><svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#i-shield"/></svg>Screen 3 new senders</button>
|
||||
|
||||
<div class="section">New for you</div>
|
||||
|
||||
<div class="row" data-new>
|
||||
<div class="row-gutter"><span class="row-dot"></span></div>
|
||||
<span class="avatar" data-hue="2">MR</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">Maya Raghunathan</span><span class="row-count">3</span><span class="row-time">11:42</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Dinner on Thursday?</span><span class="row-snippet">Priya said the place on Church Street takes bookings now</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" data-new>
|
||||
<div class="row-gutter"><span class="row-dot"></span></div>
|
||||
<span class="avatar" data-hue="6">AK</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">Arun Kulkarni</span><span class="row-time">10:15</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Re: Lease renewal for the studio</span><span class="row-snippet">Attached the revised draft. The only change is clause 7</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" data-new>
|
||||
<div class="row-gutter"><span class="row-dot"></span></div>
|
||||
<span class="avatar" data-brand>Ai</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">Airbnb</span><span class="row-time">09:03</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Your reservation in Lisbon is confirmed</span><span class="row-snippet">Check-in Friday 12 September after 15:00</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" data-new>
|
||||
<div class="row-gutter"><span class="row-dot"></span></div>
|
||||
<span class="avatar" data-hue="7">SO</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">Sam Okafor</span><span class="row-time">Yesterday</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Piano lessons, the form you sent</span><span class="row-snippet">Got it, thank you. Wednesdays at five work for us</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="seen">
|
||||
<div class="section">Previously seen</div>
|
||||
|
||||
<div class="row" data-noted>
|
||||
<div class="row-gutter"></div>
|
||||
<span class="avatar" data-hue="5">LB</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">Lena Brandt</span><span class="row-count">7</span><span class="row-time">Yesterday</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Kitchen bench quote</span><span class="row-snippet">Sounds good, Julie. Any afternoon next week works</span></div>
|
||||
</div>
|
||||
<div class="row-note">Ask about the oak finish before confirming</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="row-gutter"></div>
|
||||
<span class="avatar" data-hue="3">DP</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">Dev Patel</span><span class="row-time">Mon</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Slides from the talk</span><span class="row-snippet">Here they are, plus the reading list I mentioned</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="row-gutter"></div>
|
||||
<span class="avatar" data-brand>Ds</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">DocuSign</span><span class="row-time">Mon</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Completed: Studio lease 2026</span><span class="row-snippet">All parties have completed the envelope</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="row-gutter"></div>
|
||||
<span class="avatar" data-hue="8">HW</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">Hannah Weiss</span><span class="row-count">2</span><span class="row-time">Sun</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Photos from the Hawaii trip</span><span class="row-snippet">Finally went through them all. The ones from the ridge</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="piles">
|
||||
<button class="pile">
|
||||
<div class="pile-card" data-depth="2"></div>
|
||||
<div class="pile-card" data-depth="1"></div>
|
||||
<div class="pile-card" data-depth="0">
|
||||
<div class="pile-top"><svg width="11" height="11" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#i-later"/></svg>Reply later</div>
|
||||
<div class="pile-subject">Re: Cooper's parent-teacher conference</div>
|
||||
<div class="pile-from">Jeff Wolfe, and 2 more</div>
|
||||
</div>
|
||||
</button>
|
||||
<button class="pile">
|
||||
<div class="pile-card" data-depth="1"></div>
|
||||
<div class="pile-card" data-depth="0">
|
||||
<div class="pile-top"><svg width="11" height="11" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#i-aside"/></svg>Set aside</div>
|
||||
<div class="pile-subject">Les Misérables tickets</div>
|
||||
<div class="pile-from">Caroline Bauhaus, and 1 more</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav class="tabbar">
|
||||
<div class="tabbar-views">
|
||||
<button class="tabbar-view" data-active="true">Inbox</button>
|
||||
<button class="tabbar-view">Feed</button>
|
||||
<button class="tabbar-view">Paper Trail</button>
|
||||
</div>
|
||||
<button class="tabbar-new" title="Write"><svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#i-pen"/></svg></button>
|
||||
</nav>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,83 @@
|
||||
<!doctype html>
|
||||
<html lang="en" data-theme="light" data-phone data-touch>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Margin Mail: Thread (iPhone)</title>
|
||||
<link rel="stylesheet" href="mail.css">
|
||||
</head>
|
||||
<body>
|
||||
<svg width="0" height="0" style="position:absolute">
|
||||
<defs>
|
||||
<symbol id="i-back" viewBox="0 0 24 24"><path d="m15 18-6-6 6-6"/></symbol>
|
||||
<symbol id="i-reply" viewBox="0 0 24 24"><path d="M9 17 4 12l5-5"/><path d="M20 18v-2a4 4 0 0 0-4-4H4"/></symbol>
|
||||
<symbol id="i-later" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></symbol>
|
||||
<symbol id="i-aside" viewBox="0 0 24 24"><path d="M12 17v4"/><path d="M8 3h8l-1 7 3 3H6l3-3z"/></symbol>
|
||||
<symbol id="i-snooze" viewBox="0 0 24 24"><path d="M5 3 2 6"/><path d="m22 6-3-3"/><circle cx="12" cy="13" r="8"/><path d="M12 9v4l2 2"/></symbol>
|
||||
<symbol id="i-more" viewBox="0 0 24 24"><circle cx="5" cy="12" r="1.2"/><circle cx="12" cy="12" r="1.2"/><circle cx="19" cy="12" r="1.2"/></symbol>
|
||||
<symbol id="i-shield" viewBox="0 0 24 24"><path d="M12 3 4 6v6c0 5 3.5 8 8 9 4.5-1 8-4 8-9V6z"/><path d="m9 12 2 2 4-4"/></symbol>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
<div class="app">
|
||||
<header class="phonebar">
|
||||
<button class="icon-button" title="Back"><svg width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#i-back"/></svg></button>
|
||||
<div class="phonebar-title">Inbox</div>
|
||||
<button class="icon-button" title="More"><svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-more"/></svg></button>
|
||||
</header>
|
||||
|
||||
<div class="thread">
|
||||
<div class="thread-inner">
|
||||
<div class="thread-head">
|
||||
<h1 class="thread-subject">Lease renewal for the studio</h1>
|
||||
<div class="thread-meta">
|
||||
<span class="avatars"><span class="avatar" data-hue="6">AK</span><span class="avatar" data-hue="4">PJ</span></span>
|
||||
<span>Arun Kulkarni and you</span>
|
||||
<span>·</span>
|
||||
<span>2 messages</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="banner">
|
||||
<svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-shield"/></svg>
|
||||
<span>Blocked <b>1 tracker</b> from HubSpot. Remote images are off for this sender.</span>
|
||||
<button class="banner-action">Show images</button>
|
||||
</div>
|
||||
|
||||
<div class="msg" data-collapsed>
|
||||
<div class="msg-head">
|
||||
<span class="avatar" data-hue="4">PJ</span>
|
||||
<div class="msg-who"><div class="msg-name">You</div><div class="msg-preview">Hi Arun, thanks for sending the renewal over. Two things before I sign: the notice period in clause 7 and</div></div>
|
||||
<div class="msg-time">Mon 14:20</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="msg">
|
||||
<div class="msg-head">
|
||||
<span class="avatar" data-hue="6">AK</span>
|
||||
<div class="msg-who"><div class="msg-name">Arun Kulkarni<span class="addr">[email protected]</span></div><div class="msg-to">to you</div></div>
|
||||
<div class="msg-time">Today 10:15</div>
|
||||
</div>
|
||||
<div class="msg-body">
|
||||
<p>Hi Priyanshu,</p>
|
||||
<p>Attached the revised draft. The only change is clause 7, which now reads three months either side rather than six. The rent review in clause 12 stays as it was, tied to the index and capped at four percent.</p>
|
||||
<p>If that works, sign when you get a moment and I will countersign the same day. Happy to walk through anything on a call, Thursday afternoon is open.</p>
|
||||
<p>Best,<br>Arun</p>
|
||||
<span class="quoted">··· Show quoted text</span>
|
||||
<div class="attachments">
|
||||
<div class="attachment"><span class="ext">PDF</span>Studio-lease-2026-v2.pdf<span class="size">412 KB</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="phone-actions">
|
||||
<button class="phone-action" data-primary><svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-reply"/></svg>Reply</button>
|
||||
<button class="phone-action"><svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-later"/></svg>Reply later</button>
|
||||
<button class="phone-action"><svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-aside"/></svg>Set aside</button>
|
||||
<button class="phone-action"><svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-snooze"/></svg>Snooze</button>
|
||||
<button class="phone-action"><svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-more"/></svg>More</button>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,115 @@
|
||||
<!doctype html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Margin Mail: Screener</title>
|
||||
<link rel="stylesheet" href="mail.css">
|
||||
</head>
|
||||
<body>
|
||||
<svg width="0" height="0" style="position:absolute">
|
||||
<defs>
|
||||
<symbol id="i-search" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></symbol>
|
||||
<symbol id="i-places" viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h10"/></symbol>
|
||||
<symbol id="i-pen" viewBox="0 0 24 24"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z"/></symbol>
|
||||
<symbol id="i-chev" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></symbol>
|
||||
<symbol id="i-reply" viewBox="0 0 24 24"><path d="M9 17 4 12l5-5"/><path d="M20 18v-2a4 4 0 0 0-4-4H4"/></symbol>
|
||||
<symbol id="i-later" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></symbol>
|
||||
<symbol id="i-aside" viewBox="0 0 24 24"><path d="M12 17v4"/><path d="M8 3h8l-1 7 3 3H6l3-3z"/></symbol>
|
||||
<symbol id="i-snooze" viewBox="0 0 24 24"><path d="M5 3 2 6"/><path d="m22 6-3-3"/><circle cx="12" cy="13" r="8"/><path d="M12 9v4l2 2"/></symbol>
|
||||
<symbol id="i-archive" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="4" rx="1"/><path d="M5 8v11a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V8"/><path d="M10 12h4"/></symbol>
|
||||
<symbol id="i-more" viewBox="0 0 24 24"><circle cx="5" cy="12" r="1.2"/><circle cx="12" cy="12" r="1.2"/><circle cx="19" cy="12" r="1.2"/></symbol>
|
||||
<symbol id="i-shield" viewBox="0 0 24 24"><path d="M12 3 4 6v6c0 5 3.5 8 8 9 4.5-1 8-4 8-9V6z"/><path d="m9 12 2 2 4-4"/></symbol>
|
||||
<symbol id="i-clip" viewBox="0 0 24 24"><path d="m21 11-8.5 8.5a5 5 0 0 1-7-7L14 4a3.3 3.3 0 0 1 4.7 4.7L10.5 17a1.7 1.7 0 0 1-2.4-2.4L16 6.7"/></symbol>
|
||||
<symbol id="i-note" viewBox="0 0 24 24"><path d="M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><path d="M14 3v6h6"/></symbol>
|
||||
<symbol id="i-bolt" viewBox="0 0 24 24"><path d="M13 2 4 14h7l-1 8 9-12h-7z"/></symbol>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
<div class="app">
|
||||
<header class="titlebar">
|
||||
<div class="traffic"><i></i><i></i><i></i></div>
|
||||
<div class="lead">
|
||||
<button class="account-chip">
|
||||
<span class="avatar" data-hue="4">PJ</span>
|
||||
[email protected]
|
||||
<svg class="chev" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#i-chev"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="view-switch">
|
||||
<button class="view-option"><span class="key">1</span>Inbox</button>
|
||||
<button class="view-option"><span class="key">2</span>Feed</button>
|
||||
<button class="view-option"><span class="key">3</span>Paper Trail</button>
|
||||
</div>
|
||||
<div class="trail">
|
||||
<button class="icon-button" title="Search /"><svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-search"/></svg></button>
|
||||
<button class="icon-button" title="Places ⌘K"><svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-places"/></svg></button>
|
||||
<button class="write-button"><svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#i-pen"/></svg>Write<span class="key">c</span></button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="stage">
|
||||
<section class="list-col" data-wide>
|
||||
<div class="list">
|
||||
<div class="screen-intro">
|
||||
<h1>Screener</h1>
|
||||
<p>Three people wrote to you for the first time. Say where their mail goes, or that it goes nowhere. Nobody is told.</p>
|
||||
</div>
|
||||
|
||||
<div class="screen-list">
|
||||
<div class="screen-tools">
|
||||
<button class="button" data-variant="ghost">Clear all</button>
|
||||
</div>
|
||||
|
||||
<div class="screen-card" data-selected>
|
||||
<span class="avatar" data-hue="3">TM</span>
|
||||
<div class="screen-who">
|
||||
<div class="screen-name">Todd Markham<span class="addr">[email protected]</span></div>
|
||||
<div class="screen-subject">Re: Life insurance quote</div>
|
||||
<div class="screen-snippet">Hello Ms. Young, I hope all is well with you. I wanted to touch base on our conversation from a few weeks ago.</div>
|
||||
<div class="screen-why">Written by a person · suggested Inbox</div>
|
||||
</div>
|
||||
<div class="screen-actions">
|
||||
<button class="button" data-variant="primary">Yes, to Inbox<span class="key">y</span></button>
|
||||
<button class="button">Elsewhere<span class="key">v</span></button>
|
||||
<button class="button" data-variant="ghost">No<span class="key">n</span></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="screen-card">
|
||||
<span class="avatar" data-brand>Tb</span>
|
||||
<div class="screen-who">
|
||||
<div class="screen-name">The Browser<span class="addr">[email protected]</span></div>
|
||||
<div class="screen-subject">Five things worth reading this weekend</div>
|
||||
<div class="screen-snippet">A long piece on the history of the pencil, a short one on why bridges hum, and three more.</div>
|
||||
<div class="screen-why">Has an unsubscribe header · suggested Feed</div>
|
||||
</div>
|
||||
<div class="screen-actions">
|
||||
<button class="button" data-variant="primary">Yes, to Feed<span class="key">y</span></button>
|
||||
<button class="button">Elsewhere<span class="key">v</span></button>
|
||||
<button class="button" data-variant="ghost">No<span class="key">n</span></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="screen-card">
|
||||
<span class="avatar" data-hue="6">RM</span>
|
||||
<div class="screen-who">
|
||||
<div class="screen-name">Evite on behalf of Robyn Madison<span class="addr">[email protected]</span></div>
|
||||
<div class="screen-subject">You have an invitation from Robyn Madison</div>
|
||||
<div class="screen-snippet">Join us for Jack's 8th birthday! Robyn Madison needs your RSVP.</div>
|
||||
<div class="screen-why">Sent by a service for a person · suggested Paper Trail</div>
|
||||
</div>
|
||||
<div class="screen-actions">
|
||||
<button class="button" data-variant="primary">Yes, to Paper Trail<span class="key">y</span></button>
|
||||
<button class="button">Elsewhere<span class="key">v</span></button>
|
||||
<button class="button" data-variant="ghost">No<span class="key">n</span></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="screen-note">Screened-out mail is kept for 90 days under Screened out, then deleted. Change your mind from a sender's contact card at any time.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,282 @@
|
||||
<!doctype html>
|
||||
<html lang="en" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Margin Mail: Thread</title>
|
||||
<link rel="stylesheet" href="mail.css">
|
||||
</head>
|
||||
<body>
|
||||
<svg width="0" height="0" style="position:absolute">
|
||||
<defs>
|
||||
<symbol id="i-search" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></symbol>
|
||||
<symbol id="i-places" viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h10"/></symbol>
|
||||
<symbol id="i-pen" viewBox="0 0 24 24"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z"/></symbol>
|
||||
<symbol id="i-chev" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></symbol>
|
||||
<symbol id="i-reply" viewBox="0 0 24 24"><path d="M9 17 4 12l5-5"/><path d="M20 18v-2a4 4 0 0 0-4-4H4"/></symbol>
|
||||
<symbol id="i-later" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></symbol>
|
||||
<symbol id="i-aside" viewBox="0 0 24 24"><path d="M12 17v4"/><path d="M8 3h8l-1 7 3 3H6l3-3z"/></symbol>
|
||||
<symbol id="i-snooze" viewBox="0 0 24 24"><path d="M5 3 2 6"/><path d="m22 6-3-3"/><circle cx="12" cy="13" r="8"/><path d="M12 9v4l2 2"/></symbol>
|
||||
<symbol id="i-archive" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="4" rx="1"/><path d="M5 8v11a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V8"/><path d="M10 12h4"/></symbol>
|
||||
<symbol id="i-more" viewBox="0 0 24 24"><circle cx="5" cy="12" r="1.2"/><circle cx="12" cy="12" r="1.2"/><circle cx="19" cy="12" r="1.2"/></symbol>
|
||||
<symbol id="i-shield" viewBox="0 0 24 24"><path d="M12 3 4 6v6c0 5 3.5 8 8 9 4.5-1 8-4 8-9V6z"/><path d="m9 12 2 2 4-4"/></symbol>
|
||||
<symbol id="i-clip" viewBox="0 0 24 24"><path d="m21 11-8.5 8.5a5 5 0 0 1-7-7L14 4a3.3 3.3 0 0 1 4.7 4.7L10.5 17a1.7 1.7 0 0 1-2.4-2.4L16 6.7"/></symbol>
|
||||
<symbol id="i-note" viewBox="0 0 24 24"><path d="M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><path d="M14 3v6h6"/></symbol>
|
||||
<symbol id="i-bolt" viewBox="0 0 24 24"><path d="M13 2 4 14h7l-1 8 9-12h-7z"/></symbol>
|
||||
<symbol id="i-bell" viewBox="0 0 24 24"><path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.7 21a2 2 0 0 1-3.4 0"/></symbol>
|
||||
<symbol id="i-merge" viewBox="0 0 24 24"><circle cx="18" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><path d="M6 21V9a9 9 0 0 0 9 9"/></symbol>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
<div class="app">
|
||||
<header class="titlebar">
|
||||
<div class="traffic"><i></i><i></i><i></i></div>
|
||||
<div class="lead">
|
||||
<button class="account-chip">
|
||||
<span class="avatar" data-hue="4">PJ</span>
|
||||
[email protected]
|
||||
<svg class="chev" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#i-chev"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="view-switch">
|
||||
<button class="view-option" data-active="true"><span class="key">1</span>Inbox</button>
|
||||
<button class="view-option"><span class="key">2</span>Feed</button>
|
||||
<button class="view-option"><span class="key">3</span>Paper Trail</button>
|
||||
</div>
|
||||
<div class="trail">
|
||||
<button class="icon-button" title="Search /"><svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-search"/></svg></button>
|
||||
<button class="icon-button" title="Places ⌘K"><svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-places"/></svg></button>
|
||||
<button class="write-button"><svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#i-pen"/></svg>Write<span class="key">c</span></button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="stage">
|
||||
<section class="list-col">
|
||||
<div class="list-head">
|
||||
<h1 class="list-title">Inbox</h1>
|
||||
<button class="pill"><svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#i-shield"/></svg>Screen 3 new senders<span class="key">6</span></button>
|
||||
</div>
|
||||
<div class="list">
|
||||
<div class="section">New for you</div>
|
||||
|
||||
<div class="row" data-new>
|
||||
<div class="row-gutter"><span class="row-dot"></span></div>
|
||||
<span class="avatar" data-hue="2">MR</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">Maya Raghunathan</span><span class="row-count">3</span><span class="row-time">11:42</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Dinner on Thursday?</span><span class="row-snippet">Priya said the place on Church Street takes bookings now, want me to</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" data-new>
|
||||
<div class="row-gutter"><span class="row-dot"></span></div>
|
||||
<span class="avatar" data-hue="6">AK</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">Arun Kulkarni</span><span class="row-time">10:15</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Re: Lease renewal for the studio</span><span class="row-snippet">Attached the revised draft. The only change is clause 7, which now</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" data-new>
|
||||
<div class="row-gutter"><span class="row-dot"></span></div>
|
||||
<span class="avatar" data-brand>Ai</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">Airbnb</span><span class="row-time">09:03</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Your reservation in Lisbon is confirmed</span><span class="row-snippet">Check-in Friday 12 September after 15:00. Your host Inês will send</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" data-new data-selected>
|
||||
<div class="row-gutter"><span class="row-dot"></span></div>
|
||||
<span class="avatar" data-hue="7">SO</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">Sam Okafor</span><span class="row-count">4</span><span class="row-time">Yesterday</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Piano on Wednesdays</span><span class="row-snippet">Got it, thank you. Wednesdays at five work for us. Is there a</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="seen">
|
||||
<div class="section">Previously seen</div>
|
||||
|
||||
<div class="row" data-noted>
|
||||
<div class="row-gutter"></div>
|
||||
<span class="avatar" data-hue="5">LB</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">Lena Brandt</span><span class="row-count">7</span><span class="row-time">Yesterday</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Kitchen bench quote</span><span class="row-snippet">Sounds good, Julie. Any afternoon next week works for me.</span></div>
|
||||
</div>
|
||||
<div class="row-note">Ask about the oak finish before confirming</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="row-gutter"></div>
|
||||
<span class="avatar" data-hue="3">DP</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">Dev Patel</span><span class="row-time">Mon</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Slides from the talk</span><span class="row-snippet">Here they are, plus the reading list I mentioned. The paper on</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="row-gutter"></div>
|
||||
<span class="avatar" data-brand>Ds</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">DocuSign</span><span class="row-time">Mon</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Completed: Studio lease 2026</span><span class="row-snippet">All parties have completed the envelope. You can access the</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="row-gutter"></div>
|
||||
<span class="avatar" data-hue="8">HW</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">Hannah Weiss</span><span class="row-count">2</span><span class="row-time">Sun</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Photos from the Hawaii trip</span><span class="row-snippet">Finally went through them all. The ones from the ridge walk are</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="row-gutter"></div>
|
||||
<span class="avatar" data-hue="1">RY</span>
|
||||
<div class="row-main">
|
||||
<div class="row-top"><span class="row-sender">Russell Young</span><span class="row-time">30 Aug</span></div>
|
||||
<div class="row-bottom"><span class="row-subject">Pumpkin bread recipe</span><span class="row-snippet">From my mother's card, transcribed as best I could. Bake at 175</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="piles">
|
||||
<button class="pile">
|
||||
<div class="pile-card" data-depth="2"></div>
|
||||
<div class="pile-card" data-depth="1"></div>
|
||||
<div class="pile-card" data-depth="0">
|
||||
<div class="pile-top"><svg width="11" height="11" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#i-later"/></svg>Reply later<span class="key">4</span></div>
|
||||
<div class="pile-subject">Re: Cooper's parent-teacher conference</div>
|
||||
<div class="pile-from">Jeff Wolfe, and 2 more</div>
|
||||
</div>
|
||||
</button>
|
||||
<button class="pile">
|
||||
<div class="pile-card" data-depth="1"></div>
|
||||
<div class="pile-card" data-depth="0">
|
||||
<div class="pile-top"><svg width="11" height="11" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#i-aside"/></svg>Set aside<span class="key">5</span></div>
|
||||
<div class="pile-subject">Les Misérables tickets</div>
|
||||
<div class="pile-from">Caroline Bauhaus, and 1 more</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="pane" style="position: relative">
|
||||
<div class="pane-bar">
|
||||
<button class="verb"><svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-reply"/></svg>Reply<span class="key">r</span></button>
|
||||
<button class="verb"><svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-later"/></svg>Reply later<span class="key">l</span></button>
|
||||
<button class="verb"><svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-aside"/></svg>Set aside<span class="key">s</span></button>
|
||||
<button class="verb"><svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-snooze"/></svg>Snooze<span class="key">b</span></button>
|
||||
<span class="spacer"></span>
|
||||
<button class="verb"><svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-bell"/></svg>Notify<span class="key">⇧N</span></button>
|
||||
<button class="verb"><svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-archive"/></svg>Archive<span class="key">e</span></button>
|
||||
<button class="icon-button" title="More ."><svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-more"/></svg></button>
|
||||
</div>
|
||||
|
||||
<div class="thread">
|
||||
<div class="thread-inner">
|
||||
<div class="thread-head">
|
||||
<h1 class="thread-subject">Piano on Wednesdays<span class="renamed">renamed · was "Re: Fw: (no subject)"</span></h1>
|
||||
<div class="thread-meta">
|
||||
<span class="avatars"><span class="avatar" data-hue="7">SO</span><span class="avatar" data-brand>Sd</span><span class="avatar" data-hue="4">PJ</span></span>
|
||||
<span>Sam Okafor, Sunny Day Music and you</span>
|
||||
<span>·</span>
|
||||
<span>3 messages</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="banner" data-tone="muted">
|
||||
<svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-merge"/></svg>
|
||||
<span>Merged from <b>2 threads</b> · Sam Okafor, Sunny Day Music</span>
|
||||
<button class="banner-action">Unmerge</button>
|
||||
</div>
|
||||
|
||||
<div class="msg" data-collapsed>
|
||||
<div class="msg-head">
|
||||
<span class="avatar" data-brand>Sd</span>
|
||||
<div class="msg-who"><div class="msg-name">Sunny Day Music</div><div class="msg-preview">Enrolment received for Cooper. Term starts the week of 8 September and your teacher will be in touch to fix a</div></div>
|
||||
<div class="msg-time">12 Aug</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="msg" data-collapsed>
|
||||
<div class="msg-head">
|
||||
<span class="avatar" data-hue="4">PJ</span>
|
||||
<div class="msg-who"><div class="msg-name">You</div><div class="msg-preview">Hi Sam, form signed and attached. Would a weekday after school suit Cooper? He finishes at four and</div></div>
|
||||
<div class="msg-time">Mon 09:12</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="msg">
|
||||
<div class="msg-head">
|
||||
<span class="avatar" data-hue="7">SO</span>
|
||||
<div class="msg-who"><div class="msg-name">Sam Okafor<span class="addr">[email protected]</span></div><div class="msg-to">to you</div></div>
|
||||
<div class="msg-time">Yesterday 15:40</div>
|
||||
</div>
|
||||
<div class="msg-body">
|
||||
<p>Got it, thank you. Wednesdays at five work for us.</p>
|
||||
<p>The first lesson is on the 10th, nothing to prepare.</p>
|
||||
<p>The invitation is attached, it should land in your calendar.<br>Sam</p>
|
||||
</div>
|
||||
|
||||
<div class="invite">
|
||||
<div class="invite-date"><span class="mon">Sep</span><span class="day">10</span></div>
|
||||
<div>
|
||||
<div class="invite-title">Piano lesson · Cooper</div>
|
||||
<div class="invite-when">Wednesday 10 September · 17:00 to 17:45 · Sunny Day Music, Bandra</div>
|
||||
<div class="invite-actions">
|
||||
<button class="button" data-variant="primary">Accept<span class="key">y</span></button>
|
||||
<button class="button">Maybe<span class="key">m</span></button>
|
||||
<button class="button">Decline<span class="key">n</span></button>
|
||||
<span class="open">Open in Margin Calendar</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="reply">
|
||||
<div class="reply-head">Reply to <b>Sam Okafor</b><span style="margin-left:auto" class="key">a</span><span style="color:var(--ink-faint)">reply all</span></div>
|
||||
<div class="reply-body"><span class="placeholder">Write a reply</span></div>
|
||||
<div class="reply-foot">
|
||||
<button class="button" data-variant="primary">Send<span class="key">⌘↩</span></button>
|
||||
<button class="button" data-variant="ghost">Remind me if no reply</button>
|
||||
<span class="spacer"></span>
|
||||
<button class="icon-button" title="Attach"><svg width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-clip"/></svg></button>
|
||||
<button class="icon-button" title="Note"><svg width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><use href="#i-note"/></svg></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="popover" style="left: 680px; top: 352px">
|
||||
<div class="contact-head">
|
||||
<span class="avatar" data-hue="7">SO</span>
|
||||
<div>
|
||||
<div class="contact-name">Sam Okafor</div>
|
||||
<div class="contact-addr">[email protected]</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="contact-rows">
|
||||
<div class="contact-row"><span class="lab">Delivers to</span><span class="val"><span class="pill">Inbox<svg width="10" height="10" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#i-chev"/></svg></span></span></div>
|
||||
<div class="contact-row"><span class="lab">Notify</span><span class="val">Off · this thread only</span></div>
|
||||
<div class="contact-row"><span class="lab">Screened</span><span class="val">In, on 12 Aug</span></div>
|
||||
<div class="contact-row" data-wrap><span class="lab">Note</span><span class="val">Cooper's teacher. Prefers email over calls.</span></div>
|
||||
</div>
|
||||
<div class="contact-section">Recent threads</div>
|
||||
<div class="contact-thread"><span>Piano on Wednesdays</span><span>Today</span></div>
|
||||
<div class="contact-thread"><span>Recital in November</span><span>20 Aug</span></div>
|
||||
<div class="contact-thread"><span>Enrolment form</span><span>12 Aug</span></div>
|
||||
<div class="contact-section">Files</div>
|
||||
<div class="contact-thread"><span>Enrolment-form.pdf</span><span>12 Aug</span></div>
|
||||
<div class="contact-thread"><span>Term-dates.pdf</span><span>12 Aug</span></div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
After Width: | Height: | Size: 528 KiB |
@@ -0,0 +1,749 @@
|
||||
# Gmail API capability map
|
||||
|
||||
What the Gmail REST API (v1) and its neighbours can and cannot do for a native desktop
|
||||
client (Tauri 2, Rust backend, React frontend) that wants HEY and Superhuman style
|
||||
workflows on top of a user's Gmail account. Everything below was checked against the
|
||||
live Google docs on 2026-09-03 unless marked "unverified" or "third-party".
|
||||
|
||||
One headline before the detail: the Gmail API usage limits changed on 1 May 2026. The
|
||||
per-user rate limit is now 6,000 quota units per minute (100 per second, not the 250
|
||||
per second most older write-ups quote), `messages.get` costs 20 units (not 5) and
|
||||
`threads.get` costs 40 (not 10). Every quota figure in this document uses the new
|
||||
table. Plan sync around it.
|
||||
|
||||
## 1. Auth and distribution
|
||||
|
||||
### Scopes
|
||||
|
||||
| Scope | Unlocks | Class |
|
||||
|---|---|---|
|
||||
| `https://mail.google.com/` | Everything, including permanent delete (`messages.delete`, `batchDelete`, `threads.delete`) and IMAP/SMTP via XOAUTH2. The only scope IMAP accepts. | Restricted |
|
||||
| `https://www.googleapis.com/auth/gmail.modify` | Read, list, modify labels, trash/untrash, send, drafts, insert/import, history, watch. Not permanent delete. | Restricted |
|
||||
| `https://www.googleapis.com/auth/gmail.readonly` | Read and list only, including history and watch. | Restricted |
|
||||
| `https://www.googleapis.com/auth/gmail.compose` | Drafts create/update/send and `messages.send`. No reading. | Restricted |
|
||||
| `https://www.googleapis.com/auth/gmail.send` | `messages.send` only. | Sensitive |
|
||||
| `https://www.googleapis.com/auth/gmail.insert` | `messages.insert` and `messages.import`. | Restricted |
|
||||
| `https://www.googleapis.com/auth/gmail.labels` | `labels.*` only. | Non-sensitive |
|
||||
| `https://www.googleapis.com/auth/gmail.metadata` | List and read headers and labels, never bodies. `format=full` and `format=raw` are refused, and `messages.list` refuses the `q` parameter under this scope. | Restricted |
|
||||
| `https://www.googleapis.com/auth/gmail.settings.basic` | Filters, vacation responder, IMAP/POP/language settings, update of the primary send-as (signature, display name). | Restricted |
|
||||
| `https://www.googleapis.com/auth/gmail.settings.sharing` | Send-as alias create/delete/verify, forwarding addresses, auto-forwarding, delegates. Every one of these methods is documented as "only available to service account clients that have been delegated domain-wide authority", so a consumer desktop client cannot use them at all. | Restricted |
|
||||
| `https://www.googleapis.com/auth/contacts.other.readonly` | People API `otherContacts.list` and `otherContacts.search` (the auto-collected "people you emailed" set that powers Gmail autocomplete). | Sensitive (third-party classification, unverified against Google's master list) |
|
||||
| `https://www.googleapis.com/auth/contacts.readonly` | People API `people.connections.list`, `people.searchContacts`. | Sensitive (unverified, as above) |
|
||||
| `https://www.googleapis.com/auth/calendar.events` | Calendar `events.list`, `events.patch`, `events.import` for RSVP. | Sensitive (unverified) |
|
||||
| `https://www.googleapis.com/auth/pubsub` | Only needed if the client itself pulls from a Pub/Sub subscription. See section 3. | Cloud scope, not a Gmail scope |
|
||||
|
||||
Minimum set for the product as described: `gmail.modify` (which covers `gmail.labels`),
|
||||
`gmail.settings.basic`, `contacts.other.readonly`, `contacts.readonly`, and
|
||||
`calendar.events` if you do RSVP. Add `https://mail.google.com/` only if you use IMAP
|
||||
or want permanent delete. Any Gmail scope beyond `gmail.send` and `gmail.labels` is
|
||||
restricted, so there is no scope choice that avoids restricted-scope verification for a
|
||||
real mail client.
|
||||
|
||||
### What verification means in practice
|
||||
|
||||
Publishing status and user caps, quoted from Google's "Manage App Audience" page:
|
||||
|
||||
- **Testing**: "up to 100 test users listed in the OAuth consent screen". Every test
|
||||
user must be added by email address by the project owner. "Authorizations by a test
|
||||
user will expire seven days from the time of consent", and that includes the refresh
|
||||
token. The only exception is apps that request nothing beyond name, email and profile.
|
||||
A mail client in Testing therefore forces a full re-login every 7 days. Do not ship
|
||||
in Testing.
|
||||
- **In production, unverified**: any Google account can be presented with the
|
||||
"unverified app" interstitial and click through. The project gets "100 new users in
|
||||
total" for its lifetime; the cap "cannot be reset". Refresh tokens do not have the
|
||||
7-day expiry in this state.
|
||||
- **In production, verified**: no cap, no interstitial (after brand verification the
|
||||
app name and logo show on the consent screen).
|
||||
|
||||
Verification tiers and timelines:
|
||||
|
||||
- Brand verification (name and logo only): "typically takes 2-3 business days".
|
||||
- Sensitive scope verification: "typically takes 3-5 business days". Requires a
|
||||
domain verified in Search Console, a public homepage on that domain, a privacy
|
||||
policy hosted on the same domain and linked from the consent screen, an unlisted
|
||||
YouTube demo showing the consent flow with the client ID visible in the address bar
|
||||
and each scope in use, and a written justification per scope.
|
||||
- Restricted scope verification: all of the above plus Limited Use compliance, and the
|
||||
process "can potentially take several weeks". Re-verification is annual.
|
||||
- Security assessment (CASA, run by the App Defense Alliance): Google's wording is
|
||||
"Every app that requests access to Google users' restricted data and has the ability
|
||||
to access data from or through a third-party server must go through a security
|
||||
assessment from Google-empanelled security assessors." Assessments are assigned an
|
||||
assurance level (AL1 or AL2) and "All applications must be revalidated every year."
|
||||
Third-party pricing for the common Tier 2 / lab-verified scan is roughly USD 540 to
|
||||
1,800 per year (TAC Security via switchlabs.dev, 2025 to 2026); older figures of USD
|
||||
15,000 to 75,000 (Nylas, 2021) refer to the original pentest regime and are stale.
|
||||
|
||||
The open question that decides Margin's cost: a desktop client whose only server is
|
||||
Google, with all mail data on the user's disk, does not obviously "access data from or
|
||||
through a third-party server". Google publishes no explicit "local-only apps are exempt"
|
||||
clause, so treat this as unverified and ask the verification team in writing before
|
||||
you build a push relay (a relay that sees only the user's email address and a
|
||||
`historyId`, as Mimestream's does, may or may not count). Mimestream states it
|
||||
"applied for and completed Google's Restricted scope verification process" and that
|
||||
"Mimestream stores user email data locally on the user's device", but does not say
|
||||
whether it was assessed.
|
||||
|
||||
Exemptions Google lists for verification itself: "personal use (fewer than 100
|
||||
users)", apps in development/testing/staging, service-account-only apps, and internal
|
||||
Workspace apps. None of these is a distribution strategy.
|
||||
|
||||
### Bring your own OAuth client
|
||||
|
||||
The pattern: each user creates their own Google Cloud project, enables the Gmail API,
|
||||
configures an External consent screen, adds every scope, creates a "Desktop app" OAuth
|
||||
client, and pastes the client ID and secret into Margin. Each user is then the
|
||||
"developer" of a one-user app, which falls under the personal-use exemption and needs
|
||||
no verification. It works, and several open-source tools ship this way.
|
||||
|
||||
It is not realistic for non-technical friends. It is fifteen to twenty clicks across a
|
||||
console whose UI changes every few months, it needs the People API and Calendar API
|
||||
enabled separately, the consent screen must either stay in Testing (7-day re-login)
|
||||
or be pushed to production (scary interstitial, but stable tokens), and support
|
||||
requests will all be "the Google page looks different". Offer it as an escape hatch
|
||||
for the technical, not as the default.
|
||||
|
||||
### Installed-app OAuth flow
|
||||
|
||||
Use the loopback flow: OAuth client type "Desktop app", redirect to
|
||||
`http://127.0.0.1:{port}` (or `http://[::1]:{port}`) served by a one-shot listener in the
|
||||
Rust backend, PKCE mandatory (verifier 43 to 128 unreserved characters, `S256`
|
||||
challenge). Google states "Custom URI schemes are no longer supported due to the risk
|
||||
of app impersonation" and the out-of-band copy-paste flow is gone. The client secret
|
||||
of a Desktop client is not a secret; Google's own doc says installed apps "cannot keep
|
||||
secrets", so embedding it in the binary is expected.
|
||||
|
||||
Token facts to design around:
|
||||
|
||||
- "refresh tokens are always returned for installed applications".
|
||||
- "limit of 100 refresh tokens per Google Account per OAuth 2.0 client ID". The oldest
|
||||
is silently invalidated. Per-account, per-client, so a user on many machines is fine.
|
||||
- A refresh token dies if unused for six months, if the user revokes it, or if "The
|
||||
user changed passwords and the refresh token contains Gmail scopes". Password change
|
||||
logs Margin out. Handle `invalid_grant` by prompting re-auth, not by crashing sync.
|
||||
- Access tokens last about an hour. IMAP sessions authenticated with OAuth are "limited
|
||||
to about the validity period of the access token used (usually 1 hour)".
|
||||
|
||||
Store refresh tokens in the OS keychain (macOS Keychain, Windows Credential Manager,
|
||||
Secret Service on Linux), never in SQLite.
|
||||
|
||||
### What a small team shipping to friends should do
|
||||
|
||||
Create one Cloud project and one Desktop OAuth client. Push the consent screen to
|
||||
production unverified straight away and eat the interstitial: it costs 100 lifetime
|
||||
users, which is plenty for a friends release and avoids the 7-day token expiry that
|
||||
Testing imposes. In parallel, buy a domain, publish a homepage and privacy policy on
|
||||
it, record the demo video, and submit restricted scope verification with the explicit
|
||||
statement that the app has no server and stores all Google user data on the user's
|
||||
device; ask whether a security assessment is required. Budget for one anyway (order of
|
||||
USD 1,000 to 2,000 per year at the cheap end). Do not request `https://mail.google.com/`
|
||||
unless IMAP is in the plan; the narrower the scope list the easier the review.
|
||||
|
||||
Sources: https://developers.google.com/workspace/gmail/api/auth/scopes,
|
||||
https://support.google.com/cloud/answer/15549945,
|
||||
https://support.google.com/cloud/answer/7454865,
|
||||
https://support.google.com/cloud/answer/13464323,
|
||||
https://support.google.com/cloud/answer/13465431,
|
||||
https://developers.google.com/identity/protocols/oauth2/production-readiness/restricted-scope-verification,
|
||||
https://developers.google.com/identity/protocols/oauth2/production-readiness/sensitive-scope-verification,
|
||||
https://developers.google.com/identity/protocols/oauth2/native-app,
|
||||
https://developers.google.com/identity/protocols/oauth2 (refresh token expiration),
|
||||
https://developers.google.com/terms/api-services-user-data-policy,
|
||||
https://mimestream.com/trust/security-and-privacy,
|
||||
https://www.switchlabs.dev/post/casa-tier-2-tier-3-security-review-providers-pricing-and-the-cheapest-option (third-party),
|
||||
https://www.nylas.com/blog/google-oauth-app-verification/ (third-party, 2021).
|
||||
|
||||
## 2. Data model
|
||||
|
||||
Base URL is `https://gmail.googleapis.com/gmail/v1/users/{userId}/...`; use `me` for
|
||||
`userId`. Uploads go to `https://gmail.googleapis.com/upload/gmail/v1/...`.
|
||||
|
||||
### Message
|
||||
|
||||
Fields: `id` ("The immutable ID of the message", a 16-hex-digit string), `threadId`,
|
||||
`labelIds[]`, `snippet` ("A short part of the message text", HTML-escaped, roughly a
|
||||
sentence), `historyId` ("The ID of the last history record that modified this
|
||||
message"), `internalDate` ("The internal message creation timestamp (epoch ms)", the
|
||||
time Gmail received or created it, not the `Date` header; for `insert`/`import` you
|
||||
choose `internalDateSource=receivedTime|dateHeader`), `payload` (parsed MIME tree),
|
||||
`sizeEstimate` (bytes, approximate), `raw` (base64url RFC 2822 bytes, only with
|
||||
`format=raw`).
|
||||
|
||||
`format` on `messages.get` and `threads.get`:
|
||||
|
||||
| format | Returns | Notes |
|
||||
|---|---|---|
|
||||
| `minimal` | id, threadId, labelIds, snippet, historyId, internalDate, sizeEstimate | No headers. Same 20 units as `full`. |
|
||||
| `metadata` | minimal plus `payload.headers` | Pass `metadataHeaders=From&metadataHeaders=Subject...` to restrict which headers come back. No body parts, no MIME tree. |
|
||||
| `full` | Parsed MIME tree in `payload` | Body data is base64url in `payload.parts[].body.data`, or referenced by `attachmentId` when large. |
|
||||
| `raw` | Whole RFC 2822 message base64url in `raw` | Best for archival and for feeding a real MIME parser (mail-parser or mailparse in Rust). |
|
||||
|
||||
`MessagePart`: `partId`, `mimeType`, `filename` (only present on attachments),
|
||||
`headers[] {name, value}`, `body {attachmentId, size, data}`, `parts[]`. Body `data`
|
||||
is base64url without padding issues in practice but always decode with a
|
||||
URL-safe, padding-tolerant decoder. Large bodies and every real attachment arrive as
|
||||
`attachmentId` and must be fetched separately with
|
||||
`GET .../messages/{messageId}/attachments/{id}` (20 units, returns
|
||||
`MessagePartBody {attachmentId, size, data}`). Inline images are ordinary parts whose
|
||||
headers carry `Content-ID: <foo>` and `Content-Disposition: inline`; the HTML body
|
||||
references them as `src="cid:foo"`, and the client maps cid to the part and serves the
|
||||
bytes to the webview. Attachment IDs are widely reported to change between `get`
|
||||
calls, so store the `partId`/`Content-ID` path, not the attachment ID (unverified in
|
||||
the docs, consistently observed).
|
||||
|
||||
Sort and display by `internalDate`. The `Date` header is the sender's claim and can be
|
||||
hours off or absent.
|
||||
|
||||
### Thread
|
||||
|
||||
`{id, snippet, historyId, messages[]}`. `threads.get` accepts the same `format` and
|
||||
`metadataHeaders` and returns every message in the thread in one call for 40 units.
|
||||
Threads cannot be created directly; Gmail assigns `threadId` on delivery, `send`,
|
||||
`insert` or `import`. Threading rules are in section 4. Gmail's web UI splits a
|
||||
conversation into a new thread with the same subject after 100 messages (third-party,
|
||||
consistently reported, unverified in Google docs).
|
||||
|
||||
### Labels
|
||||
|
||||
System labels the guide lists, and whether `modify` may add or remove them:
|
||||
|
||||
| Label | Applicable via API |
|
||||
|---|---|
|
||||
| `INBOX` | Yes (remove to archive) |
|
||||
| `SPAM` | Yes |
|
||||
| `TRASH` | Yes (prefer `trash`/`untrash` methods) |
|
||||
| `UNREAD` | Yes |
|
||||
| `STARRED` | Yes |
|
||||
| `IMPORTANT` | Yes |
|
||||
| `SENT` | No |
|
||||
| `DRAFT` | No |
|
||||
| `CATEGORY_PERSONAL`, `CATEGORY_SOCIAL`, `CATEGORY_PROMOTIONS`, `CATEGORY_UPDATES`, `CATEGORY_FORUMS` | Yes |
|
||||
|
||||
"The preceding list isn't exhaustive and other reserved label names exist" (`CHAT` is
|
||||
one). Creating a user label with a reserved name returns "HTTP 400 - Invalid label
|
||||
name". System label IDs equal their names; user label IDs look like `Label_42` and
|
||||
are immutable, while names can be changed with `labels.patch`, so key everything on
|
||||
ID and re-list labels when history shows anything unexpected.
|
||||
|
||||
User label fields: `name` (a `/` in the name nests the label in the Gmail UI, so
|
||||
`Margin/Snoozed` renders under `Margin`; the parent label should exist first),
|
||||
`messageListVisibility` (`show`, `hide`), `labelListVisibility` (`labelShow`,
|
||||
`labelShowIfUnread`, `labelHide`), `color {textColor, backgroundColor}` restricted to
|
||||
a fixed palette of about a hundred hex values listed in the Label resource docs (any
|
||||
other value is rejected), `type` (`system`, `user`), and read-only counts
|
||||
`messagesTotal`, `messagesUnread`, `threadsTotal`, `threadsUnread`. Hard limit:
|
||||
"Maximum labels per mailbox: 10,000". Name length limit is not documented (the web
|
||||
UI enforces 225 characters, unverified). Labels cannot be applied to drafts.
|
||||
|
||||
### Drafts
|
||||
|
||||
`{id, message}`. The draft `id` is stable across `drafts.update`; the inner
|
||||
`message.id` changes on every update. `drafts.send` deletes the draft and returns a new
|
||||
message with a new `id` and the `SENT` label. Draft messages only ever carry the
|
||||
`DRAFT` label.
|
||||
|
||||
### History and historyId
|
||||
|
||||
`historyId` appears on every message, thread, the profile (`users.getProfile`, 1
|
||||
unit, returns `emailAddress`, `messagesTotal`, `threadsTotal`, `historyId`), and every
|
||||
`history.list` and `watch` response. Google: "History IDs increase chronologically but
|
||||
are not contiguous with random gaps in between valid IDs." A history record is
|
||||
`{id, messages[], messagesAdded[], messagesDeleted[], labelsAdded[], labelsRemoved[]}`
|
||||
where the added/removed entries carry `{message: {id, threadId, labelIds?}, labelIds[]}`.
|
||||
"We recommend using the specific change-type fields instead of" `messages[]`.
|
||||
`messagesDeleted` means permanently deleted, not trashed; trash shows up as
|
||||
`labelsAdded` with `TRASH`.
|
||||
|
||||
Stable IDs: message id, thread id, label id, draft id (until sent), historyId (as a
|
||||
cursor). Not stable: the message id behind a draft, attachment ids (reported),
|
||||
`snippet` text (can be regenerated), `resultSizeEstimate` (an estimate).
|
||||
|
||||
Sources: https://developers.google.com/workspace/gmail/api/reference/rest/v1/users.messages,
|
||||
https://developers.google.com/workspace/gmail/api/reference/rest/v1/Format,
|
||||
https://developers.google.com/workspace/gmail/api/reference/rest/v1/users.messages.attachments/get,
|
||||
https://developers.google.com/workspace/gmail/api/reference/rest/v1/users.threads,
|
||||
https://developers.google.com/workspace/gmail/api/guides/labels,
|
||||
https://developers.google.com/workspace/gmail/api/reference/rest/v1/users.labels,
|
||||
https://developers.google.com/workspace/gmail/api/guides/drafts,
|
||||
https://developers.google.com/workspace/gmail/api/reference/rest/v1/users.history/list,
|
||||
https://support.cloudhq.net/how-does-gmail-decide-to-group-emails-into-conversations/ (third-party).
|
||||
|
||||
## 3. Sync strategy
|
||||
|
||||
### Quota, the numbers that matter
|
||||
|
||||
"As of May 1, 2026, the usage limits for this API were updated."
|
||||
|
||||
| Limit | Value |
|
||||
|---|---|
|
||||
| Per minute per user per project | 6,000 quota units |
|
||||
| Per minute per project | 1,200,000 quota units |
|
||||
| Per day per project (billing threshold) | 80,000,000 quota units |
|
||||
| Batch | 100 calls max per batch, "larger than 50 requests is not recommended", counted as n requests |
|
||||
| Pricing | "All standard use of the Gmail API is available at no additional cost. Exceeding the quota request limits is planned to incur charges to your Google Cloud billing account later in 2026." |
|
||||
|
||||
Per-method costs (units):
|
||||
|
||||
| Method | Units | Method | Units |
|
||||
|---|---|---|---|
|
||||
| `messages.send`, `drafts.send`, `watch` | 100 | `messages.get`, `drafts.get`, `messages.attachments.get`, `messages.trash` | 20 |
|
||||
| `threads.get` | 40 | `threads.trash`, `threads.delete` | 20 |
|
||||
| `messages.batchModify`, `messages.batchDelete`, `stop` | 50 | `messages.insert`, `messages.import` | 25 |
|
||||
| `drafts.update` | 15 | `messages.delete`, `drafts.create`, `drafts.delete`, `threads.list`, `threads.modify` | 10 |
|
||||
| `messages.list`, `drafts.list`, `messages.modify`, `messages.untrash`, `labels.create/delete/update`, `settings.filters.create/delete`, `settings.updateVacation` | 5 | `history.list` | 2 |
|
||||
| `getProfile`, `labels.get`, `labels.list`, `settings.filters.get/list`, `settings.getVacation`, `settings.getAutoForwarding` | 1 | `settings.sendAs.create/update/verify`, `forwardingAddresses.create`, `delegates.create`, `updatePop` | 100 |
|
||||
|
||||
Third-party pages dated 2026 still quote `messages.get` at 5 and `threads.get` at 10;
|
||||
Google's page as read on 2026-09-03 says 20 and 40. There is also an undocumented
|
||||
concurrency ceiling per mailbox (Unipile reports "50 concurrent in-flight requests
|
||||
per mailbox"; Google's error page only says 429 can be "triggered by daily per-user
|
||||
limits, bandwidth limits, or concurrent request limits" and "Per-user limits cannot
|
||||
be increased").
|
||||
|
||||
What 6,000 units per minute buys: 300 `messages.get` per minute, or 150 `threads.get`,
|
||||
or 60 sends. A mailbox with 20,000 messages costs 400,000 units to fetch metadata for
|
||||
every message, which is 67 minutes at the ceiling; 100,000 messages is 5.6 hours.
|
||||
Idle polling of `history.list` every 15 seconds is 8 units per minute, so polling is
|
||||
free; hydration is what costs.
|
||||
|
||||
### Initial full sync
|
||||
|
||||
1. `getProfile` and record `historyId` before you list anything, so the first partial
|
||||
sync covers everything that changed during the crawl.
|
||||
2. `messages.list?maxResults=500&includeSpamTrash=true` (5 units per page, ids and
|
||||
threadIds only, newest first) and page with `pageToken` until exhausted. 20,000
|
||||
messages is 40 pages, 200 units, a few seconds. Store ids, threadIds and a
|
||||
"needs hydration" flag.
|
||||
3. Hydrate newest first in batches of 50 `messages.get?format=metadata` with
|
||||
`metadataHeaders` limited to what the UI and the classifier need: `From`, `To`,
|
||||
`Cc`, `Bcc`, `Reply-To`, `Subject`, `Date`, `Message-ID`, `In-Reply-To`,
|
||||
`References`, `List-Id`, `List-Unsubscribe`, `List-Unsubscribe-Post`, `Precedence`,
|
||||
`Authentication-Results`, `Content-Type`. Throttle to roughly 280 gets per
|
||||
minute, retry 429 and 403 `userRateLimitExceeded` with truncated exponential
|
||||
backoff starting at 1 second, and treat 404 as "deleted since listing".
|
||||
4. For threads with three or more messages, `threads.get?format=metadata` (40 units)
|
||||
is cheaper than per-message gets. Group step 2's ids by threadId and route.
|
||||
5. Bodies: fetch `format=full` (or `raw`) lazily on open and prefetch the last N days
|
||||
in the background. Cache decoded HTML and text in SQLite with an LRU cap.
|
||||
6. Labels: `labels.list` (1 unit) once, then again whenever history mentions a label
|
||||
id you do not know.
|
||||
|
||||
Order of hydration is a product decision: newest 30 days first, then the rest, and the
|
||||
UI must be usable while step 3 is running. Zero (Mail-0) reports initial sync "can take
|
||||
hours for large inboxes (10,000+ emails)"; with the new unit costs that is the norm,
|
||||
not the exception.
|
||||
|
||||
### Partial sync
|
||||
|
||||
`history.list?startHistoryId=X&maxResults=500` (2 units per page), optionally
|
||||
`historyTypes=messageAdded|messageDeleted|labelAdded|labelRemoved` and `labelId=`.
|
||||
Apply pages in order; the records are chronological by `id`. For each `messagesAdded`
|
||||
run `messages.get` (the record includes `labelIds`, but not headers); for
|
||||
`labelsAdded`/`labelsRemoved` update the local label set; for `messagesDeleted`
|
||||
delete locally. A message can appear in `messagesAdded` and `messagesDeleted` in the
|
||||
same window, and `messages.get` can 404 for a record you have not processed yet;
|
||||
both are normal, not errors. When the final page has no `nextPageToken`, store the
|
||||
response `historyId` as the new cursor. Never combine `q` with history; history is
|
||||
mailbox-wide and has no query parameter, so every screening decision is made locally
|
||||
on the record's `labelIds` and the fetched headers.
|
||||
|
||||
Google on validity: "A historyId is typically valid for at least a week, but in some
|
||||
rare circumstances may be valid for only a few hours. If you receive an HTTP 404 error
|
||||
response, your application should perform a full sync." The cheap recovery is: re-run
|
||||
step 2 of the full sync (ids only, 5 units per 500), diff against the local set to
|
||||
find adds and deletes, hydrate the adds, and refresh `labelIds` for the most recent N
|
||||
days with `format=minimal` (still 20 units each; there is no cheaper label-only read
|
||||
except `threads.list`, which returns only ids). Accept that label state for old mail
|
||||
may be stale until the user opens it.
|
||||
|
||||
### Polling cadence
|
||||
|
||||
Poll `history.list` every 10 to 15 seconds while the window is focused, 60 seconds in
|
||||
the background, and immediately after any local write. Cost at 15 seconds is 5,760
|
||||
units per user per day; 1,000 users idle-polling this way is 5.8 million units per
|
||||
day against the 80 million project threshold. The budget goes to hydration and full
|
||||
syncs, not polling.
|
||||
|
||||
### Push via Pub/Sub, honestly
|
||||
|
||||
`users.watch` (100 units) with `topicName=projects/{project}/topics/{topic}` where the
|
||||
project "must exactly match your Google developer project id (the one executing this
|
||||
watch request)". You grant `roles/pubsub.publisher` on the topic to
|
||||
`[email protected]`. Watches expire after 7 days and Google
|
||||
says "We recommend calling watch once per day". The notification is only
|
||||
`{"emailAddress": "...", "historyId": "..."}` and the rate is capped at "one event per
|
||||
second" per user. On receipt you run the partial sync above.
|
||||
|
||||
Delivery needs a Pub/Sub subscription. A push subscription posts to an HTTPS endpoint,
|
||||
which means a server. A pull subscription is possible from a desktop app in theory:
|
||||
`projects.subscriptions.pull` needs the `pubsub.subscriptions.consume` permission
|
||||
(`roles/pubsub.subscriber`) on the subscription and a token with
|
||||
`https://www.googleapis.com/auth/pubsub` or `cloud-platform`. The problem is the
|
||||
principal. The desktop app authenticates as the end user's Google account, which has
|
||||
no IAM on your project. Your options are to grant every user individually (a server
|
||||
or manual step), grant `allAuthenticatedUsers` (any Google account on earth could pull
|
||||
and ack everyone's notifications from the shared subscription), or ship a service
|
||||
account key in the binary (extractable, and Google will reject it in review). None is
|
||||
acceptable. Conclusion: with no server there is no push. If you want push later, the
|
||||
minimum is a small relay that owns the subscription and forwards `{emailAddress,
|
||||
historyId}` to connected clients; Mimestream runs exactly that (`push.mimestream.com`)
|
||||
and stores only "Email address, APNs device token, Device identifier". That relay may
|
||||
also drag you into the security assessment. Ship with polling.
|
||||
|
||||
### IMAP as a complement
|
||||
|
||||
Gmail IMAP (`imap.gmail.com:993`, SMTP `smtp.gmail.com:465` or `587`) authenticates with
|
||||
SASL XOAUTH2 (`base64("user=" user "^Aauth=Bearer " token "^A^A")`) using the
|
||||
`https://mail.google.com/` scope, which is restricted like the rest. Capabilities
|
||||
include `IDLE`, `CONDSTORE`, `MOVE`, `UIDPLUS`, `X-GM-EXT-1` and `APPENDLIMIT=35651584`;
|
||||
there is no `QRESYNC`. The Gmail extensions expose `X-GM-MSGID` (64-bit message id),
|
||||
`X-GM-THRID` (thread id), `X-GM-LABELS` (fetch, store and search labels) and
|
||||
`X-GM-RAW` (full Gmail search syntax over IMAP). The REST `id` is the lowercase hex of
|
||||
`X-GM-MSGID` and `threadId` the hex of `X-GM-THRID` (widely relied on, verify in a
|
||||
spike before depending on it).
|
||||
|
||||
What IMAP does that REST cannot: bulk header fetch with no unit quota (one `UID FETCH
|
||||
1:* (ENVELOPE BODYSTRUCTURE X-GM-LABELS X-GM-THRID X-GM-MSGID)` over `[Gmail]/All Mail`
|
||||
returns tens of thousands of messages in minutes, limited only by the 2,500 MB per day
|
||||
IMAP download bandwidth), and `IDLE` for near-instant new-mail notification without a
|
||||
server (one connection per watched folder, sessions "limited to about 24 hours" and
|
||||
OAuth sessions to the token lifetime, so reconnect hourly). What REST does that IMAP
|
||||
cannot: `history.list` (IMAP has no mailbox-wide change log; `CONDSTORE` gives per-folder
|
||||
`MODSEQ` only), filters, settings, send-as, vacation, drafts with stable ids, proper
|
||||
`SENT` threading via `threadId`, snippets, `sizeEstimate`, category labels as first-class
|
||||
ids, and batch HTTP. IMAP exceeding bandwidth suspends the account for "1 hour, but can
|
||||
last up to 24 hours".
|
||||
|
||||
Reasonable hybrid: REST for everything the user touches and for the change log; IMAP
|
||||
only as an optional accelerator for the initial backfill and for `IDLE` as a wake-up
|
||||
signal that triggers `history.list`. That costs you the `https://mail.google.com/`
|
||||
scope and a second protocol stack, so do it only if the initial sync time measured
|
||||
with REST alone is unacceptable.
|
||||
|
||||
### Local mirror
|
||||
|
||||
Mimestream keeps everything in a Core Data SQLite store on the Mac and syncs "directly
|
||||
with Google APIs, not through an intermediary service". Do the same: tables for
|
||||
accounts (email, historyId cursor, profile), labels (id, name, type, visibility,
|
||||
colour), messages (id, threadId, internalDate, sizeEstimate, snippet, labelIds as a
|
||||
JSON array or a join table, parsed headers as columns, hydration state), threads
|
||||
(id, latest internalDate, participant summary, derived flags), bodies (message id,
|
||||
html, text, fetched_at), attachments (message id, partId, filename, mimeType, size,
|
||||
content-id, cached path), an outbox, and an FTS5 table over subject, participants and
|
||||
body text. Everything in section 5 marked "client-side" also lives here.
|
||||
|
||||
Sources: https://developers.google.com/workspace/gmail/api/reference/quota,
|
||||
https://developers.google.com/workspace/gmail/api/guides/sync,
|
||||
https://developers.google.com/workspace/gmail/api/reference/rest/v1/users.history/list,
|
||||
https://developers.google.com/workspace/gmail/api/reference/rest/v1/users.messages/list,
|
||||
https://developers.google.com/workspace/gmail/api/guides/batch,
|
||||
https://developers.google.com/workspace/gmail/api/guides/handle-errors,
|
||||
https://developers.google.com/workspace/gmail/api/guides/push,
|
||||
https://developers.google.com/workspace/gmail/api/reference/rest/v1/users/watch,
|
||||
https://docs.cloud.google.com/pubsub/docs/access-control,
|
||||
https://docs.cloud.google.com/pubsub/docs/reference/rest/v1/projects.subscriptions/pull,
|
||||
https://mimestream.com/trust/private-push,
|
||||
https://developers.google.com/workspace/gmail/imap/imap-extensions,
|
||||
https://developers.google.com/workspace/gmail/imap/xoauth2-protocol,
|
||||
https://developers.google.com/workspace/gmail/imap/imap-smtp,
|
||||
https://knowledge.workspace.google.com/admin/gmail/gmail-bandwidth-limits,
|
||||
https://gist.github.com/emersion/2c769bc1ed60a7b7945910d35b606801 (third-party capability dump),
|
||||
https://www.unipile.com/gmail-api-limits/ (third-party).
|
||||
|
||||
## 4. Writes
|
||||
|
||||
### Labels and state
|
||||
|
||||
- `messages.modify` (5 units): `{addLabelIds[], removeLabelIds[]}`.
|
||||
- `messages.batchModify` (50 units): same body plus `ids[]`, "There is a limit of 1000
|
||||
ids per request", empty response, no per-id error report.
|
||||
- `threads.modify` (10 units): applies to every message in the thread. Note that
|
||||
"Messages in a thread might have labels that other messages in the same thread
|
||||
don't have", so thread-level views must aggregate.
|
||||
- Read/unread: remove/add `UNREAD`. Star: `STARRED`. Importance: `IMPORTANT`. Archive:
|
||||
remove `INBOX`. Spam: add `SPAM` (removes from inbox in Gmail's view). Category
|
||||
move: remove one `CATEGORY_*`, add another.
|
||||
- Trash: `messages.trash` / `threads.trash` (20) and `untrash` (5). Gmail purges trash
|
||||
after 30 days. Permanent delete: `messages.delete` (10), `batchDelete` (50),
|
||||
`threads.delete` (20); these need `https://mail.google.com/` (long-standing,
|
||||
unverified in this pass).
|
||||
- Mute: there is no mute label or method. `q=is:muted` does find muted threads, so
|
||||
the state is readable but not writable. Emulate with a `Margin/Muted` label and a
|
||||
client rule that strips `INBOX` from new messages in muted threads as they arrive in
|
||||
history. While Margin is not running, Gmail web will show them in the inbox.
|
||||
|
||||
### Sending
|
||||
|
||||
`messages.send` (100 units): body `{raw, threadId?}` where `raw` is the complete RFC
|
||||
2822 message base64url-encoded. Simple JSON body is fine for small messages; for
|
||||
anything with attachments use the upload URI with `uploadType=multipart` or
|
||||
`resumable`. Discovery document `maxSize` for send is 36,700,160 bytes (35 MB) for the
|
||||
whole encoded message; the Gmail web UI limit for attachments is 25 MB (unverified).
|
||||
`insert` and `import` accept 157,286,400 bytes (150 MB). Consumer accounts are capped
|
||||
at 500 messages per day and Workspace at 2,000 (Google's help page confirms the 500;
|
||||
the 2,000 is third-party), counted across web, IMAP and API.
|
||||
|
||||
Threading rules for a reply, all three required: the `threadId` in the request, and
|
||||
"The References and In-Reply-To headers must be set in compliance with the RFC 2822
|
||||
standard", and "The Subject headers must match" (Gmail tolerates `Re:`, `Fwd:`, `R:`
|
||||
and similar prefixes). Since the March 2019 change Gmail also requires that "an
|
||||
incoming message's Reference header, if present, must reference IDs of previous
|
||||
messages"; same subject and participants alone no longer thread. Practical recipe:
|
||||
`In-Reply-To: <parent Message-ID>`, `References: <parent References> <parent
|
||||
Message-ID>`, subject `Re: <original>`, `threadId` of the parent. Changing the subject
|
||||
starts a new thread on the recipient's side.
|
||||
|
||||
Other send facts: the `From` header must be the account's primary address or a
|
||||
verified send-as alias, otherwise Gmail rewrites it to the primary (documented
|
||||
behaviour of aliases, rewriting itself reported by developers, unverified in the
|
||||
reference). `labelIds` in the send body should be assumed ignored; apply labels with
|
||||
`messages.modify` on the returned id (unverified). Gmail generates a `Message-ID` if
|
||||
you omit one; supplying your own lets you correlate the outbox with the sent copy.
|
||||
The response contains the new `id`, `threadId` and `labelIds` (`SENT`). There is no
|
||||
scheduled send and no undo send in the API; Gmail web's Undo Send is a client-side
|
||||
hold of 5, 10, 20 or 30 seconds before the message leaves, which is exactly what
|
||||
Margin should implement.
|
||||
|
||||
### Drafts
|
||||
|
||||
`drafts.create` (10), `drafts.update` (15), `drafts.get` (20), `drafts.list` (5),
|
||||
`drafts.delete` (10), `drafts.send` (100, "You can send as-is or provide updates by
|
||||
including a new MIME message"). Put `threadId` inside `message` for reply drafts.
|
||||
Drafts created via the API appear in Gmail web's Drafts folder, which is the only
|
||||
roaming compose state you get.
|
||||
|
||||
### Settings
|
||||
|
||||
- `settings.sendAs.list/get`: enumerate the primary address and aliases with
|
||||
`displayName`, `replyToAddress`, `signature` (HTML, "added to new emails only",
|
||||
Gmail "will sanitize the HTML before saving it"), `isPrimary`, `isDefault`,
|
||||
`treatAsAlias`, `verificationStatus` (`accepted`, `pending`). `sendAs.update/patch`
|
||||
on the primary address works with `gmail.settings.basic`; "Addresses other than the
|
||||
primary address for the account can only be updated by service account clients that
|
||||
have been delegated domain-wide authority", and `create`, `delete`, `verify` are
|
||||
service-account-only too. So: read aliases, send from them, edit the primary
|
||||
signature; nothing else.
|
||||
- `settings.getVacation/updateVacation` (`gmail.settings.basic`):
|
||||
`{enableAutoReply, responseSubject, responseBodyPlainText, responseBodyHtml,
|
||||
restrictToContacts, restrictToDomain, startTime, endTime}`.
|
||||
- Filters (`gmail.settings.basic`): `settings.filters.create/get/list/delete`, no
|
||||
update ("filters must be deleted and recreated"), "you can only create a maximum of
|
||||
1,000 filters". Criteria: `from`, `to` (matches the local part, case-insensitive),
|
||||
`subject`, `query` (Gmail search syntax), `negatedQuery`, `hasAttachment`,
|
||||
`excludeChats`, `size` with `sizeComparison` (`smaller`, `larger`). Actions:
|
||||
`addLabelIds[]`, `removeLabelIds[]`, `forward` (needs a verified forwarding address,
|
||||
which you cannot create). The guide's examples use `INBOX` and `UNREAD` in
|
||||
`removeLabelIds` (skip inbox, mark read), `TRASH`, `STARRED`, `IMPORTANT` in
|
||||
`addLabelIds`, and one user label per filter; the exact accepted set is not spelled
|
||||
out. "Filters only apply to specific messages and not the entire email thread", and
|
||||
they act on mail as it arrives; the web UI's "also apply filter to matching
|
||||
conversations" has no API equivalent, so backfilling is your job with `batchModify`.
|
||||
- Forwarding addresses, auto-forwarding, delegates, POP/IMAP toggles: all
|
||||
service-account-only or admin territory. Delegates are Workspace-only ("up to 25
|
||||
delegates and up to 10 delegators").
|
||||
- `messages.import` (25 units, 150 MB) runs "standard email delivery scanning and
|
||||
classification similar to receiving via SMTP" including `processForCalendar`;
|
||||
`messages.insert` just stores. Neither sends. Useful for migration, not for the
|
||||
features here.
|
||||
|
||||
Sources: https://developers.google.com/workspace/gmail/api/reference/rest/v1/users.messages/batchModify,
|
||||
https://developers.google.com/workspace/gmail/api/reference/rest/v1/users.messages/send,
|
||||
https://gmail.googleapis.com/$discovery/rest?version=v1 (maxSize values),
|
||||
https://developers.google.com/workspace/gmail/api/guides/sending,
|
||||
https://developers.google.com/workspace/gmail/api/guides/threads,
|
||||
https://workspaceupdates.googleblog.com/2019/03/threading-changes-in-gmail-conversation-view.html,
|
||||
https://developers.google.com/workspace/gmail/api/guides/drafts,
|
||||
https://developers.google.com/workspace/gmail/api/guides/alias_and_signature_settings,
|
||||
https://developers.google.com/workspace/gmail/api/reference/rest/v1/users.settings.sendAs/create,
|
||||
https://developers.google.com/workspace/gmail/api/reference/rest/v1/users.settings.sendAs/update,
|
||||
https://developers.google.com/workspace/gmail/api/reference/rest/v1/users.settings/updateVacation,
|
||||
https://developers.google.com/workspace/gmail/api/guides/filter_settings,
|
||||
https://developers.google.com/workspace/gmail/api/reference/rest/v1/users.settings.filters,
|
||||
https://developers.google.com/workspace/gmail/api/reference/rest/v1/users.settings.filters/create,
|
||||
https://developers.google.com/workspace/gmail/api/reference/rest/v1/users.settings.forwardingAddresses/create,
|
||||
https://developers.google.com/workspace/gmail/api/reference/rest/v1/users.settings/updateAutoForwarding,
|
||||
https://developers.google.com/workspace/gmail/api/reference/rest/v1/users.settings.delegates/create,
|
||||
https://developers.google.com/workspace/gmail/api/reference/rest/v1/users.messages/import,
|
||||
https://support.google.com/mail/answer/22839, https://support.google.com/mail/answer/2819488.
|
||||
|
||||
## 5. Feature feasibility matrix
|
||||
|
||||
"Native" means the Gmail API (or the named Google API) does it as a first-class
|
||||
operation. "Emulate" means Margin builds it from labels, filters, headers and local
|
||||
state. Anything emulated with local state does not roam to another machine or to
|
||||
Gmail web unless the label mirror carries it.
|
||||
|
||||
| Feature | Native in Gmail API | Emulate client-side (how) | Not possible |
|
||||
|---|---|---|---|
|
||||
| Screener (allow-list senders, hide unknown senders from inbox) | No. Filters cannot express "sender not in a list" beyond a single `negatedQuery` capped by query length. | On every `messagesAdded` with `INBOX`, look the sender up in a local allow-list (contacts, prior correspondents, user decisions). Unknown: `modify` remove `INBOX`, add `Margin/Screened out`. Screen-in decision re-adds `INBOX` for the thread and records the sender. Only runs while Margin is running; Gmail web shows unscreened mail in Inbox in between. | Server-side enforcement without a server. |
|
||||
| Split inbox / Feed / Paper trail | Partial. `CATEGORY_PROMOTIONS`, `CATEGORY_UPDATES`, `CATEGORY_SOCIAL`, `CATEGORY_FORUMS`, `CATEGORY_PERSONAL` are applied by Gmail's classifier regardless of whether tabs are on (third-party), and are readable and settable. | Per-sender routing stored locally and mirrored as `Margin/Feed`, `Margin/Paper trail` labels via `modify`; optionally a Gmail filter per stable sender (`from:` add label, remove `INBOX`) so Gmail web matches and mail arriving while Margin is closed is routed. Seed the initial bucket from `CATEGORY_*` and `List-Id`/`Precedence: bulk` headers; let the user override per sender. | |
|
||||
| Reply later pile | No. | Label `Margin/Reply later` plus local ordering and optional local reminders. Keep or drop `INBOX` as a product choice. | |
|
||||
| Set aside pile | No. | Label `Margin/Set aside`, remove `INBOX`. | |
|
||||
| Snooze | No. Gmail's own snooze is invisible except through `q=in:snoozed` and the eventual `INBOX` label add/remove in history; no snooze date is exposed (issue 287304309 is open). | Remove `INBOX`, add `Margin/Snoozed`, store wake time in SQLite; a scheduler re-adds `INBOX` (and `UNREAD` if wanted). Fires late if Margin was closed. Do not touch messages the user snoozed in Gmail web; Mimestream documents a Gmail server bug where archiving previously-snoozed mail via the API leaves it showing in Inbox. | Cross-device wake without your own sync. |
|
||||
| Send later | No. Gmail web's Schedule send is not in the API. | Local outbox row with `send_at`; scheduler calls `messages.send` when due. Store the compose as a Gmail draft too so it survives a reinstall. App must be running at send time. | Sending while the app is closed. |
|
||||
| Undo send | No. | Hold the message in the outbox for N seconds (Gmail web offers 5, 10, 20, 30) before calling `messages.send`. After send it is gone. | Recall after send. |
|
||||
| Read receipts / open tracking | No. Gmail's read receipts are Workspace-only and admin-enabled (`Disposition-Notification-To`); consumer accounts cannot request or return them. | Adding `Disposition-Notification-To` to `raw` costs nothing but only some recipients' clients honour it (unverified). Pixel tracking needs a server you run and is the same practice Margin blocks on receipt; recommend not building it. | Open tracking without a server. |
|
||||
| Spy pixel blocking | No. | Load bodies in the webview with remote content blocked by default, rewrite `<img src=http...>` to placeholders, allow per-sender or per-message, strip 1x1 and known tracker hosts, never send `Referer`. Optional proxy through your own server later. | |
|
||||
| Unsubscribe | No (Gmail web's button is UI only). | Fetch `List-Unsubscribe` and `List-Unsubscribe-Post` via `metadataHeaders`. If `List-Unsubscribe-Post: List-Unsubscribe=One-Click` is present, POST to the HTTPS URI with body `List-Unsubscribe=One-Click` (RFC 8058, no GET, no confirmation page). Else `mailto:` via `messages.send`, else open the HTTPS link. Pair with a `from:` filter to `TRASH` or `Margin/Screened out` for stragglers. | |
|
||||
| Block sender | Yes via filters: `criteria.from`, `action.addLabelIds=[TRASH]` (or `removeLabelIds=[INBOX]` plus a label). | Backfill existing mail with `messages.list?q=from:x` and `batchModify`. | |
|
||||
| Snippets / templates | No. | Local SQLite; insert into compose. | |
|
||||
| Sticky notes on threads | No. | Local SQLite keyed by threadId. Hidden drafts in the thread show up in Gmail web and labels cannot carry text, so there is no sane Gmail-side store. | Roaming without your own sync. |
|
||||
| Rename thread subject | No; message headers are immutable. | Local display-name override keyed by threadId; replies still carry the real subject so threading holds. | Changing what Gmail web shows. |
|
||||
| Merge threads | No; `threadId` is assigned by Gmail. | Local mapping of several threadIds to one virtual thread; writes fan out to every underlying thread. | Merging in Gmail itself. |
|
||||
| Contacts autocomplete | Yes via People API: `otherContacts.list` (`contacts.other.readonly`, `readMask=names,emailAddresses`, pageSize up to 1000, sync tokens expire after 7 days), `people.connections.list` (`contacts.readonly`, up to 1000), `otherContacts.search` and `people.searchContacts` (max 30 results, send an empty-query warm-up first). | Also index From/To/Cc from synced headers locally; it is the best signal and needs no extra scope. | |
|
||||
| Attachments browsing | Partial: `q=has:attachment` or `filename:pdf` returns ids. | Index `payload.parts[].filename/mimeType/size` from `format=full` fetches into SQLite; fetch bytes on demand with `attachments.get`. | |
|
||||
| Calendar invite RSVP | Not in Gmail API. Calendar API: find the event with `events.list?iCalUID=<UID from the text/calendar part>` on `primary` (Gmail auto-adds invites depending on the user's "Add invitations to my calendar" setting, "From everyone" or "Only if the sender is known"), then `events.patch` with `attendees[self].responseStatus` in `accepted`, `tentative`, `declined` and `sendUpdates=all`; if absent, `events.import` with `iCalUID`, `start`, `end`. Needs `calendar.events`. | Parse the `text/calendar` part (`METHOD:REQUEST`) yourself and render the card. A standards-only fallback is to email a `METHOD:REPLY` iMIP message to the organiser via `messages.send`, which needs no Calendar scope but does not update the user's own calendar. | |
|
||||
| Shared / team features | Out of scope. Delegation exists but is Workspace-only and service-account-only. | | |
|
||||
| Multiple accounts | Yes: one OAuth grant per account with the same client ID; separate refresh tokens and sync cursors. | | |
|
||||
| Unified inbox | No. | Merge locally across account databases; every write routes to the owning account. | |
|
||||
| Search | Yes: `messages.list?q=` and `threads.list?q=` (5 or 10 units per page, full Gmail operator set: `from:`, `to:`, `subject:`, `label:`, `category:`, `has:attachment`, `filename:`, `list:`, `newer_than:`, `larger:`, `rfc822msgid:`, `in:snoozed`, `is:muted`, `deliveredto:`, `AROUND`, `OR`, `-`). Returns ids only; hydrate from the mirror. Not available under `gmail.metadata`. Cannot be combined with `history.list`. | SQLite FTS5 over synced subject, participants and cached bodies for instant results; fall back to server `q` for anything not yet hydrated and for operators you have not implemented. Server search is a network round trip and returns ids in relevance-ish order, so results feel slower than local. | |
|
||||
| Keyboard triage | No. | Client. Batch consecutive label changes into `batchModify` calls. | |
|
||||
| Push notifications | Yes with `users.watch` plus Pub/Sub, but delivery requires a server or an unacceptable IAM grant (section 3). | Poll `history.list`; optionally IMAP `IDLE` on `INBOX` as a wake-up. | Push with no server. |
|
||||
| Offline compose and queue | No. | Local outbox; `drafts.create` when online so the draft roams; `messages.send` on reconnect with backoff. | |
|
||||
| Categories tabs | Yes: read and set `CATEGORY_*`. Accuracy is good for big senders and mediocre for small newsletters; Promotions is a fair Feed seed, Updates is a fair Paper trail seed (receipts, notifications), Forums and Social less useful. Gmail may reclassify a sender after the user moves a few messages, which you observe as label churn in history. | Treat as a prior; sender-level rules win. | |
|
||||
| Importance markers | Yes: `IMPORTANT` label readable and settable; filters can `removeLabelIds=[IMPORTANT]`. | | |
|
||||
| Muted threads | Readable via `q=is:muted`, not settable. | `Margin/Muted` label plus client rule (section 4). | Muting that Gmail itself enforces. |
|
||||
| Labels and folders, archive, star, read state, trash, spam | Yes. | | |
|
||||
| Signature and vacation responder | Yes for the primary address (`sendAs.patch` signature, `updateVacation`). | | Creating or verifying send-as aliases (service-account-only). |
|
||||
| Permanent delete | Yes, but only with `https://mail.google.com/`. | Trash instead. | |
|
||||
|
||||
Sources: https://developers.google.com/workspace/gmail/api/guides/labels,
|
||||
https://developers.google.com/workspace/gmail/api/guides/filter_settings,
|
||||
https://issuetracker.google.com/issues/287304309 (login required),
|
||||
https://mimestream.com/help/common-issues/archiving-snoozed-messages,
|
||||
https://support.google.com/mail/answer/9413651,
|
||||
https://www.rfc-editor.org/rfc/rfc8058,
|
||||
https://developers.google.com/people/api/rest/v1/otherContacts/list,
|
||||
https://developers.google.com/people/api/rest/v1/otherContacts/search,
|
||||
https://developers.google.com/people/api/rest/v1/people.connections/list,
|
||||
https://developers.google.com/people/api/rest/v1/people/searchContacts,
|
||||
https://developers.google.com/workspace/calendar/api/v3/reference/events,
|
||||
https://developers.google.com/workspace/calendar/api/v3/reference/events/list,
|
||||
https://developers.google.com/workspace/calendar/api/v3/reference/events/import,
|
||||
https://support.google.com/calendar/answer/13159188,
|
||||
https://support.google.com/mail/answer/7190 (search operators),
|
||||
https://support.google.com/mail/thread/231661001 (categories applied with tabs off, community thread).
|
||||
|
||||
## 6. Gotchas and war stories
|
||||
|
||||
- Quota changed on 1 May 2026. Any library, blog post or mental model built on 250
|
||||
units per second per user and 5-unit `messages.get` is off by a factor of 4 to 10.
|
||||
Budget hydration at 300 messages per minute per user and expect billing for overage
|
||||
"later in 2026".
|
||||
- Full sync is now the slow part. A 100,000 message mailbox is roughly 5 to 6 hours of
|
||||
metadata hydration at the ceiling. Hydrate newest first, show partial state, and
|
||||
consider IMAP for the backfill if that is unacceptable.
|
||||
- `history.list` 404 is routine. A user who does not open Margin for a week or two
|
||||
will hit it; treat the ids-only re-list as a normal code path, not a disaster.
|
||||
- History can reference messages that no longer exist. `messages.get` 404 on a
|
||||
`messagesAdded` id means it was deleted in the same window. Log and move on.
|
||||
- Label IDs are not names. `Label_123` is the key; names change; system labels are
|
||||
their own IDs. Re-list labels when history contains an unknown id. Do not create a
|
||||
label whose name collides with a reserved name (400).
|
||||
- `labelIds` on `messages.send` should be assumed ignored; `modify` afterwards.
|
||||
- `raw` is base64url, not standard base64. `+`/`/` versus `-`/`_` mistakes produce
|
||||
corrupt MIME that Gmail rejects with a 400 that says nothing useful.
|
||||
- 35 MB (36,700,160 bytes) is the hard `send` ceiling for the whole encoded message,
|
||||
which after base64 expansion is about 25 MB of attachments. Fail early in the
|
||||
composer, not at send.
|
||||
- Messages are immutable. There is no way to change headers, subject, or body of a
|
||||
stored message. Every "edit" feature in section 5 is a local overlay.
|
||||
- `messages.list` pages at most 500 and returns only ids; `resultSizeEstimate` is an
|
||||
estimate and can be wildly wrong. `threads.list` likewise.
|
||||
- Batch: 100 max, 50 recommended, each call metered separately, and 429 inside a batch
|
||||
comes back per part, so parse every part's status. The batch path from the discovery
|
||||
document is `batch/gmail/v1` on `www.googleapis.com`.
|
||||
- 429 and 403 `userRateLimitExceeded` / `rateLimitExceeded` both want truncated
|
||||
exponential backoff (`min((2^n) + random_ms, 32 to 64 s)`) and "Start retry periods
|
||||
at least one second after the error". Per-user limits "cannot be increased for any
|
||||
reason". Watch the undocumented concurrency ceiling: keep in-flight requests per
|
||||
mailbox low (single digits) and the batch size at 25 to 50.
|
||||
- Server search is slow-ish and eventually consistent: a message you just labelled may
|
||||
not match `q=label:x` for a few seconds. Local FTS is the fast path.
|
||||
- Header decoding: RFC 2047 encoded-words (`=?UTF-8?B?...?=`) in From, Subject and
|
||||
filenames, RFC 2231 parameter continuations in `Content-Disposition`, and
|
||||
charset-labelled bodies (`iso-8859-1`, `windows-1252`, `gb2312`) all show up daily.
|
||||
Use a real MIME parser on `raw` rather than trusting `payload` for anything beyond
|
||||
headers.
|
||||
- HTML mail in a webview: sanitise (strip scripts, forms, `<meta http-equiv>`,
|
||||
external CSS, `javascript:` URLs), isolate in an iframe or Tauri child webview with
|
||||
CSP, block remote images by default, rewrite `cid:` to local resources, and expect
|
||||
broken layouts with dark mode. Practical dark-mode approach is per-message opt-in
|
||||
inversion or a light-background iframe; automatic colour inversion breaks a lot of
|
||||
newsletters.
|
||||
- Newsletter and bulk detection: `List-Id` (RFC 2919), `List-Unsubscribe`,
|
||||
`Precedence: bulk|list`, `Auto-Submitted`, `X-Mailer` and a `CATEGORY_PROMOTIONS`
|
||||
or `CATEGORY_UPDATES` label together give a reliable "not a human" signal for the
|
||||
screener's default bucket.
|
||||
- Authentication results are only in raw headers. Fetch `Authentication-Results` via
|
||||
`metadataHeaders` and parse `spf=`, `dkim=`, `dmarc=` from the `mx.google.com` stanza.
|
||||
There is no API field for spam score or phishing verdict; `SPAM` label is all you get.
|
||||
- Gmail's classifier and your screener will disagree. A first-time human sender in
|
||||
Promotions is common (Gmail keys on content, you key on sender). Make the screener
|
||||
decision sender-based and use categories only to pick the default bucket.
|
||||
- Threading edge cases: recipients and senders can see different threads for the same
|
||||
messages; a changed subject starts a new thread; Gmail web splits at 100 messages;
|
||||
mailing-list software that rewrites `Message-ID` or strips `References` produces
|
||||
orphan threads; a reply sent without `threadId` lands in a new thread even with
|
||||
correct headers.
|
||||
- Snoozed-in-Gmail messages that Margin archives can reappear in Gmail web's Inbox
|
||||
(server bug, Mimestream help page). Leave Gmail-snoozed mail alone.
|
||||
- Password change revokes every refresh token with Gmail scopes. Expect
|
||||
`invalid_grant` and re-prompt.
|
||||
- Testing-mode tokens die after 7 days. If someone reports "it logs me out weekly", the
|
||||
consent screen is still in Testing.
|
||||
- `resultSizeEstimate`, `messagesTotal` and label counts are estimates and lag. Never
|
||||
drive UI badges from them; count locally.
|
||||
- People API sync tokens expire after 7 days and "The first page of a full sync request
|
||||
has an additional quota" that returns 429 if hammered; do one full contacts sync per
|
||||
install, then incremental.
|
||||
- Sending limits are per account across all clients: 500 per day consumer. A bulk
|
||||
"unsubscribe from 200 lists via mailto" action can burn a meaningful chunk of it.
|
||||
|
||||
Sources: https://developers.google.com/workspace/gmail/api/guides/handle-errors,
|
||||
https://developers.google.com/workspace/gmail/api/reference/quota,
|
||||
https://developers.google.com/workspace/gmail/api/guides/sync,
|
||||
https://developers.google.com/workspace/gmail/api/guides/batch,
|
||||
https://developers.google.com/workspace/gmail/api/guides/threads,
|
||||
https://mimestream.com/help/common-issues/archiving-snoozed-messages,
|
||||
https://developers.google.com/identity/protocols/oauth2,
|
||||
https://developers.google.com/people/api/rest/v1/otherContacts/list.
|
||||
|
||||
## 7. Recommendation
|
||||
|
||||
REST is the only transport for v1. IMAP comes later, if at all, for two narrow jobs:
|
||||
bulk backfill when measured REST sync time is unacceptable, and `IDLE` as a wake-up
|
||||
signal. `history.list` stays the single change log; IMAP has no equivalent.
|
||||
|
||||
Request `gmail.modify`, `gmail.settings.basic`, `contacts.other.readonly`,
|
||||
`contacts.readonly` and `calendar.events`; no `https://mail.google.com/` until IMAP is
|
||||
real. Push the consent screen to production unverified for the friends release (100
|
||||
lifetime users, stable tokens) and file restricted-scope verification now, stating
|
||||
there is no server and asking in writing whether the assessment applies.
|
||||
|
||||
Everything lives in SQLite on the device: message and thread mirror, bodies with an
|
||||
LRU, FTS5 index, per-account history cursor, and an outbox with `send_at` and
|
||||
`hold_until` (send later and undo send are one mechanism). Poll `history.list` every
|
||||
10 to 15 seconds in the foreground. No server, no push; say so in the product.
|
||||
|
||||
Mirror every pile as a real label under one namespace so Gmail web stays coherent:
|
||||
`Margin/Screened out`, `Margin/Feed`, `Margin/Paper trail`, `Margin/Reply later`,
|
||||
`Margin/Set aside`, `Margin/Snoozed`, `Margin/Muted`. All but Reply later also remove
|
||||
`INBOX`, so Gmail web's inbox matches Margin's and the rest is findable by label. For
|
||||
senders the user has explicitly routed, also create a `from:` filter so routing works
|
||||
while Margin is closed; stay well under the 1,000 filter cap.
|
||||
|
||||
What cannot live in Gmail, and so does not roam to another device or to Gmail web
|
||||
until Margin ships its own sync: sticky notes, thread renames and merges, snooze wake
|
||||
times, scheduled send times, the allow-list and per-sender rules (labels show the
|
||||
outcome, not the rule), snippets. A second machine sees the labels, not the rules,
|
||||
and a snooze set on the laptop will not wake the desktop. Call this "single device of
|
||||
record" in the onboarding copy rather than discovering it in support.
|
||||
|
||||
Skip open tracking: it needs a server, contradicts the pixel blocking, and invites
|
||||
scrutiny during review. Design against these numbers: 6,000 units per minute per
|
||||
user, 20 units per metadata fetch, 100 per send, 35 MB per message, 500 sends per
|
||||
day, 1,000 filters, 10,000 labels, 500 ids per page, 1,000 ids per `batchModify`, 100
|
||||
calls per batch, history validity that can drop to hours, and no API for snooze,
|
||||
schedule send, undo send, mute, rename, notes, or push without a server.
|
||||
@@ -0,0 +1,157 @@
|
||||
# HEY overview video
|
||||
|
||||
Notes on `overview.mp4`, HEY's own two and a half minute "So how does HEY work?" walkthrough
|
||||
(1920 by 1080, 148 seconds). The frames in `screenshots/hey-video/` were pulled from it at the
|
||||
moments below. The narration is transcribed in full at the end because it is the tightest
|
||||
statement of the workflow anywhere, and the order it presents things in is the order a new user
|
||||
meets them.
|
||||
|
||||
## What the video shows, in order
|
||||
|
||||
**The Imbox** (0:06 to 0:20). A single centred column with a big serif-less "Imbox" title. Two
|
||||
groups: New for you at the top, Previously seen below, each row an avatar, subject, sender plus
|
||||
snippet, and a date. A mint pill in the top left reads "Screen 5 first-time senders". There is no
|
||||
sidebar, no count, no unread badge.
|
||||
|
||||

|
||||
|
||||
**The Screener** (0:22 to 0:33). A page titled The Screener with the line "The people below are
|
||||
trying to email you for the first time. You get to decide if you want to hear from them." One row
|
||||
per sender with a thumbs-up Yes and a thumbs-down No on the left. "Clear all" at the right.
|
||||
|
||||

|
||||
|
||||
**Reply Later** (0:36 to 0:50). Select rows by clicking their avatars, and a floating indigo
|
||||
action bar appears with Reply Later, Set Aside, Mark Seen, Move to, Reply Together, Workflow.
|
||||
After Reply Later the threads leave the list and stack in a pile at the bottom left; clicking the
|
||||
pile fans it out with a "Go to Focus & Reply" button.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
**Focus & Reply** (0:50 to 0:55). A page that lines up every Reply Later thread with the latest
|
||||
message on the left and a reply box on the right. "Send email" per item.
|
||||
|
||||

|
||||
|
||||
**Set Aside** (0:58 to 1:05). The full bulk action bar: Reply Later, Set Aside, Mark Seen, Reply
|
||||
Together, Bubble Up Later, Label, Collection, Workflow, Merge, Ignore, Trash. Set Aside builds a
|
||||
second pile at the bottom right.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
**The HEY menu** (1:08 to 1:10 and 1:27). Pressing H opens a command-palette style overlay: a
|
||||
"Type to go to a person, place, or label" field, six tiles for Imbox, The Feed, Paper Trail, Reply
|
||||
Later, Set Aside and Bubble Up with their number keys, then Your: Drafts, Sent, Contacts, HEY
|
||||
World, Clips, Snippets, Screened Out, All Files, Spam, Trash, Everything, then labels.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
**The Feed** (1:11 to 1:17). Newsletters rendered already open in a scrolling column. The narration:
|
||||
"send all your newsletters, then view them in a scrollable feed, even open them within it."
|
||||
|
||||

|
||||
|
||||
**The Paper Trail** (1:18 to 1:24). A flat list with brand avatars, subtitle "The place for
|
||||
receipts, confirmations, and other transactional emails you receive." Opening one shows the
|
||||
thread page with a bottom action bar: Reply Now, Reply Later, Set Aside, Bubble Up, More.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
**All Files** (1:30 to 1:35). A grid of every attachment with a filter by type: Images, PDFs,
|
||||
Calendar invites, Documents, Spreadsheets, Presentations, Media, Zip files, and "sent by everyone"
|
||||
to filter by person.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
**Ignore, Bubble Up, Workflows, notifications** (1:36 to 1:55). Ignore drops you off a long thread
|
||||
silently. Bubble Up schedules a thread to return to the top of the Imbox on a chosen day.
|
||||
Workflows is a kanban board of threads. The thread options menu holds Mark Unseen, Send me push
|
||||
notifications, Ignore this thread, Add a note to self, Print this thread.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
**Clips, Rename, Notes, Read Together** (1:56 to 2:19). Select text in a message and "Save clip";
|
||||
the Clips library lists every clip with its sender and thread. Click a subject to rename it for
|
||||
yourself only ("people outside of HEY won't know about the new name"). Add a sticky note under an
|
||||
Imbox row. Select several threads and Read Together puts them on one page.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
**The closing list** (2:20). Email address forwarding, reply to multiple emails at once, private
|
||||
notes to self, combine multiple threads on one page, share emails via a link, no spy tracking, HEY
|
||||
for domains, start a blog.
|
||||
|
||||

|
||||
|
||||
## What to take from it
|
||||
|
||||
The video sells three things, and it sells them in this order for a reason: consent (the
|
||||
Screener), separation (Imbox, Feed, Paper Trail), and workflow (Reply Later, Set Aside, Focus &
|
||||
Reply). Everything after that is garnish. The two piles are the single strongest visual idea, and
|
||||
the action bar printing its key letters is the cheapest way to teach a keyboard.
|
||||
|
||||
The video also shows what HEY does not have: no reading pane, no way to see a list and a thread at
|
||||
once, and a lot of round trips between pages. That is the part we are not copying.
|
||||
|
||||
## Transcript
|
||||
|
||||
So how does HEY work? Well, let's jump in. First, you get a fresh start with a new email address
|
||||
at hey.com. Once you log in, you're taken to your Imbox. And no, that's not a typo. Im stands for
|
||||
important. You have the newest emails at the top, and everything you've already read below that.
|
||||
|
||||
Now, the first time anybody emails your HEY address, their email gets sent to the Screener, which
|
||||
tells you how many people have emailed you for the first time. It's kind of like screening your
|
||||
calls. If you don't want to hear from someone, hit no. And you never have to hear from them again.
|
||||
If you do, hit yes.
|
||||
|
||||
Now back in your Imbox, let's say you want to respond to a few emails, but don't have time right
|
||||
now. Just click them, then hit Reply Later. Now those emails are organised neatly at the bottom of
|
||||
your Imbox. You can click on one to reply to it now, or go to Focus & Reply, where it lines them
|
||||
all up for you to reply to on one page. So you can knock them out one by one, distraction free.
|
||||
|
||||
Now, sometimes you have an email you want to access quickly without digging for it. Again, just
|
||||
click the email, but this time hit Set Aside. Now there's a different pile at the bottom of your
|
||||
screen for whatever you need to access quickly.
|
||||
|
||||
In addition to your Imbox, you also have the Feed, which is where you can send all your
|
||||
newsletters, then view them in a scrollable feed, even open them within it. You also have the
|
||||
Paper Trail, which is where your transactions, receipts, and shipping notices go. You just don't
|
||||
need to see them all the time.
|
||||
|
||||
And there's much more you can do in HEY. Need a file? Go to All Files to see all your files
|
||||
without having to dig through any emails. You can even filter them by file type or person. Want to
|
||||
get off a long email thread? Hit Ignore instead of asking someone to remove you. Don't want to
|
||||
forget an email? Hit Bubble Up, then schedule it to shoot to the top of your Imbox for whatever day
|
||||
you choose. Have a project with lots of steps? Use Workflows to organise emails on a kanban board.
|
||||
Don't want to miss an important email? Turn on notifications for a single thread. Need to grab a
|
||||
small detail later? Just clip it, then reference it in your Clips library instead of searching for
|
||||
it in an email. Want to change a subject line? Just rename it without messing it up for anyone
|
||||
else. Want to add a note on an email in your Imbox? Throw up a sticky note. Want to read emails
|
||||
quickly? Read it all together and you can read them all at once on one screen.
|
||||
|
||||
And it also does all these things, which is a lot. So maybe just give it a whirl yourself and
|
||||
unlock the magic of HEY at your own pace.
|
||||
@@ -0,0 +1,568 @@
|
||||
# HEY (hey.com) feature dossier
|
||||
|
||||
Research notes on HEY, the email and calendar service from 37signals, written as a reference for designing a calm, keyboard-first, Gmail-backed desktop client. Everything below was checked against hey.com, help.hey.com, 37signals' own writing and podcasts, and third-party reviews between the 2020 launch and September 2026. Where a claim could not be confirmed it is marked "unverified". Screenshots live in `screenshots/hey/` and are mostly frames from HEY's own product demo videos, plus a few images from the hey.com changelog (`/new/`) where those showed UI the videos did not.
|
||||
|
||||
## 1. Positioning
|
||||
|
||||
HEY launched on 15 June 2020 as a hosted email service, not an app on top of Gmail. Signing up gives you a new `@hey.com` address, and the service only works through HEY's own web app and native apps (web, Mac, Windows, Linux, iOS, Android, plus a PWA, a CLI and a terminal UI added in 2026). There is no IMAP or POP, you cannot read HEY mail in another client, and you cannot use HEY to check another account: the only inbound path from Gmail or iCloud is forwarding, and the only outbound path for another address is "send as" over SMTP or Google/Outlook OAuth. 37signals calls this "A Platform, Not a Client" and says the redesign was only possible because they own both ends.
|
||||
|
||||
The pitch, in HEY's own words, is "email as it should be": a set of opinions rather than a set of settings. The manifesto page (`/the-hey-way/`) lists fifteen principles. The ones that shape the product most:
|
||||
|
||||
- "No consent, no attention": nobody lands in your Imbox until you have said yes to them once in The Screener.
|
||||
- "Get less email": reduce volume at the source (senders) and separate essential from secondary (three boxes) rather than helping you move faster through a pile.
|
||||
- "Workflows, not workarounds": Reply Later, Set Aside and Bubble Up replace marking-as-unread and starring.
|
||||
- "Convention over configuration" and "HI not AI": no rules engine, no regex filters, no algorithmic sorting. You tell HEY where a sender goes and it obeys.
|
||||
- "Just say flow": no archive, no inbox zero. Read mail drops into Previously Seen and time pushes it down.
|
||||
- "Surface, don't dig": attachments, clips and notes are pulled out to the top rather than buried in threads.
|
||||
- "No bother": push notifications off by default, opted in per contact or per thread; no unread badges or counts anywhere ("Countless").
|
||||
- "Counterintelligence": spy pixels stripped and reported; images proxied so your IP never leaks.
|
||||
- "Pay with money, not privacy": flat fee, no ads, no data mining.
|
||||
|
||||
Jason Fried's framing in interviews is that "you cannot fix the email problem until you have control over who can email you", and that "spam is not the problem with email anymore"; the problem is legitimate but unwanted mail. DHH's framing is that inbox zero is "tyranny", because the archive button turned every email into an obligation.
|
||||
|
||||
Pricing and plans (September 2026):
|
||||
|
||||
| Plan | Price | Notes |
|
||||
|---|---|---|
|
||||
| HEY for You | $99/year (billed annually only) | One @hey.com address, HEY Calendar, 100 GB, HEY World blog, all features. 30-day free trial with no credit card; the trial is capped at 500 emails/day and excludes HEY World and public share links. |
|
||||
| Ultra-short addresses | $349/year for 3-character, $999/year for 2-character | Vanity addresses only. |
|
||||
| HEY for Families | $179/year total | Up to five separate @hey.com accounts under one payer. |
|
||||
| HEY for Domains | $12/user/month, first user $10/month, billed monthly | Your own domain, no @hey.com address, no HEY World. Adds multi-user admin, Extensions (sales@, support@ group addresses that can deliver to several people or forward out), Shared Threads with private comments, shared Collections and Workflows, a Catch-all box, account managers. No free trial; you get 30 days to complete setup. |
|
||||
|
||||
If you pay for one year, the @hey.com address is yours forever and can forward out even after cancelling; trial-only addresses are recycled after 90 days. Export is MBOX for mail, vCard for contacts, .ics for calendars. Import of old mail is refused on principle ("a fresh start is a blessing, not a curse"); you import contacts (vCard) and forward new mail.
|
||||
|
||||
Sources: https://www.hey.com/, https://www.hey.com/the-hey-way/, https://www.hey.com/pricing/, https://www.hey.com/domains/, https://www.hey.com/faqs/, https://help.hey.com/article/754-can-we-import-our-old-emails, https://help.hey.com/article/719-can-i-use-hey-to-check-my-other-email-accounts, https://help.hey.com/article/882-what-can-i-do-on-the-free-trial, https://techcrunch.com/2020/07/06/oh-hey/, https://37signals.com/podcast/hey-whats-going-on/
|
||||
|
||||
## 2. The workflow, end to end
|
||||
|
||||
### A new sender arrives
|
||||
|
||||
Every inbound message first passes HEY's spam filter. If it survives and the sender has never emailed you before (and is not in your contacts), it does not reach any box. It lands in The Screener, and the Imbox shows a small mint pill in the top-left corner, "Screen 3 first-time senders". The Screener itself is a page with a row per sender: avatar, name, address, subject and a snippet, with two buttons on the left, a thumbs-up "Yes" and a thumbs-down "No". Clicking the row expands the message inline so you can read it (with recipients and timestamp) before deciding, and offers "Screen in to Imbox & Reply" for the case where you want to answer immediately.
|
||||
|
||||
"Yes" screens the sender in. The default destination is the Imbox, but the Yes button has a chevron that lets you pick The Feed or the Paper Trail instead, and that choice becomes the permanent delivery rule for that sender. "No" screens them out silently: nothing is sent back, the sender cannot tell, and all future mail from them goes to a Screened Out box that auto-deletes after 90 days. The decision is per sender, not per message, and can be reversed later from Screener History or the contact page (re-screening someone in reveals whatever they sent in the last 90 days). Contacts you add or import are pre-approved and skip the Screener. If the Screener piles up, "Clear all" punts everything without decisions.
|
||||
|
||||

|
||||
The Screener. Each row is a first-time sender; Yes has a dropdown for choosing which box they go to.
|
||||
|
||||

|
||||
Clicking a row expands the message inline with recipients and timestamp, plus a "Screen in to Imbox & Reply" shortcut.
|
||||
|
||||
### Where screened-in mail goes
|
||||
|
||||
Every approved sender has exactly one delivery destination, changeable at any time from their contact page ("Delivering to..."), and changing it moves their existing mail too:
|
||||
|
||||
- The Imbox (the "im" is for important) is for people and services you want to hear from now. It is the only box with a read/unread concept.
|
||||
- The Feed is for newsletters, promotions and long reads. It renders every email already open in a single scrolling column, newest first, like a social feed. No unread state, no obligation; items are recycled after 90 days by default.
|
||||
- The Paper Trail is for receipts, confirmations and notifications. A plain chronological list with no unread state, "a digital shoebox". Senders that flood you can be bundled into one row here or in the Imbox.
|
||||
|
||||
One rule overrides the sender setting: any message with In-Reply-To or References headers (a reply, including automated "reply" notifications from Basecamp or GitHub) goes to the Imbox so you never miss a reply. HEY's advice for noisy services is "Imbox, bundled".
|
||||
|
||||

|
||||
The Feed. Every newsletter is already expanded; you scroll, and "See more" or "See less" toggles the long ones.
|
||||
|
||||

|
||||
The Paper Trail: receipts and transactional mail in a flat list with brand avatars and no unread dots.
|
||||
|
||||

|
||||
The per-contact delivery menu. Every sender has one destination and an optional "Bundled up" display mode.
|
||||
|
||||
### Triage in the Imbox
|
||||
|
||||
The Imbox is a single centred column. New For You is at the top: threads you have not opened, each with an orange dot, avatar, subject in bold, sender plus snippet underneath, date on the right. Previously Seen sits below in a slightly tinted band: everything you have read or sent, newest first, pushed down by time. A thread jumps back into New For You whenever a new reply arrives and drops back to Previously Seen once you look at it. You can "Mark Seen" without opening (from the avatar action menu), hover the New For You heading for "Mark all as seen", or press "Power Through New" to see every unread message on one page with inline reply boxes and quick actions. There is no archive: HEY's help centre answer to "Can I archive emails?" is that read mail goes to Previously Seen, and if you do not want to see it, put Cover Art over it or set the contact to recycle.
|
||||
|
||||
At the bottom of the screen two piles are always visible: Reply Later on the left, Set Aside on the right, each drawn as a small stack of cards with the top item's subject and sender. Clicking a pile fans it out; Reply Later's fan ends in a "Go to Focus & Reply" button, Set Aside's in "View the Set Aside Board". Reply Later is for things you owe a response to, Set Aside for things you need to reference (tickets, itineraries, links). Neither uses a flag icon in the list; the thread physically moves out of the list into the pile.
|
||||
|
||||
Focus & Reply is a dedicated page listing every Reply Later thread, each with the latest message on the left and a reply box on the right. Sending removes the thread from the queue and leaves a green "Sent!" banner in its place; you can do five of twelve and come back.
|
||||
|
||||
Bubble Up is HEY's snooze: pick "Later today", "Tomorrow", "This weekend", "Next week", "Surprise me", "Pick a date" or "If no reply by...", and the thread leaves the list and reappears in a Bubbled Up section above New For You at that time. It stays there until you "Pop" it, and a "Bubble Up Now" option pins something to the top immediately.
|
||||
|
||||
Bulk selection is by clicking the circle avatar on each row (or `x` on the keyboard); an action menu pops up with Reply Later, Set Aside, Mark Unseen, Move to Paper Trail, Move to The Feed, File in label, Add a Note, Read Together, Reply Together, Merge, Ignore, Trash.
|
||||
|
||||

|
||||
The Imbox: New For You on top, Previously Seen below. Note the absence of any count.
|
||||
|
||||

|
||||
The two piles at the foot of the Imbox, and the "Screen N first-time senders" pill top left.
|
||||
|
||||

|
||||
The Reply Later pile fanned open, with the "Go to Focus & Reply" button.
|
||||
|
||||

|
||||
The Set Aside pile fanned open, with "View the Set Aside Board".
|
||||
|
||||

|
||||
Focus & Reply: each Reply Later thread with its own reply box; sent ones collapse to a "Sent!" line.
|
||||
|
||||

|
||||
Bubble Up options, and the thread action bar with its single-letter key hints (R, L, A, Z, M).
|
||||
|
||||

|
||||
Power Through New: all unread mail on one page with inline replies and quick actions.
|
||||
|
||||
### Reading a thread
|
||||
|
||||
There is no reading pane. Opening a thread navigates to a full page: participant avatars and a large centred subject at the top, then each message as a white card (sender, recipient, date, body, attachments as thumbnails), and a floating action bar at the bottom with Reply Now, Reply Later, Set Aside, Bubble Up and More. Each message has its own "•••" for view original, print, download and report spam. The More menu holds thread-level actions: notification options (send me push notifications, ignore this thread), add a note to self, get a public link, start another thread, forward, label, move (including redeliver to another linked account), trash, add to Collection or Workflow, don't automatically recycle.
|
||||
|
||||
From here you can clip any selected text into a Clips Library, add private notes (text or files) between messages, rename the subject for yourself only, and merge another thread into this one. A spy-tracker banner at the top of the page tells you which vendor's pixel was stripped.
|
||||
|
||||

|
||||
A thread page. Big centred subject, message cards, floating action bar with key hints.
|
||||
|
||||
### Sending
|
||||
|
||||
Compose opens as a full "New Message" sheet (or a separate window with Shift+W): To, CC/BCC, Subject, body, then a row of "Send email", "Save draft", a clock (Send Later), a paperclip and a formatting toggle. Replies open inline under the message. Reply-all is the default when there are multiple recipients; you switch to sender-only from the To field. Drafts autosave and can be minimised to a dock at the bottom of the screen. Send Later exists (arrow next to Send email, choose day and time; scheduled mail waits in Drafts). Undo Send exists (banner after sending, or `q`; enabled by default for accounts created after December 2022, toggle in Accounts & Settings). Signatures are replaced by Name Tags: 150 characters of text, bold/italic/link only, no images. Big attachments are sent as permanent download links with no download tracking. Special send variants exist for the piles: "Send & Mark Done" from Set Aside, "Now and Pop" from Bubble Up, and "Reply to Everyone" to send one reply to several selected threads.
|
||||
|
||||

|
||||
New Message in its own window: To, CC/BCC, Subject, Send email, Save draft, Send Later clock, attach, format.
|
||||
|
||||

|
||||
Send Later: a day and time picker; scheduled emails are held in Drafts.
|
||||
|
||||
Sources: https://www.hey.com/how-it-works/, https://www.hey.com/flow/, https://help.hey.com/article/722-the-screener, https://help.hey.com/article/759-imbox, https://help.hey.com/article/840-why-are-these-notifications-going-to-my-imbox-instead-of-the-feed-paper-trail, https://help.hey.com/article/892-email-threads, https://help.hey.com/article/859-can-i-archive-emails, https://help.hey.com/article/820-how-do-i-undo-sending-a-message, https://help.hey.com/article/821-how-do-i-send-an-email-later, https://help.hey.com/article/807-reply-to-sender-or-reply-all
|
||||
|
||||
## 3. Feature catalogue
|
||||
|
||||
### The Screener
|
||||
|
||||
What it does: holds mail from any sender who has never emailed you (and is not in Contacts) until you say Yes or No. Runs after the spam filter. Yes defaults to Imbox; the Yes chevron chooses The Feed or Paper Trail. No is silent and permanent until you reverse it. Details and rules:
|
||||
|
||||
- Screener History (avatar menu, Accounts & Settings) lists everyone screened out and lets you screen them back in; doing so reveals their mail from the last 90 days.
|
||||
- Screen out an existing contact from their page: Delivering To, then Screened Out.
|
||||
- Domain screening: open a message, click the sender name, then the @domain button, and choose auto-screen in to Imbox, auto-screen out, or decide per sender. Large consumer domains such as gmail.com cannot be screened out as a whole.
|
||||
- Imported or manually added contacts bypass the Screener.
|
||||
- Speakeasy code: a secret word (key icon at the top right of the Screener) that, when it appears as a standalone word in a subject line, bypasses the Screener and marks the mail as special in the Imbox. Regenerate any time.
|
||||
- HEY Spam Corps: opt in to get a third "Spam" button in the Screener; marking spam also screens out.
|
||||
- "Clear all" empties the Screener without decisions. Screened Out and Spam are deleted after 90 days.
|
||||
- Easter egg: Shift-click on No turns the thumbs-down into a middle finger (the "F*#k No" feature from the REWORK podcast).
|
||||
- Forwarding out of HEY bypasses the Screener entirely.
|
||||
|
||||
Source: https://help.hey.com/article/722-the-screener, https://www.hey.com/features/the-screener/, https://help.hey.com/article/773-the-speakeasy-code, https://help.hey.com/article/889-spam-corps, https://37signals.com/podcast/the-f-k-no-feature/
|
||||
|
||||
### The Imbox
|
||||
|
||||
The only box with a read state. Two automatic groups, New For You and Previously Seen, plus a Bubbled Up group above them when anything is bubbled. Sent mail lands in Previously Seen. A new reply pulls a thread back to New For You. Actions: Mark Seen/Unseen from the avatar menu, Mark all as seen on the New For You heading, Power Through New, Read Together (link at the right of the New For You heading, or select rows). Bundles collapse all mail from one sender into a single row (Imbox and Paper Trail only; clicking a bundle with new mail shows all the new messages on one page). Cover Art can hide Previously Seen. There is no archive and no folders; labels are the closest thing.
|
||||
|
||||

|
||||
The HEY menu, opened from the logo or `h`: a filter box, the six boxes as tiles with their number keys, labels, then Screened Out, Spam, Trash and Everything.
|
||||
|
||||
Source: https://help.hey.com/article/759-imbox, https://help.hey.com/article/784-different-states, https://help.hey.com/article/930-mark-all-as-seen, https://help.hey.com/article/765-bundle-emails
|
||||
|
||||
### The Feed
|
||||
|
||||
Newsletter reader. Each item is rendered expanded in a card with a large title, sender line and the full HTML; long items truncate with "See more..." and can be collapsed with "See less". Newest at top. No read state and no bulk unread count; HEY remembers "where you left off" and the HEY menu flags "new since you last visited". Actions (forward, move, label) come from the circle avatar on the item. Routing to the Feed is per sender only, never per domain. Feed mail is recycled after 90 days by default. Clips can be saved from Feed text (coupon codes are the stated use case). The Feed does not support bundles.
|
||||
|
||||
Source: https://help.hey.com/article/761-the-feed, https://www.hey.com/features/the-feed/, https://www.hey.com/new/
|
||||
|
||||
### The Paper Trail
|
||||
|
||||
Flat list for receipts and transactional mail. No read state. Supports bundles per sender. You cannot turn recycling on for the whole box, but per-contact recycling still applies to mail that lives there. Android can keep the last 30 days offline.
|
||||
|
||||
Source: https://help.hey.com/article/787-paper-trail, https://www.hey.com/features/paper-trail/
|
||||
|
||||
### Reply Later
|
||||
|
||||
Button in the thread action bar (`l`) or bulk menu. Moves the thread out of the list into the Reply Later pile at the bottom left; the pile fans open on click; a dedicated Reply Later box is reachable with `4`. Take a thread out by toggling the button again; it returns to the Imbox. There is no due date on Reply Later; Bubble Up's "If no reply by" covers that case.
|
||||
|
||||
Source: https://help.hey.com/article/774-reply-later, https://www.hey.com/features/reply-later/
|
||||
|
||||
### Set Aside
|
||||
|
||||
Button (`a`). Same pile mechanic on the bottom right, plus a Set Aside Board that previews every set-aside thread as a card on one screen. Inside the Set Aside box you can drag threads into named groups at the top ("bills to pay", "trip"). Remove with "Done" (`i` in the pile), or "Mark all as Done". Replying from Set Aside offers "Send & Mark Done".
|
||||
|
||||
Source: https://help.hey.com/article/777-set-aside, https://www.hey.com/features/set-aside/, https://www.hey.com/new/
|
||||
|
||||
### Focus & Reply
|
||||
|
||||
Page listing every Reply Later thread with a reply box beside each. Replying removes the item and shows a "Sent!" line; unanswered items stay for next time. Fried: "Focus and reply takes you out of that loop completely".
|
||||
|
||||
Source: https://help.hey.com/article/764-focus-and-reply, https://www.hey.com/features/reply-mode/
|
||||
|
||||
### Bubble Up
|
||||
|
||||
HEY's snooze (`z`). Presets: Later today, Tomorrow, This weekend, Next week, Surprise me, Pick a date, If no reply by..., and Bubble Up Now. Bubbled threads sit in a Bubbled Up section at the top of the Imbox until popped; a reply to a bubbled thread moves it to New For You; when replying you can choose "Now and Pop". Works from the bulk menu and from Power Through New. A Bubble Up box lists everything scheduled (`6`).
|
||||
|
||||
Source: https://help.hey.com/article/766-bubble-up, https://www.hey.com/new/
|
||||
|
||||
### Power Through New
|
||||
|
||||
Button on the New For You heading (`o`). Shows every unread thread on one page; reply inline, mark seen, Reply Later, Set Aside or Bubble Up per item; press `x` on an item for the full action menu; "Mark all as seen" at the bottom. Untouched items remain New For You.
|
||||
|
||||
Source: https://help.hey.com/article/923-power-through-new
|
||||
|
||||
### Read Together and Reply to Everyone
|
||||
|
||||
Select several Imbox rows and choose Read Together to open them all on one scrolling page (also the way to print several at once). Reply Together / Reply to Everyone sends one reply to every selected thread.
|
||||
|
||||
Source: https://help.hey.com/article/786-read-together, https://www.hey.com/features/reply-to-everyone/
|
||||
|
||||
### Clips
|
||||
|
||||
Select text in any message (Imbox or Feed), a "Save clip" button appears. Clips go to a Clips Library (HEY menu, Clips; `app.hey.com/clips`) showing the clipped text, sender and thread. Text only, any length, unlimited, synced across devices; deleting a clip does not touch the email; clipped threads are exempt from recycling.
|
||||
|
||||

|
||||
The Clips Library: highlighted passages with their sender and thread.
|
||||
|
||||
Source: https://help.hey.com/article/771-clips
|
||||
|
||||
### Collections
|
||||
|
||||
A named page that gathers several threads (with their own notes and a Recent Files strip) without merging them. Add from the thread's More menu or the bulk menu (`n`). Originally Domains-only, now on all accounts. Sharing a Collection with teammates is Domains-only; Collections can have notes and push notifications. Manual only, no auto-sorting.
|
||||
|
||||

|
||||
A Collection: three threads and their recent files on one page.
|
||||
|
||||
Source: https://help.hey.com/article/762-collections, https://help.hey.com/article/1006-collaboration-with-collections
|
||||
|
||||
### Cover Art
|
||||
|
||||
Picture icon at the top right of Previously Seen. Choose a preset or upload PNG/JPG/GIF (up to 40 MB, under 16K by 16K). The image slides over Previously Seen so the Imbox shows only new mail; tap to reveal. Stickies: a "+" at the top left of the cover adds yellow sticky notes (reminders, snippets, links) that live on the cover. Calendar cover art (web only) shows today and tomorrow, habits, "sometime this week" tasks, countdowns, and a join-call button 15 minutes before a meeting.
|
||||
|
||||

|
||||
Cover Art hiding Previously Seen; the Imbox above it is empty ("Nothing new for you").
|
||||
|
||||

|
||||
Stickies pinned to the cover art.
|
||||
|
||||
Source: https://help.hey.com/article/781-cover-art
|
||||
|
||||
### Merge threads
|
||||
|
||||
Select two or more threads, Merge (`g`). A dialog asks "What should we call the new thread?" (prefilled from the longer thread) and lists the threads being merged. Permanent, with a confirmation; the other party is unaffected and their replies to either original thread arrive in your merged thread.
|
||||
|
||||

|
||||
The Merge dialog: name the result, confirm the list.
|
||||
|
||||
Source: https://help.hey.com/article/780-merge-threads
|
||||
|
||||
### Rename subject
|
||||
|
||||
Click the subject on a thread page. A popover asks "What would you like to call this?", notes that people outside HEY will not see the new name, and shows the original. The rename is yours only and persists no matter what others do.
|
||||
|
||||

|
||||
Renaming a "no subject" thread for yourself.
|
||||
|
||||
Source: https://help.hey.com/article/783-rename-the-subject
|
||||
|
||||
### Sticky notes on Imbox rows
|
||||
|
||||
From the bulk menu, "Add a Note" (`y`). A yellow private note appears under the row in the Imbox only, not on the thread page.
|
||||
|
||||

|
||||
A private sticky note attached to an Imbox row.
|
||||
|
||||
Source: https://help.hey.com/article/775-sticky-notes
|
||||
|
||||
### Notes to self (thread notes)
|
||||
|
||||
More menu, "Add a note to self". A blue block inside the thread, dated, with text and files (drag in, or paste images). Private; unlimited; can sit between messages. On HEY for Domains the same mechanism becomes private comments visible to teammates on a shared thread.
|
||||
|
||||

|
||||
A private note between two messages in a thread.
|
||||
|
||||
Source: https://help.hey.com/article/782-note-to-self, https://www.hey.com/features/shared-threads/
|
||||
|
||||
### Workflows
|
||||
|
||||
Kanban boards. Create a workflow, add stages, drag threads between stages; the whole thread (replies, notes, files) travels with the card. Add a thread from its More menu. On Domains, workflows are shared with all users and Extensions can auto-add incoming mail to a workflow. HEY positions Workflows plus Contact Notes as its "light CRM".
|
||||
|
||||

|
||||
A Workflow: stages as columns, threads as cards.
|
||||
|
||||
Source: https://help.hey.com/article/767-workflows, https://help.hey.com/article/929-crm
|
||||
|
||||
### Labels
|
||||
|
||||
Tags, not folders. Apply from the bulk menu (`b`) or a contact page ("Autofile in..." labels all new mail from that sender). Plus-addressing `[email protected]` auto-labels if the label exists and the sender is screened in. Labels appear as small pills on the row and as entries in the HEY menu.
|
||||
|
||||
Source: https://help.hey.com/article/884-labels
|
||||
|
||||
### Contact pages and contact notes
|
||||
|
||||
Click any avatar to reach the contact page: avatar, name, address, then a pill row: Not notifying / Delivering to Imbox / Autofile in... / Set up recycling / Add a note. Below that a rich-text Notes area, a Recent Files strip, and every thread with that person plus a Write button. Contacts list shows only approved (screened-in) contacts; unapproved still appear in search. Import vCard, export vCard, merge contacts by adding a second address, Contact Groups for addressing many people by one name.
|
||||
|
||||

|
||||
A contact page with the "When X emails you..." choice: Don't notify me or Send a push notification.
|
||||
|
||||
Source: https://help.hey.com/article/885-contacts, https://help.hey.com/article/770-contact-notes, https://help.hey.com/article/788-contact-groups
|
||||
|
||||
### Recycling
|
||||
|
||||
Off by default except The Feed (90 days). Per contact or per domain, choose 30 days, 90 days or 2 years measured from the last message in the thread; recycled mail goes to Trash and is deleted 30 days later. Longest applicable period wins. Clipped threads are exempt, and any thread can be set "Don't automatically recycle".
|
||||
|
||||
Source: https://help.hey.com/article/805-recycling-center
|
||||
|
||||
### Spy pixel blocking
|
||||
|
||||
HEY strips known tracking pixels and anything that looks like one (1x1 images, hidden trackers), names the vendor in a purple banner at the top of the thread ("You're protected. We blocked a spy tracker in this thread", expandable to explain what Hubspot or Mailchimp would have learned), and proxies all remaining images through HEY's servers so the sender never sees your IP. HEY claims about 98% coverage and publishes the list of blocked vendors. Outgoing HEY mail never contains trackers, and big-file download links are not tracked.
|
||||
|
||||

|
||||
The spy tracker banner expanded, naming the vendor.
|
||||
|
||||
Source: https://www.hey.com/spy-trackers/, https://www.hey.com/features/spy-pixel-blocker/
|
||||
|
||||
### Ignore thread (mute) and unsubscribe
|
||||
|
||||
More menu, "Ignore this thread" (bulk `-`). Replies still arrive and are appended to the thread page, but the thread never returns to New For You; a yellow banner on the thread says "You ignored this thread" with "Stop ignoring". Deleting would not work because the next reply would resurrect the thread. HEY does not document any unsubscribe action; nothing on hey.com, in the changelog or in the help centre mentions one. The HEY answer to unwanted newsletters is to screen the sender out, route them to The Feed, or set them to recycle.
|
||||
|
||||

|
||||
An ignored thread, with the "Stop ignoring" control.
|
||||
|
||||
Source: https://help.hey.com/article/769-ignore-a-thread, https://www.hey.com/features/mute-thread/
|
||||
|
||||
### Notifications
|
||||
|
||||
Push notifications are off by default everywhere. Turn them on per contact (contact page, "Send a push notification") or per thread (More menu, "Send me push notifications"), or per Collection. HEY refuses to show icon badges or unread counts "by design". Android notifications carry Mark Seen, Reply Later and Set Aside actions.
|
||||
|
||||
Source: https://help.hey.com/article/772-notifications, https://www.hey.com/features/notifications/
|
||||
|
||||
### Attachments, All Files, big files
|
||||
|
||||
All Files (HEY menu) is a library of every attachment ever received, filterable by type (images, PDFs, calendar invites, documents, spreadsheets, presentations, media, zip) and by sender at the same time, each card showing the thread it came from as a link. Signature junk (logos, social icons) is excluded. Sent Mail has a Recent Files strip of everything you have sent. Download all attachments in a thread with two clicks. Big attachments are sent as permanent direct-download links to any recipient, untracked.
|
||||
|
||||

|
||||
All Files with the type filter open.
|
||||
|
||||
Source: https://help.hey.com/article/785-all-files, https://help.hey.com/article/768-sending-large-files
|
||||
|
||||
### Search
|
||||
|
||||
`s` or `/`. Results appear as you type (first seven), Cmd+Return or "View all results" opens the full page with a "Refine your results" rail: box (Imbox, The Feed, Paper Trail), also these words, none of these words, this exact phrase, from, to, subject, date, label, has attachment. Search covers Trash. Results support bulk actions. Mobile keeps a local search history. HEY says search became "up to 7x faster" in 2025, which is an admission that it used to be slow.
|
||||
|
||||

|
||||
Quick search dropdown as you type, with "View all results".
|
||||
|
||||
Source: https://help.hey.com/article/845-search, https://www.hey.com/new/
|
||||
|
||||
### Everything, Spam, Trash, Screened Out, Sent, Drafts
|
||||
|
||||
The HEY menu's "Other stuff" section: Screened Out (auto-deleted after 90 days), Spam (90 days), Trash (30 days), Everything (`app.hey.com/topics/everything`, every message across all boxes including spam and screened out). Sent Mail and Drafts are in the "Your" section. Drafts autosave; a draft can be minimised to a dock at the bottom of the window.
|
||||
|
||||
Source: https://help.hey.com/article/903-how-can-i-see-all-my-emails, https://help.hey.com/article/1014-empty-trash-spam-or-screened-out, https://help.hey.com/article/848-email-drafts
|
||||
|
||||
### Sorting and grouping in the Imbox
|
||||
|
||||
There are no sort options. Order within a group is strictly newest first. Grouping is fixed: Bubbled Up (if any), New For You, Previously Seen. The only other structure is bundles (per sender), labels (pills), and multi-account markers (a triangle for personal, a square for work when accounts of different types are linked).
|
||||
|
||||
Source: https://help.hey.com/article/784-different-states, https://www.hey.com/link-multiple-accounts/
|
||||
|
||||
### Signatures (Name Tags), Snippets, Autoresponder, forwarding, send-as
|
||||
|
||||
- Name Tag: a 150-character text-only signature (bold, italic, link) toggled in Edit Profile; one per address, including send-as addresses.
|
||||
- Snippets: saved blocks of text or whole emails, inserted while composing (HEY menu, Snippets).
|
||||
- Autoresponder: per account, with its own message; ignores forwarded mail, lists, bulk, auto-replies and spam; one auto-reply per contact per 7 days.
|
||||
- Forwarding in: Accounts & Settings, Forwarding & Sending, Connect an address; then set forwarding at Gmail or iCloud. Replies to forwarded mail can go from the HEY address or the external one.
|
||||
- Send as: SMTP with basic auth, or OAuth for Google and Outlook. Google Advanced Protection accounts cannot be used.
|
||||
- Forwarding out: everything non-spam, bypassing the Screener. Redeliver: route a specific sender's mail from one linked account to another.
|
||||
|
||||
Source: https://help.hey.com/article/744-name-tags, https://help.hey.com/article/795-snippets, https://help.hey.com/article/776-autoresponder, https://help.hey.com/article/1055-forwarding, https://help.hey.com/article/733-sending-with-a-non-hey-email, https://help.hey.com/article/1013-redeliver
|
||||
|
||||
### Sharing: public links, Shared Threads, Extensions
|
||||
|
||||
Any thread can get a public read-only link (paid accounts only) that shows the whole thread and future replies on a HEY-formatted page; link holders cannot reply. On Domains, threads can be shared with teammates who then see everything including future replies, with private comments in blue blocks. Extensions are group addresses (sales@) delivered to several people or forwarded to a help desk, with a choice of whether new members see history.
|
||||
|
||||
Source: https://help.hey.com/article/779-shareable-links, https://www.hey.com/features/shared-threads/, https://help.hey.com/article/819-extensions
|
||||
|
||||
### HEY Calendar
|
||||
|
||||
Included since 2024. Views: Day (a single vertical timeline "telling the continuous story of your life"), Week, Year (all-day and multi-day events only). Extras: "Sometime this week" undated tasks that roll forward, Habits, Time tracking, Journal (private daily notes), countdowns, named days, day background photos, circled days, collapsed Nighttime hours, colour-coded sub-calendars, multiple reminders, multi-timezone, sharing with HEY users, ICS import and feeds, Google/Apple/Outlook import, create an event from an email (auto-linked back), calendar search. Cannot print. Keyboard: `0` toggles between mail and calendar.
|
||||
|
||||

|
||||
Week view with the "Sometime this week" row of undated tasks at the bottom.
|
||||
|
||||
Source: https://www.hey.com/calendar/, https://help.hey.com/article/800-calendar-overview, https://help.hey.com/article/900-sometime-this-week, https://help.hey.com/article/837-calendar-day-features
|
||||
|
||||
### HEY World
|
||||
|
||||
Send an email to `[email protected]` (sole To recipient) from a paid HEY for You account and it is published at `world.hey.com/you/post-title`; readers subscribe by email or RSS; you can add a bio, pin posts, edit or delete posts, and export or import subscribers. Not available on trials or on HEY for Domains.
|
||||
|
||||
Source: https://www.hey.com/world/, https://help.hey.com/article/763-hey-world
|
||||
|
||||
### Mobile apps
|
||||
|
||||
iOS and Android apps for Email and Calendar (separate apps), described as full-featured. Layout is the same single column with the piles at the bottom; the HEY menu is reachable everywhere. Swipe left/right defaults to Seen/Unseen and is customisable to Reply Later or Set Aside. iOS has home screen widgets (New for you, The Feed, Paper Trail, Reply Later, Set Aside, Screener), Siri shortcuts, share sheet, iPad multi-window and keyboard navigation. Android has Material You calendar widgets, notification actions, device contacts, and offline Paper Trail. No app icon badges on either platform.
|
||||
|
||||

|
||||
HEY on iOS: The Feed and the Imbox with the piles at the bottom.
|
||||
|
||||
Source: https://www.hey.com/apps/, https://help.hey.com/article/847-widgets, https://www.hey.com/new/
|
||||
|
||||
### Multi-account, security, CLI
|
||||
|
||||
Link any number of HEY accounts and see them merged in one Imbox (with type markers) or one at a time. Mandatory TOTP two-factor for paying customers, WebAuthn keys supported, no SMS. A dark-mode toggle independent of the OS. In 2026 HEY shipped a CLI (`curl -fsSL https://hey.com/install-cli | bash`), a TUI (`hey tui`), and an MCP server (`hey mcp`, with a `--read-only` flag) so agents such as Claude Code can screen mail, clear Reply Later and draft replies.
|
||||
|
||||
Source: https://www.hey.com/features/multi-account/, https://www.hey.com/features/security/, https://www.hey.com/agents/, https://help.hey.com/article/1189-using-ai-agents-with-hey
|
||||
|
||||
## 4. Keyboard shortcuts
|
||||
|
||||
The model is single, unmodified mnemonic keys: no two-key sequences ("g then i") and only a handful of modifier chords (Cmd/Ctrl+J for the menu, Cmd/Ctrl+Return to send, Shift+. and Shift+W). Keys are contextual: `o` is Power Through New on the Imbox page but Read Together in the bulk menu; `r` is Reply Now on a thread but Reply Together in bulk; `i` is Move to Imbox in bulk and "Done" inside the Set Aside pile. Press `?` anywhere (or the keyboard icon bottom right) for the list. The bottom action bar on a thread prints the letter next to each button.
|
||||
|
||||
Navigation (anywhere):
|
||||
|
||||
| Key | Action |
|
||||
|---|---|
|
||||
| 1 | Imbox |
|
||||
| 2 | The Feed |
|
||||
| 3 | Paper Trail |
|
||||
| 4 | Reply Later |
|
||||
| 5 | Set Aside |
|
||||
| 6 | Bubble Up |
|
||||
| 9 | Previously Seen |
|
||||
| 0 | Toggle Calendar / Email |
|
||||
| s or / | Search |
|
||||
| h or Cmd/Ctrl+J | HEY menu |
|
||||
| ? | Help and shortcut list |
|
||||
|
||||
Imbox page:
|
||||
|
||||
| Key | Action |
|
||||
|---|---|
|
||||
| w | Write a new email |
|
||||
| Shift+W | Write in a new window (desktop browsers) |
|
||||
| o | Power Through New |
|
||||
| z | Bubbled Up |
|
||||
|
||||
On a thread ("message action shortcuts"):
|
||||
|
||||
| Key | Action |
|
||||
|---|---|
|
||||
| r | Reply Now |
|
||||
| l | Move to Reply Later |
|
||||
| a | Move to Set Aside |
|
||||
| z | Bubble Up |
|
||||
| f | Forward |
|
||||
| b | Label |
|
||||
| v | Move |
|
||||
| t | Trash |
|
||||
| m | More menu (shown on the action bar; the help page lists it for adding to a Collection) |
|
||||
| u | Mark Unseen (from the More menu) |
|
||||
| Shift+. (>) | Expand message previews ("See more") |
|
||||
| Cmd/Ctrl+Return | Send |
|
||||
| q | Undo send (immediately after sending) |
|
||||
| Cmd/Ctrl+B, I, K | Bold, italic, link while composing |
|
||||
|
||||
Bulk actions (Imbox, The Feed, Paper Trail, Set Aside, Reply Later, Bubble Up lists):
|
||||
|
||||
| Key | Action |
|
||||
|---|---|
|
||||
| x | Select row |
|
||||
| j / down | Next row |
|
||||
| k / up | Previous row |
|
||||
| Enter | Open thread |
|
||||
| ; | Focus the bulk actions menu |
|
||||
| l | Move to Reply Later |
|
||||
| a | Move to Set Aside |
|
||||
| z | Bubble Up |
|
||||
| u | Mark Unseen |
|
||||
| e | Mark Seen (listed in the help centre article, not on the marketing page) |
|
||||
| i | Move to Imbox (also "Done" inside the Set Aside pile) |
|
||||
| p | Move to Paper Trail |
|
||||
| d | Move to The Feed |
|
||||
| o | Read Together |
|
||||
| r | Reply Together |
|
||||
| b | Add to label |
|
||||
| n | Add to Collection |
|
||||
| y | Add a sticky |
|
||||
| g | Merge |
|
||||
| - | Ignore |
|
||||
| t | Trash |
|
||||
|
||||
Calendar:
|
||||
|
||||
| Key | Action |
|
||||
|---|---|
|
||||
| 0 | Back to Email |
|
||||
| t | Today |
|
||||
| d | Day view |
|
||||
| w | This week |
|
||||
| u | Week view |
|
||||
| y | Year view |
|
||||
| n | New event |
|
||||
| s or / | Search |
|
||||
| b | Habits |
|
||||
| l | Time tracking |
|
||||
| j | Journal |
|
||||
| k | Write in Journal (Day view) |
|
||||
| left / right | Previous / next day |
|
||||
|
||||
Pop-up menus: Enter or Space opens, up/down move, Esc clears typed text then closes on a second press, Tab closes and moves focus on.
|
||||
|
||||
Sources: https://www.hey.com/keyboard-shortcuts/, https://help.hey.com/article/758-keyboard-shortcuts, https://www.hey.com/new/ (Shift+W)
|
||||
|
||||
## 5. Interaction and UI design notes
|
||||
|
||||
Layout. One centred column of roughly 900px on a pale grey page, holding a white "sheet" with rounded corners and a soft shadow. Chrome is minimal: Search at top left, the HEY hand logo with a chevron in the centre (this opens the HEY menu), your avatar at top right, a "< Imbox" back pill when you are inside a page. There is no sidebar and no reading pane: the list is a page, the thread is a page, the contact is a page, and you navigate between them. The HEY menu is the only global navigation and behaves like a command palette: a "Type to go to a person, place, or label..." field over six large tiles (Imbox, The Feed, Paper Trail, Reply Later, Set Aside, Bubble Up) with their number keys printed in the corner, then labels, then Screened Out, Spam, Trash, Everything.
|
||||
|
||||
Page headers. Every page has a big centred title ("Imbox", "The Screener", "Paper Trail", "Focus & Reply") flanked by thin rules, and a one-line grey subtitle explaining the page ("The place for receipts, confirmations, and other transactional emails you receive."). Section headers inside a page are small uppercase labels with a rule ("NEW FOR YOU", "PREVIOUSLY SEEN", "WANT TO GET EMAILS FROM THEM?").
|
||||
|
||||
Rows. A row is: an orange dot for unread (Imbox only), a circular avatar (photo, brand logo, or two or three initials on a saturated colour), subject in black semibold, then on the second line the sender name, a short separator, then the snippet, all in grey, and the date or time right-aligned in grey. Group threads show a cluster of tiny avatars after the subject. Attachments show a paperclip glyph after the subject, labels show as small outlined pills. Rows are about 40px tall and there is no density option. Selecting a row is done by clicking the avatar, which turns into a checked circle, and the bulk menu slides in as a floating indigo panel.
|
||||
|
||||
The two piles. At the foot of the Imbox, two stacks of two or three overlapping white cards, each with an icon (a reply-clock for Reply Later, a pin for Set Aside), the top card showing subject and sender. Click to fan them up into a vertical stack of small cards ending in a button. It is a literal desk metaphor and the strongest single visual idea in the product.
|
||||
|
||||
Thread page. Participant avatars above a large centred subject; each message a white card with sender name in bold, address in grey, "to" line, date and "•••" at right; attachments as thumbnails with file name and size; quoted text collapsed behind a chevron. A floating pill-shaped action bar sits at the bottom centre with five icon-and-label buttons and their key letters. Notes to self are blue blocks; the spy tracker warning is a purple banner at the top.
|
||||
|
||||
Compose. New mail opens as a modal sheet (or its own window). Reply is an inline box under the message; in Focus & Reply the reply box sits to the right of the message. Buttons are pills: "Send email" filled indigo, "Save draft" outlined. Formatting is behind a toggle; there is a colour tool and code blocks but no markdown (a reviewer complaint).
|
||||
|
||||
Colour and type. White surfaces, #f5-ish grey page, HEY indigo/purple (around #5522fa) for primary actions and menus, mint/teal pills for status ("Screen 5 first-time senders", "Send email" in Focus & Reply, "Done"), orange for unread dots, yellow for stickies, saturated avatar colours. Menus and popovers are solid indigo-to-purple gradients with white text, which reads as playful rather than corporate. Type is the system sans (SF on Apple), with heavy weight for page titles and a fairly small body size in lists.
|
||||
|
||||
Illustration and voice. Hand-drawn blue arrows and squiggles in marketing and onboarding, the waving-hand logo, sparkles on the empty state ("Nothing new for you."), a keyboard glyph in the bottom corner for shortcuts. Copy is first person and cheeky ("Everyone else can put stuff in your inbox, but you can't. With HEY you can."). Reviewers are split: some call the oversized "Imbox" wordmark "corny".
|
||||
|
||||
Empty states. The Imbox with nothing new shows a small sparkle and "Nothing new for you." with Previously Seen (or cover art) below. Screened Out and Spam pages explain their retention ("These will be automatically deleted after 90 days") with an "Empty" button.
|
||||
|
||||
Feed rendering. Each newsletter is a card with a big bold title (the subject), sender and address, then the HTML email at full width inside the card, truncated after roughly one screen with "See more...", and "See less..." once expanded. There are no row previews in the Feed: the preview is the email.
|
||||
|
||||
Mobile. Same structure squeezed to one column: title, New For You, rows with avatar and two-line text, piles fixed at the bottom, a floating hand button for the HEY menu. Notifications and badges are absent unless opted in. Dark mode exists on all platforms but email bodies stay light (a common complaint).
|
||||
|
||||
Sources: the screenshots in this folder, https://www.hey.com/how-it-works/, https://www.hey.com/features/, https://hulry.com/hey-email-review/, https://danielcassman.com/posts/2020/08/29/hey-email/
|
||||
|
||||
## 6. What people praise and what they criticise
|
||||
|
||||
Praise:
|
||||
|
||||
- The Screener is the feature people name first. TapSmart: the reviewer's "favorite feature by far". Hulry: "Instead of reacting to spammy senders, I am proactively blocking them out."
|
||||
- The Feed lets people batch-read newsletters two or three times a day instead of one at a time.
|
||||
- Paper Trail and All Files: receipts out of the way, attachments findable without finding the email first.
|
||||
- Reply Later and Set Aside are described as "separation of concerns"; Hulry: "HEY is all about workflows, not workarounds."
|
||||
- Silence by default and the absence of unread counts genuinely lower anxiety for people who stick with it.
|
||||
- Privacy: tracker blocking is visible and on by default, and the flat fee removes the incentive to mine data.
|
||||
- Design: "Email is fun again" (Hulry); "the best email product I've ever used" (Daniel Cassman); Agentys says the $99 "deserves credit" given the calendar, storage and apps included.
|
||||
|
||||
Criticism:
|
||||
|
||||
- Price: $99/year, annual only, no monthly option for personal accounts.
|
||||
- A new address and lock-in: you must move to @hey.com (or pay for Domains), forwarding leaves your old provider still reading your mail ("Paying for email with both data and money", Hulry), and nothing but forwarding survives if you leave. Corporate users on company domains are out.
|
||||
- No IMAP/POP, no third-party clients, no checking other accounts, no import of history. TechCrunch called it "the literal opposite of an MVP" and noted the deliberate lock-in.
|
||||
- It is entirely manual. Every new sender is a decision, and routing is per sender only: you cannot route by subject or by domain to the Feed, so form responses and automated mail are "either in or out". Agentys: "HEY makes that time feel more intentional. It does not reduce it."
|
||||
- The Screener still gets a trickle of spam (one reviewer: one to five a day), and the spam filter has false positives on bank mail.
|
||||
- No reading pane and no dense list: each thread is a page, list rows are truncated, and read mail sliding into Previously Seen frustrates people who want a stable list to work through.
|
||||
- Search was widely criticised as weak into early 2025; HEY's own "7x faster" and "Refine your results" changelog entries confirm it needed work.
|
||||
- Editor: no markdown, formatting behind buttons, partial dark mode (email bodies stay light).
|
||||
- Rigidity: "you can't pick and choose the bits you like" (TapSmart). The spy-tracker banner is "distracting" with no way to hide it (Cassman).
|
||||
- No AI features at all, a plus for some and a minus in 2026 comparisons (Agentys, MailOver).
|
||||
- Trust: reports of missing mail with support blaming the user (Phil Reynolds), no end-to-end encryption, and some users leaving over 37signals leadership controversies.
|
||||
- Early complaint about no multi-account support was fixed by account linking; early complaint about no custom domains was fixed by HEY for Domains.
|
||||
|
||||
Sources: https://www.tapsmart.com/apps/hey-email-review/, https://hulry.com/hey-email-review/, https://www.agentys.io/en/blog/is-hey-email-worth-it, https://justinharter.com/a-new-review-of-hey-email-in-2024-and-how-its-changed-my-processes/, https://danielcassman.com/posts/2020/08/29/hey-email/, https://techcrunch.com/2020/06/16/basecamp-launches-hey-a-hosted-email-service-for-neat-freaks, https://philreynolds.dev/posts/2023/bye-to-hey, https://mailover.ai/blog/best-hey-email-alternatives.html, https://en.wikipedia.org/wiki/Hey_(email_service)
|
||||
|
||||
## 7. Takeaways for a new client
|
||||
|
||||
Steal the model, not the branding. HEY's real invention is that a sender has exactly one destination and the client owns that decision, not the sender; on Gmail that maps cleanly to a "first-time sender" query (no prior thread with that address, not in Contacts) held under a `Screener` label and three destination labels that a client-side rule applies on arrival. Gmail filters can do the steady-state routing once you have decided, so the Screener only has to be a client feature for the first message.
|
||||
|
||||
Three boxes are right, two groups are right. Imbox / Feed / Paper Trail beats Gmail's five tabs because the user chose every placement and there is no algorithm to second-guess. New For You over Previously Seen is a better default than unread-mixed-with-read, and it costs nothing to implement on top of Gmail's UNREAD label. Skip Gmail's Important marker entirely.
|
||||
|
||||
Reply Later and Set Aside as physical piles are worth copying, including the fact that a thread leaves the list when you pile it. Focus & Reply is the payoff and is trivial once the pile exists. Bubble Up is just snooze, but "If no reply by" is a good addition and the Bubbled Up section at the top is better than Gmail's snoozed-mail-reappears-as-unread.
|
||||
|
||||
Keep HEY's keyboard grammar: numbers for boxes, single mnemonic letters, letters printed on the action bar, `x` to select, `;` for the bulk menu. Drop the contextual reuse (`o`, `r`, `i` meaning different things in different places); a keyboard-first client should not do that.
|
||||
|
||||
Per-thread and per-contact notification opt-in, no badges, and no counts are cheap and are the single biggest calm-ness win. Do them.
|
||||
|
||||
Rename-for-me, merge, private notes, clips and cover art all depend on owning the data model. On Gmail you can only fake them with client-side metadata that other clients will not see; rename and notes are worth that, merge probably is not (it is permanent and Gmail threading will fight you), cover art is a gimmick.
|
||||
|
||||
Do not copy the no-reading-pane, one-thread-per-page layout wholesale. It is the most consistent complaint, and a desktop client has the width. A collapsible reading pane, or a single column with a keyboard-driven inline expand, keeps the calm without the round trips.
|
||||
|
||||
Do not copy "no archive". HEY can afford it because it controls storage and retention; on Gmail, archive is how the user's other clients stay sane, so keep `e` as archive and treat Previously Seen as a view, not a policy.
|
||||
|
||||
Do not copy per-sender-only routing. The lack of subject or domain rules is HEY's most repeated functional complaint; let the Screener decision also offer "everyone at this domain" and let a rule match on List-Unsubscribe or Precedence headers to pre-suggest Feed and Paper Trail.
|
||||
|
||||
Spy pixel blocking is achievable on Gmail (proxy or strip remote images, keep a vendor list, show the banner) and is a differentiator worth the effort.
|
||||
|
||||
HEY's weakest parts are search and the editor; both are places where a Gmail-backed client gets to inherit Gmail's strengths. Lean on them.
|
||||
@@ -0,0 +1,310 @@
|
||||
# Email client landscape
|
||||
|
||||
A survey of the clients around the product we are designing: a calm, keyboard-first, Gmail-backed native desktop client, macOS first, then Linux, then phones. HEY and Superhuman are covered in their own documents; they appear here only in the comparison table. Everything below is as of early September 2026. Prices are USD. Anything we could not confirm from a primary page is marked "unverified" inline.
|
||||
|
||||
The short version: Mimestream is the only shipping native client built directly on the Gmail API and it proves the model works, but it copies Apple Mail's layout and has spent six years unable to ship scheduled send or a real snooze because it refuses to run a server. Everyone else either went web-first and AI-first (Shortwave, Notion Mail, Zero), or relays your credentials through their own servers (Spark), or is a hosted mail service rather than a client (Fastmail, Proton). Nobody has shipped a quiet, dense, keyboard-driven native Gmail client with HEY-style sender screening. That is the gap.
|
||||
|
||||
## Mimestream
|
||||
|
||||
Mimestream is a Swift, AppKit plus SwiftUI macOS client that talks only to the Gmail API. Neil Jhaveri, previously on Apple's Mail team, started it in 2019; the public beta ran from September 2020 and 1.0 shipped on 22 May 2023. The company is five people, bootstrapped, and the current release is 1.10.6 (29 July 2026). It costs $4.99 a month or $49.99 a year after a 14-day trial with no card, covers every Google account you own on up to five devices, and is subscription only: the app stops working when the subscription lapses, which is the single loudest complaint on Hacker News. It requires macOS 12 or later and is distributed directly, not through the App Store. An iOS build exists only as a TestFlight beta (open beta since July 2026, iOS 26 required) with no App Store date; one reviewer guesses autumn 2026, unverified.
|
||||
|
||||
The Gmail mapping is the most faithful in the field. Labels are real Gmail labels: many per conversation, nested, colours and sidebar visibility synced both ways, drag to re-parent, shown as coloured chips in the list and on drafts. Gmail's categories (Primary, Social, Promotions, Updates, Forums) appear as separate inboxes in the sidebar, per account, with an honest caveat in the help text: there is no Gmail API for the category settings, so the app can silently disagree with Gmail about which tabs are on. The Important marker is an opt-in overlay, `is:important` works in search, and importance is a filter criterion. Server-side filters are editable in-app, as are the vacation responder and aliases. Accounts are grouped into Profiles (Personal, Work) shown as tiles at the top of the sidebar; each profile is a unified inbox of its accounts, and 1.10 added an optional "All" profile plus per-profile notification schedules and Focus Filter support.
|
||||
|
||||
Keyboard support comes as three switchable sets: Mimestream's own, Apple Mail, and Gmail. The Gmail set is single-key (`j`/`k`, `e`, `c`, `r`, `/`, `g` then `i` or `l`, `z`, `b` for snooze, `#` for trash); the others are Cmd chords. "Go to Folder" (Shift-Cmd-O) is a fuzzy jump across labels. There is no command palette and no per-shortcut customisation, and reviewers ask for both. Sync is delta sync on the Gmail history API, and push arrives through "Private Push": the app registers `users.watch` against Mimestream's Pub/Sub topic, Google publishes only the email address and a historyId, Mimestream's relay forwards that to the Mac (APNs on iOS), and the app fetches the changes itself. The relay never holds OAuth tokens. On macOS before 26 it falls back to IMAP IDLE. It is deliberately a "streaming" client: only a sliding window of recent mail per label is cached, older mail is reachable through server search, and offline actions are queued. Search is Gmail's server search (every operator works) merged with a local From and Subject prefix index. Reviewers consistently call it the fastest-syncing client they have used; one measured a six-minute initial sync for 46k messages across three accounts and sub-1.5-second searches.
|
||||
|
||||
The limits are structural rather than accidental. No scheduled send, because the Gmail API cannot do it and the team will not run a service with account access (a design for an opt-in service using only the `gmail.send` scope has sat on the roadmap for years, 533 votes, still "Considering" as of August 2026). Snooze is a Labs feature that only hides the thread locally; the message stays in Gmail's inbox for every other client because Mimestream cannot guarantee it will be running to unsnooze. No mute (no API). No Priority Inbox sections, no bundles or custom splits, no plugins, no AppleScript, no Windows or Linux, no non-Google accounts (IMAP has 937 votes). On the API itself the founder has been consistent since 2020: it "requires multiple round trips with the server to perform some basic tasks that IMAP can execute in 1 round trip", the throttling "is fine for a very thin client like Mimestream, but would not be a good fit for a client that wanted to download every message", and "for a thick email client, the Gmail API has definite efficiency issues compared to IMAP". The changelog backs this up: per-second quota errors on accounts with many labels, Gmail enforcing a 100-request batch cap, concurrent-request limits, draft sync reworked to cut quota, and at least three releases that exist only because Google changed API behaviour without notice. Mimestream also pays for a CASA Tier 2 assessment every year.
|
||||
|
||||
Visually it is Apple Mail with better Gmail plumbing: a stock three-pane window, unified toolbar, bold sender with thread count, subject, one grey preview line, coloured label chips, attachment chips with file-type icons, round avatars in the reading pane only, system font throughout, Liquid Glass on macOS 26. A long-time beta user's verdict on HN was that the additions over Gmail "are mainly cosmetic or nice-to-haves ... it's mainly a pretty native app UI". That is unfair to the sync work and fair about the design.
|
||||
|
||||

|
||||
|
||||
*Mimestream 1.10: profile tiles, category inboxes and Favorites in the sidebar, label and attachment chips in the list, threaded reading pane.*
|
||||
|
||||

|
||||
|
||||
*Gmail's five categories rendered as separate sidebar inboxes, with Promotions selected.*
|
||||
|
||||

|
||||
|
||||
*Compose: recipient tokens, From with avatar, and an @mention rendered inline.*
|
||||
|
||||
Worth stealing:
|
||||
- Labels as first-class Gmail labels with two-way colour and visibility sync, and categories as views over `CATEGORY_*` labels.
|
||||
- Three shortcut sets with a Gmail single-key mode; fuzzy Go to Folder.
|
||||
- Profiles: grouped accounts, each a unified inbox, an "All" escape hatch, per-profile quiet hours.
|
||||
- Private Push: `users.watch` plus a relay that only ever sees a historyId, tokens never leave the device.
|
||||
- Streaming sync with per-label windows, server search merged with a small local index, offline action queue.
|
||||
- Tracking-pixel blocking on by default for a maintained list of services.
|
||||
|
||||
Not for us:
|
||||
- The Apple Mail layout and list density; a calm client needs a quieter row than bold sender, preview, coloured chips and attachment chips.
|
||||
- Client-only snooze that lies to every other Gmail client.
|
||||
- Shipping without scheduled send for six years; a tiny opt-in `gmail.send` service is the answer and they know it.
|
||||
- No command palette, no shortcut customisation.
|
||||
|
||||
Sources: https://mimestream.com, https://mimestream.com/pricing, https://mimestream.com/faqs, https://mimestream.com/releases, https://mimestream.com/blog/1.10-released, https://mimestream.com/blog/casa-verified, https://mimestream.com/trust/private-push, https://mimestream.com/help/user-guide/inbox-categories, https://mimestream.com/help/user-guide/keyboard-shortcuts, https://mimestream.com/help/user-guide/snoozing, https://mimestream.com/ios-beta, https://portal.productboard.com/mimestream/1-mimestream-roadmap, https://news.ycombinator.com/item?id=24422434, https://news.ycombinator.com/item?id=24423314, https://news.ycombinator.com/item?id=36033184, https://twitter.com/neil_jhaveri/status/1355787069155631105, https://www.macstories.net/reviews/mimestream-the-perfect-email-app-for-gmail-users-on-the-mac/, https://sixcolors.com/post/2021/10/mimestream-a-native-mac-app-with-proper-gmail-support/, https://thesweetsetup.com/mimestream-is-a-great-reliable-gmail-app-for-the-mac/, https://www.cultofmac.com/reviews/mimestream-mac-mail-app-review, https://email-tools.me/posts/mimestream-review/, https://techcrunch.com/2023/05/23/former-apple-engineers-mimestream-app-is-a-nifty-gmail-client-for-mac/
|
||||
|
||||
## Shortwave
|
||||
|
||||
Shortwave is a Gmail API client (Gmail and Workspace only; Microsoft has "no timeline") delivered as a web app with macOS and Windows desktop wrappers and iOS and Android apps. Its founders came from Google (the Firebase team) and the product reads as Inbox by Gmail's bundles and pins rebuilt around an AI assistant. Pricing is per seat per month with no free tier: Business $30, Premier $45, Max $120 (annual $24, $36, $100), after a 14-day trial. The tiers differ only in AI quota (model, requests per day, number of "AI filters"), which tells you what the company thinks it is selling. Several reviews still list a free tier and an $18 Pro plan; those are stale. The founders' attention has visibly moved to a separate product, Tasklet, which the homepage now leads with.
|
||||
|
||||
The triage model is the strongest part and is documented as "The Shortwave Method": the inbox is a list you clear. Done (`D` or `E`) archives and auto-advances, Snooze (`B` or `H`) handles date-bound items, Star (`S`) drops a thread into a Starred section pinned at the top of the list, and Todo (`T`) turns one or more threads into a named task in a Todos tab. Pin was removed in 2024 in favour of stars and todos. The list itself is sectioned Todos, Starred, Last 7 days. Bundles are not automatic classification: you enable them, per label (built-in Newsletters, Promotions, Travel, Updates, or any custom label) or per contact, and a bundle collapses to one compact row with a count and sender avatars that expands in place; one keystroke marks the whole bundle done. Splits are tabs across the top of the inbox, each defined by Gmail importance, labels, senders or an arbitrary search query, with drag-and-drop between them and bundling toggleable per split.
|
||||
|
||||
Layout is a setting: Default, Side panel (list and thread side by side) or Fullscreen (one thread at a time). Shortcuts are Gmail-style single keys plus a Cmd-K command palette and Cmd-J for the assistant. Undo send is server-side (10 seconds by default, the message still goes out if you close the app) and scheduled send takes natural-language times. On the receiving side it proxies images and blocks tracking pixels with an indicator; on the sending side it sells read statuses, link click tracking and a "recent opens" feed, which is the same tracking it blocks for you. Multiple accounts switch with Ctrl-1/2/3 but there is no unified inbox; the official workaround is Gmail forwarding. The AI layer is everywhere: a right-hand assistant sidebar, natural-language search, a one-line summary on every thread, Ghostwriter drafts trained on your sent mail, plain-English "AI filters" that label, star or archive, and MCP integrations.
|
||||
|
||||
Criticism from reviewers is consistent: Gmail-only is a hard wall, the price has roughly doubled since launch, the per-day AI caps on the cheapest tier feel restrictive, and your mailbox is indexed on Shortwave's servers and sent to third-party models, which one XDA reviewer called a dealbreaker for drafting replies. Nobody complains about speed.
|
||||
|
||||

|
||||
|
||||
*Shortwave: split tabs (Important, Support, Other, Todos) across the top, list sectioned Todos, Starred, Last 7 days, thread in a side panel with an AI summary line, assistant in the right sidebar.*
|
||||
|
||||

|
||||
|
||||
*A "Newsletters 68" bundle collapsed to one row with sender avatars; the tooltip shows a single keystroke marks all 68 done.*
|
||||
|
||||

|
||||
|
||||
*The assistant proposing to mark seven threads done. This is how much of the product surfaces: as a dialogue rather than a keystroke.*
|
||||
|
||||
Worth stealing:
|
||||
- Done, Snooze, Star, Todo as a single-key vocabulary with auto-advance; Starred section pinned at the top of the list.
|
||||
- Bundle rows: one collapsed line per label or sender with a count, expand in place, whole bundle actioned with one key. On Gmail the labels already exist.
|
||||
- Splits as tabs defined by label, sender or query; per-split bundling toggle.
|
||||
- Server-side undo send that fires with the app closed; layout as a plain three-way setting.
|
||||
|
||||
Not for us:
|
||||
- The AI-first frame: the sidebar, per-thread summaries, autocomplete and AI filters are why the cheapest seat is $24 and why reviewers raise privacy.
|
||||
- Read statuses and link tracking on send.
|
||||
- "Multi-account" without a unified inbox.
|
||||
- Web app in a desktop wrapper.
|
||||
|
||||
Sources: https://www.shortwave.com, https://www.shortwave.com/pricing, https://www.shortwave.com/features/, https://www.shortwave.com/docs/guides/method/, https://www.shortwave.com/docs/guides/bundles/, https://www.shortwave.com/blog/split-email-inbox-by-importance/, https://www.shortwave.com/blog/todos-and-stars/, https://www.shortwave.com/docs/references/shortcuts/, https://www.shortwave.com/docs/guides/customize-your-shortwave-settings/, https://www.shortwave.com/blog/announcing-scheduled-send-and-undo-send/, https://www.shortwave.com/blog/read-statuses-email-tracking/, https://www.shortwave.com/docs/how-tos/unified-universal-inbox-support/, https://www.shortwave.com/docs/how-tos/microsoft-outlook-exchange-other-sign-in-support/, https://email-tools.me/posts/shortwave-review/, https://cmdk.email/post/shortwave-review/, https://www.xda-developers.com/replaced-gmail-with-an-email-app-that-finally-makes-inbox-zero-feel-realistic/, https://9to5google.com/2024/06/05/shortwave-adds-inbox-splits/
|
||||
|
||||
## Notion Mail
|
||||
|
||||
Notion Mail is being shut down on 22 September 2026, announced 25 June 2026, seventeen months after its 15 April 2025 launch. Notion's stated reason is that "more than half of Notion Mail users manage emails without ever opening their inbox", so they are "going all in on using agents to run your inbox". Users can export drafts, scheduled mail, snippets and auto-label instructions; views and reminders are lost; the mail itself was always in Gmail. The product page already redirects. It is still worth studying because the "views" idea was the one genuinely new inbox primitive of the last two years, and because its footprint (Gmail only, macOS plus web plus a thin iOS app, Windows "soon" for its whole life, never Android, never a unified inbox) is a warning.
|
||||
|
||||
It was Gmail and Workspace only. The client was free with any Notion account, but the headline features (auto-label, AI drafting, AI-configured views) needed paid Notion AI, in practice the Business plan at $20 per seat a month annual ($24 monthly), which is where the "free but not really" reviews came from. One review also reports mail retention caps of 7 days on Free and 30 days on Plus, unverified.
|
||||
|
||||
Views live in a "Views" section of the left sidebar, as entries rather than tabs. Each view is a saved combination of filters, groups and properties, deliberately modelled on a Notion database view. Filters cover unread, read, attachment, calendar event, from, to, cc, bcc, subject, date and `label:` (nested labels unsupported). Group by date, starred, important, sender or domain, priority, label or unread, and the groups render as inline section headers in the list. Properties are user-defined columns on emails, for example a Select "Owner". A new view can be described in natural language (AI configures it), built manually, or picked from a template; default views were generated from your existing Gmail labels. Views only filter; they never label anything. Auto-label is a separate feature: a button top-right of the inbox where you type a plain-English instruction, it labels incoming mail, and it learns from you accepting or crossing out its suggestions. Everything else is competent and conventional: snippets with slash shortcuts and `{{variables}}`, meeting scheduling links through Notion Calendar, "all Gmail shortcuts" (`j`/`k`, `e`, `h` for snooze which it calls Set reminder, `l`, `#`, `z`, Ctrl-1 to 9 for accounts) plus a Cmd-K command menu, scheduled send with an explicit timezone, and a thread-style setting of side peek, centre peek or full page. Remote images were proxied but tracking pixels were not blocked; the help page said "we hope to block read receipts in the near future". Undo send was never documented.
|
||||
|
||||
Reviewers called it a pretty wrapper with shallow Notion integration (you could @-mention a page but not turn an email into a task or a database row), said view setup was manual and slow with no useful presets, found the AI drafts generic, and listed the platform gaps. The iOS app lacked snippets, scheduling, AI, schedule send and any view editing. Then the company killed it.
|
||||
|
||||

|
||||
|
||||
*Inbox grouped by label (Hiring, Support, Travel headers), Views list in the sidebar, Auto label button top right.*
|
||||
|
||||

|
||||
|
||||
*A "Travel" view grouped by trip; each group is an inline header, the sidebar lists views above mail folders.*
|
||||
|
||||

|
||||
|
||||
*The "Edit view" popover: Group, Filter, Properties and hover actions, the Notion database vocabulary applied to mail.*
|
||||
|
||||
Worth stealing:
|
||||
- Views as saved filter-plus-group definitions in the sidebar, with group headers inline in the list. Grouping by label, sender domain or date calms an inbox without any AI.
|
||||
- Thread-style toggle (side peek, centre peek, full page).
|
||||
- Gmail shortcuts by default plus a Cmd-K menu that doubles as the shortcut reference.
|
||||
- Snippets with slash shortcuts and variables; scheduled send with a visible timezone.
|
||||
|
||||
Not for us:
|
||||
- AI auto-label as the organising primitive; it needed a $20 seat and is exactly what is being deleted.
|
||||
- Properties and columns on emails; the database metaphor never connected to anything.
|
||||
- The footprint: Gmail-only, Mac plus web, no unified inbox, a mobile app missing half the features.
|
||||
|
||||
Sources: https://www.notion.com/help/notion-mail-inbox-is-going-away-what-to-do-next, https://www.notion.com/blog/introducing-notion-mail, https://www.notion.com/help/get-started-with-notion-mail, https://www.notion.com/help/views-groups-filters-and-properties, https://www.notion.com/help/navigate-your-inbox, https://www.notion.com/help/guides/organize-your-inbox-with-notion-ai-auto-labeling, https://www.notion.com/help/notion-mail-keyboard-shortcuts, https://www.notion.com/help/notion-mail-security-practices, https://www.notion.com/help/notion-mail-for-mobile, https://www.notion.com/pricing, https://www.androidauthority.com/notion-mail-is-shutting-down-3681674/, https://www.theregister.com/ai-and-ml/2026/06/26/notion-kills-its-gmail-client-after-ai-agents-keep-humans-from-troubling-inbox/5263024, https://efficient.app/apps/notion-mail, https://clean.email/blog/email-clients/notion-mail-review, https://www.eesel.ai/blog/notion-mail-reviews, https://matthiasfrank.de/en/notion-features/notion-mail/
|
||||
|
||||
## Spark
|
||||
|
||||
Spark, by Readdle, is the mass-market "smart inbox" client: Gmail, iCloud, Exchange, Outlook, Yahoo and generic IMAP on macOS, Windows, iOS, iPadOS and Android, no web client. Gmail connects through Google OAuth; whether the transport is the Gmail API or IMAP with XOAUTH2 is not documented, and Readdle's troubleshooting page about enabling IMAP for Gmail suggests IMAP, unverified. What is documented is that Readdle's servers hold an OAuth token for Gmail, Outlook and Yahoo and the actual password for Exchange, AOL and custom IMAP, because "Spark requires the server-side processing to send you push notifications". Send Later mail, including attachments, sits on their servers until sent, and "recent emails" are held for four hours. Spark +AI runs on Azure OpenAI and email content is shared with it. This credential relay has been criticised continuously since 2016. The desktop app has been an Electron rewrite since Spark 3 in October 2022; the launch dropped the reading pane, the persistent sidebar and separate windows, added a subscription and a paid-to-remove "Sent with Spark" signature, and drew a public response from Readdle's co-founder. Most of the gaps were patched back over 2023. Pricing today: Free, Plus $10 per user a month ($8.25 annual), Pro $20 ($16.58 annual). Gatekeeper, Priority, mute, templates, read statuses and AI are all paid.
|
||||
|
||||
Gatekeeper is the reason Spark is on this list. The first time a sender emails you, you decide: a horizontally scrolling row of cards labelled "New senders" sits above the inbox, each with avatar, address, latest subject and thumbs-up Accept or thumbs-down Block, plus a full-screen grid with bulk actions. Three modes per account: screen before the inbox (default), decide inside the message when you open it, or off. Blocking hides rather than deletes: blocked mail sits in a "Blocked" section of the sidebar and you unblock by opening a message there. It blocks addresses and domains, the sender is never notified, it is per account, it is not retroactive, and it is Spark-side rather than pushed into Gmail as a filter, so it only holds inside Spark. If your subscription lapses, existing blocks remain and you can only accept.
|
||||
|
||||
The Smart Inbox has a fixed order: People first, then Notifications, then Newsletters, each a card that can be unified, grouped per address, or per account. Spark 3 added two views on top, Focused List (chronological with priority items floated) and Unread Cards (unread mail grouped into People, Notifications, Newsletters), beside the plain Simple List; Notifications and Newsletters collapse into a single row of sender chips. Priority is manual (`I`, or per contact) and paints the row light orange with a lightning glyph. Set Aside (`G`) moves a thread to a bubble in the bottom-left corner that acts as a visible shelf; Pin (`D`) keeps it in a Pins section; Snooze (`S`) hides it until a time; Done (`E`) archives, with a toggle to show done items inline. Mute is paid and, per the help page, not currently available on Mac or Windows. Send Later is server-side, undo send waits five seconds by default, follow-up reminders go up to two months out, and Read Statuses (Pro) are ordinary tracking pixels. On the receiving side Spark blocks 1x1 pixels by default and only 1x1. Keyboard presets include Spark, Spark Classic, Apple Mail, Gmail, Superhuman and custom; the default set has no `j`/`k`. Cmd-K opens a Command Center that lists actions with their shortcuts. A "Home Screen" wallpaper greeting appears after idle time with counts of people, newsletters and notifications.
|
||||
|
||||
Reviewers describe the current app as heavier and busier than the 2019 version, with settings sprawling across dozens of screens and AI upsell on every surface; the free plan is thin after a seven-day trial; threading has caused missed mail; and the credential relay is the objection that never goes away.
|
||||
|
||||

|
||||
|
||||
*Gatekeeper: the "New senders" card row above the inbox with Accept and Block; below it Notifications and Newsletters collapsed into sender chip rows.*
|
||||
|
||||

|
||||
|
||||
*The Focused List: avatar rows, pinned items, Today and Yesterday sections, bundles as chips.*
|
||||
|
||||

|
||||
|
||||
*The view picker (Focused List, Unread Cards, Simple List) over an Unread Cards inbox grouped per account; orange rows are Priority.*
|
||||
|
||||
Worth stealing:
|
||||
- Gatekeeper's interaction: a first-contact strip above the inbox, sticky per sender and per domain, silent to the sender, with a visible Blocked bucket instead of deletion. Implement it as Gmail filters and labels so it holds outside our client.
|
||||
- Done as the archive verb and Set Aside as a visible shelf distinct from snooze.
|
||||
- Cmd-K palette that teaches the shortcuts; shipping a Gmail preset and a Superhuman preset.
|
||||
|
||||
Not for us:
|
||||
- The credential relay for push and Send Later.
|
||||
- Fixed People, Notifications, Newsletters ordering with bundles collapsed into chip rows; it hides subjects and the classification is opaque.
|
||||
- Electron, the Home Screen wallpaper, AI upsell everywhere, a "Sent with Spark" signature.
|
||||
|
||||
Sources: https://sparkmailapp.com, https://sparkmailapp.com/pricing, https://sparkmailapp.com/features, https://sparkmailapp.com/privacy, https://support.readdle.com/spark/privacy/privacy-explained, https://support.readdle.com/spark/spark-onboarding/accept-or-block-new-senders, https://sparkmailapp.com/help/set-up-focus/accept-or-block-new-senders, https://sparkmailapp.com/help/manage-your-inbox/customize-your-inbox, https://support.readdle.com/spark/personalization/customize-your-smart-inbox, https://sparkmailapp.com/help/set-up-focus/set-aside-vs-pin-vs-snooze, https://sparkmailapp.com/help/set-up-focus/mark-as-done, https://sparkmailapp.com/help/sending-emails/pin-and-priority, https://sparkmailapp.com/help/spark-for-teams/read-statuses, https://sparkmailapp.com/help/tips-tricks/use-keyboard-shortcuts, https://sparkmailapp.com/help/set-up-focus/spark-command-center, https://sparkmailapp.com/help/tips-tricks/how-to-enable-split-view-in-spark, https://sparkmailapp.com/features/spark-ai, https://support.readdle.com/spark/troubleshooting/enable-the-imap-protocol-for-gmail-and-g-suite-accounts, https://mjtsai.com/blog/2016/12/01/spark-mail-stores-credentials-in-cloud/, https://mjtsai.com/blog/2022/10/04/spark-switches-to-electron-and-subscriptions/, https://forums.macrumors.com/threads/popular-email-client-spark-gets-major-redesign-for-mac-moves-to-subscription-model.2363830/, https://appleinsider.com/articles/23/01/13/spark-mail-211-review-e-mail-organizer-with-gatekeeper-smart-inbox, https://cmdk.email/post/spark-mail-review/, https://thebusinessdive.com/spark-review
|
||||
|
||||
## Apple Mail categories
|
||||
|
||||
Apple's categorisation shipped in iOS 18.2 (December 2024) and reached the Mac in macOS 15.4 (31 March 2025). macOS 26 and iOS 26 added nothing to it beyond Liquid Glass toolbars; Apple's own "What's new in Mail" page for Tahoe still lists only Categories. The model is four buckets. Primary: "personal messages and time-sensitive information". Transactions: "confirmations, receipts, and shipping notices". Updates: "news, newsletters, and social updates". Promotions: "coupon and sales emails". The one rule that makes it survivable: a message in Transactions, Updates or Promotions that contains time-sensitive information is also shown in Primary. Classification is on-device machine learning, does not need Apple Intelligence hardware, and runs on every account in Mail including Gmail over IMAP, ignoring Gmail's own `CATEGORY_*` labels and classifying locally. Priority messages (time-sensitive mail floated to the top of Primary) and the one-line summaries under unread rows do need Apple Intelligence.
|
||||
|
||||
On iOS the categories are four icon pills under the Inbox title; the selected one expands with its label and colour (blue, green, purple, red), and swiping the row reaches All Mail. On macOS the same pill row sits at the top of the message list column in a standard three-column window, with "Show Mail Categories" in the More menu and the View menu; clicking the selected category again returns to All Mail. Inside Transactions, Updates and Promotions, messages from the same sender are grouped into a digest row showing the sender's logo and bulleted recent subjects; tapping opens a digest view with bulk actions. Primary is never grouped. Group by Sender can be switched off, and iOS 18.5 moved the Categories, Group by Sender and Show Contact Photos toggles into Settings. The override is "Categorize Sender" (control-click on Mac, swipe then More on iPhone): all current and future messages from that sender move to the chosen category, and the override syncs across devices.
|
||||
|
||||
The rest of Mail on macOS is relevant mainly as a floor. There is no snooze; Remind Me (1 hour, tonight, tomorrow, later) resurfaces a message at the top of the inbox. Send Later exists but needs the Mac awake with Mail open. Undo Send defaults to 10 seconds. Shortcuts are modifier chords only (Ctrl-Cmd-A archive, Shift-Cmd-D send, Ctrl-Cmd-M move to predicted mailbox), no single-key mode. Mail Privacy Protection hides your IP and prefetches remote content in the background so senders cannot see when or whether you opened a message.
|
||||
|
||||
Criticism landed fast. Six Colors titled its review "iOS 18.2 Mail is a misfire": Informed Delivery in Transactions, same-day delivery alerts not urgent enough for Primary, its own newsletter in Promotions, unread badge counting only Primary, a digest header that wastes space on a giant avatar while "the subject is crammed together and truncated", and no way to collapse an expanded digest. Macworld's line was that dozens of messages "could go unread for hours or even days". MacRumors readers: "now I've got six inboxes to check instead of one". AppleInsider, after months on the Mac, found "not a single reason to disagree with the automatic categorization". Both are true; it depends on your mail.
|
||||
|
||||

|
||||
|
||||
*macOS Tahoe Mail: the Primary pill row above the message list in a three-column window, Summarize button in the message header.*
|
||||
|
||||

|
||||
|
||||
*iOS Mail with Promotions selected: senders grouped into digest rows with bulleted recent subjects.*
|
||||
|
||||

|
||||
|
||||
*macOS 15.4 with Transactions selected and the first-run "bundled by sender" explainer banner.*
|
||||
|
||||
Worth stealing:
|
||||
- The four-bucket taxonomy seeded from Gmail's `CATEGORY_*` labels, with no ML needed, and the rule that time-sensitive mail from any bucket also appears in Primary.
|
||||
- Categorize Sender as a sticky per-sender override with an immediate visible move; All Mail one keystroke away.
|
||||
- Classification on the client; remote content handled privately by default.
|
||||
|
||||
Not for us:
|
||||
- The sender digest as built: truncated subjects, oversized logos, no collapse, no keyboard path.
|
||||
- Primary-only badge counts; categories on by default with the exit buried.
|
||||
- Modifier-only shortcuts, no snooze, Send Later that needs the machine awake.
|
||||
|
||||
Sources: https://support.apple.com/guide/mail/mlhlp1190/mac, https://support.apple.com/guide/iphone/use-categories-iphfe4a36baf/ios, https://support.apple.com/guide/mail/whats-new-cpmlwn/mac, https://support.apple.com/guide/mac-help/use-apple-intelligence-in-mail-mchlb2dbea8f/mac, https://support.apple.com/guide/mail/keyboard-shortcuts-mlhlb94f262b/mac, https://support.apple.com/guide/mail/protect-email-privacy-mlhlp1205/mac, https://support.apple.com/en-us/122868, https://appleinsider.com/articles/25/03/31/macos-sequoia-154-arrives-with-apple-mail-categories-password-timers-and-more, https://appleinsider.com/articles/24/06/25/apples-on-device-email-categorization-is-a-feature-years-in-the-making, https://sixcolors.com/post/2024/12/ios-18-2-mail-is-a-misfire/, https://www.macworld.com/article/2585948/how-to-ios-18-macos-15-change-mail-categories-list-view.html, https://forums.macrumors.com/threads/ios-18-2-heres-how-mail-categories-work.2445355/, https://9to5mac.com/2026/03/23/apple-mail-has-a-hidden-feature-that-solved-my-biggest-inbox-problem/, https://www.techradar.com/phones/ios/apples-first-ios-18-5-beta-makes-it-easier-to-get-the-old-style-apple-mail-back
|
||||
|
||||
## Fastmail and Proton Mail
|
||||
|
||||
These are hosted mail services with their own clients, not clients over someone else's mailbox, so they matter here for two things: what a no-ads, privacy-forward inbox looks like, and a couple of specific mechanisms.
|
||||
|
||||
Fastmail is an Australian company (since 1999, servers in New York) whose native protocol is JMAP, which their web, mobile and desktop apps all speak; IMAP, SMTP, CalDAV and CardDAV are also available and a public JMAP API takes bearer tokens. Individual is $6 a month or about $5 annual, Duo $10, Family $14, business tiers $4 to $10 per user, 30-day trial. It finally shipped a desktop app for Mac, Windows and Linux in October 2025, and it is a wrapper around the web app (reported as Electron by third parties; Fastmail's own post does not say). The useful idea is the per-account "labels or folders" switch: pick Labels and existing folders convert, a message can carry many labels, Archive becomes All Mail as the only place an unlabelled message lives, and the Archive button just removes the Inbox label. That is Gmail's model, and Fastmail chose it deliberately. Shortcuts are Gmail-style (`j`/`k`, `o`, `u`, `c`, `r`, `a`, `f`, `d`, `y` archive, `!` spam, `l`, `m`, `/`, `g` then a folder name) and not remappable. Snooze, scheduled send (with a Scheduled folder), undo send, pin and mute conversation all exist; Pinned, Snoozed and Scheduled are visible mailboxes. Read receipts can be requested but incoming requests are never answered. There is no pixel detection: every remote image is fetched through Fastmail's servers so the host never sees your IP, and you can block remote images by default for all senders or only unknown ones. No end-to-end encryption by design, and the Five Eyes jurisdiction argument against it is real. Masked Email, built as a JMAP extension and wired into 1Password and Bitwarden, is the other idea worth noting.
|
||||
|
||||
Proton Mail is Swiss, OpenPGP end-to-end between Proton users and zero-access encrypted at rest for everything else, with no IMAP except through the paid Bridge daemon. Web, iOS, Android, and Electron desktop apps for Windows, macOS and Linux. Free (1 GB), Mail Plus $4.99 a month or $3.99 annual, Unlimited $12.99 or $9.99 annual, Duo $14.99, Family $29.99. The mechanism to copy is "enhanced tracking protection": on by default, it strips known tracking pixels, loads remaining remote images through Proton's proxy, and on web rewrites links to remove known UTM-style parameters, then shows a shield badge in the message header with a count and a tooltip such as "Trackers blocked: 3, Links cleaned: 6". A separate "Confirm link URLs" modal, also on by default, intercepts external links. Snooze, scheduled send, undo send and requestable read receipts exist. Shortcuts are on by default but non-standard (arrows to move, `a` archive, `t` trash, `.` star, `g` then `i`/`d`/`s`/`a`/`x`/`t` to jump, Shift-Space for a command palette, no `j`/`k`), and there is a long-standing user request for a Gmail-compatible set plus an unofficial browser extension that exists purely to remap them. Layout is Column (list beside reading pane, default) or Row (message replaces list, no preview). Proton Scribe, the writing assistant, can run fully on-device after a 4 GB model download on web and desktop, which is the right way to offer AI. Criticism: Bridge makes every third-party client second-class, encrypted bodies mean search needs a local index the web app builds slowly, and the desktop app is a web wrap with settings that do not sync between devices.
|
||||
|
||||

|
||||
|
||||
*Fastmail web app: labels sidebar, label chips on rows, a pinned thread, conversation view with collapsed messages.*
|
||||
|
||||

|
||||
|
||||
*The October 2025 Fastmail desktop app for Mac: the same web UI in a native window with a Snoozed folder visible.*
|
||||
|
||||

|
||||
|
||||
*Proton Mail desktop app on Linux: three-pane with folders and labels in the sidebar.*
|
||||
|
||||

|
||||
|
||||
*Proton's tracker shield in the message header: "Trackers blocked: 3, Links cleaned: 6", plus the mailing-list Unsubscribe banner.*
|
||||
|
||||
Worth stealing:
|
||||
- Fastmail's labels model spelled out plainly: Archive removes Inbox, All Mail is ground truth, many labels per message.
|
||||
- Fastmail's `j`/`k`/`y`/`g`-jump key set and Pinned, Snoozed, Scheduled as visible mailboxes.
|
||||
- JMAP's state-token-plus-changes mental model for our local sync layer even though we talk to Gmail.
|
||||
- Proton's tracker count badge and link cleaning, shown per message; the mailing-list banner with one-click unsubscribe from `List-Unsubscribe` headers.
|
||||
- On-device AI as an explicit optional download, never a cloud default.
|
||||
|
||||
Not for us:
|
||||
- Image proxying as the only tracker defence (a native client can strip pixels locally and say what it stripped).
|
||||
- Non-standard shortcut sets; Row layout with no preview.
|
||||
- Electron desktop apps from companies that own the protocol; being genuinely native is the differentiator.
|
||||
|
||||
Sources: https://www.fastmail.com/pricing/, https://www.fastmail.com/features/, https://www.fastmail.com/blog/desktop-app/, https://www.fastmail.com/for-developers/integrating-with-fastmail/, https://www.fastmail.help/hc/en-us/articles/360058753554-Setting-up-and-using-labels, https://www.fastmail.help/hc/en-us/articles/360058753534-Keyboard-shortcuts, https://www.fastmail.help/hc/en-us/articles/1500000278102-Blocking-remote-images, https://www.fastmail.help/hc/en-us/articles/1500000278162-Read-receipts, https://www.fastmail.com/blog/how-and-why-we-built-masked-email-with-jmap-an-open-api-standard/, https://coywolf.com/news/productivity/fastmail-launches-native-desktop-apps-for-mac-windows-and-linux/, https://cyberinsider.com/email/reviews/fastmail/, https://proton.me/mail, https://proton.me/mail/pricing, https://proton.me/support/proton-plans, https://proton.me/support/email-tracker-protection, https://proton.me/blog/tracking-links-protection, https://proton.me/support/link-confirmation, https://proton.me/support/keyboard-shortcuts, https://proton.me/support/change-inbox-layout, https://proton.me/support/read-receipts, https://proton.me/blog/proton-scribe-writing-assistant, https://proton.me/blog/proton-mail-desktop-app, https://github.com/ProtonMail/inbox-desktop, https://protonmail.uservoice.com/forums/284483-proton-mail/suggestions/38545198-improve-keyboard-shortcuts-gmail-like
|
||||
|
||||
## Open-source Gmail API clients
|
||||
|
||||
Zero (Mail-0/Zero, 0.email) is the visible one: "An Open-Source Gmail Alternative for the Future of Email", MIT licensed, 10.8k stars, a YC Spring 2025 company of three people, public beta May 2025. The company has since pivoted: 0.email now says "0.email is now Orchid", an executive-assistant product over iMessage and SMS, and the Zero repo's default branch last moved on 31 August 2025 with three commits to `main` in May 2026 titled "fix build", "idk atp" and "ugh". Issues are auto-closed by a stale bot after three days. It is a hosted web app you self-host, not a desktop or mobile app. The README still says Next.js and Node; `wrangler.jsonc` says otherwise. The real stack is a React front end deployed as a Cloudflare Worker, a Hono plus tRPC server on Workers, six Durable Object classes (ZeroAgent, ZeroMCP, ZeroDB, ZeroDriver, ThreadSyncWorker, ShardRegistry), Cloudflare Workflows for initial sync, three Queues, R2 for thread bodies, Vectorize for embeddings, Workers AI, Drizzle with Postgres, Better Auth, and the googleapis SDK, plus a Microsoft Graph driver. Self-hosting therefore needs a Google OAuth client, a GCP service account with Pub/Sub admin rights, the whole Cloudflare stack, a billing SDK key, Twilio credentials and model provider keys.
|
||||
|
||||
Its Gmail usage is worth reading because it is the textbook pattern. Scopes are `https://mail.google.com/` plus `gmail.modify`. Initial sync pages `threads.list` at 100 per page and calls `threads.get` with `format=full` for each, writing into a per-connection Durable Object with SQLite, bodies to R2, embeddings to Vectorize. Incremental sync creates a Pub/Sub topic per connection, grants `[email protected]` publish rights, subscribes to a push URL, and calls `users.watch` on INBOX; the handler reads the historyId, queues it, and a consumer calls `history.list` and re-syncs the affected threads; a cron renews expiring watches. Rate limiting retries 429 and 403 `userRateLimitExceeded` and `quotaExceeded` up to ten times with a fixed 60-second delay. Features: a unified inbox with category tabs (Primary, Important, Personal, Updates, Promotions), "ask anything about your emails" chat, AI summaries and labels, AI compose, snooze, scheduled send, pinned threads, Cmd-K, an MCP server. The one Show HN comment was from a Superhuman user: "until Zero has real keyboard navigation, i'll have to wait". Issue 1839, "[oauth]: This app is blocked" (Google refusing sign-in to the unverified client), was closed by the stale bot unanswered, which is the self-hosting problem in one line: with a full-mailbox scope every self-hoster is either stuck in Testing mode (100 users, tokens expire after 7 days) or doing restricted-scope verification themselves.
|
||||
|
||||
lieer (gmi) is the feasibility reference nobody talks about: a Python tool, GPL, maintained since 2017 and still pushed to in April 2026, that pulls Gmail through the API into a maildir and mirrors Gmail labels as notmuch tags, two-way. It stores the last historyId and uses `history.list` for partial pulls, falls back to a full resync, pushes local tag changes back with `gmi push`, and sends over the API with thread matching on In-Reply-To. Its invariants are the ones any Gmail-API client ends up with: a message may carry only one of inbox, spam or trash; drafts and sent are read-only from Gmail's side; archive means removing the inbox tag; muted cannot sync because the API has no mute. It runs at million-message scale, ships a shared public OAuth client or takes your own, and its README is candid: "You don't need to verify your application, you'll just get a scary warning about unverified applications."
|
||||
|
||||
The closest technical comparators are two Tauri apps. Velo (Apache-2.0, 701 stars, created February 2026, quiet since June 2026) is Tauri v2 with a Rust backend and a React front end, Gmail via the REST API with historyId sync, OAuth PKCE where the user supplies their own client ID, SQLite with FTS5, a command palette, split-inbox tabs, offline, remote-image blocking, sandboxed rendering, and optional cloud AI; the caveat is that the Gmail sync logic lives in TypeScript inside the webview, not in Rust. Pebble (AGPL, 583 stars, created April 2026, active in August 2026) is Tauri 2 plus Rust with rusqlite and Tantivy full-text search, but talks to Gmail over IMAP, and the UI is Chinese-first. Beyond those, every established open-source client goes through IMAP: Thunderbird and Betterbird (IMAP plus OAuth, labels become duplicated folders, All Mail duplicates everything), Geary (Vala, last release May 2024), Evolution, KMail, Mailspring (Electron UI over a C++ mailsync engine, stalled 2022 to 2024 with draft-deletion and locked-database complaints, then twelve releases between January and July 2026), and the Rust and Go terminal clients Himalaya, meli and aerc. Nylas Mail, the last well-funded open-source attempt, depended on Nylas's cloud sync and was sunset in 2017.
|
||||
|
||||
What they collectively got wrong is easy to state. Nobody shipped a good native desktop client on the Gmail API: Zero is a Cloudflare web app whose company pivoted, Velo put its sync in a webview, and everyone else inherited IMAP's labels-as-folders mess. What they show is feasible: history-based delta sync, labels as tags, two-way label push, API send, and watch plus Pub/Sub for push have all been done in the open for years, and Velo shows PKCE with bring-your-own client ID works for a desktop app without a secret. The constraints to plan for, from Google's current pages: every mailbox-reading scope is restricted (`gmail.modify`, `gmail.readonly`, `mail.google.com`), so brand verification, domain verification, a privacy page, a scope justification and a demo video are unavoidable and must be repeated every 12 months. The security assessment (CASA, now Assurance Levels AL1 and AL2, roughly $500 to $1,800 and about $4,500 respectively, 4 to 12 weeks end to end, annual) is triggered for apps that "have the ability to access data from or through a third-party server"; read plainly, a client that never routes mail through our servers needs scope verification but not CASA, unverified, and worth confirming with Google before betting the roadmap on it. Live quota: 1,200,000 units a minute per project, 6,000 units a minute per user, `messages.get` 20 units, `threads.get` 40, `messages.list` 5, `history.list` 2, `messages.send` 100, `users.watch` 100, and a watch must be renewed at least every 7 days. Threading is Gmail's: to land in an existing thread you set threadId and the RFC 2822 References and In-Reply-To headers and a matching subject; labels are per message, so "thread has label" is a client-side aggregate.
|
||||
|
||||

|
||||
|
||||
*Zero: sidebar, Primary list with a Pinned group, thread view with an AI Summary box.*
|
||||
|
||||

|
||||
|
||||
*Zero on a real Gmail account: Primary, Important, Personal, Updates and Promotions tabs over the list.*
|
||||
|
||||

|
||||
|
||||
*Velo (Tauri plus Rust, Gmail REST API): dark inbox with AI Summary, Quick Replies and a contact side panel.*
|
||||
|
||||

|
||||
|
||||
*Pebble (Tauri plus Rust, IMAP): three-pane inbox with hover actions.*
|
||||
|
||||

|
||||
|
||||
*Thunderbird: the IMAP baseline, unified folders, tags and a threaded list.*
|
||||
|
||||
Worth stealing:
|
||||
- lieer's invariants verbatim: one of inbox, spam or trash; drafts and sent read-only; archive is drop INBOX; muted not syncable.
|
||||
- The watch, Pub/Sub, historyId, `history.list` pipeline, and cron renewal of watches; Zero's code is a readable reference.
|
||||
- Velo's PKCE-only OAuth with bring-your-own client ID as the fallback for people Google will not let in, and a local FTS index.
|
||||
- Tantivy or SQLite FTS5 for local search, as Pebble and Velo do.
|
||||
|
||||
Not for us:
|
||||
- Zero's architecture: server-side mailbox copies, six Durable Object classes and a service account with Pub/Sub admin rights to run an inbox. Local SQLite and `history.list` at 2 units a call need none of it and avoid the "third-party server" CASA trigger.
|
||||
- Sync logic in a webview.
|
||||
- The AI-chat-first inbox; the one keyboard user who showed up to Zero's launch walked away.
|
||||
|
||||
Sources: https://github.com/Mail-0/Zero, https://github.com/Mail-0/Zero/blob/staging/apps/server/wrangler.jsonc, https://github.com/Mail-0/Zero/blob/staging/apps/server/src/lib/driver/google.ts, https://github.com/Mail-0/Zero/blob/staging/apps/server/src/lib/factories/google-subscription.factory.ts, https://github.com/Mail-0/Zero/blob/staging/apps/server/src/lib/gmail-rate-limit.ts, https://github.com/Mail-0/Zero/issues/1839, https://0.email, https://www.ycombinator.com/launches/NTI-zero-ai-native-email, https://www.ycombinator.com/companies/orchid-ai, https://news.ycombinator.com/item?id=43862892, https://github.com/gauteh/lieer, https://github.com/gauteh/lieer/blob/master/docs/index.md, https://github.com/avihaymenahem/velo, https://github.com/QingJ01/Pebble, https://github.com/Foundry376/Mailspring, https://github.com/Foundry376/Mailspring-Sync, https://support.mozilla.org/en-US/kb/thunderbird-and-gmail, https://www.nylas.com/blog/sunsetting-nylas-mail-development/, https://developers.google.com/gmail/api/auth/scopes, https://developers.google.com/identity/protocols/oauth2/production-readiness/restricted-scope-verification, https://support.google.com/cloud/answer/15549945, https://developers.google.com/workspace/gmail/api/reference/quota, https://developers.google.com/workspace/gmail/api/guides/push, https://developers.google.com/gmail/api/guides/threads, https://deepstrike.io/blog/google-casa-security-assessment-2025, https://www.unipile.com/integrating-google-oauth-2-0-user-authentication-into-your-app/
|
||||
|
||||
## Big Mail, Canary, Missive, Front
|
||||
|
||||
Big Mail (bigmail.app, by Phillip Caudell; bigmail.com is an unrelated placeholder) was a native Mac and iOS IMAP and Gmail client launched February 2021 that sorted mail into "Scenes" (Conversations, Newsletters, Purchases, Events, Notifications), blocked trackers by default and had a HEY-style "Bouncer" for screening new senders, at $10 a month or $6.49 annual. A "Big Mail 2" rewrite never left TestFlight; the developer's last post is February 2024 and the forum's most recent thread is titled "Big Mail 2 TestFlight is dead" (March 2025). It is the closest anyone came to our product on native Apple frameworks, and it died as a one-person project; treat it as a warning about scope, not a competitor.
|
||||
|
||||
Canary Mail (canarymail.io) is a native macOS, iOS, Windows and Android client for Gmail (Google OAuth), Exchange, iCloud, Yahoo and IMAP, sold on PGP and password-protected "SecureSend" plus an AI copilot, with pixel-based read receipts and tracker blocking on by default. Annual or lifetime only: Free, Growth $36 a year (about $3 a month), Pro+ $100 a year (about $10). Three-pane, customisable shortcuts, snooze and undo on every tier, send later on Growth and up.
|
||||
|
||||
Missive (missiveapp.com) is a shared team inbox with chat over Gmail, Outlook and IMAP on web, macOS, Windows, iOS and Android (no Linux). Per user per month: Starter $18 or $14 annual, Productive $30 or $24, Business $45 or $36, no free plan. Gmail keyboard preset (`j`/`k`, `e`, `c`), Cmd-K command bar, snooze, send later, undo send, rules. It removed read receipts in September 2020 on privacy grounds and auto-blocks read trackers, which is the right call and rare in the team-inbox category.
|
||||
|
||||
Front (front.com) is the enterprise shared inbox and ticketing tool: web, Mac, Windows, iOS, Android; Gmail via Google OAuth two-way sync (API or IMAP not stated), Office 365, IMAP. Per seat per month: Starter $35 or $25 annual (up to 10 seats), Professional $85 or $65 (up to 50), Enterprise $105 annual. Snooze, send later, undo send, pixel-based "Seen receipts" per channel, Front and Gmail keyboard schemes with Cmd-K.
|
||||
|
||||
Sources: https://bigmail.app/, https://discuss.bigmail.app/t/big-mail-2-testflight-is-dead/211.json, https://discuss.bigmail.app/t/app-development-update/188.json, https://sixcolors.com/post/2021/06/big-mail-may-not-be-my-next-email-client-but-its-aiming-at-the-future/, https://9to5mac.com/2021/01/22/big-mail-email-radical-new-ui/, https://canarymail.io/pricing, https://canarymail.io/features/security, https://canarymail.io/help/read-receipts-mac-iphone, https://missiveapp.com/pricing, https://missiveapp.com/download, https://missiveapp.com/docs/advanced-features/shortcuts, https://missiveapp.com/blog/life-and-death-of-read-tracking, https://front.com/pricing, https://help.front.com/en/articles/2189, https://help.front.com/en/articles/2034, https://help.front.com/en/articles/2072
|
||||
|
||||
## Comparison table
|
||||
|
||||
Prices are USD per month; "annual" is the per-month rate when billed yearly. Reading pane: "optional" means the user can switch between a split pane and a full-page thread. Sender screening means HEY-style accept or block of first-time senders, not spam filtering. Keyboard-first: strong means full keyboard navigation with `j`/`k` and a command palette, ok means a decent shortcut set, weak means mouse-first. HEY and Superhuman appear here only for comparison; they have their own documents.
|
||||
|
||||
| App | Backend | Platforms | Price per month | Reading pane | Sender screening | Split or bundled inbox | Snooze | Send later | Undo send | Read receipts | Keyboard-first | Tracker blocking | Open source |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| HEY | Own service | Web, Mac, Windows, Linux, iOS, Android | $99 a year personal (about $8.25, no monthly); Domains $12 per user | No (page per thread) | Yes (The Screener) | Imbox, Feed, Paper Trail | Yes (Bubble Up) | Yes | Yes | No | Strong | Yes | No |
|
||||
| Superhuman | Gmail API, Outlook | Mac, Windows, web, iOS, Android | Starter $30 ($25 annual); Business $40 ($33 annual); no free tier | Optional (split by default) | No | Split Inbox by filters and AI | Yes | Yes | Yes | Yes (Read Statuses) | Strong | Partial (opt-in pixel blocking) | No |
|
||||
| Mimestream | Gmail API only | macOS (iOS in TestFlight) | $4.99 ($4.17 annual); 14-day trial; no free tier | Yes (three-pane) | No | Gmail category inboxes | Partial (client-only, Labs) | No | Yes | No | Ok (Gmail `j`/`k` set, no palette) | Yes (default on) | No |
|
||||
| Shortwave | Gmail API only | Web, Mac, Windows, iOS, Android | Business $30 ($24 annual); Premier $45 ($36); Max $120 ($100); no free tier | Optional (side panel, fullscreen) | No | Splits as tabs plus label and sender bundles | Yes | Yes | Yes (server-side, 10 s) | Yes (paid, all tiers) | Strong | Yes (proxy plus pixels) | No |
|
||||
| Notion Mail | Gmail API only | Web, Mac, iOS; shuts down 22 Sep 2026 | Free; AI needs Notion Business $20 per seat annual | Optional (side peek, centre peek, full page) | No | Views (saved filter and group) plus AI auto-label | Yes ("Set reminder") | Yes | Unverified | No | Strong | Partial (image proxy, no pixel blocking) | No |
|
||||
| Spark | IMAP, Exchange, Gmail via OAuth (transport unverified); tokens held on Readdle servers | Mac, Windows, iOS, Android | Free; Plus $10 ($8.25 annual); Pro $20 ($16.58 annual) | Optional (Split View setting) | Yes (Gatekeeper, paid) | Smart Inbox: People, Notifications, Newsletters | Yes | Yes (server-side) | Yes (5 s) | Paid (Pro, pixel-based) | Ok (Cmd-K, presets, no `j`/`k` by default) | Partial (1x1 pixels only) | No |
|
||||
| Apple Mail | IMAP, Exchange, iCloud | macOS, iOS, iPadOS | Free | Yes | No | Categories: Primary, Transactions, Updates, Promotions | No (Remind Me) | Yes (Mac must be awake) | Yes (10 s) | No | Weak (modifier chords only) | Yes (Mail Privacy Protection, opt-in) | No |
|
||||
| Fastmail | Own service (JMAP, IMAP) | Web, iOS, Android, desktop wrapper for Mac, Windows, Linux | $6 ($5 annual); Duo $10; Family $14; no free tier | Optional | No | None (labels or folders, rules, pins) | Yes | Yes | Yes (15 s) | Request only | Strong (`j`/`k`, `y`, `g` jump; not remappable) | Yes (images proxied, no count) | No (open protocol) |
|
||||
| Proton Mail | Own service (E2E; Bridge for IMAP, paid) | Web, Windows, Mac, Linux, iOS, Android | Free; Mail Plus $4.99 ($3.99 annual); Unlimited $12.99 ($9.99 annual) | Optional (column or row) | No | Categories rolling out in 2026 (unverified detail) | Yes | Yes | Yes (0 to 20 s) | Request only | Ok (non-standard keys, Shift-Space palette) | Yes (default on, count badge, link cleaning) | Yes (clients) |
|
||||
| Zero / Mail-0 | Gmail API, Microsoft Graph; hosted or self-hosted web app | Web | Free (one account); Pro $20 (about $10 annual), unverified | Yes | No | Category tabs plus AI labels | Yes | Yes | Unverified | Unverified | Weak (Cmd-K exists, no real list navigation) | Unverified | Yes (MIT) |
|
||||
| Big Mail | IMAP, Gmail | Mac, iOS, iPadOS; abandoned | $10 ($6.49 annual) in 2021 | Yes | Yes (The Bouncer) | Scenes by type | Unverified | Unverified | Yes | No | Weak | Yes (default on) | No |
|
||||
| Canary | Gmail via OAuth, Exchange, iCloud, Yahoo, IMAP | Mac, Windows, iOS, Android | Free; Growth about $3 ($36 a year); Pro+ about $10 ($100 a year); no monthly billing | Yes | No | AI prioritisation | Yes | Yes (Growth and up) | Yes (5 s) | Yes (pixel-based) | Ok (customisable) | Yes (default on) | No |
|
||||
| Missive | Gmail, Outlook, IMAP (Gmail transport unverified) | Web, Mac, Windows, iOS, Android | Starter $18 ($14 annual); Productive $30 ($24); Business $45 ($36) per user | Yes | No | Team inboxes, rules, filtered views | Yes | Yes | Yes | No (removed 2020) | Strong (Gmail preset, Cmd-K, remappable) | Yes | No |
|
||||
| Front | Gmail via OAuth (transport not stated), Office 365, IMAP | Web, Mac, Windows, iOS, Android | Starter $35 ($25 annual); Professional $85 ($65); Enterprise $105 annual, per seat | Yes | No | Shared inboxes, tags, rules, views | Yes | Yes | Yes | Yes (Seen receipts) | Ok (Gmail scheme, Cmd-K) | Unverified | No |
|
||||
|
||||
## Patterns across the field
|
||||
|
||||
Everyone has converged on the same verbs. Archive is called Done and bound to `E` in Shortwave, Spark and Missive; snooze, send later and a 5 to 15 second undo are table stakes; and every client that takes keyboards seriously ships Gmail's `j`/`k`/`e`/`c`/`/`/`g`-then-letter set, as the default (Shortwave, Notion Mail, Fastmail) or as a preset (Mimestream, Spark, Missive, Front). Cmd-K is expected: seven of the fourteen have one, and the two serious clients without it (Mimestream, Fastmail) get asked for it in every review. Image proxying or pixel blocking is universal; the only variable is whether the client says what it blocked (Proton's count badge) or stays silent (Fastmail, Notion, Apple).
|
||||
|
||||
Layout has settled on a toggle, not a position. HEY is the only page-per-thread holdout; Superhuman, Shortwave, Notion Mail, Spark and Proton all make side pane versus full page a setting, and the rest are three-pane. Ship the toggle and pick a calm default.
|
||||
|
||||
The real fork is how mail gets sorted before you see it. One camp classifies automatically: Gmail's tabs, Apple's on-device buckets, Spark's People, Notifications, Newsletters, Notion's AI auto-label, Proton's new categories, Zero's AI labels. The other asks the human once per sender: HEY's Screener, Spark's Gatekeeper, Big Mail's Bouncer. Automatic sorting is what people complain about (Six Colors on Apple, the "six inboxes" thread, Spark's opaque chips); screening is what people who have it refuse to give up, and only Spark offers both, paid and through a credential relay. Beside that, labels (Mimestream), saved views (Notion, Front, Missive) and split tabs (Superhuman, Shortwave, Zero) are three names for a saved query over labels with a group-by. Notion was the only one to say so; Shortwave's collapsed bundle row is the only one that made the query a single object you can act on with one key.
|
||||
|
||||
The gaps are specific. No native desktop client on the Gmail API does sender screening; Mimestream is the only native Gmail-API client and it has no screening, no palette, no bundles and Apple Mail's row density. Nobody has solved snooze and scheduled send on the Gmail API without a server holding tokens: Mimestream refuses and ships neither honestly, the others run servers with mailbox access, and the obvious middle (snooze as remove-INBOX plus a label, woken by whichever device is on; a `gmail.send`-only service for scheduled send) is on Mimestream's roadmap and built by nobody. Shortwave, Notion Mail and Zero advertise multi-account and have no unified inbox. Linux has no native Gmail client at all. Gmail's category settings cannot be read through the API, so every client that shows categories guesses. List-Unsubscribe is surfaced well only by Proton and Gmail. Every open-source Gmail-API client is a web app or keeps its sync in a webview, and the one native attempt that came close, Big Mail, was one person and stopped. The combination we want (native, Gmail API, screening plus categories from existing labels, dense quiet list, `j`/`k` plus Cmd-K, tokens on device, no AI in the critical path) does not exist, but every piece of it has been proven separately by someone on this list.
|
||||
|
After Width: | Height: | Size: 645 KiB |
|
After Width: | Height: | Size: 444 KiB |
|
After Width: | Height: | Size: 838 KiB |
|
After Width: | Height: | Size: 410 KiB |
|
After Width: | Height: | Size: 423 KiB |
|
After Width: | Height: | Size: 240 KiB |
|
After Width: | Height: | Size: 459 KiB |
|
After Width: | Height: | Size: 628 KiB |
|
After Width: | Height: | Size: 237 KiB |
|
After Width: | Height: | Size: 282 KiB |
|
After Width: | Height: | Size: 496 KiB |
|
After Width: | Height: | Size: 465 KiB |
|
After Width: | Height: | Size: 532 KiB |
|
After Width: | Height: | Size: 319 KiB |
|
After Width: | Height: | Size: 566 KiB |
|
After Width: | Height: | Size: 662 KiB |
|
After Width: | Height: | Size: 505 KiB |
|
After Width: | Height: | Size: 666 KiB |
|
After Width: | Height: | Size: 422 KiB |
|
After Width: | Height: | Size: 713 KiB |
|
After Width: | Height: | Size: 564 KiB |
|
After Width: | Height: | Size: 411 KiB |
|
After Width: | Height: | Size: 383 KiB |
|
After Width: | Height: | Size: 196 KiB |
|
After Width: | Height: | Size: 515 KiB |
|
After Width: | Height: | Size: 412 KiB |
|
After Width: | Height: | Size: 200 KiB |
|
After Width: | Height: | Size: 434 KiB |
|
After Width: | Height: | Size: 105 KiB |
|
After Width: | Height: | Size: 251 KiB |
|
After Width: | Height: | Size: 421 KiB |
|
After Width: | Height: | Size: 176 KiB |
|
After Width: | Height: | Size: 282 KiB |
|
After Width: | Height: | Size: 536 KiB |
|
After Width: | Height: | Size: 529 KiB |
|
After Width: | Height: | Size: 465 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 549 KiB |
|
After Width: | Height: | Size: 275 KiB |
|
After Width: | Height: | Size: 465 KiB |
|
After Width: | Height: | Size: 315 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 264 KiB |
|
After Width: | Height: | Size: 490 KiB |
|
After Width: | Height: | Size: 529 KiB |
|
After Width: | Height: | Size: 284 KiB |
|
After Width: | Height: | Size: 532 KiB |
|
After Width: | Height: | Size: 419 KiB |
|
After Width: | Height: | Size: 185 KiB |
|
After Width: | Height: | Size: 490 KiB |
|
After Width: | Height: | Size: 510 KiB |
|
After Width: | Height: | Size: 530 KiB |
|
After Width: | Height: | Size: 179 KiB |
|
After Width: | Height: | Size: 317 KiB |
|
After Width: | Height: | Size: 455 KiB |
|
After Width: | Height: | Size: 313 KiB |
|
After Width: | Height: | Size: 285 KiB |
|
After Width: | Height: | Size: 434 KiB |
|
After Width: | Height: | Size: 275 KiB |
|
After Width: | Height: | Size: 274 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 44 KiB |