@dumbmice/pi-mailbridge 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +836 -0
- package/LICENSE +21 -0
- package/README.md +598 -2
- package/bin/pi-mailbridge.mjs +31 -0
- package/config.example.json +54 -0
- package/dist/core/attachments.js +87 -0
- package/dist/core/authz.js +65 -0
- package/dist/core/commands.js +72 -0
- package/dist/core/config-freshness.js +24 -0
- package/dist/core/config.js +286 -0
- package/dist/core/deliveries.js +180 -0
- package/dist/core/discover.js +282 -0
- package/dist/core/header-encoding.js +70 -0
- package/dist/core/html.js +48 -0
- package/dist/core/inline-images.js +355 -0
- package/dist/core/mail-command.js +62 -0
- package/dist/core/mailer.js +126 -0
- package/dist/core/markdown.js +96 -0
- package/dist/core/oauth-device.js +119 -0
- package/dist/core/oauth-openid.js +48 -0
- package/dist/core/oauth.js +137 -0
- package/dist/core/prime.js +51 -0
- package/dist/core/reply-parse.js +134 -0
- package/dist/core/sent-index.js +135 -0
- package/dist/core/settle.js +122 -0
- package/dist/core/setup.js +100 -0
- package/dist/core/smoke.js +164 -0
- package/dist/core/status.js +56 -0
- package/dist/core/token-source.js +27 -0
- package/dist/core/token.js +196 -0
- package/dist/core/version.js +44 -0
- package/dist/daemon/api.js +46 -0
- package/dist/daemon/imapflow.js +75 -0
- package/dist/daemon/main.js +802 -0
- package/dist/daemon/process.js +270 -0
- package/dist/daemon/scan-queue.js +38 -0
- package/dist/daemon/script.js +44 -0
- package/dist/daemon/send-budget.js +30 -0
- package/dist/daemon/service.js +455 -0
- package/dist/daemon/source-proxy.js +64 -0
- package/dist/daemon/spawn.js +227 -0
- package/dist/daemon/watcher.js +308 -0
- package/dist/extension.js +1233 -0
- package/dist/store/journal.js +92 -0
- package/dist/store/mode-store.js +47 -0
- package/dist/store/oauth-store.js +50 -0
- package/dist/store/paths.js +33 -0
- package/dist/store/pending-store.js +212 -0
- package/dist/store/registry.js +61 -0
- package/dist/store/routes.js +223 -0
- package/dist/store/schema.js +93 -0
- package/dist/store/secret.js +70 -0
- package/dist/store/threads.js +145 -0
- package/dist/store/uid-cursor.js +112 -0
- package/docs/ARCHITECTURE.md +356 -0
- package/docs/MAPPING.md +201 -0
- package/docs/OAUTH.md +448 -0
- package/docs/SECURITY.md +273 -0
- package/docs/images/flow.svg +124 -0
- package/docs/images/notification.png +0 -0
- package/package.json +85 -4
- package/scripts/render-teaser.ts +56 -0
- package/src/core/attachments.ts +129 -0
- package/src/core/authz.ts +87 -0
- package/src/core/commands.ts +99 -0
- package/src/core/config-freshness.ts +33 -0
- package/src/core/config.ts +373 -0
- package/src/core/deliveries.ts +233 -0
- package/src/core/discover.ts +333 -0
- package/src/core/header-encoding.ts +75 -0
- package/src/core/html.ts +62 -0
- package/src/core/inline-images.ts +421 -0
- package/src/core/mail-command.ts +76 -0
- package/src/core/mailer.ts +193 -0
- package/src/core/markdown.ts +114 -0
- package/src/core/oauth-device.ts +195 -0
- package/src/core/oauth-openid.ts +61 -0
- package/src/core/oauth.ts +182 -0
- package/src/core/prime.ts +74 -0
- package/src/core/reply-parse.ts +153 -0
- package/src/core/sent-index.ts +164 -0
- package/src/core/settle.ts +179 -0
- package/src/core/setup.ts +139 -0
- package/src/core/smoke.ts +207 -0
- package/src/core/status.ts +75 -0
- package/src/core/token-source.ts +34 -0
- package/src/core/token.ts +240 -0
- package/src/core/version.ts +45 -0
- package/src/daemon/api.ts +52 -0
- package/src/daemon/imapflow.ts +101 -0
- package/src/daemon/main.ts +832 -0
- package/src/daemon/process.ts +348 -0
- package/src/daemon/scan-queue.ts +45 -0
- package/src/daemon/script.ts +45 -0
- package/src/daemon/send-budget.ts +36 -0
- package/src/daemon/service.ts +550 -0
- package/src/daemon/source-proxy.ts +88 -0
- package/src/daemon/spawn.ts +332 -0
- package/src/daemon/watcher.ts +427 -0
- package/src/extension.ts +1381 -0
- package/src/store/journal.ts +104 -0
- package/src/store/mode-store.ts +60 -0
- package/src/store/oauth-store.ts +55 -0
- package/src/store/paths.ts +38 -0
- package/src/store/pending-store.ts +282 -0
- package/src/store/registry.ts +91 -0
- package/src/store/routes.ts +267 -0
- package/src/store/schema.ts +104 -0
- package/src/store/secret.ts +69 -0
- package/src/store/threads.ts +177 -0
- package/src/store/uid-cursor.ts +135 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Monotonic IMAP UID cursor.
|
|
2
|
+
//
|
|
3
|
+
// Polling is `UID <cursor>+1:*` — never `UNSEEN`, never by SUBJECT. `\Seen` is
|
|
4
|
+
// shared mutable state: any other mail client can flip it and silently eat a
|
|
5
|
+
// reply. The cursor is ours alone and only ever moves forward.
|
|
6
|
+
//
|
|
7
|
+
// Writes are atomic (tmp + rename) so a crash cannot leave a torn file. The daemon writes it,
|
|
8
|
+
// and so does `prime` (from the extension or the CLI); the value only moves forward, so two
|
|
9
|
+
// concurrent primes cannot corrupt it and no lock is needed.
|
|
10
|
+
|
|
11
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs"
|
|
12
|
+
import { dirname } from "node:path"
|
|
13
|
+
|
|
14
|
+
import { statePath } from "./paths.ts"
|
|
15
|
+
import { StateVersionError, checkStateVersion, stamp } from "./schema.ts"
|
|
16
|
+
|
|
17
|
+
interface CursorFile {
|
|
18
|
+
v?: number
|
|
19
|
+
lastUid: number
|
|
20
|
+
/** The mailbox's UIDVALIDITY when this cursor was written. */
|
|
21
|
+
uidValidity?: number
|
|
22
|
+
/**
|
|
23
|
+
* `prime` has run. Existence of this file proves nothing: the daemon writes it on its first
|
|
24
|
+
* connect, before it has read any mail, so "has a file" cannot distinguish a fresh install
|
|
25
|
+
* (prime is wanted) from a mailbox already being watched (prime would skip mail).
|
|
26
|
+
*/
|
|
27
|
+
primed?: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function path(env: NodeJS.ProcessEnv): string {
|
|
31
|
+
return statePath(env, "uid-cursor.json")
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Is the bridge already watching this mailbox, so that priming would skip mail?
|
|
36
|
+
*
|
|
37
|
+
* True when `prime` ran before (the flag survives a renumbering reset), or when the cursor has
|
|
38
|
+
* actually been advanced past something. A file with `lastUid: 0` and no flag is a fresh install:
|
|
39
|
+
* the daemon writes it on connect before reading anything, and priming there is the whole point.
|
|
40
|
+
*/
|
|
41
|
+
export function isPrimed(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
42
|
+
const file = readFile(env)
|
|
43
|
+
return file.primed === true || file.lastUid > 0
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Record that `prime` ran, so a later run refuses. Preserves the cursor itself. */
|
|
47
|
+
export function markPrimed(env: NodeJS.ProcessEnv = process.env): void {
|
|
48
|
+
const file = readFile(env)
|
|
49
|
+
write(env, file.lastUid, file.uidValidity, true)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function readCursor(env: NodeJS.ProcessEnv = process.env): number {
|
|
53
|
+
return readFile(env).lastUid
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function readFile(env: NodeJS.ProcessEnv): CursorFile {
|
|
57
|
+
const p = path(env)
|
|
58
|
+
try {
|
|
59
|
+
if (!existsSync(p)) return { lastUid: 0 }
|
|
60
|
+
const parsed = JSON.parse(readFileSync(p, "utf8")) as Partial<CursorFile>
|
|
61
|
+
checkStateVersion("uid-cursor.json", parsed)
|
|
62
|
+
const uid = parsed?.lastUid
|
|
63
|
+
const validity = parsed?.uidValidity
|
|
64
|
+
return {
|
|
65
|
+
lastUid: typeof uid === "number" && Number.isFinite(uid) && uid >= 0 ? uid : 0,
|
|
66
|
+
...(typeof validity === "number" && Number.isFinite(validity) ? { uidValidity: validity } : {}),
|
|
67
|
+
...(parsed?.primed === true ? { primed: true } : {}),
|
|
68
|
+
}
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (error instanceof StateVersionError) throw error
|
|
71
|
+
return { lastUid: 0 }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function write(env: NodeJS.ProcessEnv, uid: number, uidValidity?: number, primed?: boolean): void {
|
|
76
|
+
const p = path(env)
|
|
77
|
+
mkdirSync(dirname(p), { recursive: true, mode: 0o700 })
|
|
78
|
+
const tmp = `${p}.${process.pid}.tmp`
|
|
79
|
+
const keepPrimed = primed ?? readFile(env).primed === true
|
|
80
|
+
const body: CursorFile = {
|
|
81
|
+
...stamp(),
|
|
82
|
+
lastUid: uid,
|
|
83
|
+
...(uidValidity === undefined ? {} : { uidValidity }),
|
|
84
|
+
...(keepPrimed ? { primed: true } : {}),
|
|
85
|
+
}
|
|
86
|
+
writeFileSync(tmp, JSON.stringify(body), { mode: 0o600 })
|
|
87
|
+
renameSync(tmp, p)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Compare the mailbox's UIDVALIDITY with the one this cursor was written under.
|
|
92
|
+
*
|
|
93
|
+
* UIDs are only meaningful within one UIDVALIDITY. A recreated or migrated folder renumbers
|
|
94
|
+
* them from 1, and a persisted high-water cursor then skips everything for ever - visible
|
|
95
|
+
* only as "examined 0". When it changes, the cursor is reset so the new UID space is read from
|
|
96
|
+
* the start. The cost is the one ARCHITECTURE.md states: the journal is space-scoped, so a
|
|
97
|
+
* renumbering can re-deliver a reply that was acked inside the route TTL.
|
|
98
|
+
*
|
|
99
|
+
* @returns true when the cursor was reset.
|
|
100
|
+
*/
|
|
101
|
+
export function syncUidValidity(
|
|
102
|
+
env: NodeJS.ProcessEnv,
|
|
103
|
+
uidValidity: number | undefined,
|
|
104
|
+
uidNext?: number,
|
|
105
|
+
): boolean {
|
|
106
|
+
if (uidValidity === undefined || !Number.isFinite(uidValidity)) return false
|
|
107
|
+
const current = readFile(env)
|
|
108
|
+
if (current.uidValidity === uidValidity) return false
|
|
109
|
+
|
|
110
|
+
// First observation on a cursor from a version that stored no validity: usually an
|
|
111
|
+
// upgrade over an unchanged mailbox, so keep the cursor. But if the cursor is at or past
|
|
112
|
+
// the mailbox's next UID, it cannot belong to this numbering - the folder was renumbered
|
|
113
|
+
// while we were not looking - and keeping it would skip everything for ever.
|
|
114
|
+
const observedFirst = current.uidValidity === undefined
|
|
115
|
+
const outside = observedFirst && uidNext !== undefined && current.lastUid >= uidNext
|
|
116
|
+
const reset = !observedFirst || outside
|
|
117
|
+
write(env, reset ? 0 : current.lastUid, uidValidity)
|
|
118
|
+
return reset
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Advance the cursor to `uid`, never backwards.
|
|
123
|
+
* @returns the cursor value after the call.
|
|
124
|
+
*/
|
|
125
|
+
export function advanceCursor(env: NodeJS.ProcessEnv, uid: number): number {
|
|
126
|
+
const current = readFile(env)
|
|
127
|
+
if (uid <= current.lastUid) return current.lastUid
|
|
128
|
+
write(env, uid, current.uidValidity)
|
|
129
|
+
return uid
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Test helper: force the cursor to a value (ignores monotonicity). */
|
|
133
|
+
export function resetCursor(env: NodeJS.ProcessEnv, uid = 0): void {
|
|
134
|
+
write(env, uid, readFile(env).uidValidity)
|
|
135
|
+
}
|