docker-mailserver/target/scripts/helpers/relay.sh
Brennan Kinney 40e2d88482
chore: Merge helpers/sasl.sh into helpers/relay.sh (#2605)
This helper was to support an earlier ENV for SASL auth support. When extracting logic into individual helpers, it was assumed this was separate from relay support, which it appears was not the case.

---

The `SASL_PASSWD` ENV is specified in tests but no longer used. There is no `external-domain.com` relay configured or tested against anywhere in the project.

The ENV was likely used in tests prior to improved relay support that allowed for adding more than a single set of relay credentials.

---

It likewise has no real relevance anywhere else outside of `relay.sh` as it's the only portion of code to operate with it.

It's only relevant for SASL auth as an SMTP client, not the SMTP server (`smtpd`) SASL support that is delegated to Dovecot. Functionality has been completely migrated into `relay.sh` as a result.

Documentation is poor for this ENV, it is unlikely in wide use? Should consider for removal.

---

The ENV has been dependent upon `RELAY_HOST` to actually enable postfix to use `/etc/postfix/sasl_passwd`, thus not likely relevant in existing setups?

---

Migrate `/etc/postfix/sasl_passwd` check from `tests.bats` as it belongs to relay tests.
2022-06-06 10:59:42 +12:00

381 lines
17 KiB
Bash

#! /bin/bash
# Support for Relay Hosts
# Description:
# This helper is responsible for configuring outbound SMTP (delivery) through relay-hosts.
#
# When mail is sent from Postfix, it is considered relaying to that destination (or the next hop).
# By default delivery external of the container would be direct to the MTA of the recipient address (destination).
# Alternatively mail can be indirectly delivered to the destination by routing through a different MTA (relay-host service).
#
# This helper is only concerned with relaying mail from authenticated submission (ports 587 + 465).
# Thus it does not deal with `relay_domains` (which routes through `relay_transport` transport, default: `master.cf:relay`),
# that is intended for forwarding inbound mail (including from port 25) for any permitted domains.
# User Docs:
# https://docker-mailserver.github.io/docker-mailserver/edge/config/advanced/mail-forwarding/relay-hosts/
# Supported `setup` commands:
# setup.sh relay add-auth <domain> <username> [<password>]
# https://github.com/docker-mailserver/docker-mailserver/blob/master/target/bin/addsaslpassword
#
# setup.sh relay add-domain <domain> <host> [<port>]
# https://github.com/docker-mailserver/docker-mailserver/blob/master/target/bin/addrelayhost
#
# setup.sh relay exclude-domain <domain>
# https://github.com/docker-mailserver/docker-mailserver/blob/master/target/bin/excluderelaydomain
# Responsible for these files:
# postfix-sasl-password.cf
# postfix-relaymap.cf
# /etc/postfix/relayhost_map
# /etc/postfix/sasl_passwd
#
# The config syntax uses white-space (any length is valid) to separate values on the same line.
# The table type `texthash` does not need to go through `postmap` after changes.
# It is however sensitive to changes when replacing the file with new content instead of appending.
# `postfix reload` or `supervisorctl restart postfix` should be run to properly apply config (which it is).
# Otherwise use another table type such as `hash` and run `postmap` on the table after modification.
#
# WARNING: Databases (tables above) are rebuilt during change detection. There is a minor chance of
# a lookup occuring during a rebuild of these files that may affect or delay delivery?
# TODO: Should instead perform an atomic operation with a temporary file + `mv` to replace?
# Or switch back to using `hash` table type if plaintext access is not needed (unless retaining file for postmap).
# Either way, plaintext copy is likely accessible if using our supported configs for providing them to the container.
# NOTE: Present support has enforced wrapping the relay host with `[]` (prevents DNS MX record lookup),
# which restricts what is supported by RELAY_HOST, although you usually do want to provide MX host directly.
# NOTE: Present support expects to always append a port with an implicit default of `25`.
# NOTE: DEFAULT_RELAY_HOST imposes neither restriction, but would only be compatible with SASL_PASSWD then when
# auth is needed. However that seems tied to RELAY_HOST to enable the /etc/postfix/sasl_passwd table lookup,
# which introduces issues if you would want DEFAULT_RELAY_HOST to use credentials..
#
# TODO: RELAY_PORT should be optional, it will use the transport default port (`postconf smtp_tcp_port`),
# That shouldn't be a breaking change, as long as the mapping is maintained correctly.
# TODO: RELAY_HOST should consider dropping `[]` and require the user to include that?
# Future refactor for _populate_relayhost_map may warrant dropping these two ENV in favor of DEFAULT_RELAY_HOST?
function _env_relay_host
{
echo "[${RELAY_HOST}]:${RELAY_PORT:-25}"
}
# Responsible for `postfix-sasl-password.cf` support:
# `/etc/postfix/sasl_passwd` example at end of file.
function _relayhost_sasl
{
if [[ ! -f /tmp/docker-mailserver/postfix-sasl-password.cf ]] \
&& [[ -z ${RELAY_USER} || -z ${RELAY_PASSWORD} ]] \
&& [[ -z ${SASL_PASSWD} ]]
then
_log 'warn' "Missing relay-host mapped credentials provided via ENV, or from postfix-sasl-password.cf"
return 1
fi
_log 'trace' "Adding relay-host credential mappings to Postfix"
# Start from a new `/etc/postfix/sasl_passwd`:
: >/etc/postfix/sasl_passwd
chown root:root /etc/postfix/sasl_passwd
chmod 0600 /etc/postfix/sasl_passwd
# SASL_PASSWD is a legacy ENV, not likely in use by any users.
#
# Single ENV for specifying `<DEFAULT_RELAY_HOST> <RELAY_USER>:<RELAY_PASSWORD>`,
# Where `<DEFAULT_RELAY_HOST>` must match the equivalent ENV,
# while the other two have no dependency to their equivalent ENV.
# SASL_PASSWD requires `smtp_sasl_password_maps` to be enabled - but that has only
# ever been via this function which relies upon RELAY_HOST. Hence redundant.
# TODO: Deprecate. Remove on next major version?
if [[ -n ${SASL_PASSWD} ]]
then
echo "${SASL_PASSWD}" >> /etc/postfix/sasl_passwd
fi
if [[ -f /tmp/docker-mailserver/postfix-sasl-password.cf ]]
then
# Add domain-specific auth from config file:
while read -r LINE
do
if ! _is_comment "${LINE}"
then
echo "${LINE}" >> /etc/postfix/sasl_passwd
fi
done < /tmp/docker-mailserver/postfix-sasl-password.cf
# Only relevant when providing this user config (unless users append elsewhere too)
postconf 'smtp_sender_dependent_authentication = yes'
fi
# Add an authenticated relay host defined via ENV config:
if [[ -n ${RELAY_USER} ]] && [[ -n ${RELAY_PASSWORD} ]]
then
echo "$(_env_relay_host) ${RELAY_USER}:${RELAY_PASSWORD}" >> /etc/postfix/sasl_passwd
fi
# Technically if only a single relay host is configured, a `static` lookup table could be used instead?:
# postconf "smtp_sasl_password_maps = static:${RELAY_USER}:${RELAY_PASSWORD}"
postconf 'smtp_sasl_password_maps = texthash:/etc/postfix/sasl_passwd'
}
# Responsible for `postfix-relaymap.cf` support:
# `/etc/postfix/relayhost_map` example at end of file.
#
# Present support uses a table lookup for sender address or domain mapping to relay-hosts,
# Populated via `postfix-relaymap.cf `, which also features a non-standard way to exclude implicitly added internal domains from the feature.
# It also maps all known sender domains (from configs postfix-accounts + postfix-virtual.cf) to the same ENV configured relay-host.
#
# TODO: The account + virtual config parsing and appending to /etc/postfix/relayhost_map seems to be an excessive `main.cf:relayhost`
# implementation, rather than leveraging that for the same purpose and selectively overriding only when needed with `/etc/postfix/relayhost_map`.
# If the issue was to opt-out select domains, if avoiding a default relay-host was not an option, then mapping those sender domains or addresses
# to a separate transport (which can drop the `relayhost` setting) would be more appropriate.
# TODO: With `sender_dependent_default_transport_maps`, we can extract out the excluded domains and route them through a separate transport.
# while deprecating that support in favor of a transport config, similar to what is offered currently via sasl_passwd and relayhost_map.
function _populate_relayhost_map
{
# Create the relayhost_map config file:
: >/etc/postfix/relayhost_map
chown root:root /etc/postfix/relayhost_map
chmod 0600 /etc/postfix/relayhost_map
# Matches lines that are not comments or only white-space:
local MATCH_VALID='^\s*[^#[:space:]]'
# This config is mostly compatible with `/etc/postfix/relayhost_map`, but additionally supports
# not providing a relay host for a sender domain to opt-out of RELAY_HOST? (2nd half of function)
if [[ -f /tmp/docker-mailserver/postfix-relaymap.cf ]]
then
_log 'trace' "Adding relay mappings from postfix-relaymap.cf"
# Match two values with some white-space between them (eg: `@example.test [relay.service.test]:465`):
local MATCH_VALUE_PAIR='\S*\s+\S'
# Copy over lines which are not a comment *and* have a destination.
sed -n -r "/${MATCH_VALID}${MATCH_VALUE_PAIR}/p" /tmp/docker-mailserver/postfix-relaymap.cf >>/etc/postfix/relayhost_map
fi
# Everything below here is to parse `postfix-accounts.cf` and `postfix-virtual.cf`,
# extracting out the domain parts (value of email address after `@`), and then
# adding those as mappings to ENV configured RELAY_HOST for lookup in `/etc/postfix/relayhost_map`.
# Provided `postfix-relaymap.cf` didn't exclude any of the domains,
# and they don't already exist within `/etc/postfix/relayhost_map`.
#
# TODO: Breaking change. Replace this lower half and remove the opt-out feature from `postfix-relaymap.cf`.
# Leverage `main.cf:relayhost` for setting a default relayhost as it was prior to this feature addition.
# Any sender domains or addresses that need to opt-out of that default relay-host can either
# map to a different relay-host, or use a separate transport (needs feature support added).
# Args: <PRINT_DOMAIN_PART_> <config filepath>
function _list_domain_parts
{
[[ -f $2 ]] && sed -n -r "/${MATCH_VALID}/ ${1}" "${2}"
}
# Matches and outputs (capture group via `/\1/p`) the domain part (value of address after `@`) in the config file.
local PRINT_DOMAIN_PART_ACCOUNTS='s/^[^@|]*@([^\|]+)\|.*$/\1/p'
local PRINT_DOMAIN_PART_VIRTUAL='s/^\s*[^@[:space:]]*@(\S+)\s.*/\1/p'
{
_list_domain_parts "${PRINT_DOMAIN_PART_ACCOUNTS}" /tmp/docker-mailserver/postfix-accounts.cf
_list_domain_parts "${PRINT_DOMAIN_PART_VIRTUAL}" /tmp/docker-mailserver/postfix-virtual.cf
} | sort -u | while read -r DOMAIN_PART
do
# DOMAIN_PART not already present in `/etc/postfix/relayhost_map`, and not listed as a relay opt-out domain in `postfix-relaymap.cf`
# `^@${DOMAIN_PART}\b` - To check for existing entry, the `\b` avoids accidental partial matches on similar domain parts.
# `^\s*@${DOMAIN_PART}\s*$` - Matches line with only a domain part (eg: @example.test) to avoid including a mapping for those domains to the RELAY_HOST.
if ! grep -q -e "^@${DOMAIN_PART}\b" /etc/postfix/relayhost_map && ! grep -qs -e "^\s*@${DOMAIN_PART}\s*$" /tmp/docker-mailserver/postfix-relaymap.cf
then
_log 'trace' "Adding relay mapping for ${DOMAIN_PART}"
echo "@${DOMAIN_PART} $(_env_relay_host)" >> /etc/postfix/relayhost_map
fi
done
postconf 'sender_dependent_relayhost_maps = texthash:/etc/postfix/relayhost_map'
}
function _relayhost_configure_postfix
{
postconf -e \
"smtp_sasl_auth_enable = yes" \
"smtp_sasl_security_options = noanonymous" \
"smtp_tls_security_level = encrypt"
}
function _setup_relayhost
{
_log 'debug' 'Setting up Postfix Relay Hosts'
if [[ -n ${DEFAULT_RELAY_HOST} ]]
then
_log 'trace' "Setting default relay host ${DEFAULT_RELAY_HOST} to /etc/postfix/main.cf"
postconf -e "relayhost = ${DEFAULT_RELAY_HOST}"
fi
if [[ -n ${RELAY_HOST} ]]
then
_log 'trace' "Setting up relay hosts (default: ${RELAY_HOST})"
_relayhost_sasl
_populate_relayhost_map
_relayhost_configure_postfix
fi
}
function _rebuild_relayhost
{
if [[ -n ${RELAY_HOST} ]]
then
_relayhost_sasl
_populate_relayhost_map
fi
}
#
# Config examples for reference
#
# main.cf:smtp_sasl_password_maps = texthash:/etc/postfix/sasl_passwd
# https://www.postfix.org/postconf.5.html#smtp_sasl_password_maps
#
# /etc/postfix/sasl_passwd
# --
# # Popular relay service examples (ports used are only to demonstrate variety):
# [smtp.sendgrid.net]:2525 apikey:actual-generated-api-key
# [in.mailjet.com]:587 apikey:secretkey
# [smtp.mailgun.org]:465 postmaster@mydomain.com:password
# [email-smtp.us-west-2.amazonaws.com]:2465 SMTPUSERNAME:SMTPPASSWORD
#
# # No explicit port provided is valid. It will use the default port of the active transport:
# [mx.relay-service.test] relay-account:relay-pass
# # Without [], a DNS lookup for MX record will be performed:
# relay-service.test relay-account:relay-pass
#
#
# # Sender dependent credentials have priority over relay host credentials.
# # They will use a matching sender dependent relay-host,
# # or fallback to a default if configured.
#
# # You can provide a full sender address to use different credentials:
# user@domain1.test relay-account:relay-pass
#
# # Or for all users in a sender domain, with different relay-host each,
# # or sharing the same relay-host with different credentials:
# @domain1.test domain1-account:domain1-pass
# @domain2.test domain2-account:domain2-pass
# main.cf:sender_dependent_relayhost_maps = texthash:/etc/postfix/relayhost_map
# https://www.postfix.org/postconf.5.html#sender_dependent_relayhost_maps
# TODO: Official Postfix SASL_README docs page names the file `/etc/postfix/sender_relay` instead.
#
# setup /etc/postfix/relayhost_map
# --
# @domain1.test [smtp.mailgun.org]:465
# @domain2.test [smtp.mailgun.org]:465
# @domain3.test [smtp.sendgrid.net]:2525
#
# # Can also use specific user or FQDN to lookup relay-host MX record:
# user@domain1.test relay-service.test
#
# Relevant Postfix docs
#
# Enabling SASL authentication in the Postfix SMTP/LMTP client:
# https://www.postfix.org/SASL_README.html#client_sasl_enable
#
# Explains required settings for SASL client auth with relay support:
# smtp_sasl_auth_enable = yes
# smtp_tls_security_level = encrypt
# smtp_tls_security_options = noanonymous
#
# Details that configured relay-hosts must have an exact match for
# successful credentials lookup in `smtp_sasl_password_maps`.
#
# Advises that `/etc/postfix/sasl_passwd` is read+write only (600) for root,
# Along with an example using `hash` lookup table instead of `texthash`.
# Configuring sender-dependent SASL authentication:
# https://www.postfix.org/SASL_README.html#client_sasl_sender
#
# Explains that `/etc/postfix/sasl_passwd` table may map lookups by
# sender address or relay-host as keys to `user:password` values.
# Sender address has priority over relay-host and only supported when
# enabled with: `smtp_sender_dependent_authentication = yes`.
#
# Likewise those senders can be matched to different relay-hosts in the:
# `sender_dependent_relayhost_maps` table, otherwise they will fallback
# to the default relay-host (`main.cf:relayhost` setting).
#
# Advice to maintainers
#
# WARNING: Maintainers be wary of relay service docs/blogs, especially their advice for configuring Postfix.
#
# Not necessary:
# - `smtp_tls_note_starttls_offer = yes` - Only adds a log to know when an unencrypted
# connection was made, but STARTTLS was offered:
# https://www.postfix.org/postconf.5.html#smtp_tls_note_starttls_offer
# - `smtp_use_tls = yes` - Implied when using `smtp_tls_security_level = encrypt`:
# https://www.postfix.org/postconf.5.html#smtp_tls_security_level
#
#
#
# MailJet:
# https://dev.mailjet.com/smtp-relay/configuration/
# https://www.mailjet.com/blog/news/which-smtp-port-mailjet/#port-465
# They describes port 465 support akin to it's prior purpose before RFC 8314 (2018).
# Every other supported port is considered "TLS" which is presumably explicit TLS (STARTTLS),
# while 465 is considered "SSL" (but unlike legacy purpose mandates authorization), presumably implicit TLS?
#
# Supported SMTP ports: https://dev.mailjet.com/smtp-relay/configuration/
# Explicit TLS: 25, 2525, 80, 587, 588 | Implicit TLS: 465
# States explicit TLS ports do not mandate TLS to connect successfully (bad).
#
#
#
# SendGrid:
# https://docs.sendgrid.com/for-developers/sending-email/integrating-with-the-smtp-api
# https://docs.sendgrid.com/for-developers/sending-email/getting-started-smtp
# Appears to make a similar distinction of port 465 as "SSL" and others "TLS".
# They at least seem aware of explicit (587) and implicit (465) TLS differences in their own blog.
# Although it's not clear if they restrict 465 to SSLv3 and earlier.. Doubtful.
#
# https://sendgrid.com/blog/whats-the-difference-between-ports-465-and-587/
# However they confusingly cite 465 is used for StartTLS (never was),
# and incorrectly describe how they deliver mail:
# https://sendgrid.com/blog/what-is-starttls/
#
# Supported SMTP ports: https://docs.sendgrid.com/for-developers/sending-email/integrating-with-the-smtp-api#smtp-ports
# Explicit TLS: 25, 2525, 587 | Implicit TLS: 465
# States explicit TLS ports do not mandate TLS to connect successfully (bad).
#
#
#
# MailGun:
# https://documentation.mailgun.com/en/latest/user_manual.html#smtp-relay
# Bad: Advises `smtp_tls_security_level = may` without enforcing TLS, allowing for unencrypted auth to relay.
# Bad: Advises setting `smtpd_tls` parameters (including legacy ones for key/cert).
# `smtpd_` is only for inbound mail, not relevant to sending / relaying mail from your MTA.
#
# Supported SMTP ports: https://documentation.mailgun.com/en/latest/user_manual.html#sending-via-smtp
# Explicit TLS: 25, 2525, 587 | Implicit TLS: 465
# All ports make TLS mandatory to connect successfully.
#
#
#
# Amazon SES:
# https://docs.aws.amazon.com/ses/latest/dg/postfix.html
# Decent docs, only lists a few unnecessary config parameters.
#
# Supported SMTP Ports: https://docs.aws.amazon.com/ses/latest/dg/smtp-connect.html
# Explicit TLS: 25, 587, 2587 | Implicit TLS: 465, 2465
# All ports make TLS mandatory to connect successfully. Port 25 may be throttled.
# Service can be configured to receive mail without requiring authentication.