@dumbmice/pi-mailbridge 0.0.0-stage → 0.1.0
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 +824 -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
package/docs/OAUTH.md
ADDED
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
# OAUTH.md — making Outlook, Microsoft 365 and Gmail work
|
|
2
|
+
|
|
3
|
+
## The short version
|
|
4
|
+
|
|
5
|
+
Bad news only for passwords. **OAuth2 works.** Microsoft retired *Basic
|
|
6
|
+
Authentication* for `outlook.com`/`hotmail.com` on **2024-09-16**. App passwords can still
|
|
7
|
+
be created in some tenants, but **IMAP rejects them** and SMTP acceptance is inconsistent —
|
|
8
|
+
treat password auth as unusable for personal Microsoft accounts and use OAuth2. The fix is a token, and this package now gets
|
|
9
|
+
one for you with a device-code login.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# 1. once: register an app and paste the client id into config.json
|
|
13
|
+
pi-mailbridge --check # tells you what is missing
|
|
14
|
+
# 2. once: approve on any device
|
|
15
|
+
pi-mailbridge --auth
|
|
16
|
+
# 3. from then on it refreshes itself
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## One-time app registration
|
|
20
|
+
|
|
21
|
+
You cannot avoid this step: Microsoft (and Google) require a client id that a
|
|
22
|
+
human consented to. It is free and takes about two minutes.
|
|
23
|
+
|
|
24
|
+
1. Open the [Entra admin center](https://entra.microsoft.com) → **Identity** →
|
|
25
|
+
**Applications** → **App registrations** → **New registration**
|
|
26
|
+
2. Name it anything (`pi-mailbridge`)
|
|
27
|
+
3. **Supported account types**: pick the one matching your account
|
|
28
|
+
- personal `@outlook.com` / `@hotmail.com` → *…and personal Microsoft accounts*
|
|
29
|
+
- work `@yourcompany.com` → *Accounts in this organizational directory only*
|
|
30
|
+
4. **Redirect URI**: leave empty (device code needs none)
|
|
31
|
+
5. Register, then copy **Application (client) ID**
|
|
32
|
+
6. **Authentication** → *Allow public client flows* → **Yes**
|
|
33
|
+
(device code is a public client flow; without this the request is rejected)
|
|
34
|
+
7. **API permissions** → *Add a permission* → *APIs my organization uses* is not
|
|
35
|
+
needed for personal accounts; for work accounts add **Microsoft Graph →
|
|
36
|
+
Delegated → `offline_access`, `IMAP.AccessAsUser.All`, `SMTP.Send`** and grant
|
|
37
|
+
consent
|
|
38
|
+
|
|
39
|
+
For Gmail: [Google Cloud console](https://console.cloud.google.com) → APIs &
|
|
40
|
+
Services → Credentials → *Create OAuth client ID* → **TVs and Limited Input
|
|
41
|
+
devices** (that is Google's name for device flow) → enable the Gmail API.
|
|
42
|
+
|
|
43
|
+
## If Google is unreachable (mainland China)
|
|
44
|
+
|
|
45
|
+
Gmail is out twice over: no Google access, and phone verification you may not have.
|
|
46
|
+
The practical providers there authenticate with an **app-specific code**
|
|
47
|
+
(授权码 / authorization code) used as the password — so, like a Gmail app password,
|
|
48
|
+
there is no OAuth app, no tenant, and no consent screen:
|
|
49
|
+
|
|
50
|
+
| Provider | IMAP | SMTP | Notes |
|
|
51
|
+
| --- | --- | --- | --- |
|
|
52
|
+
| **163** (`163.com`) | `imap.163.com:993` | `smtp.163.com:465` | Enable in 设置 → POP3/SMTP/IMAP; SMS verification, then a 授权码. Requires the IMAP `ID` command, which this client sends automatically. **No waiting period** |
|
|
53
|
+
| **126 / yeah.net / 188.com** | same pattern | same pattern | NetEase family, same service and 授权码 rules |
|
|
54
|
+
| **QQ Mail** (`qq.com`, `foxmail.com`) | `imap.qq.com:993` | `smtp.qq.com:465` | Enable **IMAP/SMTP服务** in 设置 → 账户, then a 授权码. **New accounts must be 14 days old**: *"你的QQ邮箱激活时间不满14天,故暂时无法开启"* |
|
|
55
|
+
|
|
56
|
+
All three families are built into the discovery table, so they resolve even when the
|
|
57
|
+
autoconfig service is slow or blocked:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
/mailbridge setup pi-yourname@qq.com to you@hotmail.com
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Then paste the 授权码 into both password fields and run `/mailbridge smoke`.
|
|
64
|
+
|
|
65
|
+
**`Authentication-Results` on QQ.** Verified against a live mailbox: external mail
|
|
66
|
+
arrives with a header whose authserv-id is **`mx.qq.com`**, e.g.
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
Authentication-Results: mx.qq.com; spf=pass(...) ...; dkim=pass(...) ...
|
|
70
|
+
; dmarc=pass(p=NONE sp=NONE pct=100) header.from=your-provider.example
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Mail sent from QQ to QQ gets *no* header (same server, nothing to authenticate), so a
|
|
74
|
+
self-addressed test never exercises the DMARC path — use a reply from an external
|
|
75
|
+
address. If you want to pin the relay exactly, set
|
|
76
|
+
`security.trustedAuthservId: "mx.qq.com"`; leaving it unset uses the topmost header,
|
|
77
|
+
which is the safer default because a provider can change the id.
|
|
78
|
+
|
|
79
|
+
**One thing to watch:** if every reply is rejected with `dmarc-missing`, the
|
|
80
|
+
provider is not adding an `Authentication-Results` header. This is the normal case for a
|
|
81
|
+
self-addressed (single-mailbox) setup, which is why `/mailbridge setup` turns DMARC off when
|
|
82
|
+
both addresses match. Set
|
|
83
|
+
`security.requireDmarcPass: false` — the sender allowlist still applies, so this
|
|
84
|
+
loses a defence layer rather than all of them.
|
|
85
|
+
|
|
86
|
+
## Why can't I log into Entra with my new Hotmail address?
|
|
87
|
+
|
|
88
|
+
Because it is two different systems:
|
|
89
|
+
|
|
90
|
+
| | What it is | What it's for |
|
|
91
|
+
| --- | --- | --- |
|
|
92
|
+
| **Microsoft account (MSA)** | the identity behind `you@hotmail.com` | signing in to Outlook, and the **device-code flow** |
|
|
93
|
+
| **Microsoft Entra tenant** | a *directory* that holds app registrations | `entra.microsoft.com`, `portal.azure.com` |
|
|
94
|
+
|
|
95
|
+
A brand-new Hotmail address has **no tenant**. When you sign in to the Entra
|
|
96
|
+
admin centre with it, Microsoft drops you into the shared **"Microsoft Services"
|
|
97
|
+
tenant**, where you are not a member — hence "this account does not exist".
|
|
98
|
+
Microsoft's own answer says it plainly:
|
|
99
|
+
|
|
100
|
+
> Within the default tenant, there is no linked directory for administrative
|
|
101
|
+
> actions such as app registration.
|
|
102
|
+
|
|
103
|
+
Nothing is wrong with your mailbox. You just do not have a directory to put an app in.
|
|
104
|
+
|
|
105
|
+
### Your three options, with the friction measured
|
|
106
|
+
|
|
107
|
+
| | Outlook (personal) | Gmail |
|
|
108
|
+
| --- | --- | --- |
|
|
109
|
+
| **App password** | ⚠️ can still be created; **IMAP rejects it** (SMTP may accept it) | ✅ 16-char passcode, works for both directions |
|
|
110
|
+
| **OAuth app needed** | ✅ plus an Entra **tenant** | ✅ plus a GCP project |
|
|
111
|
+
| **OAuth friction** | no tenant → "account does not exist" | mail scopes are **restricted**: verification **+ annual CASA** to publish; a consent screen left in **Testing** expires refresh tokens **every 7 days** (`invalid_grant`) |
|
|
112
|
+
| **Servers** | sometimes refuse IMAP/SMTP OAuth2 outright | reliable |
|
|
113
|
+
|
|
114
|
+
That table is the whole argument: **Gmail is easier because of app passwords, not
|
|
115
|
+
because of OAuth.** With an app password there is no OAuth app, no tenant, no
|
|
116
|
+
verification, and no token that dies weekly.
|
|
117
|
+
|
|
118
|
+
**A. Gmail + app password (recommended).**
|
|
119
|
+
|
|
120
|
+
1. Enable **2-Step Verification** on the Google account (required; app passwords
|
|
121
|
+
do not exist without it)
|
|
122
|
+
2. <https://myaccount.google.com/apppasswords> → create one → copy the 16 characters
|
|
123
|
+
3. Run setup, then paste the passcode into both password fields:
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
/mailbridge setup pi-yourname@gmail.com to you@hotmail.com force
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`--setup` picks `LOGIN` for Google automatically and attaches no OAuth block, so
|
|
130
|
+
there is nothing to register.
|
|
131
|
+
|
|
132
|
+
**B. Get a free Entra tenant for the Hotmail account.** Signing up for Azure with a
|
|
133
|
+
personal Microsoft account creates a **default directory** where you are Global
|
|
134
|
+
Administrator, and app registration then works there. Two routes:
|
|
135
|
+
|
|
136
|
+
*Global Azure free account* — <https://azure.microsoft.com/free/>:
|
|
137
|
+
|
|
138
|
+
1. Sign in with the pi Hotmail account (not via the Entra admin centre)
|
|
139
|
+
2. Complete identity verification (phone) and the profile step; a card is usually
|
|
140
|
+
required as a payment-method check even for the free tier
|
|
141
|
+
3. On completion Azure creates the default directory. Sign in to
|
|
142
|
+
<https://entra.microsoft.com> and you should now see it in the directory picker
|
|
143
|
+
4. **App registrations → New registration**
|
|
144
|
+
|
|
145
|
+
*Microsoft 365 Developer Program* — **does not work for a personal account.**
|
|
146
|
+
Joining is free, but the E5 sandbox is gated: Microsoft grants it only to
|
|
147
|
+
|
|
148
|
+
- active **Visual Studio Professional or Enterprise** subscribers
|
|
149
|
+
- **ISV Success** program members
|
|
150
|
+
- **AI Cloud Partner** program participants
|
|
151
|
+
- **Premier or Unified Support** plan members
|
|
152
|
+
|
|
153
|
+
A personal `@outlook.com`/`@hotmail.com` account that joins the program gets the
|
|
154
|
+
learning resources and a Graph tutorial, and **no dashboard option to provision a
|
|
155
|
+
sandbox** — eligibility is decided server-side and cannot be requested. If you
|
|
156
|
+
joined hoping for a tenant, that route is closed.
|
|
157
|
+
|
|
158
|
+
So the only remaining way to obtain a tenant for a personal mailbox is the **Azure
|
|
159
|
+
free account** above, which is why it asks for a card.
|
|
160
|
+
|
|
161
|
+
### If you do end up with a work/school tenant
|
|
162
|
+
|
|
163
|
+
A tenant mailbox is not equivalent to the personal one:
|
|
164
|
+
|
|
165
|
+
| pi mailbox | Authority | Risk |
|
|
166
|
+
| --- | --- | --- |
|
|
167
|
+
| `pi-xyz@hotmail.com` (personal) | `consumers/v2.0` | the personal-Outlook IMAP/SMTP refusals below |
|
|
168
|
+
| `pi@yourtenant.onmicrosoft.com` (work) | `organizations/v2.0` | tied to the tenant; a sandbox renews every 90 days |
|
|
169
|
+
|
|
170
|
+
The app must list **personal Microsoft accounts** in its supported account types for
|
|
171
|
+
the `consumers` row to work; for the `organizations` row, any tenant member will do.
|
|
172
|
+
`--setup` picks the right authority from the address suffix automatically.
|
|
173
|
+
|
|
174
|
+
> **Do not use Azure China / 21Vianet.** `azure.cn` is a physically and logically
|
|
175
|
+
> isolated sovereign cloud with its own identity endpoints
|
|
176
|
+
> (`login.partner.microsoftonline.cn`). An app registered in a 21Vianet tenant
|
|
177
|
+
> **cannot** authenticate against global Outlook.com — the device flow against
|
|
178
|
+
> `login.microsoftonline.com` will fail. You need the global cloud, i.e.
|
|
179
|
+
> `azure.microsoft.com` / `login.microsoftonline.com`. `--check` reports it and exits non-zero if
|
|
180
|
+
> `oauth.issuer` points at a sovereign-cloud domain.
|
|
181
|
+
|
|
182
|
+
> Caveat: if the Microsoft account already belongs to a tenant as a member or
|
|
183
|
+
> guest, Azure signup joins that tenant instead of creating a fresh directory.
|
|
184
|
+
> A brand-new personal account gets its own.
|
|
185
|
+
|
|
186
|
+
Whatever tenant holds the app, set **Supported account types** to include *personal Microsoft accounts* so the Hotmail identity can sign in, and
|
|
187
|
+
**Allow public client flows → Yes**.
|
|
188
|
+
|
|
189
|
+
**C. Use a GCP project for Gmail OAuth.** Only worth it if you object to app
|
|
190
|
+
passwords. You must either accept **7-day refresh tokens** in Testing status (the
|
|
191
|
+
daemon re-authorises weekly) or publish the app — and reading mail needs restricted
|
|
192
|
+
scopes, which means verification plus a recurring CASA assessment.
|
|
193
|
+
|
|
194
|
+
> Trade-off worth naming: Google calls app passwords "not recommended and
|
|
195
|
+
> unnecessary in most cases" and could retire them. OAuth is the durable path; this
|
|
196
|
+
> package supports both. For a personal mailbox today, the app password is simply
|
|
197
|
+
> the one that works without a week of paperwork.
|
|
198
|
+
|
|
199
|
+
### What the app registration needs, wherever it lives
|
|
200
|
+
|
|
201
|
+
- **Allow public client flows → Yes** (device code is a public-client flow;
|
|
202
|
+
without it you get `AADSTS7000218: the request body must contain 'client_assertion'`)
|
|
203
|
+
- Supported account types including **personal Microsoft accounts**, if the pi
|
|
204
|
+
mailbox is `@hotmail.com`/`@outlook.com`
|
|
205
|
+
- No client secret, no redirect URI
|
|
206
|
+
|
|
207
|
+
## Do I need a paid Entra license?
|
|
208
|
+
|
|
209
|
+
**No.** Entra ID **Free** is what this needs, and it is included with any Azure
|
|
210
|
+
subscription or Microsoft 365 tenant at no charge.
|
|
211
|
+
|
|
212
|
+
| What | Tier | Cost |
|
|
213
|
+
| --- | --- | --- |
|
|
214
|
+
| App registration (client id) | Free | **$0** |
|
|
215
|
+
| Device-code / PKCE / OAuth2 token issuance | Free | **$0** |
|
|
216
|
+
| Delegated IMAP/SMTP/Graph scopes | Free | **$0** |
|
|
217
|
+
| Conditional Access, PIM, Identity Protection, risk policies | **P1 / P2** | paid — **not needed** |
|
|
218
|
+
|
|
219
|
+
Microsoft's own answers say it twice: *"Simply registering app registrations in
|
|
220
|
+
Entra ID does not incur any cost … as long as you're within the limits of your
|
|
221
|
+
Entra ID license (Free, P1, or P2)"*, and *"you can continue to use these
|
|
222
|
+
credentials … after your trial ends, as long as the tenant and the app registration
|
|
223
|
+
remain active"*.
|
|
224
|
+
|
|
225
|
+
So if a portal page offers **"Microsoft Entra ID P2 — one month free, then $10.50
|
|
226
|
+
per user per month"**, decline it. It is a premium add-on for enterprise identity
|
|
227
|
+
features; nothing in this package uses them. If you already activated a P2 trial,
|
|
228
|
+
just let it lapse — the app registration keeps working on Free.
|
|
229
|
+
|
|
230
|
+
### There is no card-free route to a tenant
|
|
231
|
+
|
|
232
|
+
Stated once, plainly, because it saves a lot of wasted effort:
|
|
233
|
+
|
|
234
|
+
- **The Azure free account requires a card.** Microsoft's supported-payment-methods
|
|
235
|
+
page excludes **prepaid** and virtual cards; a non-prepaid Visa/Mastercard — and in
|
|
236
|
+
some regions a non-prepaid **UnionPay** debit or credit card — is required, with a
|
|
237
|
+
billing address matching the card.
|
|
238
|
+
- **A free or trial tenant cannot create another tenant.** Microsoft: *"Only paid
|
|
239
|
+
customers can create a new Workforce tenant … Customers using a free tenant, or a
|
|
240
|
+
trial subscription won't be able to create additional tenants."* So there is no
|
|
241
|
+
back door from inside the portal.
|
|
242
|
+
- **The Developer Program sandbox is gated** to paying program members (see above).
|
|
243
|
+
|
|
244
|
+
No card, no tenant. No tenant, no app registration. No app registration, no OAuth2,
|
|
245
|
+
and personal `outlook.com`/`hotmail.com` has no password fallback. That is the whole
|
|
246
|
+
chain, and it breaks at the first link.
|
|
247
|
+
|
|
248
|
+
**Consequence:** if you cannot or will not attach a card, a personal Microsoft
|
|
249
|
+
mailbox cannot be used by this package. Use QQ Mail, Foxmail, 163 or 126 with a 授权码
|
|
250
|
+
instead — no card, no tenant, no OAuth — and keep the Microsoft address as the one
|
|
251
|
+
you *read* mail on (`mail.to`).
|
|
252
|
+
|
|
253
|
+
### What happens when the Azure free trial ends
|
|
254
|
+
|
|
255
|
+
The free account gives 30 days and a credit. If you do **not** upgrade to
|
|
256
|
+
pay-as-you-go, the *subscription* expires — but the **directory survives**, and
|
|
257
|
+
Entra ID Free keeps working, so the app registration and its refresh tokens
|
|
258
|
+
continue to function. That is the documented separation between a subscription and
|
|
259
|
+
its associated directory.
|
|
260
|
+
|
|
261
|
+
The thing to keep alive is the *billing account*, not the subscription's resources.
|
|
262
|
+
Nothing in this package consumes Azure resources, so there is nothing to bill.
|
|
263
|
+
|
|
264
|
+
### You do not need the $200 credit, and nothing must run in 30 days
|
|
265
|
+
|
|
266
|
+
The free account is the cheapest way to obtain a **directory**. The $200 credit is
|
|
267
|
+
for Azure *resources* — virtual machines, storage, databases — and this package uses
|
|
268
|
+
**none of them**. The whole requirement is the tenant that Azure creates as a side
|
|
269
|
+
effect of the signup.
|
|
270
|
+
|
|
271
|
+
So:
|
|
272
|
+
|
|
273
|
+
- **Create no Azure resources.** Not one. There is nothing to deploy; the daemon runs
|
|
274
|
+
on your own machine.
|
|
275
|
+
- **Losing the unused credit is irrelevant.** It expires after 30 days and you will
|
|
276
|
+
never miss it, because you were never going to spend it.
|
|
277
|
+
- **You do not have to “use” Azure within 30 days.** Signing up and registering the
|
|
278
|
+
app is the whole engagement.
|
|
279
|
+
- The card is an identity check, not a purchase. With no resources provisioned there
|
|
280
|
+
is nothing to charge.
|
|
281
|
+
|
|
282
|
+
If you want the directory to be maximally durable, the cheap insurance is: at the
|
|
283
|
+
30-day mark, upgrade to **pay-as-you-go** *without creating anything*. A subscription
|
|
284
|
+
with zero resources bills zero, and it removes any doubt about the directory's
|
|
285
|
+
longevity. Doing nothing also works per Microsoft's documentation — the directory
|
|
286
|
+
outlives the subscription — but an active, empty subscription is the less ambiguous
|
|
287
|
+
state.
|
|
288
|
+
|
|
289
|
+
## Configuration
|
|
290
|
+
|
|
291
|
+
```json
|
|
292
|
+
{
|
|
293
|
+
"mail": {
|
|
294
|
+
"imap": { "host": "outlook.office365.com", "port": 993, "secure": true,
|
|
295
|
+
"user": "you@outlook.com", "password": "", "authType": "XOAUTH2" },
|
|
296
|
+
"smtp": { "host": "smtp-mail.outlook.com", "port": 587, "secure": false,
|
|
297
|
+
"user": "you@outlook.com", "password": "", "authType": "XOAUTH2" },
|
|
298
|
+
"to": "you@outlook.com"
|
|
299
|
+
},
|
|
300
|
+
"security": {
|
|
301
|
+
"allowFrom": ["you@outlook.com"],
|
|
302
|
+
"allowSelfReplies": true,
|
|
303
|
+
"requireDmarcPass": false
|
|
304
|
+
},
|
|
305
|
+
"oauth": {
|
|
306
|
+
"issuer": "https://login.microsoftonline.com/consumers/v2.0",
|
|
307
|
+
"clientId": "<application (client) id>",
|
|
308
|
+
"scope": "offline_access https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/SMTP.Send"
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Presets (`core/discover.ts`, where `setup` reads them):
|
|
314
|
+
|
|
315
|
+
| Preset | `issuer` | `scope` |
|
|
316
|
+
| --- | --- | --- |
|
|
317
|
+
| **Microsoft personal** | `https://login.microsoftonline.com/consumers/v2.0` | `offline_access https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/SMTP.Send` |
|
|
318
|
+
| **Microsoft 365** | `https://login.microsoftonline.com/organizations/v2.0` | same as above |
|
|
319
|
+
| **Google** | `https://accounts.google.com` | `https://mail.google.com/ offline_access` |
|
|
320
|
+
|
|
321
|
+
`offline_access` is not optional — without it the provider returns no refresh
|
|
322
|
+
token and you would re-approve every hour. `--check` reports it as a problem, with a non-zero exit.
|
|
323
|
+
|
|
324
|
+
### Scopes are not interchangeable
|
|
325
|
+
|
|
326
|
+
Two different worlds, and mixing them is the most common cause of "the token
|
|
327
|
+
works but the server rejects it":
|
|
328
|
+
|
|
329
|
+
- **IMAP/SMTP XOAUTH2** needs `https://outlook.office.com/IMAP.AccessAsUser.All`
|
|
330
|
+
and `https://outlook.office.com/SMTP.Send`
|
|
331
|
+
- **Graph API** needs `https://graph.microsoft.com/Mail.Read` / `Mail.Send`
|
|
332
|
+
|
|
333
|
+
## The login flow
|
|
334
|
+
|
|
335
|
+
`pi-mailbridge --auth` prints a URL and a code, then waits:
|
|
336
|
+
|
|
337
|
+
```
|
|
338
|
+
Approve this login:
|
|
339
|
+
https://microsoft.com/devicelogin?code=ABCD1234
|
|
340
|
+
Code: ABCD1234
|
|
341
|
+
|
|
342
|
+
Waiting for approval…
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
Approve from **any device** — phone, tablet, the laptop you actually have open.
|
|
346
|
+
That is the point of device code: no browser, no redirect listener, no port on the
|
|
347
|
+
daemon's machine.
|
|
348
|
+
|
|
349
|
+
The refresh token is stored in `$XDG_STATE_HOME/pi-mailbridge/oauth.json` (default `~/.local/state/…`) at mode
|
|
350
|
+
`0600`. Access tokens are cached in memory and refreshed shortly before expiry.
|
|
351
|
+
|
|
352
|
+
On the SMTP side, one 401 triggers exactly one token refresh and one retry. On the IMAP
|
|
353
|
+
side there is no such invalidation: a rejected token fails the dial and the watcher
|
|
354
|
+
re-dials on the normal backoff (capped at 60 s), so a revoked grant shows up as repeating
|
|
355
|
+
auth errors in `daemon.log` rather than a single failure - and the daemon does not stop.
|
|
356
|
+
The remedy is `/mailbridge auth` (re-authorise) followed by `/mailbridge restart`.
|
|
357
|
+
|
|
358
|
+
## When IMAP/SMTP OAuth2 still fails
|
|
359
|
+
|
|
360
|
+
### Enabling IMAP/SMTP in Outlook.com does not restore password login
|
|
361
|
+
|
|
362
|
+
Worth stating plainly, because it is the obvious thing to try: the toggle
|
|
363
|
+
(*Settings → Mail → Forwarding and IMAP → "Let devices and apps use IMAP"*) makes
|
|
364
|
+
the **protocols available**. It does not change **authentication**.
|
|
365
|
+
|
|
366
|
+
Microsoft's own [POP, IMAP and SMTP settings page](https://support.microsoft.com/en-us/outlook/pop-imap-and-smtp-settings-for-outlook-com)
|
|
367
|
+
says both things in adjacent sentences:
|
|
368
|
+
|
|
369
|
+
> POP & IMAP access is **disabled by default**. See the section below on how to
|
|
370
|
+
> enable POP or IMAP access in Outlook.com.
|
|
371
|
+
|
|
372
|
+
> **Outlook.com requires the use of Modern Auth / OAuth2.**
|
|
373
|
+
|
|
374
|
+
and its settings table lists the authentication method for every protocol:
|
|
375
|
+
|
|
376
|
+
| Protocol | Server | Authentication Method |
|
|
377
|
+
| --- | --- | --- |
|
|
378
|
+
| IMAP | `outlook.office365.com:993` | **OAuth2/Modern Auth** |
|
|
379
|
+
| POP | `outlook.office365.com:995` | **OAuth2/Modern Auth** |
|
|
380
|
+
| SMTP | `smtp-mail.outlook.com:587` | **OAuth2/Modern Auth** |
|
|
381
|
+
|
|
382
|
+
So enabling the toggle is necessary and not sufficient: it opens the port, and
|
|
383
|
+
OAuth2 is still the only way in.
|
|
384
|
+
|
|
385
|
+
**Why the internet disagrees with itself.** That same page also carries a stale
|
|
386
|
+
sentence — *"if your password is not being recognized … you may need an app
|
|
387
|
+
password"* — left over from before the shutdown. That sentence is why forum
|
|
388
|
+
threads confidently report that app passwords still work. Field reports split on
|
|
389
|
+
SMTP specifically (some clients still succeed with an app password, Microsoft's
|
|
390
|
+
Q&A said in 2025 that they were disabled, n8n's docs say not to rely on them). For
|
|
391
|
+
IMAP there is no ambiguity: **app passwords are not a workaround**.
|
|
392
|
+
|
|
393
|
+
That is what settles it here, because IMAP is how a reply reaches you. A tool that
|
|
394
|
+
could only send would not be this tool.
|
|
395
|
+
|
|
396
|
+
#### What to actually do about a personal Outlook mailbox
|
|
397
|
+
|
|
398
|
+
1. **Get an Entra tenant** (free Azure signup) and register the app — the OAuth
|
|
399
|
+
route. Enabling IMAP/SMTP was prerequisite one; the tenant is prerequisite two.
|
|
400
|
+
2. **Or use a provider that still takes a password** — Gmail app password, or
|
|
401
|
+
QQ/163 with a 授权码. Both avoid OAuth entirely.
|
|
402
|
+
|
|
403
|
+
Given the tenant friction *and* the server-side refusal reports below, option 2 is
|
|
404
|
+
usually the shorter path.
|
|
405
|
+
|
|
406
|
+
### The server may refuse a valid token
|
|
407
|
+
|
|
408
|
+
This is the honest part, and it applies to **personal** `outlook.com` only:
|
|
409
|
+
|
|
410
|
+
| Symptom | Cause | Fix |
|
|
411
|
+
| --- | --- | --- |
|
|
412
|
+
| `User is authenticated but not connected` after a successful OAuth2 flow | a server-side IMAP regression for personal accounts reported since Dec 2024 | use the Graph API |
|
|
413
|
+
| `535 Authentication unsuccessful, SmtpClientAuthentication is disabled for the Mailbox` | SMTP AUTH disabled at the mailbox level, even though IMAP works | use the Graph API |
|
|
414
|
+
| Works on IMAP but not SMTP, or vice versa | per-protocol policy | use the Graph API for both |
|
|
415
|
+
|
|
416
|
+
**The reliable answer for personal Outlook is Graph** (`POST /me/sendMail`,
|
|
417
|
+
`GET /me/mailFolders/inbox/messages/delta`) because it never speaks SMTP. It is a
|
|
418
|
+
free API, the same OAuth token works, and it needs no extra package — Node 22 has
|
|
419
|
+
global `fetch`. It is not implemented in this package yet; see the *Status* section of the README.
|
|
420
|
+
|
|
421
|
+
Microsoft 365 (work) accounts usually work fine on IMAP/SMTP XOAUTH2.
|
|
422
|
+
|
|
423
|
+
## Alternative: a token command
|
|
424
|
+
|
|
425
|
+
If you already have a token broker, skip the built-in flow:
|
|
426
|
+
|
|
427
|
+
```json
|
|
428
|
+
"authType": "XOAUTH2",
|
|
429
|
+
"tokenCommand": ["ortie", "token", "show", "-a", "outlook"]
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
The command is executed as argv (never a shell), its stdout is the token, and the
|
|
433
|
+
result is cached until near expiry. Three consecutive failures disable OAuth2 for the
|
|
434
|
+
session rather than spawning a failing process on every scan; a single transient failure is
|
|
435
|
+
retried.
|
|
436
|
+
|
|
437
|
+
Precedence: **`oauth.*` beats `tokenCommand`**. Remove the `oauth` block to switch
|
|
438
|
+
back.
|
|
439
|
+
|
|
440
|
+
## Security
|
|
441
|
+
|
|
442
|
+
- The refresh token is equivalent to your password for mail. It is `0600` inside a
|
|
443
|
+
`0700` directory and is never logged, printed, or included in an email.
|
|
444
|
+
- Revoke access at any time: Microsoft **My Account → Privacy → App permissions**
|
|
445
|
+
(or Google **Security → Third-party access**). The stored token then stops
|
|
446
|
+
working and `--auth` is needed again.
|
|
447
|
+
- Rotating `secret.key` does **not** revoke OAuth; they are separate secrets.
|
|
448
|
+
- Prefer a dedicated mailbox for this package, as with everything else here.
|