All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog.
mxctl --versionnow reports the correct version at runtime (was stuck at0.3.0since the rename)- Consolidated duplicate
get_headers()/get_raw_headers()code in system.py - Moved delayed inline imports to module level in actions.py (subprocess, sys, get_gmail_accounts)
- Fixed ARCHITECTURE.md stale claim: "Six" → "Five" pragma:no cover guards
- Public Python API — new
mxctl.apimodule with 56 importable functions for programmatic access to Apple Mail. All command internals refactored to separate data retrieval from CLI presentation. External projects can nowfrom mxctl.api import get_messages, read_messagewithout any CLI or argparse dependency.
- README cleanup — removed personal email from examples, trimmed redundant sections, tightened AI workflow documentation (488 → 434 lines)
ai-setupcommand — interactive wizard to configure Claude Code, Cursor, or Windsurf to use mxctl; detects existing config files, previews the snippet before writing, supports--jsonai-setup --printflag — dump the raw snippet to stdout for piping into Ollama Modelfiles, Aider prompts, system prompt files, or clipboard- "Pointing Your AI Assistant to mxctl" section in README —
mxctl ai-setupwalkthrough plus manual snippet for Claude Code, Cursor, Windsurf, and local AI tools
- Renamed project —
my-apple-mail-cliis nowmxctl("mail control") - Flattened CLI —
my mail inboxis nowmxctl inbox(removedmailsubcommand layer) - Config path — moved from
~/.config/my/to~/.config/mxctl/with automatic one-time migration - Version bump — 0.2.0 → 0.3.0 (breaking: new binary name and command structure)
- Package name — Python package renamed from
my_clitomxctl - GitHub repo —
Jscoats/my-apple-mail-cli→Jscoats/mxctl
- Published to PyPI —
pip install mxctlnow works - 100% test coverage — 655 tests across 20 test files (up from 422), measured with pytest-cov
- 5 demo GIFs — main commands, init wizard, AI triage, batch delete, newsletter unsubscribe (all scripted with fictional data)
- Feature comparison table — "Why Not X?" section with mxctl vs mutt vs Gmail API vs raw AppleScript
- Automated release workflow —
release.ymlcreates GitHub Release with changelog on tag push - Pre-commit hooks — ruff check + ruff format run on every commit
- Dependabot — weekly updates for pip and GitHub Actions dependencies
- pytest-cov — coverage reporting in CI and locally
- Ruff config —
[tool.ruff]in pyproject.toml with 7 rule sets (E, F, W, I, UP, B, SIM) - PyPI classifiers — 12 classifiers and 7 keywords for discoverability
- Centered README header — styled
<h1>with 5 badges (PyPI, CI, Python, coverage, license) - README table of contents — 14 sections linked
top-senderscommand — rank senders by frequency with--limit N,--json, and optional mailbox filterbatch-delete --from-sender EMAILflag — delete by sender across all mailboxes, combinable with--older-thanNOREPLY_PATTERNScentralized inconfig.py— single source of truth used bytriage,process-inbox,clean-newsletters, andweekly-reviewTEMPLATES_FILEpath centralized inconfig.py— imported bytemplates.pyandcompose.py- File locking (
_file_lock) applied to all JSON state reads and writes —config.json,state.json,mail-undo.json,mail-templates.json - 30 new tests covering templates CRUD, draft error paths, and batch dry-run edge cases (196 total)
- Silent output failures — three
cmd_mailboxes,_list_rules, andweekly-reviewAppleScript strings were plain strings instead of f-strings;FIELD_SEPARATORwas passed as literal text causing commands to silently return empty results batch-deleteindexed iteration bug — list shifts after each delete causing skips and crashesbatch-deleteGmail All Mail error — individual deletions wrapped intry/end tryso one IMAP failure doesn't abort the whole batchbatch-deletemessage IDs now logged only after a successful delete (previously logged before, so failed deletions appeared in the undo log)batch-move— no error resilience; inner loop now wrapped intry/end trymatchingbatch-deletepatternbatch-move—--limitonly exited inner loop, allowing more messages to be processed than requested; outer loop now also checks limitbatch-moveandbatch-delete— dry-run reported total matching count instead of effective count when--limitis setcmd_not_junk— hardcoded"Junk"mailbox name broke on Gmail accounts; now usesresolve_mailbox()for[Gmail]/Spamtranslationcmd_move— source and destination mailbox names not translated for Gmail accounts;resolve_mailbox()now applied to bothtriage,process-inbox,clean-newsletters,weekly-review— noreply pattern matching ran against full sender string including display name; now usesextract_email()to match against email address onlycompose.py— template file read incmd_drafthad no JSON error handling; corrupt file now produces a friendly errorcompose.py— template file read now uses_file_lock(previously read outside locking protocol)_file_lock— file handle leaked on each failed retry attempt; fixed withwith open(...)context managersetup.pycmd_init— config written with bareopen()bypassing_file_lock; now uses_save_json()_load_json— reads were not locked (writes were); now symmetriclistcommand now accepts-m/--mailboxflag (was positional-only, inconsistent with all other commands)inboxandtriagenow accept-a/--accountto scope results to a single account- Gmail mailbox name mapping —
initnow asks which accounts are Gmail;Spam,Trash,Sent,Archiveetc. auto-translate to[Gmail]/...equivalents accounts --jsonreturning empty[]instead of account datadie()return type corrected toNoReturn- Raw
\x1F/\x1FEND\x1Fliterals in AppleScript strings replaced withFIELD_SEPARATOR/RECORD_SEPARATORconstants throughout - Removed dead code: unused
_convert_dates,account_iterator,single_message_lookupfunctions; unused variable assignments intodoist_integration.pyandsystem.py
- First-run setup —
initwizard for account detection and optional Todoist token configuration - Account management —
inbox,accounts,mailboxes,create-mailbox,delete-mailbox,empty-trash,countfor scripting and status bars - Message operations —
list,read,search,mark-read,mark-unread,flag,unflag,move,delete,openfor GUI access - AI-powered features —
summary,triage,context,find-related,process-inbox - Batch operations with undo —
batch-read,batch-flag,batch-move,batch-delete,undo - Analytics —
stats,digest,show-flagged,weekly-review,clean-newsletters - Compose & templates —
draftwith template support,reply,forward,templatessubcommands - Integrations —
to-todoistfor sending emails as Todoist tasks,exportfor Mbox format - System tools —
check,headers,rules,junk,not-junk - Multi-account support with three-tier account resolution
--jsonoutput mode on every command- Comprehensive test suite (166 tests)
- Zero runtime dependencies (Python stdlib only)