Metadata-Version: 2.4
Name: m365-cli
Version: 0.1.2
Summary: Agent-friendly Microsoft 365 CLI — email, calendar, and more via the Graph API
Author-email: Chris Goetz <goetzcj@gmail.com>
License: MIT
Project-URL: Homepage, https://github.com/goetzcj/m365-cli
Project-URL: Repository, https://github.com/goetzcj/m365-cli
Project-URL: Bug Tracker, https://github.com/goetzcj/m365-cli/issues
Project-URL: Security, https://github.com/goetzcj/m365-cli/blob/main/SECURITY.md
Keywords: microsoft365,m365,office365,graph-api,cli,agent,ai-tools
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: System Administrators
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Communications :: Email
Classifier: Topic :: Office/Business :: Groupware
Classifier: Topic :: Utilities
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: msgraph-sdk>=1.47.0
Requires-Dist: msal>=1.34.0
Requires-Dist: azure-identity>=1.25.1
Requires-Dist: pydantic>=2.7.0
Requires-Dist: pydantic-settings>=2.11.0
Requires-Dist: python-dotenv>=1.0.1
Requires-Dist: click>=8.0
Requires-Dist: rich>=13.7.0
Requires-Dist: cryptography>=46.0.3
Provides-Extra: dev
Requires-Dist: pip-audit>=2.10.0; extra == "dev"
Requires-Dist: pytest>=9.0.3; extra == "dev"
Requires-Dist: pytest-asyncio>=0.23.7; extra == "dev"
Dynamic: license-file

# 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_credentials` mode.

## Install

```bash
pip install m365-cli
```

After install you'll have an `m365` command on your PATH.

## Azure app registration (one-time)

1. Azure Portal → **App registrations → New registration**.
2. Supported account types: pick what matches your tenant
   (single-tenant for most users; "common" works for personal multi-tenant).
3. After creation, go to **Authentication → Advanced settings** and set
   **Allow public client flows = Yes** (required for the device-code flow).
4. 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.
5. 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):

```bash
m365 setup
```

Then authenticate:

```bash
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):

1. `~/.m365/.env` — global config written by `m365 setup` (works from any directory)
2. `.env` in the **current working directory** — per-project override

Environment variables set in the shell always override both files. See
[`.env.example`](./.env.example) for all supported keys.

## Common commands

```bash
# 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 `$search` and `$filter`
> on the same messages request. When `--query` is supplied to `email search`,
> the `--since`, `--until`, and `--unread-only` flags are silently ignored.
> Use `email list` when 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/.key` next 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_KEY` in your `.env`.
- `m365 auth token` prints the **raw bearer access token** to stdout — use it
  only when you intend to pipe it into another tool.
- `client_credentials` mode requires `MICROSOFT_CLIENT_SECRET`. Never commit it.
  `.env`, `*.key`, `*.pem`, and `*.p12` are gitignored.
- Logs in `~/.m365/m365.log` may 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

```bash
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](./LICENSE).
