Metadata-Version: 2.4
Name: owa-mail
Version: 0.1.1
Summary: Pipe-friendly mail CLI for Outlook / Microsoft 365
License: MIT License
        
        Copyright (c) 2026 damsleth
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
        
Project-URL: Homepage, https://github.com/damsleth/owa-mail
Project-URL: Repository, https://github.com/damsleth/owa-mail
Project-URL: Issues, https://github.com/damsleth/owa-mail/issues
Project-URL: Changelog, https://github.com/damsleth/owa-mail/releases
Keywords: outlook,microsoft-365,mail,email,cli,json,graph
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: End Users/Desktop
Classifier: Operating System :: MacOS
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Communications :: Email
Classifier: Topic :: Utilities
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: owa-piggy>=0.6.0
Provides-Extra: test
Requires-Dist: pytest; extra == "test"
Dynamic: license-file

# owa-mail

Mail CLI for Outlook / Microsoft 365. Read, send, schedule, reply,
forward, move, mark and delete mail from the terminal.
Pipe-friendly JSON by default, `--pretty` for humans.

```sh
brew install damsleth/tap/owa-mail
owa-mail messages --pretty
```

Or one-shot, no install, no on-disk state:

```sh
OWA_REFRESH_TOKEN=1.AQ... OWA_TENANT_ID=<tenant-id-or-domain> \
  uvx owa-mail messages --pretty
```

`uvx` pulls owa-mail (and owa-piggy as a transitive dep) into a
throwaway venv. The two env vars feed straight through to owa-piggy's
env-only mode - nothing is written to `~/.config/`.

---

## Happy-path setup (no app registration)

[`owa-piggy`](https://github.com/damsleth/owa-piggy) owns the token
lifecycle; owa-mail just shells out to it on every call. The full
first-run flow:

```sh
# 1. Install both
brew install damsleth/tap/owa-piggy damsleth/tap/owa-mail

# 2. Seed owa-piggy once from your browser (walks you through it)
owa-piggy setup

# 3. Go
owa-mail messages --pretty
```

owa-piggy and owa-mail version independently. owa-mail expects any
owa-piggy >= 0.6.0 and sanity-checks the version on first call.

Multi-account: seed a named owa-piggy profile and pin it in owa-mail's
config.

```sh
owa-piggy setup --profile work
owa-mail config --profile work
```

`--profile` also works as a one-shot override:
`owa-mail --profile home messages`.

---

## The output contract

**JSON on stdout, logs on stderr.** Every read command emits parseable
JSON by default; `--pretty` is a human override that goes to stdout
too. The entire CLI composes with `jq`:

```sh
owa-mail messages --limit 5
```

```json
[
  {
    "id": "AAMkAG...",
    "conversation_id": "...",
    "received": "2026-04-30T08:42:11Z",
    "subject": "Hello",
    "from": "alice@example.com",
    "to": "me@example.com",
    "cc": "",
    "preview": "Just checking in...",
    "is_read": false,
    "has_attachments": false,
    "importance": "Normal",
    "flag": "NotFlagged",
    "folder_id": "AAA...",
    "web_link": "https://outlook.office.com/..."
  }
]
```

Field names in the output are stable lowercase; the backend is Outlook
REST v2 (PascalCase upstream) but owa-mail hides that detail.

```sh
owa-mail messages --unread | jq '.[] | "\(.from): \(.subject)"'
owa-mail messages --since 2026-04-01 | jq '[.[] | select(.has_attachments)] | length'
owa-mail folders | jq '.[] | select(.unread > 0)'
```

`show` returns a single message object (with `body` and `body_type`
fields included). `send`/`reply`/`forward` return `{"sent": true,
"id": "...", "send_at": null|"<iso>"}`. `mark`/`move` return the
updated message resource. `delete` writes `Deleted.` to stderr.

---

## Commands

```sh
# Read
owa-mail messages --pretty                           # Inbox, last 25
owa-mail messages --unread --limit 10 --pretty
owa-mail messages --folder SentItems --since 2026-04-01 --pretty
owa-mail messages --search 'subject:invoice'         # KQL search
owa-mail show --id AAMkAG... --pretty
owa-mail show --id AAMkAG... --html                  # raw HTML body

# Send
owa-mail send --to a@example.com --subject "hi" --body "hello"
owa-mail send --to a@b.c,c@d.e --cc x@y.z --subject "review" --body "..." --html
owa-mail send --to a@b.c --subject "later" --body "..." --send-at 2026-05-01T09:00:00Z
owa-mail send --to a@b.c --subject "draft" --body "..." --save-draft
echo "body from pipe" | owa-mail send --to a@b.c --subject "piped" --body -

# Threads
owa-mail reply --id AAMkAG... --body "thanks"
owa-mail reply-all --id AAMkAG... --body "thanks all"
owa-mail forward --id AAMkAG... --to friend@example.com --body "fyi"

# Mailbox
owa-mail folders --pretty
owa-mail mark --id AAMkAG... --read
owa-mail mark --id AAMkAG... --flag
owa-mail move --id AAMkAG... --to Archive
owa-mail delete --id AAMkAG... --confirm
```

### Folder names

The `--folder` and `--to` (move) flags accept these well-known names
(case-insensitive, with common aliases):

| Canonical      | Aliases                |
| -------------- | ---------------------- |
| `Inbox`        |                        |
| `Drafts`       | `draft`                |
| `SentItems`    | `sent`                 |
| `DeletedItems` | `deleted`, `trash`     |
| `JunkEmail`    | `junk`, `spam`         |
| `Outbox`       |                        |
| `Archive`      | `archived`             |

Anything else is treated as an opaque folder id (look one up via
`owa-mail folders | jq '.[] | {name, id}'`).

### Scheduled send

`--send-at` accepts an ISO datetime. Naive values are interpreted as
UTC; offsets are converted to UTC before being attached to the draft.

```sh
owa-mail send --to a@b.c --subject "later" --body "..." --send-at 2026-05-01T09:00:00Z
owa-mail send --to a@b.c --subject "later" --body "..." --send-at 2026-05-01T09:00:00+02:00
```

Behind the scenes owa-mail creates a draft, attaches the
`PR_DEFERRED_SEND_TIME` extended property, and dispatches it to
`/send`. Exchange Transport then holds the message in your Outbox
until the scheduled time - the same mechanism OWA's "Schedule send"
button uses.

---

## Auth

Two paths:

- **owa-piggy bridge (default)** - owa-mail shells out to
  [`owa-piggy`](https://github.com/damsleth/owa-piggy), which
  piggybacks on OWA's public SPA client. No app registration needed;
  owa-mail stores no refresh token. Optional `owa_piggy_profile`
  pins a named owa-piggy profile.
- **With an app registration** - set `OUTLOOK_APP_CLIENT_ID`,
  `OUTLOOK_REFRESH_TOKEN`, and `OUTLOOK_TENANT_ID` in the config file
  and owa-mail talks to the AAD token endpoint directly. The app
  registration must have `Mail.ReadWrite` and `Mail.Send` (delegated)
  consented for your user.

Config lives at `~/.config/owa-mail/config`:

```
# Default (owa-piggy) path - optional, pins a profile alias
owa_piggy_profile="work"

# App-registration path (optional, mutually exclusive)
OUTLOOK_APP_CLIENT_ID=""
OUTLOOK_REFRESH_TOKEN=""
OUTLOOK_TENANT_ID=""
```

---

## Dependencies

- Python 3.8+ (stdlib only - no `pip install` required at runtime)
- [`owa-piggy`](https://github.com/damsleth/owa-piggy) unless you
  bring your own app registration

## Development

```sh
git clone https://github.com/damsleth/owa-mail
cd owa-mail
python -m pip install -e '.[test]'
python -m pytest -q
```

Live mailbox tests are opt-in and hit a real Outlook account through
`owa-piggy`. Set the live profile alias and recipient explicitly, and
optionally override the Python interpreter used for `-m owa_mail`:

```sh
OWA_MAIL_LIVE=1 OWA_MAIL_LIVE_PROFILE=work OWA_MAIL_LIVE_TO=me@example.com \
  python -m pytest -q tests/test_live.py
OWA_MAIL_LIVE=1 OWA_MAIL_LIVE_PROFILE=work OWA_MAIL_LIVE_TO=me@example.com \
  OWA_MAIL_LIVE_PYTHON=python3 python -m pytest -q tests/test_live.py
```

See [`AGENTS.md`](AGENTS.md) for repo layout and ground rules.

## What's not in this version

- **Attachments** - read or send. Planned for v0.2.
- **`@odata.nextLink` pagination** - `--limit` caps a single page;
  use date bounds (`--since` / `--until`) to walk further back.
- **HTML-to-text rendering** - `--pretty` shows the API's BodyPreview
  field; `--html` on `show` prints raw HTML. owa-mail does not parse
  HTML for terminal display (stdlib-only).
- **Real-time receive** (webhooks, IMAP IDLE) - poll `messages
  --unread` from cron or your agent loop.

## Disclaimer

```
Personal tooling. The default (owa-piggy bridge) path holds no
refresh token of its own - tokens are owa-piggy's responsibility,
scoped to its profile store. The optional app-registration path
does persist a delegated refresh token in owa-mail's config file.
If you don't know why either of those might be a bad idea, don't
use it.
```
