@griffinwork40/outlook-cli 0.0.0-stage → 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Griffin Long
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,325 @@
1
- # Temporary Holding Version
1
+ # outlook-cli
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A lightweight CLI wrapper for the **Microsoft Graph v1.0 Outlook Mail API** —
4
+ read, search, send, reply, forward, draft, organize, and handle attachments
5
+ from the shell or from an agent.
6
+
7
+ Built so an AI agent can do everything email-related through a Microsoft
8
+ account without a browser.
9
+
10
+ ---
11
+
12
+ ## Quick start
13
+
14
+ ```bash
15
+ pnpm install && pnpm build
16
+
17
+ # one-time: register an Azure app and export its client id (see SETUP.md)
18
+ export GRAPH_CLIENT_ID=<application-client-id>
19
+
20
+ outlook auth login # device-code sign-in, ~30 seconds
21
+ outlook me # confirm which mailbox you are on
22
+ outlook list # newest inbox messages
23
+ ```
24
+
25
+ **First time?** Read [SETUP.md](./SETUP.md). The Azure app registration is the
26
+ only step that cannot be automated, and it takes about three minutes.
27
+
28
+ ---
29
+
30
+ ## Safety model
31
+
32
+ Email is irreversible and reaches other people. Six deliberate guardrails:
33
+
34
+ **1. `--dry-run` on every mutating command.** Prints the exact HTTP request that
35
+ would be issued, then exits without touching the network. The short-circuit
36
+ lives in one shared chokepoint (`guardMutation`), not in each command, so no
37
+ command can forget it.
38
+
39
+ ```bash
40
+ outlook send --to a@example.com --subject "Test" --body "Hi" --dry-run
41
+ ```
42
+ ```
43
+ DRY RUN — nothing was sent. The request below is what would go out:
44
+
45
+ POST https://graph.microsoft.com/v1.0/me/sendMail
46
+ Content-Type: application/json
47
+
48
+ {
49
+ "message": { "subject": "Test", ... },
50
+ "saveToSentItems": true
51
+ }
52
+ ```
53
+
54
+ **2. `delete` is recoverable; `purge` is not and is gated.** `outlook delete`
55
+ *moves* the message to Deleted Items, where you can get it back. Microsoft's
56
+ documentation never states whether a raw `DELETE` is recoverable, so that path
57
+ is exposed separately as `outlook purge` and refuses to run without `--yes`.
58
+
59
+ **3. Sends are never retried.** `sendMail`, `reply`, `replyAll`, `forward` and
60
+ draft-send carry no idempotency guarantee — an automatic retry after a network
61
+ timeout can deliver the message twice. The HTTP layer retries idempotent verbs
62
+ (GET/DELETE/PATCH) on throttling, and deliberately does not retry POST.
63
+
64
+ **4. Unknown flags are rejected, not ignored.** A misspelled flag is a hard
65
+ error before anything runs. This matters more than it sounds: with a
66
+ permissive parser, `--dryrun` is silently ignored, `dryRun` stays false, and
67
+ "show me what would be sent" becomes "send it now." Every command declares its
68
+ accepted flags in `src/flagSpec.ts`, and a test asserts no command can be
69
+ registered without an entry.
70
+
71
+ ```bash
72
+ outlook send --to a@example.com --subject "Test" --body "Hi" --dryrun
73
+ ```
74
+ ```
75
+ Unknown flag for 'send': --dryrun
76
+ Did you mean --dry-run?
77
+ ```
78
+
79
+ **5. Confirmation flags cannot be faked.** `--yes` accepts only explicit
80
+ boolean spellings. `--yes=false`, `--yes=0` and `--no-yes` all mean *no* — a
81
+ user typing `--yes=false` to decline a permanent delete gets a refusal, not a
82
+ deletion.
83
+
84
+ **6. Least privilege is available.** `outlook auth login --readonly` requests
85
+ `Mail.Read` instead of `Mail.ReadWrite` and drops `Mail.Send` entirely, so the
86
+ **access** token this session carries cannot send, delete, or modify mail. The
87
+ mode is recorded with the token and survives refreshes, and any mutating
88
+ command fails fast against a read-only session. Recommended whenever an agent
89
+ only needs to read.
90
+
91
+ It narrows the blast radius; it is not a substitute for protecting the token
92
+ file. The local read-only gate reads `mode` from that same file, and the cached
93
+ refresh token is only as narrow as the consent your tenant has already recorded
94
+ for this client — so someone holding the file can edit the mode back. Treat
95
+ `--readonly` as reducing what a leaked token is worth, not as sealing it.
96
+
97
+ ```bash
98
+ outlook auth login --readonly
99
+ outlook send --to a@example.com --subject S --body B
100
+ # This session was signed in with --readonly, so it cannot POST .../sendMail.
101
+ ```
102
+
103
+ Credentialed requests are additionally pinned to HTTPS on the Graph host, so a
104
+ malformed or hostile `@odata.nextLink` cannot redirect your bearer token to
105
+ another server. See [docs/security-review-2026-08-07.md](./docs/security-review-2026-08-07.md)
106
+ for the full audit and the two items deliberately left open.
107
+
108
+ ---
109
+
110
+ ## Restricted agent mode
111
+
112
+ An unattended agent that only files mail should not be able to send, edit
113
+ rules, or move messages anywhere it likes. Two opt-in controls narrow what the
114
+ CLI will do for it. With neither set, behavior is unchanged.
115
+
116
+ **1. A scoped token in its own cache.** `--scopes` requests exactly the listed
117
+ scopes (`offline_access` is added if missing) and records that set with the
118
+ token; `auth refresh` re-requests the same set rather than the default list, so
119
+ the grant never widens. `GRAPH_TOKEN_CACHE` keeps it apart from your main
120
+ login.
121
+
122
+ ```bash
123
+ export GRAPH_TOKEN_CACHE=~/.afk/state/outlook/agent-token.json
124
+ outlook auth login --scopes Mail.ReadWrite
125
+ outlook auth status --json # "grantScopes": ["offline_access", "Mail.ReadWrite"]
126
+ ```
127
+
128
+ **2. A mutation policy.** `OUTLOOK_POLICY=<path>` points at a JSON file that
129
+ every mutating request is checked against, inside the same `guardMutation`
130
+ chokepoint that implements `--dry-run`. Reads are never affected.
131
+
132
+ ```json
133
+ {
134
+ "$comment": "mail-filing agent: two destinations, nothing else",
135
+ "allowCommands": ["move", "mark-read"],
136
+ "move": {
137
+ "allowedDestinations": ["AAMkAGI2...ReviewFolderId", "archive"],
138
+ "denySenders": {
139
+ "file": "protected_senders.json",
140
+ "maxAgeHours": 48,
141
+ "destinations": ["AAMkAGI2...ReviewFolderId"]
142
+ }
143
+ }
144
+ }
145
+ ```
146
+
147
+ | Key | Effect |
148
+ |---|---|
149
+ | `allowCommands` | Mutating commands that may run. `"draft create"` allows one subcommand; `"draft"` allows them all. Anything else (send, reply, rules create, purge) is refused. |
150
+ | `move.allowedDestinations` | Folder ids or well-known names a move (including `delete`, which moves to `deleteditems`) may target. Well-known names match case-insensitively; ids match exactly, so list the form the agent will pass. |
151
+ | `move.denySenders` | Before a move into one of `destinations`, the CLI fetches the message's `from` (`GET /me/messages/{id}?$select=from`) and refuses if that address is in `file` (relative to the policy file): a JSON array of addresses, or `{"addresses": [...], "built_at": "<ISO time>"}`. |
152
+
153
+ It fails closed. A missing, unparseable or misspelled policy file (unknown keys
154
+ are errors) refuses every mutation. A deny list that is missing, unparseable,
155
+ or older than `maxAgeHours` (by `built_at`, else file mtime) refuses moves to
156
+ its destinations. A message with no sender address is refused for those
157
+ destinations, and so is a message id that would change the request path
158
+ (`../`, `%2e%2e`, backslashes).
159
+
160
+ Refusals exit with code **4** and, under `--json`, print
161
+ `{"error": {"code": "PolicyRefused", "rule": "...", "message": "..."}}` to
162
+ stdout. `--dry-run` still prints the request without any network call and adds
163
+ the policy's verdict; the deny-list sender lookup is a network call, so a dry
164
+ run reports it as skipped.
165
+
166
+ ```bash
167
+ export OUTLOOK_POLICY=~/.afk/config/agent-policy.json
168
+ outlook send --to a@example.com --subject S --body B
169
+ # Refused by mutation policy (allowCommands): 'send' is not in allowCommands (exit 4)
170
+ outlook move <id> --folder inbox --dry-run
171
+ # ...request...
172
+ # Policy (/home/me/.afk/config/agent-policy.json): would REFUSE (move.allowedDestinations): ...
173
+ ```
174
+
175
+ The policy is a guard inside the CLI, not an operating-system boundary. A
176
+ process that can run its own code as your user can read the token cache and
177
+ call Graph directly, or unset the variable. Pair it with a scoped token (a
178
+ `Mail.ReadWrite`-only token cannot send mail or edit inbox rules even outside
179
+ the CLI), and keep the policy and deny list files out of the agent's write
180
+ reach.
181
+
182
+ ---
183
+
184
+ ## Commands
185
+
186
+ ### Auth
187
+ | Command | Purpose |
188
+ |---|---|
189
+ | `outlook auth login` | Device-code sign-in (`--readonly`, or `--scopes a,b` for an exact scope set) |
190
+ | `outlook auth status` | Signed-in identity and token expiry (token never printed) |
191
+ | `outlook auth refresh` | Force a token refresh |
192
+ | `outlook auth logout` | Delete the cached token |
193
+
194
+ A plain `outlook auth login` requests `offline_access`, `User.Read`,
195
+ `Mail.ReadWrite`, `Mail.Send`, `MailboxSettings.ReadWrite`, `Contacts.Read`,
196
+ `Calendars.Read`, `Files.Read.All` and `Sites.Read.All`. The last three are
197
+ read-only and let the same token read calendar events and search or download
198
+ OneDrive/SharePoint files through Graph. `Files.Read.All` and `Sites.Read.All`
199
+ are delegated permissions that need **admin consent** in the Azure app
200
+ registration on work/school tenants; `Calendars.Read` does not. See
201
+ [SETUP.md](./SETUP.md#admin-consent-for-filesreadall-and-sitesreadall).
202
+ `--readonly` keeps its smaller mail-only set.
203
+
204
+ ### Reading
205
+ | Command | Purpose |
206
+ |---|---|
207
+ | `outlook me` | Your profile and primary address |
208
+ | `outlook list [--folder <name\|id>] [--unread] [--all] [--limit N]` | List messages |
209
+ | `outlook get <messageId> [--html]` | Full message, plain text by default |
210
+ | `outlook search <kql> [--folder <name\|id>] [--limit N]` | KQL search across the mailbox |
211
+ | `outlook folders list` | Mail folders with unread/total counts |
212
+ | `outlook attachments list <messageId>` | Attachments on a message |
213
+ | `outlook attachments download <messageId> <attachmentId> --out <path> [--force]` | Save an attachment |
214
+
215
+ Folder arguments accept the well-known names — `inbox`, `drafts`, `sentitems`,
216
+ `deleteditems`, `archive`, `junkemail`, and others — or a raw folder id.
217
+
218
+ ### Sending
219
+ | Command | Purpose |
220
+ |---|---|
221
+ | `outlook send --to <addr> --subject <s> [--body <text> \| --body-file <path>] [--cc] [--bcc] [--html] [--attach <path>...] [--signature <path> \| --no-signature] [--no-save-sent]` | Send a message |
222
+ | `outlook reply <messageId> --body <text>` | Reply to the sender |
223
+ | `outlook reply-all <messageId> --body <text>` | Reply to everyone |
224
+ | `outlook forward <messageId> --to <addr> [--body <text>]` | Forward |
225
+
226
+ `--to`, `--cc`, `--bcc` and `--attach` are repeatable, and address flags also
227
+ accept comma-separated lists.
228
+
229
+ ### Signatures
230
+
231
+ Microsoft Graph never adds your Outlook signature to mail sent through the API,
232
+ so the CLI appends one itself. Point it at a file once:
233
+
234
+ ```bash
235
+ echo 'OUTLOOK_SIGNATURE_FILE=/Users/you/.config/outlook/signature.txt' >> ~/.afk/config/afk.env
236
+ echo 'OUTLOOK_SIGNATURE_HTML_FILE=/Users/you/.config/outlook/signature.html' >> ~/.afk/config/afk.env # optional
237
+ ```
238
+
239
+ - **On by default once configured** for `send`, `draft create`, `reply`,
240
+ `reply-all` and `forward`. `--no-signature` skips it for one message;
241
+ `--signature <path>` uses a different file (`.html`/`.htm` = HTML).
242
+ - **Plain-text bodies** get the RFC 3676 `-- ` delimiter and the text signature.
243
+ **HTML bodies** (`--html`) get the HTML signature, or the text one escaped.
244
+ An HTML-only setup with a plain-text body is an error, not a silent skip.
245
+ - **Replies and forwards** use Graph's plain-text `comment`, so they take the
246
+ text signature, placed above the quoted original.
247
+ - **No `--body`:** a reply, reply-all or forward without `--body` still sends the signature on its own, as Outlook does when you sign an empty reply.
248
+ - **`draft update` never adds one** (it would duplicate the create-time copy).
249
+ - **Visible, never silent:** `--dry-run` shows the appended text and `--json`
250
+ reports `signatureApplied`. An unreadable signature file is a hard error.
251
+ - `~/.afk/config/afk.env` is shared by every afk agent on the machine, so any
252
+ agent sending through this CLI will sign as you. Export the variable per
253
+ shell instead if that is not what you want.
254
+ - **HTML signature files are trusted and inserted verbatim** (no sanitisation).
255
+ Treat them like the token cache: restrict write access to the file owner
256
+ (`chmod 600 ~/sig.html`). Do not point the variable at world-writable paths.
257
+
258
+ ### Drafts
259
+ | Command | Purpose |
260
+ |---|---|
261
+ | `outlook draft list` | List drafts |
262
+ | `outlook draft create --subject <s> [--to ...] [--body ...] [--attach ...]` | Create a draft |
263
+ | `outlook draft update <draftId> [--subject] [--body] [--to]` | Edit a draft |
264
+ | `outlook draft send <draftId>` | Send an existing draft |
265
+
266
+ Drafts are the safest way to send: compose, inspect, then send as a separate act.
267
+
268
+ ### Organizing
269
+ | Command | Purpose |
270
+ |---|---|
271
+ | `outlook delete <messageId>` | Move to Deleted Items (**recoverable**) |
272
+ | `outlook purge <messageId> --yes` | Hard delete (**may be unrecoverable**) |
273
+ | `outlook move <messageId> --folder <name\|id>` | Move to a folder |
274
+ | `outlook mark-read <messageId>` / `mark-unread` | Set read state |
275
+ | `outlook flag <messageId>` / `unflag` | Set follow-up flag |
276
+ | `outlook folders create --name <text>` | Create a folder (Graph creates at root only) |
277
+
278
+ ### Global flags
279
+ | Flag | Effect |
280
+ |---|---|
281
+ | `--json` | Machine-readable output on every command |
282
+ | `--dry-run` | Render the request, send nothing (mutating commands) |
283
+ | `--help` / `-h` | Usage for the command |
284
+
285
+ Pagination flags are **not** global — each command declares its own flags and
286
+ rejects anything else (see "Unknown flags fail closed" above). `--all` is
287
+ accepted by `list`, `search`, `folders`, `draft`, and `attachments`; `--limit N`
288
+ by `list`, `search`, and `draft`. The authoritative list is `COMMAND_FLAGS` in
289
+ [`src/flagSpec.ts`](./src/flagSpec.ts), and `outlook <command> --help` prints it.
290
+
291
+ ---
292
+
293
+ ## Notes and limits
294
+
295
+ - **Attachments are inline only, under 3 MB.** Larger files need a Graph upload
296
+ session, which is not implemented; the CLI fails loudly with the actual file
297
+ size rather than truncating.
298
+ - **Message ids change when a message moves between folders.** All reads send
299
+ `Prefer: IdType="ImmutableId"` so stored ids stay stable, but an id captured
300
+ before a `move` should not be reused after it.
301
+ - **`$search` and `$orderby` are not combined.** Microsoft's docs do not confirm
302
+ that pairing for messages, so `search` omits ordering rather than emitting a
303
+ request that may be rejected.
304
+ - **`$orderby` must agree with `$filter`.** Graph returns `InefficientFilter`
305
+ when ordering references properties the filter does not; the CLI surfaces that
306
+ with an explanation.
307
+
308
+ ## Development
309
+
310
+ ```bash
311
+ pnpm test # file-size gate + vitest
312
+ pnpm typecheck # tsc --noEmit
313
+ pnpm build # tsup -> dist/cli.js
314
+ pnpm test:coverage
315
+ ```
316
+
317
+ Tests never touch the network and never send mail: the HTTP layer is injected,
318
+ and command tests run against a real client whose `fetch` throws, so any
319
+ unguarded network call fails the suite instead of quietly passing.
320
+
321
+ **No source file may exceed 350 lines** — enforced by
322
+ `scripts/check-file-size.sh`, wired into `pnpm test`. At the ceiling, extract a
323
+ concern into a new file rather than compressing an existing one.
324
+
325
+ API reference notes gathered during the build live in `docs/`.
package/SETUP.md ADDED
@@ -0,0 +1,199 @@
1
+ # Setup — one-time Azure app registration
2
+
3
+ `outlook-cli` talks to your mailbox through the Microsoft Graph API. Microsoft
4
+ requires every Graph client to be a registered application, so there is exactly
5
+ one step a human has to do that no script can do for you: **register an app and
6
+ copy its client ID.**
7
+
8
+ It takes about three minutes, costs nothing, and needs no Azure subscription —
9
+ a plain Microsoft account (outlook.com, hotmail, live) or a work/school account
10
+ is enough.
11
+
12
+ ---
13
+
14
+ ## Why this step exists
15
+
16
+ There is no "just give me an API key" path for Outlook mail. Microsoft issues
17
+ credentials to *applications*, not to users, and a token is only ever minted
18
+ after you personally sign in and consent. That is also what keeps this safe:
19
+ the CLI never sees your password, and you can revoke its access at any time
20
+ without changing your password.
21
+
22
+ The client ID you create is **not a secret**. It identifies the app, not you.
23
+ The device-code flow this CLI uses is designed for exactly this case — a
24
+ command-line tool on a machine you control, with no client secret to leak.
25
+
26
+ ---
27
+
28
+ ## Step 1 — Open the app registration page
29
+
30
+ Go to <https://entra.microsoft.com> → **Applications** → **App registrations**.
31
+
32
+ Direct link: <https://entra.microsoft.com/#view/Microsoft_AAD_RegisteredApps/ApplicationsListBlade>
33
+
34
+ (The older <https://portal.azure.com> → *Azure Active Directory* → *App
35
+ registrations* path leads to the same screen.)
36
+
37
+ Sign in with the Microsoft account whose mail you want the CLI to read.
38
+
39
+ ## Step 2 — New registration
40
+
41
+ Click **+ New registration**.
42
+
43
+ - **Name:** anything — `outlook-cli` is fine. Only you ever see it.
44
+ - **Supported account types:** choose
45
+
46
+ > **Accounts in any organizational directory (Any Microsoft Entra ID tenant —
47
+ > Multitenant) and personal Microsoft accounts (e.g. Skype, Xbox)**
48
+
49
+ This is the option that makes one registration work for BOTH a personal
50
+ outlook.com mailbox and a work/school Microsoft 365 mailbox. Picking a
51
+ narrower option is the single most common reason sign-in later fails with
52
+ *"unauthorized_client"*.
53
+ - **Redirect URI:** leave it **blank**. The device-code flow does not use one.
54
+
55
+ Click **Register**.
56
+
57
+ ## Step 3 — Copy the client ID
58
+
59
+ On the app's **Overview** page, copy **Application (client) ID**. It looks like:
60
+
61
+ ```
62
+ 11111111-2222-3333-4444-555555555555
63
+ ```
64
+
65
+ That is your `GRAPH_CLIENT_ID`.
66
+
67
+ ## Step 4 — Turn on public client flows
68
+
69
+ This is the step people miss, and it is required.
70
+
71
+ Go to **Authentication** (left sidebar) → scroll to **Advanced settings** →
72
+ **Allow public client flows** → set to **Yes** → **Save**.
73
+
74
+ Without this, `outlook auth login` fails with:
75
+
76
+ ```
77
+ AADSTS7000218: The request body must contain the following parameter:
78
+ 'client_assertion' or 'client_secret'.
79
+ ```
80
+
81
+ That error means precisely this toggle is off — not that you need a secret.
82
+
83
+ ## Step 5 — Export the client ID and sign in
84
+
85
+ ```bash
86
+ export GRAPH_CLIENT_ID=11111111-2222-3333-4444-555555555555
87
+ outlook auth login
88
+ ```
89
+
90
+ You will see something like:
91
+
92
+ ```
93
+ To sign in, open https://microsoft.com/devicelogin and enter code ABCD-EFGH
94
+ ```
95
+
96
+ Open that URL, enter the code, sign in, and approve the permission list. The
97
+ CLI polls in the background and confirms once you are done. The token is cached
98
+ at `~/.afk/state/outlook/token.json` (mode `0600`) and refreshes itself from
99
+ then on — you should not need to log in again unless you revoke access.
100
+
101
+ Verify:
102
+
103
+ ```bash
104
+ outlook auth status
105
+ outlook me
106
+ ```
107
+
108
+ ---
109
+
110
+ ## Making it permanent
111
+
112
+ Add the export to your shell profile, or drop it in AFK's env file so every
113
+ session picks it up:
114
+
115
+ ```bash
116
+ echo 'GRAPH_CLIENT_ID=11111111-2222-3333-4444-555555555555' >> ~/.afk/config/afk.env
117
+ ```
118
+
119
+ Optional variables (see `.env.example`):
120
+
121
+ | Variable | Default | Purpose |
122
+ |---|---|---|
123
+ | `GRAPH_CLIENT_ID` | *(required)* | Application (client) ID from Step 3 |
124
+ | `GRAPH_TENANT_ID` | `common` | `common` supports personal **and** work accounts |
125
+ | `GRAPH_CLIENT_SECRET` | *(unset)* | Only for confidential-client mode; leave blank |
126
+ | `GRAPH_TOKEN_CACHE` | `~/.afk/state/outlook/token.json` | Token cache location |
127
+ | `OUTLOOK_SIGNATURE_FILE` | *(unset)* | Plain-text signature appended to outgoing mail (see README "Signatures") |
128
+ | `OUTLOOK_SIGNATURE_HTML_FILE` | *(unset)* | HTML signature used for `--html` bodies |
129
+
130
+ ---
131
+
132
+ ## What the CLI asks permission for
133
+
134
+ At first sign-in Microsoft shows a consent screen listing these scopes:
135
+
136
+ | Scope | Why it is needed |
137
+ |---|---|
138
+ | `offline_access` | Refresh the token so you sign in once, not daily |
139
+ | `User.Read` | Read your own profile — powers `outlook me` |
140
+ | `Mail.ReadWrite` | Read messages, manage drafts, flags, folders |
141
+ | `Mail.Send` | Send, reply, forward |
142
+ | `MailboxSettings.ReadWrite` | Read/write mailbox settings (needed for `rules create`) |
143
+ | `Contacts.Read` | Read your contacts — powers `outlook contacts` |
144
+ | `Calendars.Read` | Read your calendar events through Graph |
145
+ | `Files.Read.All` | Read OneDrive and SharePoint files you can access (search and download) |
146
+ | `Sites.Read.All` | Read SharePoint sites, documents and list items you can access |
147
+
148
+ These are **delegated** permissions: the CLI can only ever do what *you* can do,
149
+ and only while your token is valid. `outlook auth login --readonly` requests a
150
+ smaller mail-only set (`offline_access`, `User.Read`, `Mail.Read`,
151
+ `MailboxSettings.Read`) and none of the calendar or file scopes.
152
+
153
+ ### Admin consent for Files.Read.All and Sites.Read.All
154
+
155
+ `Files.Read.All` and `Sites.Read.All` are delegated permissions that need
156
+ **admin consent** in the Azure app registration on work/school tenants. Since
157
+ August 2025 Microsoft's default user-consent policy ("Let Microsoft manage your
158
+ consent settings") blocks users from consenting to them for third-party apps,
159
+ so sign-in fails at the consent screen until an admin grants them. A tenant
160
+ admin can do this once:
161
+
162
+ **App registrations** → your app → **API permissions** → **+ Add a permission**
163
+ → **Microsoft Graph** → **Delegated permissions** → add `Files.Read.All` and
164
+ `Sites.Read.All` (add the other scopes in the table too) → **Grant admin
165
+ consent for &lt;tenant&gt;**.
166
+
167
+ `Calendars.Read` does not need admin consent; like the mail scopes, the user
168
+ approves it on the sign-in consent screen. Personal Microsoft accounts
169
+ (outlook.com) have no tenant admin and consent to all of these scopes
170
+ themselves.
171
+
172
+ After upgrading from a version without these scopes, run `outlook auth logout`
173
+ then `outlook auth login` so the new token carries them.
174
+
175
+ ## Revoking access
176
+
177
+ <https://account.live.com/consent/Manage> (personal accounts) or your tenant's
178
+ *Enterprise applications* blade (work accounts). Revoking is immediate and does
179
+ not require deleting the app registration.
180
+
181
+ To just clear the local token:
182
+
183
+ ```bash
184
+ outlook auth logout
185
+ ```
186
+
187
+ ---
188
+
189
+ ## Troubleshooting
190
+
191
+ | Symptom | Cause | Fix |
192
+ |---|---|---|
193
+ | `AADSTS7000218 ... client_assertion or client_secret` | Public client flows off | Step 4 |
194
+ | `AADSTS700016 ... application not found` | Wrong or unset `GRAPH_CLIENT_ID` | Re-copy from Step 3 |
195
+ | `unauthorized_client` on a personal account | Registration is single-tenant | Re-do Step 2 with the multitenant + personal option |
196
+ | `Missing required environment variable(s): GRAPH_CLIENT_ID` | Not exported | Step 5 |
197
+ | `InvalidAuthenticationToken` on a call | Token expired and refresh failed | `outlook auth login` |
198
+ | Sign-in works, mail calls 403 | Consent was declined or partial | `outlook auth logout` then log in again and accept all scopes |
199
+ | Consent screen says "Need admin approval" (`AADSTS90094` / `AADSTS65001`) | Tenant blocks user consent to `Files.Read.All` / `Sites.Read.All` | Ask a tenant admin to grant admin consent (see "Admin consent" above), or sign in with `--readonly` / `--scopes` to skip those scopes |