Files
SODEW/sodew-bash-7.1.550/libs/modules/postfix.sh
T
2026-08-12 11:09:01 +02:00

392 lines
14 KiB
Bash

# 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"
}