Italiano: README.it.md — versione ridotta.
Command-line client for PEC (Posta Elettronica Certificata — Italian
certified email), built for both humans and AI agents. Designed to be
context-efficient: the default output strips empty fields, and --json
produces NDJSON suitable for piping into LLMs or jq.
Talks to the standard IMAP/SMTP endpoints exposed by Italian PEC providers (Aruba, Legalmail/InfoCert, Namirial, Register.it, Poste Italiane, Pec.it), all over SSL/TLS.
PEC (Posta Elettronica Certificata) is the legal-email standard for Italian businesses and professionals — used daily for invoices, official notices, contracts, public administration communications. Every Italian SME has one.
Programmatic access is fragmented: each provider (Aruba, Poste, Legalmail, Namirial, ...) ships its own SDK or webmail-only interface. Open-source tools that let AI agents send, receive, and track PEC messages are essentially non-existent.
pec-cli fills that gap:
- Agent-friendly: NDJSON output, stable exit codes, errors on stderr — pipe it into Claude, jq, or any LLM workflow.
- Human-friendly: compact text output, one command per common task (send, list, fetch, trace).
- Italian-native: built for the way Italian businesses actually use PEC — formal communications, legal evidence, document workflows.
Part of MayAI.
- Deterministic Message-ID for idempotent sends. SHA-256 of
(from, to, cc, subject, body, minute-of-send)— accidental retries collapse to the same id and providers deduplicate. - Multi-provider abstraction. Aruba, Legalmail, Namirial, Register.it, Poste, Pec.it share one CLI; presets in docs/PROVIDERS.md.
- Retry with exponential backoff. Transient IMAP / SMTP failures retry
up to 3 times (
1s, 2s, 4s, ...capped at 30s). Permanent failures (auth, SMTP 5xx) propagate immediately. defusedxmlfor daticert.xml parsing. Neutralizes XXE / billion-laughs even though the certification chain is trusted — the input is still network-sourced.- Mypy strict, zero unmotivated ignores. Strict baseline +
disallow_subclassing_any,strict_equality,extra_checks. The only# type: ignorecomments live in test code, each with a specific code and a documented reason.
- 151 tests, multi-OS / multi-Python (Ubuntu / macOS / Windows × 3.11 / 3.12 / 3.13) via GitHub Actions.
- ~86 % branch coverage on the production module (
pec_cli/), tracked in CI. - Credentials in the system keyring (macOS Keychain, Linux Secret Service, Windows DPAPI). Fernet-encrypted file as a headless fallback.
- Python 3.11+
- A working PEC account from one of the supported providers
pip install mayai-pec-cliInstalls a single pec command (plus pec-mcp for MCP servers) on your
PATH.
pec-cli ships with a native MCP server, letting AI agents like Claude access your PEC inbox directly — no subprocess, no JSON parsing.
Add to ~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"pec": {
"command": "/path/to/pec-mcp"
}
}
}Find your path with which pec-mcp.
| Client | Status |
|---|---|
| Claude Desktop | Tested |
| Cursor | Compatible (same stdio config) |
| Continue (VS Code) | Compatible (same stdio config) |
| Zed | Compatible (same stdio config) |
| ChatGPT | MCP support coming soon |
| Tool | Description |
|---|---|
pec_list |
List messages (folder, unread_only, limit) |
pec_get |
Get full message with body and cert |
pec_search |
Search by query in subject / from / body / all + optional date |
pec_list_folders |
List folders, optionally with message and unseen counts |
pec_send |
Send a PEC (requires confirm_legal_send=True) |
pec_mark_read |
Mark a message as read |
pec_mark_unread |
Mark a message as unread |
pec_move |
Move a message between folders (validates destination) |
pec_trace |
Trace receipt chain by message ID |
pec_auth_status |
Check authentication status |
# Authenticate — password prompted interactively, never passed as a flag.
pec auth login --address mia@pec.it --provider aruba
# List recent PECs as NDJSON; filter unread / by date.
pec --json list --unread --from 2025-01-01 --limit 50
# Read one message and save its attachments.
pec get 1234 --save-attachments ./attachments
# Include the parsed daticert.xml certification.
pec get 1234 --cert --json
# Trace the acceptance / delivery chain for an original message id.
pec trace 'opec123.20260321102500.12345.67.1.1@pec.it'
# Send a PEC with attachment (interactive confirmation by default).
pec send --to dest@pec.it --subject "Oggetto" --file body.txt --attach doc.pdf
# Find PECs from INPS in April 2026, then archive one of them.
pec search "INPS" --field from --from-date 2026-04-01
pec move 1234 --to "[PEC]/Archivio"| Command | Description |
|---|---|
pec auth login --address ADDR --provider P |
Prompt for password, verify via IMAP, save credentials in the system keyring (Fernet-encrypted file as fallback). |
pec auth status |
Show whether credentials are present. |
pec auth logout |
Delete saved credentials (keyring entry + any local encryption key). |
pec list [--folder F] [--unread] [--from YYYY-MM-DD] [--limit N] |
List PEC messages (default folder inbox, default limit 20). |
pec get <id> [--folder F] [--save-attachments DIR] [--cert] |
Fetch a single PEC by IMAP UID; --cert includes the parsed daticert.xml certification; --save-attachments writes attachments to DIR. |
pec trace <message-id> [--folder F] [--limit N] |
Find every receipt in the folder whose daticert.xml references this message id, ordered chronologically (accettazione → presa-in-carico → avvenuta-consegna / errore-consegna). |
pec send --to ADDR --subject S (--body T | --file F) [--attach F] [--cc ADDR] [--dry-run] [--yes] |
Send a PEC; --to, --cc, --attach are repeatable. See safety note below. |
pec search QUERY [--field subject|from|body|all] [--folder F] [--limit N] [--from-date YYYY-MM-DD] |
Search messages by content (default --field all = subject ∪ from ∪ body). |
pec list-folders [--counts] |
List available IMAP folders. --counts adds message and unseen counts per folder. |
pec mark-read <id> [--folder F] |
Mark a message as read (set \Seen). Idempotent. |
pec mark-unread <id> [--folder F] |
Mark a message as unread (clear \Seen). Idempotent. |
pec move <id> --to FOLDER [--from FOLDER] |
Move a message between folders. Uses IMAP MOVE when available, otherwise COPY + STORE \Deleted + EXPUNGE. |
PEC has the legal value of a registered letter (raccomandata). To avoid accidental sends:
- Interactive TTY:
pec sendprompts for confirmation before contacting SMTP. - Non-TTY (CI, pipes, scripts): requires
--yesexplicitly. Exit code3otherwise. --dry-runvalidates the message without contacting SMTP.- The MCP
pec_sendrequiresconfirm_legal_send=Trueand is rate-limited to 3 sends per recipient per 5 minutes per session.
The deterministic Message-ID means an immediate retry of identical content
collapses to one logical email — pair with pec trace to follow the chain.
Transient IMAP / SMTP failures (socket timeouts, server [TRYAGAIN], SMTP
4xx) retry up to 3 times with 1s, 2s, 4s, ... backoff capped at 30s.
Permanent failures (auth, SMTP 5xx) propagate immediately. SMTP retries
reuse the same MIME envelope built before the loop, so the Message-ID is
stable across attempts and providers deduplicate correctly. Pass
--verbose for retry events on stderr.
These work in any position (before or after the subcommand):
| Flag | Effect |
|---|---|
--json |
Emit one JSON object per line (NDJSON). |
--verbose |
Log IMAP/SMTP timings, retry events, and certification metadata to stderr. |
-h, --help |
Show help for the current command. |
| Code | Meaning |
|---|---|
0 |
Success |
1 |
Application error (network, send failure, bad arguments) |
2 |
Not authenticated — run pec auth login |
3 |
Refused to send: non-interactive shell without --yes |
Aruba PEC, Legalmail (InfoCert), Namirial, Register.it, Poste Italiane,
Pec.it. Endpoints and --provider values: docs/PROVIDERS.md.
pec auth login prompts for the password on stderr (never echoed, never on
argv), verifies it via IMAP, and stores it in the system keyring under
service name mayai-cli-pec. On headless boxes without a keyring backend,
it transparently falls back to a Fernet-encrypted file at
~/.config/mayai-cli/pec/credentials.json (mode 0600).
Full details — keyring fallback, key migration, file locations: docs/AUTHENTICATION.md.
- Default — compact human-readable text. Empty / null fields are stripped so terminal output stays scannable.
--json— NDJSON. One object per line; lists stream one element per line so consumers can process incrementally.--verbose— adds protocol timing lines on stderr, surfaces hidden certification attachments (daticert.xml,postacert.eml,smime.p7s/p7m), and emits retry events from thepec.retrylogger.
Errors always go to stderr, prefixed with error:.
git clone https://github.com/mayai-it/pec-cli.git
cd pec-cli
make dev # install -e .[dev]
make test # pytest
make lint # ruffMypy is run via mypy pec_cli/ (configured in pyproject.toml).
Contributing guide and PR checklist: CONTRIBUTING.md.
- docs/AUTHENTICATION.md — keyring, Fernet fallback, file locations
- docs/PROVIDERS.md — IMAP/SMTP endpoints per provider
- docs/FAQ.md — common questions and gotchas
- CHANGELOG.md — release notes
- Issues — bug reports and feature requests
MIT — see LICENSE.
