Problem
`maildb ingest run` requires `--account` to be explicitly specified. For Gmail Takeout exports, this is redundant: Takeout mboxes reliably carry the owner's address in headers like `X-Gmail-Labels`, `Delivered-To`, or the archive path itself. Typing `--account splaice@gmail.com` when that same string is already available from the data is an unnecessary source of typos and context-switching.
Current state
- `src/maildb/cli.py::ingest_run` declares `account: str = typer.Option(..., "--account", ...)` — required, no inference.
- `src/maildb/parsing.py` already reads `X-Gmail-Labels` (line 210) and other Gmail-specific headers, so the infrastructure for looking at them exists.
Proposed solution
Make `--account` optional. When omitted, inspect the first few messages of the mbox and infer the owner:
- Look for `Delivered-To` header — for Gmail, this is typically the user's address on messages they received.
- Cross-check by looking at `X-Gmail-Labels` header — Gmail Takeout puts system labels like `Inbox`, `Sent`, `Drafts` that match the owner's mailbox view.
- If the inferred address appears in the majority of the first 10 messages as either `Delivered-To` or sender, we're confident — print it and proceed.
- If inference is ambiguous (multiple addresses, or no clear winner), fail with a clear error asking the user to pass `--account` explicitly.
Implementation sketch:
```python
def _infer_source_account(mbox_path: Path, sample_size: int = 10) -> str | None:
"""Peek at first N messages of mbox; return inferred owner address or None."""
import mailbox
from collections import Counter
candidates: Counter[str] = Counter()
mb = mailbox.mbox(str(mbox_path))
for i, msg in enumerate(mb):
if i >= sample_size:
break
for header in ("Delivered-To", "X-Original-To"):
if (val := msg.get(header)):
candidates[val.strip().lower()] += 1
if not candidates:
return None
top, count = candidates.most_common(1)[0]
if count >= sample_size * 0.5: # majority threshold
return top
return None
```
CLI:
```python
@ingest_app.command("run")
def ingest_run(
mbox_path: Path = typer.Argument(...),
account: str | None = typer.Option(
None, "--account",
help="Account email. Inferred from Takeout mbox headers if omitted.",
),
...
):
if account is None:
account = _infer_source_account(mbox_path)
if account is None:
raise typer.BadParameter(
"Could not auto-detect --account from mbox headers. Pass --account explicitly."
)
typer.echo(f"Auto-detected --account {account} from mbox headers")
_validate_account(account)
...
```
Acceptance criteria
Non-goals
- Do not try to auto-detect for arbitrary mbox formats (non-Takeout). Other providers use different conventions; Takeout is the common case. Fall back to requiring `--account` for everything else.
- Do not cache inferred addresses across runs. The inference is cheap (reads 10 headers).
Tradeoffs
- Pro: Ergonomic win. Removes a common source of typos.
- Con: Adds code that can be wrong. Mitigated by (a) only running when `--account` is omitted and (b) printing the inferred value so the user can abort with Ctrl+C if it looks wrong.
- Risk: If Takeout header conventions change in the future, this breaks silently. Mitigation: the 50%-majority threshold is conservative; rare edge cases fall through to the explicit-`--account` path.
References
- `src/maildb/cli.py::ingest_run` (line ~96)
- `src/maildb/parsing.py` — existing header extraction
Problem
`maildb ingest run` requires `--account` to be explicitly specified. For Gmail Takeout exports, this is redundant: Takeout mboxes reliably carry the owner's address in headers like `X-Gmail-Labels`, `Delivered-To`, or the archive path itself. Typing `--account splaice@gmail.com` when that same string is already available from the data is an unnecessary source of typos and context-switching.
Current state
Proposed solution
Make `--account` optional. When omitted, inspect the first few messages of the mbox and infer the owner:
Implementation sketch:
```python
def _infer_source_account(mbox_path: Path, sample_size: int = 10) -> str | None:
"""Peek at first N messages of mbox; return inferred owner address or None."""
import mailbox
from collections import Counter
candidates: Counter[str] = Counter()
mb = mailbox.mbox(str(mbox_path))
for i, msg in enumerate(mb):
if i >= sample_size:
break
for header in ("Delivered-To", "X-Original-To"):
if (val := msg.get(header)):
candidates[val.strip().lower()] += 1
if not candidates:
return None
top, count = candidates.most_common(1)[0]
if count >= sample_size * 0.5: # majority threshold
return top
return None
```
CLI:
```python
@ingest_app.command("run")
def ingest_run(
mbox_path: Path = typer.Argument(...),
account: str | None = typer.Option(
None, "--account",
help="Account email. Inferred from Takeout mbox headers if omitted.",
),
...
):
if account is None:
account = _infer_source_account(mbox_path)
if account is None:
raise typer.BadParameter(
"Could not auto-detect --account from mbox headers. Pass --account explicitly."
)
typer.echo(f"Auto-detected --account {account} from mbox headers")
_validate_account(account)
...
```
Acceptance criteria
Non-goals
Tradeoffs
References