# Executes a command inside the postfix container. function postfixExec() { k3sRun exec -it deploy/"$postfixKube" -c "$postfixKube" -- "$@" } # Executes a command inside a specific Postfix pod. # $1 (pod): pod name. # $@ (...): command and arguments to execute. function postfixPodExec() { local pod="$1" [[ -n "$pod" ]] || { appError "Pod not specified"; return 1; } shift k3sRun exec pod/"$pod" -c "$postfixKube" -- "$@" } # Converts a domain name to a random 64-char postfix ID. function postfixDomain2id() { domainToRandom "$1" 64 } # Reloads the Postfix daemon. function postfixReload() { postfixExec postfix reload } # Rebuilds lmdb maps for domains, aliases, and senders. function postfixPostmapRebuild() { touch "$postfixConfigPath/$postfixDomainsFile" || return 1 touch "$postfixConfigPath/$postfixAliasesFile" || return 1 touch "$postfixConfigPath/$postfixSendersFile" || return 1 local error runError error postfixExec postmap "lmdb:/config/$postfixDomainsFile" || { appError "Failed rebuild $postfixDomainsFile: $error"; return 1; } runError error postfixExec postmap "lmdb:/config/$postfixAliasesFile" || { appError "Failed rebuild $postfixAliasesFile: $error"; return 1; } runError error postfixExec postmap "lmdb:/config/$postfixSendersFile" || { appError "Failed rebuild $postfixSendersFile: $error"; return 1; } } # Sets or removes a key/value in a postfix map file. # $1 (file): path to the map file. # $2 (key): map key. # [$3] (value): map value; omit to delete the key. function postfixConfigSet() { local file="$1" key="$2" value="$3" [[ -n "$file" ]] || { appError "File not specified"; return 1; } [[ -n "$key" ]] || { appError "Key not specified"; return 1; } value="${value//$'\r'/ }" value="${value//$'\n'/ }" local output error if [[ ! -f "$file" ]]; then mkdir -p "$(dirname -- "$file")" || { appError "Failed to create folder"; return 1; } install -o root -g root -m 644 /dev/null "$file" || { appError "Failed to create file"; return 1; } else runError error sed -i -E "/^[[:space:]]*${key}[[:space:]]+/d" "$file" || { appError "Error writing to a file: $error"; return 1; } fi if [[ -n "$value" ]]; then runShell output error printf "%s\n" "$key $value" ">>" "$file" || { appError "Error writing to a file: $error"; return 1; } fi } # Sets or removes a domain entry in the virtual domains map. # $1 (domain): site domain name. # [$2] (value): map value; omit to delete. function postfixVirtualDomainSet() { local domain domain=$(domainPrepare "$1") postfixConfigSet "$postfixConfigPath/$postfixDomainsFile" "$domain" "$2" } # Sets or removes an entry in the virtual aliases map. function postfixVirtualAliasSet() { postfixConfigSet "$postfixConfigPath/$postfixAliasesFile" "$@" } # Sets or removes a domain entry in the virtual domains map (lmdb-safe variant). # $1 (key): map key. # [$2] (value): map value; omit to delete. function postfixVirtualDomainSet2() { local key="$1" [[ -n "$key" ]] || { appError "Key not specified"; return 1; } local value="$2" local file="$postfixConfigPath/$postfixDomainsFile" local escaped escaped=$(printf '%s\n' "$key" | sed 's/[.[\*^$()+?{}|\\/]/\\&/g') sed -i "/^${escaped}[[:space:]]/d" "$file" || { appError "Failed to clean old key in $file"; return 1; } if [[ -n "$value" ]]; then printf '%s %s\n' "$key" "$value" >> "$file" || { appError "Failed to append to $file"; return 1; } fi } # Adds or removes a (sender, login) pair from the owners file and rebuilds senders map. # $1 (sender): sender address. # $2 (login): SASL login. # [$3] (save): non-empty to add; omit to remove. function postfixSenderOwnerSet() { local sender="$1" login="$2" save="$3" [[ -n "$sender" ]] || { appError "Sender not set"; return 1; } [[ -n "$login" ]] || { appError "Login not set"; return 1; } local ownersFile="$postfixConfigPath/$postfixSendersFile.owners" touch "$ownersFile" || return 1 local tmp tmp=$(mktemp) awk -v s="$sender" -v l="$login" '!(NF>=2 && $1==s && $2==l)' "$ownersFile" >"$tmp" if [[ -n "$save" ]]; then printf '%s %s\n' "$sender" "$login" >>"$tmp" fi mv -f "$tmp" "$ownersFile" postfixSendersRebuild } # Sets or removes an entry in the virtual aliases map (lmdb-safe variant). # $1 (key): map key. # [$2] (value): map value; omit to delete. function postfixVirtualAliasSet2() { local key="$1" [[ -n "$key" ]] || { appError "Key not specified"; return 1; } local value="$2" local file="$postfixConfigPath/$postfixAliasesFile" local escaped escaped=$(printf '%s\n' "$key" | sed 's/[.[\*^$()+?{}|\\/]/\\&/g') sed -i "/^${escaped}[[:space:]]/d" "$file" || { appError "Failed to clean old key in $file"; return 1; } if [[ -n "$value" ]]; then printf '%s %s\n' "$key" "$value" >> "$file" || { appError "Failed to append to $file"; return 1; } fi } # Rebuilds the senders map from the .owners file, deduplicating per sender. function postfixSendersRebuild() { local ownersFile="$postfixConfigPath/$postfixSendersFile.owners" local outFile="$postfixConfigPath/$postfixSendersFile" touch "$ownersFile" || return 1 local tmpNorm tmpOut tmpNorm=$(mktemp) tmpOut=$(mktemp) awk 'NF>=2 && $1 !~ /^#/ { print $1, $2 }' "$ownersFile" >"$tmpNorm" awk ' { s=$1; o=$2; k=s SUBSEP o; if (!seen[k]++) { if (list[s]=="") list[s]=o; else list[s]=list[s] "," o; if (!sseen[s]++) order[++n]=s; } } END { for (i=1; i<=n; i++) { s=order[i]; print s " " list[s]; } } ' "$tmpNorm" >"$tmpOut" mv -f "$tmpOut" "$outFile" rm -f "$tmpNorm" 2>/dev/null || true } # Creates, updates, or deletes a Postfix SASL user. # $1 (login): SASL username. # [$2] (password): password; omit to delete the user. function postfixSaslUserSet() { local login="$1" password="$2" [[ -n "$login" ]] || { appError "Login not specified"; return 1; } local error if [[ -z "$password" ]]; then runError error postfixExec saslpasswd2 -d -f "/config/sasldb2" -u "$postfixDefaultRealm" "$login" || { appError "Failed to delete SASL user $login" return 1 } else runError error postfixExec \ env SASL_LOGIN="$login" SASL_PWD="$password" SASL_DB="/config/sasldb2" SASL_REALM="$postfixDefaultRealm" \ sh -lc 'printf "%s\n" "$SASL_PWD" | saslpasswd2 -p -c -f "$SASL_DB" -u "$SASL_REALM" "$SASL_LOGIN"' || { appError "Failed to create/update SASL user $login: $error" return 1 } fi } # Lists all domains from the virtual domains map file. function postfixDomainList() { local file="$postfixConfigPath/$postfixDomainsFile" [[ -f "$file" ]] || { appError "Domains file not found: $file"; return 1; } awk ' /^[[:space:]]*$/ { next } /^[[:space:]]*#/ { next } { print $1 } ' "$file" | sort -u } # Registers a domain in Postfix: creates SASL user, sets sender ownership, optionally adds alias. # $1 (domain): site domain name. function postfixDomainAdd() { local domain="$1" [[ -n "$domain" ]] || { appError "Domain not specified"; return 1; } local pfxId pfxId=$(postfixDomain2id "$domain") local postfixUser postfixPass postfixUser=$(siteConfigGetOrSet "$domain" "postfixUser" "$pfxId") postfixPass=$(siteConfigGetOrCreate "$domain" "postfixPass") postfixSenderOwnerSet "$postfixPostmaster" "$postfixUser@$postfixDefaultRealm" "SAVE" || return 1 postfixSaslUserSet "$postfixUser" "$postfixPass" || return 1 local postfixForwardTo postfixForwardTo=$(siteConfigGet "$domain" "postfixForwardTo") if [[ -n "$postfixForwardTo" ]]; then postfixVirtualAliasSet "@$domain" "$postfixForwardTo" postfixVirtualDomainSet "$domain" "OK" || return 1 fi } # Removes a domain from Postfix: clears SASL user, sender ownership, alias, and domain entry. # $1 (domain): site domain name. function postfixDomainRemove() { local domain="$1" [[ -n "$domain" ]] || { appError "Domain not specified"; return 1; } local postfixUser postfixUser=$(siteConfigGet "$domain" "postfixUser") [[ -n "$postfixUser" ]] || { appError "postfixUser not found"; return 1; } postfixVirtualDomainSet "$domain" postfixSenderOwnerSet "$postfixPostmaster" "$postfixUser@$postfixDefaultRealm" postfixSaslUserSet "$postfixUser" postfixVirtualAliasSet "@$domain" } # Registers a domain in Postfix and rebuilds lmdb maps. # $1 (domain): site domain name. function postfixConfigRebuild() { local domain error domain=$(domainPrepare "$1") runError error domainCheck "$domain" || { appError "$error"; return 1; } runError error postfixDomainAdd "$domain" || { appError "$error"; return 1; } postfixPostmapRebuild } # Reloads the opendkim daemon. function postfixDkimReload() { postfixExec sh -lc "pkill -HUP -f '/usr/sbin/opendkim' 2>/dev/null" } # Lists domains that have DKIM key material present. function postfixDkimList() { ls -1 "$postfixDkimPath"/keys/*.txt 2>/dev/null | xargs -n1 basename | sed "s/\.txt$//" || true } # Generates a DKIM keypair for a domain and updates SigningTable/KeyTable. # $1 (domain): domain name. function postfixDkimAdd() { local domain="$1" [[ -n "$domain" ]] || { appError "Domain not specified"; return 1; } if ! test -s "$postfixDkimPath/keys/$domain.private" || ! test -s "$postfixDkimPath/keys/$domain.txt"; then postfixExec sh -lc " set -e mkdir -p /etc/opendkim/keys touch /etc/opendkim/SigningTable /etc/opendkim/KeyTable opendkim-genkey -b 2048 -r -D '/etc/opendkim/keys' -d '$domain' -s '$postfixDkimSelector' mv -f \"/etc/opendkim/keys/$postfixDkimSelector.private\" \"/etc/opendkim/keys/$domain.private\" mv -f \"/etc/opendkim/keys/$postfixDkimSelector.txt\" \"/etc/opendkim/keys/$domain.txt\" chown opendkim:opendkim \"/etc/opendkim/keys/$domain.private\" \"/etc/opendkim/keys/$domain.txt\" || true chmod 0600 \"/etc/opendkim/keys/$domain.private\" chmod 0644 \"/etc/opendkim/keys/$domain.txt\" " || { appError "Failed to add DKIM for $domain"; return 1; } fi sed -i "/^\*@$domain[[:space:]]/d" "$postfixDkimPath/SigningTable" sed -i "/^$postfixDkimSelector\._domainkey\.$domain[[:space:]]/d" "$postfixDkimPath/KeyTable" echo "*@$domain $postfixDkimSelector._domainkey.$domain" >> "$postfixDkimPath/SigningTable" echo "$postfixDkimSelector._domainkey.$domain $domain:$postfixDkimSelector:/etc/opendkim/keys/$domain.private" >> "$postfixDkimPath/KeyTable" postfixDkimReload } # Autocreates DKIM keys if missing and returns the TXT record, or lists available domains. # [$1] (domain): domain name; omit to list all domains. function postfixDkimGet() { local domain="$1" if [[ -z "$domain" ]]; then postfixDkimList return fi if [[ ! "$domain" =~ ^[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)+$ ]]; then appError "Invalid domain: $domain" return 1 fi postfixDkimAdd "$domain" || { appError "Failed to create DKIM for $domain"; return 1; } cat "$postfixDkimPath/keys/$domain.txt" 2>/dev/null || true } # Removes DKIM key material for a domain and reloads opendkim. # $1 (domain): domain name. function postfixDkimRemove() { local domain="$1" [[ -n "$domain" ]] || { appError "Domain not specified"; return 1; } sed -i "/^\*@$domain[[:space:]]/d" "$postfixDkimPath/SigningTable" sed -i "/^$postfixDkimSelector\._domainkey\.$domain[[:space:]]/d" "$postfixDkimPath/KeyTable" postfixDkimReload } # Sends mail through Postfix from a raw RFC822 message file. # $1 (mailFrom): envelope sender address. # $2 (msgFile): path to the RFC822 message file. function postfixMailSendFromFile() { local mailFrom="$1" msgFile="$2" [[ -n "$mailFrom" ]] || { appError "Mail not specified"; return 1; } [[ -n "$msgFile" ]] || { appError "File not specified"; return 1; } local output error runShell output error postfixExec sh -lc "sendmail -v -oi -t -f '$mailFrom'" "<" "$msgFile" || { [[ -n "$error" ]] && appError "$error" return 1 } printf '%s' "$output" } # Lists aliases from the virtual aliases map file. function postfixAliasList() { local file="$postfixConfigPath/$postfixAliasesFile" [[ -f "$file" ]] || { appError "Aliases file not found: $file"; return 1; } awk ' /^[[:space:]]*$/ { next } /^[[:space:]]*#/ { next } { print $1 " -> " $2 } ' "$file" | sort -u } # Lists senders from the senders owners file. function postfixSendersList() { local file="$postfixConfigPath/$postfixSendersFile.owners" [[ -f "$file" ]] || { appError "Senders file not found: $file"; return 1; } awk ' /^[[:space:]]*$/ { next } /^[[:space:]]*#/ { next } { print $1 " -> " $2 } ' "$file" | sort -u } # Lists all SASL users from the sasldb2 database. function postfixSaslUserList() { local output error run output error postfixExec sasldblistusers2 -f /config/sasldb2 || { appError "$error"; return 1; } awk -F':' ' /^[[:space:]]*$/ { next } { gsub(/[[:space:]]+$/, "", $1); print $1 } ' <<<"$output" } # Returns the value for a key in a postfix map file; exits non-zero if not found. # $1 (file): path to the map file. # $2 (key): map key to look up. function postfixMapHas() { local file="$1" key="$2" [[ -n "$file" ]] || { appError "File not specified"; return 1; } [[ -f "$file" ]] || { appError "File not found"; return 1; } [[ -n "$key" ]] || { appError "Key not specified"; return 1; } awk -v k="$key" 'NF>=2 && $1==k {print $2; found=1} END {exit !found}' "$file" } # Checks whether (postmaster, login) pair exists in the senders owners file. # $1 (login): SASL login to look up. function postfixSenderOwnerHas() { local login="$1" [[ -n "$login" ]] || { appError "Login not specified"; return 1; } local file="$postfixConfigPath/$postfixSendersFile.owners" [[ -f "$file" ]] || { appError "File not found"; return 1; } awk -v s="$postfixPostmaster" -v l="$login" 'NF>=2 && $1==s && $2==l {found=1} END{exit !found}' "$file" }