#!/bin/bash
# Motore della posta di TLC-Service (tlc-service.it).
# Gira come root DENTRO il sistema del pacchetto (chroot); i dati stanno in /srv/posta.
# Lo comanda il servizio di sistema del pacchetto (radice.py); a mano serve solo per le prove.
#
#   posta avvia | ferma | stato | versione
#   posta domini                                   (righe: dominio, stato, caselle, alias, azienda, nome azienda, cartella)
#   posta dominio-aggiungi <dominio> [database]
#   posta dominio-rimuovi <dominio>                (solo se non ha più caselle né alias)
#   posta caselle [dominio]                        (righe: indirizzo, nome, stato, numero, dominio, cartella)
#   posta casella-aggiungi <indirizzo> [nome] [database]   (password su stdin)
#   posta casella-password <indirizzo>             (password su stdin)
#   posta casella-nome <indirizzo> <nome>
#   posta casella-sospendi <indirizzo> si|no
#   posta casella-rimuovi <indirizzo>              (cancella anche tutta la posta della casella)
#   posta alias                                    (righe: alias, casella)
#   posta alias-aggiungi <alias> <casella>
#   posta alias-rimuovi <alias>
#   posta occupazione                              (righe: indirizzo, kB usati)
#   posta prova-password <indirizzo>               (password su stdin: come la controlla l'invio SMTP)
#   posta dkim <dominio>                           (crea la chiave se manca; riga: selettore, record TXT)
#   posta regole-applica                           (rilegge le mappe delle regole di trasporto)
#   posta db-copia                                 (copia del database degli utenti in dati/backup, per i backup)
#   posta db-ripristina <copia.sql.gz> <copia> [vuoti|niente] [<ripristinato> <archivio>]...
#                                                  (database degli utenti dal backup, e con le coppie anche gli archivi;
#                                                   quello che c'era prima va in <copia>)
#   posta archivi-mancanti                         (archivi nuovi, vuoti, per caselle e domini che non ce l'hanno)
#   posta coda                                     (posta in coda in Postfix, una riga JSON per messaggio)
#   posta aziende                                  (righe: numero, nome, descrizione, domini, caselle)
#   posta azienda-aggiungi <nome> [descrizione]
#   posta azienda-modifica <numero> <nome> [descrizione]
#   posta azienda-rimuovi <numero>                 (solo se non ha più domini)
#   posta dominio-azienda <dominio> <numero|0>     (0 = nessuna azienda)
#   posta archivi                                  (righe: tipo, nome, dominio, azienda, cartella, kB db, kB allegati, kB totali)
#   posta archivio-controlla <cartella>            (controllo dell'archivio di una casella o di un dominio)
#   posta archivio-ripara <cartella> <copia>       (prima una copia di sicurezza in <copia>, poi la riparazione)
#   posta archivio-comprimi <cartella>             (toglie lo spazio vuoto dal database)
#   posta casella-sposta <indirizzo> <database>    (sposta l'archivio così com'è: pochi secondi di fermo)
#   posta casella-ricostruisci <indirizzo> <database> <copia>
#                                                  (archivio nuovo e la posta ricopiata dentro, messaggio per messaggio)
#   posta caselle-sposta <database> <indirizzo>... (più caselle con un solo fermo)
#   posta dominio-sposta <dominio> <database>      (le cartelle pubbliche del dominio)
#   posta casella-recupera <indirizzo> <cartella> <nome>
#                                                  (la posta di un archivio ripristinato dal backup in una cartella nuova)
#   posta ripristina-archivi <copia> <ripristinato> <archivio> [<ripristinato> <archivio>...]
#                                                  (archivi dal backup al posto di quelli attuali; gli attuali vanno in <copia>)
#   posta coda-rilascia|coda-elimina <id>          (posta in quarantena o in coda)
#   posta coda-riprova                             (riprova subito la consegna di tutta la coda)
#   posta antispam-applica                         (rilegge soglie e mittenti fidati/bloccati)
#   posta certificato-applica                      (Postfix e gromox rileggono il certificato di dati/ssl)
#
# Dove sta la posta. Come in Exchange ci sono dei «database», ma qui un database è una cartella
# (un sottovolume btrfs, se il volume lo permette, così ha copie istantanee e backup suoi):
# gromox/database/<nome>. Dentro, ogni casella ha il SUO archivio (database exchange.sqlite3 più
# la cartella cid con testi e allegati) in caselle/<numero>, e ogni dominio quello delle cartelle
# pubbliche in pubblica/<dominio>. Le caselle si spostano da un database all'altro; il database
# «principale» c'è sempre.
set -euo pipefail
umask 022

DATI=/srv/posta
CONF="$DATI/config/posta.conf"
LOG="$DATI/log"
RUN=/run/posta
DBPORT=3316
export GROMOX_EXMDB_PORT=15000
SERVIZI_GROMOX="event timer http midb zcore imap pop3 delivery-queue delivery"
PHPFPM=/usr/sbin/php-fpm8.3
PORTA_WEB=8443          # posta via web, Outlook e ActiveSync in HTTPS
PORTA_USCITA=10587      # Postfix solo per gromox: la posta spedita da Outlook, web e telefoni passa dalle regole
REGOLE="$DATI/config/regole"
DKIM="$DATI/dkim"
SELETTORE=tlcmail
GDB="$DATI/gromox/database"
DB_PRINCIPALE="$GDB/principale"
COPIE="$DATI/gromox/copie"
MAPPE_REGOLE="ingresso-mittenti ingresso-destinatari ingresso-alias ingresso-copie-mittenti ingresso-copie-destinatari uscita-mittenti uscita-destinatari uscita-alias uscita-copie-mittenti uscita-copie-destinatari"
# PR_DISPLAY_NAME (0x3001001F) e PR_DISPLAY_TYPE_EX (0x39050003)
TAG_NOME=805371935
TAG_TIPO=956628995
# caselle temporanee (ricostruzione, recupero dal backup): non ricevono posta e non compaiono negli elenchi
TEMP="tlcmail-tmp-"
NON_TEMP="u.username NOT LIKE 'tlcmail-tmp-%'"

errore() { echo "ERRORE: $*" >&2; exit 1; }
nota() { echo "$(date '+%F %T') $*"; }
pl() { if [ "$1" = 1 ]; then echo "1 $2"; else echo "$1 $3"; fi; }      # «1 casella», «3 caselle»

# ---------------------------------------------------------------- impostazioni
# posta.conf lo scrive il servizio di sistema: si leggono solo chiavi note, mai eseguito.
NOME_HOST=""
ANTISPAM_ATTIVO=si
ANTISPAM_SEGNA=6
ANTISPAM_RIFIUTA=15
leggi_conf() {
	[ -f "$CONF" ] || return 0
	local k v
	while IFS='=' read -r k v; do
		v="${v%$'\r'}"; v="${v#\"}"; v="${v%\"}"
		case "$k" in
		NOME_HOST) [[ "$v" =~ ^[A-Za-z0-9.-]{1,253}$ ]] && NOME_HOST="${v,,}" ;;
		ANTISPAM_ATTIVO) [[ "$v" =~ ^(si|no)$ ]] && ANTISPAM_ATTIVO="$v" ;;
		ANTISPAM_SEGNA) [[ "$v" =~ ^[0-9]{1,3}(\.[0-9])?$ ]] && ANTISPAM_SEGNA="$v" ;;
		ANTISPAM_RIFIUTA) [[ "$v" =~ ^[0-9]{1,3}(\.[0-9])?$ ]] && ANTISPAM_RIFIUTA="$v" ;;
		esac
	done < "$CONF"
}
nome_host() {
	leggi_conf
	[ -n "$NOME_HOST" ] || NOME_HOST="$(hostname 2>/dev/null || echo posta.locale)"
}

valida_dominio() { [[ "$1" =~ ^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$ ]] && [ "${#1}" -le 253 ] || errore "dominio non valido: $1"; }
valida_indirizzo() { [[ "$1" =~ ^[a-z0-9._%+-]{1,64}@([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$ ]] && [ "${#1}" -le 254 ] || errore "indirizzo non valido: $1"; }
valida_nome() {
	[ "${#1}" -le 120 ] || errore "nome troppo lungo (al massimo 120 caratteri)"
	[[ "$1" != *[[:cntrl:]]* ]] || errore "il nome contiene caratteri non validi"
}

sql() { mariadb --protocol=socket -S /run/mysqld/mysqld.sock -uroot -N -B gromox "$@"; }
sqlq() { local s="${1//\\/\\\\}"; printf "'%s'" "${s//\'/\'\'}"; }
sqliteq() { local s="$1"; printf "'%s'" "${s//\'/\'\'}"; }       # SQLite: il \ non è speciale
id_casella() { sql -e "SELECT id FROM users WHERE username=$(sqlq "$1") AND maildir<>''"; }
id_dominio() { sql -e "SELECT id FROM domains WHERE domainname=$(sqlq "$1")"; }

# ---------------------------------------------------------------- preparazione
# I file dei dati restano anche quando si cambia il motore: se i numeri degli utenti di sistema
# sono cambiati (motore nuovo), qui si rimettono a posto i proprietari.
sistema_proprietari() {
	local dir="$1" chi="$2"
	[ -d "$dir" ] || return 0
	[ "$(stat -c %U:%G "$dir")" = "$chi" ] && return 0
	nota "sistemo i proprietari di $dir ($chi)"
	chown -R "$chi" "$dir"
}

prepara() {
	mkdir -p "$DATI"/{mysql,gromox,config,ssl,log/sync,log/redis,postfix-coda,redis,web/session,web/tmp,web/sqlite-index,rspamd,backup,antispam} \
		"$RUN" "$DKIM" "$REGOLE" /run/mysqld /run/gromox /run/php /run/php-fpm /run/redis /run/saslauthd /run/rspamd
	sistema_proprietari "$DATI/mysql" mysql:mysql
	sistema_proprietari "$DATI/gromox" gromox:gromox
	sistema_proprietari "$DATI/redis" redis:redis
	sistema_proprietari "$DATI/log/redis" redis:redis
	sistema_proprietari "$DATI/log/sync" gromox:gromox
	sistema_proprietari "$DATI/web" gromox:gromox
	sistema_proprietari "$DATI/rspamd" _rspamd:_rspamd
	if [ -d "$DATI/postfix-coda/active" ] && [ "$(stat -c %U "$DATI/postfix-coda/active")" != postfix ]; then
		nota "sistemo i proprietari della coda di Postfix"
		chown -R postfix "$DATI/postfix-coda"
		chown root "$DATI/postfix-coda"; [ -d "$DATI/postfix-coda/pid" ] && chown root "$DATI/postfix-coda/pid"
	fi
	chown redis:redis /run/redis; chmod 0750 /run/redis
	touch "$DATI/log/php-sync.log"; chown gromox:gromox "$DATI/log/php-sync.log"
	chown mysql:mysql /run/mysqld
	chown gromox:gromox /run/gromox /run/php-fpm
	chmod 0770 "$DATI/gromox"; chmod 0775 /run/gromox
	chmod 0700 "$DATI/config"
	chmod 0755 /run/saslauthd
	chown _rspamd:_rspamd /run/rspamd "$DATI/rspamd"
	chown -R gromox:gromox "$DATI/web"; chmod 0700 "$DATI/web"
	chown root:_rspamd "$DKIM"; chmod 0750 "$DKIM"
	chown root:_rspamd "$DATI/antispam"; chmod 0750 "$DATI/antispam"
	chmod 0700 "$DATI/backup"
	touch "$LOG/rspamd.log" "$LOG/php-web.log"; chown _rspamd:_rspamd "$LOG/rspamd.log"; chown gromox:gromox "$LOG/php-web.log"
	[ -L /var/lib/rspamd ] || { rm -rf /var/lib/rspamd; ln -s "$DATI/rspamd" /var/lib/rspamd; }
	local m
	for m in $MAPPE_REGOLE; do [ -f "$REGOLE/$m" ] || : > "$REGOLE/$m"; done
	for m in ingresso-intestazioni uscita-intestazioni; do [ -f "$REGOLE/$m" ] || : > "$REGOLE/$m"; done
	[ -L /var/lib/gromox ] || { rm -rf /var/lib/gromox; ln -s "$DATI/gromox" /var/lib/gromox; }
	mkdir -p "$DATI/gromox"/{user,domain,queue/mess,queue/save,queue/cache}
	chown -R gromox:gromox "$DATI/gromox/queue"
	if [ ! -s "$DATI/config/db.pw" ]; then
		openssl rand -hex 24 > "$DATI/config/db.pw"; chmod 600 "$DATI/config/db.pw"
	fi
	if [ ! -s "$DATI/ssl/cert.pem" ] || [ ! -s "$DATI/ssl/key.pem" ]; then
		nota "certificato provvisorio autofirmato per $NOME_HOST"
		openssl req -x509 -newkey rsa:2048 -nodes -days 3650 -subj "/CN=$NOME_HOST" \
			-addext "subjectAltName=DNS:$NOME_HOST" \
			-keyout "$DATI/ssl/key.pem" -out "$DATI/ssl/cert.pem" >/dev/null 2>&1
	fi
	# i servizi gromox girano come utente gromox e devono leggere il certificato
	chown root:gromox "$DATI/ssl" "$DATI/ssl/cert.pem" "$DATI/ssl/key.pem"
	chmod 0750 "$DATI/ssl"; chmod 0640 "$DATI/ssl/key.pem" "$DATI/ssl/cert.pem"
	if [ ! -d "$DATI/mysql/mysql" ]; then
		nota "inizializzo il database"
		mariadb-install-db --user=mysql --datadir="$DATI/mysql" --auth-root-authentication-method=socket \
			--skip-test-db >> "$LOG/mariadb-init.log" 2>&1
	fi
}

genera_config() {
	local pw; pw="$(cat "$DATI/config/db.pw")"
	install -d -m 0750 -o root -g gromox /etc/gromox
	# I servizi interni parlano solo su 127.0.0.1: funziona anche dove IPv6 è spento.
	local tutti="0.0.0.0" proto=ipv4
	if [ -s /proc/net/if_inet6 ]; then tutti="[::]"; proto=all; fi
	printf '127.0.0.1\tlocalhost %s %s\n' "$(hostname 2>/dev/null || echo nas)" "$NOME_HOST" > /etc/hosts
	# http_remote_host_hdr: dietro il proxy inverso di DSM (443 -> 127.0.0.1:10080) l'indirizzo vero del
	# client arriva in X-Real-IP; serve al blocco di chi sbaglia la password. gromox-http lo crede solo
	# quando la connessione viene da 127.0.0.1 (nostra modifica 0005): chi si collega diretto non può fingere.
	cat > /etc/gromox/gromox.cfg <<EOF
http_listen = 127.0.0.1:10080
http_listen_tls = $tutti:$PORTA_WEB
imap_listen = $tutti:143
imap_listen_tls = $tutti:993
pop3_listen = $tutti:110
pop3_listen_tls = $tutti:995
lda_listen = 127.0.0.1:10024
lda_listen_tls =
exmdb_listen = 127.0.0.1:$GROMOX_EXMDB_PORT
midb_listen = 127.0.0.1:5555
outgoing_smtp_url = smtp://127.0.0.1:$PORTA_USCITA
oxdisco_exonym = $NOME_HOST
lda_junk_rules = X-Spam-Status=Yes
http_remote_host_hdr = X-Real-IP
EOF
	cat > /etc/gromox/mysql_adaptor.cfg <<EOF
mysql_host = 127.0.0.1
mysql_port = $DBPORT
mysql_username = gromox
mysql_password = $pw
mysql_dbname = gromox
schema_upgrade = host:$NOME_HOST
EOF
	local f
	for f in http imap pop3 midb zcore smtp delivery; do
		{
			echo "host_id = $NOME_HOST"
			case $f in
			http) echo "http_support_tls = true"; echo "http_certificate_path = $DATI/ssl/cert.pem"
			      echo "http_private_key_path = $DATI/ssl/key.pem"; echo "user_default_lang = it" ;;
			imap) echo "imap_support_tls = true"; echo "imap_certificate_path = $DATI/ssl/cert.pem"
			      echo "imap_private_key_path = $DATI/ssl/key.pem" ;;
			pop3) echo "pop3_support_tls = true"; echo "pop3_certificate_path = $DATI/ssl/cert.pem"
			      echo "pop3_private_key_path = $DATI/ssl/key.pem" ;;
			smtp) echo "command_protocol = both" ;;
			esac
		} > "/etc/gromox/$f.cfg"
	done
	printf 'exmdb_hosts_allow = 127.0.0.1 ::ffff:127.0.0.1 ::1\n' > /etc/gromox/exmdb_provider.cfg
	printf 'midb_hosts_allow = 127.0.0.1 ::ffff:127.0.0.1 ::1\n' >> /etc/gromox/midb.cfg
	printf 'event_listen_ip = 127.0.0.1\nevent_hosts_allow = 127.0.0.1 ::ffff:127.0.0.1 ::1\n' > /etc/gromox/event.cfg
	printf 'timer_listen_ip = 127.0.0.1\ntimer_hosts_allow = 127.0.0.1 ::ffff:127.0.0.1 ::1\n' > /etc/gromox/timer.cfg
	printf 'event_host = 127.0.0.1\n' > /etc/gromox/event_proxy.cfg
	printf 'event_host = 127.0.0.1\n' > /etc/gromox/event_stub.cfg
	printf 'timer_host = 127.0.0.1\n' > /etc/gromox/timer_agent.cfg
	printf '/ 127.0.0.1 5555\n' > /etc/gromox/midb_list.txt
	# file serviti da gromox-http: la posta via web, il nostro marchio (/brand/brand.css), gli schemi di EWS
	cat > /etc/gromox/cache.txt <<EOF
* /web /usr/share/grommunio-web
* /brand /usr/share/tlcmail-brand
* /EWS/Messages.xsd /usr/share/gromox/Messages.xsd
* /EWS/Services.wsdl /usr/share/gromox/Services.wsdl
* /EWS/Types.xsd /usr/share/gromox/Types.xsd
EOF
	chgrp gromox /etc/gromox/*; chmod 0640 /etc/gromox/*

	# MariaDB piccola, solo sul NAS stesso e su una porta che non usa nessun altro
	cat > /etc/mysql/mariadb.conf.d/90-posta.cnf <<EOF
[mysqld]
datadir = $DATI/mysql
socket = /run/mysqld/mysqld.sock
bind-address = 127.0.0.1
port = $DBPORT
skip-name-resolve
performance_schema = OFF
innodb_buffer_pool_size = 48M
innodb_log_file_size = 16M
key_buffer_size = 8M
max_connections = 80
table_open_cache = 400
log_error = $LOG/mariadb.log
EOF

	# Invio dai programmi di posta (porte 587 e 465): password controllata da gromox
	# (saslauthd -> PAM -> pam_gromox -> database), le stesse di IMAP e Outlook.
	printf 'auth required pam_gromox.so service=smtp\naccount required pam_permit.so\n' > /etc/pam.d/smtp
	mkdir -p /etc/postfix/sasl
	printf 'pwcheck_method: saslauthd\nmech_list: PLAIN LOGIN\nsaslauthd_path: /run/saslauthd/mux\n' > /etc/postfix/sasl/smtpd.conf

	# Postfix: riceve da internet e consegna a gromox; spedisce per gromox e per i client.
	# Niente chroot di Postfix: siamo già dentro il sistema del pacchetto.
	postconf -e "myhostname = $NOME_HOST" "mydestination = localhost" \
		"mynetworks = 127.0.0.0/8 [::1]/128" "inet_interfaces = all" "inet_protocols = $proto" \
		"maillog_file_prefixes = /var, /dev/stdout, $LOG" \
		"queue_directory = $DATI/postfix-coda" \
		"virtual_mailbox_domains = hash:$DATI/config/postfix-domini" \
		"virtual_mailbox_maps = hash:$DATI/config/postfix-caselle" \
		"virtual_transport = lmtp:inet:127.0.0.1:10024" \
		"smtpd_sender_login_maps = hash:$DATI/config/postfix-mittenti" \
		"smtpd_tls_cert_file = $DATI/ssl/cert.pem" "smtpd_tls_key_file = $DATI/ssl/key.pem" \
		"smtpd_tls_security_level = may" "smtp_tls_security_level = may" \
		"smtpd_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1" \
		"smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1" \
		"smtpd_relay_restrictions = permit_mynetworks permit_sasl_authenticated defer_unauth_destination" \
		"smtpd_helo_required = yes" "disable_vrfy_command = yes" \
		"smtpd_sasl_type = cyrus" "smtpd_sasl_path = smtpd" "cyrus_sasl_config_path = /etc/postfix/sasl" \
		"smtpd_sasl_security_options = noanonymous" "smtpd_sasl_authenticated_header = no" \
		"message_size_limit = 52428800" "maillog_file = $LOG/postfix.log" "compatibility_level = 3.6" \
		"smtpd_milters = inet:127.0.0.1:11332" "non_smtpd_milters = inet:127.0.0.1:11332" \
		"milter_protocol = 6" "milter_default_action = accept" \
		"milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_authen}" \
		"smtpd_recipient_restrictions = check_sender_access hash:$REGOLE/ingresso-mittenti, check_recipient_access hash:$REGOLE/ingresso-destinatari"
	postconf -F '*/*/chroot = n'
	postconf -M "submission/inet=submission inet n - n - - smtpd" \
		"smtps/inet=smtps inet n - n - - smtpd" \
		"127.0.0.1:$PORTA_USCITA/inet=127.0.0.1:$PORTA_USCITA inet n - n - - smtpd" \
		"cleanup_ingresso/unix=cleanup_ingresso unix n - n - 0 cleanup" \
		"cleanup_uscita/unix=cleanup_uscita unix n - n - 0 cleanup"
	# regole di trasporto: in ingresso la porta 25, in uscita 587, 465 e la porta interna di gromox
	local verso
	for verso in ingresso uscita; do
		postconf -P "cleanup_$verso/unix/header_checks=regexp:$REGOLE/$verso-intestazioni" \
			"cleanup_$verso/unix/virtual_alias_maps=hash:$REGOLE/$verso-alias" \
			"cleanup_$verso/unix/sender_bcc_maps=hash:$REGOLE/$verso-copie-mittenti" \
			"cleanup_$verso/unix/recipient_bcc_maps=hash:$REGOLE/$verso-copie-destinatari"
	done
	postconf -P "smtp/inet/cleanup_service_name=cleanup_ingresso"
	local u="127.0.0.1:$PORTA_USCITA/inet"
	postconf -P "$u/syslog_name=postfix/uscita" "$u/smtpd_client_restrictions=permit_mynetworks,reject" \
		"$u/smtpd_relay_restrictions=permit_mynetworks,reject" "$u/smtpd_sasl_auth_enable=no" \
		"$u/smtpd_tls_security_level=none" "$u/smtpd_sender_restrictions=" "$u/smtpd_helo_required=no"
	local s
	for s in submission smtps; do
		postconf -P "$s/inet/syslog_name=postfix/$s" \
			"$s/inet/smtpd_sasl_auth_enable=yes" \
			"$s/inet/smtpd_client_restrictions=permit_sasl_authenticated,reject" \
			"$s/inet/smtpd_relay_restrictions=permit_sasl_authenticated,reject" \
			"$s/inet/smtpd_sender_restrictions=reject_sender_login_mismatch" \
			"$s/inet/milter_macro_daemon_name=ORIGINATING"
	done
	for s in submission/inet smtps/inet "127.0.0.1:$PORTA_USCITA/inet"; do
		postconf -P "$s/cleanup_service_name=cleanup_uscita" \
			"$s/smtpd_recipient_restrictions=check_sender_access,hash:$REGOLE/uscita-mittenti,check_recipient_access,hash:$REGOLE/uscita-destinatari"
	done
	postconf -P "submission/inet/smtpd_tls_security_level=encrypt" "submission/inet/smtpd_tls_auth_only=yes" \
		"smtps/inet/smtpd_tls_wrappermode=yes"
	for f in postfix-domini postfix-caselle postfix-mittenti; do
		[ -f "$DATI/config/$f" ] || : > "$DATI/config/$f"
		postmap "$DATI/config/$f"
	done
	regole_mappe
	# riporta permessi e proprietari di Postfix (coda nei dati, programmi setgid per gromox)
	postfix set-permissions >/dev/null 2>&1 || true

	# PHP per ActiveSync: un gruppo di processi che parla con gromox
	rm -f /etc/php/8.3/fpm/pool.d/www.conf
	sed -i -e "s#^pid = .*#pid = $RUN/php-fpm.pid#" -e "s#^error_log = .*#error_log = $LOG/php-fpm.log#" /etc/php/8.3/fpm/php-fpm.conf
	cat > /etc/php/8.3/fpm/pool.d/grommunio-sync.conf <<EOF
[grommunio-sync]
user = gromox
group = gromox
listen = /run/php-fpm/php-grommunio-sync-fpm.sock
listen.owner = gromox
listen.group = gromox
listen.mode = 0660
pm = ondemand
pm.max_children = 10
pm.process_idle_timeout = 60s
env[GROMOX_EXMDB_PORT] = $GROMOX_EXMDB_PORT
; dalla 8443 e dal proxy inverso di DSM sulla 443 si arriva sempre in HTTPS (la 10080 è solo interna)
env[HTTPS] = on
php_admin_value[memory_limit] = 256M
php_admin_value[error_log] = $LOG/php-sync.log
php_admin_flag[log_errors] = on
EOF
	cat > /etc/php/8.3/fpm/pool.d/grommunio-web.conf <<EOF
[grommunio-web]
user = gromox
group = gromox
listen = /run/php-fpm/php-grommunio-web-fpm.sock
listen.owner = gromox
listen.group = gromox
listen.mode = 0660
pm = ondemand
pm.max_children = 12
pm.process_idle_timeout = 60s
env[GROMOX_EXMDB_PORT] = $GROMOX_EXMDB_PORT
; dalla 8443 e dal proxy inverso di DSM sulla 443 si arriva sempre in HTTPS (la 10080 è solo interna):
; senza, la posta via web rifiuta le richieste che le passa il proxy (cookie sicuri su http)
env[HTTPS] = on
php_admin_value[memory_limit] = 512M
php_admin_value[upload_max_filesize] = 30M
php_admin_value[post_max_size] = 31M
php_admin_value[max_execution_time] = 300
php_admin_value[upload_tmp_dir] = $DATI/web/tmp
php_value[session.save_path] = $DATI/web/session
php_admin_flag[session.cookie_secure] = on
php_admin_flag[session.cookie_httponly] = on
php_admin_value[session.auto_start] = 0
php_admin_value[error_log] = $LOG/php-web.log
php_admin_flag[log_errors] = on
EOF
	genera_rspamd
}

# Rspamd: antispam sulla posta che arriva, firma DKIM su quella che parte. Parla con Postfix come
# milter (11332) e usa il Redis del motore; la posta indesiderata va nella sua cartella (lda_junk_rules).
genera_rspamd() {
	local d=/etc/rspamd/local.d
	mkdir -p "$d"
	cat > "$d/worker-proxy.inc" <<EOF
bind_socket = "127.0.0.1:11332";
milter = yes;
timeout = 120s;
count = 1;
upstream "local" {
  default = yes;
  self_scan = yes;
}
EOF
	printf 'enabled = false;\n' > "$d/worker-normal.inc"
	printf 'bind_socket = "127.0.0.1:11334";\nsecure_ip = "127.0.0.1";\ncount = 1;\n' > "$d/worker-controller.inc"
	printf 'type = "file";\nfilename = "%s/rspamd.log";\nlevel = "notice";\n' "$LOG" > "$d/logging.inc"
	printf 'local_addrs = "127.0.0.0/8, ::1";\n' > "$d/options.inc"
	printf 'servers = "/run/redis/redis-posta.sock";\n' > "$d/redis.conf"
	# soglie dal pannello; con l'antispam spento rspamd resta solo per la firma DKIM
	if [ "$ANTISPAM_ATTIVO" = si ]; then
		printf 'reject = %s;\nadd_header = %s;\ngreylist = null;\nrewrite_subject = null;\n' "$ANTISPAM_RIFIUTA" "$ANTISPAM_SEGNA" > "$d/actions.conf"
	else
		printf 'reject = null;\nadd_header = null;\ngreylist = null;\nrewrite_subject = null;\n' > "$d/actions.conf"
	fi
	# mittenti fidati e bloccati dal pannello (punteggio, non pre-risultato: la firma DKIM resta)
	local m
	for m in fidati-indirizzi fidati-domini bloccati-indirizzi bloccati-domini; do
		[ -f "$DATI/antispam/$m" ] || : > "$DATI/antispam/$m"
	done
	cat > "$d/multimap.conf" <<EOF
TLC_FIDATO_INDIRIZZO { type = "from"; filter = "email:addr"; map = "$DATI/antispam/fidati-indirizzi"; score = -50.0; description = "Mittente fidato (pannello)"; }
TLC_FIDATO_DOMINIO { type = "from"; filter = "email:domain"; map = "$DATI/antispam/fidati-domini"; score = -50.0; description = "Dominio fidato (pannello)"; }
TLC_BLOCCATO_INDIRIZZO { type = "from"; filter = "email:addr"; map = "$DATI/antispam/bloccati-indirizzi"; score = 100.0; description = "Mittente bloccato (pannello)"; }
TLC_BLOCCATO_DOMINIO { type = "from"; filter = "email:domain"; map = "$DATI/antispam/bloccati-domini"; score = 100.0; description = "Dominio bloccato (pannello)"; }
EOF
	printf 'enabled = false;\n' > "$d/greylist.conf"
	printf 'enabled = false;\n' > "$d/neural.conf"
	printf 'use = ["x-spam-status", "authentication-results"];\nskip_local = false;\nskip_authenticated = true;\n' > "$d/milter_headers.conf"
	cat > "$d/dkim_signing.conf" <<EOF
enabled = true;
path = "$DKIM/\$domain.\$selector.key";
selector = "$SELETTORE";
allow_username_mismatch = true;
sign_authenticated = true;
sign_local = true;
use_domain = "header";
use_esld = false;
try_fallback = true;
EOF
	cp "$d/dkim_signing.conf" "$d/arc.conf"
}

# ---------------------------------------------------------------- avvio e arresto
attendi_db() {
	local i
	for i in $(seq 1 60); do
		mariadb-admin --protocol=socket -S /run/mysqld/mysqld.sock -uroot ping >/dev/null 2>&1 && return 0
		sleep 1
	done
	errore "il database non risponde (vedi $LOG/mariadb.log)"
}

prepara_db() {
	local pw; pw="$(cat "$DATI/config/db.pw")"
	mariadb --protocol=socket -S /run/mysqld/mysqld.sock -uroot <<EOF
CREATE DATABASE IF NOT EXISTS gromox CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER IF NOT EXISTS 'gromox'@'127.0.0.1' IDENTIFIED BY '$pw';
CREATE USER IF NOT EXISTS 'gromox'@'localhost' IDENTIFIED BY '$pw';
ALTER USER 'gromox'@'127.0.0.1' IDENTIFIED BY '$pw';
ALTER USER 'gromox'@'localhost' IDENTIFIED BY '$pw';
GRANT ALL PRIVILEGES ON gromox.* TO 'gromox'@'127.0.0.1';
GRANT ALL PRIVILEGES ON gromox.* TO 'gromox'@'localhost';
FLUSH PRIVILEGES;
EOF
	if [ "$(sql -e "SHOW TABLES LIKE 'users'" | wc -l)" = 0 ]; then
		nota "creo le tabelle di gromox"
		gromox-dbop -C >> "$LOG/gromox-dbop.log" 2>&1
	else
		gromox-dbop -U >> "$LOG/gromox-dbop.log" 2>&1
	fi
}

vivo() { [ -f "$RUN/$1.pid" ] && kill -0 "$(cat "$RUN/$1.pid")" 2>/dev/null; }

avvia_uno() {
	local nome="$1"; shift
	vivo "$nome" && return 0
	setsid "$@" >> "$LOG/$nome.log" 2>&1 < /dev/null &
	echo $! > "$RUN/$nome.pid"
}

avvia() {
	nome_host
	prepara
	genera_config
	nota "avvio del motore della posta ($NOME_HOST)"
	if ! mariadb-admin --protocol=socket -S /run/mysqld/mysqld.sock -uroot ping >/dev/null 2>&1; then
		avvia_uno mariadb /usr/sbin/mariadbd --user=mysql
	fi
	attendi_db
	prepara_db
	aggiorna_mappe_postfix
	riorganizza
	local s
	for s in $SERVIZI_GROMOX; do
		avvia_uno "$s" "/usr/libexec/gromox/$s"
		sleep 1
	done
	[ -S /run/redis/redis-posta.sock ] && runuser -u redis -- redis-cli -s /run/redis/redis-posta.sock ping >/dev/null 2>&1 \
		|| runuser -u redis -- redis-server /etc/redis/redis-posta.conf
	vivo php-fpm || { "$PHPFPM" --fpm-config /etc/php/8.3/fpm/php-fpm.conf --pid "$RUN/php-fpm.pid"; }
	local d
	for d in $(sql -e "SELECT domainname FROM domains"); do dkim_chiave "$d" >/dev/null 2>&1 || true; done
	if ! vivo rspamd; then
		rspamd -u _rspamd -g _rspamd >> "$LOG/rspamd.log" 2>&1 || nota "rspamd non è partito (vedi $LOG/rspamd.log)"
		for _ in $(seq 1 20); do [ -s /run/rspamd/rspamd.pid ] && break; sleep 0.5; done
		[ -s /run/rspamd/rspamd.pid ] && cp /run/rspamd/rspamd.pid "$RUN/rspamd.pid"
	fi
	# -r: rimette insieme utente e dominio (prova@dominio), che la libreria SASL di Postfix separa
	vivo saslauthd || /usr/sbin/saslauthd -a pam -r -m /run/saslauthd -n 2
	[ -f /run/saslauthd/saslauthd.pid ] && cp /run/saslauthd/saslauthd.pid "$RUN/saslauthd.pid"
	postfix status >/dev/null 2>&1 || postfix start
	stato
}

ferma() {
	nota "arresto del motore della posta"
	postfix stop >/dev/null 2>&1 || true
	local s
	[ -f "$RUN/php-fpm.pid" ] && kill "$(cat "$RUN/php-fpm.pid")" 2>/dev/null || true
	[ -f "$RUN/saslauthd.pid" ] && kill "$(cat "$RUN/saslauthd.pid")" 2>/dev/null || true
	[ -f "$RUN/rspamd.pid" ] && kill "$(cat "$RUN/rspamd.pid")" 2>/dev/null || true
	for s in delivery delivery-queue pop3 imap zcore midb http timer event; do
		[ -f "$RUN/$s.pid" ] && kill "$(cat "$RUN/$s.pid")" 2>/dev/null || true
	done
	for s in $SERVIZI_GROMOX; do
		for _ in $(seq 1 20); do vivo "$s" || break; sleep 0.5; done
		vivo "$s" && kill -9 "$(cat "$RUN/$s.pid")" 2>/dev/null || true
		rm -f "$RUN/$s.pid"
	done
	runuser -u redis -- redis-cli -s /run/redis/redis-posta.sock shutdown nosave >/dev/null 2>&1 || true
	mariadb-admin --protocol=socket -S /run/mysqld/mysqld.sock -uroot shutdown >/dev/null 2>&1 || true
	rm -f "$RUN/mariadb.pid" "$RUN/saslauthd.pid" "$RUN/rspamd.pid"
}

stato() {
	local s ok=0
	for s in mariadb $SERVIZI_GROMOX; do
		if [ "$s" = mariadb ]; then
			mariadb-admin --protocol=socket -S /run/mysqld/mysqld.sock -uroot ping >/dev/null 2>&1 && echo "attivo   $s" || { echo "FERMO    $s"; ok=1; }
		elif vivo "$s"; then echo "attivo   $s"; else echo "FERMO    $s"; ok=1; fi
	done
	runuser -u redis -- redis-cli -s /run/redis/redis-posta.sock ping >/dev/null 2>&1 && echo "attivo   redis" || { echo "FERMO    redis"; ok=1; }
	vivo php-fpm && echo "attivo   php-fpm" || { echo "FERMO    php-fpm"; ok=1; }
	vivo saslauthd && echo "attivo   saslauthd" || { echo "FERMO    saslauthd"; ok=1; }
	vivo rspamd && echo "attivo   rspamd" || { echo "FERMO    rspamd"; ok=1; }
	postfix status >/dev/null 2>&1 && echo "attivo   postfix" || { echo "FERMO    postfix"; ok=1; }
	return $ok
}

versione() {
	# scritto quando si costruisce il motore (prepara-motore.sh)
	cat /etc/posta-versione 2>/dev/null || echo "motore ?"
}

# ---------------------------------------------------------------- domini e caselle
aggiorna_mappe_postfix() {
	sql -e "SELECT domainname, 'OK' FROM domains WHERE domain_status=0" > "$DATI/config/postfix-domini"
	{
		sql -e "SELECT u.username, 'OK' FROM users u WHERE u.maildir<>'' AND $NON_TEMP"
		sql -e "SELECT a.aliasname, 'OK' FROM aliases a JOIN users u ON u.username=a.mainname WHERE u.maildir<>''"
	} > "$DATI/config/postfix-caselle"
	# chi può spedire con quale mittente: ogni casella con il suo indirizzo e con i suoi alias
	{
		sql -e "SELECT u.username, u.username FROM users u WHERE u.maildir<>'' AND $NON_TEMP"
		sql -e "SELECT a.aliasname, a.mainname FROM aliases a JOIN users u ON u.username=a.mainname WHERE u.maildir<>''"
	} > "$DATI/config/postfix-mittenti"
	postmap "$DATI/config/postfix-domini" "$DATI/config/postfix-caselle" "$DATI/config/postfix-mittenti"
	postfix status >/dev/null 2>&1 && postfix reload >/dev/null 2>&1 || true
}

domini() {
	sql -e "SELECT d.domainname, d.domain_status,
		(SELECT COUNT(*) FROM users u WHERE u.domain_id=d.id AND u.maildir<>'' AND $NON_TEMP),
		(SELECT COUNT(*) FROM aliases a WHERE a.aliasname LIKE CONCAT('%@', d.domainname)),
		d.org_id, IFNULL((SELECT o.name FROM orgs o WHERE o.id=d.org_id), ''), d.homedir
		FROM domains d ORDER BY d.domainname"
}

dominio_aggiungi() {
	local d="${1,,}" db="${2:-$DB_PRINCIPALE}"; valida_dominio "$d"; valida_database "$db"
	[ -z "$(id_dominio "$d")" ] || errore "il dominio $d esiste già"
	local dir; dir="$(cartella_pubblica "$db" "$d")"
	sql -e "INSERT INTO domains (domainname, homedir, max_user, title, end_day, domain_status) VALUES ($(sqlq "$d"), $(sqlq "$dir"), 0, $(sqlq "$d"), '3333-03-03', 0)"
	install -d -o gromox -g gromox -m 0770 "$db/pubblica" "$dir" "$dir/exmdb" "$dir/cid" "$dir/log" "$dir/tmp"
	runuser -u gromox -- gromox-mkpublic "$d"
	dkim_chiave "$d" >/dev/null 2>&1 || true
	aggiorna_mappe_postfix
	echo "dominio $d creato"
}

dominio_rimuovi() {
	local d="${1,,}"; valida_dominio "$d"
	local did; did="$(id_dominio "$d")"
	[ -n "$did" ] || errore "il dominio $d non esiste"
	[ "$(sql -e "SELECT COUNT(*) FROM users WHERE domain_id=$did")" = 0 ] \
		|| errore "nel dominio $d ci sono ancora delle caselle: eliminale prima"
	[ "$(sql -e "SELECT COUNT(*) FROM aliases WHERE aliasname LIKE $(sqlq "%@$d")")" = 0 ] \
		|| errore "nel dominio $d ci sono ancora degli alias: eliminali prima"
	local dir; dir="$(sql -e "SELECT homedir FROM domains WHERE id=$did")"
	runuser -u gromox -- gromox-mbop -d "$dir" unload >/dev/null 2>&1 || true
	sql -e "DELETE FROM groups WHERE domain_id=$did; DELETE FROM domains WHERE id=$did"
	[[ "$dir" == "$DATI/gromox/domain/"?* || "$dir" == "$GDB/"*"/pubblica/"?* ]] && rm -rf -- "$dir"
	aggiorna_mappe_postfix
	echo "dominio $d eliminato"
}

leggi_password() {
	local p; IFS= read -r p || true
	[ "${#p}" -ge 8 ] || errore "la password deve avere almeno 8 caratteri"
	printf '%s' "$p"
}

caselle() {
	local filtro=""
	if [ -n "${1:-}" ]; then valida_dominio "${1,,}"; filtro="AND d.domainname=$(sqlq "${1,,}")"; fi
	sql -e "SELECT u.username, IFNULL(p.propval_str, ''), u.address_status, u.id, d.domainname, u.maildir
		FROM users u JOIN domains d ON d.id=u.domain_id
		LEFT JOIN user_properties p ON p.user_id=u.id AND p.proptag=$TAG_NOME
		WHERE u.maildir<>'' AND $NON_TEMP $filtro ORDER BY u.username"
}

casella_aggiungi() {
	local u="${1,,}" nome="${2:-}" db="${3:-}"; valida_indirizzo "$u"; valida_nome "$nome"
	local d="${u#*@}"
	local did; did="$(id_dominio "$d")"
	[ -n "$did" ] || errore "il dominio $d non esiste: aggiungilo prima"
	[ -z "$(sql -e "SELECT id FROM users WHERE username=$(sqlq "$u")")" ] || errore "la casella $u esiste già"
	[ -z "$(sql -e "SELECT aliasname FROM aliases WHERE aliasname=$(sqlq "$u")")" ] || errore "$u è già un alias di un'altra casella"
	local hash; hash="$(leggi_password | openssl passwd -6 -stdin)"
	# la cartella prende il numero della casella: un indirizzo lungo non sfora i 128 caratteri di maildir
	sql -e "INSERT INTO users (username, password, domain_id, group_id, maildir, lang, privilege_bits, address_status) VALUES ($(sqlq "$u"), $(sqlq "$hash"), $did, 0, '', 'it', 7, 0)"
	local uid; uid="$(sql -e "SELECT id FROM users WHERE username=$(sqlq "$u")")"
	# se non si sceglie, la casella va nel database dove stanno le cartelle pubbliche del suo dominio
	[ -n "$db" ] || db="$(database_di "$(sql -e "SELECT homedir FROM domains WHERE id=$did")")"
	valida_database "$db"
	local dir; dir="$(cartella_casella "$db" "$uid")"
	install -d -o gromox -g gromox -m 0770 "$db/caselle"
	sql -e "UPDATE users SET maildir=$(sqlq "$dir") WHERE id=$uid"
	# PR_DISPLAY_TYPE_EX = 0: casella normale; senza, gromox non sa cos'è e rifiuta la posta
	sql -e "INSERT INTO user_properties (user_id, proptag, propval_str) VALUES ($uid, $TAG_TIPO, '0')"
	[ -n "$nome" ] && sql -e "INSERT INTO user_properties (user_id, proptag, propval_str) VALUES ($uid, $TAG_NOME, $(sqlq "$nome"))"
	install -d -o gromox -g gromox -m 0770 "$dir" "$dir/exmdb" "$dir/tmp" "$dir/tmp/imap.rfc822" \
		"$dir/tmp/faststream" "$dir/eml" "$dir/ext" "$dir/cid" "$dir/config"
	runuser -u gromox -- gromox-mkprivate "$u"
	runuser -u gromox -- gromox-mkmidb "$u"
	aggiorna_mappe_postfix
	echo "casella $u creata"
}

casella_password() {
	local u="${1,,}"; valida_indirizzo "$u"
	[ -n "$(id_casella "$u")" ] || errore "la casella $u non esiste"
	local hash; hash="$(leggi_password | openssl passwd -6 -stdin)"
	sql -e "UPDATE users SET password=$(sqlq "$hash") WHERE username=$(sqlq "$u")"
	echo "password di $u cambiata"
}

casella_nome() {
	local u="${1,,}" nome="${2:-}"; valida_indirizzo "$u"; valida_nome "$nome"
	local uid; uid="$(id_casella "$u")"
	[ -n "$uid" ] || errore "la casella $u non esiste"
	sql -e "DELETE FROM user_properties WHERE user_id=$uid AND proptag=$TAG_NOME"
	[ -n "$nome" ] && sql -e "INSERT INTO user_properties (user_id, proptag, propval_str) VALUES ($uid, $TAG_NOME, $(sqlq "$nome"))"
	echo "nome di $u cambiato"
}

casella_sospendi() {
	local u="${1,,}"; valida_indirizzo "$u"
	local uid; uid="$(id_casella "$u")"
	[ -n "$uid" ] || errore "la casella $u non esiste"
	# address_status: 0 normale, 1 sospesa (non entra più, la posta continua ad arrivare)
	case "${2:-}" in
	si) sql -e "UPDATE users SET address_status=(address_status & ~3) | 1 WHERE id=$uid"; echo "casella $u sospesa" ;;
	no) sql -e "UPDATE users SET address_status=(address_status & ~3) WHERE id=$uid"; echo "casella $u riattivata" ;;
	*) errore "si o no" ;;
	esac
}

casella_rimuovi() {
	local u="${1,,}"; valida_indirizzo "$u"
	local uid; uid="$(id_casella "$u")"
	[ -n "$uid" ] || errore "la casella $u non esiste"
	local dir; dir="$(sql -e "SELECT maildir FROM users WHERE id=$uid")"
	# gromox tiene aperto l'archivio della casella: prima lo chiude, poi si cancella
	runuser -u gromox -- gromox-mbop -d "$dir" unload >/dev/null 2>&1 || true
	sql -e "DELETE FROM associations WHERE username=$(sqlq "$u"); DELETE FROM specifieds WHERE username=$(sqlq "$u");
		DELETE FROM forwards WHERE username=$(sqlq "$u"); DELETE FROM users WHERE id=$uid"
	[[ "$dir" == "$DATI/gromox/user/"?* || "$dir" == "$GDB/"*"/caselle/"?* ]] && rm -rf -- "$dir"
	aggiorna_mappe_postfix
	echo "casella $u eliminata"
}

alias_elenco() {
	sql -e "SELECT a.aliasname, a.mainname FROM aliases a JOIN users u ON u.username=a.mainname WHERE u.maildir<>'' ORDER BY a.aliasname"
}

alias_aggiungi() {
	local a="${1,,}" u="${2,,}"; valida_indirizzo "$a"; valida_indirizzo "$u"
	[ -n "$(id_casella "$u")" ] || errore "la casella $u non esiste"
	[ -n "$(id_dominio "${a#*@}")" ] || errore "il dominio ${a#*@} non è tra i domini del server: aggiungilo prima"
	[ -z "$(sql -e "SELECT id FROM users WHERE username=$(sqlq "$a")")" ] || errore "$a è già una casella"
	[ -z "$(sql -e "SELECT aliasname FROM aliases WHERE aliasname=$(sqlq "$a")")" ] || errore "l'alias $a esiste già"
	sql -e "INSERT INTO aliases (aliasname, mainname) VALUES ($(sqlq "$a"), $(sqlq "$u"))"
	aggiorna_mappe_postfix
	echo "alias $a -> $u creato"
}

alias_rimuovi() {
	local a="${1,,}"; valida_indirizzo "$a"
	[ -n "$(sql -e "SELECT aliasname FROM aliases WHERE aliasname=$(sqlq "$a")")" ] || errore "l'alias $a non esiste"
	sql -e "DELETE FROM aliases WHERE aliasname=$(sqlq "$a")"
	aggiorna_mappe_postfix
	echo "alias $a eliminato"
}

occupazione() {
	local u dir kb
	while IFS=$'\t' read -r u dir; do
		kb="$(du -sk -- "$dir" 2>/dev/null | cut -f1)"
		printf '%s\t%s\n' "$u" "${kb:-0}"
	done < <(sql -e "SELECT u.username, u.maildir FROM users u WHERE u.maildir<>'' AND $NON_TEMP ORDER BY u.username")
}

# ---------------------------------------------------------------- database (contenitori) e archivi
valida_database() {
	[[ "$1" =~ ^/srv/posta/gromox/database/[a-z0-9][a-z0-9-]{0,31}$ ]] || errore "database non valido: $1"
	[ -d "$1" ] || errore "il database $(basename "$1") non esiste"
}
# il database di una cartella di archivio: gromox/database/<nome>/...
database_di() {
	local dir="$1"
	if [[ "$dir" =~ ^(/srv/posta/gromox/database/[a-z0-9][a-z0-9-]*)/ ]]; then printf '%s' "${BASH_REMATCH[1]}"
	else printf '%s' "$DB_PRINCIPALE"; fi
}
# maildir e homedir sono lunghe al massimo 128 caratteri
cartella_casella() {
	local dir="$1/caselle/$2"
	[ "${#dir}" -le 128 ] || errore "percorso troppo lungo per la casella ($dir)"
	printf '%s' "$dir"
}
cartella_pubblica() {
	local dir="$1/pubblica/$2"
	[ "${#dir}" -le 128 ] || dir="$1/pubblica/d$(id_dominio "$2")"
	printf '%s' "$dir"
}

prepara_principale() {
	[ -d "$DB_PRINCIPALE" ] || mkdir -p "$DB_PRINCIPALE"
	chown gromox:gromox "$GDB" "$DB_PRINCIPALE"; chmod 0770 "$GDB" "$DB_PRINCIPALE"
	install -d -o gromox -g gromox -m 0770 "$DB_PRINCIPALE/caselle" "$DB_PRINCIPALE/pubblica" "$COPIE"
}

# Domini e caselle create prima della 0.2.0 stavano in gromox/domain/<dominio> e gromox/user/<numero>:
# vanno nel database principale. Si fa all'avvio, prima che partano i servizi di gromox.
riorganizza() {
	local d dir nuovo id u
	prepara_principale
	while IFS=$'\t' read -r d dir; do
		[ -n "$d" ] || continue
		[[ "$dir" == "$GDB/"* ]] && continue
		nuovo="$(cartella_pubblica "$DB_PRINCIPALE" "$d")"
		if [ -d "$dir" ] && [ ! -e "$nuovo" ]; then mv "$dir" "$nuovo"; fi
		[ -d "$nuovo" ] || continue
		sql -e "UPDATE domains SET homedir=$(sqlq "$nuovo") WHERE domainname=$(sqlq "$d")"
		nota "dominio $d: cartelle pubbliche spostate in $nuovo"
	done < <(sql -e "SELECT domainname, homedir FROM domains")
	while IFS=$'\t' read -r id u dir; do
		[ -n "$id" ] || continue
		[[ "$dir" == "$GDB/"* ]] && continue
		nuovo="$(cartella_casella "$DB_PRINCIPALE" "$id")"
		if [ -d "$dir" ] && [ ! -e "$nuovo" ]; then mv "$dir" "$nuovo"; fi
		[ -d "$nuovo" ] || continue
		sql -e "UPDATE users SET maildir=$(sqlq "$nuovo") WHERE id=$id"
		nota "casella $u: archivio spostato in $nuovo"
	done < <(sql -e "SELECT id, username, maildir FROM users WHERE maildir<>''")
}

# gromox tiene aperti gli archivi: per spostarne uno si fermano un attimo i servizi che li usano.
# Postfix tiene la posta in coda e la consegna appena gromox riparte: non si perde niente.
# Se il comando si interrompe a metà (errore), i servizi ripartono lo stesso (trap).
GROMOX_FERMO=0
ferma_gromox() {
	local s
	GROMOX_FERMO=1
	trap '[ "$GROMOX_FERMO" = 1 ] && riavvia_gromox' EXIT
	for s in delivery delivery-queue pop3 imap zcore midb http; do
		[ -f "$RUN/$s.pid" ] && kill "$(cat "$RUN/$s.pid")" 2>/dev/null || true
	done
	for s in delivery delivery-queue pop3 imap zcore midb http; do
		for _ in $(seq 1 30); do vivo "$s" || break; sleep 0.5; done
		vivo "$s" && kill -9 "$(cat "$RUN/$s.pid")" 2>/dev/null || true
		rm -f "$RUN/$s.pid"
	done
}
# aspetta che un servizio risponda sulla sua porta (al massimo una ventina di secondi)
attendi_porta() {
	local i
	for i in $(seq 1 40); do
		(exec 3<>"/dev/tcp/127.0.0.1/$1") 2>/dev/null && return 0
		sleep 0.5
	done
	return 1
}
riavvia_gromox() {
	local s
	GROMOX_FERMO=0
	for s in http midb zcore imap pop3 delivery-queue delivery; do
		avvia_uno "$s" "/usr/libexec/gromox/$s"
		case "$s" in
		http) attendi_porta "$GROMOX_EXMDB_PORT" || true ;;
		midb) attendi_porta 5555 || true ;;
		*) sleep 0.5 ;;
		esac
	done
	attendi_porta 143 || true
	vivo php-fpm && kill -USR2 "$(cat "$RUN/php-fpm.pid")" 2>/dev/null || true
}

# copia una cartella d'archivio: con btrfs la copia è istantanea e non occupa spazio finché non cambia
copia_archivio() {
	local da="$1" a="$2"
	rm -rf -- "$a.nuova"
	if cp -a --reflink=auto -- "$da" "$a.nuova" && mv -- "$a.nuova" "$a"; then return 0; fi
	rm -rf -- "$a.nuova"
	return 1
}

# Sposta una o più caselle in un database, con un solo fermo dei servizi: l'archivio si copia così
# com'è (con btrfs è quasi istantaneo), si aggiorna il database degli utenti e si toglie il vecchio.
caselle_sposta() {
	local db="$1"; shift; valida_database "$db"
	[ "$#" -ge 1 ] || errore "nessuna casella da spostare"
	local u uid dir nuovo fatte=0 errori=0
	install -d -o gromox -g gromox -m 0770 "$db/caselle"
	ferma_gromox
	for u in "$@"; do
		u="${u,,}"
		if ! [[ "$u" =~ ^[a-z0-9._%+-]{1,64}@([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$ ]]; then
			echo "$u: indirizzo non valido"; errori=$((errori + 1)); continue
		fi
		uid="$(id_casella "$u")"
		if [ -z "$uid" ]; then echo "$u: la casella non esiste"; errori=$((errori + 1)); continue; fi
		dir="$(sql -e "SELECT maildir FROM users WHERE id=$uid")"; nuovo="$db/caselle/$uid"
		if [ "$dir" = "$nuovo" ]; then echo "$u: è già nel database $(basename "$db")"; continue; fi
		if [ -e "$nuovo" ] || [ "${#nuovo}" -gt 128 ]; then
			echo "$u: nel database c'è già la cartella $nuovo"; errori=$((errori + 1)); continue
		fi
		if [ -d "$dir" ] && copia_archivio "$dir" "$nuovo" && sql -e "UPDATE users SET maildir=$(sqlq "$nuovo") WHERE id=$uid"; then
			rm -rf -- "$dir"
			echo "$u: spostata"; fatte=$((fatte + 1))
		else
			rm -rf -- "$nuovo"
			echo "$u: spostamento non riuscito, la casella è rimasta dov'era"; errori=$((errori + 1))
		fi
	done
	riavvia_gromox
	if [ "$errori" = 0 ]; then
		echo "caselle spostate nel database $(basename "$db"): $fatte"
	else
		echo "caselle spostate nel database $(basename "$db"): $fatte; con problemi: $errori"
		return 1
	fi
}

dominio_sposta() {
	local d="${1,,}" db="$2"; valida_dominio "$d"; valida_database "$db"
	local dir nuovo; dir="$(sql -e "SELECT homedir FROM domains WHERE domainname=$(sqlq "$d")")"
	[ -n "$dir" ] || errore "il dominio $d non esiste"
	nuovo="$(cartella_pubblica "$db" "$d")"
	[ "$dir" != "$nuovo" ] || errore "le cartelle pubbliche di $d sono già nel database $(basename "$db")"
	[ ! -e "$nuovo" ] || errore "nel database $(basename "$db") c'è già la cartella $nuovo"
	install -d -o gromox -g gromox -m 0770 "$db/pubblica"
	ferma_gromox
	if [ -d "$dir" ] && copia_archivio "$dir" "$nuovo" && sql -e "UPDATE domains SET homedir=$(sqlq "$nuovo") WHERE domainname=$(sqlq "$d")"; then
		rm -rf -- "$dir"
		riavvia_gromox
		echo "cartelle pubbliche di $d spostate nel database $(basename "$db")"
	else
		rm -rf -- "$nuovo"
		riavvia_gromox
		errore "spostamento non riuscito: le cartelle pubbliche sono rimaste dov'erano"
	fi
}

valida_copia() {
	[[ "$1" =~ ^/srv/posta/gromox/copie/[A-Za-z0-9._-]{1,120}$ ]] && [[ "$1" != *..* ]] || errore "cartella per la copia non valida"
	[ ! -e "$1" ] || errore "la cartella per la copia c'è già"
}
# un archivio ripristinato dal backup (dentro la cartella delle copie)
valida_ripristinato() {
	[[ "$1" =~ ^/srv/posta/gromox/copie/[A-Za-z0-9._-]{1,120}(/[A-Za-z0-9._-]{1,120}){0,3}$ ]] && [[ "$1" != *..* ]] \
		|| errore "cartella ripristinata non valida: $1"
	[ -f "$1/exmdb/exchange.sqlite3" ] || errore "nella cartella ripristinata $1 manca il database dell'archivio"
}
# una casella temporanea (senza accesso, non riceve posta) che punta a un archivio da cui leggere
casella_temporanea() {
	local tipo="$1" uid="$2" did="$3" dir="$4" tmp
	tmp="${TEMP}$tipo-$uid-$(date +%s)@$(sql -e "SELECT domainname FROM domains WHERE id=$did")"
	sql -e "INSERT INTO users (username, password, domain_id, group_id, maildir, lang, privilege_bits, address_status) VALUES ($(sqlq "$tmp"), '', $did, 0, $(sqlq "$dir"), 'it', 0, 1)"
	sql -e "INSERT INTO user_properties (user_id, proptag, propval_str) SELECT id, $TAG_TIPO, '0' FROM users WHERE username=$(sqlq "$tmp")"
	printf '%s' "$tmp"
}
togli_temporanea() {
	local tmp="$1" dir="$2"
	runuser -u gromox -- gromox-mbop -d "$dir" unload >/dev/null 2>&1 || true
	sql -e "DELETE p FROM user_properties p JOIN users u ON u.id=p.user_id WHERE u.username=$(sqlq "$tmp"); DELETE FROM users WHERE username=$(sqlq "$tmp")"
}

# Ricostruzione: l'archivio vecchio diventa la copia di sicurezza, si crea un archivio nuovo vuoto e
# si ricopia dentro tutta la posta, cartella per cartella e messaggio per messaggio (come lo
# spostamento di una casella in Exchange). Gli oggetti rovinati che non si leggono restano fuori.
casella_ricostruisci() {
	local u="${1,,}" db="$2" copia="$3"; valida_indirizzo "$u"; valida_database "$db"; valida_copia "$copia"
	local uid did; uid="$(id_casella "$u")"
	[ -n "$uid" ] || errore "la casella $u non esiste"
	did="$(sql -e "SELECT domain_id FROM users WHERE id=$uid")"
	local dir nuovo; dir="$(sql -e "SELECT maildir FROM users WHERE id=$uid")"; nuovo="$(cartella_casella "$db" "$uid")"
	[ -d "$dir" ] || errore "manca la cartella della casella ($dir)"
	install -d -o gromox -g gromox -m 0770 "$COPIE" "$db/caselle"
	# 1. l'archivio vecchio diventa la copia di sicurezza
	ferma_gromox
	mv -- "$dir" "$copia" || errore "non riesco a mettere da parte l'archivio: non ho toccato niente"
	# 2. una casella temporanea che punta alla copia, da cui leggere
	local tmp; tmp="$(casella_temporanea ricostruzione "$uid" "$did" "$copia")"
	# 3. archivio nuovo per la casella
	sql -e "UPDATE users SET maildir=$(sqlq "$nuovo") WHERE id=$uid"
	rm -rf -- "$nuovo"
	install -d -o gromox -g gromox -m 0770 "$nuovo" "$nuovo/exmdb" "$nuovo/tmp" "$nuovo/tmp/imap.rfc822" \
		"$nuovo/tmp/faststream" "$nuovo/eml" "$nuovo/ext" "$nuovo/cid" "$nuovo/config"
	runuser -u gromox -- gromox-mkprivate "$u" >/dev/null
	riavvia_gromox
	sleep 2
	# 4. tutta la posta dalla copia all'archivio nuovo (anche categorie e impostazioni nascoste: -a)
	local esito=0
	runuser -u gromox -- gromox-exm2mt -u "$tmp" -a -r -s IPM_SUBTREE 2>"$LOG/ricostruzione-$uid.log" \
		| runuser -u gromox -- gromox-mt2exm -u "$u" -c -x 2>>"$LOG/ricostruzione-$uid.log" || esito=1
	togli_temporanea "$tmp" "$copia"
	runuser -u gromox -- gromox-mkmidb -f "$u" >/dev/null 2>&1 || true
	[ "$esito" = 0 ] || errore "ricopiatura con errori (vedi $LOG/ricostruzione-$uid.log); la posta vecchia è nella copia $copia"
	echo "casella $u ricostruita nel database $(basename "$db"); la copia di sicurezza è in $copia"
}

# Recupero dal backup: la posta di un archivio ripristinato entra nella casella in una cartella nuova
# (la radice delle cartelle dell'archivio ripristinato, IPM_SUBTREE, prende il nome scelto).
casella_recupera() {
	local u="${1,,}" sorgente="$2" nome="$3"; valida_indirizzo "$u"; valida_ripristinato "$sorgente"; valida_nome "$nome"
	[ -n "$nome" ] || errore "manca il nome della cartella"
	local uid did; uid="$(id_casella "$u")"
	[ -n "$uid" ] || errore "la casella $u non esiste"
	did="$(sql -e "SELECT domain_id FROM users WHERE id=$uid")"
	chown -R gromox:gromox "$sorgente"
	runuser -u gromox -- sqlite3 "$sorgente/exmdb/exchange.sqlite3" \
		"UPDATE folder_properties SET propval=$(sqliteq "$nome") WHERE folder_id=9 AND proptag=$TAG_NOME;" \
		|| errore "l'archivio ripristinato non si legge"
	local tmp; tmp="$(casella_temporanea recupero "$uid" "$did" "$sorgente")"
	local esito=0
	runuser -u gromox -- gromox-exm2mt -u "$tmp" -r IPM_SUBTREE 2>"$LOG/recupero-$uid.log" \
		| runuser -u gromox -- gromox-mt2exm -u "$u" -B IPM_SUBTREE -c -x 2>>"$LOG/recupero-$uid.log" || esito=1
	togli_temporanea "$tmp" "$sorgente"
	[ "$esito" = 0 ] || errore "recupero con errori (vedi $LOG/recupero-$uid.log)"
	echo "posta recuperata nella cartella «$nome» di $u"
}

# Ripristino dal backup al posto degli archivi attuali, che vanno nella cartella di copia.
archivio_noto() {
	[ -n "$(sql -e "SELECT id FROM users WHERE maildir=$(sqlq "$1") UNION SELECT id FROM domains WHERE homedir=$(sqlq "$1")")" ] \
		|| errore "archivio sconosciuto: $1"
}
ripristina_archivi() {
	local copia="$1"; shift; valida_copia "$copia"
	[ "$#" -ge 2 ] && [ $(($# % 2)) = 0 ] || errore "servono coppie: cartella ripristinata e archivio"
	local -a da=() a=()
	while [ "$#" -ge 2 ]; do
		valida_ripristinato "$1"; archivio_noto "$2"
		da+=("$1"); a+=("$2"); shift 2
	done
	install -d -o gromox -g gromox -m 0770 "$copia"
	ferma_gromox
	local i n fatte=0 errori=0
	for i in "${!da[@]}"; do
		n="$(basename "$(dirname "${a[$i]}")")-$(basename "${a[$i]}")"
		if [ -d "${a[$i]}" ] && ! mv -- "${a[$i]}" "$copia/$n"; then
			echo "${a[$i]}: non riesco a mettere da parte l'archivio attuale"; errori=$((errori + 1)); continue
		fi
		install -d -o gromox -g gromox -m 0770 "$(dirname "${a[$i]}")"
		if mv -- "${da[$i]}" "${a[$i]}"; then
			chown -R gromox:gromox "${a[$i]}"
			echo "${a[$i]}: ripristinato"; fatte=$((fatte + 1))
		else
			rm -rf -- "${a[$i]}"
			[ -d "$copia/$n" ] && mv -- "$copia/$n" "${a[$i]}"
			echo "${a[$i]}: ripristino non riuscito, è rimasto com'era"; errori=$((errori + 1))
		fi
	done
	riavvia_gromox
	if [ "$errori" = 0 ]; then
		echo "archivi ripristinati: $fatte; quelli di prima sono nella copia $copia"
	else
		echo "archivi ripristinati: $fatte; con problemi: $errori; quelli di prima sono nella copia $copia"
		return 1
	fi
}

aziende() {
	sql -e "SELECT o.id, o.name, IFNULL(o.description, ''),
		(SELECT COUNT(*) FROM domains d WHERE d.org_id=o.id),
		(SELECT COUNT(*) FROM users u JOIN domains d ON d.id=u.domain_id WHERE d.org_id=o.id AND u.maildir<>'' AND $NON_TEMP)
		FROM orgs o ORDER BY o.name"
}

valida_azienda() {
	[ -n "$1" ] && [ "${#1}" -le 32 ] || errore "il nome dell'azienda deve avere da 1 a 32 caratteri"
	[[ "$1" != *[[:cntrl:]]* ]] || errore "il nome dell'azienda contiene caratteri non validi"
	[ "${#2}" -le 128 ] || errore "la descrizione è troppo lunga (al massimo 128 caratteri)"
	[[ "$2" != *[[:cntrl:]]* ]] || errore "la descrizione contiene caratteri non validi"
}

azienda_aggiungi() {
	local nome="$1" descr="${2:-}"; valida_azienda "$nome" "$descr"
	[ -z "$(sql -e "SELECT id FROM orgs WHERE name=$(sqlq "$nome")")" ] || errore "l'azienda $nome esiste già"
	sql -e "INSERT INTO orgs (name, description) VALUES ($(sqlq "$nome"), $(sqlq "$descr"))"
	echo "azienda $nome creata"
}

azienda_modifica() {
	local id="$1" nome="$2" descr="${3:-}"; valida_azienda "$nome" "$descr"
	[[ "$id" =~ ^[0-9]+$ ]] && [ -n "$(sql -e "SELECT id FROM orgs WHERE id=$id")" ] || errore "l'azienda non esiste"
	sql -e "UPDATE orgs SET name=$(sqlq "$nome"), description=$(sqlq "$descr") WHERE id=$id"
	echo "azienda $nome modificata"
}

azienda_rimuovi() {
	local id="$1"
	[[ "$id" =~ ^[0-9]+$ ]] && [ -n "$(sql -e "SELECT id FROM orgs WHERE id=$id")" ] || errore "l'azienda non esiste"
	[ "$(sql -e "SELECT COUNT(*) FROM domains WHERE org_id=$id")" = 0 ] || errore "l'azienda ha ancora dei domini: spostali prima"
	sql -e "DELETE FROM orgparam WHERE org_id=$id; DELETE FROM orgs WHERE id=$id"
	echo "azienda eliminata"
}

dominio_azienda() {
	local d="${1,,}" id="$2"; valida_dominio "$d"
	[ -n "$(id_dominio "$d")" ] || errore "il dominio $d non esiste"
	[[ "$id" =~ ^[0-9]+$ ]] || errore "azienda non valida"
	[ "$id" = 0 ] || [ -n "$(sql -e "SELECT id FROM orgs WHERE id=$id")" ] || errore "l'azienda non esiste"
	sql -e "UPDATE domains SET org_id=$id WHERE domainname=$(sqlq "$d")"
	echo "dominio $d spostato"
}

kb() { du -sk -- "$@" 2>/dev/null | awk '{s+=$1} END {print s+0}'; }

archivi() {
	local tipo nome d org dir
	while IFS=$'\t' read -r tipo nome d org dir; do
		[ -n "$dir" ] || continue
		printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$tipo" "$nome" "$d" "$org" "$dir" \
			"$(kb "$dir"/exmdb/exchange.sqlite3*)" "$(kb "$dir/cid")" "$(kb "$dir")"
	done < <(
		sql -e "SELECT 'pubblica', d.domainname, d.domainname, d.org_id, d.homedir FROM domains d"
		sql -e "SELECT 'casella', u.username, d.domainname, d.org_id, u.maildir FROM users u JOIN domains d ON d.id=u.domain_id WHERE u.maildir<>'' AND $NON_TEMP"
	)
}

archivio_valido() {
	local dir="$1"
	[ -n "$(sql -e "SELECT id FROM users WHERE maildir=$(sqlq "$dir") UNION SELECT id FROM domains WHERE homedir=$(sqlq "$dir")")" ] \
		|| errore "archivio sconosciuto: $dir"
	[ -f "$dir/exmdb/exchange.sqlite3" ] || errore "manca il database in $dir"
}

# Controllo senza fermare niente: struttura di SQLite (quick_check), i controlli di gromox (mbck, solo
# lettura) e l'indice di IMAP/POP3. L'ultima riga dice l'esito.
archivio_controlla() {
	archivio_valido "$1"
	local dir="$1" db="$1/exmdb/exchange.sqlite3" r esito=0
	r="$(runuser -u gromox -- sqlite3 "$db" "PRAGMA quick_check;" 2>&1 | head -12)" || true
	if [ "$r" = "ok" ]; then
		echo "struttura del database: a posto"
	else
		echo "struttura del database: rovinata"; printf '%s\n' "$r" | sed 's/^/  /'; esito=1
	fi
	r="$(runuser -u gromox -- gromox-mbck "$db" 2>&1 | tail -12)" || esito=1
	[ -z "$r" ] || { echo "controlli di gromox:"; printf '%s\n' "$r" | sed 's/^/  /'; }
	if [ -f "$dir/exmdb/midb.sqlite3" ]; then
		r="$(runuser -u gromox -- sqlite3 "$dir/exmdb/midb.sqlite3" "PRAGMA quick_check;" 2>&1 | head -5)" || true
		if [ "$r" = "ok" ]; then echo "indice IMAP/POP3: a posto"; else echo "indice IMAP/POP3: rovinato (la riparazione lo rifà)"; esito=1; fi
	fi
	if [ "$esito" = 0 ]; then echo "ESITO: a posto"; else echo "ESITO: problemi"; fi
}

# Riparazione. Prima la copia di sicurezza di tutto l'archivio (database e allegati), poi:
#  1. struttura di SQLite: se integrity_check non dice «ok», il database si ricostruisce con
#     .recover (quello che si legge ancora) e si mette al posto di quello rovinato;
#  2. gromox-mbck -p: le prenotazioni degli identificativi che mancano;
#  3. l'indice di IMAP/POP3 (midb), se è rovinato, si rifà da capo;
#  4. si ricalcola lo spazio occupato.
archivio_ripara() {
	archivio_valido "$1"; valida_copia "$2"
	local dir="$1" copia="$2" db="$1/exmdb/exchange.sqlite3" r esito=0 utente
	utente="$(sql -e "SELECT username FROM users WHERE maildir=$(sqlq "$dir")")"
	install -d -o gromox -g gromox -m 0770 "$COPIE"
	ferma_gromox
	copia_archivio "$dir" "$copia" || errore "copia di sicurezza non riuscita: non ho toccato niente"
	echo "copia di sicurezza fatta: $copia"
	r="$(runuser -u gromox -- sqlite3 "$db" "PRAGMA integrity_check;" 2>&1 | head -12)" || true
	if [ "$r" = "ok" ]; then
		echo "struttura del database: a posto"
	else
		echo "struttura del database rovinata:"; printf '%s\n' "$r" | sed 's/^/  /'
		echo "ricostruisco il database con quello che si legge ancora"
		local nuovo="$dir/exmdb/riparato.sqlite3" tabelle_prima tabelle_dopo
		rm -f -- "$nuovo" "$nuovo-wal" "$nuovo-shm" "$nuovo-journal"
		# .recover vuole sqlite_dbpage: c'è nel nostro sqlite3-recupero (quello di Ubuntu non lo ha)
		tabelle_prima="$(runuser -u gromox -- sqlite3-recupero "$db" "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;" 2>/dev/null | grep -v '^sqlite_' | tr '\n' ' ')" || true
		{ echo "PRAGMA foreign_keys=0;"; runuser -u gromox -- sqlite3-recupero "$db" ".recover" 2>>"$LOG/riparazione.log"; } \
			| runuser -u gromox -- sqlite3-recupero "$nuovo" >>"$LOG/riparazione.log" 2>&1 || true
		tabelle_dopo="$(runuser -u gromox -- sqlite3-recupero "$nuovo" "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;" 2>/dev/null | grep -v '^sqlite_\|^lost_and_found' | tr '\n' ' ')" || true
		r="$(runuser -u gromox -- sqlite3 "$nuovo" "PRAGMA integrity_check;" 2>&1 | head -3)" || true
		# il database ricostruito vale solo se ha tutte le tabelle di prima (un database vuoto è «ok» ma non serve)
		if [ "$r" = "ok" ] && [ -n "$tabelle_prima" ] && [ "$tabelle_prima" = "$tabelle_dopo" ]; then
			echo "recuperati: $(runuser -u gromox -- sqlite3 "$nuovo" "SELECT (SELECT COUNT(*) FROM folders) || ' cartelle, ' || (SELECT COUNT(*) FROM messages) || ' messaggi';" 2>/dev/null)"
			rm -f -- "$db" "$db-wal" "$db-shm"
			mv -- "$nuovo" "$db"
			chown gromox:gromox "$db"; chmod 0660 "$db"
			echo "database ricostruito"
		else
			rm -f -- "$nuovo" "$nuovo-wal" "$nuovo-shm" "$nuovo-journal"
			echo "il database non si ricostruisce: usa «Ricostruisci» (ricopia la posta in un archivio nuovo) o il ripristino dal backup"
			esito=1
		fi
	fi
	if [ "$esito" = 0 ]; then
		r="$(runuser -u gromox -- gromox-mbck -p "$db" 2>&1 | tail -12)" || esito=1
		[ -z "$r" ] || { echo "controlli di gromox:"; printf '%s\n' "$r" | sed 's/^/  /'; }
	fi
	local midb="$dir/exmdb/midb.sqlite3" rifai=0
	if [ -n "$utente" ] && [ -f "$midb" ]; then
		r="$(runuser -u gromox -- sqlite3 "$midb" "PRAGMA integrity_check;" 2>&1 | head -3)" || true
		if [ "$r" != "ok" ]; then rm -f -- "$midb" "$midb-wal" "$midb-shm"; rifai=1; fi
	fi
	riavvia_gromox
	if [ "$rifai" = 1 ]; then
		if runuser -u gromox -- gromox-mkmidb -f "$utente" >/dev/null 2>&1; then echo "indice IMAP/POP3 rifatto"
		else echo "indice IMAP/POP3 non rifatto"; esito=1; fi
	fi
	sleep 1
	runuser -u gromox -- gromox-mbop -d "$dir" recalc-sizes >/dev/null 2>&1 || true
	if [ "$esito" = 0 ]; then echo "ESITO: riparato"; else echo "ESITO: problemi"; fi
}

archivio_comprimi() {
	archivio_valido "$1"
	local prima; prima="$(kb "$1"/exmdb/exchange.sqlite3*)"
	runuser -u gromox -- gromox-mbop -d "$1" purge-datafiles >/dev/null 2>&1 || true
	runuser -u gromox -- gromox-mbop -d "$1" vacuum >/dev/null 2>&1 || errore "compattazione non riuscita"
	echo "database da $prima kB a $(kb "$1"/exmdb/exchange.sqlite3*) kB"
}

dkim_chiave() {
	local d="${1,,}"; valida_dominio "$d"
	local chiave="$DKIM/$d.$SELETTORE.key"
	if [ ! -s "$chiave" ]; then
		( umask 077; openssl genrsa -out "$chiave.nuova" 2048 >/dev/null 2>&1 ) || errore "chiave DKIM non creata"
		mv "$chiave.nuova" "$chiave"
		chown root:_rspamd "$chiave"; chmod 0640 "$chiave"
	fi
	printf '%s\tv=DKIM1; k=rsa; p=%s\n' "$SELETTORE" "$(openssl rsa -in "$chiave" -pubout -outform DER 2>/dev/null | base64 -w0)"
}

regole_mappe() {
	local m
	for m in $MAPPE_REGOLE; do [ -f "$REGOLE/$m" ] || : > "$REGOLE/$m"; postmap "$REGOLE/$m"; done
	for m in ingresso-intestazioni uscita-intestazioni; do [ -f "$REGOLE/$m" ] || : > "$REGOLE/$m"; done
}

regole_applica() {
	regole_mappe
	postfix status >/dev/null 2>&1 && postfix reload >/dev/null 2>&1 || true
	echo "regole applicate"
}

# copia del database degli utenti per il backup: domini, caselle con le password, alias, aziende, gruppi
db_copia() {
	local f="$DATI/backup/database.sql.gz"
	install -d -m 0700 "$DATI/backup"
	mariadb-dump --protocol=socket -S /run/mysqld/mysqld.sock -uroot --single-transaction --routines --events \
		--databases gromox | gzip -6 > "$f.nuovo"
	mv -f "$f.nuovo" "$f"
	echo "$f"
}

# una cartella d'archivio senza il suo database (exchange.sqlite3): se dentro c'è qualcosa va tra le copie
metti_da_parte() {
	local dir="$1" n
	[ -d "$dir" ] || return 0
	if [ -z "$(ls -A -- "$dir" 2>/dev/null)" ]; then rmdir -- "$dir"; return 0; fi
	n="$(date +%Y%m%d-%H%M%S)-senza-database-$(basename "$(dirname "$(dirname "$dir")")")-$(basename "$(dirname "$dir")")-$(basename "$dir")"
	install -d -o gromox -g gromox -m 0770 "$COPIE"
	mv -- "$dir" "$COPIE/${n:0:120}" && echo "la cartella $dir (senza database) è nelle copie: ${n:0:120}"
}

# Archivi che mancano (caselle e cartelle pubbliche senza exchange.sqlite3): si creano vuoti. Da chiamare
# con gromox fermo (dentro db_ripristina) o da solo: allora ferma gromox solo se serve.
archivi_mancanti() {
	local id u d dir n=0 da_solo="${1:-}"
	local -a caselle=() pubbliche=()
	while IFS=$'\t' read -r id u dir; do
		[ -n "$dir" ] && [ ! -f "$dir/exmdb/exchange.sqlite3" ] && caselle+=("$u"$'\t'"$dir")
	done < <(sql -e "SELECT u.id, u.username, u.maildir FROM users u WHERE u.maildir<>'' AND $NON_TEMP")
	while IFS=$'\t' read -r d dir; do
		[ -n "$dir" ] && [ ! -f "$dir/exmdb/exchange.sqlite3" ] && pubbliche+=("$d"$'\t'"$dir")
	done < <(sql -e "SELECT domainname, homedir FROM domains")
	[ "${#caselle[@]}" -gt 0 ] || [ "${#pubbliche[@]}" -gt 0 ] || return 0
	[ "$da_solo" = da-solo ] && ferma_gromox
	local voce
	for voce in "${caselle[@]}"; do
		u="${voce%%$'\t'*}"; dir="${voce#*$'\t'}"
		[[ "$dir" == "$GDB/"*"/caselle/"?* || "$dir" == "$DATI/gromox/user/"?* ]] || { echo "casella $u: cartella non valida ($dir)"; continue; }
		metti_da_parte "$dir"
		install -d -o gromox -g gromox -m 0770 "$(dirname "$dir")" "$dir" "$dir/exmdb" "$dir/tmp" "$dir/tmp/imap.rfc822" \
			"$dir/tmp/faststream" "$dir/eml" "$dir/ext" "$dir/cid" "$dir/config"
		runuser -u gromox -- gromox-mkprivate "$u" >/dev/null 2>&1 || echo "casella $u: archivio vuoto non creato"
		runuser -u gromox -- gromox-mkmidb "$u" >/dev/null 2>&1 || true
		n=$((n + 1)); echo "casella senza archivio, creata vuota: $u"
	done
	for voce in "${pubbliche[@]}"; do
		d="${voce%%$'\t'*}"; dir="${voce#*$'\t'}"
		[[ "$dir" == "$GDB/"*"/pubblica/"?* || "$dir" == "$DATI/gromox/domain/"?* ]] || { echo "dominio $d: cartella non valida ($dir)"; continue; }
		metti_da_parte "$dir"
		install -d -o gromox -g gromox -m 0770 "$(dirname "$dir")" "$dir" "$dir/exmdb" "$dir/cid" "$dir/log" "$dir/tmp"
		runuser -u gromox -- gromox-mkpublic "$d" >/dev/null 2>&1 || echo "dominio $d: cartelle pubbliche vuote non create"
		n=$((n + 1)); echo "cartelle pubbliche senza archivio, create vuote: $d"
	done
	[ "$da_solo" = da-solo ] && riavvia_gromox
	return 0
}

# Ripristino del database degli utenti (domini, caselle con le password, alias, aziende) da una copia
# del backup, con gromox fermo una volta sola:
#   1. copia di sicurezza del database di adesso in <copia>;
#   2. il database della copia al posto di quello di adesso (se non va, torna quello di prima);
#   3. gli archivi che il database ripristinato non conosce vanno da parte, in <copia>;
#   4. le coppie «archivio ripristinato dal backup» → «cartella dell'archivio» (ripristino di tutto il
#      server): l'archivio ripristinato va al suo posto, quello che c'era va in <copia>;
#   5. con «vuoti», le caselle e i domini rimasti senza archivio ne hanno uno nuovo, vuoto.
#   posta db-ripristina <copia.sql.gz> <copia> [vuoti|niente] [<ripristinato> <archivio>]...
db_ripristina() {
	local f="$1" copia="$2" vuoti="${3:-vuoti}"
	shift $(($# < 3 ? $# : 3))
	[[ "$f" =~ ^/srv/posta/gromox/copie/[A-Za-z0-9._-]{1,120}(/[A-Za-z0-9._-]{1,120}){0,3}\.sql\.gz$ ]] && [[ "$f" != *..* ]] \
		|| errore "copia del database non valida"
	[[ "$vuoti" =~ ^(vuoti|niente)$ ]] || errore "terzo argomento: vuoti o niente"
	[ $(($# % 2)) = 0 ] || errore "servono coppie: cartella ripristinata e archivio"
	local -a da=() a=()
	while [ "$#" -ge 2 ]; do
		valida_ripristinato "$1"
		[[ "$2" =~ ^/srv/posta/gromox/database/[a-z0-9][a-z0-9-]{0,31}/(caselle|pubblica)/[A-Za-z0-9._-]{1,100}$ ]] && [[ "$2" != *..* ]] \
			|| errore "archivio non valido: $2"
		da+=("$1"); a+=("$2"); shift 2
	done
	[ -s "$f" ] && gzip -t "$f" 2>/dev/null || errore "la copia del database è vuota o rovinata"
	grep -q '^-- Current Database: `gromox`' < <(zcat "$f") || errore "nella copia non c'è il database degli utenti"
	valida_copia "$copia"
	install -d -m 0700 "$copia"
	mariadb-dump --protocol=socket -S /run/mysqld/mysqld.sock -uroot --single-transaction --routines --events \
		--databases gromox | gzip -6 > "$copia/database-prima.sql.gz" || errore "copia di sicurezza del database non riuscita"
	echo "copia di sicurezza del database degli utenti fatta"
	# dove sono gli archivi adesso: se una casella è stata spostata in un altro database dopo il backup,
	# la sua posta resta dov'è (vedi sotto)
	local -A prima_u=() prima_d=()
	local id u d dir
	while IFS=$'\t' read -r id u dir; do [ -n "$id" ] && prima_u["$id"]="$u"$'\t'"$dir"; done \
		< <(sql -e "SELECT u.id, u.username, u.maildir FROM users u WHERE u.maildir<>'' AND $NON_TEMP")
	while IFS=$'\t' read -r d dir; do [ -n "$d" ] && prima_d["$d"]="$dir"; done < <(sql -e "SELECT domainname, homedir FROM domains")
	ferma_gromox
	# della copia si prende solo il database degli utenti (le copie vecchie avevano anche quelli di sistema)
	if ! { echo "DROP DATABASE IF EXISTS gromox;"; zcat "$f" | awk 'BEGIN { s = 1 } /^-- Current Database: / { s = ($0 ~ /`gromox`/) } s'; } \
			| mariadb --protocol=socket -S /run/mysqld/mysqld.sock -uroot >>"$LOG/ripristino.log" 2>&1; then
		{ echo "DROP DATABASE IF EXISTS gromox;"; zcat "$copia/database-prima.sql.gz"; } \
			| mariadb --protocol=socket -S /run/mysqld/mysqld.sock -uroot >>"$LOG/ripristino.log" 2>&1 || true
		prepara_db
		aggiorna_mappe_postfix
		errore "ripristino del database non riuscito (vedi $LOG/ripristino.log): è rimasto quello di prima"
	fi
	prepara_db
	# caselle temporanee rimaste nella copia (backup fatto durante una ricostruzione o un recupero)
	sql -e "DELETE p FROM user_properties p JOIN users u ON u.id=p.user_id WHERE u.username LIKE 'tlcmail-tmp-%'; DELETE FROM users WHERE username LIKE 'tlcmail-tmp-%'"
	echo "database degli utenti ripristinato: $(pl "$(sql -e "SELECT COUNT(*) FROM domains")" dominio domini), $(pl "$(sql -e "SELECT COUNT(*) FROM users u WHERE u.maildir<>'' AND $NON_TEMP")" casella caselle), $(pl "$(sql -e "SELECT COUNT(*) FROM aliases")" alias alias), $(pl "$(sql -e "SELECT COUNT(*) FROM orgs")" azienda aziende)"
	# caselle e cartelle pubbliche spostate in un altro database dopo il backup: se dal backup non arriva
	# il loro archivio, resta quello di adesso, dov'è (stessa casella: stesso numero e stesso indirizzo)
	local -A coperti=()
	local i v adesso lasciati=0
	for i in "${!a[@]}"; do coperti["${a[$i]}"]=1; done
	while IFS=$'\t' read -r id u dir; do
		[ -n "$dir" ] && [ -z "${coperti[$dir]:-}" ] && [ ! -f "$dir/exmdb/exchange.sqlite3" ] || continue
		v="${prima_u[$id]:-}"; [ -n "$v" ] && [ "${v%%$'\t'*}" = "$u" ] || continue
		adesso="${v#*$'\t'}"
		[ "$adesso" != "$dir" ] && [ -f "$adesso/exmdb/exchange.sqlite3" ] || continue
		sql -e "UPDATE users SET maildir=$(sqlq "$adesso") WHERE id=$id" && lasciati=$((lasciati + 1))
	done < <(sql -e "SELECT u.id, u.username, u.maildir FROM users u WHERE u.maildir<>'' AND $NON_TEMP")
	while IFS=$'\t' read -r d dir; do
		[ -n "$dir" ] && [ -z "${coperti[$dir]:-}" ] && [ ! -f "$dir/exmdb/exchange.sqlite3" ] || continue
		adesso="${prima_d[$d]:-}"
		[ -n "$adesso" ] && [ "$adesso" != "$dir" ] && [ -f "$adesso/exmdb/exchange.sqlite3" ] || continue
		sql -e "UPDATE domains SET homedir=$(sqlq "$adesso") WHERE domainname=$(sqlq "$d")" && lasciati=$((lasciati + 1))
	done < <(sql -e "SELECT domainname, homedir FROM domains")
	[ "$lasciati" = 0 ] || echo "archivi spostati in un altro database dopo il backup, lasciati dove sono: $lasciati"
	# archivi che il database ripristinato non conosce: da parte, nella copia di sicurezza
	local n orfani=0
	for dir in "$GDB"/*/caselle/* "$GDB"/*/pubblica/*; do
		[ -d "$dir" ] || continue
		[ -z "$(sql -e "SELECT id FROM users WHERE maildir=$(sqlq "$dir") UNION SELECT id FROM domains WHERE homedir=$(sqlq "$dir")")" ] || continue
		n="$(basename "$(dirname "$(dirname "$dir")")")-$(basename "$(dirname "$dir")")-$(basename "$dir")"
		install -d -m 0700 "$copia/archivi-non-piu-usati"
		mv -- "$dir" "$copia/archivi-non-piu-usati/$n" && orfani=$((orfani + 1))
	done
	[ "$orfani" = 0 ] || echo "archivi che nel backup non ci sono, messi da parte nella copia: $orfani"
	# archivi dal backup al loro posto
	local fatti=0 saltati=0 errori=0
	for i in "${!da[@]}"; do
		if [ -z "$(sql -e "SELECT id FROM users WHERE maildir=$(sqlq "${a[$i]}") UNION SELECT id FROM domains WHERE homedir=$(sqlq "${a[$i]}")")" ]; then
			saltati=$((saltati + 1)); continue          # nel database ripristinato non c'è più
		fi
		n="$(basename "$(dirname "$(dirname "${a[$i]}")")")-$(basename "$(dirname "${a[$i]}")")-$(basename "${a[$i]}")"
		if [ -d "${a[$i]}" ]; then
			install -d -m 0700 "$copia/archivi-di-prima"
			if ! mv -- "${a[$i]}" "$copia/archivi-di-prima/$n"; then
				echo "${a[$i]}: non riesco a mettere da parte l'archivio attuale"; errori=$((errori + 1)); continue
			fi
		fi
		install -d -o gromox -g gromox -m 0770 "$(dirname "${a[$i]}")"
		if mv -- "${da[$i]}" "${a[$i]}"; then
			chown -R gromox:gromox "${a[$i]}"
			fatti=$((fatti + 1))
		else
			rm -rf -- "${a[$i]}"
			[ -d "$copia/archivi-di-prima/$n" ] && mv -- "$copia/archivi-di-prima/$n" "${a[$i]}"
			echo "${a[$i]}: archivio non ripristinato"; errori=$((errori + 1))
		fi
	done
	[ "${#da[@]}" = 0 ] || echo "archivi ripristinati dal backup: $fatti$([ "$saltati" = 0 ] || echo "; di caselle che non ci sono più: $saltati")$([ "$errori" = 0 ] || echo "; non riusciti: $errori")"
	[ "$vuoti" = vuoti ] && archivi_mancanti
	aggiorna_mappe_postfix
	riavvia_gromox
	# il database degli utenti è già quello del backup: gli archivi non riusciti sono nelle righe sopra
	echo "fatto; quello che c'era prima è nella copia $copia"
}

coda() {
	postqueue -j 2>/dev/null || true
}
# un messaggio in coda: rilascia (dalla quarantena) o elimina
coda_azione() {
	local id="$2"
	[[ "$id" =~ ^[0-9A-Za-z]{5,32}$ ]] || errore "messaggio non valido"
	local r
	case "$1" in
	rilascia) r="$(postsuper -H "$id" 2>&1)" || errore "non riesco a rilasciare il messaggio" ;;
	elimina) r="$(postsuper -d "$id" 2>&1)" || errore "non riesco a eliminare il messaggio" ;;
	*) errore "azione sconosciuta" ;;
	esac
	printf '%s\n' "$r" | tail -1
	postqueue -f >/dev/null 2>&1 || true
}

# Il certificato in dati/ssl è cambiato (per esempio quello di DSM, rinnovato): Postfix e gromox lo
# rileggono. gromox lo legge solo all'avvio: un fermo di pochi secondi, come per gli spostamenti.
certificato_applica() {
	[ -s "$DATI/ssl/cert.pem" ] && [ -s "$DATI/ssl/key.pem" ] || errore "manca il certificato"
	openssl x509 -in "$DATI/ssl/cert.pem" -noout >/dev/null 2>&1 || errore "il certificato non si legge"
	chown root:gromox "$DATI/ssl" "$DATI/ssl/cert.pem" "$DATI/ssl/key.pem"
	chmod 0750 "$DATI/ssl"; chmod 0640 "$DATI/ssl/cert.pem" "$DATI/ssl/key.pem"
	postfix status >/dev/null 2>&1 && postfix reload >/dev/null 2>&1 || true
	ferma_gromox
	riavvia_gromox
	echo "certificato applicato: $(openssl x509 -in "$DATI/ssl/cert.pem" -noout -subject -enddate 2>/dev/null | tr '\n' ' ')"
}

antispam_applica() {
	leggi_conf
	genera_rspamd
	vivo rspamd && kill -HUP "$(cat "$RUN/rspamd.pid")" 2>/dev/null || true
	echo "antispam aggiornato"
}

prova_password() {
	local u="${1,,}"; valida_indirizzo "$u"
	local p; p="$(leggi_password)"
	testsaslauthd -u "$u" -p "$p" -s smtp -f /run/saslauthd/mux
}

case "${1:-}" in
avvia) avvia ;;
ferma) ferma ;;
stato) stato ;;
versione) versione ;;
domini) domini ;;
dominio-aggiungi) dominio_aggiungi "${2:?dominio}" "${3:-}" ;;
dominio-rimuovi) dominio_rimuovi "${2:?dominio}" ;;
caselle) caselle "${2:-}" ;;
casella-aggiungi) casella_aggiungi "${2:?indirizzo}" "${3:-}" "${4:-}" ;;
casella-password) casella_password "${2:?indirizzo}" ;;
casella-nome) casella_nome "${2:?indirizzo}" "${3:-}" ;;
casella-sospendi) casella_sospendi "${2:?indirizzo}" "${3:?si o no}" ;;
casella-rimuovi) casella_rimuovi "${2:?indirizzo}" ;;
alias) alias_elenco ;;
alias-aggiungi) alias_aggiungi "${2:?alias}" "${3:?casella}" ;;
alias-rimuovi) alias_rimuovi "${2:?alias}" ;;
occupazione) occupazione ;;
prova-password) prova_password "${2:?indirizzo}" ;;
dkim) dkim_chiave "${2:?dominio}" ;;
regole-applica) regole_applica ;;
db-copia) db_copia ;;
db-ripristina) db_ripristina "${2:?copia}" "${3:?copia di sicurezza}" "${4:-vuoti}" "${@:5}" ;;
archivi-mancanti) archivi_mancanti da-solo ;;
coda) coda ;;
aziende) aziende ;;
azienda-aggiungi) azienda_aggiungi "${2:?nome}" "${3:-}" ;;
azienda-modifica) azienda_modifica "${2:?numero}" "${3:?nome}" "${4:-}" ;;
azienda-rimuovi) azienda_rimuovi "${2:?numero}" ;;
dominio-azienda) dominio_azienda "${2:?dominio}" "${3:?azienda}" ;;
archivi) archivi ;;
archivio-controlla) archivio_controlla "${2:?cartella}" ;;
archivio-ripara) archivio_ripara "${2:?cartella}" "${3:?copia}" ;;
archivio-comprimi) archivio_comprimi "${2:?cartella}" ;;
casella-sposta) caselle_sposta "${3:?database}" "${2:?indirizzo}" ;;
caselle-sposta) caselle_sposta "${2:?database}" "${@:3}" ;;
casella-ricostruisci) casella_ricostruisci "${2:?indirizzo}" "${3:?database}" "${4:?copia}" ;;
dominio-sposta) dominio_sposta "${2:?dominio}" "${3:?database}" ;;
casella-recupera) casella_recupera "${2:?indirizzo}" "${3:?cartella}" "${4:?nome}" ;;
ripristina-archivi) ripristina_archivi "${2:?copia}" "${@:3}" ;;
coda-rilascia) coda_azione rilascia "${2:?messaggio}" ;;
coda-elimina) coda_azione elimina "${2:?messaggio}" ;;
coda-riprova) postqueue -f && echo "consegna riprovata" ;;
antispam-applica) antispam_applica ;;
certificato-applica) certificato_applica ;;
elenco) echo "DOMINI"; domini; echo "CASELLE"; caselle ;;
*) echo "uso: posta avvia|ferma|stato|versione|domini|dominio-aggiungi D|dominio-rimuovi D|caselle [D]|casella-aggiungi I [NOME]|casella-password I|casella-nome I NOME|casella-sospendi I si|no|casella-rimuovi I|alias|alias-aggiungi A I|alias-rimuovi A|occupazione|prova-password I" >&2; exit 2 ;;
esac
