m365-cli
Agent-friendly Microsoft 365 CLI — read and manage Outlook email, calendar, and people lookups via the Microsoft Graph API. All commands emit structured JSON to stdout; errors emit JSON to stderr with a non-zero exit code, so the tool is usable both by humans and by scripts/agents.
Status: alpha (0.1.2). Surface area and JSON shapes may change.
Requirements
- Python 3.11+
- An Azure AD app registration in the tenant you want to access
(Client ID + Tenant ID). A client secret is only required for the headless
client_credentialsmode.
Install
pip install m365-cli
After install you'll have an m365 command on your PATH.
Azure app registration (one-time)
- Azure Portal → App registrations → New registration.
- Supported account types: pick what matches your tenant (single-tenant for most users; "common" works for personal multi-tenant).
- After creation, go to Authentication → Advanced settings and set Allow public client flows = Yes (required for the device-code flow).
- Go to API permissions → Add a permission → Microsoft Graph → Delegated
and add:
User.Read,Mail.Read,Mail.ReadWrite,Mail.Send,Calendars.Read,Calendars.ReadWrite,People.Read. Grant admin consent if your tenant requires it. - Copy the Application (client) ID and Directory (tenant) ID — you'll paste them into the setup wizard.
For the headless client_credentials mode you'll also need a client secret
and Application (not Delegated) Graph permissions, with admin consent.
Configure
Run the interactive wizard (writes a .env in the current directory):
m365 setup
Then authenticate:
m365 auth login # device-code flow; opens a browser
m365 auth status # confirm who you're signed in as
Configuration is read from the following locations (later overrides earlier):
~/.m365/.env— global config written bym365 setup(works from any directory).envin the current working directory — per-project override
Environment variables set in the shell always override both files. See
.env.example for all supported keys.
Common commands
# Email
m365 email list --limit 10 --unread-only
m365 email list --since 2025-01-01 --until 2025-01-31 # filter by date range
m365 email search --query "invoice" # full-text search (server-side)
m365 email search --query "budget" --folder "Archive" # scope to a folder
m365 email read <message-id>
m365 email send --to alice@example.com --subject "hi" --body "hello"
m365 email reply <message-id> --body "thanks"
# Calendar
m365 calendar events --start 2025-04-25 --end 2025-04-30
m365 calendar create --subject "Sync" --start ... --end ... --attendee ...
m365 calendar availability --user a@example.com --user b@example.com --start ... --end ...
# People
m365 people search --query "alice"
# Auth
m365 auth login | logout | status | token
m365 --help lists every group; each group and command has its own --help.
email search vs email list
| Command | Use when… |
|---|---|
email search --query "…" |
You want full-text search across subject, body, and sender. Uses Exchange Search server-side — fast on any mailbox size. |
email list --since … --until … |
You want messages in a date range or filtered by read status. |
Limitation: The Microsoft Graph API does not allow
$searchand$filteron the same messages request. When--queryis supplied toemail search, the--since,--until, and--unread-onlyflags are silently ignored. Useemail listwhen you need date or read-state filtering without full-text search.
Where things live on disk
| Path | Purpose |
|---|---|
~/.m365/tokens.json |
OAuth tokens (refresh token Fernet-encrypted), chmod 600 |
~/.m365/.key |
Fernet encryption key, chmod 600 (auto-generated on first login) |
~/.m365/m365.log |
Rotating log file (5 MB × 3) |
./.env |
Per-project configuration (Client ID, tenant, preferences) |
Security notes
- The refresh token is encrypted at rest with Fernet. The encryption key is
stored in
~/.m365/.keynext to the ciphertext with the same permissions, so the on-disk encryption is defense-in-depth, not a barrier against a local attacker who already has read access to your home directory. Treat~/.m365/as sensitive. - To make tokens portable across machines, export the auto-generated key
(printed on first login) into
TOKEN_ENCRYPTION_KEYin your.env. m365 auth tokenprints the raw bearer access token to stdout — use it only when you intend to pipe it into another tool.client_credentialsmode requiresMICROSOFT_CLIENT_SECRET. Never commit it..env,*.key,*.pem, and*.p12are gitignored.- Logs in
~/.m365/m365.logmay contain Graph request metadata (URLs, status codes) but tokens are truncated before being logged.
Exit codes
| Code | Meaning |
|---|---|
| 0 | success |
| 1 | error |
| 2 | authentication required (run m365 auth login) |
| 3 | resource not found |
Development
git clone git@github.com:goetzcj/m365-cli.git
cd m365-cli
uv sync --extra dev # or: pip install -e ".[dev]"
pytest
License
MIT — see LICENSE.
Metadata
Release files for m365-cli 0.1.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| m365_cli-0.1.2.tar.gz | 35.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| m365_cli-0.1.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 74.6 kB
Release files / m365_cli-0.1.2.tar.gz
| Download URL | m365_cli-0.1.2.tar.gz |
|---|---|
| Size | 35.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a06ec951e72a4259f6dc8dca3da95bbfcdf2a5fc1d60e76dec87b84c74f0804d
|
|
BLAKE2b-256 checksum How to use checksums |
47aaf3d3b6a4fe9647a0cc3f06ad2d4c6b9e4cf96690ca43920edabb61721845
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.11.8 {"installer":{"name":"uv","version":"0.11.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / m365_cli-0.1.2-py3-none-any.whl
| Download URL | m365_cli-0.1.2-py3-none-any.whl |
|---|---|
| Size | 39.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b5d98f567f4910a564c7aad3db1962184f03abcd60912ab015bdddf9b1c92d5e
|
|
BLAKE2b-256 checksum How to use checksums |
9967e5cd3ffa716fb4669b7f88f5ccd9ab3203749aaf4ef925e90f6f70202419
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.11.8 {"installer":{"name":"uv","version":"0.11.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|