build the app, and ship it on Linux and Windows as well as macOS
The client itself: the sync engine and mirror, the Gmail and IMAP providers, the screener, the three boxes and two piles, compose and send, the sanitiser and tracker stripping, contacts, clips, backup, notifications and the guide. The pipeline that ships it. Three runners now build in parallel and the publish gate wants all four platform keys in latest.json before a release leaves draft. Linux gets two more packages Tauri does not build: a flatpak repackaged from the deb against GNOME 48, and a Nix package that relinks the deb against nixpkgs so it runs as a native Wayland client rather than through Xwayland. CI runs the Rust suite on all three desktops rather than on Linux alone, and rebuilds the flatpak manifest on every push to main. Two things that were only ever exercised on macOS and were wrong everywhere else. The menu bar was built by adjusting the submenus macOS is given, so on a platform whose default menu has no File or View the new File menu landed after Edit, Check for Updates landed nowhere, and the three places and the reading pane had no menu at all. The deb declared libwebkit2gtk-4.1-0 and libgtk-3-0 twice, because tauri.conf.json named the dependencies Tauri already emits. The updater key exists now, so releases can sign their artifacts.
No files matched your search
@@ -50,8 +50,20 @@ jobs:
|
|||||||
|
|
||||||
- run: node scripts/docs-check.mjs
|
- run: node scripts/docs-check.mjs
|
||||||
|
|
||||||
|
# Every desktop the release ships to, because the parts of this crate that differ by platform are
|
||||||
|
# the parts nobody runs by hand: the UserNotifications bridge on macOS, the D-Bus one everywhere
|
||||||
|
# else, the machine id the refresh tokens are sealed against. A break in one of those used to
|
||||||
|
# surface at release time on a runner nobody was watching.
|
||||||
rust:
|
rust:
|
||||||
runs-on: ubuntu-22.04
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
# Ubuntu 22.04 is the glibc baseline the release builds on, so it is what CI tests on.
|
||||||
|
- os: ubuntu-22.04
|
||||||
|
- os: macos-26
|
||||||
|
- os: windows-latest
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: rust/margin-mail
|
working-directory: rust/margin-mail
|
||||||
@@ -61,6 +73,7 @@ jobs:
|
|||||||
path: rust/margin-mail
|
path: rust/margin-mail
|
||||||
|
|
||||||
- name: Install Linux dependencies
|
- name: Install Linux dependencies
|
||||||
|
if: runner.os == 'Linux'
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y \
|
sudo apt-get install -y \
|
||||||
@@ -79,14 +92,107 @@ jobs:
|
|||||||
- uses: swatinem/rust-cache@v2
|
- uses: swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
workspaces: rust/margin-mail/src-tauri -> target
|
workspaces: rust/margin-mail/src-tauri -> target
|
||||||
|
key: ${{ matrix.os }}
|
||||||
|
|
||||||
# tauri_build::build() wants a frontendDist that exists, and build.rs wants credentials to
|
# 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.
|
# embed. The example file is what a fresh clone compiles against, so that is what CI uses.
|
||||||
- name: Stub the build inputs
|
- name: Stub the build inputs
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
mkdir -p dist && touch dist/index.html
|
mkdir -p dist && touch dist/index.html
|
||||||
cp google-credentials.example.json google-credentials.json
|
cp google-credentials.example.json google-credentials.json
|
||||||
|
|
||||||
|
# The gate docs/release.md names is the test suites, and that is all this enforces. `cargo fmt
|
||||||
|
# --check` and `cargo clippy -D warnings` both fail on the tree as it stands; adopting either
|
||||||
|
# is a cleanup pass to decide on separately, not something to bolt onto CI first.
|
||||||
- name: Test
|
- name: Test
|
||||||
working-directory: rust/margin-mail/src-tauri
|
working-directory: rust/margin-mail/src-tauri
|
||||||
run: cargo test
|
run: cargo test
|
||||||
|
|
||||||
|
# The flake at whatever release nix/release.json pins. A broken flake, or a deb that no longer
|
||||||
|
# patches against current nixpkgs, shows up here rather than at the next release. Before the
|
||||||
|
# first release there is nothing pinned and nothing to build.
|
||||||
|
nix:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
|
- id: pin
|
||||||
|
run: echo "version=$(jq -r '.version // ""' nix/release.json)" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- uses: cachix/install-nix-action@v31
|
||||||
|
if: steps.pin.outputs.version != ''
|
||||||
|
|
||||||
|
# --impure with the environment variable because the licence is FSL rather than MIT, so
|
||||||
|
# nixpkgs treats the package as unfree and refuses to build it otherwise.
|
||||||
|
- if: steps.pin.outputs.version != ''
|
||||||
|
run: NIXPKGS_ALLOW_UNFREE=1 nix build --impure .#margin-mail --print-build-logs
|
||||||
|
|
||||||
|
- if: steps.pin.outputs.version == ''
|
||||||
|
run: echo "nix/release.json pins no release yet, so there is nothing to build."
|
||||||
|
|
||||||
|
# The flatpak manifest is hand-written rather than produced by Tauri, so nothing else would catch
|
||||||
|
# a runtime that stopped carrying webkit2gtk-4.1 or a permission the app needs and does not ask
|
||||||
|
# for. The release job repackages the published deb; this one builds the same manifest against a
|
||||||
|
# deb built here, which is the only difference between them.
|
||||||
|
#
|
||||||
|
# On main rather than on every pull request: it is a full release-mode build plus a runtime
|
||||||
|
# download, the manifest changes about once a year, and a break in it is worth finding within the
|
||||||
|
# day rather than within the minute.
|
||||||
|
flatpak:
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
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
|
||||||
|
|
||||||
|
- 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 \
|
||||||
|
flatpak \
|
||||||
|
flatpak-builder
|
||||||
|
|
||||||
|
- 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
|
||||||
|
|
||||||
|
- uses: swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
workspaces: rust/margin-mail/src-tauri -> target
|
||||||
|
key: flatpak
|
||||||
|
|
||||||
|
- run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- run: cp google-credentials.example.json google-credentials.json
|
||||||
|
|
||||||
|
- run: pnpm tauri build --bundles deb
|
||||||
|
|
||||||
|
- name: Build the flatpak
|
||||||
|
run: |
|
||||||
|
mv src-tauri/target/release/bundle/deb/*.deb flatpak/margin-mail.deb
|
||||||
|
flatpak/build.sh
|
||||||
@@ -44,6 +44,12 @@ jobs:
|
|||||||
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' 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
|
jq --arg v "$VERSION" '.version = $v' package.json > "$tmp" && mv "$tmp" package.json
|
||||||
sed -i "0,/^version = \".*\"/s//version = \"$VERSION\"/" src-tauri/Cargo.toml
|
sed -i "0,/^version = \".*\"/s//version = \"$VERSION\"/" src-tauri/Cargo.toml
|
||||||
|
# Cargo.lock records margin-mail's own version, so bumping only Cargo.toml leaves the lock
|
||||||
|
# a release behind and the next build rewrites it under whoever checked it out.
|
||||||
|
awk -v v="$VERSION" '
|
||||||
|
/^name = "margin-mail"$/ { print; getline; sub(/^version = ".*"/, "version = \"" v "\""); print; next }
|
||||||
|
{ print }
|
||||||
|
' src-tauri/Cargo.lock > "$tmp" && mv "$tmp" src-tauri/Cargo.lock
|
||||||
|
|
||||||
- name: Commit and tag
|
- name: Commit and tag
|
||||||
env:
|
env:
|
||||||
@@ -51,7 +57,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
git config user.name "github-actions[bot]"
|
git config user.name "github-actions[bot]"
|
||||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
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 add src-tauri/tauri.conf.json package.json src-tauri/Cargo.toml src-tauri/Cargo.lock
|
||||||
git commit -m "chore(release): $TAG"
|
git commit -m "chore(release): $TAG"
|
||||||
for attempt in 1 2 3 4 5; do
|
for attempt in 1 2 3 4 5; do
|
||||||
git fetch origin main
|
git fetch origin main
|
||||||
@@ -93,6 +99,9 @@ jobs:
|
|||||||
- os: ubuntu-22.04
|
- os: ubuntu-22.04
|
||||||
args: "--config src-tauri/tauri.release.conf.json"
|
args: "--config src-tauri/tauri.release.conf.json"
|
||||||
rust-targets: ""
|
rust-targets: ""
|
||||||
|
- os: windows-latest
|
||||||
|
args: "--config src-tauri/tauri.release.conf.json"
|
||||||
|
rust-targets: ""
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
@@ -109,7 +118,7 @@ jobs:
|
|||||||
path: python/margin
|
path: python/margin
|
||||||
|
|
||||||
- name: Install Linux dependencies
|
- name: Install Linux dependencies
|
||||||
if: startsWith(matrix.os, 'ubuntu')
|
if: runner.os == 'Linux'
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y \
|
sudo apt-get install -y \
|
||||||
@@ -141,6 +150,7 @@ jobs:
|
|||||||
- uses: swatinem/rust-cache@v2
|
- uses: swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
workspaces: rust/margin-mail/src-tauri -> target
|
workspaces: rust/margin-mail/src-tauri -> target
|
||||||
|
key: ${{ matrix.os }}
|
||||||
|
|
||||||
- name: Install frontend dependencies
|
- name: Install frontend dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
@@ -160,6 +170,42 @@ jobs:
|
|||||||
echo "::warning::GOOGLE_CREDENTIALS secret not set, embedding placeholder credentials; this build cannot connect to Gmail."
|
echo "::warning::GOOGLE_CREDENTIALS secret not set, embedding placeholder credentials; this build cannot connect to Gmail."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# macOS shows no notifications from a bundle that is not signed, so tauri.conf.json ad-hoc
|
||||||
|
# signs at minimum; a Developer ID from the secrets replaces that, and the App Store Connect
|
||||||
|
# key notarizes on top. Only what is present is exported, because Tauri takes an empty
|
||||||
|
# APPLE_SIGNING_IDENTITY for an identity and fails the signing step on it.
|
||||||
|
- name: Provision Apple signing
|
||||||
|
if: runner.os == 'macOS'
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||||
|
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||||
|
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||||
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
|
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
||||||
|
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
|
||||||
|
APPLE_API_KEY_P8: ${{ secrets.APPLE_API_KEY_P8 }}
|
||||||
|
run: |
|
||||||
|
if [ -z "$APPLE_CERTIFICATE" ] || [ -z "$APPLE_SIGNING_IDENTITY" ]; then
|
||||||
|
echo "::warning::APPLE_CERTIFICATE or APPLE_SIGNING_IDENTITY not set; the macOS bundle will be ad-hoc signed and not notarized."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
for name in APPLE_CERTIFICATE APPLE_CERTIFICATE_PASSWORD APPLE_SIGNING_IDENTITY APPLE_TEAM_ID; do
|
||||||
|
{ echo "$name<<MARGIN_EOF"; printf '%s\n' "${!name}"; echo "MARGIN_EOF"; } >> "$GITHUB_ENV"
|
||||||
|
done
|
||||||
|
echo "Signing as $APPLE_SIGNING_IDENTITY"
|
||||||
|
if [ -n "$APPLE_API_KEY_P8" ] && [ -n "$APPLE_API_KEY" ] && [ -n "$APPLE_API_ISSUER" ]; then
|
||||||
|
printf '%s' "$APPLE_API_KEY_P8" > "$RUNNER_TEMP/AuthKey.p8"
|
||||||
|
{
|
||||||
|
echo "APPLE_API_KEY=$APPLE_API_KEY"
|
||||||
|
echo "APPLE_API_ISSUER=$APPLE_API_ISSUER"
|
||||||
|
echo "APPLE_API_KEY_PATH=$RUNNER_TEMP/AuthKey.p8"
|
||||||
|
} >> "$GITHUB_ENV"
|
||||||
|
echo "Notarizing with App Store Connect key $APPLE_API_KEY"
|
||||||
|
else
|
||||||
|
echo "::warning::APPLE_API_KEY, APPLE_API_ISSUER or APPLE_API_KEY_P8 not set; the macOS bundle will be signed and not notarized."
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Build and upload
|
- name: Build and upload
|
||||||
uses: tauri-apps/tauri-action@v0
|
uses: tauri-apps/tauri-action@v0
|
||||||
with:
|
with:
|
||||||
@@ -171,8 +217,45 @@ jobs:
|
|||||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||||
|
|
||||||
publish:
|
- name: Verify the bundle is signed and notarized
|
||||||
|
if: runner.os == 'macOS' && env.APPLE_API_KEY_PATH != ''
|
||||||
|
run: |
|
||||||
|
app="src-tauri/target/universal-apple-darwin/release/bundle/macos/Margin Mail.app"
|
||||||
|
codesign --verify --deep --strict --verbose=2 "$app"
|
||||||
|
# Gatekeeper only says "accepted" once the notarization ticket is stapled to the bundle,
|
||||||
|
# so this is the check that somebody double-clicking the dmg will actually get past.
|
||||||
|
spctl --assess --type execute --verbose=4 "$app"
|
||||||
|
xcrun stapler validate "$app"
|
||||||
|
|
||||||
|
# The flatpak is not a Tauri bundle target, so it is built here from the deb the Linux job just
|
||||||
|
# published and uploaded to the same draft release. Before publish, so a release never goes out
|
||||||
|
# with the Linux artifacts half there.
|
||||||
|
flatpak:
|
||||||
needs: [prepare, build]
|
needs: [prepare, build]
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
with:
|
||||||
|
ref: ${{ needs.prepare.outputs.tag }}
|
||||||
|
|
||||||
|
- run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y flatpak flatpak-builder
|
||||||
|
|
||||||
|
- name: Build the flatpak from the published deb
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
TAG: ${{ needs.prepare.outputs.tag }}
|
||||||
|
VERSION: ${{ needs.prepare.outputs.version }}
|
||||||
|
run: |
|
||||||
|
gh release download "$TAG" --repo "$REPO" \
|
||||||
|
--pattern "Margin.Mail_${VERSION}_amd64.deb" --output flatpak/margin-mail.deb
|
||||||
|
flatpak/build.sh "$PWD/Margin.Mail_${VERSION}_amd64.flatpak"
|
||||||
|
gh release upload "$TAG" --repo "$REPO" "Margin.Mail_${VERSION}_amd64.flatpak" --clobber
|
||||||
|
|
||||||
|
publish:
|
||||||
|
needs: [prepare, build, flatpak]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Verify manifest is complete, then publish
|
- name: Verify manifest is complete, then publish
|
||||||
@@ -184,10 +267,71 @@ jobs:
|
|||||||
gh release download "$TAG" --repo "$REPO" --pattern latest.json --output latest.json --clobber
|
gh release download "$TAG" --repo "$REPO" --pattern latest.json --output latest.json --clobber
|
||||||
echo "Platforms in latest.json:"
|
echo "Platforms in latest.json:"
|
||||||
jq '.platforms | keys' latest.json
|
jq '.platforms | keys' latest.json
|
||||||
for key in darwin-aarch64 darwin-x86_64 linux-x86_64; do
|
for key in darwin-aarch64 darwin-x86_64 linux-x86_64 windows-x86_64; do
|
||||||
if ! jq -e ".platforms[\"$key\"].url" latest.json > /dev/null; then
|
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."
|
echo "::error::latest.json is missing platform '$key', refusing to publish a partial update manifest. Re-run the release."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
gh release edit "$TAG" --repo "$REPO" --draft=false --latest
|
gh release edit "$TAG" --repo "$REPO" --draft=false --latest
|
||||||
|
|
||||||
|
# Nix is the other Linux package. The AppImage carries Ubuntu's GTK stack, which cannot talk to a
|
||||||
|
# modern Wayland compositor and silently falls back to Xwayland; the Nix package relinks the
|
||||||
|
# published deb against nixpkgs' webkit2gtk and runs as a native Wayland client. Runs after
|
||||||
|
# publish so the flake can only ever point at a release that survived the manifest check.
|
||||||
|
nix:
|
||||||
|
needs: [prepare, publish]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
# main rather than the tag: the pin lands on main, and the tag was cut before the artifact
|
||||||
|
# it needs the hash of existed.
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
with:
|
||||||
|
ref: main
|
||||||
|
|
||||||
|
- name: Pin the flake to this release
|
||||||
|
env:
|
||||||
|
VERSION: ${{ needs.prepare.outputs.version }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
run: |
|
||||||
|
URL="https://github.com/$REPO/releases/download/v$VERSION/Margin.Mail_${VERSION}_amd64.deb"
|
||||||
|
# From the published asset, so the hash is of the artifact users will actually fetch.
|
||||||
|
curl -fsSL --retry 3 -o package.deb "$URL"
|
||||||
|
HASH="sha256-$(openssl dgst -sha256 -binary package.deb | base64)"
|
||||||
|
jq -n --arg v "$VERSION" --arg h "$HASH" '{version: $v, hash: $h}' > nix/release.json
|
||||||
|
cat nix/release.json
|
||||||
|
|
||||||
|
- uses: cachix/install-nix-action@v31
|
||||||
|
|
||||||
|
# Building it is the check: a wrong hash, a library autoPatchelf cannot find or a broken flake
|
||||||
|
# stops here rather than on someone's machine. --impure and the variable because FSL is not a
|
||||||
|
# free licence, so nixpkgs refuses to build the package without being told.
|
||||||
|
- name: Build the package
|
||||||
|
run: NIXPKGS_ALLOW_UNFREE=1 nix build --impure .#margin-mail --print-build-logs
|
||||||
|
|
||||||
|
- name: Commit the pin
|
||||||
|
env:
|
||||||
|
VERSION: ${{ needs.prepare.outputs.version }}
|
||||||
|
run: |
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
git add nix/release.json
|
||||||
|
# A rerun after the pin already landed has nothing to commit, and the release is done.
|
||||||
|
if git diff --cached --quiet; then
|
||||||
|
echo "The flake already points at v$VERSION, nothing to push."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
git commit -m "point the nix package at v$VERSION"
|
||||||
|
for attempt in 1 2 3 4 5; do
|
||||||
|
git fetch origin main
|
||||||
|
git rebase origin/main
|
||||||
|
if git push origin HEAD:main; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if [ "$attempt" = "5" ]; then
|
||||||
|
echo "::error::main kept advancing; could not push the nix pin after 5 attempts. The release is published; rerun this job."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "main advanced; rebasing and retrying ($attempt)…"
|
||||||
|
sleep 3
|
||||||
|
done
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
# Margin Mail
|
# Margin Mail
|
||||||
|
|
||||||
A calm, keyboard-first mail client for Gmail on macOS and iPhone, with the parts of HEY and
|
A calm, keyboard-first mail client for Gmail on macOS, Linux, Windows 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
|
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
|
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
|
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.
|
rather than inside Gmail, and it comes with you if you leave. It is a sibling to
|
||||||
|
[margin](https://github.com/priyanshujain/margin) and
|
||||||
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
|
[Margin Calendar](https://github.com/priyanshujain/margin-calendar) and shares their stack and
|
||||||
visual language.
|
visual language.
|
||||||
|
|
||||||
Nothing is built yet. The product is defined in [docs/design.md](docs/design.md), the features in
|
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/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/keyboard.md](docs/keyboard.md) and the settings in [docs/settings.md](docs/settings.md). How
|
||||||
[docs/architecture.md](docs/architecture.md). The research it rests on is in
|
it gets built is [docs/architecture.md](docs/architecture.md) and [docs/plan.md](docs/plan.md), how
|
||||||
[docs/research/](docs/research/).
|
it ships is [docs/release.md](docs/release.md), and the research it rests on is in
|
||||||
|
[docs/research/](docs/research/). To install it, [docs/install.md](docs/install.md).
|
||||||
|
|
||||||
Licensed FSL-1.1-MIT: use it for anything except building a competing product, and every version
|
Licensed FSL-1.1-MIT: use it for anything except building a competing product, and every version
|
||||||
turns MIT two years after its release.
|
turns MIT two years after its release.
|
||||||
@@ -2,8 +2,9 @@
|
|||||||
|
|
||||||
Tauri 2, React 19, Vite, TypeScript and zustand on the front, Rust behind. The same stack as
|
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
|
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
|
title bar and the phone chrome carry over rather than being invented again. The house rules for
|
||||||
the calendar's, in `../margin-caledar/docs/conventions.md`, and apply here unchanged.
|
writing it are in [conventions.md](conventions.md), which is the calendar's file with its
|
||||||
|
examples pointed here.
|
||||||
|
|
||||||
The split is strict. Rust owns authentication, every byte to and from a mail provider, the local
|
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
|
mirror, the sync loop, MIME parsing, HTML sanitising, the portable state database, the journal
|
||||||
@@ -12,29 +13,69 @@ provider, which keeps the content security policy locked to `ipc:` as in the sib
|
|||||||
|
|
||||||
The facts about the Gmail API that this design rests on are in
|
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
|
[research/gmail-api.md](research/gmail-api.md), checked against Google's pages on 3 September
|
||||||
2026.
|
2026. The milestones, the libraries and the order they land in are in [plan.md](plan.md).
|
||||||
|
|
||||||
## Two databases, one boundary
|
## Two databases, one boundary
|
||||||
|
|
||||||
There are two SQLite databases per account and the boundary between them is the product.
|
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,
|
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
|
an FTS5 index, the provider's sync cursor, and an outbox. It holds a window of the mailbox rather
|
||||||
be thrown away and rebuilt. Its keys are the provider's ids.
|
than all of it, which the next section explains. 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,
|
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
|
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
|
derived, it is never sent to the provider, and its keys are portable. A sender is their address
|
||||||
the RFC `Message-ID` of its earliest message (the thread key), a message by its own `Message-ID`,
|
or their domain. A message is its own `Message-ID`. A thread is the **thread key**: the first
|
||||||
a sender by their address or domain. Provider ids appear in the state database nowhere. When the
|
entry of the message's `References` header, or its `In-Reply-To` when there is no `References`,
|
||||||
user moves to another provider and the same mail arrives through IMAP with the same
|
or its own `Message-ID` when it starts the conversation.
|
||||||
`Message-ID`s, every decision reattaches.
|
|
||||||
|
That definition matters more than it looks. A key derived from the earliest message the mirror
|
||||||
|
happens to hold changes when the window moves, so the same thread would carry two keys on two
|
||||||
|
devices and lose its pile on the one that had less of it. A key read off the headers of any
|
||||||
|
message in the thread is the same everywhere: on a phone holding a month, on a laptop holding a
|
||||||
|
year, and in an IMAP mailbox reached two providers from now. Provider ids appear in the state
|
||||||
|
database nowhere.
|
||||||
|
|
||||||
A view is the mirror joined to the state. The Inbox is "threads whose sender rule says Inbox, or
|
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
|
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
|
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.
|
thread summaries; the frontend never sees a label id or a rule.
|
||||||
|
|
||||||
|
## The window
|
||||||
|
|
||||||
|
Mail on the device is a window, not the mailbox. Each account carries a `window` setting of 30,
|
||||||
|
90, 180 or 365 days, or everything, and it is 30 days out of the box. Thirty days is what a
|
||||||
|
mail client is actually used for, it turns a first sync from an afternoon into a few minutes,
|
||||||
|
and everything outside it is still one search away on the provider.
|
||||||
|
|
||||||
|
The mirror holds every thread whose latest message falls inside the window. On top of that it
|
||||||
|
holds, at any age: every thread that carries app state (a pile, a snooze, a note, a rename, a
|
||||||
|
merge, a clip, ignore, notify), every starred thread, every draft, and the outbox. A thread the
|
||||||
|
user has touched is a thread the user expects to find, and the state database would otherwise
|
||||||
|
point at rows that are not there.
|
||||||
|
|
||||||
|
Eviction runs once a day and again whenever the window shrinks. It removes bodies, attachments
|
||||||
|
and rows for threads that fall outside, leaving the state database untouched; nothing a person
|
||||||
|
decided is ever evicted. Widening the window is the mirror image: the newly covered range is
|
||||||
|
backfilled in the background, newest first, through the same hydration path as the first sync, so
|
||||||
|
there is one code path for filling the mirror and not two.
|
||||||
|
|
||||||
|
The arithmetic of the first sync is the reason for all of it. `messages.list` with an `after:`
|
||||||
|
term returns the ids for the window cheaply, and metadata hydration runs in batches of 50 at 20
|
||||||
|
units a message, which is about 300 messages a minute inside the per-user budget. A month of a
|
||||||
|
busy mailbox is minutes. The whole of it, at the same rate, is hours for twenty thousand messages
|
||||||
|
and most of a working day for a hundred thousand, which is what the setting exists to let someone
|
||||||
|
choose deliberately rather than discover.
|
||||||
|
|
||||||
|
The window is visible in two places and nowhere else. The Everything place ends with one quiet
|
||||||
|
line, "Showing the last month. Older mail is on Gmail.", with the setting one click away. Search
|
||||||
|
results end with "Search older mail on Gmail", which runs the provider's search and hydrates the
|
||||||
|
hits as transient rows; the next eviction pass removes them again unless they gained state in the
|
||||||
|
meantime. A query carrying a `before:` or `after:` that lands outside the window skips the local
|
||||||
|
index and goes straight to the provider, because a local answer would be confidently wrong.
|
||||||
|
|
||||||
## The provider trait
|
## The provider trait
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -54,10 +95,44 @@ trait Provider {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Gmail implements it with the REST API. IMAP and SMTP will implement it next with folders mapped
|
Gmail implements it with the REST API. A mailbox reached over IMAP and SMTP with a password is the
|
||||||
onto the flag set and labels onto `X-GM-LABELS` or keywords, and JMAP after that with its own
|
second implementation and is what every account that is not Google uses; JMAP is a third if anyone
|
||||||
change log. The trait is shaped by what every provider can do, and everything the trait cannot do
|
ever ships it. The trait is shaped by what every provider can do, and everything the trait cannot
|
||||||
is done in the state database instead, which is why the state database exists.
|
do is done in the state database instead, which is why the state database exists.
|
||||||
|
|
||||||
|
An account carries a `kind` saying which of the two it is. Almost nothing branches on it: the
|
||||||
|
places, the piles, the Screener, snoozes, notes and search are all above the trait and cannot tell
|
||||||
|
the difference. The screens that do differ are the ones about the account itself, because an IMAP
|
||||||
|
account has no scopes to grant and no Google account page to revoke from, and it has a server and
|
||||||
|
a port to show that a Google account does not.
|
||||||
|
|
||||||
|
IMAP specifics that live only inside that implementation: a per-mailbox cursor of UIDVALIDITY,
|
||||||
|
UIDNEXT and HIGHESTMODSEQ in place of a change log, CONDSTORE with QRESYNC for the fast path and
|
||||||
|
a UID scan when the server has neither, mailbox roles read from SPECIAL-USE and XLIST flags before
|
||||||
|
falling back to names, and folders standing in for labels so a label change is a MOVE. A UIDVALIDITY
|
||||||
|
change is reported as a full resync rather than remapped, which reuses a recovery path the engine
|
||||||
|
already had rather than inventing a second one.
|
||||||
|
|
||||||
|
Three consequences of IMAP that are worth knowing before they surprise somebody. There is no
|
||||||
|
server-side snippet, so the preview line on a list row is derived from the body the first time it
|
||||||
|
is fetched and is empty until then. There is no thread id, so the portable thread key does all the
|
||||||
|
work; it is computed by the same rule on both sides, so the two agree by construction. And there is
|
||||||
|
no equivalent of archiving: a mailbox with no `\Archive` special use and no folder named like one
|
||||||
|
gets an Archive mailbox created on first sync, because a keybinding that means something different
|
||||||
|
per account is worse than a folder somebody did not ask for.
|
||||||
|
|
||||||
|
Sign-in is a password, not OAuth. Both Thunderbird and Mailspring ship their own OAuth client
|
||||||
|
credentials in the binary for Google and Microsoft, and Mailspring's source says outright that
|
||||||
|
anyone can extract theirs. Doing OAuth over IMAP would mean registering another client for
|
||||||
|
`AUTH XOAUTH2`, so a provider that publishes OAuth as its first choice, which Gmail and Fastmail
|
||||||
|
both do, is offered its own second choice instead: an app password.
|
||||||
|
|
||||||
|
Certificates are decided in one place. The loopback is trusted without asking, because a local
|
||||||
|
bridge listens there with a certificate it generated for itself and nothing sits between this
|
||||||
|
process and that socket to impersonate anybody. Every other host is verified against the webpki
|
||||||
|
roots, and a failure becomes a question carrying a fingerprint, remembered per host and port. The
|
||||||
|
one exception is a certificate naming a different host: that is refused outright and never offered
|
||||||
|
as a choice, because it is the single failure indistinguishable from an interception.
|
||||||
|
|
||||||
Gmail specifics that live only inside the Gmail implementation: `history.list` as the change log
|
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
|
and its 404 recovery, the 6,000 units per minute per user budget, `format=metadata` with a fixed
|
||||||
@@ -65,21 +140,74 @@ header list for hydration, `format=raw` for bodies, batch requests of 25 to 50,
|
|||||||
exponential backoff on 429, threading rules on send, `CATEGORY_*` labels read as a hint for the
|
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.
|
suggestion function, and the People API for contact autocomplete.
|
||||||
|
|
||||||
## Authentication and distribution
|
## One client for the whole suite
|
||||||
|
|
||||||
Loopback OAuth with PKCE from the calendar, unchanged: the consent page opens in the system
|
Margin, Margin Calendar and Margin Mail share one Google Cloud project, `margin-500217`, and one
|
||||||
browser, never in a webview the app owns, and the code lands on `127.0.0.1`. Refresh tokens are
|
OAuth client of type installed. A person who opens the third party access page of their Google
|
||||||
|
account sees a single entry called "Margin", and revoking it revokes the suite. That is the right
|
||||||
|
shape: three apps by the same author, on the same machine, holding the same person's data, should
|
||||||
|
not look like three vendors.
|
||||||
|
|
||||||
|
Mechanically it is the calendar's arrangement unchanged. `build.rs` copies
|
||||||
|
`google-credentials.json` from the repository root into `OUT_DIR`, falling back to
|
||||||
|
`google-credentials.example.json` when the real file is absent, so a fresh clone compiles and
|
||||||
|
fails at runtime with a readable "not set up yet" rather than at build time with a missing file.
|
||||||
|
Loopback OAuth with PKCE, also from the calendar: the consent page opens in the system browser,
|
||||||
|
never in a webview the app owns, and the code comes back on `127.0.0.1`. Refresh tokens are
|
||||||
sealed with XChaCha20-Poly1305 in the app data directory as the calendar does, for the same
|
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
|
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`,
|
Linux).
|
||||||
`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
|
Connect asks for `openid email`, `gmail.modify`, `gmail.settings.basic`, `contacts.readonly` and
|
||||||
consent screen to production unverified for the friends release (100 lifetime users, no weekly
|
`contacts.other.readonly`. It does not ask for `calendar.events`, and this is the one place where
|
||||||
re-login), file restricted-scope verification at once with the statement that there is no server
|
Google's rules cost the user something: installed apps get no incremental authorization, so a
|
||||||
and all Google user data stays on the device, ask in writing whether the security assessment
|
scope cannot be added to a live token. The first time somebody answers an invite, the app runs
|
||||||
applies, and budget for it anyway. Bring-your-own OAuth client stays as an escape hatch in
|
the whole authorization again with `calendar.events` in the list and replaces the stored token.
|
||||||
settings for the technical.
|
Backup does the same with `drive.file` when it is turned on. Both say so before they start.
|
||||||
|
|
||||||
|
Granular consent is always on for this client, which means the tick boxes on the consent page are
|
||||||
|
the user's to clear and the app cannot assume it got what it asked for. The `scope` field of the
|
||||||
|
token response is stored per account and is the truth. Every feature that needs a scope checks it
|
||||||
|
first and, when it is missing, renders one sentence and a Grant button that re-runs consent for
|
||||||
|
the full list. Without `gmail.modify` there is no app at all, so the account is not added and the
|
||||||
|
screen says which permission was declined and what it was for.
|
||||||
|
|
||||||
|
### What the shared client costs
|
||||||
|
|
||||||
|
`gmail.modify` and `gmail.settings.basic` are restricted scopes. The shared project must pass
|
||||||
|
restricted scope verification, and until it does, every Margin app shows the unverified screen and
|
||||||
|
the three of them share one pool of 100 lifetime users. Verification has to be renewed annually,
|
||||||
|
and a lapse blocks the whole suite rather than one app. That is the price of the single "Margin"
|
||||||
|
entry, and it is worth stating in the same breath as the benefit.
|
||||||
|
|
||||||
|
The plan is the one in the research: push to production unverified for the friends release, file
|
||||||
|
restricted scope verification immediately 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 in settings as the escape hatch for the
|
||||||
|
technical.
|
||||||
|
|
||||||
|
### The hedge
|
||||||
|
|
||||||
|
If verification is refused, or priced at a number that is not worth paying, Gmail over IMAP and
|
||||||
|
SMTP with a Google app password is the way in. It needs no Cloud project, no verification, and it
|
||||||
|
has no user cap. Gmail's IMAP extensions carry the pieces the REST API was giving us:
|
||||||
|
`X-GM-LABELS` for labels, `X-GM-THRID` for the provider's thread id, `X-GM-MSGID` for a stable
|
||||||
|
per-message id, all of which land in the same mirror columns. IDLE and CONDSTORE replace
|
||||||
|
`history.list` as the change log. RSVP goes out as an iMIP reply by mail instead of through the
|
||||||
|
Calendar API. Contacts come from the mirror, which is where autocomplete looks first anyway.
|
||||||
|
|
||||||
|
This is not a rewrite, it is the `Provider` trait's second implementation, and knowing that is
|
||||||
|
half the reason the trait is drawn where it is.
|
||||||
|
|
||||||
|
Removing an account therefore reaches further than this device. It hands the grant back to Google
|
||||||
|
first, and because the endpoint acts on the authorization rather than on the string it is handed,
|
||||||
|
that signs the person out of every Margin app on every machine they own; the confirmation says so
|
||||||
|
before it runs. Then the token and the registry entry go, and by default the account's mirror and
|
||||||
|
state database with them. The confirmation carries one box, ticked, for that last part: unticked,
|
||||||
|
the pair is moved from `accounts/<id>` to `kept/<id>`, where nothing that enumerates accounts can
|
||||||
|
see it, and it is moved back the day the same account is added again, decisions and all. An IMAP
|
||||||
|
account has nothing at Google to hand back, so removing one forgets its passwords and stops there.
|
||||||
|
The two actions used to be two buttons, Remove and Revoke, and read as a choice nobody could make.
|
||||||
|
|
||||||
## Sync
|
## Sync
|
||||||
|
|
||||||
@@ -88,17 +216,61 @@ grant no desktop app should hold, and a relay is the thing that might drag the a
|
|||||||
annual security assessment. `history.list` costs 2 units; polling every 12 seconds in the
|
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.
|
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,
|
Initial sync fills the window, newest first, and the app is usable as soon as the first page
|
||||||
so hydration runs at about 300 messages a minute per account. A 20,000 message mailbox takes
|
lands. A thin bar in the account chip says how far the mirror has got. Attachments are fetched on
|
||||||
about an hour of background work; a 100,000 message mailbox most of a working day. The order is
|
open and cached under a size cap. An account that fails repeatedly is paused by a circuit breaker
|
||||||
newest first, the app is usable as soon as the first page lands, and a thin bar in the account
|
rather than retried into a rate limit, and the account chip says so.
|
||||||
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
|
An account joins the engine the moment it is connected, and its first pass starts then rather
|
||||||
is the whole mailbox by decision; a setting caps the age for people who want less on disk.
|
than at the next tick, which can be a minute away while the consent browser has the focus. One
|
||||||
|
pass at a time per account: a pass somebody asked for while the loop's is running gets the status
|
||||||
|
the running one is producing, and a first sync is never listed twice. A first sync that a quit or
|
||||||
|
a tunnel cut short is picked up by the next pass and reported the same way, with the count
|
||||||
|
carrying on from where it stopped, so an account still arriving never looks idle.
|
||||||
|
|
||||||
|
Bodies are the exception, and the rule is worth stating plainly: **opening a thread never waits on
|
||||||
|
the network.** `thread_view` is a local read. A message whose body has not arrived comes back with
|
||||||
|
`bodyPending` and the pane draws a placeholder where the text goes, then `thread_hydrate` fetches
|
||||||
|
what is missing eight at a time and the bodies appear on a `store-changed` of scope `thread`, which
|
||||||
|
refreshes the open thread and deliberately does not touch the list.
|
||||||
|
|
||||||
|
Behind that, the cache warms itself. Once the first sync has finished, each foreground pass takes
|
||||||
|
the forty newest bodies it does not have, which is about two hundred a minute against Gmail's six
|
||||||
|
thousand units and leaves room for roughly a hundred explicit opens in the same minute. The phase
|
||||||
|
is `caching` while it runs and the header says so. Two exclusions keep the queue moving: a body the
|
||||||
|
provider will not give up is remembered for the life of the process, so a handful of unfetchable
|
||||||
|
messages at the head cannot stall everything behind them, and transient rows pulled in by a
|
||||||
|
provider search are skipped, because a body fetched for a row the next eviction pass deletes is
|
||||||
|
twenty units spent on nothing. Both are excluded from the progress count as well as the fetch: work
|
||||||
|
nobody is going to do is not work outstanding.
|
||||||
|
|
||||||
Every write is optimistic: it lands in the mirror, renders, and is pushed behind. Consecutive
|
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
|
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.
|
reconnect, with the thread showing "Waiting to send" until a send goes. A failure that is about
|
||||||
|
the connection or the account holds the queue; one the provider raised about the row itself (an
|
||||||
|
id it no longer has, a label it never had) is retried once and then dropped with its reason said
|
||||||
|
once, so a dead row never blocks the rows behind it. While a flag or label change is still queued,
|
||||||
|
the change log's word on those labels is applied under it rather than over it: what this device
|
||||||
|
decided is the truth about a message until the server has heard it.
|
||||||
|
|
||||||
|
Failures are handled the way Mailspring handles them, which is why nobody using Mailspring has seen
|
||||||
|
a sync error. A connection that dropped under a request (reset, closed before the answer, cut off in
|
||||||
|
the body) is tried again at once and then after a second, on a fresh connection, at the call site;
|
||||||
|
the pool keeps its connections alive with HTTP/2 pings so one the machine slept through is found
|
||||||
|
dead before a request lands on it. A pass that still fails is written down and otherwise kept
|
||||||
|
quiet: the chip does not move for one failure, because the next poll is twelve seconds away and is
|
||||||
|
the retry. It moves on the second in a row ("Offline" for the network, "Sync trouble" for the
|
||||||
|
rest) and the account pauses for five minutes on the fourth. Only three things are ever toasted:
|
||||||
|
the pause, because pressing sync is the way out of it; a refused token or a missing scope, because
|
||||||
|
nothing else mends them; and a write the provider refused for good, once, because the change did
|
||||||
|
not take. Sending a message and creating a draft are the two calls never repeated on a dropped
|
||||||
|
connection, since the first attempt may have gone through.
|
||||||
|
|
||||||
|
Every failure is also appended to `margin-mail.log` in the app data directory, capped at 256 KB:
|
||||||
|
each failed pass with the provider's sentence, each body that would not come, each command the
|
||||||
|
frontend called that answered with an error, and each uncaught error in the webview. An app
|
||||||
|
launched from the Finder has no stderr anybody will read, and a report of "sync failed" with
|
||||||
|
nothing behind it cannot be debugged.
|
||||||
|
|
||||||
Seen, starred, archived, trashed and spam are provider flags and go through the trait. Nothing in
|
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
|
the piles, the Screener, snoozes or notes ever touches the provider, so a user who screens out
|
||||||
@@ -112,18 +284,25 @@ payload. The tables are a materialised view of the journal. This is what makes r
|
|||||||
without a server and what makes the backup meaningful.
|
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,
|
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
|
list names under a prefix. Two implementations ship: Google Drive and Cloudflare R2 through S3
|
||||||
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
|
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,
|
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
|
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.
|
has been offline for a month simply replays what it missed.
|
||||||
|
|
||||||
|
The Drive implementation follows margin's, with one thing worth being accurate about: margin does
|
||||||
|
not use Drive's app-data space. It holds the `drive.file` scope and writes whole unencrypted files
|
||||||
|
into a visible folder named `margin`, which is deliberate, because a person should be able to see
|
||||||
|
their own backup. Margin Mail keeps the same scope, adds no new one, and writes encrypted journal
|
||||||
|
segments under `margin/mail/<account-hash>/<device-id>/`. Files created by the shared client are
|
||||||
|
visible to every Margin app, which is what makes one folder work for three of them. The HTTP
|
||||||
|
parts of margin's `gdrive.rs` port across; the encryption, the journal and the merge are new here.
|
||||||
|
|
||||||
Everything uploaded is encrypted on the device with XChaCha20-Poly1305 under a key that is
|
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.
|
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
|
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
|
the only way to attach a second device or restore after a lost one, and the Backup section of
|
||||||
so in one sentence.
|
settings says so in one sentence.
|
||||||
|
|
||||||
Version one on macOS alone does not need the merge. The journal shape is there from the first
|
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.
|
commit so that the iPhone can join without a migration.
|
||||||
@@ -134,18 +313,21 @@ Message bodies are parsed from raw RFC 2822 with a real MIME parser (`mail-parse
|
|||||||
the provider's pre-parsed payload beyond headers, because encoded words, parameter
|
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
|
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:`
|
webview: scripts, forms, event handlers, `<meta>` refreshes, external stylesheets, `javascript:`
|
||||||
and `data:` navigation are removed; `cid:` references are rewritten to a local resource scheme;
|
and `data:` navigation are removed; `cid:` references are rewritten to inline data so the body
|
||||||
every remote `<img>` is replaced with a placeholder and its source recorded. The body renders in
|
carries its own images. The body renders in a sandboxed iframe inside the app's webview so the
|
||||||
an iframe with a strict CSP inside the app's webview so the message can never touch the app.
|
message can never touch the app, and it renders on the paper surface in both palettes when it
|
||||||
|
arrived as HTML: mail written for a white page usually sets a text colour and no background, and
|
||||||
|
inverting it breaks more than it fixes. Plain text is rendered by this app rather than by its
|
||||||
|
sender, so it follows the theme like everything else.
|
||||||
|
|
||||||
Tracker stripping happens in the same pass: images with a known tracking host (a maintained
|
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
|
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
|
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
|
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,
|
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
|
which the Privacy section of settings says plainly. Outgoing mail never contains a tracker and the
|
||||||
requests a read receipt. Links are rewritten on click to drop known tracking parameters, with a
|
app never requests a read receipt. Links are rewritten on click to drop known tracking parameters,
|
||||||
setting to turn that off.
|
with a setting to turn that off.
|
||||||
|
|
||||||
Refresh tokens and the backup key are sealed, never in SQLite. The mirror and the state database
|
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
|
are files in the app data directory and inherit the OS's disk encryption; encrypting them again
|
||||||
@@ -158,31 +340,18 @@ macOS: overlay title bar with the traffic lights on the header's centre line, cl
|
|||||||
hides it and Cmd-Q quits, all from the calendar. iOS second: the same code with the phone chrome
|
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
|
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
|
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
|
snooze evaluation and a short sync.
|
||||||
AppImage.
|
|
||||||
|
Linux and Windows are the same code with the platform branches taken the other way: no traffic
|
||||||
|
lights, closing quits, and the menu bar built rather than adjusted, because neither is given the
|
||||||
|
File and View submenus macOS starts with. Linux ships as a deb, an AppImage, a flatpak and a Nix
|
||||||
|
package, Windows as an msi and an exe; which of the four a Linux reader should pick is in
|
||||||
|
[install.md](install.md).
|
||||||
|
|
||||||
## Order of work
|
## Order of work
|
||||||
|
|
||||||
The sync engine and the reading pane are the two hard things and neither proves the other, so the
|
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
|
first milestone after the scaffold is one account, authentication, the mirror, and a read-only
|
||||||
sanitised, tracker-stripped reading pane. Everything the app is for depends on those being
|
Inbox with a sanitised, tracker-stripped reading pane. Everything the app is for depends on those
|
||||||
right.
|
being right, and everything after them is additive. The milestones and their work packages are in
|
||||||
|
[plan.md](plan.md).
|
||||||
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,139 @@
|
|||||||
|
# Conventions
|
||||||
|
|
||||||
|
This project is a sibling to margin and Margin Calendar and follows their conventions deliberately
|
||||||
|
rather than inventing new ones. When something here is unclear, the answer is almost always "do
|
||||||
|
what the calendar does", and the file to look at is named below.
|
||||||
|
|
||||||
|
## Rust
|
||||||
|
|
||||||
|
`Result<T, String>` everywhere. No `anyhow`, no custom error enum except
|
||||||
|
`provider::ProviderError`, which exists only because the sync engine has to branch: a revoked
|
||||||
|
token, a missing scope, a 429 to back off from, and a 404 from `history.list` that means the
|
||||||
|
change log expired and a full list is the answer rather than a failure.
|
||||||
|
|
||||||
|
DTOs crossing the IPC boundary live in `src-tauri/src/dto.rs` and are marked
|
||||||
|
`#[serde(rename_all = "camelCase")]`. That file is the contract and is frozen: implementation
|
||||||
|
modules add bodies, not fields. Its mirror is `src/ipc.ts`.
|
||||||
|
|
||||||
|
Read Google's responses through the ported `read_json`, which takes the body to a `String` first
|
||||||
|
so the error payload survives into the message rather than becoming "expected value at line 1".
|
||||||
|
It is the calendar's function and it lives with the Gmail client.
|
||||||
|
|
||||||
|
Heavy synchronous work goes behind `#[tauri::command(async)]` on a synchronous fn, which is
|
||||||
|
margin's trick in `pdf.rs` for getting off the main thread without hand-writing `spawn_blocking`.
|
||||||
|
Every command that touches SQLite qualifies.
|
||||||
|
|
||||||
|
Provider-specific behaviour stays inside the provider's module. Nothing above
|
||||||
|
`src-tauri/src/provider/` may know what a Gmail label id looks like, and nothing outside the
|
||||||
|
mirror may know that a thread has a provider id at all. The sync engine talks to the trait, which
|
||||||
|
is what lets `provider::fake` drive the whole engine in `cargo test` without credentials.
|
||||||
|
|
||||||
|
Comments are rare and explain why, never what. Match the density in `lib.rs`.
|
||||||
|
|
||||||
|
## TypeScript
|
||||||
|
|
||||||
|
One zustand store per domain in `src/store/`. No middleware. One selector call per field
|
||||||
|
(`useThing((s) => s.field)`, never a destructured object), actions as inline arrow properties, and
|
||||||
|
`set((s) => ...)` returning `{}` to no-op.
|
||||||
|
|
||||||
|
Async actions use a string phase union (`"idle" | "syncing" | "error"`), never boolean loading
|
||||||
|
flags. Errors stringify with `String(e)` and surface as a toast.
|
||||||
|
|
||||||
|
Side effects that touch disk, the DOM or Tauri live in a sibling module, never inside the store.
|
||||||
|
|
||||||
|
The OAuth connect flow in `src/store/useAccounts.ts` reuses margin's
|
||||||
|
promise-holding-its-own-resolver pattern from `useBackup.ts`: `connect()` returns a promise whose
|
||||||
|
`resolve` is stashed in state for a later Tauri event to settle.
|
||||||
|
|
||||||
|
Typed IPC wrappers live in `src/api/`, one module per domain, one thin function per command. They
|
||||||
|
are written once against the frozen contract; add bodies to Rust, not new wrappers.
|
||||||
|
|
||||||
|
## The design system
|
||||||
|
|
||||||
|
Three layers, and the rule is that each may only reach down.
|
||||||
|
|
||||||
|
Tokens are `src/styles/tokens.css`, which is a seam rather than a list: it imports the set
|
||||||
|
margin-shared holds for all three apps, then `src/styles/mail.css` for what mail adds on top.
|
||||||
|
Every colour, radius, size, duration and font stack is in one of those two, and nothing else in
|
||||||
|
the app may declare a token.
|
||||||
|
|
||||||
|
Primitives are `src/ui/`: the button, the keycap, the row, the avatar, the panel, the popover, the
|
||||||
|
toast. They read tokens and nothing else, and every one of them appears in every state on the Kit
|
||||||
|
page at `#/kit`, which is how a restyle gets reviewed.
|
||||||
|
|
||||||
|
Screens are `src/screens/`. A screen composes primitives and may never write a colour, a radius or
|
||||||
|
a size. If a screen needs a value that is not available to it, the answer is a new token or a new
|
||||||
|
primitive, not a literal.
|
||||||
|
|
||||||
|
## Places and stages
|
||||||
|
|
||||||
|
A `Place` in the frozen contract is a query over threads, and three of the palette's entries are
|
||||||
|
not that: Contacts lists people, Clips lists passages, All files lists attachments. Focus & Reply is
|
||||||
|
a fourth, a page over the Reply later pile rather than somewhere you can be. None of them belongs in
|
||||||
|
`Place`, and none is an overlay either, because an overlay is something you dismiss to get back to
|
||||||
|
what you were doing and these are somewhere you go.
|
||||||
|
|
||||||
|
So they are a stage, in `src/store/useStage.ts`, and the rule is that a stage wins over a place:
|
||||||
|
opening Contacts leaves the Inbox where it was and Escape puts you back on it. The current place
|
||||||
|
and the current stage are both on the root element as `data-place` and `data-stage`, which is what
|
||||||
|
a test reads and what a stylesheet keys off, so neither has to ask the app what it thinks it is
|
||||||
|
showing.
|
||||||
|
|
||||||
|
## CSS
|
||||||
|
|
||||||
|
Flat kebab-case class names, not BEM. State is a `data-*` attribute, never an `is-` class.
|
||||||
|
|
||||||
|
Every colour, radius and size goes through a token. If a value is not in the token layer, add it
|
||||||
|
to `src/styles/mail.css` rather than writing a literal.
|
||||||
|
|
||||||
|
Dark mode is `data-theme` on `<html>`, with both palettes defining an identical variable set.
|
||||||
|
Never a media query for theme.
|
||||||
|
|
||||||
|
Transitions name explicit properties and use `var(--ease)`. Never `transition: all`.
|
||||||
|
|
||||||
|
Responsiveness is JS-driven. `usePhone()` and `useTouch()` in `src/useMedia.ts` write `data-phone`
|
||||||
|
and `data-touch` on the root, and styles read those attributes rather than adding media queries.
|
||||||
|
They answer different questions. `data-phone` is a window too narrow for the desktop chrome and it governs
|
||||||
|
layout; `data-touch` is a coarse pointer and it governs interaction. A tablet is touch and not a
|
||||||
|
phone, a narrow desktop window is a phone and not touch, and treating either as a proxy for the
|
||||||
|
other is how a hover-only control ends up unreachable. Both are also set by the boot script in
|
||||||
|
`index.html`, so the first paint is already the right shape.
|
||||||
|
|
||||||
|
A rule that reads "you cannot hover here" belongs on `data-touch`. A rule that reads "there is no
|
||||||
|
room for this" belongs on `data-phone`.
|
||||||
|
|
||||||
|
There is no container query in this repository. The calendar has exactly one, on the event block,
|
||||||
|
and it earned it: what decides how many lines of a title fit is the block's own width and not the
|
||||||
|
window's. Nothing here has met that bar yet, and nothing may reach for one without the same kind
|
||||||
|
of reason.
|
||||||
|
|
||||||
|
Overlays follow margin's `.overlay` and `.panel` idiom, which is in `src/styles/app.css`, and
|
||||||
|
every one of them registers with `useEscapeLayer` from `src/escape.ts` so Escape unwinds the
|
||||||
|
layers in order.
|
||||||
|
|
||||||
|
## Icons
|
||||||
|
|
||||||
|
Feather-style 24x24 stroke `d` strings, named in `src/ui/icons.ts` and passed to
|
||||||
|
`<Icon d={...} />`. The handful of glyphs the whole suite shares are re-exported from
|
||||||
|
`margin-shared/icons` so a search here and a search in the calendar are the same drawing; the
|
||||||
|
verbs and the piles are mail's own. There is no icon set and no registry, and there will not be
|
||||||
|
one. An icon-only button always carries a `title` with its shortcut written in real glyphs.
|
||||||
|
|
||||||
|
## Storage keys
|
||||||
|
|
||||||
|
Anything in `localStorage` is prefixed `marginmail-`, following margin's convention: the theme is
|
||||||
|
`marginmail-theme` in `src/theme.ts`, and the fonts, the text size and the reading pane follow the
|
||||||
|
same shape. Keys read before first paint are restored by the blocking IIFE in `index.html`, which
|
||||||
|
is why they are flat strings rather than one blob.
|
||||||
|
|
||||||
|
## Work packages
|
||||||
|
|
||||||
|
A work package owns a set of files and never edits another package's files. When a package needs
|
||||||
|
something that lives in another one, the answer is to agree the interface up front (which is what
|
||||||
|
`dto.rs` and `src/ipc.ts` are for) and stub behind it, not to reach across. A package that has to
|
||||||
|
edit somebody else's file was cut in the wrong place.
|
||||||
|
|
||||||
|
## Never
|
||||||
|
|
||||||
|
No CSS framework, no component library, no router, no zustand middleware, no directory trees in
|
||||||
|
any document, and no em dashes anywhere including code comments.
|
||||||
@@ -17,10 +17,10 @@ named. We refused HEY's layout (one column, a page per thread, a round trip for
|
|||||||
insistence that routing is per sender only, and its refusal to let you archive.
|
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
|
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
|
command palette that reaches every place, every command and every setting, so the app teaches
|
||||||
list beside a reading pane, so you triage without leaving the list. Remind me if no reply, undo
|
itself. A list beside a reading pane, so you triage without leaving the list. Remind me if no
|
||||||
send, the contact card. We refused the tracking pixels, the AI surface that ships your mail to a
|
reply, undo send, the contact card. We refused the tracking pixels, the AI surface that ships your
|
||||||
vendor, the inbox-zero streak, the tiny fixed type, and the price.
|
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,
|
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
|
screening decisions, notes, renames and clips are keyed on the mail itself (the RFC Message-ID
|
||||||
@@ -34,9 +34,9 @@ There are three boxes and one gate, and every sender has exactly one destination
|
|||||||
|
|
||||||
**Inbox** is for people and for the few services you want to hear from as they arrive. It is a
|
**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
|
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.
|
everything you have opened or sent sinks to Previously seen beneath it. Neither group carries a
|
||||||
A reply pulls a thread back up. There is an archive key, because some people need an empty list
|
count. A reply pulls a thread back up. There is an archive key, because some people need an empty
|
||||||
to feel finished, but nothing in the design pushes you towards it.
|
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,
|
**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
|
newest first, with a marker where you left off. There is no read state and no obligation. You
|
||||||
@@ -53,9 +53,10 @@ decision can be per address or per domain, which is the thing HEY's users ask fo
|
|||||||
will not give them. Replies to a thread you are already in bypass the whole system and land in
|
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.
|
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,
|
On first run there is no Screener avalanche. Everyone the account already knows is screened in,
|
||||||
routed by the same suggestion rules, and movable later from their contact card. Only genuinely
|
routed by the same suggestion rules, and movable later from their contact card: your Google
|
||||||
new senders from that point on are held.
|
contacts, everyone in the mail that came down with the first sync, and everyone you wrote to in
|
||||||
|
it. Only genuinely new senders from that point on are held.
|
||||||
|
|
||||||
## The two piles
|
## The two piles
|
||||||
|
|
||||||
@@ -76,16 +77,20 @@ Every verb is one unmodified key, the same key Gmail and Superhuman use where th
|
|||||||
(`j`, `k`, `e`, `r`, `a`, `f`, `c`, `/`, `x`, `u`, `z`) and HEY's letters for HEY's verbs (`l`
|
(`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
|
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
|
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
|
teaches the keyboard. Cmd-K opens the palette, which reaches every place, every command and
|
||||||
every setting. The full map and the reasoning for each conflict are in [keyboard.md](keyboard.md).
|
every setting, though settings itself is a screen you can sit in rather than a list you pass
|
||||||
|
through. The full map and the reasoning for each conflict are in [keyboard.md](keyboard.md).
|
||||||
|
|
||||||
## Quiet by design
|
## Quiet by design
|
||||||
|
|
||||||
No badge, no unread count, no streak, no photograph when the list is empty. No notification
|
No unread count, no streak, no photograph when the list is empty. No notification unless you
|
||||||
unless you turned it on for that thread or that person. Remote images do not load until you ask,
|
turned it on for that thread or that person. The one number anywhere is the dock badge, and it
|
||||||
tracking pixels are removed before the message renders, and the banner tells you whose pixel it
|
counts New for you rather than unread mail: what is waiting for a decision once the Screener, the
|
||||||
was. Nothing you send carries a tracker and nothing reports when it was opened. Links open with
|
Feed and the Paper Trail have taken everything that is not. That is a fact about your Inbox rather
|
||||||
their tracking parameters removed.
|
than a reason to open the app, and it turns off in Notifications. 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
|
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
|
does it, and it is explainable in a sentence on every Screener card. The design leaves a seat for
|
||||||
@@ -113,15 +118,17 @@ passage with a link back. All of it roams through the backup store, none of it t
|
|||||||
|
|
||||||
Not a team tool: no shared threads, comments, or read statuses. Not a calendar: invites hand off
|
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
|
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
|
summarised or drafted for you. Not an archive: the device keeps a window of recent mail, a month
|
||||||
the whole point is never opening it.
|
unless you ask for more, and the rest stays on Gmail where a search still reaches it. Not a Gmail
|
||||||
|
skin: the Gmail web UI is not a consideration, since the whole point is never opening it.
|
||||||
|
|
||||||
## Visual language
|
## Visual language
|
||||||
|
|
||||||
Lifted from margin and the calendar unchanged: warm paper, ink and two softer inks, hairline
|
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
|
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
|
`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:
|
bodies, because mail is reading and reading deserves a text face. Both are the default rather than
|
||||||
|
the law: Appearance offers the six faces the suite bundles and whatever else is on the machine. The additions are mail-specific:
|
||||||
a row hover and a row selection wash, a warmer band for Previously seen, a note surface, a dot
|
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
|
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.
|
account edges. No CSS framework, no component library, hand-written CSS on tokens.
|
||||||
|
|||||||
@@ -21,8 +21,9 @@ reaches all of them.
|
|||||||
| `5` | Set aside | The Set aside pile |
|
| `5` | Set aside | The Set aside pile |
|
||||||
| `6` | Screener | First messages from senders with no decision yet |
|
| `6` | Screener | First messages from senders with no decision yet |
|
||||||
| `7` | Snoozed | Threads waiting to return, with their return time |
|
| `7` | Snoozed | Threads waiting to return, with their return time |
|
||||||
| `0` | Everything | Every thread in the account, including archived, in date order |
|
| `0` | Everything | Every thread on the device, including archived, spam and screened out, in date order |
|
||||||
| palette | Sent, Drafts, Starred, Screened out, Spam, Trash | The usual folders |
|
| palette | Sent, Drafts, Starred | The usual folders |
|
||||||
|
| palette | Screened out, Spam, Trash | Under Other: the places you go looking in rather than read |
|
||||||
| palette | All files, Clips, Contacts | The libraries |
|
| palette | All files, Clips, Contacts | The libraries |
|
||||||
| palette | Labels | The provider's labels or folders, one place each |
|
| palette | Labels | The provider's labels or folders, one place each |
|
||||||
|
|
||||||
@@ -30,6 +31,13 @@ Every place except Feed, Screener and Focus & Reply is a list column beside the
|
|||||||
Feed and Screener take the whole stage because their content is inline. A place remembers its
|
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.
|
scroll position and selection while the app is open.
|
||||||
|
|
||||||
|
Every place is a view over what is on the device, and what is on the device is a window of the
|
||||||
|
mailbox: the last 30 days by default, or 90, 180, 365 days or everything, set per account. Threads
|
||||||
|
you have done something to are kept whatever their age. Only Everything says any of this out loud,
|
||||||
|
in one quiet line at the foot of the list, "Showing the last month. Older mail is on Gmail.", with
|
||||||
|
the setting one click away. The mechanism is in [architecture.md](architecture.md) and the setting
|
||||||
|
is in [settings.md](settings.md).
|
||||||
|
|
||||||
## 2. Routing and the Screener
|
## 2. Routing and the Screener
|
||||||
|
|
||||||
### Destinations
|
### Destinations
|
||||||
@@ -43,8 +51,8 @@ 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
|
- 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.
|
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
|
- A message from someone the account already knows is screened in on first run and routed by the
|
||||||
screened in on first run and routed by the suggestion rules, never held.
|
suggestion rules, never held. Who counts as known is settled under First run below.
|
||||||
|
|
||||||
### The Screener
|
### The Screener
|
||||||
|
|
||||||
@@ -60,9 +68,10 @@ subject, snippet, and a one-line reason with the suggested destination. Keys:
|
|||||||
sender into the Inbox and opens a reply.
|
sender into the Inbox and opens a reply.
|
||||||
- Clear all screens out every sender currently waiting, after a confirmation.
|
- 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
|
Screened out mail sits in the Screened out place for as long as the storage window keeps it and
|
||||||
decision is done from the sender's contact card, and re-screening someone in brings back whatever
|
falls off the device with everything else of that age. There is no second retention rule to
|
||||||
they sent in the last 90 days.
|
remember, and a wider window means a longer memory. Reversing a decision is done from the sender's
|
||||||
|
contact card, and re-screening someone in brings back whatever they sent that is still here.
|
||||||
|
|
||||||
### Suggestions
|
### Suggestions
|
||||||
|
|
||||||
@@ -85,10 +94,26 @@ wrong suggestion is a one-line fix.
|
|||||||
|
|
||||||
### First run
|
### First run
|
||||||
|
|
||||||
When an account is added, every sender in the mirror is screened in with a rule set by the same
|
When an account is added, everyone it already knows 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
|
suggestion function, silently. A month of mail is not by itself a good answer to who a person
|
||||||
applies to that sender's existing threads immediately. Only senders whose first message arrives
|
knows, so the seed is drawn from three cheap sources at once: every sender and every recipient
|
||||||
after the account was added are held in the Screener.
|
inside the storage window, the People API's `connections` and `otherContacts` lists, and the Sent
|
||||||
|
mail inside the window. Anyone in any of the three is screened in. All three are already fetched
|
||||||
|
or already on the way, so this costs a pair of extra calls and no waiting.
|
||||||
|
|
||||||
|
The pass runs at the end of the first sync rather than when the first-run panel is dismissed, so
|
||||||
|
the Inbox fills as the mail arrives instead of sitting empty for as long as somebody takes to read
|
||||||
|
a panel. It runs once per account and is guarded, which is the whole of what makes the Screener a
|
||||||
|
gate: if it ran again on a later sync it would screen in every new sender the moment they wrote.
|
||||||
|
The guard cuts the other way too: asked before the crawl has finished, as it is the moment an
|
||||||
|
account is added from Settings, the seed answers "not yet" rather than marking itself done over a
|
||||||
|
mirror with nothing in it, and the sync's own idle asks again. A seed on record as having
|
||||||
|
screened in nobody is run once more when the mirror is ready, which repairs an account that was
|
||||||
|
marked that way before the rule existed without anybody removing it and connecting it again.
|
||||||
|
|
||||||
|
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
|
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
|
(default one week) as seen, so New for you holds only what is recent. This is the only bulk
|
||||||
@@ -110,8 +135,12 @@ holds it until it is opened.
|
|||||||
- A new message in a Previously seen thread moves the thread to New for you.
|
- 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
|
- `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`).
|
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
|
- There are no counts on the groups or on the place. The one count anywhere is the dock badge,
|
||||||
you exists as a setting and is off.
|
which is the size of New for you across every account, and it is on by default and turns off in
|
||||||
|
Notifications. It is not the mailbox's unread count: a thread held in the Screener, routed to the
|
||||||
|
Feed or the Paper Trail, piled, snoozed or ignored is unread and is not waiting for you. Zero
|
||||||
|
takes the badge off rather than showing a nought. macOS and Linux carry it; Windows would need a
|
||||||
|
drawn overlay icon and does not have one yet.
|
||||||
- A note on a thread shows as a single line under its row.
|
- 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
|
Seen state is the provider's read state (Gmail: `UNREAD`), so it is not app state. Everything
|
||||||
@@ -200,12 +229,14 @@ 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.
|
- 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
|
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
|
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.
|
sender always. Attachments are chips; pressing one opens the file with whatever owns its type.
|
||||||
- Attachments are fetched when the thread is opened, not during sync, and cached.
|
- 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,
|
- 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
|
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
|
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.
|
there yet it is imported first. The Calendar permission is not asked for when the account is
|
||||||
|
added, so the first RSVP says it needs it and runs the consent page again; until then the card
|
||||||
|
renders read-only.
|
||||||
- Links show their real destination on hover and open with known tracking parameters removed.
|
- 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
|
- Read together: select several threads with `x` and press `Enter`; the pane shows them one
|
||||||
after another with a heading each.
|
after another with a heading each.
|
||||||
@@ -290,15 +321,26 @@ focused. Built from the local index; nothing is fetched until you open one.
|
|||||||
|
|
||||||
### Ignore
|
### Ignore
|
||||||
|
|
||||||
`m` on a thread. New messages still arrive and append, but the thread never returns to New for
|
`m` on a thread. New messages still arrive and append, and the thread rises with them because the
|
||||||
you and never notifies. A banner on the thread says "You are ignoring this thread" with Stop
|
Inbox is in time order, but it never reads as new, never counts on the badge and never notifies. A
|
||||||
ignoring. Local.
|
banner on the thread says "You are ignoring this thread" with Stop ignoring. Local.
|
||||||
|
|
||||||
### Notifications
|
### Notifications
|
||||||
|
|
||||||
Off by default everywhere. `Shift+N` on a thread turns them on for that thread; the contact card
|
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
|
turns them on for a person; Settings turns them on for a place, and has one switch over all of it
|
||||||
thread. There is no badge unless the setting is turned on. Local.
|
for the machine, which is what "nothing on this laptop" means without touching a single thread. The
|
||||||
|
sync pass that brings a message in is what posts the notification, and only for mail that arrived
|
||||||
|
after the app came up, so a first sync, a rebuild or a week away says nothing about the backlog; one
|
||||||
|
message is three lines, the app's name, the sender and the subject, and several in one pass are one
|
||||||
|
notification counting them and naming the senders. On macOS the system asks once whether the app
|
||||||
|
may notify at all, the first time anything here is turned on or the test button is pressed, and a
|
||||||
|
refusal is undone in System Settings rather than here. Clicking one brings the app to the front and
|
||||||
|
opens the thread in the list it shows in; a click on the grouped one opens the account's Inbox, and
|
||||||
|
a click on the sample from Settings only brings the app to the front. The dock badge is a setting
|
||||||
|
in the same section rather than a notification, it counts New for you, and it is the one thing here
|
||||||
|
that is on. Local.
|
||||||
|
|
||||||
|
|
||||||
## 11. Contacts and the contact card
|
## 11. Contacts and the contact card
|
||||||
|
|
||||||
@@ -319,8 +361,38 @@ State: notes, delivery, notify are local. Provider: nothing.
|
|||||||
the app POSTs and confirms; with a `mailto:` header it sends the message; otherwise it opens the
|
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".
|
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.
|
- 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
|
- `!` marks spam and `#` trashes, both provider changes, both with an undo toast.
|
||||||
on the provider's schedule; the Trash place has an Empty button.
|
|
||||||
|
### Screened out, Spam and Trash
|
||||||
|
|
||||||
|
Three places under Other in the palette, below the daily ones and below the labels, with no number
|
||||||
|
keys of their own. Where a place sits is the honest statement of how often you should be in it, and
|
||||||
|
these are the ones you go looking in rather than the ones you read.
|
||||||
|
|
||||||
|
None of them is a filter of ours. Gmail's spam filter runs on Gmail's side before the app sees a
|
||||||
|
message, and the mirror takes what the mailbox holds: every list call sets `includeSpamTrash`, so a
|
||||||
|
junked message is already on the device with its body indexed whether or not anything shows it.
|
||||||
|
Screened out is the one we own, and it is a routing destination rather than a folder, so its rules
|
||||||
|
are in section 2.
|
||||||
|
|
||||||
|
Getting mail back out is the point of all three, and the verb that puts it back is the verb that
|
||||||
|
put it there, the way the piles already work. `#` in Trash puts a thread back, `!` in Spam takes
|
||||||
|
the spam mark off. Gmail restores a message's labels when the `TRASH` label comes off, so a thread
|
||||||
|
put back lands where it was rather than in the Inbox. A thread taken out of Spam is routed like any
|
||||||
|
other: to its sender's box if that sender has a rule, and to the Screener if they do not, which is
|
||||||
|
the decision you still owe them.
|
||||||
|
|
||||||
|
There are three ways back, in the order you will want them. A wrong keystroke is the toast that is
|
||||||
|
already up, "Trashed · Undo" and `z`. A rescue a week later is the place and the verb. After thirty
|
||||||
|
days Gmail empties its own trash, the message leaves the mirror with it, and nothing local changes
|
||||||
|
that.
|
||||||
|
|
||||||
|
There is no Empty button. Permanently deleting through the Gmail API needs the
|
||||||
|
`https://mail.google.com/` scope, which is total access to the mailbox, and asking every account
|
||||||
|
for that so a button can destroy things thirty days earlier than Gmail will anyway is a bad trade.
|
||||||
|
Each place states the rule instead, in one line at the foot of the list: "Gmail empties this after
|
||||||
|
30 days." Screened out carries no such line, because it falls off with the storage window like
|
||||||
|
everything else of its age and there is no second retention rule to learn.
|
||||||
|
|
||||||
## 13. Selection and bulk actions
|
## 13. Selection and bulk actions
|
||||||
|
|
||||||
@@ -332,16 +404,28 @@ Enter for Read together. Every bulk action is one undo.
|
|||||||
## 14. Search
|
## 14. Search
|
||||||
|
|
||||||
`/` focuses search. Results replace the list column and the reading pane works as usual. 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
|
is local over what is on the device: subject, participants, snippet and body text, with operators
|
||||||
`from:`, `to:`, `subject:`, `has:attachment`, `filename:`, `in:` (any place), `before:` and
|
`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
|
`after:`, `label:`.
|
||||||
runs as a second pass and its results append with a note. Results open in place and `Esc`
|
|
||||||
returns to the previous place.
|
Because the device holds a window, a local result set is a partial answer and says so. Every list
|
||||||
|
of results ends with "Search older mail on Gmail", which runs the provider's search, appends the
|
||||||
|
hits and hydrates them as they arrive. Those rows behave like any other row, and the next eviction
|
||||||
|
pass takes them away again unless they picked up a pile, a note or some other decision in the
|
||||||
|
meantime. A query whose `before:` or `after:` falls outside the window skips the local index
|
||||||
|
entirely and goes to the provider, because a local answer to that question would be wrong rather
|
||||||
|
than merely short. Results open in place and `Esc` returns to the previous place.
|
||||||
|
|
||||||
|
Search reaches Spam, Trash and Screened out, and names the place on the row when it does. The
|
||||||
|
message you most need to find is the one something else decided you should not see, and a search
|
||||||
|
that skipped those three would be one you had to already know the answer to use. `in:` narrows to
|
||||||
|
a single place when that is what you meant.
|
||||||
|
|
||||||
## 15. Accounts
|
## 15. Accounts
|
||||||
|
|
||||||
Add as many Gmail accounts as you like. Each has its own places, sender rules, piles and
|
Add as many Gmail accounts as you like. Each has its own places, sender rules, piles, Screener,
|
||||||
Screener. The account chip in the title bar switches (`Ctrl+1` to `Ctrl+9`) and offers All
|
storage window and granted permissions, so a work account can keep a year while a personal one
|
||||||
|
keeps a month. 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
|
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
|
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
|
the account you are looking at, and the From field switches it. Sent mail goes through the
|
||||||
@@ -355,13 +439,38 @@ are the provider's, they roam with the mailbox, and they are not how Margin orga
|
|||||||
|
|
||||||
## 17. Settings and export
|
## 17. Settings and export
|
||||||
|
|
||||||
`Cmd+,` opens settings as a panel: Accounts (add, remove, signature, aliases), Backup (Google
|
Settings is a place, not a panel. `Cmd+,`, the palette, the account chip and the app menu all lead
|
||||||
Drive or Cloudflare R2, and the recovery phrase), Appearance (theme, reading pane, row density
|
to the same full-stage screen, with a rail of sections down the left and one section at a time on
|
||||||
for the phone), Sending (undo delay, reply-all default, instant intro text), Privacy (remote
|
the right. Twelve sections cover the accounts and their permissions, appearance, the storage
|
||||||
images, link cleaning, per-sender allowances), Notifications (badge, sound), Keyboard (the keymap
|
window, privacy, the Screener, the piles and snooze, writing, notifications, the keymap, backup and
|
||||||
file), Data (export mail as mbox per account, export app state as JSON, import app state).
|
the recovery phrase, every export the app offers, and the version. Each is specified in
|
||||||
|
[settings.md](settings.md), including which of them live on the device and which roam.
|
||||||
|
|
||||||
## 18. Not in version one
|
## 18. Help
|
||||||
|
|
||||||
|
The app is unlike the mail clients people arrive from, and none of the differences are
|
||||||
|
discoverable by poking at the interface, so there is a tour, a question mark in the corner, and a
|
||||||
|
guide behind it. Each is specified in [help.md](help.md).
|
||||||
|
|
||||||
|
The tour is nine slides in a sheet, over the Inbox, run once for every account that is added and
|
||||||
|
skipped with one key. It follows the first-run panel above and names the Screener, the three
|
||||||
|
boxes, the two piles, snooze, the keyboard, the palette, the things kept beside the mail, what is
|
||||||
|
off by default, and undo.
|
||||||
|
|
||||||
|
The corner button opens the tour again, the guide, and the keyboard shortcuts. It is not drawn
|
||||||
|
while the compose card is open, while an overlay is up, in the guide itself, or on a phone.
|
||||||
|
|
||||||
|
The guide is a panel over the whole window: a search field, a rail of sections, and one article at
|
||||||
|
a time. How to do each thing the app does, and last, the questions people actually ask. Every
|
||||||
|
article leads with its answer and shows it in a drawn figure, a screenshot or a table of keys, and
|
||||||
|
a search nothing answers offers to file the question against the repository. Closing it puts back
|
||||||
|
the place, the open thread and the scroll. Its pictures come from the dev fixture through
|
||||||
|
`just guide-shots` and are committed, because they ship in the bundle.
|
||||||
|
|
||||||
|
State: which accounts have had their first run, and therefore their tour, is a device fact in
|
||||||
|
localStorage beside the first-run panel's own flag. Provider: nothing.
|
||||||
|
|
||||||
|
## 19. Not in version one
|
||||||
|
|
||||||
Send later. Snippets. Any AI. Shared threads, team comments, read statuses. Workflows, collections,
|
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
|
cover art. A calendar sidebar. Unified search across accounts (search is per account until the
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
# Help
|
||||||
|
|
||||||
|
Three things, and they are separate on purpose: a tour that runs once when an account is added, a
|
||||||
|
question mark in the corner that is there for good, and a guide behind it that answers the
|
||||||
|
questions this app raises by not working like the last one. Behaviour that is being explained is
|
||||||
|
specified in [features.md](features.md); the keys are in [keyboard.md](keyboard.md) and every
|
||||||
|
keycap printed anywhere here is generated from the same binding table, so a remapped key is what
|
||||||
|
the help says.
|
||||||
|
|
||||||
|
The premise is that this app is unusual and that pretending otherwise is what makes people leave.
|
||||||
|
Mail from somebody new does not arrive. There are three boxes rather than one. Flags are two piles
|
||||||
|
with keys on them. None of that is discoverable by poking at it, and none of it is a reason to
|
||||||
|
turn the first hour into a wizard either.
|
||||||
|
|
||||||
|
## The tour
|
||||||
|
|
||||||
|
Nine slides in a sheet, over the Inbox it is talking about. It follows the first-run panel, which
|
||||||
|
is the moment the account is set up and the mail is in, and it ends where the app expects you to
|
||||||
|
carry on: the Screener, the three boxes, the two piles, snooze, the keyboard, the palette, the
|
||||||
|
things kept beside the mail, what is quiet by default, and undo.
|
||||||
|
|
||||||
|
Skip is the first control on it and Escape is the same answer, so it costs one keystroke to
|
||||||
|
refuse. The arrows and the return key move through it, the dots at the foot say how far in you are
|
||||||
|
and can be pressed to jump, and the last slide says where to find all of this again.
|
||||||
|
|
||||||
|
It runs for every account that is added rather than only the first. The panel it follows is per
|
||||||
|
account too: a second mailbox on a shared machine is somebody else's first look at the app, and
|
||||||
|
the flag that remembers is on the device rather than in the state that roams. Somebody adding
|
||||||
|
their own second account has seen it before and skips it, which is one key.
|
||||||
|
|
||||||
|
The slides state facts and nothing else. No animation beyond a fade, nothing bouncing, no
|
||||||
|
illustration that is not a diagram of the real thing, and no screenshot: the app is behind the
|
||||||
|
sheet, so a picture of it would be a picture of what is already there.
|
||||||
|
|
||||||
|
## The corner
|
||||||
|
|
||||||
|
A small round question mark fixed in the bottom right, and the only permanent chrome the app has
|
||||||
|
outside the header. It opens three rows: the tour again, the guide, and the keyboard shortcuts
|
||||||
|
with its key printed.
|
||||||
|
|
||||||
|
It gets out of the way rather than floating over everything. It is not drawn while the compose
|
||||||
|
card is open, because they share that corner and compose is the thing you are doing; not while any
|
||||||
|
overlay is up, because it would be under the scrim; not in the guide, which is where it goes; and
|
||||||
|
not on a phone, where the tab bar owns that corner and the palette behind the places button is
|
||||||
|
what reaches all three.
|
||||||
|
|
||||||
|
The same three are in the native Help menu, above Report an Issue, because that is the first place
|
||||||
|
a Mac user looks and a menu bar item costs nothing.
|
||||||
|
|
||||||
|
## The guide
|
||||||
|
|
||||||
|
A panel over the whole window, with the app dimmed behind it. It is read about the app rather than
|
||||||
|
instead of it, so nothing behind it can be pressed while it is up and the close control puts back
|
||||||
|
exactly what was there: the place, the open thread, the scroll position. Escape is the same answer.
|
||||||
|
Inside, a search field across the whole width, then a rail of sections down the left and one
|
||||||
|
article at a time on the right at a reading measure.
|
||||||
|
|
||||||
|
Search is the first thing in the panel and it takes the keyboard the moment the guide opens,
|
||||||
|
because somebody who came here has a question rather than an appetite for a table of contents. It
|
||||||
|
reads what the articles say and not only what they are called. A query nothing answers is the one
|
||||||
|
moment the guide has failed, so that is where it offers to open a question against the repository,
|
||||||
|
labelled `question` and titled with what was typed, and it says out loud that the page it opens is
|
||||||
|
public.
|
||||||
|
|
||||||
|
Articles are written to be looked at before they are read. The answer is the first paragraph, a
|
||||||
|
drawn figure or a screenshot carries the idea under it, and a verb that would otherwise be a
|
||||||
|
sentence in a list of six is a table of the key and what it does, generated from the binding table.
|
||||||
|
The rule is enforced rather than hoped for: `guide.test.ts` fails on a paragraph over seventy five
|
||||||
|
words, and on an article over a hundred and twenty words with nothing in it to look at.
|
||||||
|
|
||||||
|
The figures are drawn from the app's own parts rather than exported from a drawing program, in
|
||||||
|
`src/screens/guide/Figures.tsx`. A picture is the app photographed and a figure is an idea drawn,
|
||||||
|
and the ideas are the things no camera can be pointed at: where mail goes, what a pile does, how
|
||||||
|
long a send is held.
|
||||||
|
|
||||||
|
It was a stage first, and that was wrong twice over. A stage wins over a place, so the header sat
|
||||||
|
above it with three box buttons that changed a place nobody could see; and a library about the app
|
||||||
|
is not somewhere you go instead of your mail, it is something you hold up in front of it. Making it
|
||||||
|
a panel found the second half of the same bug: the title bar carries a stacking order of its own so
|
||||||
|
that a popover can hang off the account chip, and it was above every scrim in the app, which left
|
||||||
|
the palette, the shortcut sheet and the tour all reachable past their own scrim at the top of the
|
||||||
|
window. Overlays now sit above it, which is the rule the phone stylesheet had already worked out
|
||||||
|
for its tab bar.
|
||||||
|
|
||||||
|
The sections run in the order somebody meets the problem: getting started, the Screener, reading,
|
||||||
|
triage, writing, organising, accounts, and then the questions. An article is prose with steps
|
||||||
|
where there are steps and the key printed where there is a key. The questions are the last section
|
||||||
|
and they are the ones people actually ask: where a newsletter went, whether a screened-out sender
|
||||||
|
is told (they are not), why mail from last year is not here, why there is no Empty Trash button,
|
||||||
|
whether anything reads the mail with AI (nothing does), and what happens to the mailbox if the app
|
||||||
|
is deleted.
|
||||||
|
|
||||||
|
The pictures come from the browser suite rather than from anybody's mailbox. `just guide-shots`
|
||||||
|
drives the dev fixture and writes ten PNGs into `public/guide/`, at twice the display size for a
|
||||||
|
retina screen, and they are committed because they ship inside the bundle. The recipe is not part
|
||||||
|
of `just test-ui`: an ordinary run of the suite must not rewrite files that are in the tree.
|
||||||
|
|
||||||
|
## Not here
|
||||||
|
|
||||||
|
No coach marks over the interface, no tooltip that follows you around, no "did you know" after the
|
||||||
|
third launch, no checklist of things to finish, no progress bar over your own mailbox. The tour is
|
||||||
|
one sheet you can refuse, and after that help is somewhere you go and find rather than something
|
||||||
|
that arrives while you are reading your mail.
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
# Installing Margin Mail
|
||||||
|
|
||||||
|
Every build comes from the same release. Pick the file for your machine from
|
||||||
|
[the latest release](https://github.com/priyanshujain/margin-mail/releases/latest) and follow the
|
||||||
|
section below for it. The version number in the file names changes with every release; `0.1.0`
|
||||||
|
stands in for it throughout.
|
||||||
|
|
||||||
|
Whatever you install, the app keeps its mirror of your mail, your accounts and every decision
|
||||||
|
you have ever made about a sender in one directory, and uninstalling never touches it. Where that
|
||||||
|
directory is, and how to move it, is at the end.
|
||||||
|
|
||||||
|
## macOS
|
||||||
|
|
||||||
|
Apple Silicon and Intel are the same file: the dmg carries a universal binary.
|
||||||
|
|
||||||
|
1. Download `Margin.Mail_0.1.0_universal.dmg`.
|
||||||
|
2. Open it and drag **Margin Mail** onto the Applications folder in the same window.
|
||||||
|
3. Eject the disk image and open the app from Applications or Spotlight.
|
||||||
|
|
||||||
|
The bundle is signed with a Developer ID and notarized, so it opens on the first double click with
|
||||||
|
no right-click-and-Open dance and no trip to System Settings.
|
||||||
|
|
||||||
|
macOS asks about notifications the first time the app has something to tell you rather than at
|
||||||
|
launch. If you say no and change your mind, it is under Notifications in System Settings, and the
|
||||||
|
app's Settings has a button that takes you straight there.
|
||||||
|
|
||||||
|
The minimum is macOS 10.15.
|
||||||
|
|
||||||
|
## Linux
|
||||||
|
|
||||||
|
Four packages, and they are not equal. If you are on Wayland, or on NixOS, use Nix. Otherwise the
|
||||||
|
deb on Debian and Ubuntu, the flatpak on anything else, and the AppImage when you want no install
|
||||||
|
at all.
|
||||||
|
|
||||||
|
### deb, on Debian and Ubuntu
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo apt install ./Margin.Mail_0.1.0_amd64.deb
|
||||||
|
```
|
||||||
|
|
||||||
|
`apt` rather than `dpkg -i`, because it pulls `libwebkit2gtk-4.1-0` and `libgtk-3-0` in for you.
|
||||||
|
The app then appears in your launcher. Upgrading is the same command with the newer file, and
|
||||||
|
`sudo apt remove margin-mail` reverses it.
|
||||||
|
|
||||||
|
Built on Ubuntu 22.04, so 22.04 is the oldest release it runs on. Anything older has a glibc the
|
||||||
|
binary was not linked against and will refuse to start.
|
||||||
|
|
||||||
|
### flatpak, on everything else
|
||||||
|
|
||||||
|
```
|
||||||
|
flatpak install ./Margin.Mail_0.1.0_amd64.flatpak
|
||||||
|
flatpak run studio.margin.mail
|
||||||
|
```
|
||||||
|
|
||||||
|
A single self-contained file: it brings its own GTK and WebKit, so it does not care what your
|
||||||
|
distribution ships. It is not on Flathub and will not be, because a Flathub build has to come from
|
||||||
|
source and this app's Google client is embedded at compile time from a file that is deliberately
|
||||||
|
not in the repository.
|
||||||
|
|
||||||
|
The sandbox is narrow on purpose. The app gets the network, the notification service and your
|
||||||
|
downloads directory, and nothing else. That means saved attachments and exports land in
|
||||||
|
`~/Downloads` and can go nowhere else, and the app's data lives under
|
||||||
|
`~/.var/app/studio.margin.mail/` rather than in the usual place, so a flatpak install and a deb
|
||||||
|
install do not see each other's mail.
|
||||||
|
|
||||||
|
`flatpak uninstall studio.margin.mail` removes it, and add `--delete-data` to take the mail with
|
||||||
|
it.
|
||||||
|
|
||||||
|
### Nix
|
||||||
|
|
||||||
|
The package relinks the published deb against nixpkgs' own GTK and WebKit, which is the only build
|
||||||
|
here that runs as a native Wayland client rather than falling back to Xwayland.
|
||||||
|
|
||||||
|
```
|
||||||
|
NIXPKGS_ALLOW_UNFREE=1 nix run --impure github:priyanshujain/margin-mail#margin-mail
|
||||||
|
```
|
||||||
|
|
||||||
|
`NIXPKGS_ALLOW_UNFREE` because the licence is FSL rather than MIT, so nix asks first. To keep it,
|
||||||
|
add the flake as an input and the overlay to your configuration:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{
|
||||||
|
inputs.margin-mail.url = "github:priyanshujain/margin-mail";
|
||||||
|
|
||||||
|
# In your nixpkgs configuration:
|
||||||
|
nixpkgs.overlays = [ inputs.margin-mail.overlays.default ];
|
||||||
|
nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [ "margin-mail" ];
|
||||||
|
environment.systemPackages = [ pkgs.margin-mail ];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
A Nix install does not update itself. The store is read only, so the app reports the new version
|
||||||
|
and tells you to update the flake instead of trying to replace its own binary.
|
||||||
|
|
||||||
|
### AppImage, when you want no install
|
||||||
|
|
||||||
|
```
|
||||||
|
chmod +x Margin.Mail_0.1.0_amd64.AppImage
|
||||||
|
./Margin.Mail_0.1.0_amd64.AppImage
|
||||||
|
```
|
||||||
|
|
||||||
|
Nothing is written outside your home directory and there is nothing to uninstall. It carries
|
||||||
|
Ubuntu's GTK stack, which cannot talk to a modern Wayland compositor, so on Wayland it runs through
|
||||||
|
Xwayland and looks slightly soft on a HiDPI screen. That is the trade for a single portable file.
|
||||||
|
|
||||||
|
To get it into your launcher rather than running it from a terminal, `just install` from a clone
|
||||||
|
does the whole thing, or by hand:
|
||||||
|
|
||||||
|
```
|
||||||
|
install -Dm755 Margin.Mail_0.1.0_amd64.AppImage ~/.local/bin/margin-mail
|
||||||
|
```
|
||||||
|
|
||||||
|
and write a `.desktop` file pointing `Exec` at it.
|
||||||
|
|
||||||
|
## Windows
|
||||||
|
|
||||||
|
Two installers, and either is fine. The `.exe` is the friendlier one; the `.msi` is what you want
|
||||||
|
if you are deploying by policy.
|
||||||
|
|
||||||
|
1. Download `Margin.Mail_0.1.0_x64-setup.exe`.
|
||||||
|
2. Run it. Windows SmartScreen will say it does not recognise the publisher, because the installer
|
||||||
|
is not code-signed. Click **More info**, then **Run anyway**.
|
||||||
|
3. The app appears in the Start menu.
|
||||||
|
|
||||||
|
Uninstalling is through Apps in Settings, the same as anything else.
|
||||||
|
|
||||||
|
Windows 10 1803 or newer, and WebView2, which every supported Windows already has.
|
||||||
|
|
||||||
|
## Updating
|
||||||
|
|
||||||
|
Every install except Nix and the flatpak updates itself. The app checks the release feed on launch,
|
||||||
|
tells you when something newer is out, and installs it when you say so. **Check for Updates** in
|
||||||
|
the File menu, or the application menu on macOS, asks immediately.
|
||||||
|
|
||||||
|
Nix updates with the flake. The flatpak updates with `flatpak update studio.margin.mail` once you
|
||||||
|
have installed the newer bundle file, since a single-file bundle carries no remote to check.
|
||||||
|
|
||||||
|
## Where your mail lives
|
||||||
|
|
||||||
|
One directory per platform, and it survives uninstalling:
|
||||||
|
|
||||||
|
- macOS: `~/Library/Application Support/studio.margin.mail`
|
||||||
|
- Linux: `~/.local/share/studio.margin.mail`, or `~/.var/app/studio.margin.mail/data/studio.margin.mail` under flatpak
|
||||||
|
- Windows: `%APPDATA%\studio.margin.mail`
|
||||||
|
|
||||||
|
It holds the mirror of your mail, the state database of every decision you have made, and your
|
||||||
|
sealed refresh tokens. Copying it to another machine moves everything except the tokens, which are
|
||||||
|
sealed against the machine that stored them, so the accounts ask to be connected again and nothing
|
||||||
|
else changes.
|
||||||
|
|
||||||
|
To remove the app and its mail together, uninstall and then delete that directory.
|
||||||
|
|
||||||
|
## Building it yourself
|
||||||
|
|
||||||
|
`just install` from a clone builds for the machine you are sitting at and puts it where that
|
||||||
|
machine expects to find applications. [release.md](release.md) has what a build needs and how a
|
||||||
|
release is cut.
|
||||||
@@ -34,8 +34,8 @@ app. Bindings live in a keymap file the user can edit; the defaults are what fol
|
|||||||
| `e` | Archive | yes |
|
| `e` | Archive | yes |
|
||||||
| `u` | Toggle seen | yes |
|
| `u` | Toggle seen | yes |
|
||||||
| `Shift+S` | Toggle star | yes |
|
| `Shift+S` | Toggle star | yes |
|
||||||
| `#` | Trash | yes |
|
| `#` | Trash, and put back in Trash (toggle) | yes |
|
||||||
| `!` | Spam | yes |
|
| `!` | Spam, and not spam in Spam (toggle) | yes |
|
||||||
| `l` | Reply later (toggle) | no |
|
| `l` | Reply later (toggle) | no |
|
||||||
| `s` | Set aside (toggle) | no |
|
| `s` | Set aside (toggle) | no |
|
||||||
| `b` | Snooze… | no |
|
| `b` | Snooze… | no |
|
||||||
|
|||||||
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 603 KiB |
|
Before Width: | Height: | Size: 223 KiB After Width: | Height: | Size: 226 KiB |
|
After Width: | Height: | Size: 266 KiB |
@@ -0,0 +1,128 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" data-theme="light">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Margin Mail: Connect</title>
|
||||||
|
<link rel="stylesheet" href="mail.css">
|
||||||
|
<style>
|
||||||
|
/* The welcome screen is the only place in the app that is not a list, a pane or an overlay, so
|
||||||
|
these are its own pieces. Every value is a token. */
|
||||||
|
.welcome {
|
||||||
|
margin: auto;
|
||||||
|
padding: 0 24px 56px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-mark {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-title {
|
||||||
|
margin: 0;
|
||||||
|
font-family: var(--font-heading);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 32px;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-line {
|
||||||
|
margin: 9px 0 28px;
|
||||||
|
max-width: 30em;
|
||||||
|
color: var(--ink-soft);
|
||||||
|
font-size: var(--t-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* One field and one button, which is the whole of the first step. The button is larger than a
|
||||||
|
button anywhere else in the app for the same reason it is the only one here. */
|
||||||
|
.imap-address {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 24em;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.imap-address .field {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-label {
|
||||||
|
color: var(--ink-faint);
|
||||||
|
font-size: var(--t-1);
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 9px 12px;
|
||||||
|
border: 1px solid var(--line-strong);
|
||||||
|
border-radius: var(--r-sm);
|
||||||
|
background: var(--raised);
|
||||||
|
font: inherit;
|
||||||
|
font-size: var(--t-4);
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.imap-address .button {
|
||||||
|
min-height: 38px;
|
||||||
|
padding: 0 20px;
|
||||||
|
border-radius: var(--r-md);
|
||||||
|
font-size: var(--t-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-privacy {
|
||||||
|
margin: 22px 0 0;
|
||||||
|
max-width: 42em;
|
||||||
|
color: var(--ink-faint);
|
||||||
|
font-size: var(--t-2);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="app">
|
||||||
|
<header class="titlebar">
|
||||||
|
<div class="traffic"><i></i><i></i><i></i></div>
|
||||||
|
<div class="lead"></div>
|
||||||
|
<div></div>
|
||||||
|
<div class="trail"></div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="stage">
|
||||||
|
<div class="welcome">
|
||||||
|
<div class="welcome-mark">
|
||||||
|
<svg width="54" height="54" viewBox="0 0 54 54" fill="none">
|
||||||
|
<rect width="54" height="54" rx="13" style="fill: var(--accent)"/>
|
||||||
|
<rect x="14" y="18.5" width="26" height="17" rx="3.2" style="stroke: var(--accent-contrast)" stroke-width="2"/>
|
||||||
|
<path d="m14.8 20.5 10.5 7.6a3 3 0 0 0 3.4 0l10.5-7.6" style="stroke: var(--accent-contrast)" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 class="welcome-title">Margin Mail</h1>
|
||||||
|
<p class="welcome-line">A quiet, keyboard-first client for your mail, where every decision you make stays on your own machine.</p>
|
||||||
|
|
||||||
|
<form class="imap-address">
|
||||||
|
<div class="field">
|
||||||
|
<label class="field-label" for="address">Email address</label>
|
||||||
|
<input class="field-input" id="address" type="email" value="[email protected]">
|
||||||
|
</div>
|
||||||
|
<button class="button" data-variant="primary" type="button">Continue</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p class="welcome-privacy">Any mailbox works here: Google, Fastmail, iCloud, Yahoo, Proton through Bridge, a mailbox where you work, or anything else that speaks IMAP. Margin works out the servers from the address, and nothing is stored until the account is added.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,294 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" data-theme="light">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Margin Mail: First run</title>
|
||||||
|
<link rel="stylesheet" href="mail.css">
|
||||||
|
<style>
|
||||||
|
/* The first-run panel. Everything else on this page is the Inbox as it already is. */
|
||||||
|
.onboard-lead {
|
||||||
|
margin: 0;
|
||||||
|
font-size: var(--t-4);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboard-sub {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--ink-soft);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field .onboard-sub {
|
||||||
|
margin: -1px 0 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.choice {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.choice-label {
|
||||||
|
margin-right: 2px;
|
||||||
|
color: var(--ink-faint);
|
||||||
|
font-size: var(--t-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.choice-option {
|
||||||
|
padding: 4px 11px;
|
||||||
|
border: 1px solid var(--line-strong);
|
||||||
|
border-radius: var(--r-pill);
|
||||||
|
background: var(--raised);
|
||||||
|
color: var(--ink-soft);
|
||||||
|
font-size: var(--t-2);
|
||||||
|
transition: background 120ms var(--ease);
|
||||||
|
}
|
||||||
|
|
||||||
|
.choice-option[data-on] {
|
||||||
|
border-color: var(--accent);
|
||||||
|
background: var(--accent);
|
||||||
|
color: var(--accent-contrast);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</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>
|
||||||
|
</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">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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" data-empty>
|
||||||
|
<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">Nothing yet</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button class="pile" data-empty>
|
||||||
|
<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">Nothing yet</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>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="overlay">
|
||||||
|
<div class="panel">
|
||||||
|
<div class="panel-head">
|
||||||
|
<h2>You are set up</h2>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<p class="onboard-lead"><b>1,284 senders</b> were screened in already, because you have written to them or they are in your contacts: 96 to the Inbox, 71 to the Feed and 1,117 to the Paper Trail.</p>
|
||||||
|
<p class="onboard-sub">From here on, anyone new waits in the Screener until you say where their mail goes. Nobody is told either way.</p>
|
||||||
|
<div class="field">
|
||||||
|
<span class="field-label">Start fresh</span>
|
||||||
|
<p class="onboard-sub">Optional. Mark older mail as seen, so New for you holds only what is recent. Reversible for seven days.</p>
|
||||||
|
<div class="choice">
|
||||||
|
<span class="choice-label">Older than</span>
|
||||||
|
<button class="choice-option">a day</button>
|
||||||
|
<button class="choice-option" data-on>a week</button>
|
||||||
|
<button class="choice-option">a month</button>
|
||||||
|
<button class="choice-option">three months</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel-foot">
|
||||||
|
<button class="button">Start fresh</button>
|
||||||
|
<button class="button" data-variant="primary">Done</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -105,7 +105,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
<p class="screen-note">Screened-out mail sits under Screened out for as long as this account keeps mail on the device. Change your mind from a sender's contact card at any time.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -0,0 +1,371 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" data-theme="light">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Margin Mail: Settings</title>
|
||||||
|
<link rel="stylesheet" href="mail.css">
|
||||||
|
<style>
|
||||||
|
/* Settings is a place, so it takes the stage: a rail of sections on the left, one section on the
|
||||||
|
right. These are its own pieces; every value is a token. */
|
||||||
|
.settings {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-rail {
|
||||||
|
flex: none;
|
||||||
|
width: 210px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1px;
|
||||||
|
padding: 12px 10px;
|
||||||
|
border-right: 1px solid var(--line);
|
||||||
|
background: var(--sidebar);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-rail h1 {
|
||||||
|
margin: 4px 8px 10px;
|
||||||
|
font-family: var(--font-heading);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 17px;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-tab {
|
||||||
|
padding: 7px 10px;
|
||||||
|
border-radius: var(--r-sm);
|
||||||
|
color: var(--ink-soft);
|
||||||
|
font-size: var(--t-3);
|
||||||
|
text-align: left;
|
||||||
|
transition: background 120ms var(--ease), color 120ms var(--ease);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-tab:hover {
|
||||||
|
background: var(--accent-wash);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-tab[data-active] {
|
||||||
|
background: var(--accent-wash);
|
||||||
|
color: var(--ink);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-panel {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 24px 36px 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-inner {
|
||||||
|
max-width: 640px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-title {
|
||||||
|
margin: 0;
|
||||||
|
font-family: var(--font-heading);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 22px;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-note {
|
||||||
|
margin: 6px 0 20px;
|
||||||
|
color: var(--ink-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.acct {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--r-lg);
|
||||||
|
background: var(--raised);
|
||||||
|
}
|
||||||
|
|
||||||
|
.acct-head {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 36px 1fr auto;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.acct-head .avatar {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.acct-name {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: var(--t-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.acct-addr {
|
||||||
|
color: var(--ink-faint);
|
||||||
|
font-size: var(--t-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.swatches {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swatch {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
border-radius: var(--r-pill);
|
||||||
|
}
|
||||||
|
|
||||||
|
.swatch[data-hue="1"] { background: var(--hue-1); }
|
||||||
|
.swatch[data-hue="2"] { background: var(--hue-2); }
|
||||||
|
.swatch[data-hue="3"] { background: var(--hue-3); }
|
||||||
|
.swatch[data-hue="4"] { background: var(--hue-4); }
|
||||||
|
.swatch[data-hue="5"] { background: var(--hue-5); }
|
||||||
|
.swatch[data-hue="6"] { background: var(--hue-6); }
|
||||||
|
.swatch[data-hue="7"] { background: var(--hue-7); }
|
||||||
|
.swatch[data-hue="8"] { background: var(--hue-8); }
|
||||||
|
|
||||||
|
.swatch[data-on] {
|
||||||
|
box-shadow: 0 0 0 2px var(--raised), 0 0 0 3px var(--ink-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.set-rows {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 7px;
|
||||||
|
margin-top: 12px;
|
||||||
|
padding-top: 11px;
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.set-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: var(--t-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.set-row .lab {
|
||||||
|
flex: none;
|
||||||
|
width: 84px;
|
||||||
|
color: var(--ink-faint);
|
||||||
|
}
|
||||||
|
|
||||||
|
.set-row .val {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scopes {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding-top: 6px;
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scopes .section {
|
||||||
|
padding: 4px 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scope {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 9px;
|
||||||
|
min-height: 26px;
|
||||||
|
font-size: var(--t-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scope .tick {
|
||||||
|
flex: none;
|
||||||
|
color: var(--ink-faint);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scope .what {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scope .state {
|
||||||
|
color: var(--ink-faint);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scope[data-missing] .what {
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scope-why {
|
||||||
|
margin: 1px 0 5px 25px;
|
||||||
|
color: var(--ink-faint);
|
||||||
|
font-size: var(--t-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.acct-foot {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-quiet {
|
||||||
|
margin: 18px 0 0;
|
||||||
|
color: var(--ink-faint);
|
||||||
|
font-size: var(--t-2);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</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-check" viewBox="0 0 24 24"><path d="m5 12 5 5L19 7"/></symbol>
|
||||||
|
<symbol id="i-minus" viewBox="0 0 24 24"><path d="M6 12h12"/></symbol>
|
||||||
|
<symbol id="i-plus" viewBox="0 0 24 24"><path d="M12 6v12M6 12h12"/></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="settings">
|
||||||
|
<nav class="settings-rail">
|
||||||
|
<h1>Settings</h1>
|
||||||
|
<button class="settings-tab" data-active>Accounts</button>
|
||||||
|
<button class="settings-tab">Appearance</button>
|
||||||
|
<button class="settings-tab">Mail</button>
|
||||||
|
<button class="settings-tab">Privacy</button>
|
||||||
|
<button class="settings-tab">Screener</button>
|
||||||
|
<button class="settings-tab">Piles and snooze</button>
|
||||||
|
<button class="settings-tab">Writing</button>
|
||||||
|
<button class="settings-tab">Notifications</button>
|
||||||
|
<button class="settings-tab">Keyboard</button>
|
||||||
|
<button class="settings-tab">Backup</button>
|
||||||
|
<button class="settings-tab">Data</button>
|
||||||
|
<button class="settings-tab">About</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="settings-panel">
|
||||||
|
<div class="settings-inner">
|
||||||
|
<h2 class="settings-title">Accounts</h2>
|
||||||
|
<p class="settings-note">Two accounts, each with its own places, rules and piles. The colour is the edge on a row in All accounts.</p>
|
||||||
|
|
||||||
|
<div class="acct">
|
||||||
|
<div class="acct-head">
|
||||||
|
<span class="avatar" data-hue="4">PJ</span>
|
||||||
|
<div>
|
||||||
|
<div class="acct-name">Priyanshu Jain</div>
|
||||||
|
<div class="acct-addr">[email protected]</div>
|
||||||
|
</div>
|
||||||
|
<div class="swatches">
|
||||||
|
<span class="swatch" data-hue="1"></span>
|
||||||
|
<span class="swatch" data-hue="2"></span>
|
||||||
|
<span class="swatch" data-hue="3"></span>
|
||||||
|
<span class="swatch" data-hue="4" data-on></span>
|
||||||
|
<span class="swatch" data-hue="5"></span>
|
||||||
|
<span class="swatch" data-hue="6"></span>
|
||||||
|
<span class="swatch" data-hue="7"></span>
|
||||||
|
<span class="swatch" data-hue="8"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="set-rows">
|
||||||
|
<div class="set-row"><span class="lab">Signature</span><span class="val">Priyanshu Jain · 73ai</span></div>
|
||||||
|
<div class="set-row"><span class="lab">Aliases</span><span class="val">[email protected], [email protected]</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="scopes">
|
||||||
|
<div class="section">Permissions</div>
|
||||||
|
|
||||||
|
<div class="scope">
|
||||||
|
<svg class="tick" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><use href="#i-check"/></svg>
|
||||||
|
<span class="what">Read and change your mail</span><span class="state">Granted</span>
|
||||||
|
</div>
|
||||||
|
<div class="scope">
|
||||||
|
<svg class="tick" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><use href="#i-check"/></svg>
|
||||||
|
<span class="what">Read your signature and aliases</span><span class="state">Granted</span>
|
||||||
|
</div>
|
||||||
|
<div class="scope">
|
||||||
|
<svg class="tick" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><use href="#i-check"/></svg>
|
||||||
|
<span class="what">Read your contacts</span><span class="state">Granted</span>
|
||||||
|
</div>
|
||||||
|
<div class="scope">
|
||||||
|
<svg class="tick" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><use href="#i-check"/></svg>
|
||||||
|
<span class="what">Read the people you have written to</span><span class="state">Granted</span>
|
||||||
|
</div>
|
||||||
|
<div class="scope">
|
||||||
|
<svg class="tick" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><use href="#i-check"/></svg>
|
||||||
|
<span class="what">Keep a backup in your Drive</span><span class="state">Granted</span>
|
||||||
|
</div>
|
||||||
|
<div class="scope" data-missing>
|
||||||
|
<svg class="tick" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><use href="#i-minus"/></svg>
|
||||||
|
<span class="what">Answer calendar invitations</span>
|
||||||
|
<button class="button">Grant</button>
|
||||||
|
</div>
|
||||||
|
<p class="scope-why">Calendar is not connected, so an invitation renders but Accept, Maybe and Decline do nothing. Granting reopens the Google consent page.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="acct">
|
||||||
|
<div class="acct-head">
|
||||||
|
<span class="avatar" data-hue="2">PS</span>
|
||||||
|
<div>
|
||||||
|
<div class="acct-name">Personal</div>
|
||||||
|
<div class="acct-addr">[email protected]</div>
|
||||||
|
</div>
|
||||||
|
<div class="swatches">
|
||||||
|
<span class="swatch" data-hue="1"></span>
|
||||||
|
<span class="swatch" data-hue="2" data-on></span>
|
||||||
|
<span class="swatch" data-hue="3"></span>
|
||||||
|
<span class="swatch" data-hue="4"></span>
|
||||||
|
<span class="swatch" data-hue="5"></span>
|
||||||
|
<span class="swatch" data-hue="6"></span>
|
||||||
|
<span class="swatch" data-hue="7"></span>
|
||||||
|
<span class="swatch" data-hue="8"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="set-rows">
|
||||||
|
<div class="set-row"><span class="lab">Signature</span><span class="val">Not set</span></div>
|
||||||
|
<div class="set-row"><span class="lab">Aliases</span><span class="val">None on the provider</span></div>
|
||||||
|
<div class="set-row"><span class="lab">Permissions</span><span class="val">All granted</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="acct-foot">
|
||||||
|
<button class="button"><svg width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#i-plus"/></svg>Add account</button>
|
||||||
|
<button class="button" data-variant="ghost">Remove an account</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="settings-quiet">Margin Mail, Margin and Margin Calendar share one Google client, so your Google account lists them once, as Margin, and revoking it revokes all three.</p>
|
||||||
|
<div class="acct-foot"><button class="button">Bring your own OAuth client</button></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
# Plan
|
||||||
|
|
||||||
|
How the app gets built, in the order it gets built, and what it is built out of. The product is
|
||||||
|
specified in [design.md](design.md) and [features.md](features.md); this document is about the
|
||||||
|
work rather than the result, and it exists so that somebody picking the repository up in six
|
||||||
|
months can see why the pieces landed in the order they did.
|
||||||
|
|
||||||
|
Work is cut into milestones, and a milestone into work packages. A package is a unit somebody can
|
||||||
|
finish: it owns a set of files, it ends with something that runs, and it never edits another
|
||||||
|
package's files. The package names below are the ones the source comments already use, so a
|
||||||
|
placeholder that says "the contract lands in F3" means the work package named here.
|
||||||
|
|
||||||
|
## The milestones
|
||||||
|
|
||||||
|
**M0, foundations.** F1 is the scaffold: the Tauri crate, the Vite front end, the icons, the
|
||||||
|
justfile and CI. F2 is the design system, which is `src/styles/tokens.css` and the primitives that
|
||||||
|
sit on it, reviewed through the Kit page. F3 is the contracts: `src-tauri/src/dto.rs` and its
|
||||||
|
mirror `src/ipc.ts`, frozen before anything implements them. F4 is the documentation, this file
|
||||||
|
among it. None of M0 sends a byte to Google, and that is the point: the shape is settled while it
|
||||||
|
is still cheap to change.
|
||||||
|
|
||||||
|
**M1, read.** The milestone that proves the two hard things. R1 is authentication and accounts:
|
||||||
|
the loopback flow, the sealed token, the granted scopes. R2 is the Gmail client behind the
|
||||||
|
`Provider` trait, with the quota arithmetic and the backoff in it. R3 is the mirror and the sync
|
||||||
|
loop, including the storage window and eviction. R4 is the message pipeline: the MIME parse, the
|
||||||
|
sanitiser, the tracker stripper. R5 is the shell, which is the header, the list column, the
|
||||||
|
reading pane and the keyboard. R6 is connect and onboarding, the first thing a new user meets and
|
||||||
|
the last thing built in this milestone, because it cannot be designed honestly until the sync it
|
||||||
|
narrates exists. At the end of M1 the app reads mail and does nothing else.
|
||||||
|
|
||||||
|
**M2, triage.** T1 is flags and undo: seen, star, archive, trash, spam, each optimistic and each
|
||||||
|
reversible. T2 is selection and bulk actions. T3 is search, local over the window with the
|
||||||
|
provider as the second pass. T4 is labels. At the end of M2 it is a fast Gmail client and nothing
|
||||||
|
more, which is worth having in the hands of one user for a week before the next milestone changes
|
||||||
|
what it is.
|
||||||
|
|
||||||
|
**M3, places.** P1 is the state database and its journal, the schema that everything after this
|
||||||
|
depends on. P2 is routing: sender rules, the suggestion function, the overrides. P3 is the
|
||||||
|
Screener and the first-run pass. P4 is the Feed and the Paper Trail. P5 is contacts, the contact
|
||||||
|
card and autocomplete. This is the milestone where it stops being a Gmail client.
|
||||||
|
|
||||||
|
**M4, piles.** L1 is Reply later, Set aside and Focus & Reply. L2 is snooze with lazy evaluation.
|
||||||
|
L3 is notes, rename and merge. L4 is the rest of what the state database makes possible: clips,
|
||||||
|
All files, ignore, per-thread notifications and unsubscribe.
|
||||||
|
|
||||||
|
**M5, writing.** W1 is the editor and drafts. W2 is the send pipeline: the outbox, the undo delay,
|
||||||
|
attachments, threading headers. W3 is instant intro, remind me if no reply, and calendar invites
|
||||||
|
including the re-authorization that RSVP needs.
|
||||||
|
|
||||||
|
**M6, accounts, settings and backup.** A1 is more than one account and the unified view. A2 is the
|
||||||
|
settings screen, specified in [settings.md](settings.md). A3 is the backup store, the encryption
|
||||||
|
and the recovery phrase, with the journal merge behind it.
|
||||||
|
|
||||||
|
**M7, ship.** S1 is the release pipeline: signing, notarisation, the updater. S2 is the
|
||||||
|
verification materials Google's restricted scope review wants, which is a privacy policy that is
|
||||||
|
true, a demo video, and a written justification for each scope. Then the platforms in order: S3
|
||||||
|
the iPhone, S4 Linux, S5 the IMAP provider. They are last because each one is a second copy of a
|
||||||
|
problem already solved once, and solving it twice before it is solved once is how a project stalls.
|
||||||
|
|
||||||
|
## What comes from the siblings
|
||||||
|
|
||||||
|
Very little here is new, and that is deliberate. Margin Mail sits beside margin and Margin
|
||||||
|
Calendar on disk and takes from both.
|
||||||
|
|
||||||
|
From the calendar: the OAuth loopback flow with PKCE and its Google-specific handling, the sealed
|
||||||
|
token store, the deep-link path that mobile needs instead of a loopback listener, the overlay
|
||||||
|
title bar and the macOS window behaviour, the `data-phone` and `data-touch` scheme with the boot
|
||||||
|
script that sets them before first paint, the escape-layer stack, the palette, the zustand store
|
||||||
|
idiom, the release workflow's shape, and the `build.rs` trick that embeds
|
||||||
|
`google-credentials.json` with the example file as a fallback.
|
||||||
|
|
||||||
|
From margin: the HTTP half of `gdrive.rs`, which is folder lookup, upload and download against
|
||||||
|
Drive's v3 API; the trick of putting heavy synchronous work behind `#[tauri::command(async)]`; and
|
||||||
|
`margin-shared`, which is a real dependency rather than a copy. The tokens, the icon strings and
|
||||||
|
the font catalogue come from that package through a relative path in `package.json`, which is why
|
||||||
|
CI checks out both repositories side by side.
|
||||||
|
|
||||||
|
From neither: the mirror, the state database, the journal, the sanitiser and everything to do with
|
||||||
|
mail. Those are this repository's own work.
|
||||||
|
|
||||||
|
## The libraries
|
||||||
|
|
||||||
|
Every version below was checked against crates.io and npm on 3 September 2026.
|
||||||
|
|
||||||
|
On the Rust side, `mail-parser` 0.11 with `full_encoding` parses bodies from the raw RFC 2822
|
||||||
|
bytes; the feature is not optional, because the charsets it adds are the ones that still turn up
|
||||||
|
in real mail every day. `mail-builder` 0.5 builds outgoing MIME. `css-inline` 0.21 folds the
|
||||||
|
editor's stylesheet into the markup before the message is built, so a client that drops `<style>`
|
||||||
|
still renders what was written. `ammonia` 4.1 is the sanitiser, and two of its hooks do the work
|
||||||
|
that matters: `attribute_filter` rewrites `img src`, which is where tracker stripping and `cid:`
|
||||||
|
substitution happen, and `filter_style_properties` narrows inline CSS to an allowlist of
|
||||||
|
properties that can never take a `url()`, which closes the last route a message has to fetch
|
||||||
|
something.
|
||||||
|
|
||||||
|
`rusqlite` 0.40 with `bundled`, so FTS5 is compiled in rather than depending on what the
|
||||||
|
platform's libsqlite3 happened to be built with, and so several accounts sharing one process share
|
||||||
|
one predictable SQLite. `reqwest` 0.13 with `gzip`, `json` and `http2`: Gmail's JSON compresses by
|
||||||
|
an order of magnitude and hydration is thousands of responses, and a batch of fifty shares a
|
||||||
|
connection with the poll loop. Note that its TLS feature is now called `rustls` rather than
|
||||||
|
`rustls-tls`; the old name is a build error, not a warning. `chacha20poly1305` 0.11 seals tokens
|
||||||
|
and backup segments, `argon2` 0.6 derives the backup key from the recovery phrase slowly enough to
|
||||||
|
matter, `bip39` 2.2 produces the phrase, and `chrono` and `fontdb` do the obvious.
|
||||||
|
|
||||||
|
Two deliberate omissions. There is no `oauth2` crate: the calendar's hand-rolled flow is already
|
||||||
|
tested against Google's actual behaviour, including the parts that do not match the spec, and
|
||||||
|
replacing working code with a dependency that has to be taught the same lessons is not a trade.
|
||||||
|
There is no `tokio-rusqlite`: it pins an older rusqlite than the one above, and the synchronous
|
||||||
|
command trick makes it unnecessary anyway.
|
||||||
|
|
||||||
|
On the front end, `@tiptap/react` 3.31 with StarterKit is the editor, as in margin. `react-virtuoso`
|
||||||
|
4.18 renders the grouped list, because a mailbox list is long, its rows are two heights, and it has
|
||||||
|
group headers, which is exactly the case hand-rolled virtualisation gets wrong. Beyond those,
|
||||||
|
React, zustand and the Tauri API, and nothing else.
|
||||||
|
|
||||||
|
One consequence worth writing down. Message bodies render in an iframe with `srcdoc`, which
|
||||||
|
inherits the app's content security policy rather than escaping it, so the frame cannot fetch
|
||||||
|
anything: inline `cid:` images are rewritten to `data:` URIs by the sanitiser, and a remote image
|
||||||
|
the user has chosen to allow is fetched by Rust, without cookies or referrer, and handed to the
|
||||||
|
frame the same way. Every byte a message displays has been through Rust first.
|
||||||
|
|
||||||
|
The sandbox attribute is `allow-same-origin` rather than empty, and the reason is worth keeping.
|
||||||
|
An empty sandbox gives the frame an opaque origin, and a document the parent cannot reach is a
|
||||||
|
document the parent cannot measure: there is then no way to size the frame to its content, so every
|
||||||
|
message carries its own scrollbar, and no way to catch a click on a link, so nothing opens. Keeping
|
||||||
|
`allow-same-origin` leaves every other restriction in place, forms and top navigation included, and
|
||||||
|
scripts are still blocked three times over: the sandbox disables them without `allow-scripts`, the
|
||||||
|
content security policy is `script-src 'self'` which stops inline scripts and inline event handlers
|
||||||
|
whatever the origin, and the sanitiser removed them before either got a say.
|
||||||
|
|
||||||
|
## The shape of the repository
|
||||||
|
|
||||||
|
The front end is `src`. Screens live in `src/screens`, the primitives they are built from in
|
||||||
|
`src/ui`, one zustand store per domain in `src/store`, the typed IPC wrappers in `src/api`, and
|
||||||
|
the stylesheets in `src/styles`, where `tokens.css` is the only file allowed to hold a raw colour.
|
||||||
|
`src/ipc.ts` is the frontend half of the contract and `src/screens/Kit.tsx` is the page that
|
||||||
|
renders every primitive in every state, which is how a restyle gets reviewed.
|
||||||
|
|
||||||
|
The backend is `src-tauri/src`. `dto.rs` is the other half of the contract and is frozen once M0
|
||||||
|
ends. `lib.rs` holds the app setup, the menu and `emit_store_changed`. The Google client, the
|
||||||
|
mirror, the state database and the sync loop each get a module, and the Gmail specifics stay
|
||||||
|
inside the Gmail one so that the second provider has somewhere to be.
|
||||||
|
|
||||||
|
Documentation is `docs`, with the mockups under `docs/mockups`: HTML sources in `src` beside a
|
||||||
|
shared `mail.css`, rendered to PNGs by `docs/mockups/render.sh`, which pulls the fonts from the
|
||||||
|
margin repository and Chromium from the calendar's `node_modules` so nothing binary is vendored
|
||||||
|
here. The prose gate is `scripts/docs-check.mjs`. Local builds and installs are the `justfile`;
|
||||||
|
CI and releases are the two workflows in `.github/workflows`.
|
||||||
|
|
||||||
|
## How the work is verified
|
||||||
|
|
||||||
|
Four gates, all of them runnable on a laptop before anything is handed over.
|
||||||
|
|
||||||
|
`just test` runs the Vitest suites and `cargo test`. Both must be green; there are no retries
|
||||||
|
anywhere in this repository, because a test that passes on the second attempt is lying about
|
||||||
|
something.
|
||||||
|
|
||||||
|
`just test-ui` runs Playwright against the real UI, with `src/ipc.ts` routed to the dev fixture so
|
||||||
|
no browser ever talks to Google. The viewport is pinned to 1440 by 900, the same size the mockups
|
||||||
|
were rendered at, because half of what the suite asserts is geometry: the 420 px list column, the
|
||||||
|
row height, whether the piles are on screen. It also writes screenshots as it goes, starting with
|
||||||
|
the Kit page in both palettes under `screenshots/`, so a visual regression shows up as a diff
|
||||||
|
rather than as a complaint two weeks later, and it greps the stylesheets for a hex literal outside
|
||||||
|
the token layer, which is the one rule a reviewer will not reliably catch by eye.
|
||||||
|
|
||||||
|
`just docs` is the prose gate: no em dashes, no directory trees, no relative link that goes
|
||||||
|
nowhere.
|
||||||
|
|
||||||
|
`pnpm build` is `tsc` then Vite, so the typecheck and the bundle are one step, and
|
||||||
|
`pnpm fonts:check` catches the vendored font copy under `public/fonts` drifting from the
|
||||||
|
`margin-shared` package. CI runs all of it, checking out this repository and margin side by side
|
||||||
|
because the shared package is reached by a relative path.
|
||||||
|
|
||||||
|
Beyond the gates, the mockups in [ui.md](ui.md) are the target rather than an impression of it.
|
||||||
|
A screen is finished when it looks like its render, not when it looks reasonable.
|
||||||
|
|
||||||
|
## What was learned from reading Mailspring
|
||||||
|
|
||||||
|
Mailspring is GPL-3.0 and was read, not copied. Five things in it are worth having and are in this
|
||||||
|
design because of it. Bodies live in their own table with a `fetched_at` column, prefetched inside
|
||||||
|
a window and evicted outside it, which is where the storage window in
|
||||||
|
[architecture.md](architecture.md) comes from. The engine hands the UI a typed delta stream rather
|
||||||
|
than telling it to refetch, which here is the `store-changed` event with a scope in it, so a note
|
||||||
|
landing does not make the list reload its bodies. The provider's thread id and the `References`
|
||||||
|
threading are kept as separate columns rather than collapsed into a hash of the headers, because
|
||||||
|
they disagree often enough to matter. An account that fails to sync repeatedly is paused by a
|
||||||
|
circuit breaker instead of being retried into a rate limit. And SQLite gets a busy timeout,
|
||||||
|
because several accounts share one process and one connection.
|
||||||
|
|
||||||
|
The list of what to avoid is just as useful. Nothing may depend on the app running at a particular
|
||||||
|
wall-clock time, which is why snooze is evaluated lazily. No metadata is held in a cloud service.
|
||||||
|
No tracking pixels, no rewritten links, and no contact lookup that sends a correspondent's address
|
||||||
|
to a server to find out who they are. And no header that never expires: everything cached has a
|
||||||
|
rule for when it goes.
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
# Releasing
|
||||||
|
|
||||||
|
## Installing locally
|
||||||
|
|
||||||
|
`just install` builds the app for whatever machine you are sitting at and puts it where that
|
||||||
|
machine expects to find applications: `/Applications` on macOS, or the package manager on Linux.
|
||||||
|
It is the same command whether or not the app is already installed, so it doubles as the update.
|
||||||
|
On macOS it asks a running copy to quit first, because replacing a bundle under a live process
|
||||||
|
leaves it half old and half new, and starts the new one once it is in place, so the copy on screen
|
||||||
|
is never older than the copy installed. `just uninstall` reverses it and leaves the data directory
|
||||||
|
alone, which matters more here than it does in the siblings: that directory holds the state
|
||||||
|
database, and the state database is every decision you have ever made about a sender.
|
||||||
|
|
||||||
|
The local build skips the dmg and builds only the `.app`, since nothing about copying a bundle into
|
||||||
|
place needs a disk image and building one is the slowest part of a mac bundle. A locally installed
|
||||||
|
app has no updater artifacts, so it will not update itself. Rerun `just install`.
|
||||||
|
|
||||||
|
## Signing
|
||||||
|
|
||||||
|
macOS shows no notifications from an app whose bundle is not signed, and `tauri build` on its own
|
||||||
|
leaves only the linker's signature on the binary, which does not count: the app never appears under
|
||||||
|
Notifications in System Settings and is never asked. So `tauri.conf.json` names `-` as the signing
|
||||||
|
identity and every macOS build is at least ad-hoc signed as a bundle, which is enough for
|
||||||
|
notifications. A real identity replaces that wherever one is available. `just build` sources
|
||||||
|
`~/.margin-signing/studio.margin.app.env` when it exists (another directory with
|
||||||
|
`MARGIN_SIGNING_DIR`), which exports `APPLE_SIGNING_IDENTITY`, and Tauri takes that over the config.
|
||||||
|
The release workflow does the same from repository secrets and notarizes when the App Store Connect
|
||||||
|
key is there too:
|
||||||
|
|
||||||
|
- `APPLE_CERTIFICATE` and `APPLE_CERTIFICATE_PASSWORD`, the Developer ID Application certificate as
|
||||||
|
a base64 `.p12` and its password. Tauri imports it into a temporary keychain for the build.
|
||||||
|
- `APPLE_SIGNING_IDENTITY` and `APPLE_TEAM_ID`, the identity's name and the team behind it.
|
||||||
|
- `APPLE_API_KEY`, `APPLE_API_ISSUER` and `APPLE_API_KEY_P8`, the App Store Connect API key id, its
|
||||||
|
issuer and the contents of the `.p8`. Without these three the bundle is signed and not notarized,
|
||||||
|
which Gatekeeper minds on a download and notifications do not.
|
||||||
|
|
||||||
|
From the signing directory that is:
|
||||||
|
|
||||||
|
```
|
||||||
|
cd ~/.margin-signing
|
||||||
|
gh secret set APPLE_CERTIFICATE < <(base64 -i developer-id.p12)
|
||||||
|
gh secret set APPLE_CERTIFICATE_PASSWORD < developer-id.p12.pass
|
||||||
|
gh secret set APPLE_SIGNING_IDENTITY --body "Developer ID Application: <name> (<team>)"
|
||||||
|
gh secret set APPLE_TEAM_ID --body "<team>"
|
||||||
|
gh secret set APPLE_API_KEY --body "<key id>"
|
||||||
|
gh secret set APPLE_API_ISSUER --body "<issuer>"
|
||||||
|
gh secret set APPLE_API_KEY_P8 < AuthKey.p8
|
||||||
|
```
|
||||||
|
|
||||||
|
## Before the first release
|
||||||
|
|
||||||
|
Two things have to exist that do not yet.
|
||||||
|
|
||||||
|
**The updater key.** Done. The pair was generated with
|
||||||
|
`pnpm tauri signer generate -w ~/.tauri/margin_mail_updater.key`, the public half is in
|
||||||
|
`src-tauri/tauri.release.conf.json` and the private half and its password are the repository's
|
||||||
|
`TAURI_SIGNING_PRIVATE_KEY` and `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` secrets. The private half and
|
||||||
|
its password sit beside the siblings' in `~/.tauri`, and only there. The public half is baked into
|
||||||
|
every build, so the private half can never be rotated without stranding everyone who has not
|
||||||
|
updated yet: back it up somewhere that is not the machine that made it.
|
||||||
|
|
||||||
|
**The verification.** `gmail.modify` and `gmail.settings.basic` are restricted scopes, so the shared
|
||||||
|
Cloud project has to pass restricted scope verification before this app is anything other than a
|
||||||
|
hundred lifetime users behind an unverified consent screen. That is a form, a privacy policy that is
|
||||||
|
true, a demo video and a written justification per scope, and it is per project rather than per
|
||||||
|
client, so it covers margin and Margin Calendar too and a lapse blocks all three. The hedge is in
|
||||||
|
[architecture.md](architecture.md): Gmail over IMAP and SMTP with an app password needs no project,
|
||||||
|
no verification and has no cap.
|
||||||
|
|
||||||
|
## Cutting a release
|
||||||
|
|
||||||
|
Releases are manual: run the **Release** workflow from the Actions tab. Leave the version empty to
|
||||||
|
bump the patch number, or give one to set it. The workflow bumps `tauri.conf.json`, `package.json`
|
||||||
|
and `Cargo.toml` together, commits that to main, tags it, and builds the tag rather than whatever
|
||||||
|
main happens to be by then.
|
||||||
|
|
||||||
|
Three runners build in parallel: a universal macOS bundle, an x86_64 Linux one and an x86_64
|
||||||
|
Windows one. Nothing is published until all three have landed. The publish job downloads
|
||||||
|
`latest.json` and refuses to take the release out of draft unless `darwin-aarch64`,
|
||||||
|
`darwin-x86_64`, `linux-x86_64` and `windows-x86_64` are all in it. A half-populated manifest is
|
||||||
|
worse than no release: the updater would offer an update to the platforms that made it and error on
|
||||||
|
the ones that did not.
|
||||||
|
|
||||||
|
Linux builds on Ubuntu 22.04 on purpose. The bundle will not run on anything older than the glibc it
|
||||||
|
was linked against, so it is built on the oldest release that is supported.
|
||||||
|
|
||||||
|
The Windows installers are not code-signed, so SmartScreen warns on the first download until the
|
||||||
|
app has built up reputation. A certificate would go in as `WINDOWS_CERTIFICATE` and
|
||||||
|
`WINDOWS_CERTIFICATE_PASSWORD` and needs nothing else changed.
|
||||||
|
|
||||||
|
Phones do not come from this pipeline at all. The store is their update channel.
|
||||||
|
|
||||||
|
## The two Linux packages Tauri does not build
|
||||||
|
|
||||||
|
Tauri produces the deb and the AppImage. The flatpak and the Nix package are built from the deb
|
||||||
|
afterwards, by two more jobs, and [install.md](install.md) says which of the four a reader should
|
||||||
|
actually pick.
|
||||||
|
|
||||||
|
The **flatpak** job runs between the build and the publish, so a release never goes out with the
|
||||||
|
Linux artifacts half there. It downloads the deb from the draft release, runs
|
||||||
|
`flatpak/build.sh` over the manifest in the same directory, and uploads a single-file bundle
|
||||||
|
beside it. The manifest is hand-written because Tauri has no flatpak bundler, and it pins
|
||||||
|
`org.gnome.Platform` 48, which is the newest runtime that still carries the GTK3 WebKit wry links
|
||||||
|
against. The sandbox gets the network, the notification service and the downloads directory, and
|
||||||
|
nothing else. CI builds the same manifest on every push to main, against a deb built there, which
|
||||||
|
is the only way a break in it gets found before a release.
|
||||||
|
|
||||||
|
The **nix** job runs after the publish, so the flake can only ever point at a release that survived
|
||||||
|
the manifest check. It hashes the published deb into `nix/release.json`, builds the package to
|
||||||
|
prove the pin works, and commits the pin to main. `NIXPKGS_ALLOW_UNFREE` and `--impure` are in that
|
||||||
|
command because FSL is not a free licence and `nix/package.nix` says so honestly rather than
|
||||||
|
claiming MIT to dodge the prompt.
|
||||||
|
|
||||||
|
Adding a Homebrew cask is the one distribution route the siblings have and this does not. It is a
|
||||||
|
job at the end of the release workflow, a `Casks/margin-mail.rb` in `priyanshujain/homebrew-margin`
|
||||||
|
and a `HOMEBREW_TAP_DEPLOY_KEY` secret; margin's `release.yml` has the job to copy.
|
||||||
|
|
||||||
|
## What the build needs
|
||||||
|
|
||||||
|
Both jobs check out this repository and the public margin repository side by side, because
|
||||||
|
`package.json` depends on `margin-shared` through a relative path. A token edited there is meant to
|
||||||
|
show up in every Margin app at once, and a copy vendored here would defeat that.
|
||||||
|
|
||||||
|
Ten repository secrets, all of them set. The three below, and the seven Apple ones under Signing
|
||||||
|
above:
|
||||||
|
|
||||||
|
- `GOOGLE_CREDENTIALS`, the contents of the real `google-credentials.json`. The build writes it to
|
||||||
|
the repository root and `build.rs` embeds it. Without it the build falls back to the example file
|
||||||
|
and warns, which produces an app that runs and then says it is not set up yet. It is the same
|
||||||
|
value in all three Margin repositories, because it is the same OAuth client.
|
||||||
|
- `TAURI_SIGNING_PRIVATE_KEY` and `TAURI_SIGNING_PRIVATE_KEY_PASSWORD`, which sign the updater
|
||||||
|
artifacts.
|
||||||
|
|
||||||
|
The OAuth client secret ends up inside the shipped binary. That is how installed apps work and
|
||||||
|
Google does not treat it as confidential: an installed client cannot keep a secret, which is why the
|
||||||
|
flow uses PKCE and why the token exchange is safe without one.
|
||||||
|
|
||||||
|
## Updates
|
||||||
|
|
||||||
|
Installed copies check
|
||||||
|
`https://github.com/priyanshujain/margin-mail/releases/latest/download/latest.json` and update
|
||||||
|
themselves from it. `--latest` on the publish step is what moves that pointer, so a release that
|
||||||
|
fails the manifest check stays a draft and nobody is offered a broken update.
|
||||||
|
|
||||||
|
A packaged install does not update itself. The Nix wrapper in the siblings sets a
|
||||||
|
`PACKAGED_BY` variable and the app checks for it before it would touch its own binary, which in a
|
||||||
|
read-only store it could not replace anyway; this app reads `MARGIN_MAIL_PACKAGED_BY` for the same
|
||||||
|
reason, and reports the newer version and the upgrade command instead.
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
# Settings
|
||||||
|
|
||||||
|
Settings is a place, not a panel. `Cmd+,` opens it, so does the palette, so does the account chip,
|
||||||
|
so does the app menu, and all four land on the same screen: the whole stage, a rail of section
|
||||||
|
names down the left, one section on the right. No tabs, no modal, no nested pages. `Esc` goes back
|
||||||
|
to the place you came from, `j` and `k` walk the rail, and the screen is in
|
||||||
|
[mockups/settings.png](mockups/settings.png).
|
||||||
|
|
||||||
|
The reason it is a place is that a mail client accumulates decisions. Twelve sections of them is
|
||||||
|
too much for an overlay that steals the window and too much for a palette that shows one row at a
|
||||||
|
time, and both shapes make a person who is looking for the storage window read the whole list
|
||||||
|
twice. A rail is boring and it works.
|
||||||
|
|
||||||
|
What follows is the section list, in rail order, with what each one holds. Behaviour that is not
|
||||||
|
about the control itself is specified in [features.md](features.md).
|
||||||
|
|
||||||
|
## Accounts
|
||||||
|
|
||||||
|
The first section and the one people arrive at. Each linked account is a card: its colour (one of
|
||||||
|
the eight muted hues, and the same colour the account's rows carry in All accounts), the display
|
||||||
|
name, the signature for that address, and its aliases. The colour is pickable, the name and
|
||||||
|
signature are editable, and the aliases are read rather than edited here. An IMAP account says
|
||||||
|
plainly that it has none: neither IMAP nor SMTP has a way to publish them, so such an account only
|
||||||
|
ever sends as its own address.
|
||||||
|
|
||||||
|
What sits under the card depends on what kind of account it is, because the two have nothing in
|
||||||
|
common below the name.
|
||||||
|
|
||||||
|
A Google account shows the permissions it granted, one line each: mail, mail settings, contacts,
|
||||||
|
other contacts, calendar, backup. Granular consent means any of them can be missing, so each line
|
||||||
|
either reads as granted or carries a Grant button that runs the consent page again for the full
|
||||||
|
list and replaces the stored token. The wording of a missing line says what it costs in plain
|
||||||
|
terms, not what the scope is called: "Calendar is not connected, so invites are read-only."
|
||||||
|
|
||||||
|
An IMAP account has no permissions to show, because nothing was granted: it shows the two servers
|
||||||
|
it is actually using, incoming and outgoing, with the port and the security for each, the username
|
||||||
|
it logs in with, and where the password is kept. A permission list on such an account would be six
|
||||||
|
lines of Google vocabulary and a Grant button that opened a consent page for an account that has no
|
||||||
|
Google behind it. The footnote about the three Margin apps sharing one Google client is likewise
|
||||||
|
only shown when one of the accounts is Google.
|
||||||
|
|
||||||
|
Remove an account lists the accounts with one button each. The confirmation says what removing
|
||||||
|
costs, which for a Google account is Margin's access at Google for all three Margin apps on every
|
||||||
|
machine, because they share one client, and carries a single box, ticked, to delete the account's
|
||||||
|
mail and decisions from this computer as well. Unticked, they are set aside on disk and come back
|
||||||
|
if the account is added again. There is no separate Revoke: removing always revokes, and the box
|
||||||
|
is the only choice left.
|
||||||
|
|
||||||
|
Add account asks for the address and nothing else, and runs the flow the welcome screen runs, in a
|
||||||
|
sheet: a Google address hands over to the browser and the strip above the list waits for it, any
|
||||||
|
other address gets its sign-in step in the same sheet, and an Outlook address is told why it cannot
|
||||||
|
be added rather than given a button that fails. Once the account is written, a panel takes the
|
||||||
|
window: first the storage window, the same five spans as the row below with a month chosen
|
||||||
|
already, because the first sync reads it; then the sync itself, the engine's own line, a bar the
|
||||||
|
width of the panel and the count of messages in. Nothing closes it. When the pass ends it opens that account's Inbox, and the first-run
|
||||||
|
panel over it says how many senders were screened in. A first pass that stops shows the provider's
|
||||||
|
sentence with Try again and Go in anyway. The flow is specified in [ui.md](ui.md). Remove
|
||||||
|
account asks once, then deletes that
|
||||||
|
account's mirror, its state database and its stored secret, and says that the mail itself is
|
||||||
|
untouched on the provider. Last on the section, quiet and collapsed, is bring your own OAuth client: a
|
||||||
|
field for a client id and secret for someone who would rather not share the suite's, with one
|
||||||
|
sentence saying what it is for and that nothing else changes.
|
||||||
|
|
||||||
|
## Appearance
|
||||||
|
|
||||||
|
Theme: light, dark, or follow the system. Interface font and text font, from the catalogue
|
||||||
|
described at the foot of this document. Text size, which scales the reading pane's body copy and
|
||||||
|
nothing else, because the chrome is already at the size it wants to be. Reading pane on or off,
|
||||||
|
which is the same switch as `Cmd+\`. Density, which only appears on a phone and only chooses
|
||||||
|
between the comfortable row and a tighter one.
|
||||||
|
|
||||||
|
## Mail
|
||||||
|
|
||||||
|
The storage window, per account, as a row of choices: 30 days, 90 days, 180 days, a year,
|
||||||
|
everything. Under it, one line saying what the account currently holds and how far back it
|
||||||
|
reaches. Shrinking the window says how many threads will be evicted before it does it; widening
|
||||||
|
starts a backfill and shows the same thin bar the first sync uses. Threads carrying a pile, a
|
||||||
|
note, a snooze or any other decision are kept regardless, and the section says so.
|
||||||
|
|
||||||
|
Then two smaller things: the attachment cache cap, with the space currently used beside it, and
|
||||||
|
whether to prefetch message bodies inside the window when the app is idle. Both are about disk and
|
||||||
|
neither changes what is in a list.
|
||||||
|
|
||||||
|
## Privacy
|
||||||
|
|
||||||
|
Remote images: never, ask per message, or always. Link cleaning on or off. A list of the senders
|
||||||
|
allowed to load images, added from the contact card and removable here.
|
||||||
|
|
||||||
|
This section carries the one sentence the app owes the reader about what showing an image means:
|
||||||
|
loading a remote image tells the server that hosts it that you opened the message, from your IP
|
||||||
|
address, at that moment. Nothing else in the app reveals that, and the sentence sits under the
|
||||||
|
control rather than in a help page.
|
||||||
|
|
||||||
|
## Screener
|
||||||
|
|
||||||
|
The gate on or off. With it off, a sender with no rule is routed by the suggestion function and
|
||||||
|
nothing is held; with it on, first contact waits. Whether replies to threads you are already in
|
||||||
|
are held (they are not, by default, and the switch exists for the small number of people who want
|
||||||
|
absolutely everything screened). Suggestions on or off: with them off, the Screener card shows the
|
||||||
|
three destinations and no recommendation.
|
||||||
|
|
||||||
|
## Piles and snooze
|
||||||
|
|
||||||
|
The times behind the snooze choices: later today, tomorrow morning, the weekend, next week. Each
|
||||||
|
is a time of day or an offset the user can set once and forget. On a phone, the two swipe actions
|
||||||
|
and which direction each is on. And the Feed's automatic trash age, which is off by default and
|
||||||
|
can be set here for every Feed sender or from a contact card for one of them.
|
||||||
|
|
||||||
|
## Writing
|
||||||
|
|
||||||
|
The undo delay: five, ten, twenty or thirty seconds, ten by default. Whether `r` means reply or
|
||||||
|
reply all. The instant intro text, as an editable template with the placeholder it uses. And the
|
||||||
|
signature per account, which is the same field as the one on the account card in Accounts, shown
|
||||||
|
here because this is where somebody writing a signature will look for it.
|
||||||
|
|
||||||
|
## Notifications
|
||||||
|
|
||||||
|
New mail can arrive as a push notification on the machine the app is running on: a banner from the
|
||||||
|
system with the app's name, the sender and the subject, and a click on it opens the thread. Off by
|
||||||
|
default and the section says so first, with one
|
||||||
|
|
||||||
|
exception it names: the dock badge, which is on. One switch, Allow notifications, is the system's
|
||||||
|
permission and the app's own preference together, because nobody cares which of the two is saying
|
||||||
|
no. Turning it on asks the system when it has never been asked, and a yes turns the Inbox on with
|
||||||
|
it so the switch does something; a no leaves it off over one line saying notifications are turned
|
||||||
|
off for the app in System Settings and one button that opens them there, since the answer lives
|
||||||
|
there and not here. Only while the switch is on does the rest show: the three places, so somebody
|
||||||
|
who wants to be told about the Inbox and never about the Feed can say that once rather than thread
|
||||||
|
by thread, and a Send a test notification button. There is no sound setting. A notification comes
|
||||||
|
with the system's sound, and the system's own pane is where that is muted. The first place turned
|
||||||
|
on in the first run panel asks the same question. Last is the dock badge, with a note that it is
|
||||||
|
not a notification and needs no permission: it counts unseen Inbox threads across every account,
|
||||||
|
which is what is waiting for a decision rather than what is unread, and turning it off here takes
|
||||||
|
it off the dock at once.
|
||||||
|
|
||||||
|
## Keyboard
|
||||||
|
|
||||||
|
The keymap is a file, not a table of pickers. This section says where it is, opens it in the
|
||||||
|
system editor, and resets it to the defaults in [keyboard.md](keyboard.md). A file that fails to
|
||||||
|
parse is reported here with the line number and the defaults stay in force until it is fixed.
|
||||||
|
|
||||||
|
## Backup
|
||||||
|
|
||||||
|
Which store, if any: Google Drive or Cloudflare R2 with S3 credentials. The status of the
|
||||||
|
connection and when the last backup completed. The recovery phrase, shown once when backup is
|
||||||
|
first turned on and never again, with the sentence saying that it is the only way to attach a
|
||||||
|
second device or restore after a lost one, and that writing it down now is the whole of the
|
||||||
|
arrangement. Restore takes a recovery phrase and pulls the journal back down.
|
||||||
|
|
||||||
|
Never again is not a policy, it is the mechanism. The phrase is generated, the key it derives is
|
||||||
|
sealed, and the phrase itself is dropped; a device that could show it again would be a device that
|
||||||
|
had kept it, and then the phrase would protect nothing that the disk did not already give away.
|
||||||
|
The section says that in a line rather than apologising for it.
|
||||||
|
|
||||||
|
The store holds ciphertext and file names and nothing else, which the section says in a line
|
||||||
|
rather than a paragraph.
|
||||||
|
|
||||||
|
## Data
|
||||||
|
|
||||||
|
Export mail as mbox, per account, from the window that is on the device. Export app state as JSON
|
||||||
|
and import it back, which is the portable form of every decision the app holds. Storage used,
|
||||||
|
broken down into the mirror, the attachment cache and the state database. Clear the mirror, which
|
||||||
|
deletes the local copy of the mail, keeps every decision, and resyncs the window from scratch; it
|
||||||
|
is the answer to a corrupt database and it asks once.
|
||||||
|
|
||||||
|
## About
|
||||||
|
|
||||||
|
The version, the licence (FSL-1.1-MIT, with the line about each release turning MIT after two
|
||||||
|
years), check for updates on the desktop, and the "packaged by" note that names who built the
|
||||||
|
binary. Nothing else. This section is where somebody files a bug from, so the version string is
|
||||||
|
selectable.
|
||||||
|
|
||||||
|
## Where a setting lives
|
||||||
|
|
||||||
|
Two places, and the split is not arbitrary.
|
||||||
|
|
||||||
|
**Device settings** live in a `settings.json` in the app data directory, written atomically by
|
||||||
|
replacing the file rather than editing it, so a crash mid-write leaves the old file rather than
|
||||||
|
half a new one. Theme, both fonts, text size, the storage window, the cache caps, notifications,
|
||||||
|
the keymap path and the backup store's configuration are all device settings. They describe this
|
||||||
|
machine: the window that suits a laptop with a small disk is not the window that suits a desktop,
|
||||||
|
and a notification preference that roamed to a phone would be wrong on arrival.
|
||||||
|
|
||||||
|
**Roaming settings** are per account decisions that should follow the person rather than the
|
||||||
|
hardware: sender rules, signatures, the instant intro text. They live in the state database and go
|
||||||
|
through its journal like every other decision, so they reach a second device through the backup
|
||||||
|
store and survive a reinstall. The rule of thumb is that anything a person would be annoyed to
|
||||||
|
retype on a new machine roams, and anything about this machine's screen or disk does not.
|
||||||
|
|
||||||
|
## Fonts
|
||||||
|
|
||||||
|
The two font controls reuse margin-shared's catalogue rather than defining one here, because a
|
||||||
|
face named in one Margin app and missing in another is the bug that catalogue exists to prevent. A
|
||||||
|
`FontRef` is stored, not a family name, so a bundled face and a system face that happen to share a
|
||||||
|
name stay distinct.
|
||||||
|
|
||||||
|
Six families are bundled: Hanken Grotesk, Literata, EB Garamond, Lora, Source Serif 4 and
|
||||||
|
Fraunces. Alongside them the picker lists every family `fontdb` finds on the machine. Interface
|
||||||
|
font writes `--font-ui` on the root and text font writes `--font-heading`, which is the same pair
|
||||||
|
of variables the stylesheet already reads, so changing a face is a token change and touches
|
||||||
|
nothing else.
|
||||||
@@ -5,6 +5,89 @@ HTML mockups in [mockups/src/](mockups/src/), built on the real token set, so th
|
|||||||
target rather than an impression of it; `mockups/render.sh` regenerates them. Behaviour is
|
target rather than an impression of it; `mockups/render.sh` regenerates them. Behaviour is
|
||||||
specified in [features.md](features.md), keys in [keyboard.md](keyboard.md).
|
specified in [features.md](features.md), keys in [keyboard.md](keyboard.md).
|
||||||
|
|
||||||
|
## Connecting
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The first screen, and the only one that is not the app. The wordmark, a sentence under it, one
|
||||||
|
field for the address, and Continue. No provider is named as a button, because a person knows
|
||||||
|
their address and does not always know who runs the mailbox behind it, and a screen that opens with
|
||||||
|
a Google button and an "anything else" button under it has already decided who it was built for.
|
||||||
|
Under the field, in the faint ink, the sentence that says what works: Google, Fastmail, iCloud,
|
||||||
|
Yahoo, Proton through Bridge, a mailbox where you work, or anything else that speaks IMAP.
|
||||||
|
|
||||||
|
Continue reads the domain and works the rest out. The button says "Looking up northgate.example"
|
||||||
|
while it does, the field is held, and Stop sits beside it, so a press never looks like nothing
|
||||||
|
happened and a lookup that was never going to answer is not a wait anybody is held to. Under the
|
||||||
|
faint sentence, quieter still, is Enter the servers myself, for somebody who already knows nobody
|
||||||
|
publishes theirs. Four things can come next.
|
||||||
|
|
||||||
|
A Google address, whether gmail.com or a work domain whose mail is delivered to Google, goes to the
|
||||||
|
browser. The screen becomes Waiting for Google, with Open link again and Copy link below it,
|
||||||
|
because the browser that opened is not always the browser in front of you, and the sentence that
|
||||||
|
says what the sign-in means sits under them:
|
||||||
|
|
||||||
|
> Sign-in happens in your browser with Google. Margin never sees your password. The key Google
|
||||||
|
> hands back is stored only on this device, and you can revoke it any time from your Google
|
||||||
|
> account.
|
||||||
|
|
||||||
|
The consent page opens on the address that was typed rather than on Google's chooser. Closing it
|
||||||
|
comes quietly back to the address, still in its field.
|
||||||
|
|
||||||
|
Every other address goes to a sign-in step headed with the provider's own name: "Sign in to
|
||||||
|
Fastmail". Its first sentence says where the servers came from, before the password is typed into
|
||||||
|
them, because a configuration the provider publishes and one guessed by trying the usual server
|
||||||
|
names are different promises. Under it, the two servers in one line each, then Your name and
|
||||||
|
Password. Where the provider is one that refuses the password you sign in with, the hint under the
|
||||||
|
field says so and names the place in that provider's settings where an app password is made; that
|
||||||
|
is the single most common way a mail setup fails, and it is said before it can. Add account tests
|
||||||
|
both servers and, if they answer, adds the account and goes straight to the mail. A refusal is
|
||||||
|
explained on the same panel, with the server's own sentence and what to do about it. Change the
|
||||||
|
servers opens the sheet with both halves editable; a certificate nobody vouches for is a question
|
||||||
|
of its own, with the fingerprint to check it by.
|
||||||
|
|
||||||
|
An address nobody publishes settings for lands on the same sign-in step with a different first
|
||||||
|
sentence and Enter the servers in place of Add account, which opens the sheet with the usual names
|
||||||
|
already typed in as a starting point.
|
||||||
|
|
||||||
|
Between the account being written and its mail arriving there is one question, on the welcome
|
||||||
|
stage and in the panel alike: how far back this device holds. The same five spans as the Storage
|
||||||
|
window row in Settings, a month chosen already, one Start button. It is asked rather than assumed
|
||||||
|
because the first sync reads the answer, and a year of a busy mailbox is a wait somebody should
|
||||||
|
have chosen. The progress that follows is named after the answer: "Bringing in the last month",
|
||||||
|
"Bringing in the last year", "Bringing in everything".
|
||||||
|
|
||||||
|
An account added while the app is already up, from Settings or from the sign-in step for a
|
||||||
|
password account, does not land quietly. The moment it is written, "Bringing in the address" takes
|
||||||
|
the window as a panel: the question first, then the engine's line ("Listing your mail", "Fetching
|
||||||
|
the newest mail first"), a bar the width of the panel, and "1,204 of 4,812 messages" under it.
|
||||||
|
There is no close control that works, no Escape and no way through the scrim, because there is
|
||||||
|
nothing to do but wait and the wait is short. When the pass ends the panel goes, Settings goes
|
||||||
|
with it, and the window is that account's Inbox with the first-run panel over it. A first pass
|
||||||
|
that stops turns the panel into the provider's own sentence with Try again and Go in anyway, the
|
||||||
|
same two ways on the welcome screen offers.
|
||||||
|
|
||||||
|
An Outlook, Hotmail or Microsoft 365 address, recognised from the domain or from where the mail is
|
||||||
|
delivered, is told plainly that Microsoft turned off password sign-in and that the sign-in it wants
|
||||||
|
instead needs a registration Margin does not have yet. A HEY or Tuta address is told that there is
|
||||||
|
no IMAP behind it at all. Neither asks for a password that would only fail, and both offer the way
|
||||||
|
back to the address.
|
||||||
|
|
||||||
|
Then the screen becomes progress: "Bringing in the last month", a thin bar, and a count of what
|
||||||
|
has arrived. Nobody should have to watch it, so it turns into the Inbox as soon as the first page
|
||||||
|
of threads lands and the rest fills in behind.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The first-run panel arrives over that Inbox once the pass over senders has finished. It says how
|
||||||
|
many senders were screened in and where they went, offers Start fresh with its age picker (a week
|
||||||
|
by default), and ends with Done. Skipping is Done. Start fresh is the only bulk write the app ever
|
||||||
|
proposes, and the panel says so in a line rather than in a warning.
|
||||||
|
|
||||||
|
The tour follows it, whichever way it ended: nine slides on what this app does that the last one
|
||||||
|
did not, with Skip as the first control on them. It runs for every account that is added, and
|
||||||
|
[help.md](help.md) says why that is not the same as running it at every launch.
|
||||||
|
|
||||||
## The window
|
## The window
|
||||||
|
|
||||||

|

|
||||||
@@ -13,20 +96,34 @@ One header row, then the stage. The header carries the account chip on the left
|
|||||||
chevron, the switcher and All accounts behind it), the three boxes as a segmented switch in the
|
chevron, the switcher and All accounts behind it), the three boxes as a segmented switch in the
|
||||||
centre with their number keys printed, and on the right search, the places button (which opens
|
centre with their number keys printed, and on the right search, the places button (which opens
|
||||||
the palette on its Places group) and Write. On macOS the traffic lights float over the left lane.
|
the palette on its Places group) and Write. On macOS the traffic lights float over the left lane.
|
||||||
Nothing else is persistent: no sidebar, no folder tree, no toolbar. Sync status is not shown
|
Nothing else is persistent: no sidebar, no folder tree, no toolbar. Sync says what it is doing in
|
||||||
unless something is wrong, in which case the account chip carries a small note ("Offline",
|
the account chip's lane and nowhere else: a note when something is wrong ("Offline", "Signed out",
|
||||||
"Signed out").
|
"Sync trouble", "Paused"), in the engine's own word since only it knows the kind of failure,
|
||||||
|
and otherwise one faint line naming the work and its progress while there is any ("Caching recent
|
||||||
|
mail, 340 of 1,412"). A toast is raised only for the pause, for a refused token or a missing
|
||||||
|
permission, and for a write the provider refused for good; everything else the chip carries and the
|
||||||
|
next poll answers. It goes when the work does. Nothing about it can be pressed, and it is in the
|
||||||
|
chip's lane rather than the centre so that a line growing and shrinking never moves the boxes.
|
||||||
|
|
||||||
|
A mailbox is readable long before it is complete, so the alternative to that line is an app that
|
||||||
|
looks finished while it is still working, and a first impression of mail that is missing rather
|
||||||
|
than mail that is on its way.
|
||||||
|
|
||||||
The stage is the list column, 420 px, and the reading pane. The list column has a head (the
|
The stage is the list column, 420 px, and the reading pane. The list column has a head (the
|
||||||
place's name in the text face and, in the Inbox, the Screener pill), the list, and the two piles
|
place's name in the text face and, in the Inbox, the Screener pill), the list, and the two piles
|
||||||
at the foot. With the pane hidden the list takes the width and a thread opens in place.
|
at the foot. With the pane hidden the list takes the width and a thread opens in place. A list
|
||||||
|
that has run out of what is on the device closes with one faint line saying so: in Everything it
|
||||||
|
reads "Showing the last month. Older mail is on Gmail.", and in search results it is the button
|
||||||
|
that runs the provider's search instead.
|
||||||
|
|
||||||
### Rows
|
### Rows
|
||||||
|
|
||||||
Two lines at 58 px. Left gutter for the new-mail dot, a 30 px avatar (initials on one of the
|
Two lines at 46 px. Left gutter for the new-mail dot, a 30 px avatar (initials on one of the
|
||||||
eight muted hues, or a bordered brand mark for companies), then sender and time on the first
|
eight muted hues, or a bordered brand mark for companies), then sender and time on the first
|
||||||
line and subject and snippet on the second. New mail: a dot and a 600-weight sender. Seen mail:
|
line and subject and snippet on the second. Unseen mail: sender and subject at 600 weight in the
|
||||||
no dot, subject in the soft ink. A message count sits between sender and time when the thread
|
full ink. Seen mail: regular weight, subject in the soft ink. That weight is the only read
|
||||||
|
indicator there is, no dot, no band and no count, which is Superhuman's rule and the one signal that
|
||||||
|
cannot lag behind another. A message count sits between sender and time when the thread
|
||||||
has more than one. A note shows as one line under the row on the note surface. In All accounts
|
has more than one. A note shows as one line under the row on the note surface. In All accounts
|
||||||
the row has a 2 px coloured left edge for its account. The selected row has a 2 px accent edge
|
the row has a 2 px coloured left edge for its account. The selected row has a 2 px accent edge
|
||||||
and a wash; hover is a lighter wash. There are no chips, no icons on hover, and no checkboxes
|
and a wash; hover is a lighter wash. There are no chips, no icons on hover, and no checkboxes
|
||||||
@@ -34,9 +131,12 @@ until `x` is pressed, at which point the gutter shows one.
|
|||||||
|
|
||||||
### Groups
|
### Groups
|
||||||
|
|
||||||
New for you, Previously seen (on a slightly warmer band), and Back when a snooze has returned.
|
The Inbox is one list in time order, with Back above it when a snooze has returned. There is no New
|
||||||
Group headings are small uppercase labels with a rule, the same as every section label in the
|
for you and no Previously seen: HEY splits by read state and Superhuman does not, and this app went
|
||||||
family. The New for you heading carries "Mark all as seen" at its right; nothing carries a count.
|
with Superhuman once it had tried carrying both. Other places group by age (Today, This week, This
|
||||||
|
month, Earlier) and Sent by recipient. Group headings are small uppercase labels with a rule, the
|
||||||
|
same as every section label in the family; nothing carries a count. Mark all as seen is a palette
|
||||||
|
row and a key.
|
||||||
|
|
||||||
### The piles
|
### The piles
|
||||||
|
|
||||||
@@ -48,7 +148,8 @@ selection exists the piles give way to the action bar.
|
|||||||
### The reading pane
|
### The reading pane
|
||||||
|
|
||||||
A bar of verbs with keys (Reply, Reply later, Set aside, Snooze, then Archive and More on the
|
A bar of verbs with keys (Reply, Reply later, Set aside, Snooze, then Archive and More on the
|
||||||
right), then the thread: subject in Literata at 22 px, a participants line with stacked avatars,
|
right), then the thread. More, or `.`, drops the rest of the thread's verbs with their keys in the
|
||||||
|
order the keyboard table lists them, and leaves out any that mean nothing where you are. Then the thread: subject in Literata at 22 px, a participants line with stacked avatars,
|
||||||
the tracker banner when anything was stripped, then messages. Each message has an avatar, the
|
the tracker banner when anything was stripped, then messages. Each message has an avatar, the
|
||||||
sender's name and address, "to you", and the time; older messages collapse to a preview line.
|
sender's name and address, "to you", and the time; older messages collapse to a preview line.
|
||||||
Bodies are Literata at 15 px on a 46 em measure for text mail, and the sanitised HTML for
|
Bodies are Literata at 15 px on a 46 em measure for text mail, and the sanitised HTML for
|
||||||
@@ -60,9 +161,39 @@ key, Remind me if no reply, and attach and note icons.
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
Driven by `data-theme` with the shared dark palette. Message bodies stay on the paper surface
|
Driven by `data-theme` with the shared dark palette, and the one thing that needs stating
|
||||||
in dark mode too, because HTML mail is written for a light background and inverting it breaks
|
precisely is what happens to a message body.
|
||||||
more than it fixes; the pane's own chrome goes dark around it.
|
|
||||||
|
A message renders on a light page when the sender painted one, and on the app's own surface when
|
||||||
|
they did not. That is the whole rule, and what decides it is paint rather than `Content-Type`. A
|
||||||
|
newsletter lays out a page: a wash behind a 600 px card, a header band, a footer in grey. It keeps
|
||||||
|
that page in both palettes, because it is a design and taking it apart loses more than it saves.
|
||||||
|
A colleague's mail composed in a client that happens to send HTML carries bold runs, a list, four
|
||||||
|
links and a signature, and no colour and no surface at all. There is nothing in it that wants a
|
||||||
|
white page, so it reads like the rest of the app. Either way the pane's own chrome goes dark
|
||||||
|
around the body.
|
||||||
|
|
||||||
|
This used to say "a message that arrived as HTML stays on the paper surface", and that was wrong.
|
||||||
|
Most HTML mail is somebody typing to you. Pinning all of it to white put a slab of white in the
|
||||||
|
middle of a dark window for every ordinary message anybody was sent, which is a much bigger loss
|
||||||
|
than the newsletter case it was protecting.
|
||||||
|
|
||||||
|
The hazard the old rule existed for is real, and it is handled on the other side. A sender who
|
||||||
|
sets a text colour without setting a background is dark ink on a dark page, so on the theme
|
||||||
|
surface Rust takes off, before the body is ever cached, any author colour that would not read on
|
||||||
|
our own paper: what survives is the mid-tone band a brand red or a heading grey lives in, and what
|
||||||
|
goes is the near-black that would vanish on our dark page and the near-white that would vanish on
|
||||||
|
our light one. A background the sender painted under their own text is judged with it, and it is
|
||||||
|
what gives way first.
|
||||||
|
|
||||||
|
The document inside the frame is told which of the two it is, with `color-scheme`. On a painted
|
||||||
|
page that is `only light`, without which the browser puts its own dark canvas and its own dark
|
||||||
|
form controls under a page we have just finished painting white.
|
||||||
|
|
||||||
|
The decision is made once, in the sanitiser, and stored on the body row, because opening a thread
|
||||||
|
is a local read and is not allowed to grow a walk over the markup. Every heuristic misses, so a
|
||||||
|
message head in dark carries one quiet control that overrules it for that message, for as long as
|
||||||
|
the app is open.
|
||||||
|
|
||||||
## The Screener
|
## The Screener
|
||||||
|
|
||||||
@@ -71,8 +202,8 @@ more than it fixes; the pane's own chrome goes dark around it.
|
|||||||
The whole stage. A centred title and one sentence of explanation, then a card per sender:
|
The whole stage. A centred title and one sentence of explanation, then a card per sender:
|
||||||
avatar, name and address, subject, snippet, and a pill with the reason and the suggested box.
|
avatar, name and address, subject, snippet, and a pill with the reason and the suggested box.
|
||||||
On the right, three buttons with their keys: Yes to the suggestion, Elsewhere, No. The focused
|
On the right, three buttons with their keys: Yes to the suggestion, Elsewhere, No. The focused
|
||||||
card has a faint ring. Under the list, one faint sentence about the 90-day retention and how to
|
card has a faint ring. Under the list, one faint sentence about how long screened-out mail stays
|
||||||
reverse a decision. Clear all is a ghost button at the top right.
|
and how to reverse a decision. Clear all is a ghost button at the top right.
|
||||||
|
|
||||||
## The Feed
|
## The Feed
|
||||||
|
|
||||||
@@ -120,9 +251,42 @@ Replies do not use the card; they are the box at the end of the thread in the pa
|
|||||||

|

|
||||||
|
|
||||||
`Cmd+K`. A panel at 12 vh from the top over a scrim, with a single field and a list grouped into
|
`Cmd+K`. A panel at 12 vh from the top over a scrim, with a single field and a list grouped into
|
||||||
Places, Actions, People, Settings. Every row prints its key. Typing filters across groups. It is
|
Places, Labels, Other, Actions, People, Settings. Every row prints its key. Typing filters across
|
||||||
the only menu in the app and the way every setting is reached; the places button in the header
|
groups. It is the only menu in the app and the way every setting is reached; the places button in
|
||||||
opens it on the Places group. The shortcut sheet behind `?` is generated from the same data.
|
the header opens it on the Places group. The shortcut sheet behind `?` is generated from the same
|
||||||
|
data.
|
||||||
|
|
||||||
|
Other is Screened out, Spam and Trash, and it is last of the three place groups on purpose: none
|
||||||
|
of them has a number key, and a menu that put them beside the Inbox would be claiming they are
|
||||||
|
somewhere you go daily. Their rules are in [features.md](features.md) section 12.
|
||||||
|
|
||||||
|
## Settings
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The whole stage, and a place rather than an overlay. A rail of section names down the left with
|
||||||
|
one selected, and that section on the right at a 640 px measure. No tabs and no nesting: a section
|
||||||
|
is a column of labelled controls with a sentence under any control that needs one.
|
||||||
|
|
||||||
|
Accounts is the section it opens on. Each account is a card with its colour, its name and address,
|
||||||
|
its signature and aliases, and under them the permissions it granted, one line each. A permission
|
||||||
|
that is missing says what it costs in a sentence and carries a Grant button. Add account sits at
|
||||||
|
the foot of the list. Every section is specified in [settings.md](settings.md).
|
||||||
|
|
||||||
|
## Help
|
||||||
|
|
||||||
|
A small round question mark fixed in the bottom right corner, the only permanent chrome outside
|
||||||
|
the header, opening three rows: the tour, the guide, and the keyboard shortcuts with its key. It is
|
||||||
|
not drawn while the compose card is open, since they share that corner, nor under an overlay, nor
|
||||||
|
on a phone.
|
||||||
|
|
||||||
|
The guide is a panel over the whole window: a search field across the full width, then a rail of
|
||||||
|
sections down the left and one article on the right at a reading measure. Search has the keyboard
|
||||||
|
the moment it opens, and a query nothing answers offers to file the question against the
|
||||||
|
repository. An article leads with its answer and then shows it: a drawn figure, a screenshot, or a
|
||||||
|
table of the key beside the verb, and the last section is the questions. The app is dimmed behind
|
||||||
|
it and none of it can be pressed, and closing puts back the place, the open thread and the scroll.
|
||||||
|
[help.md](help.md) has the whole of it.
|
||||||
|
|
||||||
## Thread details
|
## Thread details
|
||||||
|
|
||||||
@@ -151,10 +315,17 @@ later, left for Set aside, both changeable in settings.
|
|||||||
|
|
||||||
## Empty states
|
## Empty states
|
||||||
|
|
||||||
Every empty list says one quiet thing in the text face and nothing else: "Nothing new for you"
|
Every empty list says one quiet thing in the text face and nothing else: "Nothing here" in an
|
||||||
above Previously seen; "Nothing here" in an empty place; "No one is waiting" in the Screener;
|
empty place; "No one is waiting" in the Screener;
|
||||||
"Nothing due" in Snoozed. No illustration, no photograph, no streak.
|
"Nothing due" in Snoozed. No illustration, no photograph, no streak.
|
||||||
|
|
||||||
|
A place that is empty because its account has not arrived yet is not empty, and does not say it
|
||||||
|
is. While the first sync is bringing the mailbox in, from the welcome screen or from Settings, the
|
||||||
|
list shows the engine's own line ("Listing your mail", "Fetching the newest mail first"), the thin
|
||||||
|
bar the welcome screen uses, and the count under it. "Nothing here" waits until the crawl has
|
||||||
|
finished, and so does the Screener pill in the Inbox, because until the seed has run the number on
|
||||||
|
it is every sender the account has met so far.
|
||||||
|
|
||||||
## Motion
|
## Motion
|
||||||
|
|
||||||
Transitions name explicit properties and use the family's one easing curve, 120 ms for hovers
|
Transitions name explicit properties and use the family's one easing curve, 120 ms for hovers
|
||||||
@@ -163,8 +334,8 @@ the toast is the acknowledgement. The compose card slides up; the palette fades.
|
|||||||
|
|
||||||
## Not in the mockups
|
## Not in the mockups
|
||||||
|
|
||||||
Settings (a standard panel with the sections listed in features.md), search results (the list
|
Search results (the list column with a query in the head and the provider's search offered at
|
||||||
column with a query in the head), All files (a card grid with a filter row), Clips (a list of
|
its foot), All files (a card grid with a filter row), Clips (a list of passages), Contacts (a list
|
||||||
passages), Contacts (a list with the same row anatomy), the snooze picker (a small popover with
|
with the same row anatomy), the snooze picker (a small popover with the six choices and their
|
||||||
the six choices and their keys), and the selection action bar (the piles' footprint filled with
|
keys), and the selection action bar (the piles' footprint filled with verbs). All of them are
|
||||||
verbs). All of them are compositions of the pieces above and need no new visual vocabulary.
|
compositions of the pieces above and need no new visual vocabulary.
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
description = "Margin Mail, a calm, keyboard-first mail client";
|
||||||
|
|
||||||
|
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
|
||||||
|
outputs =
|
||||||
|
{ self, nixpkgs }:
|
||||||
|
let
|
||||||
|
system = "x86_64-linux";
|
||||||
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
overlays.default = final: prev: {
|
||||||
|
margin-mail = final.callPackage ./nix/package.nix { };
|
||||||
|
};
|
||||||
|
|
||||||
|
packages.${system} = {
|
||||||
|
margin-mail = pkgs.callPackage ./nix/package.nix { };
|
||||||
|
default = self.packages.${system}.margin-mail;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
margin-mail.deb
|
||||||
|
build/
|
||||||
|
repo/
|
||||||
|
*.flatpak
|
||||||
|
.flatpak-builder/
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Build the flatpak from the deb sitting beside this script as margin-mail.deb, and write a
|
||||||
|
# single-file bundle next to it. Used by both CI jobs and by hand; the only difference between the
|
||||||
|
# two is where the deb came from.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
here=$(cd "$(dirname "$0")" && pwd)
|
||||||
|
cd "$here"
|
||||||
|
|
||||||
|
id=studio.margin.mail
|
||||||
|
deb=margin-mail.deb
|
||||||
|
bundle=${1:-$here/margin-mail.flatpak}
|
||||||
|
|
||||||
|
[ -f "$deb" ] || { echo "flatpak/build.sh: no $deb beside this script." >&2; exit 1; }
|
||||||
|
|
||||||
|
runtime_version=$(sed -n "s/^runtime-version: *'\(.*\)'/\1/p" "$id.yml")
|
||||||
|
|
||||||
|
# --user so nothing here needs root, and --if-not-exists so a second run is free.
|
||||||
|
flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
||||||
|
flatpak install --user --noninteractive flathub \
|
||||||
|
"org.gnome.Platform//$runtime_version" \
|
||||||
|
"org.gnome.Sdk//$runtime_version"
|
||||||
|
|
||||||
|
rm -rf build repo
|
||||||
|
flatpak-builder --user --disable-rofiles-fuse --force-clean --repo=repo build "$id.yml"
|
||||||
|
flatpak build-bundle repo "$bundle" "$id"
|
||||||
|
|
||||||
|
echo "Wrote $bundle"
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<component type="desktop-application">
|
||||||
|
<id>studio.margin.mail</id>
|
||||||
|
<name>Margin Mail</name>
|
||||||
|
<summary>A calm, keyboard-first mail client for Gmail</summary>
|
||||||
|
|
||||||
|
<metadata_license>CC0-1.0</metadata_license>
|
||||||
|
<project_license>LicenseRef-proprietary=https://github.com/priyanshujain/margin-mail/blob/main/LICENSE</project_license>
|
||||||
|
|
||||||
|
<developer id="studio.margin">
|
||||||
|
<name>Margin</name>
|
||||||
|
</developer>
|
||||||
|
|
||||||
|
<description>
|
||||||
|
<p>
|
||||||
|
New senders wait at the door until you let them in, people and newsletters and receipts live
|
||||||
|
in three separate boxes, and every decision you make is kept beside the mail rather than
|
||||||
|
inside it.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Trackers are stripped from what you read and never sent from what you write. The mirror of
|
||||||
|
your mail and every decision you have made about a sender are files on your own disk, so
|
||||||
|
changing provider costs you nothing.
|
||||||
|
</p>
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<launchable type="desktop-id">studio.margin.mail.desktop</launchable>
|
||||||
|
<url type="homepage">https://github.com/priyanshujain/margin-mail</url>
|
||||||
|
<url type="bugtracker">https://github.com/priyanshujain/margin-mail/issues</url>
|
||||||
|
|
||||||
|
<categories>
|
||||||
|
<category>Office</category>
|
||||||
|
<category>Email</category>
|
||||||
|
</categories>
|
||||||
|
|
||||||
|
<content_rating type="oars-1.1"/>
|
||||||
|
</component>
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# The flatpak, built by repackaging the deb rather than from source. Tauri has no flatpak bundler,
|
||||||
|
# and building from source here would produce an app that runs and then says Google is not set up:
|
||||||
|
# the OAuth client is embedded at compile time from a file that is deliberately not in the repo.
|
||||||
|
#
|
||||||
|
# GNOME 48 is the runtime because it is the newest that still carries webkit2gtk-4.1, which is the
|
||||||
|
# GTK3 WebKit wry links against. When that stops being true the answer is a GTK4 wry, not an older
|
||||||
|
# runtime.
|
||||||
|
id: studio.margin.mail
|
||||||
|
runtime: org.gnome.Platform
|
||||||
|
runtime-version: '48'
|
||||||
|
sdk: org.gnome.Sdk
|
||||||
|
command: margin-mail
|
||||||
|
|
||||||
|
finish-args:
|
||||||
|
- --socket=wayland
|
||||||
|
- --socket=fallback-x11
|
||||||
|
- --share=ipc
|
||||||
|
- --device=dri
|
||||||
|
# Gmail, IMAP, SMTP, and the loopback listener that catches Google's OAuth redirect. The redirect
|
||||||
|
# comes back to 127.0.0.1 inside the sandbox, which is where the app is listening, so this is the
|
||||||
|
# whole of what the connect flow needs; the consent page itself opens in the host's browser
|
||||||
|
# through the OpenURI portal, which needs no permission of its own.
|
||||||
|
- --share=network
|
||||||
|
- --talk-name=org.freedesktop.Notifications
|
||||||
|
# Saved attachments and exports go to the downloads directory and nowhere else.
|
||||||
|
- --filesystem=xdg-download
|
||||||
|
|
||||||
|
modules:
|
||||||
|
- name: margin-mail
|
||||||
|
buildsystem: simple
|
||||||
|
build-commands:
|
||||||
|
- mkdir -p deb
|
||||||
|
- ar x margin-mail.deb --output deb
|
||||||
|
- tar -C deb -xf deb/data.tar.gz
|
||||||
|
|
||||||
|
- install -Dm755 deb/usr/bin/margin-mail /app/bin/margin-mail
|
||||||
|
|
||||||
|
# Flatpak wants the desktop file and every icon named for the app id, and the deb names them
|
||||||
|
# after the product and the binary, so both are renamed on the way in.
|
||||||
|
- install -Dm644 "deb/usr/share/applications/Margin Mail.desktop" /app/share/applications/studio.margin.mail.desktop
|
||||||
|
- desktop-file-edit --set-icon=studio.margin.mail /app/share/applications/studio.margin.mail.desktop
|
||||||
|
- |
|
||||||
|
for src in deb/usr/share/icons/hicolor/*/apps/margin-mail.png; do
|
||||||
|
size=$(basename "$(dirname "$(dirname "$src")")")
|
||||||
|
install -Dm644 "$src" "/app/share/icons/hicolor/$size/apps/studio.margin.mail.png"
|
||||||
|
done
|
||||||
|
|
||||||
|
- install -Dm644 studio.margin.mail.metainfo.xml /app/share/metainfo/studio.margin.mail.metainfo.xml
|
||||||
|
|
||||||
|
sources:
|
||||||
|
- type: file
|
||||||
|
path: margin-mail.deb
|
||||||
|
- type: file
|
||||||
|
path: studio.margin.mail.metainfo.xml
|
||||||
@@ -24,6 +24,11 @@ test:
|
|||||||
test-ui:
|
test-ui:
|
||||||
pnpm test:ui
|
pnpm test:ui
|
||||||
|
|
||||||
|
# Recapture the pictures the guide ships with, from the fixture rather than from anybody's mailbox.
|
||||||
|
# They are committed, so this is not part of `test-ui`: an ordinary run must not rewrite the tree.
|
||||||
|
guide-shots:
|
||||||
|
GUIDE_SHOTS=1 pnpm exec playwright test tests/guide-shots.spec.ts
|
||||||
|
|
||||||
# The prose gate: no em dashes, no broken relative links, no directory trees.
|
# The prose gate: no em dashes, no broken relative links, no directory trees.
|
||||||
docs:
|
docs:
|
||||||
node scripts/docs-check.mjs
|
node scripts/docs-check.mjs
|
||||||
@@ -35,6 +40,18 @@ build:
|
|||||||
# The .app on macOS, the .deb and AppImage on Linux. No dmg: nothing here needs a disk image to
|
# The .app on macOS, the .deb and AppImage on Linux. No dmg: nothing here needs a disk image to
|
||||||
# copy a bundle into place, and building one is the slowest part of a mac bundle.
|
# copy a bundle into place, and building one is the slowest part of a mac bundle.
|
||||||
pnpm install
|
pnpm install
|
||||||
|
# macOS shows no notifications from a bundle that is not signed, so tauri.conf.json ad-hoc signs
|
||||||
|
# at minimum and a real identity replaces that when this machine has one: the signing directory
|
||||||
|
# holds an env file naming it, and Tauri reads APPLE_SIGNING_IDENTITY over the config.
|
||||||
|
if [ "$(uname -s)" = Darwin ]; then
|
||||||
|
signing="${MARGIN_SIGNING_DIR:-$HOME/.margin-signing}/studio.margin.app.env"
|
||||||
|
if [ -f "$signing" ]; then
|
||||||
|
set -a; . "$signing"; set +a
|
||||||
|
echo "Signing as $APPLE_SIGNING_IDENTITY"
|
||||||
|
else
|
||||||
|
echo "No $signing; the bundle will be ad-hoc signed."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
case "$(uname -s)" in
|
case "$(uname -s)" in
|
||||||
Darwin) pnpm tauri build --bundles app ;;
|
Darwin) pnpm tauri build --bundles app ;;
|
||||||
Linux) pnpm tauri build --bundles deb,appimage ;;
|
Linux) pnpm tauri build --bundles deb,appimage ;;
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
# The Linux package: the deb the release workflow published, relinked against nixpkgs' own GTK and
|
||||||
|
# WebKit so it runs as a native Wayland client. It is a binary package by necessity: the Google
|
||||||
|
# OAuth client is embedded at compile time from a file that is deliberately not in the repo, so
|
||||||
|
# anything built from source here would run and then say Google is not set up.
|
||||||
|
#
|
||||||
|
# Ported from Margin Calendar's nix/package.nix, which is the sibling to read when this needs work.
|
||||||
|
{
|
||||||
|
lib,
|
||||||
|
stdenv,
|
||||||
|
fetchurl,
|
||||||
|
dpkg,
|
||||||
|
autoPatchelfHook,
|
||||||
|
wrapGAppsHook3,
|
||||||
|
cairo,
|
||||||
|
dbus,
|
||||||
|
gdk-pixbuf,
|
||||||
|
glib,
|
||||||
|
glib-networking,
|
||||||
|
gsettings-desktop-schemas,
|
||||||
|
gtk3,
|
||||||
|
libsoup_3,
|
||||||
|
mesa,
|
||||||
|
runtimeShell,
|
||||||
|
webkitgtk_4_1,
|
||||||
|
xdg-utils,
|
||||||
|
}:
|
||||||
|
|
||||||
|
let
|
||||||
|
release = lib.importJSON ./release.json;
|
||||||
|
in
|
||||||
|
stdenv.mkDerivation {
|
||||||
|
pname = "margin-mail";
|
||||||
|
inherit (release) version;
|
||||||
|
|
||||||
|
src = fetchurl {
|
||||||
|
url = "https://github.com/priyanshujain/margin-mail/releases/download/v${release.version}/Margin.Mail_${release.version}_amd64.deb";
|
||||||
|
inherit (release) hash;
|
||||||
|
};
|
||||||
|
|
||||||
|
unpackPhase = ''
|
||||||
|
runHook preUnpack
|
||||||
|
dpkg-deb -x $src .
|
||||||
|
runHook postUnpack
|
||||||
|
'';
|
||||||
|
|
||||||
|
nativeBuildInputs = [
|
||||||
|
dpkg
|
||||||
|
autoPatchelfHook
|
||||||
|
wrapGAppsHook3
|
||||||
|
];
|
||||||
|
|
||||||
|
buildInputs = [
|
||||||
|
cairo
|
||||||
|
dbus
|
||||||
|
gdk-pixbuf
|
||||||
|
glib
|
||||||
|
glib-networking
|
||||||
|
gsettings-desktop-schemas
|
||||||
|
gtk3
|
||||||
|
libsoup_3
|
||||||
|
webkitgtk_4_1
|
||||||
|
];
|
||||||
|
|
||||||
|
# The binary sits under lib/ so wrapGAppsHook wraps the launcher in bin/ and nothing else.
|
||||||
|
#
|
||||||
|
# Outside NixOS there is no /run/opengl-driver, so the libglvnd this build links finds no EGL
|
||||||
|
# driver and WebKit aborts its web process on the spot. The launcher points it at nixpkgs' Mesa
|
||||||
|
# instead, unless something like nixGL already did. The xdg-open shim strips that again for the
|
||||||
|
# browser the app opens for the Google consent page, which has a Mesa of its own.
|
||||||
|
installPhase = ''
|
||||||
|
runHook preInstall
|
||||||
|
mkdir -p $out/bin $out/lib/margin-mail/bin
|
||||||
|
cp usr/bin/margin-mail $out/lib/margin-mail/
|
||||||
|
cp -r usr/share $out/
|
||||||
|
mv "$out/share/applications/Margin Mail.desktop" $out/share/applications/margin-mail.desktop
|
||||||
|
|
||||||
|
cat > $out/bin/margin-mail <<LAUNCHER
|
||||||
|
#!${runtimeShell}
|
||||||
|
if [ ! -d /run/opengl-driver ]; then
|
||||||
|
: "\''${__EGL_VENDOR_LIBRARY_FILENAMES:=$(echo ${mesa}/share/glvnd/egl_vendor.d/*.json)}"
|
||||||
|
: "\''${LIBGL_DRIVERS_PATH:=${mesa}/lib/dri}"
|
||||||
|
: "\''${GBM_BACKENDS_PATH:=${mesa}/lib/gbm}"
|
||||||
|
export __EGL_VENDOR_LIBRARY_FILENAMES LIBGL_DRIVERS_PATH GBM_BACKENDS_PATH
|
||||||
|
fi
|
||||||
|
exec $out/lib/margin-mail/margin-mail "\$@"
|
||||||
|
LAUNCHER
|
||||||
|
|
||||||
|
cat > $out/lib/margin-mail/bin/xdg-open <<SHIM
|
||||||
|
#!${runtimeShell}
|
||||||
|
unset __EGL_VENDOR_LIBRARY_FILENAMES LIBGL_DRIVERS_PATH GBM_BACKENDS_PATH
|
||||||
|
exec ${xdg-utils}/bin/xdg-open "\$@"
|
||||||
|
SHIM
|
||||||
|
|
||||||
|
chmod +x $out/bin/margin-mail $out/lib/margin-mail/bin/xdg-open
|
||||||
|
runHook postInstall
|
||||||
|
'';
|
||||||
|
|
||||||
|
# The variable is how the app knows the store owns the binary, so "Check for updates" reports the
|
||||||
|
# new version and the upgrade command instead of trying to replace a file it cannot write.
|
||||||
|
preFixup = ''
|
||||||
|
gappsWrapperArgs+=(
|
||||||
|
--prefix PATH : $out/lib/margin-mail/bin
|
||||||
|
--set MARGIN_MAIL_PACKAGED_BY nix
|
||||||
|
)
|
||||||
|
'';
|
||||||
|
|
||||||
|
meta = {
|
||||||
|
description = "A calm, keyboard-first mail client for Gmail";
|
||||||
|
longDescription = "New senders wait at the door until you let them in, people and newsletters and receipts live in three separate boxes, and every decision you make is kept beside the mail rather than inside it.";
|
||||||
|
homepage = "https://github.com/priyanshujain/margin-mail";
|
||||||
|
# Not one of lib.licenses: FSL is source available rather than free, and it turns MIT two years
|
||||||
|
# after each release. `free = false` is what makes nix ask before building it, which is correct
|
||||||
|
# and is why the workflows pass NIXPKGS_ALLOW_UNFREE.
|
||||||
|
license = {
|
||||||
|
shortName = "FSL-1.1-MIT";
|
||||||
|
fullName = "Functional Source License, Version 1.1, MIT Future License";
|
||||||
|
url = "https://github.com/priyanshujain/margin-mail/blob/main/LICENSE";
|
||||||
|
free = false;
|
||||||
|
redistributable = true;
|
||||||
|
};
|
||||||
|
sourceProvenance = [ lib.sourceTypes.binaryNativeCode ];
|
||||||
|
platforms = [ "x86_64-linux" ];
|
||||||
|
mainProgram = "margin-mail";
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"version": "",
|
||||||
|
"hash": ""
|
||||||
|
}
|
||||||
@@ -33,6 +33,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.62.1",
|
"@playwright/test": "^1.62.1",
|
||||||
"@tauri-apps/cli": "^2",
|
"@tauri-apps/cli": "^2",
|
||||||
|
"@types/node": "^24.0.0",
|
||||||
"@types/react": "^19.1.8",
|
"@types/react": "^19.1.8",
|
||||||
"@types/react-dom": "^19.1.6",
|
"@types/react-dom": "^19.1.6",
|
||||||
"@vitejs/plugin-react": "^4.6.0",
|
"@vitejs/plugin-react": "^4.6.0",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import { defineConfig } from "@playwright/test";
|
import { defineConfig } from "@playwright/test";
|
||||||
|
|
||||||
const PORT = 1440;
|
const PORT = 1450;
|
||||||
const BASE_URL = `http://localhost:${PORT}`;
|
const BASE_URL = `http://localhost:${PORT}`;
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ importers:
|
|||||||
'@tauri-apps/cli':
|
'@tauri-apps/cli':
|
||||||
specifier: ^2
|
specifier: ^2
|
||||||
version: 2.11.4
|
version: 2.11.4
|
||||||
|
'@types/node':
|
||||||
|
specifier: ^24.0.0
|
||||||
|
version: 24.13.3
|
||||||
'@types/react':
|
'@types/react':
|
||||||
specifier: ^19.1.8
|
specifier: ^19.1.8
|
||||||
version: 19.2.18
|
version: 19.2.18
|
||||||
@@ -62,16 +65,16 @@ importers:
|
|||||||
version: 19.2.7(@types/[email protected])
|
version: 19.2.7(@types/[email protected])
|
||||||
'@vitejs/plugin-react':
|
'@vitejs/plugin-react':
|
||||||
specifier: ^4.6.0
|
specifier: ^4.6.0
|
||||||
version: 4.7.0([email protected])
|
version: 4.7.0([email protected](@types/[email protected]))
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ~5.8.3
|
specifier: ~5.8.3
|
||||||
version: 5.8.3
|
version: 5.8.3
|
||||||
vite:
|
vite:
|
||||||
specifier: ^7.0.4
|
specifier: ^7.0.4
|
||||||
version: 7.3.6
|
version: 7.3.6(@types/[email protected])
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^3.2.4
|
specifier: ^3.2.4
|
||||||
version: 3.2.7
|
version: 3.2.7(@types/[email protected])
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -738,6 +741,9 @@ packages:
|
|||||||
'@types/[email protected]':
|
'@types/[email protected]':
|
||||||
resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
|
resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
|
||||||
|
|
||||||
|
'@types/[email protected]':
|
||||||
|
resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==}
|
||||||
|
|
||||||
'@types/[email protected]':
|
'@types/[email protected]':
|
||||||
resolution: {integrity: sha512-I8bPpDLcHBv1qiIiXDCy71Rt8eQDKJP0sMSWJphDdAcdqiJ1sGpZamavoEIRZmYzjia9LuEb2HlYdDpmoENpvQ==}
|
resolution: {integrity: sha512-I8bPpDLcHBv1qiIiXDCy71Rt8eQDKJP0sMSWJphDdAcdqiJ1sGpZamavoEIRZmYzjia9LuEb2HlYdDpmoENpvQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1072,6 +1078,9 @@ packages:
|
|||||||
engines: {node: '>=14.17'}
|
engines: {node: '>=14.17'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==}
|
resolution: {integrity: sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -1766,6 +1775,10 @@ snapshots:
|
|||||||
|
|
||||||
'@types/[email protected]': {}
|
'@types/[email protected]': {}
|
||||||
|
|
||||||
|
'@types/[email protected]':
|
||||||
|
dependencies:
|
||||||
|
undici-types: 7.18.2
|
||||||
|
|
||||||
'@types/[email protected](@types/[email protected])':
|
'@types/[email protected](@types/[email protected])':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/react': 19.2.18
|
'@types/react': 19.2.18
|
||||||
@@ -1776,7 +1789,7 @@ snapshots:
|
|||||||
|
|
||||||
'@types/[email protected]': {}
|
'@types/[email protected]': {}
|
||||||
|
|
||||||
'@vitejs/[email protected]([email protected])':
|
'@vitejs/[email protected]([email protected](@types/[email protected]))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.7
|
'@babel/core': 7.29.7
|
||||||
'@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/[email protected])
|
'@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/[email protected])
|
||||||
@@ -1784,7 +1797,7 @@ snapshots:
|
|||||||
'@rolldown/pluginutils': 1.0.0-beta.27
|
'@rolldown/pluginutils': 1.0.0-beta.27
|
||||||
'@types/babel__core': 7.20.5
|
'@types/babel__core': 7.20.5
|
||||||
react-refresh: 0.17.0
|
react-refresh: 0.17.0
|
||||||
vite: 7.3.6
|
vite: 7.3.6(@types/[email protected])
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -1796,13 +1809,13 @@ snapshots:
|
|||||||
chai: 5.3.3
|
chai: 5.3.3
|
||||||
tinyrainbow: 2.0.0
|
tinyrainbow: 2.0.0
|
||||||
|
|
||||||
'@vitest/[email protected]([email protected])':
|
'@vitest/[email protected]([email protected](@types/[email protected]))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/spy': 3.2.7
|
'@vitest/spy': 3.2.7
|
||||||
estree-walker: 3.0.3
|
estree-walker: 3.0.3
|
||||||
magic-string: 0.30.21
|
magic-string: 0.30.21
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
vite: 7.3.6
|
vite: 7.3.6(@types/[email protected])
|
||||||
|
|
||||||
'@vitest/[email protected]':
|
'@vitest/[email protected]':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -2128,6 +2141,8 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]([email protected]):
|
[email protected]([email protected]):
|
||||||
dependencies:
|
dependencies:
|
||||||
browserslist: 4.28.8
|
browserslist: 4.28.8
|
||||||
@@ -2138,13 +2153,13 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
react: 19.2.8
|
react: 19.2.8
|
||||||
|
|
||||||
[email protected]:
|
[email protected](@types/[email protected]):
|
||||||
dependencies:
|
dependencies:
|
||||||
cac: 6.7.14
|
cac: 6.7.14
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
es-module-lexer: 1.7.0
|
es-module-lexer: 1.7.0
|
||||||
pathe: 2.0.3
|
pathe: 2.0.3
|
||||||
vite: 7.3.6
|
vite: 7.3.6(@types/[email protected])
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@types/node'
|
- '@types/node'
|
||||||
- jiti
|
- jiti
|
||||||
@@ -2159,7 +2174,7 @@ snapshots:
|
|||||||
- tsx
|
- tsx
|
||||||
- yaml
|
- yaml
|
||||||
|
|
||||||
[email protected]:
|
[email protected](@types/[email protected]):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.28.2
|
esbuild: 0.28.2
|
||||||
fdir: 6.5.0([email protected])
|
fdir: 6.5.0([email protected])
|
||||||
@@ -2168,13 +2183,14 @@ snapshots:
|
|||||||
rollup: 4.63.1
|
rollup: 4.63.1
|
||||||
tinyglobby: 0.2.17
|
tinyglobby: 0.2.17
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
|
'@types/node': 24.13.3
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
|
|
||||||
[email protected]:
|
[email protected](@types/[email protected]):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/chai': 5.2.3
|
'@types/chai': 5.2.3
|
||||||
'@vitest/expect': 3.2.7
|
'@vitest/expect': 3.2.7
|
||||||
'@vitest/mocker': 3.2.7([email protected])
|
'@vitest/mocker': 3.2.7([email protected](@types/[email protected]))
|
||||||
'@vitest/pretty-format': 3.2.7
|
'@vitest/pretty-format': 3.2.7
|
||||||
'@vitest/runner': 3.2.7
|
'@vitest/runner': 3.2.7
|
||||||
'@vitest/snapshot': 3.2.7
|
'@vitest/snapshot': 3.2.7
|
||||||
@@ -2192,9 +2208,11 @@ snapshots:
|
|||||||
tinyglobby: 0.2.17
|
tinyglobby: 0.2.17
|
||||||
tinypool: 1.1.1
|
tinypool: 1.1.1
|
||||||
tinyrainbow: 2.0.0
|
tinyrainbow: 2.0.0
|
||||||
vite: 7.3.6
|
vite: 7.3.6(@types/[email protected])
|
||||||
vite-node: 3.2.4
|
vite-node: 3.2.4(@types/[email protected])
|
||||||
why-is-node-running: 2.3.0
|
why-is-node-running: 2.3.0
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/node': 24.13.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- jiti
|
- jiti
|
||||||
- less
|
- less
|
||||||
|
|||||||
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 410 KiB |
|
After Width: | Height: | Size: 473 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 213 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 239 KiB |
|
After Width: | Height: | Size: 36 KiB |
@@ -0,0 +1,127 @@
|
|||||||
|
/* The stylesheet inside a message's iframe.
|
||||||
|
*
|
||||||
|
* It is served raw out of public/ rather than bundled, because the frame is a document of its own
|
||||||
|
* and cannot see the app's stylesheets. Every value it uses is a custom property that
|
||||||
|
* src/screens/MessageBody.tsx writes onto the frame's root from the app's own computed tokens, so
|
||||||
|
* this file holds the shape of a message body and no colour, no face and no size of its own.
|
||||||
|
*
|
||||||
|
* The two faces are declared again for the same reason: a child document does not inherit the
|
||||||
|
* parent's @font-face rules, and without these a message renders in Georgia. They are the two
|
||||||
|
* default slots; a face chosen in settings needs its declaration here as well. */
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Hanken Grotesk";
|
||||||
|
src: url("/fonts/HankenGrotesk-VF.ttf") format("truetype-variations");
|
||||||
|
font-weight: 100 900;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Literata";
|
||||||
|
src: url("/fonts/Literata-VF.ttf") format("truetype-variations");
|
||||||
|
font-weight: 200 900;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Literata";
|
||||||
|
src: url("/fonts/Literata-Italic-VF.ttf") format("truetype-variations");
|
||||||
|
font-weight: 200 900;
|
||||||
|
font-style: italic;
|
||||||
|
font-display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
/* The parent measures scrollHeight to size the frame, so nothing in here may scroll itself. */
|
||||||
|
overflow: hidden;
|
||||||
|
/* What this document is, in its own words: `only light` for a page the sender painted and ours
|
||||||
|
for everything else. It decides the canvas the browser paints before the body does, the colours
|
||||||
|
it gives anything it draws itself, and what a rule inside the message would be told about
|
||||||
|
`prefers-color-scheme`. Written from MessageBody.tsx the way every other value here is.
|
||||||
|
|
||||||
|
The `only light` half is the one no mail client ships. A body pinned to a white page in a dark
|
||||||
|
window is still a document, and a document with nothing to say about its own scheme gets the
|
||||||
|
machine's, which puts a dark canvas and dark user agent colours under a page we just painted
|
||||||
|
white. */
|
||||||
|
color-scheme: var(--m-color-scheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
max-width: var(--m-measure);
|
||||||
|
/* Painted rather than left transparent, because a message that painted its own page is pinned to
|
||||||
|
the light surface in both palettes and has to bring that surface with it. On the theme the
|
||||||
|
value is the app's own paper and this rule is invisible. */
|
||||||
|
background: var(--m-paper);
|
||||||
|
color: var(--m-ink);
|
||||||
|
font-family: var(--m-font-text);
|
||||||
|
font-size: var(--m-size);
|
||||||
|
/* 1.5 rather than the 1.6 this was drawn at. At 15px that is 22.5px a line against 24px, which
|
||||||
|
is worth more on a long message than every margin in the pane put together, and it is still
|
||||||
|
prose rather than a spreadsheet. Anything tighter than 1.45 and the text face stops being
|
||||||
|
worth having. */
|
||||||
|
line-height: 1.5;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Transactional mail is data rather than prose, so it reads in the interface face. */
|
||||||
|
body[data-plain] {
|
||||||
|
font-family: var(--m-font-ui);
|
||||||
|
font-size: var(--m-size-plain);
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A sender's image is whatever size the sender made it, and the pane is not that wide. */
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
max-width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
padding-left: 14px;
|
||||||
|
border-left: 2px solid var(--m-line);
|
||||||
|
color: var(--m-faint);
|
||||||
|
}
|
||||||
|
|
||||||
|
pre,
|
||||||
|
code {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||||
|
font-size: 0.92em;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: var(--m-wash);
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
height: 1px;
|
||||||
|
margin: 16px 0;
|
||||||
|
border: 0;
|
||||||
|
background: var(--m-line);
|
||||||
|
}
|
||||||
@@ -102,18 +102,68 @@ version = "0.7.8"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
|
checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "asn1-rs"
|
||||||
|
version = "0.7.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8"
|
||||||
|
dependencies = [
|
||||||
|
"asn1-rs-derive",
|
||||||
|
"asn1-rs-impl",
|
||||||
|
"displaydoc",
|
||||||
|
"nom",
|
||||||
|
"num-traits",
|
||||||
|
"rusticata-macros",
|
||||||
|
"thiserror 2.0.20",
|
||||||
|
"time",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "asn1-rs-derive"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
"synstructure",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "asn1-rs-impl"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-broadcast"
|
name = "async-broadcast"
|
||||||
version = "0.7.2"
|
version = "0.7.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
|
checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"event-listener",
|
"event-listener 5.4.2",
|
||||||
"event-listener-strategy",
|
"event-listener-strategy",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-channel"
|
||||||
|
version = "1.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35"
|
||||||
|
dependencies = [
|
||||||
|
"concurrent-queue",
|
||||||
|
"event-listener 2.5.3",
|
||||||
|
"futures-core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-channel"
|
name = "async-channel"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
@@ -152,6 +202,29 @@ dependencies = [
|
|||||||
"slab",
|
"slab",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-imap"
|
||||||
|
version = "0.11.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9a6728e0f7931b36d725ac234fcb02539e9f7888dbeaaa8a18d9ea5792181570"
|
||||||
|
dependencies = [
|
||||||
|
"async-channel 2.5.0",
|
||||||
|
"async-compression",
|
||||||
|
"base64 0.22.1",
|
||||||
|
"bytes",
|
||||||
|
"chrono",
|
||||||
|
"futures",
|
||||||
|
"imap-proto",
|
||||||
|
"log",
|
||||||
|
"nom",
|
||||||
|
"pin-project",
|
||||||
|
"pin-utils",
|
||||||
|
"self_cell",
|
||||||
|
"stop-token",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-io"
|
name = "async-io"
|
||||||
version = "2.6.0"
|
version = "2.6.0"
|
||||||
@@ -176,7 +249,7 @@ version = "3.4.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
|
checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"event-listener",
|
"event-listener 5.4.2",
|
||||||
"event-listener-strategy",
|
"event-listener-strategy",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
@@ -187,14 +260,14 @@ version = "2.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75"
|
checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-channel",
|
"async-channel 2.5.0",
|
||||||
"async-io",
|
"async-io",
|
||||||
"async-lock",
|
"async-lock",
|
||||||
"async-signal",
|
"async-signal",
|
||||||
"async-task",
|
"async-task",
|
||||||
"blocking",
|
"blocking",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"event-listener",
|
"event-listener 5.4.2",
|
||||||
"futures-lite",
|
"futures-lite",
|
||||||
"rustix",
|
"rustix",
|
||||||
]
|
]
|
||||||
@@ -315,6 +388,12 @@ version = "0.22.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64"
|
||||||
|
version = "0.23.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64ct"
|
name = "base64ct"
|
||||||
version = "1.8.3"
|
version = "1.8.3"
|
||||||
@@ -415,7 +494,7 @@ version = "1.7.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa"
|
checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-channel",
|
"async-channel 2.5.0",
|
||||||
"async-task",
|
"async-task",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
"futures-lite",
|
"futures-lite",
|
||||||
@@ -804,6 +883,12 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "critical-section"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-channel"
|
name = "crossbeam-channel"
|
||||||
version = "0.5.16"
|
version = "0.5.16"
|
||||||
@@ -989,6 +1074,12 @@ dependencies = [
|
|||||||
"syn 2.0.119",
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "data-encoding"
|
||||||
|
version = "2.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dbus"
|
name = "dbus"
|
||||||
version = "0.9.12"
|
version = "0.9.12"
|
||||||
@@ -1031,6 +1122,20 @@ dependencies = [
|
|||||||
"thiserror 2.0.20",
|
"thiserror 2.0.20",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "der-parser"
|
||||||
|
version = "10.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6"
|
||||||
|
dependencies = [
|
||||||
|
"asn1-rs",
|
||||||
|
"displaydoc",
|
||||||
|
"nom",
|
||||||
|
"num-bigint",
|
||||||
|
"num-traits",
|
||||||
|
"rusticata-macros",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deranged"
|
name = "deranged"
|
||||||
version = "0.5.8"
|
version = "0.5.8"
|
||||||
@@ -1324,6 +1429,12 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "event-listener"
|
||||||
|
version = "2.5.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "event-listener"
|
name = "event-listener"
|
||||||
version = "5.4.2"
|
version = "5.4.2"
|
||||||
@@ -1340,7 +1451,7 @@ version = "0.5.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
|
checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"event-listener",
|
"event-listener 5.4.2",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1426,7 +1537,7 @@ version = "0.5.8"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646"
|
checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"roxmltree",
|
"roxmltree 0.20.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1484,6 +1595,21 @@ version = "1.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures"
|
||||||
|
version = "0.3.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3"
|
||||||
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
|
"futures-core",
|
||||||
|
"futures-executor",
|
||||||
|
"futures-io",
|
||||||
|
"futures-sink",
|
||||||
|
"futures-task",
|
||||||
|
"futures-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-channel"
|
name = "futures-channel"
|
||||||
version = "0.3.34"
|
version = "0.3.34"
|
||||||
@@ -1559,6 +1685,7 @@ version = "0.3.34"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
|
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
"futures-macro",
|
"futures-macro",
|
||||||
@@ -1980,6 +2107,76 @@ dependencies = [
|
|||||||
"arrayvec",
|
"arrayvec",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hickory-net"
|
||||||
|
version = "0.26.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "084e7bd6a377435d568f652153e571b50970d7ccc1d1eeec0519f834632287e1"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"cfg-if",
|
||||||
|
"data-encoding",
|
||||||
|
"futures-channel",
|
||||||
|
"futures-io",
|
||||||
|
"futures-util",
|
||||||
|
"hickory-proto",
|
||||||
|
"idna",
|
||||||
|
"ipnet",
|
||||||
|
"jni 0.22.4",
|
||||||
|
"rand 0.10.2",
|
||||||
|
"thiserror 2.0.20",
|
||||||
|
"tinyvec",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hickory-proto"
|
||||||
|
version = "0.26.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7e2da0694c15b44c6f68a6b05e0233617008c54080e31d6eb848d858a9c5b38d"
|
||||||
|
dependencies = [
|
||||||
|
"data-encoding",
|
||||||
|
"idna",
|
||||||
|
"ipnet",
|
||||||
|
"jni 0.22.4",
|
||||||
|
"once_cell",
|
||||||
|
"prefix-trie",
|
||||||
|
"rand 0.10.2",
|
||||||
|
"ring",
|
||||||
|
"thiserror 2.0.20",
|
||||||
|
"tinyvec",
|
||||||
|
"tracing",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hickory-resolver"
|
||||||
|
version = "0.26.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0e4f9f4603319422d482738f3f6fe5aac03157fdbfed1cd85a3ff45adb09072f"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"futures-util",
|
||||||
|
"hickory-net",
|
||||||
|
"hickory-proto",
|
||||||
|
"ipconfig",
|
||||||
|
"ipnet",
|
||||||
|
"jni 0.22.4",
|
||||||
|
"moka",
|
||||||
|
"ndk-context",
|
||||||
|
"once_cell",
|
||||||
|
"parking_lot",
|
||||||
|
"rand 0.10.2",
|
||||||
|
"resolv-conf",
|
||||||
|
"smallvec",
|
||||||
|
"system-configuration",
|
||||||
|
"thiserror 2.0.20",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "html5ever"
|
name = "html5ever"
|
||||||
version = "0.38.0"
|
version = "0.38.0"
|
||||||
@@ -2254,6 +2451,15 @@ dependencies = [
|
|||||||
"icu_properties",
|
"icu_properties",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "imap-proto"
|
||||||
|
version = "0.16.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "25f6af35c6a517aea5c72314abe90134980d2ae6a763809b50c208b3e429d71f"
|
||||||
|
dependencies = [
|
||||||
|
"nom",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "1.9.3"
|
version = "1.9.3"
|
||||||
@@ -2295,11 +2501,27 @@ dependencies = [
|
|||||||
"hybrid-array",
|
"hybrid-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ipconfig"
|
||||||
|
version = "0.3.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222"
|
||||||
|
dependencies = [
|
||||||
|
"socket2",
|
||||||
|
"widestring",
|
||||||
|
"windows-registry 0.6.1",
|
||||||
|
"windows-result 0.4.1",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ipnet"
|
name = "ipnet"
|
||||||
version = "2.12.1"
|
version = "2.12.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78"
|
checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "is-docker"
|
name = "is-docker"
|
||||||
@@ -2418,6 +2640,36 @@ dependencies = [
|
|||||||
"windows-sys 0.45.0",
|
"windows-sys 0.45.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jni"
|
||||||
|
version = "0.22.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"combine",
|
||||||
|
"jni-macros",
|
||||||
|
"jni-sys 0.4.1",
|
||||||
|
"log",
|
||||||
|
"simd_cesu8",
|
||||||
|
"thiserror 2.0.20",
|
||||||
|
"walkdir",
|
||||||
|
"windows-link 0.2.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jni-macros"
|
||||||
|
version = "0.22.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"rustc_version",
|
||||||
|
"simd_cesu8",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jni-sys"
|
name = "jni-sys"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
@@ -2500,6 +2752,12 @@ dependencies = [
|
|||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lazy_static"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libappindicator"
|
name = "libappindicator"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
@@ -2644,6 +2902,24 @@ dependencies = [
|
|||||||
"hashify",
|
"hashify",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mail-send"
|
||||||
|
version = "0.6.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8355326d14f08c9de5e9badfe606d9ba8fea2febb60fe4318ecbac33cfb7b41e"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.23.1",
|
||||||
|
"gethostname",
|
||||||
|
"md5",
|
||||||
|
"rand 0.9.5",
|
||||||
|
"rustls",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"rustls-platform-verifier 0.7.0",
|
||||||
|
"smtp-proto",
|
||||||
|
"tokio",
|
||||||
|
"tokio-rustls",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "maplit"
|
name = "maplit"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
@@ -2656,6 +2932,7 @@ version = "0.0.1"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"ammonia",
|
"ammonia",
|
||||||
"argon2",
|
"argon2",
|
||||||
|
"async-imap",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"bip39",
|
"bip39",
|
||||||
"block2",
|
"block2",
|
||||||
@@ -2663,14 +2940,21 @@ dependencies = [
|
|||||||
"chrono",
|
"chrono",
|
||||||
"css-inline",
|
"css-inline",
|
||||||
"fontdb",
|
"fontdb",
|
||||||
|
"futures",
|
||||||
|
"hickory-resolver",
|
||||||
"mail-builder",
|
"mail-builder",
|
||||||
"mail-parser",
|
"mail-parser",
|
||||||
|
"mail-send",
|
||||||
"objc2",
|
"objc2",
|
||||||
"objc2-foundation",
|
"objc2-foundation",
|
||||||
"objc2-ui-kit",
|
"objc2-ui-kit",
|
||||||
|
"objc2-user-notifications",
|
||||||
"rand 0.8.8",
|
"rand 0.8.8",
|
||||||
"reqwest 0.13.1",
|
"reqwest 0.13.1",
|
||||||
|
"roxmltree 0.21.1",
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
|
"rustls",
|
||||||
|
"rustls-pki-types",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2",
|
||||||
@@ -2684,7 +2968,11 @@ dependencies = [
|
|||||||
"tauri-plugin-updater",
|
"tauri-plugin-updater",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-rustls",
|
||||||
|
"tokio-util",
|
||||||
"url",
|
"url",
|
||||||
|
"webpki-roots",
|
||||||
|
"x509-parser",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2709,6 +2997,12 @@ dependencies = [
|
|||||||
"web_atoms",
|
"web_atoms",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "md5"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7ebb8d8732c6a6df3d8f032a82911cfc747e00efb95cc46e8d0acd5b5b88570c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.8.3"
|
version = "2.8.3"
|
||||||
@@ -2739,6 +3033,12 @@ version = "0.3.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "minimal-lexical"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "minisign-verify"
|
name = "minisign-verify"
|
||||||
version = "0.2.5"
|
version = "0.2.5"
|
||||||
@@ -2776,6 +3076,23 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "moka"
|
||||||
|
version = "0.12.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4293f18e7567a1caf3c584855554377025c65e0aa445344d04171f5ad63d19b9"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-channel",
|
||||||
|
"crossbeam-epoch",
|
||||||
|
"crossbeam-utils",
|
||||||
|
"equivalent",
|
||||||
|
"parking_lot",
|
||||||
|
"portable-atomic",
|
||||||
|
"smallvec",
|
||||||
|
"tagptr",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "muda"
|
name = "muda"
|
||||||
version = "0.19.3"
|
version = "0.19.3"
|
||||||
@@ -2812,6 +3129,12 @@ dependencies = [
|
|||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ndk-context"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ndk-sys"
|
name = "ndk-sys"
|
||||||
version = "0.6.0+11769913"
|
version = "0.6.0+11769913"
|
||||||
@@ -2839,6 +3162,16 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nom"
|
||||||
|
version = "7.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
"minimal-lexical",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "notify-rust"
|
name = "notify-rust"
|
||||||
version = "4.18.0"
|
version = "4.18.0"
|
||||||
@@ -2853,12 +3186,31 @@ dependencies = [
|
|||||||
"zbus",
|
"zbus",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-bigint"
|
||||||
|
version = "0.4.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367"
|
||||||
|
dependencies = [
|
||||||
|
"num-integer",
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-conv"
|
name = "num-conv"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
|
checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-integer"
|
||||||
|
version = "0.1.47"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b"
|
||||||
|
dependencies = [
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
version = "0.2.19"
|
version = "0.2.19"
|
||||||
@@ -3080,6 +3432,8 @@ version = "0.3.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e"
|
checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bitflags 2.13.1",
|
||||||
|
"block2",
|
||||||
"objc2",
|
"objc2",
|
||||||
"objc2-foundation",
|
"objc2-foundation",
|
||||||
]
|
]
|
||||||
@@ -3098,11 +3452,24 @@ dependencies = [
|
|||||||
"objc2-foundation",
|
"objc2-foundation",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "oid-registry"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7"
|
||||||
|
dependencies = [
|
||||||
|
"asn1-rs",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.4"
|
version = "1.21.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
dependencies = [
|
||||||
|
"critical-section",
|
||||||
|
"portable-atomic",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "open"
|
name = "open"
|
||||||
@@ -3311,12 +3678,38 @@ dependencies = [
|
|||||||
"siphasher",
|
"siphasher",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-project"
|
||||||
|
version = "1.1.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924"
|
||||||
|
dependencies = [
|
||||||
|
"pin-project-internal",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-project-internal"
|
||||||
|
version = "1.1.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-project-lite"
|
name = "pin-project-lite"
|
||||||
version = "0.2.17"
|
version = "0.2.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-utils"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "piper"
|
name = "piper"
|
||||||
version = "0.2.5"
|
version = "0.2.5"
|
||||||
@@ -3442,6 +3835,17 @@ version = "0.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
|
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "prefix-trie"
|
||||||
|
version = "0.8.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4cf6e3177f0684016a5c209b00882e15f8bdd3f3bb48f0491df10cd102d0c6e7"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
"ipnet",
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro-crate"
|
name = "proc-macro-crate"
|
||||||
version = "1.3.1"
|
version = "1.3.1"
|
||||||
@@ -3835,7 +4239,7 @@ dependencies = [
|
|||||||
"quinn",
|
"quinn",
|
||||||
"rustls",
|
"rustls",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"rustls-platform-verifier",
|
"rustls-platform-verifier 0.6.2",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sync_wrapper",
|
"sync_wrapper",
|
||||||
@@ -3853,6 +4257,12 @@ dependencies = [
|
|||||||
"webpki-roots",
|
"webpki-roots",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "resolv-conf"
|
||||||
|
version = "0.7.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ring"
|
name = "ring"
|
||||||
version = "0.17.14"
|
version = "0.17.14"
|
||||||
@@ -3873,6 +4283,15 @@ version = "0.20.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
|
checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "roxmltree"
|
||||||
|
version = "0.21.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f1964b10c76125c36f8afe190065a4bf9a87bf324842c05701330bba9f1cacbb"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rsqlite-vfs"
|
name = "rsqlite-vfs"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@@ -3923,6 +4342,15 @@ dependencies = [
|
|||||||
"semver",
|
"semver",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rusticata-macros"
|
||||||
|
version = "4.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632"
|
||||||
|
dependencies = [
|
||||||
|
"nom",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "1.1.4"
|
version = "1.1.4"
|
||||||
@@ -3943,6 +4371,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06"
|
checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-lc-rs",
|
"aws-lc-rs",
|
||||||
|
"log",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"ring",
|
"ring",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
@@ -3981,7 +4410,28 @@ checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"core-foundation 0.10.1",
|
"core-foundation 0.10.1",
|
||||||
"core-foundation-sys",
|
"core-foundation-sys",
|
||||||
"jni",
|
"jni 0.21.1",
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
|
"rustls",
|
||||||
|
"rustls-native-certs",
|
||||||
|
"rustls-platform-verifier-android",
|
||||||
|
"rustls-webpki",
|
||||||
|
"security-framework",
|
||||||
|
"security-framework-sys",
|
||||||
|
"webpki-root-certs",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-platform-verifier"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0"
|
||||||
|
dependencies = [
|
||||||
|
"core-foundation 0.10.1",
|
||||||
|
"core-foundation-sys",
|
||||||
|
"jni 0.22.4",
|
||||||
"log",
|
"log",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustls",
|
"rustls",
|
||||||
@@ -4160,6 +4610,12 @@ dependencies = [
|
|||||||
"smallvec",
|
"smallvec",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "self_cell"
|
||||||
|
version = "1.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2ab42ca02749e120097e328d91d415325bdf43b1c72c4c8badf37375fe40a813"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "semver"
|
name = "semver"
|
||||||
version = "1.0.28"
|
version = "1.0.28"
|
||||||
@@ -4374,6 +4830,22 @@ version = "0.3.10"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
|
checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simd_cesu8"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520"
|
||||||
|
dependencies = [
|
||||||
|
"rustc_version",
|
||||||
|
"simdutf8",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simdutf8"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "siphasher"
|
name = "siphasher"
|
||||||
version = "1.0.3"
|
version = "1.0.3"
|
||||||
@@ -4401,6 +4873,12 @@ version = "1.16.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f"
|
checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smtp-proto"
|
||||||
|
version = "0.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e365d0fff6ae5036364ab62f236fa669ce05acbd5878df32d3755d48cfc1491b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socket2"
|
name = "socket2"
|
||||||
version = "0.6.5"
|
version = "0.6.5"
|
||||||
@@ -4477,6 +4955,18 @@ version = "1.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "stop-token"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "af91f480ee899ab2d9f8435bfdfc14d08a5754bd9d3fef1f1a1c23336aad6c8b"
|
||||||
|
dependencies = [
|
||||||
|
"async-channel 1.9.0",
|
||||||
|
"cfg-if",
|
||||||
|
"futures-core",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "string_cache"
|
name = "string_cache"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
@@ -4619,6 +5109,12 @@ dependencies = [
|
|||||||
"version-compare",
|
"version-compare",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tagptr"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tao"
|
name = "tao"
|
||||||
version = "0.35.3"
|
version = "0.35.3"
|
||||||
@@ -4637,7 +5133,7 @@ dependencies = [
|
|||||||
"gdkwayland-sys",
|
"gdkwayland-sys",
|
||||||
"gdkx11-sys",
|
"gdkx11-sys",
|
||||||
"gtk",
|
"gtk",
|
||||||
"jni",
|
"jni 0.21.1",
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"ndk",
|
"ndk",
|
||||||
@@ -4704,7 +5200,7 @@ dependencies = [
|
|||||||
"gtk",
|
"gtk",
|
||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"http",
|
"http",
|
||||||
"jni",
|
"jni 0.21.1",
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"mime",
|
"mime",
|
||||||
@@ -4949,7 +5445,7 @@ dependencies = [
|
|||||||
"dpi",
|
"dpi",
|
||||||
"gtk",
|
"gtk",
|
||||||
"http",
|
"http",
|
||||||
"jni",
|
"jni 0.21.1",
|
||||||
"objc2",
|
"objc2",
|
||||||
"objc2-ui-kit",
|
"objc2-ui-kit",
|
||||||
"objc2-web-kit",
|
"objc2-web-kit",
|
||||||
@@ -4972,7 +5468,7 @@ checksum = "4e6fac707727b7a2f48e4ded90976324267371073edbb415ffb73bb0458d203f"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"gtk",
|
"gtk",
|
||||||
"http",
|
"http",
|
||||||
"jni",
|
"jni 0.21.1",
|
||||||
"log",
|
"log",
|
||||||
"objc2",
|
"objc2",
|
||||||
"objc2-app-kit",
|
"objc2-app-kit",
|
||||||
@@ -5187,9 +5683,21 @@ dependencies = [
|
|||||||
"mio",
|
"mio",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"socket2",
|
"socket2",
|
||||||
|
"tokio-macros",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-macros"
|
||||||
|
version = "2.7.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 3.0.4",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-rustls"
|
name = "tokio-rustls"
|
||||||
version = "0.26.4"
|
version = "0.26.4"
|
||||||
@@ -5208,6 +5716,7 @@ checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
"futures-io",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
"libc",
|
"libc",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
@@ -5857,6 +6366,12 @@ dependencies = [
|
|||||||
"windows-core 0.61.2",
|
"windows-core 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "widestring"
|
||||||
|
version = "1.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi"
|
name = "winapi"
|
||||||
version = "0.3.9"
|
version = "0.3.9"
|
||||||
@@ -6377,7 +6892,7 @@ dependencies = [
|
|||||||
"gtk",
|
"gtk",
|
||||||
"http",
|
"http",
|
||||||
"javascriptcore-rs",
|
"javascriptcore-rs",
|
||||||
"jni",
|
"jni 0.21.1",
|
||||||
"libc",
|
"libc",
|
||||||
"ndk",
|
"ndk",
|
||||||
"objc2",
|
"objc2",
|
||||||
@@ -6424,6 +6939,23 @@ dependencies = [
|
|||||||
"pkg-config",
|
"pkg-config",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "x509-parser"
|
||||||
|
version = "0.18.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202"
|
||||||
|
dependencies = [
|
||||||
|
"asn1-rs",
|
||||||
|
"data-encoding",
|
||||||
|
"der-parser",
|
||||||
|
"lazy_static",
|
||||||
|
"nom",
|
||||||
|
"oid-registry",
|
||||||
|
"rusticata-macros",
|
||||||
|
"thiserror 2.0.20",
|
||||||
|
"time",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "xattr"
|
name = "xattr"
|
||||||
version = "1.6.1"
|
version = "1.6.1"
|
||||||
@@ -6473,7 +7005,7 @@ dependencies = [
|
|||||||
"async-trait",
|
"async-trait",
|
||||||
"blocking",
|
"blocking",
|
||||||
"enumflags2",
|
"enumflags2",
|
||||||
"event-listener",
|
"event-listener 5.4.2",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-lite",
|
"futures-lite",
|
||||||
"hex",
|
"hex",
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ sha2 = "0.10"
|
|||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
url = "2"
|
url = "2"
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
tokio = { version = "1", features = ["sync", "time"] }
|
tokio = { version = "1", features = ["sync", "time", "net", "io-util", "rt"] }
|
||||||
|
|
||||||
# gzip because Gmail's JSON compresses by an order of magnitude and hydration is thousands of
|
# gzip because Gmail's JSON compresses by an order of magnitude and hydration is thousands of
|
||||||
# responses; http2 because a batch of 50 and the poll loop share one connection.
|
# responses; http2 because a batch of 50 and the poll loop share one connection.
|
||||||
@@ -69,6 +69,68 @@ bip39 = { version = "2.2", features = ["rand"] }
|
|||||||
# The system families the Appearance section offers alongside the six bundled ones.
|
# The system families the Appearance section offers alongside the six bundled ones.
|
||||||
fontdb = "0.24"
|
fontdb = "0.24"
|
||||||
|
|
||||||
|
# The second provider: a mailbox reached over IMAP and SMTP with a password, which is every
|
||||||
|
# account that is not Google. Proton is one of these, through Bridge on the loopback.
|
||||||
|
#
|
||||||
|
# `async-imap` defaults to async-std, so both its default features are off and the tokio pair is
|
||||||
|
# named instead: there is one runtime in this process and it is Tauri's.
|
||||||
|
async-imap = { version = "0.11", default-features = false, features = ["runtime-tokio", "tokio"] }
|
||||||
|
# `async-imap` speaks futures-io and `tokio-rustls` speaks tokio-io, so the stream is bridged.
|
||||||
|
tokio-util = { version = "0.7", features = ["compat"] }
|
||||||
|
# `async-imap` returns its responses as futures streams, so the combinators come with it.
|
||||||
|
futures = "0.3"
|
||||||
|
|
||||||
|
# TLS for both, and the seam where a self-signed certificate is decided about.
|
||||||
|
#
|
||||||
|
# Every `ClientConfig` in this crate must name `rustls::crypto::ring::default_provider()`
|
||||||
|
# explicitly. Both crypto providers are in the tree because of what reqwest pulls, so rustls has
|
||||||
|
# no process default to fall back on and building a config without one panics at runtime rather
|
||||||
|
# than failing to compile.
|
||||||
|
tokio-rustls = { version = "0.26", default-features = false, features = ["ring", "tls12", "logging"] }
|
||||||
|
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12", "logging"] }
|
||||||
|
rustls-pki-types = "1"
|
||||||
|
webpki-roots = "1"
|
||||||
|
# The fingerprint shown in the certificate question, and the hostname check that decides whether
|
||||||
|
# there is a question at all rather than a refusal.
|
||||||
|
x509-parser = "0.18"
|
||||||
|
|
||||||
|
# Sending. Same author as mail-parser and mail-builder, and pinned to ring for the same reason as
|
||||||
|
# above. No `dkim`: nothing here signs outgoing mail.
|
||||||
|
mail-send = { version = "0.6", default-features = false, features = ["ring", "tls12", "cram-md5", "digest-md5", "md5", "rand"] }
|
||||||
|
|
||||||
|
# Autodiscovery. `hickory-resolver` is for the MX rung, which is what recognises a vanity domain
|
||||||
|
# sitting on Google Workspace or Fastmail; `roxmltree` reads the clientConfig documents, which are
|
||||||
|
# small, read-once and never written.
|
||||||
|
hickory-resolver = { version = "0.26", default-features = false, features = ["system-config", "tokio"] }
|
||||||
|
roxmltree = "0.21"
|
||||||
|
|
||||||
|
# Notifications on macOS are posted through UserNotifications directly (notify/macos.rs says why the
|
||||||
|
# plugin's own path shows nothing on macOS 26). These are the versions wry already resolves, so the
|
||||||
|
# build keeps a single copy of objc2.
|
||||||
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
|
objc2 = "0.6"
|
||||||
|
objc2-foundation = { version = "0.3", default-features = false, features = [
|
||||||
|
"std",
|
||||||
|
"NSDictionary",
|
||||||
|
"NSError",
|
||||||
|
|
||||||
|
"NSObject",
|
||||||
|
"NSString",
|
||||||
|
] }
|
||||||
|
objc2-user-notifications = { version = "0.3", default-features = false, features = [
|
||||||
|
"std",
|
||||||
|
"block2",
|
||||||
|
"UNUserNotificationCenter",
|
||||||
|
"UNNotification",
|
||||||
|
"UNNotificationContent",
|
||||||
|
"UNNotificationRequest",
|
||||||
|
"UNNotificationResponse",
|
||||||
|
"UNNotificationSettings",
|
||||||
|
"UNNotificationSound",
|
||||||
|
"UNNotificationTrigger",
|
||||||
|
] }
|
||||||
|
block2 = "0.6"
|
||||||
|
|
||||||
# There is no auto-updater and no process to restart on a phone: the store is the update channel.
|
# There is no auto-updater and no process to restart on a phone: the store is the update channel.
|
||||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||||
tauri-plugin-process = "2"
|
tauri-plugin-process = "2"
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
Return-Path: <[email protected]>
|
||||||
|
MIME-Version: 1.0
|
||||||
|
Date: Mon, 31 Aug 2026 18:41:07 +0000
|
||||||
|
Message-ID: <[email protected]>
|
||||||
|
Subject: Completed: Studio lease 2026
|
||||||
|
From: DocuSign NA3 System <[email protected]>
|
||||||
|
To: Priyanshu Jain <[email protected]>
|
||||||
|
X-Mailer: Microsoft Outlook 16.0
|
||||||
|
Content-Type: multipart/mixed; boundary="_004_DM6PR11MB4491_"
|
||||||
|
|
||||||
|
--_004_DM6PR11MB4491_
|
||||||
|
Content-Type: text/plain; charset="us-ascii"
|
||||||
|
Content-Transfer-Encoding: quoted-printable
|
||||||
|
|
||||||
|
All parties have completed the envelope. You can access the signed copy
|
||||||
|
from the link below or from the attached PDF.
|
||||||
|
|
||||||
|
--_004_DM6PR11MB4491_
|
||||||
|
Content-Type: application/pdf; name="=?utf-8?Q?Studio_lease_2026_=28sign=C3=A9=29.pdf?="
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
Content-Disposition: attachment; filename="=?utf-8?Q?Studio_lease_2026_=28sign=C3=A9=29.pdf?="
|
||||||
|
|
||||||
|
JVBERi0xLjQKMSAwIG9iajw8L1R5cGUvQ2F0YWxvZy9QYWdlcyAyIDAgUj4+ZW5kb2JqCjIgMCBv
|
||||||
|
Ymo8PC9UeXBlL1BhZ2VzL0tpZHNbMyAwIFJdL0NvdW50IDE+PmVuZG9iagozIDAgb2JqPDwvVHlw
|
||||||
|
ZS9QYWdlL1BhcmVudCAyIDAgUi9NZWRpYUJveFswIDAgNTk1IDg0Ml0+PmVuZG9iagp0cmFpbGVy
|
||||||
|
PDwvUm9vdCAxIDAgUj4+CiUlRU9GCg==
|
||||||
|
|
||||||
|
--_004_DM6PR11MB4491_--
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<clientConfig version="1.1">
|
||||||
|
<emailProvider id="MessagingEngine">
|
||||||
|
<domain>fastmail.com</domain>
|
||||||
|
<displayName>Fastmail</displayName>
|
||||||
|
<displayShortName>Fastmail</displayShortName>
|
||||||
|
<incomingServer type="imap">
|
||||||
|
<hostname>imap.fastmail.com</hostname>
|
||||||
|
<port>993</port>
|
||||||
|
<socketType>SSL</socketType>
|
||||||
|
<authentication>OAuth2</authentication>
|
||||||
|
<username>%EMAILADDRESS%</username>
|
||||||
|
</incomingServer>
|
||||||
|
<incomingServer type="pop3">
|
||||||
|
<hostname>pop.fastmail.com</hostname>
|
||||||
|
<port>995</port>
|
||||||
|
<socketType>SSL</socketType>
|
||||||
|
<authentication>OAuth2</authentication>
|
||||||
|
<username>%EMAILADDRESS%</username>
|
||||||
|
</incomingServer>
|
||||||
|
<outgoingServer type="smtp">
|
||||||
|
<hostname>smtp.fastmail.com</hostname>
|
||||||
|
<port>465</port>
|
||||||
|
<socketType>SSL</socketType>
|
||||||
|
<authentication>OAuth2</authentication>
|
||||||
|
<username>%EMAILADDRESS%</username>
|
||||||
|
</outgoingServer>
|
||||||
|
<instruction url="https://www.fastmail.help/hc/en-us/articles/1500000278342">
|
||||||
|
<descr lang="en">Server Names and Ports</descr>
|
||||||
|
</instruction>
|
||||||
|
<instruction url="https://www.fastmail.help/hc/en-us/articles/360058753054">
|
||||||
|
<descr lang="en">Using Fastmail with Mozilla Thunderbird</descr>
|
||||||
|
</instruction>
|
||||||
|
|
||||||
|
</emailProvider>
|
||||||
|
<webMail>
|
||||||
|
<loginPage url="https://app.fastmail.com/login/?domain=%EMAILDOMAIN%" />
|
||||||
|
</webMail>
|
||||||
|
</clientConfig>
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
<clientConfig version="1.1">
|
||||||
|
<emailProvider id="googlemail.com">
|
||||||
|
<domain>gmail.com</domain>
|
||||||
|
<domain>googlemail.com</domain>
|
||||||
|
<!-- MX, for Google Apps -->
|
||||||
|
<domain>google.com</domain>
|
||||||
|
<domain>jazztel.es</domain>
|
||||||
|
<domain>nyu.edu</domain>
|
||||||
|
|
||||||
|
<displayName>Google Mail</displayName>
|
||||||
|
<displayShortName>GMail</displayShortName>
|
||||||
|
|
||||||
|
<incomingServer type="imap">
|
||||||
|
<hostname>imap.gmail.com</hostname>
|
||||||
|
<port>993</port>
|
||||||
|
<socketType>SSL</socketType>
|
||||||
|
<username>%EMAILADDRESS%</username>
|
||||||
|
<authentication>OAuth2</authentication>
|
||||||
|
<authentication>password-cleartext</authentication>
|
||||||
|
</incomingServer>
|
||||||
|
<incomingServer type="pop3">
|
||||||
|
<hostname>pop.gmail.com</hostname>
|
||||||
|
<port>995</port>
|
||||||
|
<socketType>SSL</socketType>
|
||||||
|
<username>%EMAILADDRESS%</username>
|
||||||
|
<authentication>OAuth2</authentication>
|
||||||
|
<authentication>password-cleartext</authentication>
|
||||||
|
<pop3>
|
||||||
|
<leaveMessagesOnServer>true</leaveMessagesOnServer>
|
||||||
|
</pop3>
|
||||||
|
</incomingServer>
|
||||||
|
<outgoingServer type="smtp">
|
||||||
|
<hostname>smtp.gmail.com</hostname>
|
||||||
|
<port>465</port>
|
||||||
|
<socketType>SSL</socketType>
|
||||||
|
<username>%EMAILADDRESS%</username>
|
||||||
|
<authentication>OAuth2</authentication>
|
||||||
|
<authentication>password-cleartext</authentication>
|
||||||
|
</outgoingServer>
|
||||||
|
|
||||||
|
<documentation url="http://mail.google.com/support/bin/answer.py?answer=13273">
|
||||||
|
<descr>How to enable IMAP/POP3 in GMail</descr>
|
||||||
|
</documentation>
|
||||||
|
<documentation url="http://mail.google.com/support/bin/topic.py?topic=12806">
|
||||||
|
<descr>How to configure email clients for IMAP</descr>
|
||||||
|
</documentation>
|
||||||
|
<documentation url="http://mail.google.com/support/bin/topic.py?topic=12805">
|
||||||
|
<descr>How to configure email clients for POP3</descr>
|
||||||
|
</documentation>
|
||||||
|
<documentation url="http://mail.google.com/support/bin/answer.py?answer=86399">
|
||||||
|
<descr>How to configure TB 2.0 for POP3</descr>
|
||||||
|
</documentation>
|
||||||
|
</emailProvider>
|
||||||
|
|
||||||
|
<oAuth2>
|
||||||
|
<issuer>accounts.google.com</issuer>
|
||||||
|
<!-- https://developers.google.com/identity/protocols/oauth2/scopes -->
|
||||||
|
<scope>https://mail.google.com/ https://www.googleapis.com/auth/contacts https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/carddav</scope>
|
||||||
|
<authURL>https://accounts.google.com/o/oauth2/auth</authURL>
|
||||||
|
<tokenURL>https://www.googleapis.com/oauth2/v3/token</tokenURL>
|
||||||
|
</oAuth2>
|
||||||
|
|
||||||
|
<enable visiturl="https://mail.google.com/mail/?ui=2&shva=1#settings/fwdandpop">
|
||||||
|
<instruction>You need to enable IMAP access</instruction>
|
||||||
|
</enable>
|
||||||
|
|
||||||
|
<webMail>
|
||||||
|
<loginPage url="https://accounts.google.com/ServiceLogin?service=mail&continue=http://mail.google.com/mail/"/>
|
||||||
|
<loginPageInfo url="https://accounts.google.com/ServiceLogin?service=mail&continue=http://mail.google.com/mail/">
|
||||||
|
<username>%EMAILADDRESS%</username>
|
||||||
|
<usernameField id="Email"/>
|
||||||
|
<passwordField id="Passwd"/>
|
||||||
|
<loginButton id="signIn"/>
|
||||||
|
</loginPageInfo>
|
||||||
|
</webMail>
|
||||||
|
|
||||||
|
</clientConfig>
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
<clientConfig version="1.1">
|
||||||
|
<emailProvider id="posteo.de">
|
||||||
|
<domain>posteo.de</domain>
|
||||||
|
<domain>posteo.at</domain>
|
||||||
|
<domain>posteo.be</domain>
|
||||||
|
<domain>posteo.ca</domain>
|
||||||
|
<domain>posteo.ch</domain>
|
||||||
|
<domain>posteo.cl</domain>
|
||||||
|
<domain>posteo.co</domain>
|
||||||
|
<domain>posteo.co.uk</domain>
|
||||||
|
<domain>posteo.com</domain>
|
||||||
|
<domain>posteo.com.br</domain>
|
||||||
|
<domain>posteo.cr</domain>
|
||||||
|
<domain>posteo.cz</domain>
|
||||||
|
<domain>posteo.dk</domain>
|
||||||
|
<domain>posteo.ee</domain>
|
||||||
|
<domain>posteo.es</domain>
|
||||||
|
<domain>posteo.eu</domain>
|
||||||
|
<domain>posteo.fi</domain>
|
||||||
|
<domain>posteo.gl</domain>
|
||||||
|
<domain>posteo.gr</domain>
|
||||||
|
<domain>posteo.hn</domain>
|
||||||
|
<domain>posteo.hr</domain>
|
||||||
|
<domain>posteo.hu</domain>
|
||||||
|
<domain>posteo.ie</domain>
|
||||||
|
<domain>posteo.in</domain>
|
||||||
|
<domain>posteo.is</domain>
|
||||||
|
<domain>posteo.it</domain>
|
||||||
|
<domain>posteo.jp</domain>
|
||||||
|
<domain>posteo.la</domain>
|
||||||
|
<domain>posteo.li</domain>
|
||||||
|
<domain>posteo.lt</domain>
|
||||||
|
<domain>posteo.lu</domain>
|
||||||
|
<domain>posteo.me</domain>
|
||||||
|
<domain>posteo.mx</domain>
|
||||||
|
<domain>posteo.my</domain>
|
||||||
|
<domain>posteo.net</domain>
|
||||||
|
<domain>posteo.nl</domain>
|
||||||
|
<domain>posteo.no</domain>
|
||||||
|
<domain>posteo.nz</domain>
|
||||||
|
<domain>posteo.org</domain>
|
||||||
|
<domain>posteo.pe</domain>
|
||||||
|
<domain>posteo.pl</domain>
|
||||||
|
<domain>posteo.pm</domain>
|
||||||
|
<domain>posteo.pt</domain>
|
||||||
|
<domain>posteo.ro</domain>
|
||||||
|
<domain>posteo.se</domain>
|
||||||
|
<domain>posteo.sg</domain>
|
||||||
|
<domain>posteo.si</domain>
|
||||||
|
<domain>posteo.tn</domain>
|
||||||
|
<domain>posteo.uk</domain>
|
||||||
|
<domain>posteo.us</domain>
|
||||||
|
<displayName>Posteo</displayName>
|
||||||
|
<displayShortName>Posteo</displayShortName>
|
||||||
|
<incomingServer type="imap">
|
||||||
|
<hostname>posteo.de</hostname>
|
||||||
|
<port>993</port>
|
||||||
|
<socketType>SSL</socketType>
|
||||||
|
<username>%EMAILADDRESS%</username>
|
||||||
|
<authentication>password-cleartext</authentication>
|
||||||
|
</incomingServer>
|
||||||
|
<incomingServer type="imap">
|
||||||
|
<hostname>posteo.de</hostname>
|
||||||
|
<port>143</port>
|
||||||
|
<socketType>STARTTLS</socketType>
|
||||||
|
<username>%EMAILADDRESS%</username>
|
||||||
|
<authentication>password-cleartext</authentication>
|
||||||
|
</incomingServer>
|
||||||
|
<incomingServer type="pop3">
|
||||||
|
<hostname>posteo.de</hostname>
|
||||||
|
<port>995</port>
|
||||||
|
<socketType>SSL</socketType>
|
||||||
|
<username>%EMAILADDRESS%</username>
|
||||||
|
<authentication>password-cleartext</authentication>
|
||||||
|
</incomingServer>
|
||||||
|
<incomingServer type="pop3">
|
||||||
|
<hostname>posteo.de</hostname>
|
||||||
|
<port>110</port>
|
||||||
|
<socketType>STARTTLS</socketType>
|
||||||
|
<username>%EMAILADDRESS%</username>
|
||||||
|
<authentication>password-cleartext</authentication>
|
||||||
|
</incomingServer>
|
||||||
|
<outgoingServer type="smtp">
|
||||||
|
<hostname>posteo.de</hostname>
|
||||||
|
<port>465</port>
|
||||||
|
<socketType>SSL</socketType>
|
||||||
|
<username>%EMAILADDRESS%</username>
|
||||||
|
<authentication>password-cleartext</authentication>
|
||||||
|
</outgoingServer>
|
||||||
|
<outgoingServer type="smtp">
|
||||||
|
<hostname>posteo.de</hostname>
|
||||||
|
<port>587</port>
|
||||||
|
<socketType>STARTTLS</socketType>
|
||||||
|
<username>%EMAILADDRESS%</username>
|
||||||
|
<authentication>password-cleartext</authentication>
|
||||||
|
</outgoingServer>
|
||||||
|
<documentation url="https://posteo.de/hilfe/wie-richte-ich-posteo-in-einem-mailprogramm-ein-pop3-imap-und-smtp"/>
|
||||||
|
</emailProvider>
|
||||||
|
</clientConfig>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<clientConfig version="1.1">
|
||||||
|
<emailProvider id="protonmail.com">
|
||||||
|
<domain>protonmail.com</domain>
|
||||||
|
<displayName>ProtonMail</displayName>
|
||||||
|
<displayShortName>ProtonMail</displayShortName>
|
||||||
|
<incomingServer type="imap">
|
||||||
|
<hostname>127.0.0.1</hostname>
|
||||||
|
<port>1143</port>
|
||||||
|
<socketType>STARTTLS</socketType>
|
||||||
|
<authentication>password-cleartext</authentication>
|
||||||
|
<username>%EMAILADDRESS%</username>
|
||||||
|
</incomingServer>
|
||||||
|
<outgoingServer type="smtp">
|
||||||
|
<hostname>127.0.0.1</hostname>
|
||||||
|
<port>1025</port>
|
||||||
|
<socketType>STARTTLS</socketType>
|
||||||
|
<authentication>password-cleartext</authentication>
|
||||||
|
<username>%EMAILADDRESS%</username>
|
||||||
|
</outgoingServer>
|
||||||
|
<documentation url="https://protonmail.com/support">
|
||||||
|
<descr lang="en">Support page</descr>
|
||||||
|
</documentation>
|
||||||
|
</emailProvider>
|
||||||
|
</clientConfig>
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
Return-Path: <[email protected]>
|
||||||
|
MIME-Version: 1.0
|
||||||
|
Date: Wed, 2 Sep 2026 15:40:11 +0530
|
||||||
|
Message-ID: <[email protected]>
|
||||||
|
Subject: Invitation: Piano lesson: Cooper @ Wed 9 Sep 2026 17:00 - 17:45 (IST)
|
||||||
|
([email protected])
|
||||||
|
From: Sam Okafor <[email protected]>
|
||||||
|
To: Priyanshu Jain <[email protected]>
|
||||||
|
Content-Type: multipart/mixed; boundary="mixed-piano-0001"
|
||||||
|
|
||||||
|
--mixed-piano-0001
|
||||||
|
Content-Type: multipart/alternative; boundary="alt-piano-0001"
|
||||||
|
|
||||||
|
--alt-piano-0001
|
||||||
|
Content-Type: text/plain; charset=UTF-8; format=flowed; delsp=yes
|
||||||
|
Content-Transfer-Encoding: 7bit
|
||||||
|
|
||||||
|
You have been invited to the following event.
|
||||||
|
|
||||||
|
Piano lesson: Cooper
|
||||||
|
When: Wed 9 Sep 2026 17:00 to 17:45 India Standard Time
|
||||||
|
Where: Sunny Day Music, Bandra
|
||||||
|
Who: Sam Okafor, Priyanshu Jain
|
||||||
|
|
||||||
|
--alt-piano-0001
|
||||||
|
Content-Type: text/html; charset=UTF-8
|
||||||
|
Content-Transfer-Encoding: quoted-printable
|
||||||
|
|
||||||
|
<html><body><div style=3D"padding:16px">
|
||||||
|
<p>You have been invited to the following event.</p>
|
||||||
|
<h3>Piano lesson: Cooper</h3>
|
||||||
|
<p><b>When</b> Wed 9 Sep 2026 17:00 to 17:45 India Standard Time<br>
|
||||||
|
<b>Where</b> Sunny Day Music, Bandra<br>
|
||||||
|
<b>Who</b> Sam Okafor, Priyanshu Jain</p>
|
||||||
|
</div></body></html>
|
||||||
|
|
||||||
|
--alt-piano-0001
|
||||||
|
Content-Type: text/calendar; charset=UTF-8; method=REQUEST
|
||||||
|
Content-Transfer-Encoding: 7bit
|
||||||
|
|
||||||
|
BEGIN:VCALENDAR
|
||||||
|
PRODID:-//Google Inc//Google Calendar 70.9054//EN
|
||||||
|
VERSION:2.0
|
||||||
|
CALSCALE:GREGORIAN
|
||||||
|
METHOD:REQUEST
|
||||||
|
BEGIN:VTIMEZONE
|
||||||
|
TZID:Asia/Kolkata
|
||||||
|
X-LIC-LOCATION:Asia/Kolkata
|
||||||
|
BEGIN:STANDARD
|
||||||
|
TZOFFSETFROM:+0530
|
||||||
|
TZOFFSETTO:+0530
|
||||||
|
TZNAME:IST
|
||||||
|
DTSTART:19700101T000000
|
||||||
|
END:STANDARD
|
||||||
|
END:VTIMEZONE
|
||||||
|
BEGIN:VEVENT
|
||||||
|
DTSTART;TZID=Asia/Kolkata:20260909T170000
|
||||||
|
DTEND;TZID=Asia/Kolkata:20260909T174500
|
||||||
|
DTSTAMP:20260902T101500Z
|
||||||
|
ORGANIZER;CN=Sunny Day Music:mailto:[email protected]
|
||||||
|
UID:[email protected]
|
||||||
|
ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=T
|
||||||
|
RUE;CN=Priyanshu Jain;X-NUM-GUESTS=0:mailto:[email protected]
|
||||||
|
ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;CN=Sam Oka
|
||||||
|
for;X-NUM-GUESTS=0:mailto:[email protected]
|
||||||
|
CREATED:20260902T101500Z
|
||||||
|
DESCRIPTION:First lesson for Cooper. Nothing to prepare\, and there is a pia
|
||||||
|
no here\, so nothing to carry either.
|
||||||
|
LAST-MODIFIED:20260902T101500Z
|
||||||
|
LOCATION:Sunny Day Music\, Bandra
|
||||||
|
SEQUENCE:0
|
||||||
|
STATUS:CONFIRMED
|
||||||
|
SUMMARY:Piano lesson: Cooper
|
||||||
|
TRANSP:OPAQUE
|
||||||
|
END:VEVENT
|
||||||
|
END:VCALENDAR
|
||||||
|
|
||||||
|
--alt-piano-0001--
|
||||||
|
|
||||||
|
--mixed-piano-0001
|
||||||
|
Content-Type: application/ics; name="invite.ics"
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
Content-Disposition: attachment; filename="invite.ics"
|
||||||
|
|
||||||
|
QkVHSU46VkNBTEVOREFSDQpQUk9ESUQ6LS8vR29vZ2xlIEluYy8vR29vZ2xlIENhbGVuZGFyIDcw
|
||||||
|
LjkwNTQvL0VODQpWRVJTSU9OOjIuMA0KQ0FMU0NBTEU6R1JFR09SSUFODQpNRVRIT0Q6UkVRVUVT
|
||||||
|
VA0KQkVHSU46VlRJTUVaT05FDQpUWklEOkFzaWEvS29sa2F0YQ0KWC1MSUMtTE9DQVRJT046QXNp
|
||||||
|
YS9Lb2xrYXRhDQpCRUdJTjpTVEFOREFSRA0KVFpPRkZTRVRGUk9NOiswNTMwDQpUWk9GRlNFVFRP
|
||||||
|
OiswNTMwDQpUWk5BTUU6SVNUDQpEVFNUQVJUOjE5NzAwMTAxVDAwMDAwMA0KRU5EOlNUQU5EQVJE
|
||||||
|
DQpFTkQ6VlRJTUVaT05FDQpCRUdJTjpWRVZFTlQNCkRUU1RBUlQ7VFpJRD1Bc2lhL0tvbGthdGE6
|
||||||
|
MjAyNjA5MDlUMTcwMDAwDQpEVEVORDtUWklEPUFzaWEvS29sa2F0YToyMDI2MDkwOVQxNzQ1MDAN
|
||||||
|
CkRUU1RBTVA6MjAyNjA5MDJUMTAxNTAwWg0KT1JHQU5JWkVSO0NOPVN1bm55IERheSBNdXNpYzpt
|
||||||
|
YWlsdG86bGVzc29uc0BzdW5ueWRheW11c2ljLmV4YW1wbGUNClVJRDo0YzliMmY3YS1waWFuby1j
|
||||||
|
b29wZXJAc3VubnlkYXltdXNpYy5leGFtcGxlDQpBVFRFTkRFRTtDVVRZUEU9SU5ESVZJRFVBTDtS
|
||||||
|
T0xFPVJFUS1QQVJUSUNJUEFOVDtQQVJUU1RBVD1ORUVEUy1BQ1RJT047UlNWUD1UDQogUlVFO0NO
|
||||||
|
PVByaXlhbnNodSBKYWluO1gtTlVNLUdVRVNUUz0wOm1haWx0bzpwakA3M2FpLm9yZw0KQVRURU5E
|
||||||
|
RUU7Q1VUWVBFPUlORElWSURVQUw7Uk9MRT1SRVEtUEFSVElDSVBBTlQ7UEFSVFNUQVQ9QUNDRVBU
|
||||||
|
RUQ7Q049U2FtIE9rYQ0KIGZvcjtYLU5VTS1HVUVTVFM9MDptYWlsdG86c2FtQHN1bm55ZGF5bXVz
|
||||||
|
aWMuZXhhbXBsZQ0KQ1JFQVRFRDoyMDI2MDkwMlQxMDE1MDBaDQpERVNDUklQVElPTjpGaXJzdCBs
|
||||||
|
ZXNzb24gZm9yIENvb3Blci4gTm90aGluZyB0byBwcmVwYXJlXCwgYW5kIHRoZXJlIGlzIGEgcGlh
|
||||||
|
DQogbm8gaGVyZVwsIHNvIG5vdGhpbmcgdG8gY2FycnkgZWl0aGVyLg0KTEFTVC1NT0RJRklFRDoy
|
||||||
|
MDI2MDkwMlQxMDE1MDBaDQpMT0NBVElPTjpTdW5ueSBEYXkgTXVzaWNcLCBCYW5kcmENClNFUVVF
|
||||||
|
TkNFOjANClNUQVRVUzpDT05GSVJNRUQNClNVTU1BUlk6UGlhbm8gbGVzc29uOiBDb29wZXINClRS
|
||||||
|
QU5TUDpPUEFRVUUNCkVORDpWRVZFTlQNCkVORDpWQ0FMRU5EQVINCg==
|
||||||
|
|
||||||
|
--mixed-piano-0001--
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
Return-Path: <[email protected]>
|
||||||
|
MIME-Version: 1.0
|
||||||
|
Date: Tue, 1 Sep 2026 14:07:33 +0900
|
||||||
|
Message-ID: <[email protected]>
|
||||||
|
Subject: =?ISO-2022-JP?B?GyRCMnE1RCROO3FOQSRyQXckaiReJDkbKEI=?=
|
||||||
|
From: =?ISO-2022-JP?B?GyRCOjRGIxsoQiAbJEIyVjtSGyhC?= <[email protected]>
|
||||||
|
To: Priyanshu Jain <[email protected]>
|
||||||
|
X-Mailer: Becky! ver. 2.75
|
||||||
|
Content-Type: text/plain; charset=ISO-2022-JP
|
||||||
|
Content-Transfer-Encoding: 7bit
|
||||||
|
|
||||||
|
$B$*@$OC$K$J$C$F$*$j$^$9!#:4F#$G$9!#(B
|
||||||
|
|
||||||
|
$BMh=5$N2q5D$N;qNA$r$*Aw$j$7$^$9!#$43NG'$N$&$(!"$40U8+$r$$$?$@$1$^$9$H(B
|
||||||
|
$B9,$$$G$9!#(B
|
||||||
|
|
||||||
|
$B$h$m$7$/$*4j$$$$$?$7$^$9!#(B
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
Return-Path: <[email protected]>
|
||||||
|
MIME-Version: 1.0
|
||||||
|
Date: Sun, 30 Aug 2026 19:38:12 +0200
|
||||||
|
Message-ID: <[email protected]>
|
||||||
|
Subject: =?iso-8859-1?Q?Votre_r=E9servation_est_confirm=E9e?=
|
||||||
|
From: =?iso-8859-1?Q?Th=E9=E2tre_du_Ch=E2telet?= <[email protected]>
|
||||||
|
To: [email protected]
|
||||||
|
Content-Type: text/plain; charset=iso-8859-1
|
||||||
|
Content-Transfer-Encoding: 8bit
|
||||||
|
|
||||||
|
Bonjour,
|
||||||
|
|
||||||
|
Votre réservation est confirmée pour la deuxième soirée. Les places sont
|
||||||
|
au rang H, côté jardin, et le théâtre ouvre à dix-neuf heures.
|
||||||
|
|
||||||
|
Merci de présenter ce message à l'entrée. Aucun billet papier ne sera
|
||||||
|
envoyé.
|
||||||
|
|
||||||
|
Bien cordialement,
|
||||||
|
La billetterie
|
||||||
|
Théâtre du Châtelet
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
Return-Path: <[email protected]>
|
||||||
|
MIME-Version: 1.0
|
||||||
|
Date: Tue, 1 Sep 2026 08:15:33 +0000
|
||||||
|
Message-ID: <[email protected]>
|
||||||
|
Subject: Fwd: Dock schedule for the week
|
||||||
|
From: Harbour Line Ops <[email protected]>
|
||||||
|
To: Priyanshu Jain <[email protected]>
|
||||||
|
Content-Type: text/html; charset=UTF-8
|
||||||
|
Content-Transfer-Encoding: 7bit
|
||||||
|
|
||||||
|
<html><body>
|
||||||
|
<p>Forwarding the schedule. The chart was in the original.</p>
|
||||||
|
<p><img src="cid:[email protected]" width="480" height="320"
|
||||||
|
alt="Dock schedule chart"></p>
|
||||||
|
<p>Nothing else changed this week.</p>
|
||||||
|
</body></html>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
Return-Path: <[email protected]>
|
||||||
|
MIME-Version: 1.0
|
||||||
|
Date: Mon, 24 Aug 2026 17:55:30 -0400
|
||||||
|
Message-ID: <[email protected]>
|
||||||
|
Subject: Pumpkin bread recipe
|
||||||
|
From: "Young, Russell \"Russ\"" <[email protected]>
|
||||||
|
To: "Jain, Priyanshu" <[email protected]>,
|
||||||
|
"Weiss, Hannah (nee Fischer)" <[email protected]>
|
||||||
|
Reply-To: "Young, Russell \"Russ\"" <[email protected]>
|
||||||
|
Content-Type: text/plain; charset=UTF-8
|
||||||
|
Content-Transfer-Encoding: 7bit
|
||||||
|
|
||||||
|
From my mother's card, transcribed as best I could. Bake at 175 for
|
||||||
|
fifty minutes, and do not open the oven before forty.
|
||||||
|
|
||||||
|
Two loaves, or one loaf and twelve muffins. The muffins take half the
|
||||||
|
time and are better the next day.
|
||||||
|
|
||||||
|
Russ
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
Return-Path: <[email protected]>
|
||||||
|
MIME-Version: 1.0
|
||||||
|
Date: Thu, 3 Sep 2026 09:03:20 +0530
|
||||||
|
Message-ID: <[email protected]>
|
||||||
|
Subject: =?UTF-8?B?WW91ciByZXNlcnZhdGlvbiBpbiBMaXNib24gaXMgY29uZmlybWVkLCBJbsM=?=
|
||||||
|
=?UTF-8?B?qnMgaXMgZXhwZWN0aW5nIHlvdQ==?=
|
||||||
|
From: Airbnb <[email protected]>
|
||||||
|
Reply-To: =?UTF-8?Q?In=C3=AAs?= <[email protected]>
|
||||||
|
To: [email protected]
|
||||||
|
X-Auto-Response-Suppress: All
|
||||||
|
Content-Type: text/plain; charset=UTF-8
|
||||||
|
Content-Transfer-Encoding: 8bit
|
||||||
|
|
||||||
|
Check-in Friday 12 September after 15:00. Your host Inês will send the
|
||||||
|
door code the evening before.
|
||||||
|
|
||||||
|
Rua da Bica de Duarte Belo 42, Lisboa. The tram stops at the top of the
|
||||||
|
street and the walk down is five minutes.
|
||||||
|
|
||||||
|
Airbnb
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
Return-Path: <[email protected]>
|
||||||
|
MIME-Version: 1.0
|
||||||
|
Date: Sun, 30 Aug 2026 21:04:55 +0100
|
||||||
|
Message-ID: <[email protected]>
|
||||||
|
Subject: =?UTF-8?B?TGVzIE1pc8OpcmFibGVzIHRpY2tldHMsIHNlY29uZCBuaWdodA==?=
|
||||||
|
From: Caroline Bauhaus <[email protected]>
|
||||||
|
To: Priyanshu Jain <[email protected]>, Maya Raghunathan <[email protected]>
|
||||||
|
Content-Type: text/plain; charset=UTF-8
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
|
||||||
|
SSBnb3QgZm91ciBmb3IgdGhlIHNlY29uZCBuaWdodCByYXRoZXIgdGhhbiB0aGUgZmlyc3QsIHRo
|
||||||
|
ZSBzZWF0cyBhcmUKYmV0dGVyIGFuZCBpdCBpcyB0ZW4gcG91bmRzIGxlc3MgZWFjaC4gUm93IEgs
|
||||||
|
IHNsaWdodGx5IGxlZnQgb2YgY2VudHJlLgoKTm9ib2R5IGhhcyB0byBwYXkgbWUgYmFjayB1bnRp
|
||||||
|
bCBhZnRlciwgYW5kIGlmIEthcnRoaWsgZHJvcHMgb3V0IEkga25vdwp0d28gcGVvcGxlIHdobyB3
|
||||||
|
b3VsZCB0YWtlIGhpcy4KCkNhcm9saW5lCg==
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
Return-Path: <[email protected]>
|
||||||
|
MIME-Version: 1.0
|
||||||
|
Date: Wed, 2 Sep 2026 17:26:41 +0200
|
||||||
|
Message-ID: <[email protected]>
|
||||||
|
Subject: =?utf-8?Q?Angebot=3A_K=C3=BCchenarbeitsplatte_in_Eiche_massi?=
|
||||||
|
=?utf-8?Q?v_=28Nachtrag=29?=
|
||||||
|
From: =?iso-8859-1?Q?Lena_Brandt_=28Tischlerei_M=FCnchen=29?= <[email protected]>
|
||||||
|
To: Priyanshu Jain <[email protected]>
|
||||||
|
Content-Type: text/plain; charset=UTF-8
|
||||||
|
Content-Transfer-Encoding: 8bit
|
||||||
|
|
||||||
|
Sounds good, Julie. Any afternoon next week works for me.
|
||||||
|
|
||||||
|
Der Nachtrag betrifft nur die Oberflaeche: geoelt statt lackiert, wie
|
||||||
|
besprochen. Die Kante bleibt gerade.
|
||||||
|
|
||||||
|
Lena
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
render-version: 2
|
||||||
|
message-id: [email protected]
|
||||||
|
in-reply-to: -
|
||||||
|
references-first: -
|
||||||
|
from: DocuSign NA3 System <[email protected]>
|
||||||
|
to: Priyanshu Jain <[email protected]>
|
||||||
|
cc: -
|
||||||
|
bcc: -
|
||||||
|
reply-to: -
|
||||||
|
date-ms: 1788201667000
|
||||||
|
subject: Completed: Studio lease 2026
|
||||||
|
is-html: false
|
||||||
|
surface: theme
|
||||||
|
snippet: All parties have completed the envelope. You can access the signed copy from the link below or from the attached PDF.
|
||||||
|
list-id: -
|
||||||
|
auto-submitted: -
|
||||||
|
precedence: -
|
||||||
|
unsubscribe: -
|
||||||
|
blocked-images: 0
|
||||||
|
attachment: part=2 name="Studio lease 2026 (signé).pdf" type=application/pdf size=193 inline=false cid=- sha256=e5a636ac00ea3c17
|
||||||
|
|
||||||
|
--- html ---
|
||||||
|
<p>All parties have completed the envelope. You can access the signed copy from the link below or from the attached PDF.</p>
|
||||||
|
|
||||||
|
--- quoted: none ---
|
||||||
|
|
||||||
|
--- text ---
|
||||||
|
All parties have completed the envelope. You can access the signed copy
|
||||||
|
from the link below or from the attached PDF.
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
render-version: 2
|
||||||
|
message-id: [email protected]
|
||||||
|
in-reply-to: -
|
||||||
|
references-first: -
|
||||||
|
from: Sam Okafor <[email protected]>
|
||||||
|
to: Priyanshu Jain <[email protected]>
|
||||||
|
cc: -
|
||||||
|
bcc: -
|
||||||
|
reply-to: -
|
||||||
|
date-ms: 1788343811000
|
||||||
|
subject: Invitation: Piano lesson: Cooper @ Wed 9 Sep 2026 17:00 - 17:45 (IST) ([email protected])
|
||||||
|
is-html: true
|
||||||
|
surface: theme
|
||||||
|
snippet: You have been invited to the following event. Piano lesson: Cooper When Wed 9 Sep 2026 17:00 to 17:45 India Standard Time Where Sunny Day Music, Bandra Who Sam Okafor, Priyanshu Jain
|
||||||
|
list-id: -
|
||||||
|
auto-submitted: -
|
||||||
|
precedence: -
|
||||||
|
unsubscribe: -
|
||||||
|
blocked-images: 0
|
||||||
|
invite-uid: [email protected]
|
||||||
|
invite-summary: Piano lesson: Cooper
|
||||||
|
invite-start-ms: 1788953400000
|
||||||
|
invite-end-ms: 1788956100000
|
||||||
|
invite-all-day: false
|
||||||
|
invite-location: Sunny Day Music, Bandra
|
||||||
|
invite-organizer: Sunny Day Music <[email protected]>
|
||||||
|
invite-my-response: NeedsAction
|
||||||
|
invite-description: First lesson for Cooper. Nothing to prepare, and there is a piano here, so nothing to carry either.
|
||||||
|
|
||||||
|
--- html ---
|
||||||
|
<div style="padding:16px">
|
||||||
|
<p>You have been invited to the following event.</p>
|
||||||
|
<h3>Piano lesson: Cooper</h3>
|
||||||
|
<p><b>When</b> Wed 9 Sep 2026 17:00 to 17:45 India Standard Time<br>
|
||||||
|
<b>Where</b> Sunny Day Music, Bandra<br>
|
||||||
|
<b>Who</b> Sam Okafor, Priyanshu Jain</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
--- quoted: none ---
|
||||||
|
|
||||||
|
--- text ---
|
||||||
|
You have been invited to the following event.
|
||||||
|
|
||||||
|
Piano lesson: Cooper
|
||||||
|
When: Wed 9 Sep 2026 17:00 to 17:45 India Standard Time
|
||||||
|
Where: Sunny Day Music, Bandra
|
||||||
|
Who: Sam Okafor, Priyanshu Jain
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
render-version: 2
|
||||||
|
message-id: [email protected]
|
||||||
|
in-reply-to: -
|
||||||
|
references-first: -
|
||||||
|
from: 佐藤 花子 <[email protected]>
|
||||||
|
to: Priyanshu Jain <[email protected]>
|
||||||
|
cc: -
|
||||||
|
bcc: -
|
||||||
|
reply-to: -
|
||||||
|
date-ms: 1788239253000
|
||||||
|
subject: 会議の資料を送ります
|
||||||
|
is-html: false
|
||||||
|
surface: theme
|
||||||
|
snippet: お世話になっております。佐藤です。 来週の会議の資料をお送りします。ご確認のうえ、ご意見をいただけますと 幸いです。 よろしくお願いいたします。
|
||||||
|
list-id: -
|
||||||
|
auto-submitted: -
|
||||||
|
precedence: -
|
||||||
|
unsubscribe: -
|
||||||
|
blocked-images: 0
|
||||||
|
|
||||||
|
--- html ---
|
||||||
|
<p>お世話になっております。佐藤です。</p>
|
||||||
|
<p>来週の会議の資料をお送りします。ご確認のうえ、ご意見をいただけますと<br>
|
||||||
|
幸いです。</p>
|
||||||
|
<p>よろしくお願いいたします。</p>
|
||||||
|
|
||||||
|
--- quoted: none ---
|
||||||
|
|
||||||
|
--- text ---
|
||||||
|
お世話になっております。佐藤です。
|
||||||
|
|
||||||
|
来週の会議の資料をお送りします。ご確認のうえ、ご意見をいただけますと
|
||||||
|
幸いです。
|
||||||
|
|
||||||
|
よろしくお願いいたします。
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
render-version: 2
|
||||||
|
message-id: [email protected]
|
||||||
|
in-reply-to: -
|
||||||
|
references-first: -
|
||||||
|
from: Théâtre du Châtelet <[email protected]>
|
||||||
|
to: <[email protected]>
|
||||||
|
cc: -
|
||||||
|
bcc: -
|
||||||
|
reply-to: -
|
||||||
|
date-ms: 1788111492000
|
||||||
|
subject: Votre réservation est confirmée
|
||||||
|
is-html: false
|
||||||
|
surface: theme
|
||||||
|
snippet: Bonjour, Votre réservation est confirmée pour la deuxième soirée. Les places sont au rang H, côté jardin, et le théâtre ouvre à dix-neuf heures. Merci de présenter ce message à l'entrée. Aucun billet…
|
||||||
|
list-id: -
|
||||||
|
auto-submitted: -
|
||||||
|
precedence: -
|
||||||
|
unsubscribe: -
|
||||||
|
blocked-images: 0
|
||||||
|
|
||||||
|
--- html ---
|
||||||
|
<p>Bonjour,</p>
|
||||||
|
<p>Votre réservation est confirmée pour la deuxième soirée. Les places sont au rang H, côté jardin, et le théâtre ouvre à dix-neuf heures.</p>
|
||||||
|
<p>Merci de présenter ce message à l'entrée. Aucun billet papier ne sera envoyé.</p>
|
||||||
|
<p>Bien cordialement,<br>
|
||||||
|
La billetterie<br>
|
||||||
|
Théâtre du Châtelet</p>
|
||||||
|
|
||||||
|
--- quoted: none ---
|
||||||
|
|
||||||
|
--- text ---
|
||||||
|
Bonjour,
|
||||||
|
|
||||||
|
Votre réservation est confirmée pour la deuxième soirée. Les places sont
|
||||||
|
au rang H, côté jardin, et le théâtre ouvre à dix-neuf heures.
|
||||||
|
|
||||||
|
Merci de présenter ce message à l'entrée. Aucun billet papier ne sera
|
||||||
|
envoyé.
|
||||||
|
|
||||||
|
Bien cordialement,
|
||||||
|
La billetterie
|
||||||
|
Théâtre du Châtelet
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
render-version: 2
|
||||||
|
message-id: [email protected]
|
||||||
|
in-reply-to: -
|
||||||
|
references-first: -
|
||||||
|
from: Harbour Line Ops <[email protected]>
|
||||||
|
to: Priyanshu Jain <[email protected]>
|
||||||
|
cc: -
|
||||||
|
bcc: -
|
||||||
|
reply-to: -
|
||||||
|
date-ms: 1788250533000
|
||||||
|
subject: Fwd: Dock schedule for the week
|
||||||
|
is-html: true
|
||||||
|
surface: theme
|
||||||
|
snippet: Forwarding the schedule. The chart was in the original. Nothing else changed this week.
|
||||||
|
list-id: -
|
||||||
|
auto-submitted: -
|
||||||
|
precedence: -
|
||||||
|
unsubscribe: -
|
||||||
|
blocked-images: 0
|
||||||
|
|
||||||
|
--- html ---
|
||||||
|
|
||||||
|
<p>Forwarding the schedule. The chart was in the original.</p>
|
||||||
|
<p><img alt="Dock schedule chart" width="480" height="320"></p>
|
||||||
|
<p>Nothing else changed this week.</p>
|
||||||
|
|
||||||
|
--- quoted: none ---
|
||||||
|
|
||||||
|
--- text ---
|
||||||
|
Forwarding the schedule. The chart was in the original. Nothing else changed this week.
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
render-version: 2
|
||||||
|
message-id: [email protected]
|
||||||
|
in-reply-to: -
|
||||||
|
references-first: -
|
||||||
|
from: Young, Russell "Russ" <[email protected]>
|
||||||
|
to: Jain, Priyanshu <[email protected]>, Weiss, Hannah (nee Fischer) <[email protected]>
|
||||||
|
cc: -
|
||||||
|
bcc: -
|
||||||
|
reply-to: Young, Russell "Russ" <[email protected]>
|
||||||
|
date-ms: 1787608530000
|
||||||
|
subject: Pumpkin bread recipe
|
||||||
|
is-html: false
|
||||||
|
surface: theme
|
||||||
|
snippet: From my mother's card, transcribed as best I could. Bake at 175 for fifty minutes, and do not open the oven before forty. Two loaves, or one loaf and twelve muffins. The muffins take half the time…
|
||||||
|
list-id: -
|
||||||
|
auto-submitted: -
|
||||||
|
precedence: -
|
||||||
|
unsubscribe: -
|
||||||
|
blocked-images: 0
|
||||||
|
|
||||||
|
--- html ---
|
||||||
|
<p>From my mother's card, transcribed as best I could. Bake at 175 for fifty minutes, and do not open the oven before forty.</p>
|
||||||
|
<p>Two loaves, or one loaf and twelve muffins. The muffins take half the time and are better the next day.</p>
|
||||||
|
<p>Russ</p>
|
||||||
|
|
||||||
|
--- quoted: none ---
|
||||||
|
|
||||||
|
--- text ---
|
||||||
|
From my mother's card, transcribed as best I could. Bake at 175 for
|
||||||
|
fifty minutes, and do not open the oven before forty.
|
||||||
|
|
||||||
|
Two loaves, or one loaf and twelve muffins. The muffins take half the
|
||||||
|
time and are better the next day.
|
||||||
|
|
||||||
|
Russ
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
render-version: 2
|
||||||
|
message-id: [email protected]
|
||||||
|
in-reply-to: -
|
||||||
|
references-first: -
|
||||||
|
from: Airbnb <[email protected]>
|
||||||
|
to: <[email protected]>
|
||||||
|
cc: -
|
||||||
|
bcc: -
|
||||||
|
reply-to: Inês <[email protected]>
|
||||||
|
date-ms: 1788406400000
|
||||||
|
subject: Your reservation in Lisbon is confirmed, Inês is expecting you
|
||||||
|
is-html: false
|
||||||
|
surface: theme
|
||||||
|
snippet: Check-in Friday 12 September after 15:00. Your host Inês will send the door code the evening before. Rua da Bica de Duarte Belo 42, Lisboa. The tram stops at the top of the street and the walk down…
|
||||||
|
list-id: -
|
||||||
|
auto-submitted: -
|
||||||
|
precedence: -
|
||||||
|
unsubscribe: -
|
||||||
|
blocked-images: 0
|
||||||
|
|
||||||
|
--- html ---
|
||||||
|
<p>Check-in Friday 12 September after 15:00. Your host Inês will send the door code the evening before.</p>
|
||||||
|
<p>Rua da Bica de Duarte Belo 42, Lisboa. The tram stops at the top of the street and the walk down is five minutes.</p>
|
||||||
|
<p>Airbnb</p>
|
||||||
|
|
||||||
|
--- quoted: none ---
|
||||||
|
|
||||||
|
--- text ---
|
||||||
|
Check-in Friday 12 September after 15:00. Your host Inês will send the
|
||||||
|
door code the evening before.
|
||||||
|
|
||||||
|
Rua da Bica de Duarte Belo 42, Lisboa. The tram stops at the top of the
|
||||||
|
street and the walk down is five minutes.
|
||||||
|
|
||||||
|
Airbnb
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
render-version: 2
|
||||||
|
message-id: [email protected]
|
||||||
|
in-reply-to: -
|
||||||
|
references-first: -
|
||||||
|
from: Caroline Bauhaus <[email protected]>
|
||||||
|
to: Priyanshu Jain <[email protected]>, Maya Raghunathan <[email protected]>
|
||||||
|
cc: -
|
||||||
|
bcc: -
|
||||||
|
reply-to: -
|
||||||
|
date-ms: 1788120295000
|
||||||
|
subject: Les Misérables tickets, second night
|
||||||
|
is-html: false
|
||||||
|
surface: theme
|
||||||
|
snippet: I got four for the second night rather than the first, the seats are better and it is ten pounds less each. Row H, slightly left of centre. Nobody has to pay me back until after, and if Karthik drops…
|
||||||
|
list-id: -
|
||||||
|
auto-submitted: -
|
||||||
|
precedence: -
|
||||||
|
unsubscribe: -
|
||||||
|
blocked-images: 0
|
||||||
|
|
||||||
|
--- html ---
|
||||||
|
<p>I got four for the second night rather than the first, the seats are better and it is ten pounds less each. Row H, slightly left of centre.</p>
|
||||||
|
<p>Nobody has to pay me back until after, and if Karthik drops out I know two people who would take his.</p>
|
||||||
|
<p>Caroline</p>
|
||||||
|
|
||||||
|
--- quoted: none ---
|
||||||
|
|
||||||
|
--- text ---
|
||||||
|
I got four for the second night rather than the first, the seats are
|
||||||
|
better and it is ten pounds less each. Row H, slightly left of centre.
|
||||||
|
|
||||||
|
Nobody has to pay me back until after, and if Karthik drops out I know
|
||||||
|
two people who would take his.
|
||||||
|
|
||||||
|
Caroline
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
render-version: 2
|
||||||
|
message-id: [email protected]
|
||||||
|
in-reply-to: -
|
||||||
|
references-first: -
|
||||||
|
from: Lena Brandt (Tischlerei München) <[email protected]>
|
||||||
|
to: Priyanshu Jain <[email protected]>
|
||||||
|
cc: -
|
||||||
|
bcc: -
|
||||||
|
reply-to: -
|
||||||
|
date-ms: 1788362801000
|
||||||
|
subject: Angebot: Küchenarbeitsplatte in Eiche massiv (Nachtrag)
|
||||||
|
is-html: false
|
||||||
|
surface: theme
|
||||||
|
snippet: Sounds good, Julie. Any afternoon next week works for me. Der Nachtrag betrifft nur die Oberflaeche: geoelt statt lackiert, wie besprochen. Die Kante bleibt gerade. Lena
|
||||||
|
list-id: -
|
||||||
|
auto-submitted: -
|
||||||
|
precedence: -
|
||||||
|
unsubscribe: -
|
||||||
|
blocked-images: 0
|
||||||
|
|
||||||
|
--- html ---
|
||||||
|
<p>Sounds good, Julie. Any afternoon next week works for me.</p>
|
||||||
|
<p>Der Nachtrag betrifft nur die Oberflaeche: geoelt statt lackiert, wie besprochen. Die Kante bleibt gerade.</p>
|
||||||
|
<p>Lena</p>
|
||||||
|
|
||||||
|
--- quoted: none ---
|
||||||
|
|
||||||
|
--- text ---
|
||||||
|
Sounds good, Julie. Any afternoon next week works for me.
|
||||||
|
|
||||||
|
Der Nachtrag betrifft nur die Oberflaeche: geoelt statt lackiert, wie
|
||||||
|
besprochen. Die Kante bleibt gerade.
|
||||||
|
|
||||||
|
Lena
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
render-version: 2
|
||||||
|
message-id: [email protected]
|
||||||
|
in-reply-to: [email protected]
|
||||||
|
references-first: [email protected]
|
||||||
|
from: Jeff Wolfe <[email protected]>
|
||||||
|
to: <[email protected]>, <[email protected]>, Bauhaus, Caroline <[email protected]>
|
||||||
|
cc: <[email protected]>
|
||||||
|
bcc: -
|
||||||
|
reply-to: -
|
||||||
|
date-ms: 1788382927000
|
||||||
|
subject: Re: Cooper's parent-teacher conference
|
||||||
|
is-html: false
|
||||||
|
surface: theme
|
||||||
|
snippet: 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. If you would prefer a video call instead of meeting at school, I am…
|
||||||
|
list-id: -
|
||||||
|
auto-submitted: -
|
||||||
|
precedence: -
|
||||||
|
unsubscribe: -
|
||||||
|
blocked-images: 0
|
||||||
|
|
||||||
|
--- html ---
|
||||||
|
<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>
|
||||||
|
<p>Jeff Wolfe<br>
|
||||||
|
Year 4, Oakridge</p>
|
||||||
|
|
||||||
|
--- quoted: none ---
|
||||||
|
|
||||||
|
--- text ---
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Jeff Wolfe
|
||||||
|
Year 4, Oakridge
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
render-version: 2
|
||||||
|
message-id: [email protected]
|
||||||
|
in-reply-to: -
|
||||||
|
references-first: -
|
||||||
|
from: Ledger Lines <[email protected]>
|
||||||
|
to: <[email protected]>
|
||||||
|
cc: -
|
||||||
|
bcc: -
|
||||||
|
reply-to: -
|
||||||
|
date-ms: 1788067838000
|
||||||
|
subject: The bond market, again
|
||||||
|
is-html: false
|
||||||
|
surface: theme
|
||||||
|
snippet: Rates did the one thing nobody had written a note about, which is nothing at all. Here is what that means for the long end. The full piece is on the site. This mail is the summary.
|
||||||
|
list-id: weekly.ledgerlines.example
|
||||||
|
auto-submitted: -
|
||||||
|
precedence: -
|
||||||
|
unsubscribe: one-click=false mailto=mailto:[email protected] url=-
|
||||||
|
blocked-images: 0
|
||||||
|
|
||||||
|
--- html ---
|
||||||
|
<p>Rates did the one thing nobody had written a note about, which is nothing at all. Here is what that means for the long end.</p>
|
||||||
|
<p>The full piece is on the site. This mail is the summary.</p>
|
||||||
|
|
||||||
|
--- quoted: none ---
|
||||||
|
|
||||||
|
--- text ---
|
||||||
|
Rates did the one thing nobody had written a note about, which is
|
||||||
|
nothing at all. Here is what that means for the long end.
|
||||||
|
|
||||||
|
The full piece is on the site. This mail is the summary.
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
render-version: 2
|
||||||
|
message-id: [email protected]
|
||||||
|
in-reply-to: -
|
||||||
|
references-first: -
|
||||||
|
from: Discount Tire <[email protected]>
|
||||||
|
to: <[email protected]>
|
||||||
|
cc: -
|
||||||
|
bcc: -
|
||||||
|
reply-to: -
|
||||||
|
date-ms: 1787752800000
|
||||||
|
subject: Your air pressure reminder
|
||||||
|
is-html: true
|
||||||
|
surface: theme
|
||||||
|
snippet: It has been 30 days since your last check. Stop by any store for a free air pressure check, no appointment needed. Book now Find a store near you
|
||||||
|
list-id: -
|
||||||
|
auto-submitted: -
|
||||||
|
precedence: -
|
||||||
|
unsubscribe: one-click=false mailto=- url=https://discounttire.example/u/4471
|
||||||
|
blocked-images: 0
|
||||||
|
|
||||||
|
--- html ---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p>It has been 30 days since your last check.
|
||||||
|
Stop by any store for a free air pressure check, no appointment needed.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<p><a target="_blank" rel="noopener noreferrer">Book
|
||||||
|
now</a></p>
|
||||||
|
<p><a href="https://discounttire.example/stores" target="_blank" rel="noopener noreferrer">Find a store near you</a></p>
|
||||||
|
|
||||||
|
--- quoted: none ---
|
||||||
|
|
||||||
|
--- text ---
|
||||||
|
It has been 30 days since your last check. Stop by any store for a free air pressure check, no appointment needed. Book now Find a store near you
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
render-version: 2
|
||||||
|
message-id: [email protected]
|
||||||
|
in-reply-to: -
|
||||||
|
references-first: -
|
||||||
|
from: Clare Dunn <[email protected]>
|
||||||
|
to: Priyanshu Jain <[email protected]>
|
||||||
|
cc: -
|
||||||
|
bcc: -
|
||||||
|
reply-to: -
|
||||||
|
date-ms: 1788182527000
|
||||||
|
subject: The signature block, and one photo
|
||||||
|
is-html: true
|
||||||
|
surface: theme
|
||||||
|
snippet: The photo you asked for is below. Clare
|
||||||
|
list-id: -
|
||||||
|
auto-submitted: -
|
||||||
|
precedence: -
|
||||||
|
unsubscribe: -
|
||||||
|
blocked-images: 0
|
||||||
|
|
||||||
|
--- html ---
|
||||||
|
|
||||||
|
<p>The photo you asked for is below.</p>
|
||||||
|
<p><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==" width="600" height="400" alt="The site"></p>
|
||||||
|
<p>Clare</p>
|
||||||
|
|
||||||
|
--- quoted: none ---
|
||||||
|
|
||||||
|
--- text ---
|
||||||
|
The photo you asked for is below. Clare
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
render-version: 2
|
||||||
|
message-id: [email protected]
|
||||||
|
in-reply-to: -
|
||||||
|
references-first: -
|
||||||
|
from: Hannah Weiss <[email protected]>
|
||||||
|
to: Priyanshu Jain <[email protected]>
|
||||||
|
cc: -
|
||||||
|
bcc: -
|
||||||
|
reply-to: -
|
||||||
|
date-ms: 1788085180000
|
||||||
|
subject: Photos from the Hawaii trip
|
||||||
|
is-html: true
|
||||||
|
surface: theme
|
||||||
|
snippet: Finally went through them all. The ones from the ridge walk are the best of the lot, two of them are below and the rest are in the folder. Hannah
|
||||||
|
list-id: -
|
||||||
|
auto-submitted: -
|
||||||
|
precedence: -
|
||||||
|
unsubscribe: -
|
||||||
|
blocked-images: 0
|
||||||
|
|
||||||
|
--- html ---
|
||||||
|
<div>
|
||||||
|
<p>Finally went through them all. The ones from the ridge walk are the
|
||||||
|
best of the lot, two of them are below and the rest are in the folder.</p>
|
||||||
|
<p><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAACJ0lEQVR42g3Loc6GIBQA0P9xvmg0EAgEAoFgYMxwA2MEAmOGm5jBwEwkRjA4EsnkA/6efv5+mkyazppRzbkWUkulF9DK6TVoQG127bL2RYemt1vj3w/IBHQGRoFzEBKkggVAOVgDAILZwWXwBUKD7YYvWDJZOltGLedWSCuVXcAqZ9dgAa3ZrcvWFxua3W77BU8mT2fPqOfcC+ml8gt45fwaPKA3u3fZ++JD89vtvxDJFOkcGY2cRyGjVHGBqFxcQwSMZo8uR19iaHG74xeQTEhnZBQ5RyFRKlwAlcM1ICCaHV1GXzA03G78QiJTonNiNHGehExSpQWScmkNCTCZPbmcfEmhpe1OXzjIdND5YPTg/BDykOpY4FDuWMMBeJj9cPnw5Qjt2O7jCyeZTjqfjJ6cn0KeUp0LnMqdazgBT7OfLp++nKGd231+oZCp0LkwWjgvQhapygJFubKGAljMXlwuvpTQynaXL1QyVTpXRivnVcgqVV2gKlfXUAGr2avL1ZcaWt3u+oWLTBedL0Yvzi8hL6muBS7lrjVcgJfZL5cvX67Qru2+vtDJ1OncGe2cdyG7VH2BrlxfQwfsZu8ud196aH27+xcGmQadB6OD8yHkkGosMJQbaxiAw+zD5eHLCG1s9/jCQ6aHzg+jD+ePkI9UzwKPcs8aHsDH7I/Ljy9PaM92P194yfTS+WX05fwV8pXqXeBV7l3DC/ia/XX59eUN7d3uF/8BbBpFEE55jaYAAAAASUVORK5CYII=" alt="The ridge at seven" width="16" height="16"></p>
|
||||||
|
<p><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAACJ0lEQVR42g3Loc6GIBQA0P9xvmg0EAgEAoFgYMxwA2MEAmOGm5jBwEwkRjA4EsnkA/6efv5+mkyazppRzbkWUkulF9DK6TVoQG127bL2RYemt1vj3w/IBHQGRoFzEBKkggVAOVgDAILZwWXwBUKD7YYvWDJZOltGLedWSCuVXcAqZ9dgAa3ZrcvWFxua3W77BU8mT2fPqOfcC+ml8gt45fwaPKA3u3fZ++JD89vtvxDJFOkcGY2cRyGjVHGBqFxcQwSMZo8uR19iaHG74xeQTEhnZBQ5RyFRKlwAlcM1ICCaHV1GXzA03G78QiJTonNiNHGehExSpQWScmkNCTCZPbmcfEmhpe1OXzjIdND5YPTg/BDykOpY4FDuWMMBeJj9cPnw5Qjt2O7jCyeZTjqfjJ6cn0KeUp0LnMqdazgBT7OfLp++nKGd231+oZCp0LkwWjgvQhapygJFubKGAljMXlwuvpTQynaXL1QyVTpXRivnVcgqVV2gKlfXUAGr2avL1ZcaWt3u+oWLTBedL0Yvzi8hL6muBS7lrjVcgJfZL5cvX67Qru2+vtDJ1OncGe2cdyG7VH2BrlxfQwfsZu8ud196aH27+xcGmQadB6OD8yHkkGosMJQbaxiAw+zD5eHLCG1s9/jCQ6aHzg+jD+ePkI9UzwKPcs8aHsDH7I/Ljy9PaM92P194yfTS+WX05fwV8pXqXeBV7l3DC/ia/XX59eUN7d3uF/8BbBpFEE55jaYAAAAASUVORK5CYII=" alt="Looking back down" width="16" height="16"></p>
|
||||||
|
<p>Hannah</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
--- quoted: none ---
|
||||||
|
|
||||||
|
--- text ---
|
||||||
|
Finally went through them all. The ones from the ridge walk are the best
|
||||||
|
of the lot, two of them are below and the rest are in the folder.
|
||||||
|
|
||||||
|
Hannah
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
render-version: 2
|
||||||
|
message-id: [email protected]
|
||||||
|
in-reply-to: -
|
||||||
|
references-first: -
|
||||||
|
from: Nadia Osei <[email protected]>
|
||||||
|
to: Priyanshu Jain <[email protected]>
|
||||||
|
cc: -
|
||||||
|
bcc: -
|
||||||
|
reply-to: -
|
||||||
|
date-ms: 1788343491000
|
||||||
|
subject: Invitation: Quarterly review @ Thu 15 Oct 2026 14:00 - 15:00 (BST)
|
||||||
|
is-html: false
|
||||||
|
surface: theme
|
||||||
|
snippet: You have been invited to Quarterly review on Thursday 15 October 2026, 14:00 to 15:00 London time.
|
||||||
|
list-id: -
|
||||||
|
auto-submitted: -
|
||||||
|
precedence: -
|
||||||
|
unsubscribe: -
|
||||||
|
blocked-images: 0
|
||||||
|
invite-uid: [email protected]
|
||||||
|
invite-summary: Quarterly review
|
||||||
|
invite-start-ms: 1792069200000
|
||||||
|
invite-end-ms: 1792072800000
|
||||||
|
invite-all-day: false
|
||||||
|
invite-location: Westferry, meeting room 2
|
||||||
|
invite-organizer: Nadia Osei <[email protected]>
|
||||||
|
invite-my-response: NeedsAction
|
||||||
|
invite-description: The quarterly numbers, then the roadmap. Bring the deck.
|
||||||
|
|
||||||
|
--- html ---
|
||||||
|
<p>You have been invited to Quarterly review on Thursday 15 October 2026, 14:00 to 15:00 London time.</p>
|
||||||
|
|
||||||
|
--- quoted: none ---
|
||||||
|
|
||||||
|
--- text ---
|
||||||
|
You have been invited to Quarterly review on Thursday 15 October 2026,
|
||||||
|
14:00 to 15:00 London time.
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
render-version: 2
|
||||||
|
message-id: [email protected]
|
||||||
|
in-reply-to: -
|
||||||
|
references-first: -
|
||||||
|
from: City Power <[email protected]>
|
||||||
|
to: Priyanshu Jain <[email protected]>
|
||||||
|
cc: -
|
||||||
|
bcc: -
|
||||||
|
reply-to: -
|
||||||
|
date-ms: 1788383091000
|
||||||
|
subject: Bill payment pending
|
||||||
|
is-html: true
|
||||||
|
surface: theme
|
||||||
|
snippet: Dear Customer, 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. Confirmation number…
|
||||||
|
list-id: -
|
||||||
|
auto-submitted: auto-generated
|
||||||
|
precedence: -
|
||||||
|
unsubscribe: -
|
||||||
|
blocked-images: 0
|
||||||
|
attachment: part=2 name="Statement-August.pdf" type=application/pdf size=193 inline=false cid=- sha256=e5a636ac00ea3c17
|
||||||
|
|
||||||
|
--- html ---
|
||||||
|
|
||||||
|
<p>Dear Customer,</p>
|
||||||
|
<p>Your one-time electronic payment of <b>98.26</b> for your City Power bill has been received and is pending. Your payment will be posted within two business days.</p>
|
||||||
|
<p>Confirmation number 116400046977232</p>
|
||||||
|
<p>Thank you,<br>City Power Customer Care</p>
|
||||||
|
|
||||||
|
--- quoted: none ---
|
||||||
|
|
||||||
|
--- text ---
|
||||||
|
Dear Customer,
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Confirmation number 116400046977232
|
||||||
|
|
||||||
|
Please keep this message for your records. If you did not make this
|
||||||
|
payment, call us on the number printed on your statement.
|
||||||
|
|
||||||
|
Thank you,
|
||||||
|
City Power Customer Care
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
render-version: 2
|
||||||
|
message-id: 01000198a2f4c1b2-9b0c1d2e-3f4a-5b6c-7d8e-9f0a1b2c3d4e@thebrowser.example
|
||||||
|
in-reply-to: -
|
||||||
|
references-first: -
|
||||||
|
from: The Browser <[email protected]>
|
||||||
|
to: <[email protected]>
|
||||||
|
cc: -
|
||||||
|
bcc: -
|
||||||
|
reply-to: -
|
||||||
|
date-ms: 1788411603000
|
||||||
|
subject: Five things worth reading this weekend
|
||||||
|
is-html: true
|
||||||
|
surface: theme
|
||||||
|
snippet: 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. The pencil, at length. Henry Petroski wrote a whole book about the…
|
||||||
|
list-id: the-browser.list.thebrowser.example
|
||||||
|
auto-submitted: -
|
||||||
|
precedence: bulk
|
||||||
|
unsubscribe: one-click=true mailto=mailto:[email protected]?subject=unsub url=https://thebrowser.example/unsubscribe?u=91827&id=8f3a1c
|
||||||
|
blocked-images: 0
|
||||||
|
|
||||||
|
--- html ---
|
||||||
|
|
||||||
|
<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.</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.</p>
|
||||||
|
<p><b>Why bridges hum.</b> A short explanation of vortex shedding from an engineer who spent a career listening to cables.</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>
|
||||||
|
<p><a href="https://thebrowser.example/unsubscribe?u=91827&id=8f3a1c" target="_blank" rel="noopener noreferrer">Unsubscribe</a></p>
|
||||||
|
|
||||||
|
--- quoted: none ---
|
||||||
|
|
||||||
|
--- text ---
|
||||||
|
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.
|
||||||
|
|
||||||
|
THE PENCIL, AT LENGTH. Henry Petroski wrote a whole book about the pencil
|
||||||
|
and this essay is the argument for why that was a reasonable thing to do.
|
||||||
|
|
||||||
|
WHY BRIDGES HUM. A short explanation of vortex shedding from an engineer
|
||||||
|
who spent a career listening to cables.
|
||||||
|
|
||||||
|
Unsubscribe: https://thebrowser.example/unsubscribe?u=91827&id=8f3a1c
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
render-version: 2
|
||||||
|
message-id: -
|
||||||
|
in-reply-to: -
|
||||||
|
references-first: -
|
||||||
|
from: Sunny Day Music <[email protected]>
|
||||||
|
to: <[email protected]>
|
||||||
|
cc: -
|
||||||
|
bcc: -
|
||||||
|
reply-to: -
|
||||||
|
date-ms: 1786512739000
|
||||||
|
subject: Enrolment received for Cooper
|
||||||
|
is-html: false
|
||||||
|
surface: theme
|
||||||
|
snippet: Enrolment received for Cooper. Term starts the week of 8 September and your teacher will be in touch to fix a weekly slot. Nothing further is needed from you today. The term dates and the studio…
|
||||||
|
list-id: -
|
||||||
|
auto-submitted: -
|
||||||
|
precedence: -
|
||||||
|
unsubscribe: -
|
||||||
|
blocked-images: 0
|
||||||
|
|
||||||
|
--- html ---
|
||||||
|
<p>Enrolment received for Cooper. Term starts the week of 8 September and your teacher will be in touch to fix a weekly slot.</p>
|
||||||
|
<p>Nothing further is needed from you today. The term dates and the studio address are on the enrolment form you signed.</p>
|
||||||
|
|
||||||
|
--- quoted: none ---
|
||||||
|
|
||||||
|
--- text ---
|
||||||
|
Enrolment received for Cooper. Term starts the week of 8 September and
|
||||||
|
your teacher will be in touch to fix a weekly slot.
|
||||||
|
|
||||||
|
Nothing further is needed from you today. The term dates and the studio
|
||||||
|
address are on the enrolment form you signed.
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
render-version: 2
|
||||||
|
message-id: AM0PR07MB5218F1C0D9E2A4E1B@AM0PR07MB5218.eurprd07.prod.outlook.com
|
||||||
|
in-reply-to: [email protected]
|
||||||
|
references-first: [email protected]
|
||||||
|
from: Young, Russell <[email protected]>
|
||||||
|
to: Priyanshu Jain <[email protected]>
|
||||||
|
cc: -
|
||||||
|
bcc: -
|
||||||
|
reply-to: -
|
||||||
|
date-ms: 1788277269000
|
||||||
|
subject: RE: The lease, clause 14
|
||||||
|
is-html: true
|
||||||
|
surface: theme
|
||||||
|
snippet: Clause 14 is the standard form. I would not sign it as drafted. Russell
|
||||||
|
list-id: -
|
||||||
|
auto-submitted: -
|
||||||
|
precedence: -
|
||||||
|
unsubscribe: -
|
||||||
|
blocked-images: 0
|
||||||
|
|
||||||
|
--- html ---
|
||||||
|
|
||||||
|
<p>Clause 14 is the standard form. I would not sign it as drafted.</p>
|
||||||
|
<p>Russell</p>
|
||||||
|
<div style="border:none;border-top:solid #E1E1E1 1.0pt;padding:3.0pt 0cm 0cm 0cm">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
--- quoted ---
|
||||||
|
<hr>
|
||||||
|
<div>
|
||||||
|
<p><b>From:</b> Priyanshu Jain <[email protected]><br>
|
||||||
|
<b>Sent:</b> Tuesday, 1 September 2026 12:00<br>
|
||||||
|
<b>Subject:</b> The lease, clause 14</p>
|
||||||
|
<p>Russell, is clause 14 negotiable or is it the landlord's standard form?</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
--- text ---
|
||||||
|
Clause 14 is the standard form. I would not sign it as drafted.
|
||||||
|
|
||||||
|
Russell
|
||||||
|
|
||||||
|
-----Original Message-----
|
||||||
|
From: Priyanshu Jain <[email protected]>
|
||||||
|
Sent: Tuesday, 1 September 2026 12:00
|
||||||
|
To: Young, Russell <[email protected]>
|
||||||
|
Subject: The lease, clause 14
|
||||||
|
|
||||||
|
Russell, is clause 14 negotiable or is it the landlord's standard form?
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
render-version: 2
|
||||||
|
message-id: [email protected]
|
||||||
|
in-reply-to: -
|
||||||
|
references-first: -
|
||||||
|
from: Maya Raghunathan <[email protected]>
|
||||||
|
to: Priyanshu Jain <[email protected]>
|
||||||
|
cc: -
|
||||||
|
bcc: -
|
||||||
|
reply-to: -
|
||||||
|
date-ms: 1788415929000
|
||||||
|
subject: Dinner on Thursday?
|
||||||
|
is-html: false
|
||||||
|
surface: theme
|
||||||
|
snippet: Priya said the place on Church Street takes bookings now, want me to put us down for four at eight? Everyone can make Thursday except Karthik, who has the badminton thing and says he will come late…
|
||||||
|
list-id: -
|
||||||
|
auto-submitted: -
|
||||||
|
precedence: -
|
||||||
|
unsubscribe: -
|
||||||
|
blocked-images: 0
|
||||||
|
|
||||||
|
--- html ---
|
||||||
|
<p>Priya said the place on Church Street takes bookings now, want me to put us down for four at eight? Everyone can make Thursday except Karthik, who has the badminton thing and says he will come late.</p>
|
||||||
|
<p>If you would rather somewhere quieter there is the Malleswaram place we went to in June. Say by tomorrow and I will call them.</p>
|
||||||
|
<p>Maya</p>
|
||||||
|
|
||||||
|
--- quoted: none ---
|
||||||
|
|
||||||
|
--- text ---
|
||||||
|
Priya said the place on Church Street takes bookings now, want me to put
|
||||||
|
us down for four at eight? Everyone can make Thursday except Karthik, who
|
||||||
|
has the badminton thing and says he will come late.
|
||||||
|
|
||||||
|
If you would rather somewhere quieter there is the Malleswaram place we
|
||||||
|
went to in June. Say by tomorrow and I will call them.
|
||||||
|
|
||||||
|
Maya
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
render-version: 2
|
||||||
|
message-id: [email protected]
|
||||||
|
in-reply-to: -
|
||||||
|
references-first: -
|
||||||
|
from: Field Notes Dispatch <[email protected]>
|
||||||
|
to: <[email protected]>
|
||||||
|
cc: -
|
||||||
|
bcc: -
|
||||||
|
reply-to: -
|
||||||
|
date-ms: 1788168600000
|
||||||
|
subject: September: the notebook that survived a washing machine
|
||||||
|
is-html: false
|
||||||
|
surface: theme
|
||||||
|
snippet: 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. Also this month: the autumn edition…
|
||||||
|
list-id: dispatch.fieldnotes.example
|
||||||
|
auto-submitted: -
|
||||||
|
precedence: -
|
||||||
|
unsubscribe: one-click=false mailto=- url=https://fieldnotes.example/u/7712
|
||||||
|
blocked-images: 0
|
||||||
|
|
||||||
|
--- html ---
|
||||||
|
<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. The paper weight is unchanged at 100 g/m², and the price stays at €12 for the three pack.</p>
|
||||||
|
<p>The equals sign is written = when it stands alone, which is the part of this encoding that catches parsers out. Trailing whitespace is kept by encoding it, like the space at the end of the previous line.</p>
|
||||||
|
|
||||||
|
--- quoted: none ---
|
||||||
|
|
||||||
|
--- text ---
|
||||||
|
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.
|
||||||
|
|
||||||
|
Also this month: the autumn edition ships on the 15th. Three colours, the usual count. The paper weight is unchanged at 100 g/m², and the price stays at €12 for the three pack.
|
||||||
|
|
||||||
|
The equals sign is written = when it stands alone, which is the part of this encoding that catches parsers out. Trailing whitespace is kept
|
||||||
|
by encoding it, like the space at the end of the previous line.
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
render-version: 2
|
||||||
|
message-id: [email protected]
|
||||||
|
in-reply-to: [email protected]
|
||||||
|
references-first: [email protected]
|
||||||
|
from: Dev Patel <[email protected]>
|
||||||
|
to: Priyanshu Jain <[email protected]>
|
||||||
|
cc: -
|
||||||
|
bcc: -
|
||||||
|
reply-to: -
|
||||||
|
date-ms: 1788191095000
|
||||||
|
subject: Re: Slides from the talk
|
||||||
|
is-html: true
|
||||||
|
surface: theme
|
||||||
|
snippet: Here they are, plus the reading list I mentioned. The paper on cache oblivious layouts is the one to start with.
|
||||||
|
list-id: -
|
||||||
|
auto-submitted: -
|
||||||
|
precedence: -
|
||||||
|
unsubscribe: -
|
||||||
|
blocked-images: 0
|
||||||
|
|
||||||
|
--- html ---
|
||||||
|
<div dir="ltr">Here they are, plus the reading list I mentioned. The paper
|
||||||
|
on cache oblivious layouts is the one to start with.</div>
|
||||||
|
|
||||||
|
--- quoted ---
|
||||||
|
<br>
|
||||||
|
<div>
|
||||||
|
<div dir="ltr">On Mon, 31 Aug 2026 at 18:40, Priyanshu
|
||||||
|
Jain <<a href="mailto:[email protected]" target="_blank" rel="noopener noreferrer">[email protected]</a>> wrote:</div>
|
||||||
|
<blockquote style="margin:0 0 0 0.8ex;border-left:1px solid #ccc;padding-left:1ex">
|
||||||
|
<div dir="ltr">Good talk. Could you send the slides, and the reading list
|
||||||
|
you put up at the end?</div>
|
||||||
|
<br>
|
||||||
|
<div>
|
||||||
|
<div dir="ltr">On Fri, 28 Aug 2026 at 12:02, Dev Patel
|
||||||
|
<<a href="mailto:[email protected]" target="_blank" rel="noopener noreferrer">[email protected]</a>>
|
||||||
|
wrote:</div>
|
||||||
|
<blockquote style="margin:0 0 0 0.8ex;border-left:1px solid #ccc;padding-left:1ex">
|
||||||
|
<div dir="ltr">Talk is on Monday at six, room 4.02. Come if you are free.</div>
|
||||||
|
</blockquote>
|
||||||
|
</div>
|
||||||
|
</blockquote>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
--- text ---
|
||||||
|
Here they are, plus the reading list I mentioned. The paper on cache
|
||||||
|
oblivious layouts is the one to start with.
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
render-version: 2
|
||||||
|
message-id: [email protected]
|
||||||
|
in-reply-to: [email protected]
|
||||||
|
references-first: [email protected]
|
||||||
|
from: Sam Okafor <[email protected]>
|
||||||
|
to: Priyanshu Jain <[email protected]>
|
||||||
|
cc: -
|
||||||
|
bcc: -
|
||||||
|
reply-to: -
|
||||||
|
date-ms: 1788343802000
|
||||||
|
subject: Re: Fw: (no subject)
|
||||||
|
is-html: false
|
||||||
|
surface: theme
|
||||||
|
snippet: Got it, thank you. Wednesdays at five work for us. The first lesson is on the 9th, nothing to prepare. The invitation is attached, it should land in your calendar. Sam
|
||||||
|
list-id: -
|
||||||
|
auto-submitted: -
|
||||||
|
precedence: -
|
||||||
|
unsubscribe: -
|
||||||
|
blocked-images: 0
|
||||||
|
|
||||||
|
--- html ---
|
||||||
|
<p>Got it, thank you. Wednesdays at five work for us.</p>
|
||||||
|
<p>The first lesson is on the 9th, nothing to prepare. The invitation is attached, it should land in your calendar.</p>
|
||||||
|
<p>Sam</p>
|
||||||
|
|
||||||
|
--- quoted ---
|
||||||
|
<p>On Mon, 31 Aug 2026 at 09:12, Priyanshu Jain <[email protected]> wrote:<br>
|
||||||
|
> Hi Sam, form signed and attached. Would a weekday after school suit<br>
|
||||||
|
> Cooper? He finishes at four and we are ten minutes away on foot.<br>
|
||||||
|
><br>
|
||||||
|
> On Wed, 12 Aug 2026 at 11:02, Sunny Day Music<br>
|
||||||
|
> <[email protected]> wrote:<br>
|
||||||
|
> > Enrolment received for Cooper. Term starts the week of 8 September<br>
|
||||||
|
> > and your teacher will be in touch to fix a weekly slot.<br>
|
||||||
|
> ><br>
|
||||||
|
> > Nothing further is needed from you today.<br>
|
||||||
|
><br>
|
||||||
|
> Thanks,<br>
|
||||||
|
> Priyanshu</p>
|
||||||
|
|
||||||
|
--- text ---
|
||||||
|
Got it, thank you. Wednesdays at five work for us.
|
||||||
|
|
||||||
|
The first lesson is on the 9th, nothing to prepare. The invitation is
|
||||||
|
attached, it should land in your calendar.
|
||||||
|
|
||||||
|
Sam
|
||||||
|
|
||||||
|
On Mon, 31 Aug 2026 at 09:12, Priyanshu Jain <[email protected]> wrote:
|
||||||
|
> Hi Sam, form signed and attached. Would a weekday after school suit
|
||||||
|
> Cooper? He finishes at four and we are ten minutes away on foot.
|
||||||
|
>
|
||||||
|
> On Wed, 12 Aug 2026 at 11:02, Sunny Day Music
|
||||||
|
> <[email protected]> wrote:
|
||||||
|
> > Enrolment received for Cooper. Term starts the week of 8 September
|
||||||
|
> > and your teacher will be in touch to fix a weekly slot.
|
||||||
|
> >
|
||||||
|
> > Nothing further is needed from you today.
|
||||||
|
>
|
||||||
|
> Thanks,
|
||||||
|
> Priyanshu
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
render-version: 2
|
||||||
|
message-id: [email protected]
|
||||||
|
in-reply-to: -
|
||||||
|
references-first: -
|
||||||
|
from: Spotify <[email protected]>
|
||||||
|
to: <[email protected]>
|
||||||
|
cc: -
|
||||||
|
bcc: -
|
||||||
|
reply-to: -
|
||||||
|
date-ms: 1788417018000
|
||||||
|
subject: Your receipt
|
||||||
|
is-html: false
|
||||||
|
surface: theme
|
||||||
|
snippet: Thank you for purchasing Spotify Premium. Amount charged 11.99, on the card ending 4417. Your next payment is due on 3 October 2026. This mailbox is not monitored. Manage your subscription from the…
|
||||||
|
list-id: -
|
||||||
|
auto-submitted: auto-generated
|
||||||
|
precedence: bulk
|
||||||
|
unsubscribe: -
|
||||||
|
blocked-images: 0
|
||||||
|
|
||||||
|
--- html ---
|
||||||
|
<p>Thank you for purchasing Spotify Premium. Amount charged 11.99, on the card ending 4417. Your next payment is due on 3 October 2026.</p>
|
||||||
|
<p>This mailbox is not monitored. Manage your subscription from the account page.</p>
|
||||||
|
|
||||||
|
--- quoted: none ---
|
||||||
|
|
||||||
|
--- text ---
|
||||||
|
Thank you for purchasing Spotify Premium. Amount charged 11.99, on the
|
||||||
|
card ending 4417. Your next payment is due on 3 October 2026.
|
||||||
|
|
||||||
|
This mailbox is not monitored. Manage your subscription from the account
|
||||||
|
page.
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
render-version: 2
|
||||||
|
message-id: [email protected]
|
||||||
|
in-reply-to: [email protected]
|
||||||
|
references-first: [email protected]
|
||||||
|
from: Arun Kulkarni <[email protected]>
|
||||||
|
to: Priyanshu Jain <[email protected]>
|
||||||
|
cc: Meridian Leasing <[email protected]>
|
||||||
|
bcc: -
|
||||||
|
reply-to: -
|
||||||
|
date-ms: 1788410702000
|
||||||
|
subject: Re: Lease renewal for the studio
|
||||||
|
is-html: false
|
||||||
|
surface: theme
|
||||||
|
snippet: Hi Priyanshu, 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…
|
||||||
|
list-id: -
|
||||||
|
auto-submitted: -
|
||||||
|
precedence: -
|
||||||
|
unsubscribe: -
|
||||||
|
blocked-images: 0
|
||||||
|
|
||||||
|
--- html ---
|
||||||
|
<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>
|
||||||
|
|
||||||
|
--- quoted: none ---
|
||||||
|
|
||||||
|
--- text ---
|
||||||
|
Hi Priyanshu,
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Best,
|
||||||
|
Arun
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
render-version: 2
|
||||||
|
message-id: [email protected]
|
||||||
|
in-reply-to: -
|
||||||
|
references-first: -
|
||||||
|
from: Lena Brandt <[email protected]>
|
||||||
|
to: Priyanshu Jain <[email protected]>
|
||||||
|
cc: -
|
||||||
|
bcc: -
|
||||||
|
reply-to: -
|
||||||
|
date-ms: 1788333120000
|
||||||
|
subject: Kitchen bench quote
|
||||||
|
is-html: false
|
||||||
|
surface: theme
|
||||||
|
snippet: The quote is attached. Prices hold for thirty days. Lena
|
||||||
|
list-id: -
|
||||||
|
auto-submitted: -
|
||||||
|
precedence: -
|
||||||
|
unsubscribe: -
|
||||||
|
blocked-images: 0
|
||||||
|
attachment: part=2 name="Angebot Küchenarbeitsplatte Eiche massiv 2026.pdf" type=application/pdf size=193 inline=false cid=- sha256=e5a636ac00ea3c17
|
||||||
|
|
||||||
|
--- html ---
|
||||||
|
<p>The quote is attached. Prices hold for thirty days.</p>
|
||||||
|
<p>Lena</p>
|
||||||
|
|
||||||
|
--- quoted: none ---
|
||||||
|
|
||||||
|
--- text ---
|
||||||
|
The quote is attached. Prices hold for thirty days.
|
||||||
|
|
||||||
|
Lena
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
render-version: 2
|
||||||
|
message-id: [email protected]
|
||||||
|
in-reply-to: -
|
||||||
|
references-first: -
|
||||||
|
from: Todd Markham <[email protected]>
|
||||||
|
to: Priyanshu Jain <[email protected]>
|
||||||
|
cc: -
|
||||||
|
bcc: -
|
||||||
|
reply-to: -
|
||||||
|
date-ms: 1788187692000
|
||||||
|
subject: Re: Life insurance quote
|
||||||
|
is-html: false
|
||||||
|
surface: theme
|
||||||
|
snippet: Hello Ms. Young, I hope all is well with you. I wanted to touch base on our conversation from a few weeks ago. Following up on the quote I sent through last week for the twenty year term policy. If…
|
||||||
|
list-id: -
|
||||||
|
auto-submitted: -
|
||||||
|
precedence: -
|
||||||
|
unsubscribe: -
|
||||||
|
blocked-images: 0
|
||||||
|
|
||||||
|
--- html ---
|
||||||
|
<p>Hello Ms. Young, I hope all is well with you. I wanted to touch base on our conversation from a few weeks ago.</p>
|
||||||
|
<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>
|
||||||
|
<p>Todd</p>
|
||||||
|
|
||||||
|
--- quoted: none ---
|
||||||
|
|
||||||
|
--- text ---
|
||||||
|
Hello Ms. Young, I hope all is well with you. I wanted to touch base on
|
||||||
|
our conversation from a few weeks ago.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Todd
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
render-version: 2
|
||||||
|
message-id: [email protected]
|
||||||
|
in-reply-to: -
|
||||||
|
references-first: -
|
||||||
|
from: Evite on behalf of Robyn Madison <[email protected]>
|
||||||
|
to: <[email protected]>
|
||||||
|
cc: -
|
||||||
|
bcc: -
|
||||||
|
reply-to: Robyn Madison <[email protected]>
|
||||||
|
date-ms: 1788315761000
|
||||||
|
subject: You have an invitation from Robyn Madison
|
||||||
|
is-html: false
|
||||||
|
surface: theme
|
||||||
|
snippet: Join us for Jack's 8th birthday! Robyn Madison needs your RSVP. Saturday 19 September, from two in the afternoon, at the park by the lake. Bring nothing but a hat.
|
||||||
|
list-id: -
|
||||||
|
auto-submitted: -
|
||||||
|
precedence: bulk
|
||||||
|
unsubscribe: one-click=false mailto=- url=https://evite.example/optout/99120
|
||||||
|
blocked-images: 0
|
||||||
|
|
||||||
|
--- html ---
|
||||||
|
<p>Join us for Jack's 8th birthday! Robyn Madison needs your RSVP.</p>
|
||||||
|
<p>Saturday 19 September, from two in the afternoon, at the park by the lake. Bring nothing but a hat.</p>
|
||||||
|
|
||||||
|
--- quoted: none ---
|
||||||
|
|
||||||
|
--- text ---
|
||||||
|
Join us for Jack's 8th birthday! Robyn Madison needs your RSVP.
|
||||||
|
|
||||||
|
Saturday 19 September, from two in the afternoon, at the park by the
|
||||||
|
lake. Bring nothing but a hat.
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
render-version: 2
|
||||||
|
message-id: [email protected]
|
||||||
|
in-reply-to: -
|
||||||
|
references-first: -
|
||||||
|
from: Lena Brandt <[email protected]>
|
||||||
|
to: Priyanshu Jain <[email protected]>
|
||||||
|
cc: -
|
||||||
|
bcc: -
|
||||||
|
reply-to: -
|
||||||
|
date-ms: 1788188733000
|
||||||
|
subject: The worktop, and one more thing
|
||||||
|
is-html: true
|
||||||
|
surface: theme
|
||||||
|
snippet: The worktop is oiled rather than lacquered, as we agreed. The delivery slot moved to the fourteenth. Everything else is unchanged. Lena Brandt, Brandt Tischlerei
|
||||||
|
list-id: -
|
||||||
|
auto-submitted: -
|
||||||
|
precedence: -
|
||||||
|
unsubscribe: -
|
||||||
|
blocked-images: 0
|
||||||
|
|
||||||
|
--- html ---
|
||||||
|
<div style="font-family:Arial , sans-serif">
|
||||||
|
<p>The worktop is oiled rather than lacquered, as we agreed.</p>
|
||||||
|
<p style="color:#d32f2f">The delivery slot moved to the fourteenth.</p>
|
||||||
|
<p><font>Everything else is unchanged.</font></p>
|
||||||
|
<p style="color:#666666">Lena Brandt, Brandt Tischlerei</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
--- quoted: none ---
|
||||||
|
|
||||||
|
--- text ---
|
||||||
|
The worktop is oiled rather than lacquered, as we agreed. The delivery slot moved to the fourteenth. Everything else is unchanged. Lena Brandt, Brandt Tischlerei
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
render-version: 2
|
||||||
|
message-id: [email protected]
|
||||||
|
in-reply-to: -
|
||||||
|
references-first: -
|
||||||
|
from: Harbour Market <[email protected]>
|
||||||
|
to: <[email protected]>
|
||||||
|
cc: -
|
||||||
|
bcc: -
|
||||||
|
reply-to: -
|
||||||
|
date-ms: 1788326100000
|
||||||
|
subject: The autumn box is back
|
||||||
|
is-html: true
|
||||||
|
surface: paper
|
||||||
|
snippet: The autumn box is back, and it is heavier than last year. See what is in it
|
||||||
|
list-id: news.harbourmarket.example
|
||||||
|
auto-submitted: -
|
||||||
|
precedence: bulk
|
||||||
|
unsubscribe: -
|
||||||
|
blocked-images: 0
|
||||||
|
|
||||||
|
--- html ---
|
||||||
|
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" border="0">
|
||||||
|
<tbody><tr><td align="center" style="padding:20px 0">
|
||||||
|
<table width="600" cellpadding="0" cellspacing="0" border="0">
|
||||||
|
<tbody><tr><td style="padding:28px;color:#2b2b2b;font-family:Georgia , serif">
|
||||||
|
<p style="margin:0 0 12px">The autumn box is back, and it is heavier than last year.</p>
|
||||||
|
<p style="margin:0"><a href="https://harbourmarket.example/boxes" target="_blank" rel="noopener noreferrer">See what is in it</a></p>
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
--- quoted: none ---
|
||||||
|
|
||||||
|
--- text ---
|
||||||
|
The autumn box is back, and it is heavier than last year. See what is in it
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
render-version: 2
|
||||||
|
message-id: [email protected]
|
||||||
|
in-reply-to: -
|
||||||
|
references-first: -
|
||||||
|
from: The Long Read <[email protected]>
|
||||||
|
to: <[email protected]>
|
||||||
|
cc: -
|
||||||
|
bcc: -
|
||||||
|
reply-to: -
|
||||||
|
date-ms: 1788415211000
|
||||||
|
subject: The weekend edition
|
||||||
|
is-html: true
|
||||||
|
surface: paper
|
||||||
|
snippet: The weekend edition Three pieces to settle into, and one to argue with. The pencil, at length Why bridges hum You are getting this because you asked for it. Unsubscribe
|
||||||
|
list-id: weekly.thelongread.example
|
||||||
|
auto-submitted: -
|
||||||
|
precedence: bulk
|
||||||
|
unsubscribe: one-click=false mailto=- url=https://thelongread.example/unsubscribe?u=4471
|
||||||
|
blocked-images: 0
|
||||||
|
|
||||||
|
--- html ---
|
||||||
|
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" border="0" bgcolor="#f4f1ea">
|
||||||
|
<tbody><tr><td align="center" style="padding:24px 0">
|
||||||
|
<table width="600" cellpadding="0" cellspacing="0" border="0" bgcolor="#ffffff">
|
||||||
|
<tbody><tr><td style="padding:32px">
|
||||||
|
<h1 style="font-family:Georgia , serif;font-size:26px;color:#1a1a1a;margin:0 0 18px">The weekend edition</h1>
|
||||||
|
<p style="font-family:Georgia , serif;font-size:16px;color:#333333;margin:0 0 14px">Three pieces to settle into, and one to argue with.</p>
|
||||||
|
<p style="margin:0 0 14px"><a href="https://thelongread.example/pencil" style="color:#8a5a2b" target="_blank" rel="noopener noreferrer">The pencil, at length</a></p>
|
||||||
|
<p style="margin:0"><a href="https://thelongread.example/bridges" style="color:#8a5a2b" target="_blank" rel="noopener noreferrer">Why bridges hum</a></p>
|
||||||
|
</td></tr>
|
||||||
|
<tr><td bgcolor="#efece4" style="padding:18px 32px;font-size:12px;color:#6b6b6b">
|
||||||
|
You are getting this because you asked for it.
|
||||||
|
<a href="https://thelongread.example/unsubscribe?u=4471" target="_blank" rel="noopener noreferrer">Unsubscribe</a>
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
--- quoted: none ---
|
||||||
|
|
||||||
|
--- text ---
|
||||||
|
The weekend edition Three pieces to settle into, and one to argue with. The pencil, at length Why bridges hum You are getting this because you asked for it. Unsubscribe
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
render-version: 2
|
||||||
|
message-id: [email protected]
|
||||||
|
in-reply-to: -
|
||||||
|
references-first: -
|
||||||
|
from: Arun Mehta <[email protected]>
|
||||||
|
to: Priyanshu Jain <[email protected]>
|
||||||
|
cc: -
|
||||||
|
bcc: -
|
||||||
|
reply-to: -
|
||||||
|
date-ms: 1788327842000
|
||||||
|
subject: Three things before Thursday
|
||||||
|
is-html: true
|
||||||
|
surface: theme
|
||||||
|
snippet: Morning Priyanshu, Three things before Thursday, none of them urgent : The revised lease is with the landlord's solicitor. The service charge reconciliation went out on Monday. Parking bay 12 is…
|
||||||
|
list-id: -
|
||||||
|
auto-submitted: -
|
||||||
|
precedence: -
|
||||||
|
unsubscribe: -
|
||||||
|
blocked-images: 0
|
||||||
|
|
||||||
|
--- html ---
|
||||||
|
<div>
|
||||||
|
<p>Morning Priyanshu,</p>
|
||||||
|
<p>Three things before Thursday, none of them <b>urgent</b>:</p>
|
||||||
|
<ul>
|
||||||
|
<li>The <b>revised lease</b> is with the landlord's solicitor.</li>
|
||||||
|
<li>The service charge reconciliation went out on Monday.</li>
|
||||||
|
<li>Parking bay 12 is yours from the first.</li>
|
||||||
|
</ul>
|
||||||
|
<p>Links, in case you want them now rather than Thursday:
|
||||||
|
<a href="https://meridianproperties.example/lease/8812" target="_blank" rel="noopener noreferrer">the draft</a>,
|
||||||
|
<a href="https://meridianproperties.example/charges/2026" target="_blank" rel="noopener noreferrer">the reconciliation</a>,
|
||||||
|
<a href="https://meridianproperties.example/parking" target="_blank" rel="noopener noreferrer">the bay plan</a> and the
|
||||||
|
<a href="https://meridianproperties.example/contact" target="_blank" rel="noopener noreferrer">office number</a>.</p>
|
||||||
|
<p>Best,<br>Arun</p>
|
||||||
|
<div>
|
||||||
|
<p>Arun Mehta<br>
|
||||||
|
Meridian Properties<br>
|
||||||
|
+91 22 6100 4400</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
--- quoted: none ---
|
||||||
|
|
||||||
|
--- text ---
|
||||||
|
Morning Priyanshu, Three things before Thursday, none of them urgent : The revised lease is with the landlord's solicitor. The service charge reconciliation went out on Monday. Parking bay 12 is yours from the first. Links, in case you want them now rather than Thursday: the draft , the reconciliation , the bay plan and the office number . Best, Arun Arun Mehta Meridian Properties +91 22 6100 4400
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
render-version: 2
|
||||||
|
message-id: [email protected]
|
||||||
|
in-reply-to: -
|
||||||
|
references-first: -
|
||||||
|
from: Station House <[email protected]>
|
||||||
|
to: <[email protected]>
|
||||||
|
cc: -
|
||||||
|
bcc: -
|
||||||
|
reply-to: -
|
||||||
|
date-ms: 1788510600000
|
||||||
|
subject: Tonight at the Station House
|
||||||
|
is-html: true
|
||||||
|
surface: theme
|
||||||
|
snippet: Doors at seven, the band at eight. Tickets on the door only, cash or card. Sold out on Saturday.
|
||||||
|
list-id: news.stationhouse.example
|
||||||
|
auto-submitted: -
|
||||||
|
precedence: -
|
||||||
|
unsubscribe: -
|
||||||
|
blocked-images: 0
|
||||||
|
|
||||||
|
--- html ---
|
||||||
|
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p>Doors at seven, the band at eight.</p>
|
||||||
|
<p>Tickets on the door only, cash or card.</p>
|
||||||
|
<p style="color:#d32f2f">Sold out on Saturday.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
--- quoted: none ---
|
||||||
|
|
||||||
|
--- text ---
|
||||||
|
Doors at seven, the band at eight. Tickets on the door only, cash or card. Sold out on Saturday.
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
render-version: 2
|
||||||
|
message-id: [email protected]
|
||||||
|
in-reply-to: -
|
||||||
|
references-first: -
|
||||||
|
from: Bramble Coffee <[email protected]>
|
||||||
|
to: <[email protected]>
|
||||||
|
cc: -
|
||||||
|
bcc: -
|
||||||
|
reply-to: -
|
||||||
|
date-ms: 1788247367000
|
||||||
|
subject: Your receipt from Bramble Coffee
|
||||||
|
is-html: true
|
||||||
|
surface: paper
|
||||||
|
snippet: Thanks, that is paid. Flat white and a cardamom bun £6.40 on the card ending 4417 Bramble Coffee, 14 Rivington Street
|
||||||
|
list-id: -
|
||||||
|
auto-submitted: auto-generated
|
||||||
|
precedence: -
|
||||||
|
unsubscribe: -
|
||||||
|
blocked-images: 0
|
||||||
|
|
||||||
|
--- html ---
|
||||||
|
<div style="background-color:#faf7f0;padding:28px;font-family:Helvetica , Arial , sans-serif">
|
||||||
|
<div style="max-width:520px;margin:0 auto">
|
||||||
|
<p style="font-size:18px;margin:0 0 16px">Thanks, that is paid.</p>
|
||||||
|
<p style="margin:0 0 8px">Flat white and a cardamom bun</p>
|
||||||
|
<p style="margin:0 0 16px">£6.40 on the card ending 4417</p>
|
||||||
|
<p style="font-size:12px;color:#7a7a7a;margin:0">Bramble Coffee, 14 Rivington Street</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
--- quoted: none ---
|
||||||
|
|
||||||
|
--- text ---
|
||||||
|
Thanks, that is paid. Flat white and a cardamom bun £6.40 on the card ending 4417 Bramble Coffee, 14 Rivington Street
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
render-version: 2
|
||||||
|
message-id: [email protected]
|
||||||
|
in-reply-to: -
|
||||||
|
references-first: -
|
||||||
|
from: Arun Kulkarni <[email protected]>
|
||||||
|
to: Priyanshu Jain <[email protected]>
|
||||||
|
cc: -
|
||||||
|
bcc: -
|
||||||
|
reply-to: -
|
||||||
|
date-ms: 1788157964000
|
||||||
|
subject: The studio, one more thing
|
||||||
|
is-html: true
|
||||||
|
surface: theme
|
||||||
|
snippet: Priyanshu, the floor plan you asked for is below. The measurements are from the survey in June, not the original drawing. Arun
|
||||||
|
list-id: -
|
||||||
|
auto-submitted: -
|
||||||
|
precedence: -
|
||||||
|
unsubscribe: -
|
||||||
|
blocked-images: 1
|
||||||
|
blocked-url: https://meridianproperties.in/img/studio-floorplan.jpg
|
||||||
|
tracker: HubSpot https://track.hubspot.com/__ptq.gif?k=8812&r=pj%4073ai.org
|
||||||
|
tracker: meridianproperties.in https://meridianproperties.in/o/open.png?id=8812
|
||||||
|
|
||||||
|
--- html ---
|
||||||
|
|
||||||
|
<p>Priyanshu, the floor plan you asked for is below. The measurements are
|
||||||
|
from the survey in June, not the original drawing.</p>
|
||||||
|
<p><img alt="Studio floor plan" width="640" height="420"></p>
|
||||||
|
<p>Arun</p>
|
||||||
|
<img style="border:0" width="1" height="1" alt="">
|
||||||
|
<img alt="" width="1" height="1">
|
||||||
|
|
||||||
|
--- quoted: none ---
|
||||||
|
|
||||||
|
--- text ---
|
||||||
|
Priyanshu, the floor plan you asked for is below. The measurements are from the survey in June, not the original drawing. Arun
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
render-version: 2
|
||||||
|
message-id: [email protected]
|
||||||
|
in-reply-to: -
|
||||||
|
references-first: -
|
||||||
|
from: Inês Carvalho <[email protected]>
|
||||||
|
to: Priyanshu Jain <[email protected]>
|
||||||
|
cc: -
|
||||||
|
bcc: -
|
||||||
|
reply-to: -
|
||||||
|
date-ms: 1788505920000
|
||||||
|
subject: Instruções para a chegada, com um café à espera
|
||||||
|
is-html: false
|
||||||
|
surface: theme
|
||||||
|
snippet: Olá Priyanshu, O código da porta é 4417 e o elevador está do lado direito. Deixo café e pão na cozinha para a manhã seguinte. Se o comboio atrasar, telefone. Não há problema nenhum. Inês
|
||||||
|
list-id: -
|
||||||
|
auto-submitted: -
|
||||||
|
precedence: -
|
||||||
|
unsubscribe: -
|
||||||
|
blocked-images: 0
|
||||||
|
|
||||||
|
--- html ---
|
||||||
|
<p>Olá Priyanshu,</p>
|
||||||
|
<p>O código da porta é 4417 e o elevador está do lado direito. Deixo café e pão na cozinha para a manhã seguinte.</p>
|
||||||
|
<p>Se o comboio atrasar, telefone. Não há problema nenhum.</p>
|
||||||
|
<p>Inês</p>
|
||||||
|
|
||||||
|
--- quoted: none ---
|
||||||
|
|
||||||
|
--- text ---
|
||||||
|
Olá Priyanshu,
|
||||||
|
|
||||||
|
O código da porta é 4417 e o elevador está do lado direito. Deixo café e
|
||||||
|
pão na cozinha para a manhã seguinte.
|
||||||
|
|
||||||
|
Se o comboio atrasar, telefone. Não há problema nenhum.
|
||||||
|
|
||||||
|
Inês
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
Return-Path: <[email protected]>
|
||||||
|
MIME-Version: 1.0
|
||||||
|
Date: Wed, 2 Sep 2026 16:02:07 -0500
|
||||||
|
Message-ID: <[email protected]>
|
||||||
|
In-Reply-To: <[email protected]>
|
||||||
|
References: <[email protected]>
|
||||||
|
Subject: Re: Cooper's parent-teacher conference
|
||||||
|
From: Jeff Wolfe <[email protected]>
|
||||||
|
To: Year 4 parents: [email protected], [email protected],
|
||||||
|
"Bauhaus, Caroline" <[email protected]>;,
|
||||||
|
undisclosed-recipients: ;
|
||||||
|
Cc: Oakridge Office: [email protected];
|
||||||
|
Content-Type: text/plain; charset=UTF-8
|
||||||
|
Content-Transfer-Encoding: 7bit
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Jeff Wolfe
|
||||||
|
Year 4, Oakridge
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
Return-Path: <[email protected]>
|
||||||
|
Received: from a.mx.ledgerlines.example (a.mx.ledgerlines.example
|
||||||
|
[198.51.100.24]) by mx.73ai.org with ESMTPS id 991ab2
|
||||||
|
for <[email protected]>; Sun, 30 Aug 2026 11:00:41 +0530 (IST)
|
||||||
|
Received: by a.mx.ledgerlines.example with SMTP id 22c1f0;
|
||||||
|
Sun, 30 Aug 2026 05:30:39 +0000 (UTC)
|
||||||
|
MIME-Version: 1.0
|
||||||
|
Date: Sun, 30 Aug 2026 05:30:38 +0000 (UTC)
|
||||||
|
Message-ID: <[email protected]>
|
||||||
|
Subject: The
|
||||||
|
bond
|
||||||
|
market,
|
||||||
|
again
|
||||||
|
From: Ledger Lines
|
||||||
|
<[email protected]>
|
||||||
|
To:
|
||||||
|
[email protected]
|
||||||
|
List-Id: Ledger Lines <weekly.ledgerlines.example>
|
||||||
|
List-Unsubscribe: <mailto:[email protected]>
|
||||||
|
X-Campaign-Tags: bonds,rates,quarterly,long-form,subscriber-only,archive
|
||||||
|
,resend,segment-b
|
||||||
|
Content-Type: text/plain; charset=us-ascii
|
||||||
|
Content-Transfer-Encoding: 7bit
|
||||||
|
|
||||||
|
Rates did the one thing nobody had written a note about, which is
|
||||||
|
nothing at all. Here is what that means for the long end.
|
||||||
|
|
||||||
|
The full piece is on the site. This mail is the summary.
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
Return-Path: <[email protected]>
|
||||||
|
MIME-Version: 1.0
|
||||||
|
Date: Wed, 26 Aug 2026 08:00:00 -0600
|
||||||
|
Message-ID: <[email protected]>
|
||||||
|
Subject: Your air pressure reminder
|
||||||
|
From: Discount Tire <[email protected]>
|
||||||
|
To: [email protected]
|
||||||
|
List-Unsubscribe: <https://discounttire.example/u/4471>
|
||||||
|
Content-Type: text/html; charset=UTF-8
|
||||||
|
Content-Transfer-Encoding: 7bit
|
||||||
|
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="refresh" content="5;url=https://discounttire.example/go">
|
||||||
|
<link rel="stylesheet" href="https://discounttire.example/css/mail.css">
|
||||||
|
<style>body { font-family: Arial, sans-serif }</style>
|
||||||
|
<script>window.location = "https://discounttire.example/track?o=1";</script>
|
||||||
|
</head>
|
||||||
|
<body onload="fetch('https://discounttire.example/beacon')">
|
||||||
|
<div style="background-image: url('https://discounttire.example/bg.png')">
|
||||||
|
<p onclick="alert('opened')">It has been 30 days since your last check.
|
||||||
|
Stop by any store for a free air pressure check, no appointment needed.</p>
|
||||||
|
</div>
|
||||||
|
<form action="https://discounttire.example/book" method="post">
|
||||||
|
<input type="text" name="postcode" placeholder="Postcode">
|
||||||
|
<button type="submit">Find a store</button>
|
||||||
|
</form>
|
||||||
|
<iframe src="https://discounttire.example/offer" width="600" height="200"
|
||||||
|
frameborder="0"></iframe>
|
||||||
|
<p><a href="javascript:void(document.location='https://evil.example')">Book
|
||||||
|
now</a></p>
|
||||||
|
<p><a href="https://discounttire.example/stores">Find a store near you</a></p>
|
||||||
|
<object data="https://discounttire.example/flash.swf"></object>
|
||||||
|
<embed src="https://discounttire.example/flash.swf">
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
Return-Path: <[email protected]>
|
||||||
|
MIME-Version: 1.0
|
||||||
|
Date: Mon, 31 Aug 2026 14:22:07 +0100
|
||||||
|
Message-ID: <[email protected]>
|
||||||
|
Subject: The signature block, and one photo
|
||||||
|
From: Clare Dunn <[email protected]>
|
||||||
|
To: Priyanshu Jain <[email protected]>
|
||||||
|
Content-Type: multipart/related; type="text/html"; boundary="_004_AM6PR03MB4419_"
|
||||||
|
|
||||||
|
--_004_AM6PR03MB4419_
|
||||||
|
Content-Type: text/html; charset="utf-8"
|
||||||
|
Content-Transfer-Encoding: 7bit
|
||||||
|
|
||||||
|
<html><body>
|
||||||
|
<p>The photo you asked for is below.</p>
|
||||||
|
<p><img src="cid:[email protected]" width="600" height="400" alt="The site"></p>
|
||||||
|
<p>Clare</p>
|
||||||
|
</body></html>
|
||||||
|
|
||||||
|
--_004_AM6PR03MB4419_
|
||||||
|
Content-Type: image/png; name="site.png"
|
||||||
|
Content-Description: site.png
|
||||||
|
Content-Disposition: attachment; filename="site.png"; size=70
|
||||||
|
Content-ID: <[email protected]>
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
|
||||||
|
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==
|
||||||
|
|
||||||
|
--_004_AM6PR03MB4419_--
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
Return-Path: <[email protected]>
|
||||||
|
MIME-Version: 1.0
|
||||||
|
Date: Sun, 30 Aug 2026 12:19:40 +0200
|
||||||
|
Message-ID: <[email protected]>
|
||||||
|
Subject: Photos from the Hawaii trip
|
||||||
|
From: Hannah Weiss <[email protected]>
|
||||||
|
To: Priyanshu Jain <[email protected]>
|
||||||
|
Content-Type: multipart/related; type="multipart/alternative";
|
||||||
|
boundary="rel-hawaii-04"
|
||||||
|
|
||||||
|
--rel-hawaii-04
|
||||||
|
Content-Type: multipart/alternative; boundary="alt-hawaii-04"
|
||||||
|
|
||||||
|
--alt-hawaii-04
|
||||||
|
Content-Type: text/plain; charset=UTF-8
|
||||||
|
Content-Transfer-Encoding: 7bit
|
||||||
|
|
||||||
|
Finally went through them all. The ones from the ridge walk are the best
|
||||||
|
of the lot, two of them are below and the rest are in the folder.
|
||||||
|
|
||||||
|
Hannah
|
||||||
|
|
||||||
|
--alt-hawaii-04
|
||||||
|
Content-Type: text/html; charset=UTF-8
|
||||||
|
Content-Transfer-Encoding: 7bit
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p>Finally went through them all. The ones from the ridge walk are the
|
||||||
|
best of the lot, two of them are below and the rest are in the folder.</p>
|
||||||
|
<p><img src="cid:[email protected]" alt="The ridge at seven"
|
||||||
|
width="16" height="16"></p>
|
||||||
|
<p><img src="cid:[email protected]" alt="Looking back down"
|
||||||
|
width="16" height="16"></p>
|
||||||
|
<p>Hannah</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
--alt-hawaii-04--
|
||||||
|
|
||||||
|
--rel-hawaii-04
|
||||||
|
Content-Type: image/png; name="ridge-walk-1.png"
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
Content-ID: <[email protected]>
|
||||||
|
Content-Disposition: inline; filename="ridge-walk-1.png"
|
||||||
|
|
||||||
|
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAACJ0lEQVR42g3Loc6GIBQA0P9xvmg0
|
||||||
|
EAgEAoFgYMxwA2MEAmOGm5jBwEwkRjA4EsnkA/6efv5+mkyazppRzbkWUkulF9DK6TVoQG127bL2
|
||||||
|
RYemt1vj3w/IBHQGRoFzEBKkggVAOVgDAILZwWXwBUKD7YYvWDJZOltGLedWSCuVXcAqZ9dgAa3Z
|
||||||
|
rcvWFxua3W77BU8mT2fPqOfcC+ml8gt45fwaPKA3u3fZ++JD89vtvxDJFOkcGY2cRyGjVHGBqFxc
|
||||||
|
QwSMZo8uR19iaHG74xeQTEhnZBQ5RyFRKlwAlcM1ICCaHV1GXzA03G78QiJTonNiNHGehExSpQWS
|
||||||
|
cmkNCTCZPbmcfEmhpe1OXzjIdND5YPTg/BDykOpY4FDuWMMBeJj9cPnw5Qjt2O7jCyeZTjqfjJ6c
|
||||||
|
n0KeUp0LnMqdazgBT7OfLp++nKGd231+oZCp0LkwWjgvQhapygJFubKGAljMXlwuvpTQynaXL1Qy
|
||||||
|
VTpXRivnVcgqVV2gKlfXUAGr2avL1ZcaWt3u+oWLTBedL0Yvzi8hL6muBS7lrjVcgJfZL5cvX67Q
|
||||||
|
ru2+vtDJ1OncGe2cdyG7VH2BrlxfQwfsZu8ud196aH27+xcGmQadB6OD8yHkkGosMJQbaxiAw+zD
|
||||||
|
5eHLCG1s9/jCQ6aHzg+jD+ePkI9UzwKPcs8aHsDH7I/Ljy9PaM92P194yfTS+WX05fwV8pXqXeBV
|
||||||
|
7l3DC/ia/XX59eUN7d3uF/8BbBpFEE55jaYAAAAASUVORK5CYII=
|
||||||
|
|
||||||
|
--rel-hawaii-04
|
||||||
|
Content-Type: image/png; name="ridge-walk-2.png"
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
Content-ID: <[email protected]>
|
||||||
|
Content-Disposition: inline; filename="ridge-walk-2.png"
|
||||||
|
|
||||||
|
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAACJ0lEQVR42g3Loc6GIBQA0P9xvmg0
|
||||||
|
EAgEAoFgYMxwA2MEAmOGm5jBwEwkRjA4EsnkA/6efv5+mkyazppRzbkWUkulF9DK6TVoQG127bL2
|
||||||
|
RYemt1vj3w/IBHQGRoFzEBKkggVAOVgDAILZwWXwBUKD7YYvWDJZOltGLedWSCuVXcAqZ9dgAa3Z
|
||||||
|
rcvWFxua3W77BU8mT2fPqOfcC+ml8gt45fwaPKA3u3fZ++JD89vtvxDJFOkcGY2cRyGjVHGBqFxc
|
||||||
|
QwSMZo8uR19iaHG74xeQTEhnZBQ5RyFRKlwAlcM1ICCaHV1GXzA03G78QiJTonNiNHGehExSpQWS
|
||||||
|
cmkNCTCZPbmcfEmhpe1OXzjIdND5YPTg/BDykOpY4FDuWMMBeJj9cPnw5Qjt2O7jCyeZTjqfjJ6c
|
||||||
|
n0KeUp0LnMqdazgBT7OfLp++nKGd231+oZCp0LkwWjgvQhapygJFubKGAljMXlwuvpTQynaXL1Qy
|
||||||
|
VTpXRivnVcgqVV2gKlfXUAGr2avL1ZcaWt3u+oWLTBedL0Yvzi8hL6muBS7lrjVcgJfZL5cvX67Q
|
||||||
|
ru2+vtDJ1OncGe2cdyG7VH2BrlxfQwfsZu8ud196aH27+xcGmQadB6OD8yHkkGosMJQbaxiAw+zD
|
||||||
|
5eHLCG1s9/jCQ6aHzg+jD+ePkI9UzwKPcs8aHsDH7I/Ljy9PaM92P194yfTS+WX05fwV8pXqXeBV
|
||||||
|
7l3DC/ia/XX59eUN7d3uF/8BbBpFEE55jaYAAAAASUVORK5CYII=
|
||||||
|
|
||||||
|
--rel-hawaii-04--
|
||||||