@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.
Files changed (111) hide show
  1. package/CHANGELOG.md +836 -0
  2. package/LICENSE +21 -0
  3. package/README.md +598 -2
  4. package/bin/pi-mailbridge.mjs +31 -0
  5. package/config.example.json +54 -0
  6. package/dist/core/attachments.js +87 -0
  7. package/dist/core/authz.js +65 -0
  8. package/dist/core/commands.js +72 -0
  9. package/dist/core/config-freshness.js +24 -0
  10. package/dist/core/config.js +286 -0
  11. package/dist/core/deliveries.js +180 -0
  12. package/dist/core/discover.js +282 -0
  13. package/dist/core/header-encoding.js +70 -0
  14. package/dist/core/html.js +48 -0
  15. package/dist/core/inline-images.js +355 -0
  16. package/dist/core/mail-command.js +62 -0
  17. package/dist/core/mailer.js +126 -0
  18. package/dist/core/markdown.js +96 -0
  19. package/dist/core/oauth-device.js +119 -0
  20. package/dist/core/oauth-openid.js +48 -0
  21. package/dist/core/oauth.js +137 -0
  22. package/dist/core/prime.js +51 -0
  23. package/dist/core/reply-parse.js +134 -0
  24. package/dist/core/sent-index.js +135 -0
  25. package/dist/core/settle.js +122 -0
  26. package/dist/core/setup.js +100 -0
  27. package/dist/core/smoke.js +164 -0
  28. package/dist/core/status.js +56 -0
  29. package/dist/core/token-source.js +27 -0
  30. package/dist/core/token.js +196 -0
  31. package/dist/core/version.js +44 -0
  32. package/dist/daemon/api.js +46 -0
  33. package/dist/daemon/imapflow.js +75 -0
  34. package/dist/daemon/main.js +802 -0
  35. package/dist/daemon/process.js +270 -0
  36. package/dist/daemon/scan-queue.js +38 -0
  37. package/dist/daemon/script.js +44 -0
  38. package/dist/daemon/send-budget.js +30 -0
  39. package/dist/daemon/service.js +455 -0
  40. package/dist/daemon/source-proxy.js +64 -0
  41. package/dist/daemon/spawn.js +227 -0
  42. package/dist/daemon/watcher.js +308 -0
  43. package/dist/extension.js +1233 -0
  44. package/dist/store/journal.js +92 -0
  45. package/dist/store/mode-store.js +47 -0
  46. package/dist/store/oauth-store.js +50 -0
  47. package/dist/store/paths.js +33 -0
  48. package/dist/store/pending-store.js +212 -0
  49. package/dist/store/registry.js +61 -0
  50. package/dist/store/routes.js +223 -0
  51. package/dist/store/schema.js +93 -0
  52. package/dist/store/secret.js +70 -0
  53. package/dist/store/threads.js +145 -0
  54. package/dist/store/uid-cursor.js +112 -0
  55. package/docs/ARCHITECTURE.md +356 -0
  56. package/docs/MAPPING.md +201 -0
  57. package/docs/OAUTH.md +448 -0
  58. package/docs/SECURITY.md +273 -0
  59. package/docs/images/flow.svg +124 -0
  60. package/docs/images/notification.png +0 -0
  61. package/package.json +85 -4
  62. package/scripts/render-teaser.ts +56 -0
  63. package/src/core/attachments.ts +129 -0
  64. package/src/core/authz.ts +87 -0
  65. package/src/core/commands.ts +99 -0
  66. package/src/core/config-freshness.ts +33 -0
  67. package/src/core/config.ts +373 -0
  68. package/src/core/deliveries.ts +233 -0
  69. package/src/core/discover.ts +333 -0
  70. package/src/core/header-encoding.ts +75 -0
  71. package/src/core/html.ts +62 -0
  72. package/src/core/inline-images.ts +421 -0
  73. package/src/core/mail-command.ts +76 -0
  74. package/src/core/mailer.ts +193 -0
  75. package/src/core/markdown.ts +114 -0
  76. package/src/core/oauth-device.ts +195 -0
  77. package/src/core/oauth-openid.ts +61 -0
  78. package/src/core/oauth.ts +182 -0
  79. package/src/core/prime.ts +74 -0
  80. package/src/core/reply-parse.ts +153 -0
  81. package/src/core/sent-index.ts +164 -0
  82. package/src/core/settle.ts +179 -0
  83. package/src/core/setup.ts +139 -0
  84. package/src/core/smoke.ts +207 -0
  85. package/src/core/status.ts +75 -0
  86. package/src/core/token-source.ts +34 -0
  87. package/src/core/token.ts +240 -0
  88. package/src/core/version.ts +45 -0
  89. package/src/daemon/api.ts +52 -0
  90. package/src/daemon/imapflow.ts +101 -0
  91. package/src/daemon/main.ts +832 -0
  92. package/src/daemon/process.ts +348 -0
  93. package/src/daemon/scan-queue.ts +45 -0
  94. package/src/daemon/script.ts +45 -0
  95. package/src/daemon/send-budget.ts +36 -0
  96. package/src/daemon/service.ts +550 -0
  97. package/src/daemon/source-proxy.ts +88 -0
  98. package/src/daemon/spawn.ts +332 -0
  99. package/src/daemon/watcher.ts +427 -0
  100. package/src/extension.ts +1381 -0
  101. package/src/store/journal.ts +104 -0
  102. package/src/store/mode-store.ts +60 -0
  103. package/src/store/oauth-store.ts +55 -0
  104. package/src/store/paths.ts +38 -0
  105. package/src/store/pending-store.ts +282 -0
  106. package/src/store/registry.ts +91 -0
  107. package/src/store/routes.ts +267 -0
  108. package/src/store/schema.ts +104 -0
  109. package/src/store/secret.ts +69 -0
  110. package/src/store/threads.ts +177 -0
  111. package/src/store/uid-cursor.ts +135 -0
@@ -0,0 +1,101 @@
1
+ // Real imapflow client factory.
2
+ //
3
+ // Kept separate from watcher.ts so the watcher stays testable: nothing here runs
4
+ // in a unit test. Note the two auth modes mirror core/mailer.ts — LOGIN uses an
5
+ // app password, XOAUTH2 needs an access token fetched before connect().
6
+
7
+ import {
8
+ ImapFlow,
9
+ type AuthOptions,
10
+ type FetchQueryObject,
11
+ type MessageRange,
12
+ type SearchObject,
13
+ type StoreOptions,
14
+ } from "imapflow"
15
+
16
+ import type { ResolvedConfig } from "../core/config.ts"
17
+ import type { ImapClientFactory, ImapClientLike } from "./watcher.ts"
18
+ import { createCommandTokenProvider } from "../core/oauth.ts"
19
+
20
+ export interface ImapflowFactoryOptions {
21
+ /** Supplies an access token for XOAUTH2. Required when imap auth is XOAUTH2. */
22
+ getAccessToken?: () => Promise<string>
23
+ }
24
+
25
+ /** Auth block for imapflow, honouring the configured auth type. */
26
+ export async function buildImapAuth(config: ResolvedConfig, getAccessToken?: () => Promise<string>): Promise<AuthOptions> {
27
+ const imap = config.mail.imap
28
+ if (imap.authType === "XOAUTH2") {
29
+ if (!getAccessToken) {
30
+ throw new Error("pi-mailbridge: imap.authType is XOAUTH2 but no token provider is configured")
31
+ }
32
+ const accessToken = await getAccessToken()
33
+ if (!accessToken) throw new Error("pi-mailbridge: OAuth2 token provider returned an empty token for IMAP")
34
+ return { user: imap.user, accessToken }
35
+ }
36
+ return { user: imap.user, pass: imap.password }
37
+ }
38
+
39
+ /**
40
+ * Adapt the real client to the narrow interface the watcher uses.
41
+ *
42
+ * An explicit adapter rather than a cast: every call below is checked against
43
+ * imapflow's real signature, so a major bump that renames or re-shapes a method
44
+ * fails the build instead of failing at 3am.
45
+ */
46
+ export function adapt(client: ImapFlow): ImapClientLike {
47
+ return {
48
+ connect: () => client.connect(),
49
+ logout: () => client.logout(),
50
+ mailboxOpen: (path: string) => client.mailboxOpen(path),
51
+
52
+ // imapflow returns `false`/`undefined` for an empty result set. Normalizing
53
+ // here is not cosmetic: without it a quiet mailbox would throw inside the scan.
54
+ search: async (query: Record<string, unknown>, options?: { uid?: boolean }) => {
55
+ const result = await client.search(query as SearchObject, options)
56
+ return Array.isArray(result) ? result : []
57
+ },
58
+
59
+ fetchOne: (uid: number, query: Record<string, unknown>, options?: { uid?: boolean }) =>
60
+ client.fetchOne(uid, query as FetchQueryObject, options),
61
+
62
+ messageFlagsAdd: (uid: number, flags: string[], options?: { uid?: boolean }) =>
63
+ client.messageFlagsAdd(uid as MessageRange, flags, options as StoreOptions | undefined),
64
+
65
+ // ImapFlow's event map cannot be expressed generically; this is the one
66
+ // boundary that needs a cast, and the events we listen for are covered by
67
+ // the watcher's own tests.
68
+ on: (event, listener) => {
69
+ client.on(event as never, listener as never)
70
+ },
71
+
72
+ get usable() {
73
+ return client.usable
74
+ },
75
+
76
+ // Pass the whole mailbox through, never a hand-built subset: an enumerated copy has
77
+ // silently dropped a field twice (uidValidity, then uidNext) and both times the guard
78
+ // that read it went inert in production with the tests still green.
79
+ get mailbox() {
80
+ return client.mailbox === false ? undefined : client.mailbox
81
+ },
82
+ }
83
+ }
84
+
85
+ export function createImapflowFactory(options: ImapflowFactoryOptions = {}): ImapClientFactory {
86
+ return async (config, tuning) => {
87
+ const auth = await buildImapAuth(config, options.getAccessToken)
88
+ const client = new ImapFlow({
89
+ host: config.mail.imap.host,
90
+ port: config.mail.imap.port,
91
+ secure: config.mail.imap.secure,
92
+ // imapflow 2 exposes a typed AuthOptions; no cast needed.
93
+ auth,
94
+ // Renew IDLE well under the server's ~29 minute cutoff.
95
+ maxIdleTime: tuning.idleRenewMs,
96
+ // Never let imapflow log credentials or raw protocol.
97
+ logger: false,
98
+ })
99
+ return adapt(client)
100
+ }
101
+ }