Created by: ston1th 2019-05-29 18:14:59
Updated by: ston1th 2022-01-08 13:36:49
Visibility: Public

OpenBSD Mail Server

Sources:

Intro

How to setup an OpenBSD 6.6 Mail Server with:

Note: This is mostly a copy/past guide. It is still recommended to read the man-pages to ‘understand’ why and how things work.

Note: You need to replace all occurrences of example.com or example.org with your domain of choice.

Ports required:

Mail-Flow

Incoming mail (blacklisted)

pf -> relay to spamd (tarpit)

Incoming mail (greylisted)

pf -> relay to opensmtpd -> rspamd (greylist handling)

Incoming mail (whitelisted)

pf -> relay to opensmtpd -> rspamd (spam filtering) ->
deliver to mda (kopano-dagent)

Outgoing mail (webapp)

kopano (kopano-spooler) -> opensmtpd ->
rspamd (DKIM) -> relay out

Outgoing mail (SMTP)

opensmtpd (AUTH) -> rspamd (DKIM) -> relay out

DNS

Basic DNS config:

cat <<EOF
        IN MX 10 mx.example.com.

@ 86400 IN TXT "v=spf1 a:mx.example.com -all"
        IN CAA 128 issue "letsencrypt.org"
        IN CAA 128 issuewild ";"

mx      IN A <your ip>
mail    IN A <your ip>
mta-sts IN A <your ip>
_mta-sts IN TXT "v=STSv1; id=$(date +%Y%m%d%H%M%SZ)"
EOF

Rspamd

Install:

pkg_add rspamd opensmtpd-filter-rspamd redis p5-Mail-SpamAssassin

Change the controller password:

mkdir /etc/rspamd/{local.d,override.d}

echo "password = \"$(rspamadm pw)\";" > /etc/rspamd/local.d/worker-controller.inc

echo 'local_addrs = "127.0.0.0/8, ::1";' > /etc/rspamd/local.d/options.inc

echo "enabled = true;" > /etc/rspamd/local.d/mx_check.conf

cat <<EOF> /etc/rspamd/local.d/classifier-bayes.conf
servers = "127.0.0.1";
backend = "redis";
EOF

cat <<EOF> /etc/rspamd/local.d/redis.conf
servers = "127.0.0.1";
write_servers = "127.0.0.1";
EOF

cat <<EOF> /etc/rspamd/local.d/neural.conf
servers = 127.0.0.1:6379; # Redis server to store learning data and ANN

train {
  max_train = 1k; # Number of trains per epoch
  max_usages = 20; # Number of learn iterations while ANN data is valid
  spam_score = 8; # Score to learn spam
  ham_score = -2; # Score to learn ham
  learning_rate = 0.01; # Rate of learning (Torch only)
  max_iterations = 25; # Maximum iterations of learning (Torch only)
}

timeout = 20; # Increase redis timeout
EOF

cat <<EOF> /etc/rspamd/local.d/force_actions.conf
rules {
 NOMX {
   action = "reject";
   expression = "MX_MISSING";
   message = "Sending domain has no MX record";
 }
}
EOF
cat <<EOF> /etc/rspamd/local.d/multimap.conf
spammy_tld_env_from {
    type = "from";
    filter = "email:domain:tld";
    map = "https://raw.githubusercontent.com/mxroute/rspamd_rules/master/lists/tlds.txt";
    symbol = "SPAMMY_TLD_ENVFROM";
    description = "Sending TLD likely spam";
    score = 1.5;
    regexp = true;
}

spammy_tld_from {
    type = "header";
    header = "from";
    filter = "email:domain:tld";
    map = "https://raw.githubusercontent.com/mxroute/rspamd_rules/master/lists/tlds.txt";
    symbol = "SPAMMY_TLD_FROM";
    require_symbols = "!SPAMMY_TLD_ENVFROM";
    description = "Sending TLD likely spam";
    score = 1.5;
    regexp = true;
}

MXROUTE_BODY_SPAM {
    type = "content";
    filter = "text";
    map = "https://raw.githubusercontent.com/mxroute/rspamd_rules/master/lists/body-spam.map";
    symbol = "MXROUTE_BODY_SPAM";
    prefilter = true;
    action = "reject";
    regexp = true;
    message = "The text of this email contained a string that we identified to be spam.";
}

SPAMMY_SUBJECT {
    type = "header";
    header = "subject";
    map = "https://raw.githubusercontent.com/mxroute/rspamd_rules/master/lists/subject-spam.map";
    symbol = "SPAMMY_SUBJ";
    prefilter = true;
    action = "reject";
    regexp = true;
}

SENDER_FROM_BLACKLIST {
    type = "from";
    map = "https://raw.githubusercontent.com/mxroute/rspamd_rules/master/lists/blacklisted-sender-strings.map";
    regexp = true;
    description = "Blacklisted sender";
    prefilter = true;
    filter = "email:addr"
    action = "reject";
    message = "This sender has been blocked for matching a known spam trend";
}

MXROUTE_EXPLORING {
    type = "content";
    filter = "text";
    map = "https://raw.githubusercontent.com/mxroute/rspamd_rules/master/lists/exploring.map";
    symbol = "MXROUTE_EXPLORING";
    prefilter = true;
    action = "add_header";
    regexp = true;
    message = "We are observing this data to see how many and what kind of data it returns.";
}
EOF

cat <<EOF> /etc/rspamd/local.d/mx_check.conf
  timeout = 5.0;
  symbol_bad_mx = "MX_INVALID";
  symbol_no_mx = "MX_MISSING";
  symbol_good_mx = "MX_GOOD";
  expire = 86400;
  key_prefix = "rmx";
  enabled = true;
  greylist_invalid = false;
exclude_domains = [
    "https://raw.githubusercontent.com/mxroute/rspamd_rules/master/lists/mxcheck_exclude.inc",
];
EOF

cat <<EOF> /etc/rspamd/override.d/actions.conf
reject = 15;
add_header = 8;
greylist = 40;
EOF

cat <<EOF> /etc/rspamd/override.d/fuzzy_check.conf
rule "rspamd.com" {
  servers = "";
}
EOF

cat <<EOF> /etc/rspamd/override.d/metrics.conf
symbol "MIME_BASE64_TEXT_BOGUS" {
  weight = 3;
  description = "Has text part encoded in base64 that does not contain any 8bit characters";
}

symbol "DMARC_POLICY_REJECT" {
  weight = 100;
  description = "Domain owner requested this be rejected by DMARC";
}

symbol "MXROUTE_BODY_SPAM" {
  weight = 100;
  description = "Message identified by us as likely spam";
}

symbol "ONCE_RECEIVED_STRICT" {
  weight = 0;
  description = "Lets kill this rule";
}

symbol "DBL_ABUSE" {
  weight = 100;
  description = "Email mentions domain listed at Spamhaus DBL";
}

symbol "NO_BOUNCE" {
  weight = 100;
  description = "No bounce loops";
}

symbol "FREEMAIL_TO" {
  weight = 0.0;
  description "Freemail recipients";
}

symbol "SPAMMY_SUBJ" {
  weight = 100;
  description = "Email subject matches spam trend";
}

symbol "IP_SCORE" {
  weight = 0.0;
  description = "IP reputation";
}

symbol "FROM_NEQ_DISPLAY_NAME" {
  weight = 0.0;
  description = "Display name contains an email address different to the From address";
}

symbol "R_SUSPICIOUS_URL" {
  weight = 3.0;
  description = "Suspicious URL";
}

symbol "FORGED_SENDER" {
  weight = 1.5;
  description = "From address has been spoofed";
}

symbol "FREEMAIL_FROM" {
  weight = 3.0;
  description = "From address has been spoofed";
}

symbol "R_RATELIMIT" {
  weight = 9;
  description = "Rate limited";
}

symbol "BROKEN_HEADERS" {
  weight = 0;
  description = "Header check";
}

symbol "FORGED_RECIPIENTS" {
  weight = 0;
  description = "Recipient check";
}

symbol "HAS_PHPMAILER_SIG" {
  weight = 2;
  description = "PHP Mailer header"
}

symbol "SPAM_FLAG" {
  weight = 7;
  description = "Identified as spam by previous server"
}

symbol "MIME_BAD_ATTACHMENT" {
  weight = 0;
  description = "Bad attachment"
}

symbol "BLACKLIST_DMARC" {
  weight = 0;
  description = "Mail comes from the whitelisted domain and has valid failed DMARC and DKIM policies";
}

symbol "MX_INVALID" {
  weight = 0;
  description = "From domain has no valid MX records or this rule sucks";
}

symbol "DBL_SPAM" {
  weight = 15;
  description = "Contains reference domain listed at SpamHaus";
}

symbol "SPOOF_REPLYTO" {
  weight = 0;
  description = "Spoofing reply-to has valid use cases so setting score to 0";
}
EOF

DKIM

Generate keys:

Note: add the outputs of rspamadm dkim_keygen to your DNS config.

mkdir -p /var/lib/rspamd/dkim
rspamadm dkim_keygen -s 'dkim' -b 2048 -d example.com -k /var/lib/rspamd/dkim/example.com.dkim.key
rspamadm dkim_keygen -s 'eddsa' -d example.com -t ed25519 -k /var/lib/rspamd/dkim/example.com.eddsa.key
chmod 640 /var/lib/rspamd/dkim/*
chgrp _rspamd /var/lib/rspamd/dkim/*

Config:

cat <<EOF> /etc/rspamd/local.d/dkim_signing.conf
# allow_username_mismatch = true;

domain {
  example.com {
    selectors [
      {
        path = "/var/lib/rspamd/dkim/example.com.dkim.key";
        selector = "dkim";
      },
      {
        path = "/var/lib/rspamd/dkim/example.com.eddsa.key";
        selector = "eddsa";
      }
    ]
  }
  example.org {
    path = "/var/lib/rspamd/dkim/example.org.eddsa.key";
    selector = "eddsa";
  }
}
EOF
cat <<'EOF'> /etc/rspamd/local.d/worker-normal.inc                                                                                                        
dkim_signing {
  .include(try=true; priority=1,duplicate=merge) "$LOCAL_CONFDIR/local.d/dkim_signing.conf"
}
EOF

Start

Rspamd version 1.9.4 causes zombie processes on shutdown.

Stop workaround:

sed -i '/rc_cmd/d' /etc/rc.d/rspamd
cat <<'EOF'>> /etc/rc.d/rspamd
rc_stop() {
        pkill -T "${daemon_rtable}" -xf "${pexp}"
        i=0
        while [[ $i -lt 5 ]]; do sleep 1; rc_check || return; i=$((i+1)); done
        pkill -9 -T "${daemon_rtable}" -xf "${pexp}"
}

rc_cmd $1
EOF
rcctl enable redis rspamd
rcctl start redis rspamd

httpd

Basic config at OpenBSD httpd

Kopano config:

prefork 4

mx="mx.example.com"
mail="mail.example.com"
mtasts="mta-sts.example.com"

server $mx {
  listen on * port 80
  location "/.well-known/acme-challenge/*" {
    root "/acme"
    request strip 2
  }
  location "/*" {
    block return 301 "https://$mail$DOCUMENT_URI"
  }
}

server $mail {
  listen on * port 80
  location "/.well-known/acme-challenge/*" {
    root "/acme"
    request strip 2
  }
  location "/*" {
    block return 301 "https://$SERVER_NAME$DOCUMENT_URI"
  }
}

server $mail {
  listen on * tls port 443
  tls {
    certificate "/etc/ssl/mx.example.com.fullchain.pem"
    key "/etc/ssl/private/mx.example.com.key"
    ciphers "AEAD-AES256-GCM-SHA384:AEAD-CHACHA20-POLY1305-SHA256:AEAD-AES128-GCM-SHA256:ECDHE+AESGCM+AES256:ECDHE+CHACHA20:ECDHE+AESGCM+AES128"
    ecdhe "X25519:secp384r1:secp521r1"
  }
  connection {
    max request body 20971520
  }
  hsts { max-age 31536000, subdomains }
  root "/kopano-webapp"
  directory index "index.php"
  location "/robots.txt" {
    root "/robots"
  }
  location "*.php" {
    fastcgi socket "/run/php-fpm.sock"
  }
  location "/Microsoft-Server-ActiveSync" {
    root "/z-push/index.php"
    fastcgi socket "/run/php-fpm.sock"
  }
}

server $mtasts {
  listen on * port 80
  location "/.well-known/acme-challenge/*" {
    root "/acme"
    request strip 2
  }
  location "/*" {
    block return 301 "https://$SERVER_NAME$DOCUMENT_URI"
  }
}

server $mtasts {
  listen on * tls port 443
  tls {
    certificate "/etc/ssl/mx.example.com.fullchain.pem"
    key "/etc/ssl/private/mx.example.com.key"
    ciphers "AEAD-AES256-GCM-SHA384:AEAD-CHACHA20-POLY1305-SHA256:AEAD-AES128-GCM-SHA256:ECDHE+AESGCM+AES256:ECDHE+CHACHA20:ECDHE+AESGCM+AES128"
    ecdhe "X25519:secp384r1:secp521r1"
  }
  hsts { max-age 31536000, subdomains }
  root "/htdocs"
}

Add mta-sts file:

mkdir /var/www/htdocs/.well-known
cat <<EOF> /var/www/htdocs/.well-known/mta-sts.txt
version: STSv1
mode: enforce
mx: mx.example.com
max_age: 604800
EOF

Lets Encrypt

Domains:

See: Lets Encrypt

Config:

domain mx.example.com {
        alternative names { mail.example.com, mta-sts.example.com }
        domain key "/etc/ssl/private/mx.example.com.key"
        domain certificate "/etc/ssl/mx.example.com.pem"
        domain full chain certificate "/etc/ssl/mx.example.com.fullchain.pem"
        sign with letsencrypt
}
cat <<'EOF' >/usr/local/sbin/cron-acme-update
#!/bin/sh
/usr/sbin/acme-client example.com

if [ $? -eq 0 ]; then
        rcctl restart smtpd spamd httpd kopano_gateway kopano_ical
fi
EOF

smtp authentication

Install: https://git.giftfish.de/ston1th/smtp_auth_sync

smtpd

Boot startup:

rcctl enable smtpd
rcctl set smtpd flags -v

Virtual Domains

This file holds all virtual domains:

cat /etc/mail/vdomains
example.com
example.org

Virtual Users

Create the virtual system user:

useradd -d /var/empty -s /sbin/nologin vmail

This file holds the virtual user mappings:

# cat /etc/mail/vusers
john@example.com vmail
@example.com john@example.com
doe@example.org vmail
hostmaster@example.org doe@example.org

Create secrets file:

touch /etc/mail/secrets
chmod 600 /etc/mail/secrets

smtpd.conf

Replace example.com with your domain in /etc/mail/smtpd.conf.

smtp max-message-size 20M
pki mx.giftfish.de cert "/etc/ssl/mail.example.com.fullchain.pem"
pki mx.giftfish.de key "/etc/ssl/private/mail.example.com.key"
smtp ciphers AEAD-AES256-GCM-SHA384:AEAD-CHACHA20-POLY1305-SHA256:AEAD-AES128-GCM-SHA256:ECDHE+AESGCM+AES256:ECDHE+CHACHA20:ECDHE+AESGCM+AES128

filter check_dyndns phase connect match rdns regex { '.*\.dyn\..*', '.*\.dsl\..*' } \
    disconnect "550 no residential connections"

filter check_rdns phase connect match !rdns \
    disconnect "550 no rDNS entry"

filter check_fcrdns phase connect match !fcrdns \
    disconnect "550 no FCrDNS entry"

#filter senderscore \
#    proc-exec "filter-senderscore -blockBelow 10 -junkBelow 70 -slowFactor 5000"

filter rspamd proc-exec "filter-rspamd"
filter header proc-exec "filter-header"

table aliases "/etc/mail/aliases"
table vdomains "/etc/mail/vdomains"
table vusers "/etc/mail/vusers"
table secrets "/etc/mail/secrets"

listen on lo0 mask-src filter { header, rspamd }
listen on egress tls pki mail.example.com auth-optional \
    filter { check_dyndns, check_rdns, check_fcrdns, rspamd }

listen on egress smtps pki mail.example.com auth-optional \
    filter { check_dyndns, check_rdns, check_fcrdns, rspamd }

listen on egress port submission tls pki mail.example.com \
    auth <secrets> mask-src filter { header, rspamd }

action "local" maildir junk alias <aliases>
action "outbound" relay helo mx.giftfish.de
action "kopano" mda "/usr/local/sbin/kopano-dagent -s %{dest:lowercase|strip}" virtual <vusers> user "_kopano"

match for local action "local"
match from any for domain <vdomains> action "kopano"
match from any auth action "outbound"
match from local for any action "outbound"
match !for domain <vdomains> reject

Header filter

Headers filtered:

Based on: https://github.com/jirutka/opensmtpd-filter-rewrite-from

cat <<'EOF'> /usr/local/libexec/smtpd/filter-header
#!/usr/bin/awk -f
function die(msg) {
	printf("mail-from: %s\n", msg) > "/dev/stderr"
	exit 1
}
function output(type, v, t, s, msg) {
	if (v < "0.5") {
		print(type, t, s, msg)
	} else {
		print(type, s, t, msg)
	}
	fflush()
}
BEGIN {
	ARGC = 0
	FS = "|"
	OFS = FS
	_ = FS
	in_body[""] = 0
}
"config|ready" == $0 {
	print("register|filter|smtp-in|data-line")
	print("register|ready")
	fflush()
	next
}
"config" == $1 {
	next
}
"filter" == $1 {
	if (NF < 7) {
		die("invalid filter command: expected >6 fields!")
	}
	ver = $2
	sid = $6
	token = $7
	line = substr($0, length($1$2$3$4$5$6$7) + 8)
}
"filter|smtp-in|data-line" == $1_$4_$5 {
	if (line == "") {
		in_body[sid] = 1
	}
	if (!in_body[sid] && match(line, /^User-Agent:[\t ]*|^X-Mailer:[\t ]*/)) {
		next
	}
	if (line == ".") {
		delete in_body[sid]
	}
	output("filter-dataline", ver, token, sid, line)
}
EOF
chmod 755 /usr/local/libexec/smtpd/filter-header

Restart smtpd: rcctl restart smtpd

spamd

Create custom spam files:

touch /etc/mail/custom.black

Config:

cat <<EOF> /etc/mail/spamd.conf
all:\
        :nixspam:custom_black:

nixspam:\\
        :black:\\
        :msg="Your address %A is in the nixspam list\n\\
        See http://www.heise.de/ix/nixspam/dnsbl_en/ for details":\\
        :method=http:\\
        :file=www.openbsd.org/spamd/nixspam.gz

custom_black:\\
         :black:\\
         :msg="Your address %A has been manually blacklisted":\\
         :method=file:\\
         :file=/etc/mail/custom.black:
EOF

Boot startup:

Replace example.com with your domain.

rcctl enable spamd
rcctl set spamd flags "-v -G 4:4:864 -K /etc/ssl/private/example.com.key -C /etc/ssl/example.com.fullchain.pem"
rcctl enable spamlogd

Create spamd-setup cronjob: echo "*/5 * * * * /usr/libexec/spamd-setup -b >/dev/null 2>&1" >> /var/cron/tabs/root

pf Rules

cat <<EOF>> /etc/pf.conf
set limit table-entries 400000
set skip on lo

block all

smtp_ports="{ smtp, smtps }"
in_tcp = "{ ssh, http, https, imaps, 8443 }"
pass in on egress proto tcp to any port $in_tcp
bgp_addrs = "{ 64.142.121.62, 217.31.80.170, 2a00:15a8:0:100:0:d91f:50aa:1 }"
pass in on egress proto tcp from $bgp_addrs to any port bgp
pass in on egress proto icmp

table <spamd> persist
table <bgp_spamd> persist
table <bruteforce> persist

match in all scrub (no-df random-id max-mss 1440)
block in quick log from urpf-failed label uRPF
block quick log from <bruteforce>

block quick proto {tcp,udp} user www

pass in quick log on egress proto tcp from any to any port 587
pass in quick log on egress proto tcp \
  from { <bgp_spamd>, <spamd> } \
  to egress port $smtp_ports \
  rdr-to 127.0.0.1 port spamd

pass in quick log on egress proto tcp \
  from any to any port $smtp_ports \
  flags S/SA modulate state

out_tcp = "{ ssh, domain, smtp, smtps, http, https }"
pass out log on egress proto tcp to any port $out_tcp
pass out log on egress proto tcp to $bgp_addrs port bgp
out_udp = "{ domain, ntp }"
pass out log on egress proto udp to any port $out_udp
pass out log on egress proto { icmp, icmp6 } to any
EOF
pfctl -f /etc/pf.conf

Start spamd and spamlogd:

rcctl start spamd
rcctl start spamlogd

MariaDB

Basic config at OpenBSD MariaDB

Create database:

mysql -u root -p

CREATE DATABASE kopano CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
GRANT ALL ON kopano.* TO 'kopano'@'localhost' IDENTIFIED BY '<password>';
FLUSH PRIVILEGES;

Kopano

Sets required:

Install: pkg_add kopano-core kopano-mapi py3-bsddb3 py3-magic

Boot startup: rcctl enable kopano

Make sure mysqld starts before kopano: rcctl order mysqld

Config

Change services to start:

Note: The services kopano_search and kopano_spamd are broken and do not work.

perl -p -i -e 's/^(_pkg_scripts=.*)/#$1\n_pkg_scripts="kopano_server kopano_spooler kopano_monitor kopano_gateway kopano_ical"/' /etc/rc.d/kopano

Allow socket in chroot:

mkdir /var/www/run/kopano
chown _kopano: /var/www/run/kopano
/etc/kopano/dagent.cfg

spam_header_name = X-Spam
/etc/kopano/server.cfg

# Replace
server_listen = *:236
# with
server_listen =

# Add:
server_pipe_name = /var/run/kopano/server.sock /var/www/run/kopano/server.sock

# Set DB Credentials:

mysql_user = kopano
mysql_password = <password>
mysql_socket = /var/run/mysql/mysql.sock

disabled_features = pop3
log_file = /var/log/kopano/server.log
/etc/kopano/gateway.cfg

pop3_listen =
pop3s_listen =
imap_listen =
imaps_listen = *:993
server_socket = file:///var/run/kopano/server.sock
disable_plaintext_auth = yes
ssl_private_key_file = /etc/ssl/private/example.com.key
ssl_certificate_file = /etc/ssl/example.com.fullchain.pem
tls_min_proto = tls1.2
ssl_ciphers = AEAD-AES256-GCM-SHA384:AEAD-CHACHA20-POLY1305-SHA256:AEAD-AES128-GCM-SHA256:ECDHE+AESGCM+AES256:ECDHE+CHACHA20:ECDHE+AESGCM+AES128
ssl_prefer_server_ciphers = yes
log_file = /var/log/kopano/gateway.log
/etc/kopano/ical.cfg

ical_listen =
icals_listen = *:8443
server_socket = file:///var/run/kopano/server.sock
ssl_private_key_file = /etc/ssl/private/example.com.key
ssl_certificate_file = /etc/ssl/example.com.fullchain.pem
tls_min_proto = tls1.2
ssl_ciphers = AEAD-AES256-GCM-SHA384:AEAD-CHACHA20-POLY1305-SHA256:AEAD-AES128-GCM-SHA256:ECDHE+AESGCM+AES256:ECDHE+CHACHA20:ECDHE+AESGCM+AES128
ssl_prefer_server_ciphers = yes
log_file = /var/log/kopano/ical.log
/etc/kopano/spooler.cfg

log_file = /var/log/kopano/spooler.log
/etc/kopano/dagent.cfg

log_file = /var/log/kopano/dagent.log

Start kopano: rcctl start kopano

Create a user: kopano-admin -c "john@example.com" -f "John Doe" -e "john@example.com" -P

Credentials:

DB Upgrade

kopano-dbadm k-1216
kopano-dbadm usmp

Kopano Webapp

Install: pkg_add kopano-webapp

Boot startup: rcctl enable php74_fpm

Config

echo "extension=mapi.so" > /etc/php-7.4/kopano.ini
sed -i -e 's/^pm.max_children.*/pm.max_children = 20/' \
  -e 's/^pm.start_servers.*/pm.start_servers = 4/' \
  -e 's/^pm.min_spare_servers.*/pm.min_spare_servers = 2/' \
  -e 's/^pm.max_spare_servers.*/pm.max_spare_servers = 6/' /etc/php-fpm.conf
sed -i 's/^expose_php.*/expose_php = off/' /etc/php-7.4.ini

Change Server Socket:

sed -i 's|^.define("DEFAULT_SERVER".*|define("DEFAULT_SERVER", "file:///run/kopano/server.sock");|' /var/www/kopano-webapp/config.php

Remove unused plugins:

rm -rf /var/www/kopano-webapp/plugins/{filesbackendSMB,filesbackendOwncloud}

Start php-fpm: rcctl start php74_fpm

Z-Push

Install: pkg_add z-push

Config

Change Server Socket:

sed -i "s|^define('MAPI_SERVER'.*|define('MAPI_SERVER', 'file:///run/kopano/server.sock');|" /var/www/z-push/backend/kopano/config.php

Optionals

Kopano Passwd Plugin

The password plugin cannot be used anymore.

The required functions have been removes from kopano-core in: https://stash.kopano.io/projects/KC/repos/kopanocore/commits/9a003acd34857cf2c27d0d9c581af0e233b48510#php7-ext/main.cpp

OpenBGPD

Project: bgp-spamd

Boot startup: rcctl enable bgpd

Config

OpenBGPD config:

cat <<'EOF'> /etc/bgpd.conf
spamdAS="65066"

AS 65001
fib-update no                # Mandatory, to not update the local routing table
nexthop qualify via default  # Make sure the nexthop is valid

group "spamd-bgp" {
        remote-as $spamdAS
        multihop 64
        export none
        # us.bgp-spamd.net
        neighbor 64.142.121.62
        # eu.bgp-spamd.net
        neighbor 217.31.80.170
        # IPv6 eu.bgp-spamd.net
        #neighbor 2a00:15a8:0:100:0:d91f:50aa:1
}

deny to any
deny from any
allow from group "spamd-bgp"

match from group "spamd-bgp" community $spamdAS:666 set pftable "bgp_spamd"
EOF
chmod 600 /etc/bgpd.conf

Start OpenBGPD: rcctl start bgpd

Add these options to your /etc/pf.conf:

set limit table-entries 400000

table <bgp_spamd_bypass> persist
table <bgp_spamd> persist

bgp_addrs = "{ 64.142.121.62, 217.31.80.170, 2a00:15a8:0:100:0:d91f:50aa:1 }"
pass in on egress proto tcp from $bgp_addrs to any port bgp

Change your pf smtp rules to match these:

pass in quick log on egress proto tcp \
  from { <bgp_spamd_bypass>, <nospamd>, <spamd-white> } \
  to any port $smtp_ports \
  flags S/SA modulate state

pass in quick proto tcp from { <bgp_spamd>, <spamd> } \
  to egress port $smtp_ports \
  rdr-to 127.0.0.1 port spamd

Reload pf: pfctl -f /etc/pf.conf

Edit the /etc/mail/spamd.conf:

# Add bgp-spamd to the all list:
all:\
        :nixspam:custom_black:bgp-spamd:

# Add the bgp-spamd list:
bgp-spamd:\
         :black:\
         :msg="Your address %A has sent mail to a spamtrap\n\
          within the last 24 hours":\
         :method=file:\
         :file=/etc/mail/spamd.black:

Create the spamd-reload script:

cat <<'EOF'> /usr/local/sbin/spamd-update
#!/bin/sh

black="/etc/mail/spamd.black"
custom="/etc/mail/custom.black"

bgpctl show rib community 65066:666|tail -n+5|
 awk '{sub(/\/.*/, "", $1);print $1}'|uniq >${black}

for ip in $(cat ${custom}); do
  /usr/sbin/spamdb -d ${ip} 2> /dev/null
done

/usr/libexec/spamd-setup -b
EOF
chmod 0744 /usr/local/sbin/spamd-update

Replace the spamd-setup cronjob from above with the new script and run it once manually: /usr/local/sbin/spamd-update

Backup

#!/bin/sh

db="kopano"
db_user="kopano"
db_pw="<password>"

set -A paths \
/etc/kopano \
/etc/mail \
/etc/rspamd \
/etc/pf.conf \
/etc/httpd.conf \
/etc/acme \
/etc/ssl \
/etc/acme-client.conf \
/etc/bgpd.conf \
/etc/rc.conf.local \
/etc/my.cnf \
/etc/rc.d/kopano \
/etc/php-7.3/kopano.ini \
/etc/php-7.3.ini \
/var/rspamd \
/var/lib/rspamd \
/var/db/spamd \
/var/db/kopano \
/var/www/htdocs/robots.txt \
/var/www/htdocs/.well-known \
/var/www/kopano-webapp/config.php \
/var/www/kopano-webapp/server/includes/core/class.webappauthentication.php \
/var/www/z-push/backend/kopano/config.php \
/usr/local/sbin/spamd-update \
/usr/local/sbin/cron-acme-update \
/var/cron/tabs/root \
/var/redis/dump.rdb \
/tmp/*.pf \
/tmp/${db}.sql

for t in $(pfctl -s Tables); do
  pfctl -t ${t} -T show > /tmp/${t}.pf
done

mysqldump -u ${db_user} -p${db_pw} ${db} >/tmp/${db}.sql
redis-cli <<EOF
SAVE
EOF

tar cfvz /root/mailserver_backup_$(date +%Y%m%d_%H%M%S).tgz ${paths[@]}

rm /tmp/${db}.sql /tmp/*.pf

Restore backup:

pkg_add rspamd opensmtpd-filter-rspamd kopano-core kopano-mapi py3-bsddb3 py3-magic kopano-webapp z-push
tar xfpvz mailserver_backup<date>.tgz -C /
mkdir /var/www/run/kopano
chown _kopano: /var/www/run/kopano
useradd -d /var/empty -s /sbin/nologin vmail

Restore pf tables:

for t in $(ls /tmp/*.pf); do pfctl -t ${t%.pf} -T add -f ${t}; done

Restore database:

mysql -u root -p kopano < /tmp/kopano.sql

Bug fixes

Watchdog

cat <<'EOF'>/usr/local/sbin/watchdog
#!/bin/sh

for p in $(rcctl ls failed); do
  rcctl start ${p}
done

chmod 755 /usr/local/sbin/watchdog
echo "*/5 * * * * /usr/local/sbin/watchdog >/dev/null 2>&1" >> /var/cron/tabs/root

Manual SPAM and HAM reporting

NOTE: replace example.com with your domain.

For the manual spam and ham reporting to work properly, you have to forward the desired mails as attachment to spam@example.com (for spam training - bad mails) and ham@example.com (for ham training - good mails).

Attachment parser script:

cat <<"EOF"> /usr/local/bin/attachment_report.pl
#!/usr/bin/perl
use strict;
use warnings;
use Config;
use Email::MIME::Attachment::Stripper;

my $rspamc = "/usr/local/bin/rspamc";
my $learn;
if ($ARGV[0] eq "spam") {
  $learn = "${rspamc} learn_spam";
} elsif ($ARGV[0] eq "ham") {
  $learn = "${rspamc} learn_ham";
} else {
  die "error: argument must be 'spam' or 'ham'\n";
}

if ($Config{osname} eq "openbsd") {
  eval "use OpenBSD::Pledge";
  if ($Config{osvers} >= "6.6") {
    eval "use OpenBSD::Unveil";
    unveil($rspamc, "rx") or die "Unable to unveil: $!";
  }
  pledge("proc exec prot_exec") or die "Unable to pledge: $!";
}

local $/;
my $mail = <STDIN>;
my $s = Email::MIME::Attachment::Stripper->new($mail);
my @a = $s->attachments;

for my $i (1 .. $#a) {
  open(IN, "| $learn") or die "$learn failed: $!\n";
  print IN $a[$i]{payload};
  close(IN);
}
EOF
chmod 755 /usr/local/bin/attachment_report.pl

Create spam virtual users:

cat <<EOF> /etc/mail/spam
spam@example.com vmail
ham@example.com vmail
EOF

Add the following lines to your smtpd.conf:

table spam "/etc/mail/spam"
action "ham" mda "/usr/local/bin/attachment_report.pl ham" virtual <spam> user "_rspamd"
action "spam" mda "/usr/local/bin/attachment_report.pl spam" virtual <spam> user "_rspamd"
match from local for domain <vdomains> rcpt-to "ham@example.com" action "ham"
match from local for domain <vdomains> rcpt-to "spam@example.com" action "spam"

END