#!/bin/bash set -euo pipefail KC_BASE="${KC_BASE:?}" KC_REALM="${KC_REALM:?}" KC_ADMIN_USER="${KC_ADMIN_USER:?}" KC_ADMIN_PASS="${KC_ADMIN_PASS:?}" MAILU_DOMAIN="${MAILU_DOMAIN:?}" if ! command -v jq >/dev/null 2>&1; then apt-get update && apt-get install -y jq curl >/dev/null fi list_mail_accounts() { local user_id="${1}" local export_out # Nextcloud Mail does not provide a list command; export is safe (does not print passwords). if ! export_out=$(/usr/sbin/runuser -u www-data -- php occ mail:account:export "${user_id}" 2>/dev/null); then echo "WARN: unable to export mail accounts for ${user_id}; skipping sync for safety" >&2 return 1 fi # The export output is human-readable and includes blocks like: # Account 10: # - E-Mail: user@example.com # Extract "account-id email" pairs. awk ' /^Account[[:space:]]+[0-9]+:/ { id=$2; sub(/:$/, "", id); next; } id != "" && /@/ { if (match($0, /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/, m)) { printf("%s\t%s\n", id, m[0]); id=""; } } ' <<<"${export_out}" | sort -u } token=$( curl -s -d "grant_type=password" \ -d "client_id=admin-cli" \ -d "username=${KC_ADMIN_USER}" \ -d "password=${KC_ADMIN_PASS}" \ "${KC_BASE}/realms/master/protocol/openid-connect/token" | jq -r '.access_token' ) if [[ -z "${token}" || "${token}" == "null" ]]; then echo "Failed to obtain admin token" exit 1 fi cd /var/www/html users=$(curl -s -H "Authorization: Bearer ${token}" \ "${KC_BASE}/admin/realms/${KC_REALM}/users?max=2000") echo "${users}" | jq -c '.[]' | while read -r user; do username=$(echo "${user}" | jq -r '.username') keycloak_email=$(echo "${user}" | jq -r '.email // empty') mailu_email=$(echo "${user}" | jq -r '(.attributes.mailu_email[0] // .attributes.mailu_email // empty)') app_pw=$(echo "${user}" | jq -r '(.attributes.mailu_app_password[0] // .attributes.mailu_app_password // empty)') if [[ -z "${mailu_email}" ]]; then if [[ -n "${keycloak_email}" && "${keycloak_email,,}" == *"@${MAILU_DOMAIN,,}" ]]; then mailu_email="${keycloak_email}" else mailu_email="${username}@${MAILU_DOMAIN}" fi fi [[ -z "${mailu_email}" || -z "${app_pw}" ]] && continue if ! accounts=$(list_mail_accounts "${username}"); then continue fi # Manage only internal Mailu-domain accounts; leave any external accounts untouched. mailu_accounts=$(awk -v d="${MAILU_DOMAIN,,}" 'tolower($2) ~ ("@" d "$") {print}' <<<"${accounts}" || true) desired_email="${mailu_email}" primary_id="" primary_email="" if [[ -n "${mailu_accounts}" ]]; then while IFS=$'\t' read -r account_id account_email; do if [[ -z "${primary_id}" ]]; then primary_id="${account_id}" primary_email="${account_email}" fi if [[ "${account_email,,}" == "${desired_email,,}" ]]; then primary_id="${account_id}" primary_email="${account_email}" break fi done <<<"${mailu_accounts}" echo "Updating ${username} mail account ${primary_id} (${primary_email})" /usr/sbin/runuser -u www-data -- php occ mail:account:update -q "${primary_id}" \ --name "${username}" \ --email "${desired_email}" \ --imap-host mail.bstein.dev \ --imap-port 993 \ --imap-ssl-mode ssl \ --imap-user "${desired_email}" \ --imap-password "${app_pw}" \ --smtp-host mail.bstein.dev \ --smtp-port 587 \ --smtp-ssl-mode tls \ --smtp-user "${desired_email}" \ --smtp-password "${app_pw}" \ --auth-method password >/dev/null 2>&1 || true # Remove any extra Mailu-domain accounts for this user to prevent duplicates. while IFS=$'\t' read -r account_id account_email; do if [[ "${account_id}" == "${primary_id}" ]]; then continue fi echo "Deleting extra mail account ${account_id} (${account_email})" /usr/sbin/runuser -u www-data -- php occ mail:account:delete -q "${account_id}" >/dev/null 2>&1 || true done <<<"${mailu_accounts}" else echo "Creating mail account for ${username} (${desired_email})" /usr/sbin/runuser -u www-data -- php occ mail:account:create -q \ "${username}" "${username}" "${desired_email}" \ mail.bstein.dev 993 ssl "${desired_email}" "${app_pw}" \ mail.bstein.dev 587 tls "${desired_email}" "${app_pw}" password >/dev/null 2>&1 || true fi done