- Go 96.1%
- Shell 3.5%
- Go Template 0.4%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
All checks were successful
nagonag (Push) / nagonag (push) Successful in 4m46s
Two simplifications from the design review. config.Secret: the previous IMAPConfig redaction enumerated all eight fields three times over (String, GoString, LogValue), so every new field meant three edits and a silent observability gap if one was missed. Replaced with a Secret string type carrying the redaction itself. fmt walks into struct fields and honours each field's Stringer/GoStringer, so %v/%+v/%#v now redact automatically at any nesting depth, for any struct holding a Secret, with no per-struct cooperation - verified by a test using a locally declared struct rather than IMAPConfig. IMAPConfig.LogValue stays, because slog does *not* descend into a struct's fields looking for LogValuer: logging cfg.IMAP directly would reflect the struct and print the password despite Secret's own LogValue. That is now the only enumeration, and it is fail-safe (a field missing from the list is omitted from output, not leaked). IMAPConfig.String and GoString are gone; Secret covers them. Secret being a distinct type also means the compiler located every place the plaintext is needed; those now read cfg.IMAP.Password.Reveal(), which is greppable. mapstructure decodes YAML/env into it unchanged (string kind), so config loading and the existing Load tests are unaffected. Options everywhere: Doctor, List and RemoveKeywords took DebugIMAP and ExposeCredentials as adjacent positional booleans, where a transposition would silently enable credential exposure in the debug trace and still compile. They now take the same Options struct Run and RunMailbox already used, so callers name what they set. main() builds one Options and passes it to all four, removing the duplicated literal it previously constructed only for Run. (co)authored by ai:claude-sonnet-5 |
||
| .claude | ||
| .codex | ||
| .forgejo/workflows | ||
| .gemini | ||
| cmd/xr-invoiced | ||
| debian/source | ||
| deploy | ||
| doc | ||
| internal | ||
| scripts | ||
| .gitignore | ||
| .gogogo.conf | ||
| .golangci.yml | ||
| config.example.yaml | ||
| config_path_docs_regression_test.go | ||
| go.mod | ||
| go.sum | ||
| LICENSE | ||
| PLAN.md | ||
| README.md | ||
XRechnung IMAP Scanner
A Go service that connects to an IMAP server, monitors mailboxes in real-time via IMAP IDLE, detects XRechnung invoices (UBL / UN-CEFACT CII XML and Factur-X hybrid PDFs), and applies a custom IMAP flag to matching messages for easy filtering in mail clients.
User Guide — Thunderbird setup: see and filter tagged invoices
Administration Guide — installation, configuration, deployment
Features
- IMAP IDLE listener: Real-time monitoring of incoming mail; auto-reconnect on connection drop
- Multiple mailboxes: Monitor several mailboxes in parallel, each with its own IMAP connection
- Mailbox heartbeat logs: Every 60 minutes per mailbox, logs a summary window with processed/tagged/errors/reconnects and
initial/startup/idlesource counters - XRechnung detection:
- UBL Invoice (namespace:
urn:oasis:names:specification:ubl:schema:xsd:Invoice-2) - UN-CEFACT CII (namespace:
urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100) - CustomizationID validation against XRechnung profiles
- Factur-X/ZUGFeRD hybrid PDFs (embedded XML via PDF object model, pdfcpu)
- UBL Invoice (namespace:
- Persistent state: Tracks
UIDVALIDITY+ last-scanned UID per mailbox; no re-scan on restart - Custom IMAP keyword: Configurable keyword name (default:
ius-xrechnung) - Secure deployment: systemd unit with hardening, env var config, graceful shutdown
Quick Start
go build -o xr-invoiced ./cmd/xr-invoiced/
./xr-invoiced --doctor # verify IMAP credentials
./xr-invoiced --since "last month" # preview what would be tagged
./xr-invoiced --apply # tag messages for real, enter IDLE
How It Works
- Startup: Loads config, connects to IMAP (one connection per mailbox)
- Initial scan: On first run or after
UIDVALIDITYchange, scans all existing messages; logs are markedscan=initial - State/Resume scan: On normal startup, resumes from saved state and scans only messages newer than the last-seen UID; logs are marked
scan=startup - State persistence: Saves per-mailbox
UIDVALIDITY+LastUIDto JSON file - IDLE loop: Enters IMAP IDLE and listens for new messages; after wake-up, every processed message is logged with
scan=idle - Message processing: For each new message:
- Fetches full body + headers
- Walks MIME attachments
- Checks each attachment for XRechnung
- Applies custom flag if match found
- Reconnection: On IDLE timeout (≈28 min) or connection drop, auto-reconnects and resumes
- Heartbeat: Every 60 minutes per mailbox, emits
heartbeatwith counters for the previous 60-minute window:processed,tagged,errors,reconnectsinitial,startup,idle
Logging
All log output goes to stdout only by default. --log-format=syslog/color-syslog is available if you want syslog-style <priority> framing on stdout, but auto-detection never selects it on its own.
The output format is autodetected based on the runtime environment:
│ Condition │ Format │ Description │
│──────────────────────────────────│─────────────────│──────────────────────────────────────│
│ systemd unit (auto) │ journal │ native journal export, structured fields, journalctl KEY=value filterable │
│ stdout is a terminal (auto) │ color-plain │ human-readable, ANSI colors │
│ otherwise, e.g. redirected (auto)│ jsonl │ structured JSON, one object per line │
Override with --log-format <format>: auto (default), plain/color-plain, jsonl/color-jsonl, syslog/color-syslog, journal/color-journal.
Architecture
cmd/xr-invoiced/main.go Entry point; flag parsing; logging setup; signal handling
internal/app/app.go Orchestration: doctor/list/remove-keyword, per-mailbox scan+IDLE loop
internal/config/config.go Viper-based YAML + env override
internal/heartbeat/heartbeat.go Per-mailbox activity counters + periodic heartbeat log
internal/infomessage/ One-time "tagging is active" info message
internal/journalhandler/ Native systemd journal socket handler (structured fields)
internal/traceio/ Truncating writer for --debug=imap wire traces
internal/scanner/scanner.go Orchestrator: fetch → walk → detect → flag
internal/scanner/attachment.go MIME part walker
internal/state/state.go Per-mailbox UIDVALIDITY + LastUID persistence
internal/xrechnung/detector.go XML namespace + CustomizationID validation
internal/xrechnung/pdf.go Factur-X/ZUGFeRD PDF embedded-file extraction (pdfcpu)
IMAP client (TLS/STARTTLS, IDLE, fetch, search, store) is go.schlittermann.de/heiko/imapclient, an external dependency rather than an in-repo package.
Known Limitations & Future Work
- SASL mechanisms: Uses plain LOGIN; no OAuth2/SASL XOAUTH2 support
- Email notifications: No alerting on scan errors; structured log output only
- Parallel processing: Messages are processed sequentially; parallel fetch/detect is a planned enhancement
License
MIT (or your chosen license)
References
- IMAP RFC 3501 & RFC 2177 (IDLE)
- XRechnung Specification
- Factur-X / ZUGFeRD Standard
- go-imap Documentation
Glossary
| Term | Meaning |
|---|---|
| Flag | Generic IMAP per-message attribute (RFC 3501). Covers system flags and keywords. |
| System flag | Predefined by the RFC, backslash-prefixed: \Seen, \Answered, \Flagged, \Deleted, \Draft. |
| Keyword | User-defined flag without backslash, e.g. ius-xrechnung. This is what xr-invoiced sets. |
| Tag | Thunderbird's UI name for an IMAP keyword. Same data, different vocabulary. |