#!/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"