commit 74733b36b795e2ed2ff03f59a78e22419f315789 Author: Rikdekker Date: Wed May 27 07:55:56 2026 +0200 Initial commit: bulk-link mail accounts via occ Wrapper script around `occ mail:account:create` to bulk-link IMAP/SMTP accounts for multiple Nextcloud users from a CSV file. Includes README with usage, limitations (no connection test, no initial sync), and .gitignore that blocks real credential CSVs. Co-Authored-By: Claude Opus 4.7 (1M context) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6ba7abf --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +# Echte CSV's met credentials nooit committen +accounts.csv +accounts.*.csv +!accounts.example.csv + +# OS / editor +.DS_Store +*.swp diff --git a/README.md b/README.md new file mode 100644 index 0000000..920d804 --- /dev/null +++ b/README.md @@ -0,0 +1,84 @@ +# Nextcloud-ConnectMail + +Bulk-link IMAP/SMTP mailaccounts voor Nextcloud-gebruikers via `occ mail:account:create`. + +Bedoeld voor admins die in één keer mail-accounts willen koppelen voor meerdere users — +bijvoorbeeld bij onboarding, migratie, of provisioning op basis van een externe lijst. + +## Wanneer dit script gebruiken + +De Nextcloud Mail-app biedt **geen** REST-endpoint om als admin een account voor een +andere user te koppelen. Wel bestaat `occ mail:account:create ...`, maar dat +is positioneel en omslachtig voor bulk-werk. Dit script wrapt die call en leest een CSV. + +Alternatieven: +- **Provisioning-templates** (admin-UI in Mail): één template per domein, géén + per-user credentials. Werkt alleen als alle users hetzelfde mail-cluster delen. +- **Eigen REST-controller**: vereist een patch op de Mail-app. + +## Vereisten + +- Shell-toegang tot de Nextcloud-server (rechtstreeks of via SSH/docker exec) +- `occ` uitvoerbaar als web-user (meestal `www-data`) +- De Nextcloud Mail-app geïnstalleerd en geactiveerd +- `python3` op de host waar het script draait (voor RFC-4180 CSV-parsing) + +## Gebruik + +```bash +OCC_CMD="docker exec -u www-data nc-dev php occ" \ + ./mail-bulk-link.sh accounts.csv +``` + +Voor classic installs: + +```bash +OCC_CMD="sudo -u www-data php /var/www/nextcloud/occ" \ + ./mail-bulk-link.sh accounts.csv +``` + +Remote via SSH: + +```bash +OCC_CMD="ssh hetzner-ax42 docker exec -u www-data nc-dev php occ" \ + ./mail-bulk-link.sh accounts.csv +``` + +## CSV-formaat + +Header verplicht, kolomvolgorde vrij. Verplichte kolommen: + +``` +userId,name,email,imapHost,imapPort,imapSsl,imapUser,imapPass,smtpHost,smtpPort,smtpSsl,smtpUser,smtpPass +``` + +Optionele kolom: `authMethod` (`password` of `xoauth2`, default `password`). + +- `imapSsl` / `smtpSsl`: `ssl`, `tls`, of `none` +- Regels die beginnen met `#` worden overgeslagen +- Quote velden met komma's volgens RFC 4180 (`"foo,bar"`) + +Zie [accounts.example.csv](accounts.example.csv) voor een voorbeeld. + +## Beperkingen + +- **Geen connectie-test.** De `occ`-command slaat blind op — foute credentials worden + geaccepteerd. De user merkt het pas bij de eerste sync. De REST-route (`POST + /api/accounts`) doet wel een live IMAP/SMTP-test, maar werkt alleen voor de + ingelogde user zelf. +- **Geen initial mailbox-sync.** Wordt door de REST-route wel getriggerd, door + `occ` niet. Eerste sync gebeurt bij de eerstvolgende background-job of bij eerste + login van de user. +- **Wachtwoorden in CSV.** Plaintext op disk tijdens uitvoer. Na de run: + `shred -u accounts.csv`. Nooit committen. + +## Security + +- Voeg `accounts.csv` (en varianten) toe aan `.gitignore` van je deploy-repo +- Draai dit alleen vanuit een omgeving waar je sowieso admin-rechten hebt +- Voor productie: overweeg de credentials uit een secrets-store te halen en de + CSV on-the-fly te genereren in tmpfs + +## Licentie + +AGPL-3.0-or-later (consistent met Nextcloud Mail). diff --git a/accounts.example.csv b/accounts.example.csv new file mode 100644 index 0000000..6c795b3 --- /dev/null +++ b/accounts.example.csv @@ -0,0 +1,4 @@ +userId,name,email,imapHost,imapPort,imapSsl,imapUser,imapPass,smtpHost,smtpPort,smtpSsl,smtpUser,smtpPass +alice,Werk,alice@voorbeeld.nl,imap.voorbeeld.nl,993,ssl,alice@voorbeeld.nl,geheim1,smtp.voorbeeld.nl,587,tls,alice@voorbeeld.nl,geheim1 +bob,Werk,bob@voorbeeld.nl,imap.voorbeeld.nl,993,ssl,bob@voorbeeld.nl,geheim2,smtp.voorbeeld.nl,587,tls,bob@voorbeeld.nl,geheim2 +# carol,Vakantie,carol@voorbeeld.nl,imap.voorbeeld.nl,993,ssl,carol@voorbeeld.nl,geheim3,smtp.voorbeeld.nl,587,tls,carol@voorbeeld.nl,geheim3 diff --git a/mail-bulk-link.sh b/mail-bulk-link.sh new file mode 100755 index 0000000..9e4f61b --- /dev/null +++ b/mail-bulk-link.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash +# +# Bulk-link IMAP/SMTP accounts in Nextcloud Mail via `occ mail:account:create`. +# +# Usage: +# OCC_CMD="docker exec -u www-data nc-dev php occ" ./mail-bulk-link.sh accounts.csv +# OCC_CMD="sudo -u www-data php /var/www/nextcloud/occ" ./mail-bulk-link.sh accounts.csv +# +# CSV format (header required, exact column names): +# userId,name,email,imapHost,imapPort,imapSsl,imapUser,imapPass,smtpHost,smtpPort,smtpSsl,smtpUser,smtpPass[,authMethod] +# +# - imapSsl/smtpSsl: ssl | tls | none +# - authMethod: optional, defaults to "password" (use "xoauth2" for OAuth) +# - Lines starting with # and empty lines are skipped. +# - Quote fields containing commas with double quotes per RFC 4180. +# - Passwords with shell-special chars are safe — they're passed as argv, not eval'd. + +set -euo pipefail + +OCC_CMD="${OCC_CMD:-}" +if [[ -z "$OCC_CMD" ]]; then + echo "ERROR: Set OCC_CMD env var, e.g.:" >&2 + echo " OCC_CMD=\"docker exec -u www-data nc-dev php occ\" $0 accounts.csv" >&2 + exit 2 +fi + +if [[ $# -ne 1 ]]; then + echo "Usage: OCC_CMD=\"...\" $0 " >&2 + exit 2 +fi + +CSV="$1" +if [[ ! -r "$CSV" ]]; then + echo "ERROR: cannot read $CSV" >&2 + exit 2 +fi + +# Read header and build column index map +IFS=, read -r -a HEADER < "$CSV" +declare -A COL +for i in "${!HEADER[@]}"; do + name="${HEADER[$i]//$'\r'/}" # strip CR if file is CRLF + name="${name// /}" + COL["$name"]=$i +done + +required=(userId name email imapHost imapPort imapSsl imapUser imapPass smtpHost smtpPort smtpSsl smtpUser smtpPass) +for col in "${required[@]}"; do + if [[ -z "${COL[$col]:-}" && "${COL[$col]:-x}" != "0" ]]; then + echo "ERROR: missing required CSV column '$col'" >&2 + exit 2 + fi +done + +# Minimal RFC-4180 parser: handles quoted fields and embedded commas. +# Outputs one field per line on FD 3; caller reads with mapfile. +parse_csv_line() { + local line="$1" + python3 -c ' +import csv, sys +for row in csv.reader([sys.argv[1]]): + for f in row: + print(f) +' "$line" +} + +ok=0 +fail=0 +skipped=0 +lineno=1 # header already consumed + +# Read remaining lines +tail -n +2 "$CSV" | while IFS= read -r line || [[ -n "$line" ]]; do + lineno=$((lineno + 1)) + # Skip empty / comment lines + [[ -z "${line//[[:space:]]/}" ]] && continue + [[ "${line:0:1}" == "#" ]] && continue + + mapfile -t fields < <(parse_csv_line "$line") + + get() { echo "${fields[${COL[$1]}]:-}"; } + + userId=$(get userId) + name=$(get name) + email=$(get email) + imapHost=$(get imapHost); imapPort=$(get imapPort); imapSsl=$(get imapSsl) + imapUser=$(get imapUser); imapPass=$(get imapPass) + smtpHost=$(get smtpHost); smtpPort=$(get smtpPort); smtpSsl=$(get smtpSsl) + smtpUser=$(get smtpUser); smtpPass=$(get smtpPass) + authMethod="" + if [[ -n "${COL[authMethod]:-}" ]]; then + authMethod=$(get authMethod) + fi + authMethod="${authMethod:-password}" + + # Basic sanity check + for v in userId name email imapHost imapPort imapSsl imapUser imapPass smtpHost smtpPort smtpSsl smtpUser smtpPass; do + if [[ -z "${!v}" ]]; then + echo "[line $lineno] SKIP: empty field '$v'" >&2 + skipped=$((skipped + 1)) + continue 2 + fi + done + + echo "[line $lineno] linking $email -> user '$userId' ..." + + # shellcheck disable=SC2086 + if $OCC_CMD mail:account:create \ + "$userId" "$name" "$email" \ + "$imapHost" "$imapPort" "$imapSsl" "$imapUser" "$imapPass" \ + "$smtpHost" "$smtpPort" "$smtpSsl" "$smtpUser" "$smtpPass" \ + "$authMethod"; then + ok=$((ok + 1)) + else + echo "[line $lineno] FAILED for user '$userId' ($email)" >&2 + fail=$((fail + 1)) + fi +done + +echo "---" +echo "Done. ok=$ok fail=$fail skipped=$skipped"