## What changed
- add `--readonly` to `add-account`, requesting `gmail.readonly` alone instead of the default read + modify pair
- treat Gmail write access as a set, `gmail.modify` and `mail.google.com`, so a check no longer misses an account holding only the broader scope
- refuse `--readonly` against an account that already holds write access, since that access cannot be narrowed in place, and point at the revoke-and-re-add procedure that works
- refuse `--readonly` against a token that predates scope recording, since its grant cannot be verified and tokens that old were minted with read + modify
- carry non-Gmail grants forward on every re-authorization, so Calendar and Drive are never dropped by a Gmail scope change
- warn before a plain `add-account` re-widens an account that currently has no write access
- stop sync's expired-token re-auth and `add-calendar` from silently unioning write scopes back into a narrowed grant
- accept `mail.google.com` in the trash deletion pre-flight, which previously demanded `gmail.modify` and prompted accounts that already exceeded it
## Why
Sync only reads mail, but `add-account` has always requested `gmail.modify` alongside `gmail.readonly`, so there was no way to run msgvault against Gmail with a read-only grant.
Restricting scopes on the Cloud Console's Data Access page does not achieve this. That page is a declaration used for verification review, not a request-time restriction and the authorization server grants whatever the client requests. Declining a scope at the consent screen fails too, because `authorize` rejects a token that comes back narrower than requested. Changing what the client requests is the only lever.
Access already granted cannot be narrowed, and this was measured rather than assumed. Re-authorizing with a smaller scope set gives msgvault a narrower token, but the previous grant stays live: a refresh token issued beforehand still returns access tokens carrying `gmail.modify`. Revoking that credential is not a fix either, because revocation applies to the whole grant and takes the replacement with it. Both were confirmed against a live Google account.
So `--readonly` refuses an account that already holds write access instead of performing a change that would look like narrowing without being one. The way to make such an account read-only is to revoke at Google and grant again, which is documented in the setup guide and named in the refusal message.
The deletion pre-flight change matters only once accounts can be read-only. A read-only account later escalated for permanent deletion holds `mail.google.com` without `gmail.modify`, which the old check read as insufficient for trashing even though full access covers it.
## Usage
```console
msgvault add-account you@gmail.com --readonly
```
Default behaviour is unchanged. Nothing is persisted about how an account was set up, the no-op and warning behaviours derive from its current grant.
## Limitations
- access already granted cannot be narrowed. A refresh token issued before a narrowing attempt keeps working with its original scopes, and revocation applies to the whole grant, so revoking it would invalidate the replacement too — both verified against a live account. `--readonly` therefore refuses an account that already holds Gmail write access, `--force` included; the documented procedure is to revoke at Google and grant again. `--readonly` on a new account is unaffected
- `--readonly` is rejected for service accounts whose scope comes from the domain-wide delegation grant in the Admin Console rather than from msgvault flags
Also expands the Google Cloud Console setup walkthrough in `docs/guides/oauth-setup.md`.
Co-Authored-By: Claude Opus 5
Adds a
--readonlyflag toadd-accountthat requests Gmail read access (gmail.readonly) instead of the default read + modify pair.Reconciliation of #567: quetza's latest revision (adopted verbatim as the base commit — its live-account testing established that access already granted cannot be narrowed in place), plus one hardening commit on top.
What changed
add-account --readonlyauthorizes an account with read-only Gmail access. Sync, search, and the TUI work as normal; deletion prompts to re-authorize with write access.--readonlyrefuses an account that already holds write access (--forceincluded) and prints the procedure that works — revoke msgvault at myaccount.google.com/permissions, delete the token file, re-add with--readonly.--readonlyauthorization, the granted scopes are checked: if Google returned write access anyway, a warning names it and the removal procedure.gmail.sendin an externally minted token) can't pass as narrow.--readonlydecision resolves Gmail alias spellings (dots, plus-addresses, case,googlemail.com) against stored tokens, so a second spelling can't read as a fresh account while the stored spelling keeps its access.remove-accountrevokes the Gmail grant (best-effort) before deleting the token file, so removing an account and re-adding it read-only yields a genuinely fresh grant.add-calendar) no longer silently restore write access to a narrowed account.--readonly, so following them doesn't undo the narrowing.mail.google.comas sufficient for trashing; it previously demandedgmail.modifyeven from accounts with broader access.Why
Sync only reads mail, but
add-accountalways requestedgmail.modify, so there was no way to run msgvault with a read-only grant. Restricting scopes in the Google Cloud Console doesn't help — that page is a declaration for verification review, not a limit on what gets granted — so changing what the client requests is the only lever.Usage
msgvault add-account you@gmail.com --readonlyDefault behavior without
--readonlyis unchanged.Limitations
--readonlyis rejected for service accounts; their scopes come from the domain-wide delegation grant in the Admin Console.Also expands the Google Cloud Console setup walkthrough in
docs/guides/oauth-setup.md.🤖 Generated with Claude Code