feat(gmail,update): finish the OAuth2 flow, and tell admins about new releases

Gmail now connects. Authorisation-code flow with PKCE, the verifier kept
server-side rather than in a cookie — the callback is a top-level
navigation from Google, and a SameSite=Lax cookie is the kind of thing
that works until a browser tightens its defaults. State is checked, the
attempt expires after ten minutes, and the spent verifier is deleted
rather than left at rest.

Scope is gmail.readonly and the sync is one-way on purpose. Unstarring a
message does not delete the task; completing the task does not touch the
mailbox. Two-way would mean deciding what happens when both sides
changed, and the only honest answer is a sync model far larger than the
feature. The query carries newer_than:30d so connecting an account with
a decade of starred mail does not create a decade of tasks.

The update check is off unless the operator turns it on, asks GitHub at
most every six hours, and sends nothing about the instance — no
identifier, no version, no counts. A development build never reports an
update: somebody running their own tree is ahead of every release, and
telling them otherwise trains them to ignore the notice. Only
administrators see it, because only they can act on it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kristian Wind
2026-08-17 08:21:51 +02:00
co-authored by Claude Opus 5
parent ba5892fa9e
commit 5546e191ac
14 changed files with 1441 additions and 15 deletions
+11
View File
@@ -31,6 +31,13 @@ type testServer struct {
func newTestServer(t *testing.T) *testServer {
t.Helper()
return newTestServerWith(t, nil)
}
// newTestServerWith lets a test vary the configuration — a registered OAuth client,
// update checking on — without every other test paying for it.
func newTestServerWith(t *testing.T, configure func(*config.Config)) *testServer {
t.Helper()
db, err := store.Open(filepath.Join(t.TempDir(), "test.db"))
if err != nil {
@@ -46,6 +53,10 @@ func newTestServer(t *testing.T) *testServer {
ResetTTL: time.Hour,
Dev: true,
}
if configure != nil {
configure(cfg)
}
// Discard: a passing test should print nothing, and these handlers log freely.
log := slog.New(slog.NewTextHandler(io.Discard, nil))