Skip to content

Release v0.6.0: rename to mailvault, single CLI, Graph built-in, package restructure - #2

Merged
sniner merged 17 commits into
mainfrom
mailvault-rework
Jul 31, 2026
Merged

Release v0.6.0: rename to mailvault, single CLI, Graph built-in, package restructure#2
sniner merged 17 commits into
mainfrom
mailvault-rework

Conversation

@sniner

@sniner sniner commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Release branch for v0.6.0. Same project, renamed from imapbackup to mailvault, with a consolidated CLI, Microsoft Graph built in, and an internal restructure. No behaviour change beyond the documented breaking changes.

User-facing (CHANGELOG 0.6.0)

  • Rename imapbackupmailvault (import package + distribution). Old ib-* commands: stay on v0.5.0.
  • Single mailvault <command> replaces ib-mailbox/ib-archive/ib-copy; db-from-archiverebuild-db, ib-copy folderscopy --list-folders; one mailvault.exe.
  • MS Graph is now a core dependency ([graph] extra removed).
  • Fix: malformed Message-ID (<>) no longer crashes on Python 3.11/3.12.
  • Minimum Python 3.11.

Internal (no behaviour change)

  • Workflows on house standard: ci.yml (lint + typecheck + pytest 3.11/3.12/3.13), release.yml action majors bumped.
  • ruff config + lint gate; ruff format applied.
  • Package restructure:
    • backend/base.py (interface), imap.py (was mailbox.py), graph.py
    • store/cas.py, metadb.py (MetaDatabase)
    • cli/__init__.py (parser/dispatch) + commands.py
    • archive.pyimporter.py (ExternalMailArchive); jobs.update_db_from_archiverebuild_metadb
  • No lazy imports; no circular deps.

Status

  • CI green on f0f41fc; mergeable: MERGEABLE, CLEAN (fast-forward, 16 commits over main).
  • End-to-end tested mailvault backup against a real mailbox (dedup + incremental idempotency verified).

Post-merge steps (not done here)

  1. Mark ready + merge to main.
  2. Tag v0.6.0 → release workflow builds mailvault.exe.
  3. Rename the GitHub repo imapbackupmailvault (README links already point there).

Security note

An earlier commit accidentally included a vim swap file of a credentials config (via git add -A); removed via history rewrite + force-push, .gitignore hardened. Treat those credentials as compromised.

- add ci.yml: basedpyright typecheck + pytest matrix (3.11/3.12/3.13) on push/PR
- release.yml: bump JS actions to current majors (fixes the node20 deprecation)
    checkout v4->v7, setup-python v5->v7, setup-uv v5->v9,
    action-gh-release v2->v3, upload-artifact v4->v7

Release notes already come from CHANGELOG (house standard). The ruff lint job
is deferred to the scrubbing pass: the code is not yet ruff-clean and the
`[tool.ruff]` config still uses top-level `ignore` (should move to
`[tool.ruff.lint]`).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MAeRbKVFkVhmHUGW8dCToY
@sniner
sniner force-pushed the mailvault-rework branch from 16c4fe9 to 6a41fb7 Compare July 31, 2026 19:16
sniner and others added 4 commits July 31, 2026 21:20
CPython's email header parser raises IndexError (not just HeaderParseError)
on malformed values like "<>" on 3.11/3.12; only 3.13+ returns cleanly.
normalize_message_id promised "unusable -> empty string" but let the
IndexError escape, which would crash a run when a server reports such an ID.
Guard the parse and return "" on any parser failure. Surfaced by the new
CI test matrix (3.11/3.12/3.13).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MAeRbKVFkVhmHUGW8dCToY
Rename the import package (src/imapbackup -> src/mailvault) and the
distribution name. Updates all internal + test imports, the
importlib.metadata version lookup, the [project.scripts] entry-point
module paths, the dev self-dependency, and the PyInstaller source paths
in release.yml. Entry-point command names (ib-*) stay for now; folding
them into a single `mailvault` command is the next step.

README and the CHANGELOG breaking-changes prose are deferred to the
rename/CLI documentation step (they still describe the ib-* commands that
are about to change). Repo URLs (github.com/sniner/imapbackup) unchanged.

Tests green on 3.11/3.12/3.13, basedpyright clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MAeRbKVFkVhmHUGW8dCToY
msal and httpx move into [project].dependencies; the optional `graph`
extra and the dev self-reference are removed. Microsoft 365 is a
first-class mailbox source for mailvault, so Graph support always ships.
release.yml now installs `.` instead of `.[graph]`, and the project
description is broadened beyond IMAP. CHANGELOG note under 0.6.0.

The lazy `from mailvault.graph import MSGraphClient` in mailbox.py stays:
it avoids a mailbox<->graph circular import, not for optionality.

Tests green on 3.11/3.12/3.13, basedpyright clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MAeRbKVFkVhmHUGW8dCToY
…t` command

Replace the three ib-* tools with a single `mailvault <command>` CLI
(argparse, nested subparsers). cli/__init__.py builds the parser and
dispatches; the former cli modules become pure handler modules
(mailbox: folders/backup/verify; copy; archive: stats/import/addresses/
compress/decompress/rebuild-db). Add __main__.py for `python -m mailvault`
and the PyInstaller build.

- ib-copy folders    -> mailvault copy --list-folders
- db-from-archive    -> rebuild-db (under `mailvault archive`)
- unified globals: -v/--verbose, -q/--quiet, --log-file, --config,
  --allow-exec, --job (given before the command)
- pyproject: single `mailvault` entry point
- release.yml: build one mailvault.exe

The job logic in mailvault.jobs is unchanged. CHANGELOG breaking-changes
note under 0.6.0; the README rewrite stays deferred. Tests green on
3.11/3.12/3.13, basedpyright clean; CLI smoke-tested (help, --version,
error exit codes, archive stats end-to-end).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MAeRbKVFkVhmHUGW8dCToY
@sniner
sniner force-pushed the mailvault-rework branch from 5b3e429 to 3bca699 Compare July 31, 2026 19:56
sniner and others added 2 commits July 31, 2026 22:14
Add an explicit ruff config (line-length 96, select = E/F/I/W/UP; move the
deprecated top-level `ignore` under [tool.ruff.lint]) and add ruff to the dev
group. Apply `ruff format` and `ruff check --fix` across the tree (UP017
datetime.timezone.utc -> datetime.UTC, import sorting, ...) and wrap the
remaining long SQL/select strings by hand.

- move the shared `dummy_eml_bytes` fixture to tests/conftest.py (removes the
  imports that tripped F811)
- test_graph: drop the now-pointless pytest.importorskip("httpx") (httpx is a
  core dep) and the `_httpx` module indirection; import httpx at the top
- graph.py: hoist the httpx/msal imports to module level now that Graph is core
  (no lazy import inside __init__); use httpx.TransportError directly
- remove an unused local in test_mailbox
- ci.yml: add a lint job (ruff check + ruff format --check)

No behaviour change. Tests green on 3.11/3.12/3.13, basedpyright clean, ruff clean.
The mailbox<->graph lazy import in mailbox.py stays (real circular import).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MAeRbKVFkVhmHUGW8dCToY
Rewrite the README around the single `mailvault` command:
- rename callout (formerly imapbackup; stay on v0.5.0 for the old ib-* commands)
- Overview of the subcommand groups; examples updated (globals before the command)
- a "Migrating from ib-*" mapping table
- MS Graph documented as built-in (no [graph] extra)
- one mailvault.exe on Windows

Repo URLs point to sniner/mailvault (the repo is renamed at merge; GitHub
redirects the old name afterwards). Add the rename as the headline breaking
change in the 0.6.0 CHANGELOG, and bump pyproject/uv.lock to 0.6.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MAeRbKVFkVhmHUGW8dCToY
@sniner sniner changed the title Rework: mailvault rename, CLI consolidation, workflow house-standard Rework: rename to mailvault, single CLI, Graph built-in (v0.6.0) Jul 31, 2026
sniner and others added 9 commits July 31, 2026 23:55
The shared backend types (the MailboxClient protocol, MessageRef, BackupResult)
lived in mailbox.py, so graph.py imported mailbox while mailbox.py imported
graph -- a cycle that forced a lazy `import` inside Mailbox.__enter__. Move
those types into a neutral `mailvault.backend` module (it only depends on cas).
Now graph and mailbox both depend on backend, the cycle is gone, and the
MSGraphClient import in mailbox.py is a normal top-level import. No lazy imports
left.

No behaviour change. Tests green on 3.11/3.12/3.13, basedpyright + ruff clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MAeRbKVFkVhmHUGW8dCToY
mailbox.py was misnamed -- it is the IMAP backend, not a generic mailbox.
Introduce a mailvault.backend package:

- backend/base.py   -- shared interface (MailboxClient, MessageRef, BackupResult)
- backend/imap.py   -- the IMAP backend (was mailbox.py: ImapClient, Mailbox, ...)
- backend/graph.py  -- the MS Graph backend (moved unchanged)
- backend/__init__.py re-exports the base types

Update jobs.py and the tests accordingly (test_mailbox.py -> test_imap.py).
Import direction stays acyclic: imap -> graph -> base -> cas.

No behaviour change. Tests green on 3.11/3.12/3.13, basedpyright + ruff clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MAeRbKVFkVhmHUGW8dCToY
The project releases via GitHub Actions; the Forgejo workflow was unused. If it
is ever needed again it can be recreated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MAeRbKVFkVhmHUGW8dCToY
datetime.fromisoformat accepts a trailing `Z` from Python 3.11 on, which is the
minimum version, so the `.replace("Z", "+00:00")` shim and its 3.10 rationale
are obsolete. Drop them.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MAeRbKVFkVhmHUGW8dCToY
The per-command-group handler modules (cli/mailbox.py, cli/archive.py,
cli/copy.py) were thin stubs whose names echoed the old ib-* tools and clashed
with the new backend/imap.py. Merge them into a single cli/commands.py
(run_mailbox / run_archive / run_copy); cli/__init__.py stays the parser +
dispatcher. cli/ is now just __init__.py + commands.py.

No behaviour change. Tests green on 3.11/3.12/3.13, basedpyright + ruff clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MAeRbKVFkVhmHUGW8dCToY
…porter.py

Group the on-disk storage of a mailvault archive into a store/ package:
- store/cas.py      content-addressed blob store (was cas.py)
- store/storedb.py  metadata SQLite index (was storedb.py)
- store/__init__.py re-exports ContentAddressedStorage, StoreDatabase, ...

archive.py becomes importer.py, and its MailArchive class becomes
ExternalMailArchive: it reads *external* .eml archives (incl. Docuware) and
imports them into the store, so the old name clashed with the store being the
actual archive.

Kept the module name `storedb` (not store/db) because `db` is the connection
variable everywhere; a `db` module would shadow it.

No behaviour change. Tests green on 3.11/3.12/3.13, basedpyright + ruff clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MAeRbKVFkVhmHUGW8dCToY
…Database

Name the metadata index by its content and stop the `Store*`-inside-`store/`
stutter: store/metadb.py with MetaDatabase / MetaDatabaseConnection reads as
"the store's metadata database" without repeating the namespace.
tests/test_storedb.py -> tests/test_metadb.py.

No behaviour change. Tests green on 3.11/3.12/3.13, basedpyright + ruff clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MAeRbKVFkVhmHUGW8dCToY
Align the metadata-DB rebuild helper with the metadb naming and the CLI's
`archive rebuild-db` verb; "update" was imprecise (it rebuilds the index from
the CAS content). Point the never-used MetaDatabase path fallback at store.db
too. The CLI command `rebuild-db`, the config option `with_db`, and the
`store.db` filename stay unchanged (user-facing).

No behaviour change. Tests green on 3.11/3.12/3.13, basedpyright + ruff clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MAeRbKVFkVhmHUGW8dCToY
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MAeRbKVFkVhmHUGW8dCToY
@sniner sniner changed the title Rework: rename to mailvault, single CLI, Graph built-in (v0.6.0) Release v0.6.0: rename to mailvault, single CLI, Graph built-in, package restructure Jul 31, 2026
…en config

Structural cleanups from the 0.6.0 review, no change to the normal backup/
verify/copy flows (232 tests green):

- Move the mailbox factory out of backend/imap.py into backend/session.py
  (open_mailbox); ImapClient is now self-contained (connect/close) and no
  longer reaches into a Mailbox's private connection. The IMAP module no
  longer imports the Graph module.
- Deduplicate the per-message store/callback/result logic and the
  whole-mailbox loop into base.store_message() / base.run_full_backup(),
  shared by both backends.
- Replace the untyped metadata dict with a mailutils.MessageMetadata
  dataclass across the callback boundary.
- Replace functools.lru_cache on the metadb lookup methods with per-instance
  caches (no instance leak, no unbounded growth).
- Validate job configuration up front (conf.ConfigError): unknown backend or
  missing Graph credentials now fail early with a clear, job-named message.
- copy --idle now raises a clear error for non-IMAP sources instead of failing
  internally.
- Minor: DEFAULT_DB_NAME constant, drop dead blocksize fallback, document the
  Docuware .eml-only and YAML no-global cases, "from"/"to" address markers,
  remove metadb dev __main__ block.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RCE2sib8vw3CNzwBhD7K7M
@sniner
sniner marked this pull request as ready for review July 31, 2026 23:12
@sniner
sniner merged commit a2fe33e into main Jul 31, 2026
5 checks passed
@sniner
sniner deleted the mailvault-rework branch July 31, 2026 23:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant