Skip to content

Latest commit

 

History

History
1688 lines (1205 loc) · 68.3 KB

File metadata and controls

1688 lines (1205 loc) · 68.3 KB

Self-Hosting Guide

A step-by-step guide to setting up outlook-cli for yourself or your team. No prior Azure experience required.


Table of Contents

  1. Prerequisites
  2. Register an Azure App (5 minutes)
  3. Install outlook-cli
  4. Authenticate Your First Account
  5. Verify It Works
  6. Short IDs and Message References
  7. Common Operations
  8. Common Workflows
  9. Add More Accounts
  10. Configure Permissions
  11. Configuration Resolution
  12. Set Up Watch Mode
  13. OpenClaw / NanoClaw Integration
  14. Diagnostics — Logs, Telemetry, Doctor
  15. Rolling Out to Your Team
  16. Enterprise Deployment
  17. Troubleshooting Quick Reference
  18. Enterprise COM Bridge Deployment

1. Prerequisites

You need:

  • A Microsoft account — personal (Outlook.com), work (Microsoft 365), or school. If you have an email address ending in @outlook.com, @hotmail.com, @live.com, or your company uses Microsoft 365, you're set. Any account type that can sign in to outlook.office.com will work with outlook-cli — the CLI uses the same Microsoft Graph API that the Outlook web app uses.
  • Node.js 18+ (for the Node.js version) or .NET 8 SDK (for the .NET version). You only need one. The Node.js implementation runs as ES modules directly — there is no build or transpilation step. The .NET implementation compiles to a NativeAOT binary that needs no runtime on the target machine.
  • A computer running Windows, macOS, or Linux. All three platforms are tested in CI. The ~/.outlook-cli/ directory stores config, token caches, and operation logs in the same format across all platforms.

Cost: Everything here is free. The Azure App Registration is free. The Microsoft Graph API calls are free (included with any Microsoft account). outlook-cli is MIT licensed. There are no usage limits beyond Microsoft's standard Graph API throttling (which is generous for individual use — typically 10,000 requests per 10 minutes per user).


2. Register an Azure App

This is a one-time setup. Once registered, the same app can be shared by everyone in your organization.

What is an Azure App Registration?

When outlook-cli connects to your email, Microsoft needs to know which program is asking for access. An "App Registration" is like a name tag — it says "I'm outlook-cli, and I'd like to read this user's email." It's free and takes about 5 minutes.

A single App Registration can serve your entire organization — every user authenticates with their own Microsoft credentials, gets their own private token cache, and nobody can access anyone else's data. The Client ID is just an identifier (not a secret), so it's safe to share it in documentation, chat messages, or config files. This is why the multi-account setup (Section 9) reuses the same Client ID across personal, work, and shared accounts.

Option A: Use the Azure Portal (visual, point-and-click)

Step 2.1: Open the App Registrations page

Go to one of these URLs (they both go to the same place):

Sign in with your Microsoft account. If you see a "Welcome" screen or "Get started" page, look for "App registrations" in the left sidebar or search bar.

Can't find it? The Azure portal has a confusing number of menus. The direct link above is the fastest way. If even that doesn't work, try: portal.azure.com → hamburger menu (☰) → "Microsoft Entra ID" → "App registrations" in the left sidebar.

Step 2.2: Create a new registration

  1. Click the "+ New registration" button at the top

  2. Fill in the form:

    Field What to enter
    Name outlook-cli (or any name you want)
    Supported account types Select "Accounts in any organizational directory and personal Microsoft accounts" — this is the most flexible option and works for both personal and work accounts
    Redirect URI Leave this blank for now (we'll add it next)
  3. Click "Register"

Step 2.3: Copy your Client ID

After registering, you'll land on the app's Overview page. You'll see two important values:

  • Application (client) ID — a long string like a1b2c3d4-e5f6-7890-abcd-ef1234567890. Copy this. You'll need it in every step below.
  • Directory (tenant) ID — you can ignore this for now (outlook-cli defaults to common, which works for most people).

💡 Tip: Save the Client ID somewhere handy — a sticky note, a password manager, a text file. You'll use it once per computer you set up.

Step 2.4: Add the redirect URI

  1. In the left sidebar, click "Authentication"
  2. Click "+ Add a platform"
  3. Select "Mobile and desktop applications"
  4. Under Custom redirect URIs, type: http://localhost:53847/callback
  5. Click "Configure"
  6. Scroll down to "Advanced settings"
  7. Find "Allow public client flows" and set it to Yes
  8. Click "Save" at the top of the page

Step 2.5: Add API permissions

This tells Microsoft which data outlook-cli is allowed to access.

  1. In the left sidebar, click "API permissions"
  2. Click "+ Add a permission"
  3. Select "Microsoft Graph" (it's usually the first option)
  4. Select "Delegated permissions" (NOT "Application permissions")
  5. In the search box, search for and check each of these:

Required permissions (add all of these):

Permission What it does
User.Read Read your profile (usually already added)
Mail.Read Read your email
Mail.ReadWrite Create drafts, move messages, flag messages
Calendars.Read Read your calendar
Calendars.ReadWrite Create calendar events
offline_access Stay logged in (refresh tokens)

Optional — for delegate access (shared mailboxes):

Permission What it does
Mail.Read.Shared Read another user's mailbox (if they've granted you access)
Mail.ReadWrite.Shared Create drafts in another user's mailbox
Mail.Send.Shared Send on behalf of another user
Calendars.Read.Shared Read another user's calendar

Optional — for sending email:

Permission What it does
Mail.Send Send email from your own account

Optional — for OneDrive file access:

Permission What it does
Files.Read Browse and download files from OneDrive
Files.ReadWrite Upload files, create folders, create sharing links

Note: OneDrive scopes are opt-in — they are not requested during login unless you add them to your Azure App Registration. Add these only if you plan to use outlook-cli drive commands.

Optional — for contacts:

Permission What it does
Contacts.Read Read your contacts (used by contacts list, contacts search)
  1. Click "Add permissions"

⚠️ Security note: Mail.Send lets the CLI send email. If you don't need to send email from the CLI, leave it off. You can always add it later. The permission system in outlook-cli lets you control which accounts can send, even if the Azure app allows it.

🚫 Never add these: Mail.ReadWrite.All, Calendars.ReadWrite.All, or any permission ending in .All — those give application-level access to every user's mailbox, which is dangerous and unnecessary.

Option B: Use the PowerShell script (automated)

Run the provided script from the scripts/ directory:

# Basic setup (read + write email and calendar):
.\scripts\self-host-azure.ps1

# Include shared/delegate mailbox permissions:
.\scripts\self-host-azure.ps1 -IncludeShared

# Include direct send permission:
.\scripts\self-host-azure.ps1 -IncludeSend

# All permissions + custom app name:
.\scripts\self-host-azure.ps1 -AppName "my-outlook-tool" -IncludeSend -IncludeShared

The script:

  • Installs the Az PowerShell module if needed
  • Opens your browser for Azure login
  • Creates the app registration with the correct permissions
  • Outputs the Client ID you'll need for the next step

Open scripts/self-host-azure.ps1 to see exactly what it does. Every permission GUID has a comment explaining the permission name and what it grants.


3. Install outlook-cli

Choose one of the two implementations. They're fully interchangeable — same commands, same config, same token cache. A token cache encrypted by the Node.js implementation decrypts correctly in the .NET implementation and vice versa, because both use the same binary format: AES-256-GCM with PBKDF2-SHA512 key derivation at 310,000 iterations (see SECURITY.md for details). You can switch between implementations at any time without re-authenticating.

Option A: Node.js (easiest to get started)

# Clone the repository
git clone https://github.com/jeffstall/outlook-cli.git
cd outlook-cli

# Install dependencies
npm install

# Verify it works
node bin/outlook-cli.js --version

You'll run commands as node bin/outlook-cli.js <command>.

Tip: To use just outlook-cli instead of node bin/outlook-cli.js, run npm link (may need admin/sudo).

Option B: .NET Native Binary (fastest, no runtime needed)

Pre-built binaries are available for:

Platform File
Windows x64 publish/win-x64/outlook-cli.exe
Windows ARM64 publish/win-arm64/outlook-cli.exe
macOS Apple Silicon publish/osx-arm64/outlook-cli
Linux x64 publish/linux-x64/outlook-cli

To build from source:

# Clone the repository
git clone https://github.com/jeffstall/outlook-cli.git
cd outlook-cli

# Build a native binary for your platform (requires .NET 8 SDK)
# Windows x64:
dotnet publish src/dotnet/OutlookCli.csproj -c Release -r win-x64 --self-contained /p:PublishAot=true -o publish/win-x64

# macOS:
dotnet publish src/dotnet/OutlookCli.csproj -c Release -r osx-arm64 --self-contained /p:PublishAot=true -o publish/osx-arm64

# Linux:
dotnet publish src/dotnet/OutlookCli.csproj -c Release -r linux-x64 --self-contained /p:PublishAot=true -o publish/linux-x64

# Or build all platforms at once:
./build.ps1 dotnet -Publish -Runtime all

The resulting binary is ~10 MB, fully self-contained — no .NET runtime needed on the target machine.

You can also run without publishing (requires .NET 8 SDK on the machine):

dotnet run --project src/dotnet -- <command>

Note: When using dotnet run, put -- before your outlook-cli arguments to separate them from dotnet's own flags.

Which should I choose?

Node.js .NET Binary
Setup effort npm install dotnet publish (or download binary)
Runtime needed Node.js 18+ Nothing (self-contained)
Startup time ~200ms ~50ms
Binary size ~80 MB (with node_modules) ~10 MB
Best for Development, scripting Production, distribution

4. Authenticate Your First Account

Now connect outlook-cli to your Microsoft account.

Option A: Use the setup script (recommended)

# Basic setup — opens browser for Microsoft login:
.\scripts\self-host-client.ps1 -ClientId YOUR_CLIENT_ID

# Work/school account with specific tenant:
.\scripts\self-host-client.ps1 -ClientId YOUR_CLIENT_ID -AccountAlias "work" -Tenant "contoso.onmicrosoft.com"

# Force a specific implementation:
.\scripts\self-host-client.ps1 -ClientId YOUR_CLIENT_ID -Implementation dotnet

The script auto-detects whether to use Node.js or .NET, handles authentication, and verifies everything works. See scripts/self-host-client.ps1 for the full source.

Option B: Manual setup

On a computer with a web browser (most people):

# Node.js:
node bin/outlook-cli.js auth login --client-id YOUR_CLIENT_ID

# .NET:
outlook-cli.exe auth login --client-id YOUR_CLIENT_ID

This opens your browser. Sign in with your Microsoft account, approve the permissions, and you're done.

On a server or VM without a browser:

outlook-cli auth login --client-id YOUR_CLIENT_ID --device-code

This shows a message like:

To sign in, use a web browser to open https://microsoft.com/devicelogin
and enter the code ABCD-EFGH to authenticate.

Open that URL on any device (your phone, another computer), enter the code, and sign in. The CLI will automatically detect the login.

Set it permanently (so you don't type the client ID every time):

# Add a named account with the client ID baked in
outlook-cli account add personal --client-id YOUR_CLIENT_ID

# Then authenticate
outlook-cli auth login --account personal

5. Verify It Works

Run these commands to confirm everything is connected:

# Check authentication status
outlook-cli auth status
# Should show: Authenticated: ✓ yes

# List your inbox
outlook-cli mail inbox

# View today's calendar
outlook-cli calendar today

If you see your emails and calendar events, you're all set! The auth status command validates that your token cache can be decrypted and that the cached refresh token is still valid. If it shows "Authenticated: ✗ no", your token has expired — run outlook-cli auth login to re-authenticate.

Note: In the examples below, outlook-cli means either node bin/outlook-cli.js (Node.js) or ./outlook-cli.exe (.NET). The commands are identical.


6. Short IDs and Message References

How short IDs work

When you run mail inbox, mail search, or any command that lists messages, outlook-cli assigns each result a short numeric ID (#1, #2, #3, etc.) displayed alongside the message. These short IDs are a convenience feature so you don't need to copy and paste the full Microsoft Graph message IDs, which are opaque strings like AAMkAGI2TG93AAA=. You can use a short ID anywhere a full message ID is accepted — mail read 1, mail reply 3, mail move 2 --folder Archive, and so on. The # prefix is optional: both 1 and #1 work.

Where the mapping is stored

The mapping from short IDs to full Graph IDs is stored in ~/.outlook-cli/last-results.json. This file is overwritten entirely every time a list command runs — there is no append or merge. The file records a timestamp, the count of results, and a simple { "1": "AAMk...", "2": "AAMk..." } dictionary. Both the Node.js and .NET implementations read and write the same file in the same format.

The clobber problem

Because last-results.json is a single global file, any list command in any terminal window overwrites it. If you have two terminal windows open and run inbox in one, the short IDs from the other window's previous search are gone. This is by design — short IDs are meant for quick interactive use in a single terminal session, not for durable references.

Here's the most common way this bites you:

$ outlook-cli mail inbox          # Short IDs 1-7 now map to inbox messages
$ outlook-cli mail read 1         # ✓ Reads first inbox message

$ outlook-cli mail search "meeting"  # Short IDs 1-3 now map to search results!
$ outlook-cli mail read 1         # ⚠ Reads first SEARCH result, not inbox message

The second mail read 1 resolves to a completely different message because search overwrote the mapping that inbox created.

When to use full Graph IDs

For any of these scenarios, use the full Graph message ID instead of short IDs:

  • Scripts and automation — A cron job or agent should capture the full ID from --json output (e.g., outlook-cli mail inbox --json | jq '.[0].id') and pass it to subsequent commands. Short IDs are unreliable when multiple processes may run list commands.
  • Multi-window workflows — If you frequently switch between terminal windows, copy the full ID from the output or use --json to extract it.
  • CI/CD pipelines — Never rely on last-results.json in automated environments. Use --json output and parse the id field.

Checking what the current short IDs point to

You can inspect the mapping file directly:

cat ~/.outlook-cli/last-results.json
{
  "timestamp": "2026-01-15T14:32:01.000Z",
  "count": 7,
  "ids": {
    "1": "AAMkAGI2TG93AAA=",
    "2": "AAMkAGI2TG94BBB=",
    "3": "AAMkAGI2TG95CCC="
  }
}

If the timestamp is from a different terminal session or an unexpected command, the IDs may not be what you expect. When in doubt, re-run mail inbox to refresh the mapping.


7. Common Operations

📬 Reading Email

The mail inbox command fetches messages from the Microsoft Graph API's /me/mailFolders/inbox/messages endpoint. Results are sorted by received date (newest first) and each message is assigned a short numeric ID for use in subsequent commands (see Short IDs). By default, 25 messages are returned — use --top to change this limit.

# List your inbox (latest 25 messages)
outlook-cli mail inbox

# Show only unread messages
outlook-cli mail inbox --unread

# Show the latest 10
outlook-cli mail inbox --top 10

# List messages in a specific folder
outlook-cli mail inbox --folder "Sent Items"
outlook-cli mail inbox --folder "Archive"

# Read a specific email (use short ID from inbox output, or full Graph ID)
outlook-cli mail read 1
outlook-cli mail read MESSAGE_ID

# Read in plain text (strips HTML)
outlook-cli mail read MESSAGE_ID --plain

# Search for emails (uses Microsoft Graph $search, supports KQL syntax)
outlook-cli mail search "quarterly report"
outlook-cli mail search "from:alice@example.com"
outlook-cli mail search "hasAttachments:true"

# List all your mail folders
outlook-cli mail folders

Note: The search command also saves short IDs, overwriting the ones from inbox. If you need to reference both inbox and search results, capture the full IDs from --json output. See Short IDs for details.

✉️ Composing Email

Drafting and sending are separate operations in outlook-cli. The mail draft command creates a message in your Drafts folder without sending it — this is a safety feature, especially when agents or scripts compose email. The mail send command sends a previously-created draft, and requires Mail.Send in the account's allowed permissions (see Configure Permissions). The --yes flag skips the confirmation prompt, which is required for non-interactive use (scripts, agents).

# Create a draft (does NOT send — saves to Drafts folder)
outlook-cli mail draft --to "bob@example.com" --subject "Hello" --body "Hi Bob!"

# Draft with multiple recipients
outlook-cli mail draft \
  --to "bob@example.com,carol@example.com" \
  --cc "manager@example.com" \
  --subject "Project Update" \
  --body "Here's the latest..."

# Draft from an HTML file
outlook-cli mail draft --to "team@example.com" --subject "Newsletter" --body-file newsletter.html

# Reply to a message
outlook-cli mail reply MESSAGE_ID --body "Thanks for the update!"

# Reply to all
outlook-cli mail reply MESSAGE_ID --body "Acknowledged" --reply-all

# Forward a message
outlook-cli mail forward MESSAGE_ID --to "boss@example.com" --comment "FYI — see below"

# Send a draft (requires Mail.Send permission)
outlook-cli mail send DRAFT_ID

📁 Organizing Email

These commands modify messages in your mailbox. Each accepts either a short ID from the last list command or a full Graph message ID. The move command calls the Graph API's /messages/{id}/move endpoint and returns the message's new ID (Graph reassigns IDs when moving between folders). The flag and mark-read commands use PATCH requests to update message properties in place.

# Move a message to Archive
outlook-cli mail move MESSAGE_ID --folder Archive

# Move to a custom folder
outlook-cli mail move MESSAGE_ID --folder "Project Alpha"

# Flag a message for follow-up
outlook-cli mail flag MESSAGE_ID

# Unflag
outlook-cli mail flag MESSAGE_ID --unflag

# Mark as read
outlook-cli mail mark-read MESSAGE_ID

# Mark as unread
outlook-cli mail mark-read MESSAGE_ID --unread

📅 Calendar

# View today's events
outlook-cli calendar today

# View this week
outlook-cli calendar week

# View a date range
outlook-cli calendar range --start 2026-01-15 --end 2026-01-22

# View a specific event
outlook-cli calendar view EVENT_ID

# List all your calendars
outlook-cli calendar list-calendars

# Create an event
outlook-cli calendar create \
  --subject "Team Standup" \
  --start "2026-01-15T09:00:00" \
  --end "2026-01-15T09:30:00" \
  --attendees "alice@example.com,bob@example.com" \
  --location "Conference Room A"

👥 Contacts

# Search for a contact
outlook-cli contacts search "Alice Johnson"

# Set up an alias for quick access
outlook-cli contacts alias set alice alice.johnson@example.com

# Now use the alias anywhere
outlook-cli mail draft --to alice --subject "Quick question" --body "..."

📊 Output Formats

Every command supports multiple output formats via the --format flag (or the shorthand --json). The rendering engine uses a strategy pattern — render.js dispatches to format-specific modules (formatter.js, markdown.js, html.js) where each module exports functions named after entity types (mailList, mailDetail, eventList, etc.). The --output flag writes to a file instead of stdout. JSON output is the most important format for scripting and agent integration because it includes the full Graph API message IDs and all metadata.

# Human-readable text (default)
outlook-cli mail inbox

# JSON (for scripts, piping to jq, or agent integration)
outlook-cli mail inbox --json

# Markdown
outlook-cli mail inbox --format markdown

# HTML (great for saving to a file)
outlook-cli mail inbox --format html --output inbox.html

📋 JSON Input Files

For complex operations, use a JSON file instead of many flags. The --input flag loads parameters from a JSON file, but CLI flags always override JSON values — this lets you use a template file and selectively override fields on the command line. If the JSON includes a bodyFile path, it takes priority over an inline body value. HTML content type is auto-detected when the file extension is .html or .htm. See JSON-INPUT.md for the full schema and supported fields.

# Create draft.json:
cat > draft.json << 'EOF'
{
  "to": "bob@example.com,carol@example.com",
  "cc": "manager@example.com",
  "subject": "Weekly Status Report",
  "body": "Here's this week's update...\n\n- Feature A: Complete\n- Feature B: In progress",
  "importance": "high"
}
EOF

# Use it:
outlook-cli mail draft --input draft.json --yes --json

8. Common Workflows

These multi-step examples show realistic sequences you'd use in day-to-day work. Each step builds on the previous one using short IDs — remember that short IDs are reassigned whenever a list command runs (see Short IDs).

Read and reply to recent email

# Step 1: See what's in your inbox
$ outlook-cli mail inbox --top 5
#1  alice@example.com    "Q3 Budget Review"         2026-01-15 09:12
#2  bob@contoso.com      "Standup Notes"             2026-01-15 08:45
#3  carol@example.com    "Lunch tomorrow?"           2026-01-14 17:30

# Step 2: Read the full message
$ outlook-cli mail read 1
From: alice@example.com
Subject: Q3 Budget Review
Can you review the attached spreadsheet and confirm the numbers?

# Step 3: Reply
$ outlook-cli mail reply 1 --body "Reviewed — numbers look correct. Approved."
# Creates a reply draft. Add --yes to skip confirmation.

# Step 4: Send the reply (if Mail.Send is allowed)
$ outlook-cli mail send DRAFT_ID --yes

Search, flag, and move a message

# Step 1: Find the message
$ outlook-cli mail search "project alpha deadline"
#1  pm@contoso.com       "Project Alpha — deadline moved"   2026-01-14
#2  pm@contoso.com       "RE: Project Alpha timeline"       2026-01-10

# Step 2: Flag it for follow-up
$ outlook-cli mail flag 1

# Step 3: Move it to a project folder
$ outlook-cli mail move 1 --folder "Project Alpha"
# Note: after move, the message gets a new Graph ID. The short ID #1 still
# points to the old ID in last-results.json, so further operations on this
# message require the new ID from the move command's output.

Draft, review, and send

# Step 1: Create a draft
$ outlook-cli mail draft \
    --to "team@contoso.com" \
    --subject "Weekly Status" \
    --body-file status-update.html \
    --yes --json
# JSON output includes the draft's message ID

# Step 2: Review what you drafted
$ outlook-cli mail read DRAFT_ID

# Step 3: Send it
$ outlook-cli mail send DRAFT_ID --yes

Script-safe workflow with full IDs

When automating, always use --json and extract the full ID to avoid short ID clobber issues:

# Capture the full ID of the first inbox message
MSG_ID=$(outlook-cli mail inbox --json | jq -r '.[0].id')

# Now use it directly — this is immune to short ID overwrites
outlook-cli mail read "$MSG_ID"
outlook-cli mail flag "$MSG_ID"
outlook-cli mail move "$MSG_ID" --folder Archive --yes

9. Add More Accounts

You can manage multiple email accounts (personal + work, multiple work accounts, etc.):

# Add a personal Outlook.com account
outlook-cli account add personal --client-id YOUR_CLIENT_ID

# Add a work/school account (with specific tenant)
outlook-cli account add work --client-id YOUR_CLIENT_ID --tenant contoso.onmicrosoft.com

# Authenticate each account
outlook-cli auth login --account personal
outlook-cli auth login --account work

# Set a default account
outlook-cli account set-default work

# Use a specific account with any command
outlook-cli mail inbox --account personal
outlook-cli mail inbox --account work

# List all accounts
outlook-cli account list

Tip: Multiple users in your organization can share the same Client ID. The Client ID identifies the application (outlook-cli), not the user. Each person authenticates with their own Microsoft credentials and gets their own private token cache file (cache-{alias}.enc), encrypted with a machine-specific key or passphrase. No one can access anyone else's data — the Client ID is not a secret and can be shared freely in documentation or config files.

Multi-Account Examples

The --account (or -a) flag selects which account to use for a command. The --as flag accesses another user's mailbox via delegate permissions. Here's a complete walkthrough.

Default account behavior

When you don't specify --account, the default account is used:

# These are equivalent (assuming "work" is the default account):
outlook-cli mail inbox
outlook-cli mail inbox --account work

Check which account is the default:

outlook-cli account list
#   personal            john@outlook.com
#   work     (default)  john@contoso.com

Switching between accounts

# Read personal inbox
outlook-cli mail inbox --account personal

# Read work inbox
outlook-cli mail inbox --account work
# or shorthand:
outlook-cli mail inbox -a work

# Search across accounts (run separately — no cross-account search):
outlook-cli mail search "quarterly report" --account personal
outlook-cli mail search "quarterly report" --account work

# Check calendars for different accounts
outlook-cli calendar today --account personal
outlook-cli calendar today --account work

Running commands against different accounts

Common multi-account workflows:

# Draft on work account, check calendar on personal
outlook-cli mail draft --to "alice@contoso.com" --subject "Status" --body "Done." --account work
outlook-cli calendar today --account personal

# Watch mode on a specific account
outlook-cli watch --account work --interval 30

# Authentication status for each account
outlook-cli auth status --account personal
outlook-cli auth status --account work

Delegate access with --as

The --as flag lets you access another user's mailbox — if they've granted you delegate permissions. You can combine --account and --as. Under the hood, --as changes the Graph API path from /me to /users/{email}, so all API calls target the delegated mailbox instead of your own. This requires the .Shared permission variants (e.g., Mail.Read.Shared) to be granted in your Azure App Registration. See DELEGATE-ACCESS.md for how to configure delegate permissions in Exchange/Entra.

# Read your manager's inbox using your work credentials
outlook-cli --account work --as manager@contoso.com mail inbox

# Use a contact alias for convenience
outlook-cli contacts alias set boss manager@contoso.com
outlook-cli --account work --as boss mail inbox

# Draft in a shared mailbox
outlook-cli --account work --as shared-team@contoso.com mail draft \
  --to "client@example.com" --subject "Follow-up" --body "..." --yes

# Send a draft on behalf of the shared mailbox
outlook-cli --account work --as shared-team@contoso.com mail send DRAFT_ID --yes

# View a colleague's calendar (if they've shared it with you)
outlook-cli --account work --as boss calendar today

Node.js vs .NET — same commands

Both implementations accept the same flags:

# Node.js
node bin/outlook-cli.js mail inbox --account work --as boss

# .NET (published binary)
outlook-cli.exe mail inbox --account work --as boss

# .NET (dotnet run)
dotnet run --project src/dotnet -- mail inbox --account work --as boss

See MULTI-ACCOUNT.md for account storage details and DELEGATE-ACCESS.md for delegate permission setup.


10. Configure Permissions

outlook-cli has a configurable permission system that controls what each account is allowed to do. This is stored in ~/.outlook-cli/accounts.json. The permission system operates as defense-in-depth: even if your Azure App Registration grants Mail.Send, outlook-cli will block sending unless the account's local configuration also allows it. The token validator (src/node/security/token-validator.js) checks both the MSAL result's scopes array and the decoded JWT scp claim on every token acquisition, so a compromised or overly-broad token cannot bypass local permission rules.

Basic permission structure

{
  "defaults": {
    "permissions": {
      "allowed": ["User.Read", "Mail.Read", "Mail.ReadWrite", "Calendars.Read", "Calendars.ReadWrite", "offline_access"],
      "forbidden": ["Mail.Send"]
    }
  },
  "accounts": {
    "personal": {
      "clientId": "YOUR_CLIENT_ID",
      "tenant": "common"
    },
    "work": {
      "clientId": "YOUR_CLIENT_ID",
      "tenant": "contoso.onmicrosoft.com",
      "permissions": {
        "allowed": ["+Mail.Send"],
        "send_to": ["team@contoso.com", "reports@contoso.com"]
      }
    }
  },
  "default": "personal"
}

What this means:

  • defaults — Applied to all accounts unless overridden. Here, Mail.Send is forbidden globally, which means no account can send email unless it explicitly opts in with the + modifier. This is a safe starting point — you can always relax permissions per-account.
  • personal account — Inherits defaults and adds no overrides. Cannot send email. Can read and organize mail, read and write calendar events.
  • work account — Adds Mail.Send (the + prefix overrides the forbidden list), but restricts sending to whitelisted recipients (send_to). If someone (or an agent) tries to send to an address not in the send_to list, outlook-cli rejects the request locally before it reaches the Graph API. This is especially useful for agent-integrated accounts where you want to limit blast radius.

Permission modifiers

Prefix Meaning Example
(none) Replace the list entirely "allowed": ["Mail.Read"]
+ Add to parent/default list "+Mail.Send" adds sending capability
- Remove from parent/default list "-Calendars.ReadWrite" removes calendar write

Parent chains

Accounts can inherit from other accounts, forming a hierarchy. When outlook-cli resolves permissions for an account, it walks the parent chain, merging allowed and forbidden lists at each level. The + and - modifiers apply relative to the inherited list. This avoids repeating common permissions across many accounts — define a base profile once, then customize per-account. Circular parent references are detected and rejected at startup.

{
  "accounts": {
    "base": {
      "clientId": "...",
      "permissions": { "allowed": ["Mail.Read", "Calendars.Read"] }
    },
    "power-user": {
      "clientId": "...",
      "parent": "base",
      "permissions": { "allowed": ["+Mail.ReadWrite", "+Calendars.ReadWrite"] }
    }
  }
}

The power-user account inherits everything from base and adds write permissions.

🔒 Security: Mail.ReadWrite.All is a forbidden scope in outlook-cli and will be rejected even if granted in Azure. This application-level permission grants access to every mailbox in the organization and is never appropriate for a CLI tool. See SECURITY.md for the full security model.


11. Configuration Resolution

outlook-cli resolves settings from multiple sources in a specific order. Understanding this order prevents surprises when a setting doesn't behave as expected.

Resolution order (last wins)

  1. Hardcoded defaultstenantId: "common", logLevel: "warn". These are baked into the source code and provide sensible starting values.
  2. Environment variablesOUTLOOK_CLI_CLIENT_ID, OUTLOOK_CLI_TENANT_ID, OUTLOOK_CLI_PASSPHRASE, OUTLOOK_CLI_LOG_LEVEL. Set these in your shell profile or CI environment.
  3. Config file (~/.outlook-cli/config.json) — Overrides environment variables. This is intentional: the config file represents the user's persistent preference, while environment variables may be set globally for other tools.
  4. Per-account settings in accounts.json — Account-specific clientId, tenantId, and permissions override global config.
  5. CLI flags--client-id, --tenant, --account, --verbose, etc. These always win.

Surprising behavior: config.json beats env vars

The most common surprise is that config.json overrides environment variables. For example:

# You set this in your shell:
export OUTLOOK_CLI_CLIENT_ID="env-client-id-111"

# But config.json contains:
# { "clientId": "file-client-id-222" }

# Result: outlook-cli uses "file-client-id-222"

This happens because loadConfig() in src/node/config.js uses Object.assign(config, fileConfig) — the file config overwrites any env-var-derived values. If you need environment variables to take priority, don't set the same key in config.json. This design choice ensures that a user's explicit config file settings aren't silently overridden by inherited shell environment variables.

Per-account overrides

The accounts.json file can override clientId and tenantId per account, which is useful when different accounts belong to different Azure tenants or use different App Registrations:

{
  "accounts": {
    "personal": { "clientId": "aaa-bbb", "tenant": "common" },
    "work":     { "clientId": "ccc-ddd", "tenant": "contoso.onmicrosoft.com" }
  }
}

In this example, the personal account uses one App Registration while the work account uses a different one. CLI flags like --client-id override even account-specific values.


12. Set Up Watch Mode

Watch mode monitors your mailbox and calendar for changes in real-time. It uses Microsoft Graph delta queries (not webhooks in poll/fast-poll mode) to efficiently detect changes without re-downloading your entire inbox each time. Delta tokens are stored per-account in ~/.outlook-cli/delta-{alias}.json — if a delta token expires (typically after 30 days of inactivity), outlook-cli automatically falls back to a full sync. Three modes are available:

Mode Latency Setup Use Case
poll (default) 30–60s None General monitoring, rules engine
fast-poll 5–60s adaptive None Agent integration without tunnel
webhook ~1–5s push Tunnel (cloudflared) Gateway/agent integration

Simple watch (one account)

# Default poll mode (delta queries every 60 seconds)
outlook-cli watch

# Fast-poll: adaptive 5–60 second polling
outlook-cli watch --mode fast-poll --interval 10 --no-calendar

# Webhook: near-instant push notifications
outlook-cli watch --mode webhook

# Output as JSON Lines (for piping to agents)
outlook-cli watch --mode webhook --jsonl | my-agent-process

Multi-job watch (rules engine)

For complex scenarios — watching multiple accounts, filtering specific emails, taking automated actions — create a JSON config file:

{
  "jobs": [
    {
      "name": "urgent-mail",
      "account": "work",
      "schedule": "30s",
      "rules": [
        {
          "conditions": {
            "importance": "high",
            "from": ["boss@contoso.com", "vp@contoso.com"]
          },
          "actions": [
            { "type": "flag" },
            { "type": "move", "folder": "Urgent" }
          ]
        }
      ]
    },
    {
      "name": "newsletter-cleanup",
      "account": "personal",
      "schedule": "5m",
      "rules": [
        {
          "conditions": {
            "from": ["*@newsletter.example.com"],
            "subject_contains": ["unsubscribe", "weekly digest"]
          },
          "actions": [
            { "type": "mark_read" },
            { "type": "move", "folder": "Newsletters" }
          ]
        }
      ]
    }
  ]
}

Run it:

outlook-cli watch --config watch-rules.json

# Validate without running
outlook-cli watch --config watch-rules.json --validate

Schedule formats

Format Example Meaning
Duration 30s, 5m, 1h, 2h30m Check every N seconds/minutes/hours
Cron */5 * * * * Cron expression (every 5 minutes)
Time of day 09:00, 17:30 Run once daily at this time

See WATCH-MODE.md for full options reference.


13. OpenClaw / NanoClaw Integration

outlook-cli integrates with AI agent frameworks in two modes:

  1. As a Skill — The agent invokes outlook-cli commands to read/write email
  2. As a Gateway — outlook-cli watches for incoming email and delivers messages to the agent in real-time

Automated Setup

The skill/ directory contains ready-to-use scripts for both platforms. You don't need to clone the entire repo — just copy the skill/openclaw/ or skill/nanoclaw/ directory and point --bin to your outlook-cli binary.

OpenClaw Quick Start

# Windows (PowerShell)
.\skill\openclaw\setup.ps1 -ClientId "YOUR_CLIENT_ID"
.\skill\openclaw\gateway.ps1    # starts real-time email gateway

# Linux / macOS (bash)
./skill/openclaw/setup.sh --client-id YOUR_CLIENT_ID
./skill/openclaw/gateway.sh     # starts real-time email gateway

NanoClaw Quick Start

# In your NanoClaw container
./skill/nanoclaw/setup.sh --client-id YOUR_CLIENT_ID

# Or with environment variables
OUTLOOK_CLI_CLIENT_ID=your-id OUTLOOK_CLI_PASSPHRASE=your-passphrase \
  ./skill/nanoclaw/gateway.sh

Mode 1: Skill (Agent invokes CLI commands)

Your agent runs outlook-cli commands directly to interact with email and calendar.

OpenClaw

Add the Skill definition from skill/openclaw/SKILL.md to your OpenClaw configuration. The agent can then run:

outlook-cli mail inbox --json
outlook-cli mail search "project alpha" --json
outlook-cli mail reply MESSAGE_ID --body "Agent-drafted reply..." --yes --json
outlook-cli calendar today --json

Security: By default, the agent can create drafts but not send them. To enable sending, add Mail.Send to the account's allowed permissions and configure a send_to whitelist (see Configure Permissions).

NanoClaw

Use skill/nanoclaw/SKILL.md for your container configuration. Key setup:

volumes:
  - ~/.outlook-cli:/root/.outlook-cli    # Persist tokens across restarts

environment:
  OUTLOOK_CLI_CLIENT_ID: "your-client-id"
  OUTLOOK_CLI_PASSPHRASE: "your-secure-passphrase"

Since containers don't have a browser, run setup with device code flow:

./skill/nanoclaw/setup.sh --client-id YOUR_CLIENT_ID

Mode 2: Gateway (email as a real-time channel)

The gateway scripts watch for incoming emails and deliver them to the agent as JSONL events. They automatically select the best watch mode:

  • Webhook mode if cloudflared is installed — near-instant (~1–5 second) push notifications
  • Fast-poll mode as fallback — adaptive 5–60 second polling

OpenClaw Gateway

# Windows — starts gateway, outputs JSONL to stdout
.\skill\openclaw\gateway.ps1

# With a custom binary location
.\skill\openclaw\gateway.ps1 -OutlookCliBin "C:\tools\outlook-cli.exe"

# Forward events to an HTTP endpoint
.\skill\openclaw\gateway.ps1 -GatewayUrl "http://localhost:8080/incoming"
# Linux/macOS
./skill/openclaw/gateway.sh
./skill/openclaw/gateway.sh --gateway-url http://localhost:8080/incoming

Configure OpenClaw to consume the gateway:

channels:
  - name: outlook-email
    type: process
    command: ./skill/openclaw/gateway.sh
    format: jsonl

NanoClaw Gateway

# NanoClaw container config
channels:
  - name: outlook
    type: process
    command: /opt/outlook-cli/skill/nanoclaw/gateway.sh
    format: jsonl

The gateway outputs JSONL to stdout. Each line is a change event that NanoClaw interprets as an incoming message.

Bidirectional Conversation Flow

Here's the full flow for an email-based AI assistant:

  1. User sends email to the agent's account
  2. Gateway detects the new message (via webhook push or fast-poll) and delivers it as JSONL
  3. Agent receives the message and processes it — e.g., checks the calendar:
    outlook-cli calendar today --json
  4. Agent replies using skill commands:
    outlook-cli mail reply MESSAGE_ID --body "Here's your calendar..." --yes --json
  5. User receives the reply in their inbox

Installing Tunnel for Webhook Mode

For near-instant notification delivery, install a tunnel tool:

Platform Command
Windows winget install Cloudflare.cloudflared
macOS brew install cloudflare/cloudflare/cloudflared
Linux See Cloudflare docs

Without a tunnel, the gateway scripts automatically fall back to fast-poll mode (5-second adaptive polling).


14. Diagnostics — Logs, Telemetry, Doctor

outlook-cli includes built-in diagnostics for troubleshooting and monitoring. Every CLI operation is logged with a correlation ID, so you can trace a full request chain from command invocation through Graph API calls to completion or failure. For full details, see DIAGNOSTICS.md.

Operations Log

Every CLI command is automatically recorded in a SQLite database at ~/.outlook-cli/outlook-cli.db (using WAL mode for concurrent access). Each operation gets a unique correlation ID that links the start, completion, and any errors together. The database is managed by src/node/db/database.js and logged by src/node/db/logger.js. This log is invaluable for diagnosing intermittent failures — you can filter by account, status, time range, or correlation ID to find exactly what happened.

# Show recent operations
outlook-cli log search

# Filter by account or status
outlook-cli log search --account work --status failed

# Summary dashboard (last 7 days)
outlook-cli log summary

# Trace a specific operation chain
outlook-cli log search --correlation-id <id>

Doctor (Health Check)

Run a comprehensive health check of your setup:

outlook-cli doctor

This validates Node.js version, config files (accounts.json, config.json, aliases.json), token cache encryption, Graph API connectivity, and granted scopes. Each check reports ✅/❌ with fix instructions. Running doctor after initial setup or after moving ~/.outlook-cli/ to a new machine is a good practice — it catches the most common misconfiguration issues in seconds.

Telemetry (Optional)

Disabled by default. Enable for detailed API timing, request counts, and performance metrics. Telemetry uses an in-memory ring buffer (1,000 events) with optional SQLite persistence. All data stays local — nothing is sent externally, ever. This is useful for diagnosing slow commands, tracking API rate-limit headers, and profiling startup time. See DIAGNOSTICS.md for details on interpreting telemetry output.

OUTLOOK_CLI_TELEMETRY=1 outlook-cli mail inbox

All data stays local — nothing is sent externally. Set OUTLOOK_CLI_TELEMETRY=0 or remove the variable to disable. See DIAGNOSTICS.md for details.


15. Rolling Out to Your Team

Share the Client ID

One App Registration works for your entire organization. Share the Client ID (it's not a secret — it's just an identifier, like a username). Each person authenticates with their own Microsoft account and gets their own encrypted token cache file. The token cache uses AES-256-GCM encryption with PBKDF2-SHA512 key derivation (310,000 iterations), keyed to either a machine-specific value or a user-supplied OUTLOOK_CLI_PASSPHRASE. Even if someone copies another user's cache-{alias}.enc file, they cannot decrypt it without the original machine's key material or passphrase.

Quick setup for your team

Copy the scripts/ folder (and optionally the pre-built binary) to a file share:

\\server\tools\outlook-cli\
├── scripts\
│   ├── self-host-azure.ps1      # Admin runs this once
│   └── self-host-client.ps1     # Each user runs this
├── publish\
│   └── win-x64\
│       └── outlook-cli.exe      # Pre-built native binary
└── README.txt                   # Your Client ID + instructions

Then send your team this:

Getting started with outlook-cli:

  1. Open PowerShell and run:
    \\server\tools\outlook-cli\scripts\self-host-client.ps1 -ClientId CLIENT_ID_HERE -RepoPath \\server\tools\outlook-cli
    
  2. Sign in with your Microsoft account when the browser opens
  3. That's it! Try: \\server\tools\outlook-cli\publish\win-x64\outlook-cli.exe mail inbox

Full guide: \\server\tools\outlook-cli\docs\self-hosting.md

Centralized configuration

For teams, you can distribute a pre-configured accounts.json:

{
  "defaults": {
    "permissions": {
      "allowed": ["User.Read", "Mail.Read", "Mail.ReadWrite", "Calendars.Read", "Calendars.ReadWrite", "offline_access"],
      "forbidden": ["Mail.Send"]
    }
  },
  "accounts": {
    "work": {
      "clientId": "YOUR_SHARED_CLIENT_ID",
      "tenant": "contoso.onmicrosoft.com"
    }
  },
  "default": "work"
}

Have each team member place this in their ~/.outlook-cli/accounts.json, then run outlook-cli auth login.

Admin consent (work/school accounts)

For Microsoft 365 organizational accounts, an admin may need to grant consent for the app before users can authenticate. Without admin consent, each user sees a "needs admin approval" error on first login. Go to:

Entra admin center → Enterprise applications → outlook-cli → Permissions → Grant admin consent

This is a one-time action that approves the app for all users in the organization.


16. Enterprise Deployment

This section covers production deployment patterns for enterprise environments.

Secrets Management

Never hardcode passphrases. In production, set the encryption passphrase via a secrets manager:

Azure Key Vault:

export OUTLOOK_CLI_PASSPHRASE=$(az keyvault secret show --vault-name my-vault --name outlook-cli-pass --query value -o tsv)
outlook-cli mail inbox

HashiCorp Vault:

export OUTLOOK_CLI_PASSPHRASE=$(vault kv get -field=passphrase secret/outlook-cli)
outlook-cli mail inbox

Kubernetes secret:

env:
  - name: OUTLOOK_CLI_PASSPHRASE
    valueFrom:
      secretKeyRef:
        name: outlook-cli-secrets
        key: passphrase

Without OUTLOOK_CLI_PASSPHRASE, encryption falls back to a machine-derived key (username@hostname:alias). This is adequate for personal use but predictable if an attacker knows your machine identity. See SECURITY-DESIGN.md §4 for the full threat analysis.

Monitoring and Alerting

outlook-cli stores telemetry in ~/.outlook-cli/outlook-cli.db (SQLite). Key metrics to monitor:

# Check recent Graph API performance
outlook-cli telemetry summary

# Look for authentication failures
outlook-cli log search --status failed --since "24 hours ago"

# Run automated health check
outlook-cli doctor

Metrics to track:

  • Authentication failure rate (log search --status failed)
  • Graph API response times (telemetry summary — avg, p95, p99)
  • Rate limit hits (429 responses in telemetry data)
  • Cache decryption failures (indicates passphrase/machine mismatch)

Alerting thresholds:

  • Auth failure rate > 5% → check token expiry, investigate passphrase changes
  • P95 response time > 5s → Graph API degradation or network issues
  • Rate limit hits > 10/hour → reduce polling frequency or batch operations

Multi-Instance Deployments

Multiple CLI instances can share the same ~/.outlook-cli/ directory safely:

  • Token cache: AES-256-GCM encrypted files. MSAL handles token refresh race conditions internally. Two instances may both refresh, but the last write wins and both get valid tokens.
  • SQLite database: Uses WAL (Write-Ahead Logging) mode for concurrent read/write access. Multiple readers are fully concurrent; writes are serialized automatically.
  • Short IDs (last-results.json): NOT safe for multi-instance use. Each instance overwrites the short ID mapping. Use full Graph IDs (--json | jq '.id') in multi-instance or agent scenarios.
  • Delta tokens: One delta token per account. If two instances watch the same account, they'll see duplicate notifications. Use a single watcher per account.

Recommended pattern for agents:

# Each agent instance uses full Graph IDs, not short IDs
MSG_ID=$(outlook-cli mail inbox --top 1 --json --account agent-acct | jq -r '.[0].id')
outlook-cli mail read "$MSG_ID" --json --account agent-acct

Backup and Recovery

What to back up:

File Purpose Sensitive?
accounts.json Account configuration, client IDs, aliases No secrets (client IDs are public)
aliases.json Human-friendly account name mappings No
config.json CLI preferences and defaults No
cache-*.enc Encrypted token caches Encrypted (useless without passphrase)
outlook-cli.db Operation logs and telemetry No secrets, but contains metadata

Recovery after data loss:

# Re-authenticate (regenerates token cache)
outlook-cli auth login --account <alias>

# If accounts.json is lost, re-create accounts
outlook-cli auth login --client-id YOUR_CLIENT_ID --account my-account

Token caches are regenerated on login. The only unrecoverable data is operation history in the SQLite database (logs, telemetry), which is diagnostic only.


17. Troubleshooting Quick Reference

This section covers the most common issues. For in-depth troubleshooting with root cause analysis and step-by-step fixes, see troubleshooting.md. You can also run outlook-cli doctor to automatically diagnose most setup problems.

"AADSTS700016: Application not found"

  • Double-check the Client ID matches your app registration. It should be a GUID like a1b2c3d4-e5f6-7890-abcd-ef1234567890.
  • If using a work account, the app may need to be registered in your organization's tenant (not your personal tenant). Check with your Azure AD admin.
  • Verify you haven't accidentally deleted the App Registration in the Azure portal.

"redirect_uri does not match"

  • Ensure http://localhost:53847/callback is set as the redirect URI in your Azure App Registration under Authentication → Mobile and desktop applications.
  • Ensure the platform type is "Mobile and desktop applications" (not "Web"). The web platform requires HTTPS and a different auth flow, which outlook-cli doesn't use.
  • If you've changed the port, make sure the redirect URI matches exactly — the port number, path, and protocol must all match.

"AADSTS65001: consent required"

  • First login requires you to click "Accept" on the consent screen in the browser. If you dismiss or cancel, authentication fails.
  • For work accounts, your admin may need to grant tenant-wide consent (see Rolling Out). Until consent is granted, users see "needs admin approval" instead of the consent screen.
  • If you've added new API permissions to the App Registration after initial consent, users may need to re-consent by running outlook-cli auth login again.

"Port 53847 is in use"

  • Another program (or another instance of outlook-cli) is using that port. The interactive login flow starts a temporary HTTP server on localhost:53847 to receive the OAuth callback.
  • Use --device-code instead to avoid the local server entirely: outlook-cli auth login --device-code --client-id YOUR_CLIENT_ID. This works on any machine, including servers and containers.
  • On Windows, you can find what's using the port with netstat -ano | findstr :53847 and stop that process.

"Could not decrypt token cache"

  • The token cache (cache-{alias}.enc) is encrypted with AES-256-GCM, keyed to a machine-specific value or the OUTLOOK_CLI_PASSPHRASE environment variable. If you moved ~/.outlook-cli/ from another machine, the key material doesn't match — re-authenticate: outlook-cli auth login.
  • In containers, always set OUTLOOK_CLI_PASSPHRASE to a consistent value so the cache survives container restarts. Without it, each container instance generates a different machine key and can't decrypt caches from previous runs.
  • outlook-cli handles decryption failures gracefully: it logs a warning, starts with an empty cache, and the user re-authenticates on next use. No data is lost — the old encrypted file is simply ignored.

Device code shows "undefined" or blank

  • "Allow public client flows" is not enabled in the app registration (Step 2.4, item 7). Without this, the device code grant type is rejected by Azure AD.
  • Verify the Supported account types includes your account type. If you registered the app as "single tenant" but are using a personal Microsoft account, the flow will fail.
  • Run with --verbose for detailed error output. The verbose log shows the exact MSAL error code and message, which helps identify the root cause.

"No valid token" errors

  • Your token may have expired. Re-authenticate: outlook-cli auth login. With the offline_access permission, tokens refresh automatically via MSAL's silent token acquisition — you shouldn't need to re-login unless the refresh token itself expires (typically 90 days of inactivity).
  • If using offline_access permission, tokens refresh automatically. Without it, you'll need to re-login periodically. The offline_access scope is strongly recommended — add it in your Azure App Registration if it's missing.
  • Run outlook-cli auth status to check whether the current token is valid before investigating further.

Node.js vs .NET gives different results

  • Both implementations share the same config and token cache in ~/.outlook-cli/. The binary cache format (salt(32) + iv(16) + authTag(16) + ciphertext) is identical, so a cache encrypted by Node.js decrypts in .NET and vice versa.
  • Make sure you're running the latest version of both. The .NET NativeAOT binary in publish/ is compiled at a point in time and doesn't auto-update — rebuild it after git pull with dotnet publish or ./build.ps1 dotnet -Publish.
  • If one works and the other doesn't, try re-authenticating with the working one — the shared cache will be refreshed. Check outlook-cli --version on both to confirm they match.

"Network error" or "ENOTFOUND" or "ETIMEDOUT"

  • Check your internet connectivity and DNS resolution. outlook-cli needs to reach graph.microsoft.com and login.microsoftonline.com.
  • If you're behind a corporate proxy, set the HTTPS_PROXY environment variable. Node.js respects this for outbound HTTPS connections.
  • Transient network errors are retried automatically for 429 (rate limit) responses, but other network failures are not retried by default. If the error is intermittent, simply re-run the command.

Config file seems to override my environment variables

This is by design. The config file (~/.outlook-cli/config.json) intentionally takes priority over environment variables — see Configuration Resolution for the full explanation. If you want environment variables to win, remove the conflicting key from config.json. You can inspect your resolved config with outlook-cli --verbose which logs the final merged configuration.

Short IDs point to wrong messages

Short IDs in ~/.outlook-cli/last-results.json are overwritten every time any list command runs (inbox, search, mail inbox --folder ...). If you ran a search after listing your inbox, the short IDs now point to search results. See Short IDs for details and workarounds. For scripts, always use full Graph IDs from --json output.

Need more help?

  • Run outlook-cli doctor for automated health checks
  • Use outlook-cli --verbose for detailed logging of every step
  • Check the operations log: outlook-cli log search --status failed
  • Review the docs: architecture.md, usage.md, SECURITY.md, troubleshooting.md
  • File an issue: https://github.com/jeffstall/outlook-cli/issues

18. Enterprise COM Bridge Deployment

For enterprise environments where Microsoft Graph API is blocked or unavailable — for example, on-premises Exchange without Graph API access, or corporate policies that prohibit Azure App Registrations — outlook-cli can connect through a COM Bridge server that communicates with Outlook Desktop installed on a Windows machine.

What is the COM Bridge?

The COM Bridge is a small HTTP server (src/bridge/) that runs on a Windows machine with Outlook Desktop installed. It receives HMAC-authenticated HTTP requests from the CLI and translates them into Outlook COM automation calls. This gives you full mail, calendar, and contacts access without needing the Microsoft Graph API or Azure.

Your machine (any OS)              Windows machine (Bridge host)
┌──────────────────┐               ┌─────────────────────────────┐
│  outlook-cli     │  HTTP+HMAC    │  COM Bridge Server          │
│  (Node.js or     │──────────────▶│  (ASP.NET Core)             │
│   .NET binary)   │               │       │                     │
└──────────────────┘               │       ▼                     │
                                   │  Outlook Desktop (COM)      │
                                   │       │                     │
                                   │       ▼                     │
                                   │  Exchange / Microsoft 365   │
                                   └─────────────────────────────┘

Prerequisites

On the bridge host machine (Windows):

  • Windows 10/11 or Windows Server 2019+
  • Microsoft Outlook Desktop (classic Outlook, not the new Outlook) — must be installed and signed in to your Exchange/M365 account
  • .NET 8 SDK (to build) or a pre-built binary
  • Outlook must be running in a user session (COM requires a logged-in desktop session — it cannot run as a headless Windows Service)

On your CLI machine (any OS):

  • outlook-cli installed (Node.js or .NET version)
  • Network access to the bridge host on the configured port

Step 18.1: Build the Bridge

On the bridge host machine:

# Clone the repository (or copy the source)
git clone https://github.com/jeffstall/outlook-cli.git
cd outlook-cli

# Build and publish
dotnet publish src/bridge -c Release -o publish-bridge

This produces publish-bridge/outlook-bridge.exe (or similar — the actual binary name comes from the project).

Step 18.2: Generate a Shared Secret

The bridge and CLI authenticate with a shared HMAC-SHA256 secret. Generate a strong random key:

# PowerShell — generate a 64-character hex secret
-join ((1..32) | ForEach-Object { '{0:x2}' -f (Get-Random -Max 256) })
# Linux/Mac — generate a 64-character hex secret
openssl rand -hex 32

Save this secret — you'll need it for both the bridge config and the CLI account config. Treat it like a password.

Step 18.3: Configure the Bridge

Create a bridge.json file in the bridge directory (or next to the executable):

{
  "bridgeSecret": "your-64-char-hex-secret-from-step-2",
  "port": 9080,
  "accessMode": "read-write",
  "sendControls": {
    "maxRecipients": 25,
    "requireSubject": true,
    "allowedRecipients": ["*@contoso.com", "*@partner.com"],
    "blockedRecipients": ["ceo@contoso.com"]
  }
}
Setting Description Default
bridgeSecret HMAC-SHA256 shared secret (required)
port HTTP port to listen on 9080
accessMode read-write or read-only read-write
sendControls.maxRecipients Maximum recipients per message 50
sendControls.requireSubject Block sends without a subject false
sendControls.allowedRecipients Glob patterns for allowed recipients ["*"]
sendControls.blockedRecipients Glob patterns for blocked recipients []

Step 18.4: Start the Bridge

# Start the bridge (Outlook must be running)
cd publish-bridge
.\outlook-bridge.exe

# Or use the convenience scripts from the repo root:
.\bridge-config.ps1    # First-time setup: generates secret, configures both sides
.\bridge-start.ps1     # Start bridge with health check
.\bridge-stop.ps1      # Stop the running bridge process

The bridge will:

  1. Start a Kestrel HTTP server on the configured port
  2. Connect to Outlook Desktop via COM automation
  3. Log incoming requests to the console

Verify it's running:

# Health check (no authentication required)
Invoke-RestMethod http://localhost:9080/health
# Should return: { "status": "healthy", "outlook": "connected" }

Step 18.5: Configure the CLI

On your CLI machine, add a bridge-backed account:

# Add a bridge account
outlook-cli account add work-bridge \
  --client-id "not-used" \
  --provider bridge \
  --bridge-url "http://bridge-machine:9080" \
  --bridge-secret "your-64-char-hex-secret"

# Or edit ~/.outlook-cli/accounts.json directly:
{
  "accounts": {
    "work-bridge": {
      "clientId": "not-used-for-bridge",
      "provider": "bridge",
      "bridgeUrl": "http://bridge-machine:9080",
      "bridgeSecret": "your-64-char-hex-secret"
    }
  }
}

Step 18.6: Multi-Account Bridge Setup (Optional)

If your Outlook profile has multiple accounts (e.g., work + shared mailbox), you can route different CLI accounts to different Outlook stores through the same bridge server.

Step 1: Discover available stores

# List all Outlook stores the bridge can access
curl http://localhost:8465/api/stores -H "X-Bridge-Auth: ..."

# Returns:
# [
#   { "storeId": "00000...", "displayName": "user@contoso.com" },
#   { "storeId": "00000...", "displayName": "shared-mailbox@contoso.com" }
# ]

Step 2: Map bridge accounts in bridge.json

Add an accounts block that maps friendly names to Outlook store display names:

{
  "bridgeSecret": "your-secret",
  "port": 8465,
  "accounts": {
    "work": { "storeDisplayName": "user@contoso.com" },
    "shared": { "storeDisplayName": "shared-mailbox@contoso.com" }
  }
}

Step 3: Register CLI accounts

# Primary mailbox (no --bridge-account needed, uses default store)
outlook-cli account add bridge \
  --provider bridge --client-id not-used \
  --bridge-url http://localhost:8465 --bridge-secret "$SECRET"

# Shared mailbox — route to a specific store
outlook-cli account add shared \
  --provider bridge --client-id not-used \
  --bridge-url http://localhost:8465 --bridge-secret "$SECRET" \
  --bridge-account shared

Or use the config script:

.\bridge-config.ps1 -Alias shared -BridgeAccount shared

Step 4: Use normally

outlook-cli mail inbox --account bridge    # → default store (your mailbox)
outlook-cli mail inbox --account shared    # → shared-mailbox@contoso.com
outlook-cli calendar today --account shared

The ?account= parameter is appended automatically to all bridge HTTP requests. The bridge resolves the account name to a store ID and routes all operations (inbox, search, send, calendar, contacts) to that store.

Note: If no --bridge-account is specified, the bridge uses Outlook's default store (the primary email account).

Step 18.7: Test the Connection

# List inbox via bridge
outlook-cli mail inbox --account work-bridge

# Read a message
outlook-cli mail read 1 --account work-bridge

# Search
outlook-cli mail search "quarterly report" --account work-bridge

# Calendar
outlook-cli calendar today --account work-bridge

# GAL search — find people by alias or name
outlook-cli contacts search mikehill --account work-bridge
outlook-cli contacts search "Mike Hillberg" --account work-bridge --json

All standard commands work identically — the CLI doesn't know (or care) whether it's talking to Graph API or the bridge.

Security Considerations

  • HMAC-SHA256: Every request is signed with the shared secret. The bridge verifies the signature, a timestamp (±60s), and a random nonce to prevent replay attacks.
  • Read-only mode: Set accessMode: "read-only" to block all write operations (send, delete, move, etc.) at the bridge level, regardless of what the CLI requests.
  • Send controls: Even in read-write mode, the bridge enforces recipient whitelists/blocklists and a maximum recipient count.
  • Network: The bridge listens on HTTP (not HTTPS). For production, place it behind a reverse proxy with TLS, or restrict network access to trusted hosts.
  • No credentials stored: The bridge does not store any email credentials — it relies on Outlook Desktop's existing authentication session.

Running as a Startup Task

To keep the bridge running automatically:

# Task Scheduler — run at logon (requires user session for COM)
$action = New-ScheduledTaskAction -Execute "C:\path\to\publish-bridge\outlook-bridge.exe"
$trigger = New-ScheduledTaskTrigger -AtLogon
Register-ScheduledTask -TaskName "OutlookBridge" -Action $action -Trigger $trigger -Description "outlook-cli COM Bridge"

Important: The bridge must run in an interactive user session because Outlook COM requires a desktop. It cannot run as a Windows Service without special configuration (e.g., enabling "Allow service to interact with desktop" — not recommended).

Troubleshooting Bridge Issues

Problem Cause Solution
ECONNREFUSED Bridge not running or wrong port Check bridge is running, verify port in bridge.json
401 Unauthorized HMAC secret mismatch Ensure bridgeSecret matches in both bridge.json and accounts.json
403 Forbidden Read-only mode blocking writes Change accessMode to read-write in bridge.json
500 COM Error Outlook not running or not signed in Start Outlook Desktop and sign in to your account
Timestamp expired Clock skew between machines Sync clocks via NTP (must be within 60 seconds)
Empty inbox Outlook profile not configured Ensure Outlook has the correct email account configured