msimerson/Mail-Toaster-6

View on GitHub
provision/dovecot.sh

Summary

Maintainability
Test Coverage
#!/bin/sh

set -e -u

. mail-toaster.sh

export JAIL_START_EXTRA="allow.sysvipc=1"
export JAIL_CONF_EXTRA=""
export JAIL_FSTAB="$ZFS_DATA_MNT/vpopmail/home $ZFS_JAIL_MNT/dovecot/usr/local/vpopmail nullfs rw 0 0"

mt6-include vpopmail
mt6-include mua

allow_sysvipc_stage()
{
    tell_status "allow sysvipc for the staged jail"
    jail -m name=stage allow.sysvipc=1
}

install_dovecot()
{
    tell_status "installing dovecot package"
    stage_pkg_install dovecot dovecot-pigeonhole curl perl5 gmake mysql80-client

    tell_status "configure dovecot port options"
    stage_make_conf dovecot2_SET 'mail_dovecot2_SET=MYSQL LIBWRAP EXAMPLES'
    stage_make_conf dovecot_SET 'mail_dovecot_SET=MYSQL LIBWRAP EXAMPLES'

    tell_status "creating vpopmail user & group"
    stage_exec pw groupadd -n vpopmail -g 89
    stage_exec pw useradd -n vpopmail -s /nonexistent -d /usr/local/vpopmail -u 89 -g 89 -m -h-

    if [ "$TLS_LIBRARY" = "libressl" ]; then
        echo 'DEFAULT_VERSIONS+=ssl=libressl' >> "$STAGE_MNT/etc/make.conf"
    fi

    tell_status "building dovecot"

    export BATCH=${BATCH:="1"}
    stage_port_install mail/dovecot
    stage_port_install mail/dovecot-pigeonhole
}

configure_dovecot_local_conf() {
    local _localconf="$ZFS_DATA_MNT/dovecot/etc/local.conf"

    store_config "$_localconf" <<'EO_DOVECOT_LOCAL'
#mail_debug = yes
listen = *, ::
auth_verbose=yes
auth_mechanisms = plain login digest-md5 cram-md5 scram-sha-1 scram-sha-256
auth_username_format = %Lu
disable_plaintext_auth = no
first_valid_gid = 89
first_valid_uid = 89
last_valid_gid = 89
last_valid_uid = 89
mail_privileged_group = 89
login_greeting = Mail Toaster (Dovecot) ready.
mail_plugins = $mail_plugins quota
protocols = imap pop3 lmtp sieve
service auth {
  unix_listener auth-client {
    mode = 0660
  }
  unix_listener auth-master {
    mode = 0600
  }
#  unix_listener /var/spool/postfix/private/auth {
#    # SASL for Postfix smtp-auth
#    mode = 0666
#  }
}

service lmtp {
  user = vpopmail
  inet_listener lmtp {
    port = 24
  }
  unix_listener lmtp {
    #mode = 0666
  }
}

passdb {
  driver = sql
  args = /data/etc/dovecot-sql.conf.ext
}
userdb {
  driver = prefetch
}
userdb {
  # This userdb is used only by lda.
  driver = sql
  args = /data/etc/dovecot-sql.conf.ext
}

shutdown_clients = no
verbose_proctitle = yes
protocol imap {
  imap_client_workarounds = delay-newmail  tb-extra-mailbox-sep
  mail_max_userip_connections = 45
  mail_plugins = $mail_plugins imap_quota trash imap_sieve
}
protocol pop3 {
  pop3_client_workarounds = outlook-no-nuls oe-ns-eoh
  pop3_uidl_format = %08Xu%08Xv
}
protocol lmtp {
  mail_fsync = optimized
  mail_plugins = $mail_plugins sieve
}

# default TLS certificate (no SNI)
ssl_cert = </data/etc/ssl/certs/dovecot.pem
ssl_key = </data/etc/ssl/private/dovecot.pem

# example TLS SNI (see https://wiki.dovecot.org/SSL/DovecotConfiguration)
#local_name mail.example.com {
#  ssl_cert = </data/etc/ssl/certs/mail.example.com.pem
#  ssl_key = </data/etc/ssl/private/mail.example.com.pem
#}

# dovecot 2.3+ supports a ssl_dh file
ssl_dh = </etc/ssl/dhparam.pem

# recommended settings for high security (2019)
ssl_prefer_server_ciphers = yes
ssl_cipher_list = AES128+EECDH:AES128+EDH

login_access_sockets = tcpwrap

service tcpwrap {
  unix_listener login/tcpwrap {
    mode = 0600
    user = $default_login_user
    group = $default_login_user
  }
  user = root
}
service managesieve-login {
  inet_listener sieve {
    port = 4190
  }
}
plugin {
  quota = maildir:User quota
  quota_rule = *:storage=1G
  quota_rule2 = Trash:storage=+10%%
  quota_rule3 = Spam:storage=+20%%

  sieve_plugins = sieve_imapsieve sieve_extprograms

  # From elsewhere to Junk, train as Spam
  imapsieve_mailbox1_name = Junk
  imapsieve_mailbox1_causes = COPY
  imapsieve_mailbox1_after  = file:/usr/local/lib/dovecot/sieve/report-spam.sieve

  # From elsewhere to Spam, train as Spam
  imapsieve_mailbox2_name = Spam
  imapsieve_mailbox2_causes = COPY
  imapsieve_mailbox2_after  = file:/usr/local/lib/dovecot/sieve/report-spam.sieve

  # From Junk to elsewhere, train as Ham
  imapsieve_mailbox3_name = *
  imapsieve_mailbox3_from = Junk
  imapsieve_mailbox3_causes = COPY
  imapsieve_mailbox3_after  = file:/usr/local/lib/dovecot/sieve/report-ham.sieve

  # From Spam to elsewhere, train as Ham
  imapsieve_mailbox4_name = *
  imapsieve_mailbox4_from = Spam
  imapsieve_mailbox4_causes = COPY
  imapsieve_mailbox4_after  = file:/usr/local/lib/dovecot/sieve/report-ham.sieve

  # From elsewhere to Archive, train as Ham
  imapsieve_mailbox5_name = Archive
  imapsieve_mailbox5_causes = COPY
  imapsieve_mailbox5_after  = file:/usr/local/lib/dovecot/sieve/report-ham.sieve

  sieve_pipe_bin_dir = /usr/local/lib/dovecot/sieve

  sieve_global_extensions = +vnd.dovecot.pipe
}

namespace inbox {
  mail_location = maildir:~/Maildir
  mailbox Spam {
    auto = no
    special_use = \Junk
  }
  mailbox Archive {
    special_use = \Archive
  }
}
EO_DOVECOT_LOCAL

}

configure_dovecot_sql_conf()
{
    local _localconf="$ZFS_DATA_MNT/dovecot/etc/local.conf"
    if grep -q -E 'driver[[:space:]]*=[[:space:]]*sql' $_localconf; then
        tell_status "passdb conversion to SQL already complete"
    else
        tell_status "converting dovecot passdb to SQL"
        jexec stage perl -i.bak -0777 -pe 's/passdb \{.*?\}/passdb {
  driver = sql
  args = \/data\/etc\/dovecot-sql.conf.ext
 }/sg;
 s/userdb \{.*?\}/userdb {
   driver = prefetch
 }
 userdb {
   # used only by lda.
   driver = sql
   args = \/data\/etc\/dovecot-sql.conf.ext
 }/sg' /data/etc/local.conf
    fi

    _localconf="$ZFS_DATA_MNT/dovecot/etc/dovecot-sql.conf.ext"
    if grep -q -E 'driver[[:space:]]*=[[:space:]]mysql' $_localconf; then
        tell_status "SQL configured."
    else
        tell_status "configuring SQL"
        local _sqlconf="$ZFS_DATA_MNT/dovecot/etc/dovecot-sql.conf.ext"

        # shellcheck disable=SC2034
        _vpass=$(grep -v ^# "$ZFS_DATA_MNT/vpopmail/home/etc/vpopmail.mysql" | head -n1 | cut -f4 -d'|')

        store_config "$_sqlconf" "overwrite" <<EO_DOVECOT_SQL
  driver = mysql
  default_pass_scheme = PLAIN
  connect = host=mysql user=vpopmail password=$_vpass dbname=vpopmail

  password_query = SELECT \\
    CONCAT(v.pw_name, '@', v.pw_domain) AS user \\
    ,v.pw_clear_passwd AS password \\
    ,v.pw_dir AS userdb_home \\
    ,89 AS userdb_uid \\
    ,89 AS userdb_gid \\
    ,CONCAT('*:bytes=', REPLACE(SUBSTRING_INDEX(v.pw_shell, 'S', 1), 'NOQUOTA', '0')) AS userdb_quota_rule \\
    FROM vpopmail v \\
      LEFT JOIN aliasdomains a ON a.alias='%d' \\
    WHERE v.pw_name = '%n' \\
      AND (v.pw_domain='%d' OR v.pw_domain=a.domain) \\
      AND ('%a'!='995' OR !(v.pw_gid & 2)) \\
      AND ('%a'!='993' OR !(v.pw_gid & 8))

  user_query = SELECT pw_dir as home \\
    ,89 AS uid ,89 AS gid \\
    ,CONCAT('*:bytes=', REPLACE(SUBSTRING_INDEX(pw_shell, 'S', 1), 'NOQUOTA', '0')) AS quota_rule \\
    FROM vpopmail \\
    WHERE pw_name = '%n' \\
      AND pw_domain = '%d'

  iterate_query = SELECT CONCAT(pw_name, '@', pw_domain) AS user FROM vpopmail
EO_DOVECOT_SQL
    fi
}

configure_example_config()
{
    local _dcdir="$ZFS_DATA_MNT/dovecot/etc"

    if [ -f "$_dcdir/dovecot.conf" ]; then
        tell_status "dovecot config files already present"
        return
    fi

    tell_status "installing example config files"
    cp -R "$STAGE_MNT/usr/local/etc/dovecot/example-config/" "$_dcdir/"
    sed -i.bak \
        -e 's/^#listen = \*, ::/listen = \*/' \
        "$_dcdir/dovecot.conf"
}

configure_system_auth()
{
    local _authconf="$ZFS_DATA_MNT/dovecot/etc/conf.d/10-auth.conf"
    if ! grep -qs '^!include auth\-system' "$_authconf"; then
        tell_status "system auth already disabled"
        return
    fi

    tell_status "disabling auth-system"
    sed -i.bak \
        -e '/^\!include auth-system/ s/\!/#!/' \
        "$_authconf"
}

configure_vsz_limit()
{
    local _master="$ZFS_DATA_MNT/dovecot/etc/conf.d/10-master.conf"
    if grep -q ^default_vsz_limit "$_master"; then
        tell_status "vsz_limit already configured"
        return
    fi

    tell_status "bumping up default_vsz_limit 256 -> 384"
    sed -i.bak \
        -e '/^#default_vsz_limit/ s/#//; s/256/384/' \
        "$_master"
}

configure_tls_certs()
{
    local _sslconf="$ZFS_DATA_MNT/dovecot/etc/conf.d/10-ssl.conf"
    if grep -qs ^ssl_cert "$_sslconf"; then
        tell_status "removing ssl_cert from 10-ssl.conf"
        sed -i.bak \
            -e '/ssl_cert/ s/^s/#s/' \
            -e '/ssl_key/ s/^s/#s/' \
            "$_sslconf"
    fi

    local _localconf="$ZFS_DATA_MNT/dovecot/etc/local.conf"
    if grep -qs dovecot.pem "$_localconf"; then
        sed -i.bak \
            -e "/^ssl_cert/ s/dovecot/${TOASTER_MAIL_DOMAIN}/" \
            -e "/^ssl_key/ s/dovecot/${TOASTER_MAIL_DOMAIN}/" \
            "$_localconf"
    fi

    local _ssldir="$ZFS_DATA_MNT/dovecot/etc/ssl"
    if [ ! -d "$_ssldir/certs" ]; then
        mkdir -p "$_ssldir/certs"
        chmod 644 "$_ssldir/certs"
    fi

    if [ ! -d "$_ssldir/private" ]; then
        mkdir "$_ssldir/private"
        chmod 644 "$_ssldir/private"
    fi

    local _installed_crt="$_ssldir/certs/${TOASTER_MAIL_DOMAIN}.pem"
    if [ -f "$_installed_crt" ]; then
        tell_status "dovecot TLS certificates already installed"
        return
    fi

    tell_status "installing dovecot TLS certificates"
    cp /etc/ssl/certs/server.crt "$_ssldir/certs/${TOASTER_MAIL_DOMAIN}.pem"
    # sunset after Dovecot 2.3 released
    cat /etc/ssl/dhparam.pem >> "$_ssldir/certs/${TOASTER_MAIL_DOMAIN}.pem"
    # /sunset
    cp /etc/ssl/private/server.key "$_ssldir/private/${TOASTER_MAIL_DOMAIN}.pem"
}

configure_postfix_with_sasl()
{
    # ignore this, it doesn't exist. Yet. Maybe not ever. It's one way to
    # configure a MSA with dovecot auth.
    stage_pkg_install postfix

    stage_exec postconf -e "relayhost = $TOASTER_MSA"
    stage_exec postconf -e 'smtpd_sasl_type = dovecot'
    stage_exec postconf -e 'smtpd_sasl_path = private/auth'
    stage_exec postconf -e 'smtpd_sasl_auth_enable = yes'
    stage_exec postconf -e 'smtpd_recipient_restrictions = permit_sasl_authenticated,permit_mynetworks,reject_unauth_destination'
    stage_exec postconf -e "smtpd_tls_cert_file = /data/etc/ssl/certs/$TOASTER_HOSTNAME.pem"
    stage_exec postconf -e "smtpd_tls_key_file = /data/etc/ssl/private/$TOASTER_HOSTNAME.pem"
    stage_exec postconf -e 'smtp_tls_security_level = may'

    for _s in 512 1024 2048; do
        openssl dhparam -out /tmp/dh$_s.tmp $_s
        chmod 644 /tmp/dh${_s}.tmp
        mv /tmp/dh${_s}.tmp "$STAGE_MNT/usr/local/etc/postfix/dh${_s}.pem"
        stage_exec postconf -e "smtpd_tls_dh${_s}_param_file = \${config_directory}/dh${_s}.pem"
    done

    stage_sysrc postfix_enable="YES"
    stage_exec service postfix start
}

compile_sieve()
{
    stage_exec /usr/local/bin/sievec -c /data/etc/dovecot.conf "/usr/local/lib/dovecot/sieve/$1"
}

configure_sieve_report_ham()
{
    if [ -x "$SIEVE_DIR/report-ham.sieve" ]; then
        return
    fi

    store_config "$SIEVE_DIR/report-ham.sieve" <<'EO_REPORT_HAM'
require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"];

if environment :matches "imap.mailbox" "*" {
  set "mailbox" "${1}";
}

if string "${mailbox}" "Trash" {
  stop;
}

if environment :matches "imap.user" "*" {
  set "username" "${1}";
}

EO_REPORT_HAM
}

configure_sieve_report_spam()
{
    if [ -x "$SIEVE_DIR/report-spam.sieve" ]; then
        return
    fi

    store_config "$SIEVE_DIR/report-spam.sieve" <<'EO_REPORT_SPAM'
# https://wiki2.dovecot.org/Pigeonhole/Sieve
require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"];

if environment :matches "imap.user" "*" {
  set "username" "${1}";
}

EO_REPORT_SPAM
}

configure_sieve_learn_rspamd()
{
    if ! grep ^jail_list /etc/rc.conf | grep -q rspamd; then
        echo "skip rspamd learning: it is not enabled"
        return
    fi

    tell_status "adding learn-ham-rspamd.sh"
    tee "$SIEVE_DIR/learn-ham-rspamd.sh" <<EO_RSPAM_LEARN_HAM
exec /usr/local/bin/curl -s -S -XPOST --data-binary @- http://$(get_jail_ip rspamd):11334/learnham
EO_RSPAM_LEARN_HAM
    chmod +x "$SIEVE_DIR/learn-ham-rspamd.sh"

    if ! grep rspamd "$SIEVE_DIR/report-ham.sieve"; then
        tell_status "enabling rspamd learning in report-ham.sieve"
        tee -a "$SIEVE_DIR/report-ham.sieve" <<'EO_REPORT_HAM_RSPAMD'
pipe :copy "learn-ham-rspamd.sh" [ "${username}" ];
EO_REPORT_HAM_RSPAMD
        compile_sieve report-ham.sieve
    fi

    tell_status "adding learn-spam-rspamd.sh"
    tee "$SIEVE_DIR/learn-spam-rspamd.sh" <<EO_RSPAM_LEARN_SPAM
exec /usr/local/bin/curl -s -S -XPOST --data-binary @- http://$(get_jail_ip rspamd):11334/learnspam
EO_RSPAM_LEARN_SPAM
    chmod +x "$SIEVE_DIR/learn-spam-rspamd.sh"

    if ! grep rspamd "$SIEVE_DIR/report-spam.sieve"; then
        tell_status "enabling rspamd learning in report-spam.sieve"
        tee -a "$SIEVE_DIR/report-spam.sieve" <<'EO_REPORT_SPAM_RSPAMD'
pipe :copy "learn-spam-rspamd.sh" [ "${username}" ];
EO_REPORT_SPAM_RSPAMD
        compile_sieve report-spam.sieve
    fi
}

configure_sieve_learn_spamassassin()
{
    if ! grep ^jail_list /etc/rc.conf | grep -q spamassassin; then
        echo "skip spamassassin learning: it is not enabled"
        return
    fi

    if [ ! -x "$ZFS_DATA_MNT/dovecot/bin/spamc" ]; then
        tell_status "copying spamc into /data/bin"
        cp "$ZFS_JAIL_MNT/spamassassin/usr/local/bin/spamc" \
            "$ZFS_DATA_MNT/dovecot/bin/spamc"
    fi

    tell_status "creating learn-ham-sa.sh"
    tee "$SIEVE_DIR/learn-ham-sa.sh" <<EO_RSPAM_LEARN_HAM
exec /data/bin/spamc -d $(get_jail_ip spamassassin) --learntype=ham -u \${1}
EO_RSPAM_LEARN_HAM
    chmod +x "$SIEVE_DIR/learn-ham-sa.sh"

    if ! grep learn-ham-sa "$SIEVE_DIR/report-ham.sieve"; then
        tell_status "enabling spamassassin learning in report-ham.sieve"
        tee -a "$SIEVE_DIR/report-ham.sieve" <<'EO_REPORT_HAM_SA'
pipe :copy "learn-ham-sa.sh" [ "${username}" ];
EO_REPORT_HAM_SA
        compile_sieve report-ham.sieve
    fi

    tell_status "creating learn-spam-sa.sh"
    tee "$SIEVE_DIR/learn-spam-sa.sh" <<EO_RSPAM_LEARN_SPAM
exec /data/bin/spamc -d $(get_jail_ip spamassassin) --learntype=spam -u \${1}
EO_RSPAM_LEARN_SPAM
    chmod +x "$SIEVE_DIR/learn-spam-sa.sh"

    if ! grep learn-spam-sa "$SIEVE_DIR/report-spam.sieve"; then
        tell_status "enabling spamassassin learning in report-spam.sieve"
        tee -a "$SIEVE_DIR/report-spam.sieve" <<'EO_REPORT_SPAM_SA'
pipe :copy "learn-spam-sa.sh" [ "${username}" ];
EO_REPORT_SPAM_SA
        compile_sieve report-spam.sieve
    fi
}

configure_sieve()
{
    SIEVE_DIR="$STAGE_MNT/usr/local/lib/dovecot/sieve"
    if [ ! -d "$SIEVE_DIR" ]; then
        mkdir "$SIEVE_DIR"
    fi

    local _lc="$ZFS_DATA_MNT/dovecot/etc/local.conf"
    if [ -f "$_lc" ] && ! grep -q sieve "$_lc"; then
        tell_status "sieve not configured. Update local.conf and reinstall dovecot to enable"
        return
    fi

    configure_sieve_report_ham
    configure_sieve_report_spam

    configure_sieve_learn_rspamd
    configure_sieve_learn_spamassassin
}

configure_dovecot_pf()
{
    _pf_etc="$ZFS_DATA_MNT/dovecot/etc/pf.conf.d"

    store_config "$_pf_etc/insecure_mua" <<EO_PF_INSECURE
# 10.0.0.0/8
# 172.16.0.0/12
# 192.168.0.0/16
EO_PF_INSECURE

    store_config "$_pf_etc/rdr.conf" <<EO_PF_RDR
int_ip4 = "$(get_jail_ip dovecot)"
int_ip6 = "$(get_jail_ip6 dovecot)"

# to permit legacy users to access insecure POP3 & IMAP, add their IPs/masks
table <insecure_mua> persist file "$_pf_etc/insecure_mua"

rdr inet  proto tcp from any to <ext_ip4> port { 993 995 } -> \$int_ip4
rdr inet6 proto tcp from any to <ext_ip6> port { 993 995 } -> \$int_ip6

rdr inet  proto tcp from <insecure_mua> to <ext_ip4> port { 110 143 } -> \$int_ip4
rdr inet6 proto tcp from <insecure_mua> to <ext_ip6> port { 110 143 } -> \$int_ip6
EO_PF_RDR

    store_config "$_pf_etc/allow.conf" <<EO_PF_ALLOW
int_ip4 = "$(get_jail_ip dovecot)"
int_ip6 = "$(get_jail_ip6 dovecot)"

table <dovecot_int> persist { \$int_ip4, \$int_ip6 }

pass in quick proto tcp from any to <ext_ip> port { 993 995 }
pass in quick proto tcp from any to <dovecot_int> port { 993 995 }

pass in quick proto tcp from <insecure_mua> to <dovecot_int> port { 110 143 }
EO_PF_ALLOW
}

configure_dovecot()
{
    for _d in etc bin; do
        local _dcdir="$ZFS_DATA_MNT/dovecot/${_d}"

        if [ ! -d "$_dcdir" ]; then
            tell_status "creating $_dcdir"
            echo "mkdir $_dcdir"
            mkdir "$_dcdir"
        fi
    done

    configure_dovecot_local_conf
    configure_example_config
    configure_dovecot_sql_conf
    configure_system_auth
    configure_vsz_limit
    configure_tls_certs
    configure_sieve
    configure_dovecot_pf

    mkdir -p "$STAGE_MNT/var/spool/postfix/private"
}

start_dovecot()
{
    tell_status "starting dovecot"
    stage_sysrc dovecot_enable=YES
    stage_sysrc dovecot_config="/data/etc/dovecot.conf"
    stage_exec service dovecot start
}

test_dovecot()
{
    tell_status "testing dovecot"
    stage_listening 993 3
    stage_listening 995 3

    MUA_TEST_USER="postmaster@${TOASTER_MAIL_DOMAIN}"
    MUA_TEST_PASS=$(jexec vpopmail /usr/local/vpopmail/bin/vuserinfo -C "${MUA_TEST_USER}")
    MUA_TEST_HOST=$(get_jail_ip stage)
    export MUA_TEST_HOST; export MUA_TEST_USER; export MUA_TEST_PASS

    test_imap
    test_pop3
    echo "it worked"
}

base_snapshot_exists || exit
create_staged_fs dovecot
mkdir -p "$STAGE_MNT/usr/local/vpopmail"
start_staged_jail dovecot
allow_sysvipc_stage
install_dovecot
configure_dovecot
stage_resolv_conf
start_dovecot
test_dovecot
promote_staged_jail dovecot