A simple IMAP client, scanning defined mailboxes for XRechnung attachments
  • Go 96.1%
  • Shell 3.5%
  • Go Template 0.4%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Heiko Schlittermann (ai) ac0577bb00
All checks were successful
nagonag (Push) / nagonag (push) Successful in 4m46s
refactor: config.Secret type, Options for every app entry point ai:claude-sonnet-5
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
2026-08-01 18:35:20 +02:00
.claude docs: consolidate agent guidance 2026-05-11 20:34:40 +02:00
.codex refactor: replace pkg/imapclient with go.schlittermann.de/heiko/imapclient ai:claude-sonnet-5 2026-08-01 14:37:21 +02:00
.forgejo/workflows cicd: workflow renamed 2026-05-19 08:42:36 +02:00
.gemini docs: consolidate agent guidance 2026-05-11 20:34:40 +02:00
cmd/xr-invoiced refactor: config.Secret type, Options for every app entry point ai:claude-sonnet-5 2026-08-01 18:35:20 +02:00
debian/source build: add lintian override 2026-05-15 22:59:58 +02:00
deploy systemd: comment out default service options 2026-05-12 23:58:23 +02:00
doc refactor: declutter root into internal/ packages, wire hslog ai:claude-sonnet-5 2026-08-01 14:26:16 +02:00
internal refactor: config.Secret type, Options for every app entry point ai:claude-sonnet-5 2026-08-01 18:35:20 +02:00
scripts imapclient: fix NOTIFY cleanup and reduce capability check duplication 2026-05-12 21:31:40 +02:00
.gitignore ignore the config.yaml 2026-05-11 17:26:13 +02:00
.gogogo.conf refactor: declutter root into internal/ packages, wire hslog ai:claude-sonnet-5 2026-08-01 14:26:16 +02:00
.golangci.yml lint: no golines formatter 2026-05-11 23:14:24 +02:00
config.example.yaml build: new .gogogo.conf 2026-05-13 23:34:24 +02:00
config_path_docs_regression_test.go refactor: declutter root into internal/ packages, wire hslog ai:claude-sonnet-5 2026-08-01 14:26:16 +02:00
go.mod deps: bump hslog for the jsonl emphasis-leak fix ai:claude-sonnet-5 2026-08-01 18:29:38 +02:00
go.sum deps: bump hslog for the jsonl emphasis-leak fix ai:claude-sonnet-5 2026-08-01 18:29:38 +02:00
LICENSE add license 2026-05-14 17:58:11 +02:00
PLAN.md cleanup 2026-05-23 16:36:11 +02:00
README.md refactor: replace pkg/imapclient with go.schlittermann.de/heiko/imapclient ai:claude-sonnet-5 2026-08-01 14:37:21 +02:00

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/idle source 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)
  • 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

  1. Startup: Loads config, connects to IMAP (one connection per mailbox)
  2. Initial scan: On first run or after UIDVALIDITY change, scans all existing messages; logs are marked scan=initial
  3. 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
  4. State persistence: Saves per-mailbox UIDVALIDITY + LastUID to JSON file
  5. IDLE loop: Enters IMAP IDLE and listens for new messages; after wake-up, every processed message is logged with scan=idle
  6. Message processing: For each new message:
    • Fetches full body + headers
    • Walks MIME attachments
    • Checks each attachment for XRechnung
    • Applies custom flag if match found
  7. Reconnection: On IDLE timeout (≈28 min) or connection drop, auto-reconnects and resumes
  8. Heartbeat: Every 60 minutes per mailbox, emits heartbeat with counters for the previous 60-minute window:
    • processed, tagged, errors, reconnects
    • initial, 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

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.