No description
  • Python 97.7%
  • Dockerfile 2.3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Clément BREISCH 6d5e937ddb
All checks were successful
Mail2NC CI / Container smoke test (push) Successful in 46s
Mail2NC CI / Unit tests (push) Successful in 47s
Mail2NC CI / Publish tagged multi-arch image (push) Successful in 59s
Mail2NC CI / Security and release validation (push) Successful in 2m22s
Mail2NC CI / Promote release to minor channel and latest (push) Successful in 11s
Merge pull request 'CI: fiabiliser le contrôle post-promotion' (#5) from fix/release-verification into main
2026-08-12 01:22:48 +00:00
.github/workflows ci: make release verification reliable 2026-08-12 01:18:10 +00:00
examples ci: publish stable minor release channels (#3) 2026-08-12 00:45:52 +00:00
tests feat: make mail processing reliable and release-ready 2026-08-11 21:51:52 +00:00
.dockerignore initial commit: imap to nextcloud attachment forwarder 2026-02-11 11:13:45 +00:00
.gitignore feat: make mail processing reliable and release-ready 2026-08-11 21:51:52 +00:00
.metadata.json feat: make mail processing reliable and release-ready 2026-08-11 21:51:52 +00:00
Dockerfile ci: publish stable minor release channels (#3) 2026-08-12 00:45:52 +00:00
LICENSE feat: make mail processing reliable and release-ready 2026-08-11 21:51:52 +00:00
main.py feat: make mail processing reliable and release-ready 2026-08-11 21:51:52 +00:00
README.md ci: publish stable minor release channels (#3) 2026-08-12 00:45:52 +00:00
requirements.lock feat: make mail processing reliable and release-ready 2026-08-11 21:51:52 +00:00

Mail2NC

Mail2NC polls an IMAP mailbox over TLS and uploads message attachments to Nextcloud through WebDAV. It is designed to run as a small, non-root, distroless container.

Requirements

  • An IMAP account reachable over TLS.
  • A Nextcloud account with write access to the destination folder. Use a dedicated Nextcloud app password when OIDC or two-factor authentication is enabled.
  • A persistent, writable volume mounted at /data.
  • Registry credentials with pull access to git.bexsys.fr/clement/mail2nc.

Quick start

The hardened example uses Docker secrets and a persistent named volume:

install -d -m 0700 examples/secrets
printf '%s' 'imap-password' > examples/secrets/imap_password
printf '%s' 'nextcloud-app-password' > examples/secrets/nextcloud_password
sudo chown 65532:65532 examples/secrets/*
sudo chmod 0400 examples/secrets/*
docker compose -f examples/docker-compose.yml up -d

Never commit the files under examples/secrets/. Releases keep immutable SemVer tags such as v2.0.1 and commit tags such as sha-<commit>. Runtime deployments track the floating minor channel :v2.0: only an exact digest which has passed both vulnerability scanners and the multi-architecture check is promoted to :v2.0 and :latest. Serialized release guards refuse to move the minor channel or :latest back to an older SemVer version.

Local Docker Compose implements file-backed secrets as bind mounts, so the source files must be readable by container UID 65532. Keep their containing directory restricted and their files owned by that UID as shown above.

Configuration

Credentials and service configuration support the _FILE suffix. For example, IMAP_PASS_FILE=/run/secrets/imap_password reads the secret from that file. If both forms are present, the file takes precedence. LOG_LEVEL, STATE_FILE and HEALTH_FILE are read directly from the environment; path overrides must therefore be set directly for both the process and its container health check.

Required settings

Variable Description
IMAP_HOST IMAP server hostname.
IMAP_USER IMAP login name.
IMAP_PASS IMAP password.
NEXTCLOUD_URL HTTPS Nextcloud base URL, without the WebDAV suffix.
NEXTCLOUD_USER Nextcloud user used for WebDAV.
NEXTCLOUD_PASS Nextcloud password or, preferably, app password.

Optional settings

Variable Default Description
IMAP_PORT 993 IMAP TLS port.
IMAP_FOLDER INBOX Mailbox to monitor.
LEGACY_UIDVALIDITY unset Required only to migrate a legacy last_uid state; it must exactly match the selected mailbox.
NEXTCLOUD_FOLDER /Scans Destination below the user's Nextcloud files root.
POLL_INTERVAL 30 Seconds between successful polling attempts.
CONNECT_TIMEOUT 10 Network connection timeout in seconds.
READ_TIMEOUT 60 Network read timeout in seconds.
MAX_ATTACHMENT_BYTES 52428800 Maximum decoded attachment size (50 MiB).
MAX_MESSAGE_BYTES 73400320 Maximum raw RFC 822 message size (70 MiB), checked before and after body fetch.
MAX_TOTAL_ATTACHMENT_BYTES 52428800 Maximum cumulative decoded attachment size per message (50 MiB).
MAX_ATTACHMENTS 20 Maximum number of attachments per message.
NO_ATTACHMENT_POLICY halt halt leaves the message unread and stops the batch; mark_seen explicitly skips it.
BACKOFF_MAX 300 Maximum retry delay after consecutive poll failures.
STATE_FILE /data/state.json Durable processing-state path. Keep it on the persistent volume.
HEALTH_FILE /data/status.json Health-state path. Keep it on the persistent volume.
HEALTH_MAX_AGE max(180, 3 × POLL_INTERVAL) Maximum age of the last successful poll.
LOG_LEVEL INFO Python logging level.

NEXTCLOUD_URL must use HTTPS. IMAP TLS and HTTPS certificates are validated against the container trust store.

Processing guarantees

Mail2NC processes UIDs in ascending order and stops at the first message it cannot finish. It only advances its durable watermark after the message is handled successfully, preventing a later UID from permanently hiding an earlier failure.

Attachments with a filename are accepted whether they are declared as attachment or inline. Filenames are sanitized before being encoded into the WebDAV URL. Upload, folder-creation, IMAP search/fetch/store and login results are checked explicitly.

A message with no usable attachment is fail-closed by default: it remains unread, the watermark does not advance and the current batch stops. Set NO_ATTACHMENT_POLICY=mark_seen only when silently skipping such messages is the intended behavior.

Mail2NC uses the standard Nextcloud endpoint /remote.php/dav/files/{user}/...; see the Nextcloud WebDAV documentation.

State and recovery

The named volume contains two small JSON files:

  • /data/state.json: version 2 state with the mailbox UIDVALIDITY and last committed UID.
  • /data/status.json: last attempt, last successful poll and consecutive failure count.

{"last_uid": N} is considered ambiguous and is never migrated automatically. Set LEGACY_UIDVALIDITY to the value independently verified on the selected mailbox; migration proceeds only when the two values match exactly. Once the state is version 2, that compatibility setting is ignored.

A changed UIDVALIDITY, invalid JSON or unreadable state stops processing instead of resetting to UID zero. Back up /data, inspect the mailbox and resolve the state deliberately; deleting the state can replay old messages and attachments.

Health check

The image health check runs:

/usr/bin/python3 /app/main.py --healthcheck

It becomes unhealthy for fatal state errors, after three consecutive poll failures, or when the last successful poll is too old. A process that is alive but cannot reach IMAP or Nextcloud will therefore not remain falsely healthy.

Local validation

python3 -m pip install --require-hashes -r requirements.lock
python3 -m unittest discover --start-directory tests --verbose
docker build --tag mail2nc:dev .

The CI workflow runs unit tests and an amd64 smoke build for pull requests and branch pushes. A stable SemVer tag such as v2.0.1 publishes both :v2.0.1 and :sha-<commit>. Trivy and Grype are pinned by digest, fail closed on scan or parse errors, reject every fixable vulnerability and allow no count increase above the reviewed upstream-only baseline. Repository misconfiguration and secret scans run as part of the same gate. Only then is that exact scanned multi-architecture digest promoted to the matching minor channel (for example :v2.0) and to :latest. The serialized SemVer guard prevents an older patch from moving its minor channel backwards and prevents any older stable release from moving :latest backwards; pushing main never publishes.

License

MIT