first import of mailserver config

This commit is contained in:
Daniel Frank 2019-05-10 23:21:28 +02:00
parent d4cdbc8ccd
commit 292215f770
Signed by: tokudan
GPG key ID: 063CCCAD04182D32
19 changed files with 811 additions and 0 deletions

20
README.md Normal file
View file

@ -0,0 +1,20 @@
hamburg.freifunk.net Mailserver
===============================
Initiales Setup
-----
1. System starten
2. URL anpassen und aufrufen: http://127.0.0.1/setup.php?lostpw=1
3. Neues Setup-Passwort vergeben und den Hash generieren.
4. Hash in der Datei variables.nix ersetzen und das System neu bauen und starten.
5. URL anpassen und aufrufen: http://127.0.0.1/setup.php
6. Admin-Account über die Website anlegen
7. URL anpassen und aufrufen: http://127.0.0.1/
8. Mail konfigurieren.
Development
-----
Starten des Systems:
QEMU_NET_OPTS="hostfwd=tcp:127.0.0.1:2222-:22,hostfwd=tcp:127.0.0.1:8080-:80,hostfwd=tcp:127.0.0.1:2525-:25" nixos-shell
Zugriff dann per SSH über 127.0.0.1:2222 und HTTP über 127.0.0.1:8080.

View file

@ -8,8 +8,51 @@
imports = imports =
[ # Include the results of the hardware scan. [ # Include the results of the hardware scan.
./hardware-configuration.nix ./hardware-configuration.nix
./variables.nix
./mailserver.nix
]; ];
# Use the staging environment of Let's encrypt and accept their certificates as well...
security.acme.production = false;
security.pki.certificates = [
''
-----BEGIN CERTIFICATE-----
MIIFATCCAumgAwIBAgIRAKc9ZKBASymy5TLOEp57N98wDQYJKoZIhvcNAQELBQAw
GjEYMBYGA1UEAwwPRmFrZSBMRSBSb290IFgxMB4XDTE2MDMyMzIyNTM0NloXDTM2
MDMyMzIyNTM0NlowGjEYMBYGA1UEAwwPRmFrZSBMRSBSb290IFgxMIICIjANBgkq
hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA+pYHvQw5iU3v2b3iNuYNKYgsWD6KU7aJ
diddtZQxSWYzUI3U0I1UsRPTxnhTifs/M9NW4ZlV13ZfB7APwC8oqKOIiwo7IwlP
xg0VKgyz+kT8RJfYr66PPIYP0fpTeu42LpMJ+CKo9sbpgVNDZN2z/qiXrRNX/VtG
TkPV7a44fZ5bHHVruAxvDnylpQxJobtCBWlJSsbIRGFHMc2z88eUz9NmIOWUKGGj
EmP76x8OfRHpIpuxRSCjn0+i9+hR2siIOpcMOGd+40uVJxbRRP5ZXnUFa2fF5FWd
O0u0RPI8HON0ovhrwPJY+4eWKkQzyC611oLPYGQ4EbifRsTsCxUZqyUuStGyp8oa
aoSKfF6X0+KzGgwwnrjRTUpIl19A92KR0Noo6h622OX+4sZiO/JQdkuX5w/HupK0
A0M0WSMCvU6GOhjGotmh2VTEJwHHY4+TUk0iQYRtv1crONklyZoAQPD76hCrC8Cr
IbgsZLfTMC8TWUoMbyUDgvgYkHKMoPm0VGVVuwpRKJxv7+2wXO+pivrrUl2Q9fPe
Kk055nJLMV9yPUdig8othUKrRfSxli946AEV1eEOhxddfEwBE3Lt2xn0hhiIedbb
Ftf/5kEWFZkXyUmMJK8Ra76Kus2ABueUVEcZ48hrRr1Hf1N9n59VbTUaXgeiZA50
qXf2bymE6F8CAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMB
Af8wHQYDVR0OBBYEFMEmdKSKRKDm+iAo2FwjmkWIGHngMA0GCSqGSIb3DQEBCwUA
A4ICAQBCPw74M9X/Xx04K1VAES3ypgQYH5bf9FXVDrwhRFSVckria/7dMzoF5wln
uq9NGsjkkkDg17AohcQdr8alH4LvPdxpKr3BjpvEcmbqF8xH+MbbeUEnmbSfLI8H
sefuhXF9AF/9iYvpVNC8FmJ0OhiVv13VgMQw0CRKkbtjZBf8xaEhq/YqxWVsgOjm
dm5CAQ2X0aX7502x8wYRgMnZhA5goC1zVWBVAi8yhhmlhhoDUfg17cXkmaJC5pDd
oenZ9NVhW8eDb03MFCrWNvIh89DDeCGWuWfDltDq0n3owyL0IeSn7RfpSclpxVmV
/53jkYjwIgxIG7Gsv0LKMbsf6QdBcTjhvfZyMIpBRkTe3zuHd2feKzY9lEkbRvRQ
zbh4Ps5YBnG6CKJPTbe2hfi3nhnw/MyEmF3zb0hzvLWNrR9XW3ibb2oL3424XOwc
VjrTSCLzO9Rv6s5wi03qoWvKAQQAElqTYRHhynJ3w6wuvKYF5zcZF3MDnrVGLbh1
Q9ePRFBCiXOQ6wPLoUhrrbZ8LpFUFYDXHMtYM7P9sc9IAWoONXREJaO08zgFtMp4
8iyIYUyQAbsvx8oD2M8kRvrIRSrRJSl6L957b4AFiLIQ/GgV2curs0jje7Edx34c
idWw1VrejtwclobqNMVtG3EiPUIpJGpbMcJgbiLSmKkrvQtGng==
-----END CERTIFICATE-----
''
];
# Configuration options for the mailserver
variables = {
mailAdmin = "postmaster@mail2.hamburg.freifunk.net";
useSSL = true;
};
# Use the GRUB 2 boot loader. # Use the GRUB 2 boot loader.
boot.loader.grub.enable = true; boot.loader.grub.enable = true;
boot.loader.grub.version = 2; boot.loader.grub.version = 2;

136
dovecot.nix Normal file
View file

@ -0,0 +1,136 @@
{ config, lib, pkgs, ... }:
let
dovecotSQL = pkgs.writeText "dovecot-sql.conf" ''
driver = sqlite
connect = ${config.variables.pfadminDataDir}/postfixadmin.db
password_query = SELECT username AS user, password FROM mailbox WHERE username = '%Lu' AND active='1'
user_query = SELECT username AS user FROM mailbox WHERE username = '%Lu' AND active='1'
'';
dovecotConf = pkgs.writeText "dovecot.conf" ''
default_internal_user = dovecot2
default_internal_group = dovecot2
protocols = imap lmtp pop3 sieve
${lib.optionalString (config.variables.useSSL) ''
ssl = yes
ssl_cert = </var/lib/acme/dovecot2.${config.variables.myFQDN}/fullchain.pem
ssl_key = </var/lib/acme/dovecot2.${config.variables.myFQDN}/key.pem
''
}
disable_plaintext_auth = no
auth_mechanisms = plain login
userdb {
driver = sql
args = ${dovecotSQL}
}
passdb {
driver = sql
args = ${dovecotSQL}
}
mail_home = ${config.variables.vmailBaseDir}/%Lu/
mail_location = maildir:${config.variables.vmailBaseDir}/%Lu/Maildir
mail_uid = ${toString config.variables.vmailUID}
mail_gid = ${toString config.variables.vmailGID}
service auth {
unix_listener ${config.variables.dovecotAuthSocket} {
user = ${config.services.postfix.user}
group = ${config.services.postfix.group}
mode = 0600
}
}
service lmtp {
unix_listener ${config.variables.dovecotLmtpSocket} {
user = ${config.services.postfix.user}
group = ${config.services.postfix.group}
mode = 0600
}
}
protocol lmtp {
mail_plugins = sieve
}
protocol imap {
mail_plugins = $mail_plugins imap_sieve
}
namespace inbox {
inbox = yes
location =
mailbox Drafts {
special_use = \Drafts
auto = subscribe
}
mailbox Junk {
special_use = \Junk
auto = subscribe
}
mailbox Sent {
special_use = \Sent
auto = subscribe
}
mailbox Trash {
special_use = \Trash
auto = subscribe
}
mailbox Archive {
special_use = \Archive
auto = subscribe
}
prefix =
}
plugin {
sieve_after = ${(pkgs.callPackage ./sieve-after.nix {}) }
sieve_plugins = sieve_imapsieve sieve_extprograms
# From elsewhere to Spam folder
imapsieve_mailbox1_name = Junk
imapsieve_mailbox1_causes = COPY
imapsieve_mailbox1_before = file:${(pkgs.callPackage ./sieve-report-spam-ham.nix {})}/report-spam.sieve
# From Spam folder to elsewhere
imapsieve_mailbox2_name = *
imapsieve_mailbox2_from = Junk
imapsieve_mailbox2_causes = COPY
imapsieve_mailbox2_before = file:${(pkgs.callPackage ./sieve-report-spam-ham.nix {})}/report-ham.sieve
sieve_pipe_bin_dir = ${(pkgs.callPackage ./sieve-pipe-bin-dir.nix {})}
sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.environment
}
'';
in
{
# Configure certificates...
security = lib.mkIf config.variables.useSSL {
acme.certs."dovecot2.${config.variables.myFQDN}" = {
domain = "${config.variables.myFQDN}";
user = config.services.nginx.user;
group = config.services.dovecot2.group;
allowKeysForGroup = true;
postRun = "systemctl restart dovecot2.service";
# cheat by getting the webroot from another certificate configured through nginx.
webroot = config.security.acme.certs."${config.variables.myFQDN}".webroot;
};
};
# Make sure at least the self-signed certs are available before trying to start postfix
systemd.services.dovecot2.after = lib.mkIf config.variables.useSSL [ "acme-selfsigned-certificates.target" ];
# Setup dovecot
networking.firewall.allowedTCPPorts = [ 110 143 993 995 4190 ];
services.dovecot2 = {
enable = true;
configFile = "${dovecotConf}";
modules = [ pkgs.dovecot_pigeonhole ];
};
systemd.services."vmail-setup" = {
serviceConfig.Type = "oneshot";
wantedBy = [ "multi-user.target" ];
script = ''
mkdir -p ${config.variables.vmailBaseDir}
chown -c ${config.variables.vmailUser}:${config.variables.vmailGroup} ${config.variables.vmailBaseDir}
chmod -c 0700 ${config.variables.vmailBaseDir}
'';
};
}

18
mailserver.nix Normal file
View file

@ -0,0 +1,18 @@
{ config, pkgs, ... }:
{
# Import some configuration as they are too long to be easily readable here
imports = [
./dovecot.nix
./postfix.nix
./postfixadmin.nix
./roundcube.nix
./rspamd.nix
];
users.groups."${config.variables.vmailGroup}" = { gid = config.variables.vmailGID; };
users.users."${config.variables.vmailUser}" = {
uid = config.variables.vmailUID;
group = config.variables.vmailGroup;
hashedPassword = "!";
};
}

35
pkg-postfixadmin.nix Normal file
View file

@ -0,0 +1,35 @@
{ stdenv, lib, fetchFromGitHub, config ? null, cacheDir ? null }:
stdenv.mkDerivation rec {
name = "postfixadmin-${version}";
version = "3.2.2";
rev = "${name}";
src = fetchFromGitHub {
inherit rev;
owner = "postfixadmin";
repo = "postfixadmin";
sha256 = "0bkjdmn63yinf217fnn3wq13pc0yklmnsbrgxjv22vpync42f9vh";
};
phases = [ "unpackPhase" "installPhase" ];
installPhase = ''
cp -Rp ./ $out/
${lib.optionalString (config != null) ''
ln -s ${config} $out/config.local.php
''}
${lib.optionalString (cacheDir != null) ''
ln -s ${cacheDir}/templates_c $out/templates_c
''}
'';
meta = with stdenv.lib; {
description = "Postfix Admin";
homepage = http://postfixadmin.sourceforge.net/;
license = licenses.gpl2;
maintainers = with maintainers; [ tokudan ];
platforms = platforms.all;
};
}

32
pkg-roundcube.nix Normal file
View file

@ -0,0 +1,32 @@
{ stdenv, lib, fetchurl, acl, librsync, ncurses, openssl, zlib, conf ? null, temp ? null, logs ? null }:
stdenv.mkDerivation rec {
name = "roundcube-${version}";
version = "1.3.9";
url = "https://github.com/roundcube/roundcubemail/releases/download/${version}/roundcubemail-${version}-complete.tar.gz";
src = fetchurl {
inherit url;
curlOpts = "--location";
sha256 = "1b91amcpzb7935hpm67iqw92bl5r1a0rkfrc8gfm8w9sngzv8vbj";
};
phases = [ "unpackPhase" "installPhase" ];
installPhase = ''
cp -Rp ./ $out/
cd "$out"
${lib.optionalString (conf != null) "ln -s ${conf} $out/config/config.inc.php"}
${lib.optionalString (temp != null) "mv temp temp.dist; ln -s ${temp} $out/temp"}
${lib.optionalString (logs != null) "mv logs logs.dist; ln -s ${logs} $out/logs"}
'';
meta = with stdenv.lib; {
description = "Roundcube";
homepage = https://roundcube.net/;
license = licenses.agpl3;
maintainers = with maintainers; [ tokudan ];
platforms = platforms.all;
};
}

80
postfix.nix Normal file
View file

@ -0,0 +1,80 @@
{ config, lib, pkgs, ... }:
let
pfvirtual_mailbox_domains = pkgs.writeText "virtual_mailbox_domains.cf" ''
dbpath = ${config.variables.pfadminDataDir}/postfixadmin.db
query = SELECT domain FROM domain WHERE domain='%s' AND active = '1'
'';
pfvirtual_alias_maps = pkgs.writeText "virtual_alias_maps.cf" ''
dbpath = ${config.variables.pfadminDataDir}/postfixadmin.db
query = SELECT goto FROM alias WHERE address='%s' AND active = '1'
'';
pfvirtual_alias_domain_maps = pkgs.writeText "virtual_alias_domain_maps.cf" ''
dbpath = ${config.variables.pfadminDataDir}/postfixadmin.db
query = SELECT goto FROM alias,alias_domain WHERE alias_domain.alias_domain = '%d' and alias.address = ('%u' || '@' || alias_domain.target_domain) AND alias.active = 1 AND alias_domain.active='1'
'';
pfvirtual_alias_domain_catchall_maps = pkgs.writeText "virtual_alias_domain_catchall_maps.cf" ''
dbpath = ${config.variables.pfadminDataDir}/postfixadmin.db
query = SELECT goto FROM alias,alias_domain WHERE alias_domain.alias_domain = '%d' and alias.address = ('@' || alias_domain.target_domain) AND alias.active = 1 AND alias_domain.active='1'
'';
pfvirtual_mailbox_maps = pkgs.writeText "virtual_mailbox_maps.cf" ''
dbpath = ${config.variables.pfadminDataDir}/postfixadmin.db
query = SELECT maildir FROM mailbox WHERE username='%s' AND active = '1'
'';
pfvirtual_alias_domain_mailbox_maps = pkgs.writeText "virtual_alias_domain_mailbox_maps.cf" ''
dbpath = ${config.variables.pfadminDataDir}/postfixadmin.db
query = SELECT maildir FROM mailbox,alias_domain WHERE alias_domain.alias_domain = '%d' and mailbox.username = ('%u' || '@' || alias_domain.target_domain) AND mailbox.active = 1 AND alias_domain.active='1'
'';
in
{
# Configure Postfix to support SQLite
nixpkgs.config.packageOverrides = pkgs: { postfix = pkgs.postfix.override { withSQLite = true; }; };
# SSL/TLS specific configuration
security = lib.mkIf config.variables.useSSL {
# Configure the certificates...
acme.certs."postfix.${config.variables.myFQDN}" = {
domain = "${config.variables.myFQDN}";
group = config.services.postfix.group;
allowKeysForGroup = true;
postRun = "systemctl restart postfix.service";
# cheat by getting some settings from another certificate configured through nginx.
user = config.security.acme.certs."${config.variables.myFQDN}".user;
webroot = config.security.acme.certs."${config.variables.myFQDN}".webroot;
};
};
systemd = lib.mkIf config.variables.useSSL {
# Make sure at least the self-signed certs are available before trying to start postfix
services.postfix.after = [ "acme-selfsigned-certificates.target" ];
};
# Setup Postfix
networking.firewall.allowedTCPPorts = [ 25 587 ];
services.postfix = {
enable = true;
enableSmtp = true;
enableSubmission = true;
config = {
myhostname = config.variables.myFQDN;
mynetworks_style = "host";
recipient_delimiter = "+";
relay_domains = "";
smtpd_milters = "unix:${config.variables.rspamdMilterSocket}";
non_smtpd_milters = "unix:${config.variables.rspamdMilterSocket}";
smtpd_sasl_path = config.variables.dovecotAuthSocket;
smtpd_sasl_type = "dovecot";
smtpd_tls_auth_only = "yes";
smtpd_tls_chain_files = lib.mkIf config.variables.useSSL "/var/lib/acme/postfix.${config.variables.myFQDN}/full.pem";
smtpd_tls_loglevel = "1";
smtpd_tls_received_header = "yes";
smtpd_tls_security_level = "may";
smtp_tls_loglevel = "1";
smtp_tls_security_level = "may";
virtual_alias_maps = "proxy:sqlite:${pfvirtual_alias_maps}, proxy:sqlite:${pfvirtual_alias_domain_maps}, proxy:sqlite:${pfvirtual_alias_domain_catchall_maps}";
virtual_mailbox_domains = "proxy:sqlite:${pfvirtual_mailbox_domains}";
virtual_mailbox_maps = "proxy:sqlite:${pfvirtual_mailbox_maps}, proxy:sqlite:${pfvirtual_alias_domain_mailbox_maps}";
virtual_transport = "lmtp:unix:${config.variables.dovecotLmtpSocket}";
};
rootAlias = config.variables.mailAdmin;
postmasterAlias = config.variables.mailAdmin;
};
}

102
postfixadmin.nix Normal file
View file

@ -0,0 +1,102 @@
{ config, lib, pkgs, ... }:
let
phppoolName = "postfixadmin_pool";
pfaGroup = config.variables.pfaGroup;
pfaUser = config.variables.pfaUser;
postfixadminpkg = config.variables.postfixadminpkg;
pfadminDataDir = config.variables.pfadminDataDir;
cacheDir = config.variables.postfixadminpkgCacheDir;
phpfpmHostPort = config.variables.pfaPhpfpmHostPort;
in
{
# Setup the user and group
users.groups."${pfaGroup}" = { };
users.users."${pfaUser}" = {
isSystemUser = true;
group = "${pfaGroup}";
description = "PHP User for postfixadmin";
};
# Setup nginx
networking.firewall.allowedTCPPorts = [ 80 443 ];
services.nginx.enable = true;
services.nginx.virtualHosts."${config.variables.pfaDomain}" = {
forceSSL = config.variables.useSSL;
enableACME = config.variables.useSSL;
default = true;
root = "${postfixadminpkg}/public";
extraConfig = ''
access_log /tmp/nginx/log/$host combined;
charset utf-8;
etag off;
add_header etag "\"${builtins.substring 11 32 postfixadminpkg}\"";
index index.php;
location ~* \.php$ {
# Zero-day exploit defense.
# http://forum.nginx.org/read.php?2,88845,page=3
# Won't work properly (404 error) if the file is not stored on this
# server, which is entirely possible with php-fpm/php-fcgi.
# Comment the 'try_files' line out if you set up php-fpm/php-fcgi on
# another machine. And then cross your fingers that you won't get hacked.
try_files $uri =404;
# NOTE: You should have "cgi.fix_pathinfo = 0;" in php.ini
fastcgi_split_path_info ^(.+\.php)(/.+)$;
# With php5-cgi alone:
fastcgi_pass ${phpfpmHostPort};
fastcgi_index index.php;
fastcgi_param GATEWAY_INTERFACE CGI/1.1;
fastcgi_param SERVER_SOFTWARE nginx;
fastcgi_param QUERY_STRING $query_string;
fastcgi_param REQUEST_METHOD $request_method;
fastcgi_param CONTENT_TYPE $content_type;
fastcgi_param CONTENT_LENGTH $content_length;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
fastcgi_param REQUEST_URI $request_uri;
fastcgi_param DOCUMENT_URI $document_uri;
fastcgi_param DOCUMENT_ROOT $document_root;
fastcgi_param SERVER_PROTOCOL $server_protocol;
fastcgi_param REMOTE_ADDR $remote_addr;
fastcgi_param REMOTE_PORT $remote_port;
fastcgi_param SERVER_ADDR $server_addr;
fastcgi_param SERVER_PORT $server_port;
fastcgi_param SERVER_NAME $server_name;
fastcgi_param HTTP_PROXY "";
}
'';
};
systemd.services."postfixadmin-setup" = {
serviceConfig.Type = "oneshot";
wantedBy = [ "multi-user.target" ];
script = ''
# Setup the data directory with the database and the cache directory
mkdir -p ${pfadminDataDir}
chmod -c 751 ${pfadminDataDir}
chown -c ${pfaUser}:${pfaGroup} ${pfadminDataDir}
mkdir -p ${cacheDir}/templates_c
chown -Rc ${pfaUser}:${pfaGroup} ${cacheDir}/templates_c
chmod -Rc 751 ${cacheDir}/templates_c
'';
};
services.phpfpm.pools."${phppoolName}" = {
listen = phpfpmHostPort;
extraConfig = ''
user = ${pfaUser}
pm = dynamic
pm.max_children = 75
pm.min_spare_servers = 5
pm.max_spare_servers = 20
pm.max_requests = 10
catch_workers_output = 1
php_admin_value[upload_max_filesize] = 42M
php_admin_value[post_max_size] = 42M
php_admin_value[memory_limit] = 128M
php_admin_value[cgi.fix_pathinfo] = 1
'';
};
}

109
roundcube.nix Normal file
View file

@ -0,0 +1,109 @@
{ config, lib, pkgs, ... }:
let
poolName = "roundcube_pool";
roundcube = (pkgs.callPackage ./pkg-roundcube.nix {
conf = pkgs.writeText "roundcube-config.inc.php" ''
<?php
$config = array();
$config['db_dsnw'] = 'sqlite:///${config.variables.roundcubeDataDir}/roundcube.sqlite?mode=0600';
$config['default_host'] = 'tls://${config.variables.myFQDN}';
$config['smtp_server'] = 'tls://${config.variables.myFQDN}';
$config['smtp_port'] = 587;
$config['smtp_user'] = '%u';
$config['smtp_pass'] = '%p';
$config['product_name'] = 'Webmail';
$config['des_key'] = 'JQgS7JcnFMNcU3cHKrr880wO';
$config['plugins'] = array(
'archive',
'managesieve',
'zipdownload',
);
$config['skin'] = 'larry';
'';
temp = "${config.variables.roundcubeDataDir}/temp";
logs = "${config.variables.roundcubeDataDir}/logs";
} );
in
{
services.nginx.virtualHosts."${config.variables.roundcubeFQDN}" = {
forceSSL = config.variables.useSSL;
enableACME = config.variables.useSSL;
root = "${roundcube}/public_html";
locations."~ ^/favicon.ico/.*$" = {
extraConfig = "try_files $uri kins/larry/images/$uri;";
};
locations."/" = {
extraConfig = ''
index index.php;
try_files $uri /public/$uri /index.php$is_args$args;
etag off;
add_header etag "\"${builtins.substring 11 32 roundcube}\"";
'';
};
locations."~ [^/]\.php(/|$)" = {
extraConfig = ''
etag off;
add_header etag "\"${builtins.substring 11 32 roundcube}\"";
fastcgi_split_path_info ^(.+?\.php)(/.*)$;
if (!-f $document_root$fastcgi_script_name) {
return 404;
}
fastcgi_pass ${config.variables.roundcubePhpfpmHostPort};
fastcgi_index index.php;
fastcgi_param QUERY_STRING $query_string;
fastcgi_param REQUEST_METHOD $request_method;
fastcgi_param CONTENT_TYPE $content_type;
fastcgi_param CONTENT_LENGTH $content_length;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info;
fastcgi_param REQUEST_URI $request_uri;
fastcgi_param DOCUMENT_URI $document_uri;
fastcgi_param DOCUMENT_ROOT $document_root;
fastcgi_param SERVER_PROTOCOL $server_protocol;
fastcgi_param GATEWAY_INTERFACE CGI/1.1;
fastcgi_param SERVER_SOFTWARE nginx/$nginx_version;
fastcgi_param REMOTE_ADDR $remote_addr;
fastcgi_param REMOTE_PORT $remote_port;
fastcgi_param SERVER_ADDR $server_addr;
fastcgi_param SERVER_PORT $server_port;
fastcgi_param SERVER_NAME $server_name;
fastcgi_param HTTPS $https;
fastcgi_param HTTP_PROXY "";
'';
};
};
systemd.services.roundcube-install = {
serviceConfig.Type = "oneshot";
wantedBy = [ "multi-user.target" ];
script = ''
mkdir -p ${config.variables.roundcubeDataDir}/temp ${config.variables.roundcubeDataDir}/logs
chown -Rc ${config.variables.roundcubeUser} ${config.variables.roundcubeDataDir}
chmod -c 700 ${config.variables.roundcubeDataDir}
'';
};
services.phpfpm.pools."${poolName}" = {
listen = config.variables.roundcubePhpfpmHostPort;
extraConfig = ''
user = ${config.variables.roundcubeUser}
pm = dynamic
pm.max_children = 75
pm.min_spare_servers = 5
pm.max_spare_servers = 20
pm.max_requests = 10
catch_workers_output = 1
'';
};
users.extraUsers."${config.variables.roundcubeUser}" = { };
}

71
rspamd.nix Normal file
View file

@ -0,0 +1,71 @@
{ config, lib, pkgs, ... }:
let
rspamdExtraConfig = pkgs.writeText "rspamd-extra.conf" ''
secure_ip = [::1]
actions {
reject = null;
}
options {
filters: "chartable,dkim,dkim_signing,spf,surbl,regexp,fuzzy_check"
}
milter_headers {
extended_spam_headers = true;
}
classifier {
bayes {
autolearn = true;
}
}
dkim_signing {
path = "/var/lib/rspamd/dkim/$domain.$selector.key";
check_pubkey = true;
}
'';
in
{
#networking.firewall.allowedTCPPorts = [ 110 143 993 995 ];
environment.systemPackages = [
(pkgs.writeShellScriptBin "dkim-generate" ''
if [ $# -ne 1 ]; then
echo Usage: dkim-generate DOMAIN >&2
exit 1
fi
rspamd=${pkgs.rspamd}/bin/rspamadm
mkdir -p /var/lib/rspamd/dkim
$rspamd dkim_keygen -b 2048 -d "$1" -s dkim | ${pkgs.gawk}/bin/awk '/^-/ {KEY= ! KEY; print; next} KEY {print} !KEY {print > "/dev/stderr"}' >/var/lib/rspamd/dkim/"$1".dkim.key 2>/var/lib/rspamd/dkim/"$1".dkim.dns
ls -l /var/lib/rspamd/dkim/"$1".dkim.key /var/lib/rspamd/dkim/"$1".dkim.dns
'') ];
services.rspamd = {
enable = true;
# Just shove our own configuration up rspamd's rear end with high prio as the default configuration structure is a mess
extraConfig = ''
.include(try=true,priority=10,duplicate=merge) "${rspamdExtraConfig}"
'';
workers = {
controller = {
enable = true;
extraConfig = ''
secure_ip = [::1]
'';
bindSockets = [
"[::1]:11334"
{ mode = "0666"; owner = config.variables.vmailUser; socket = "/run/rspamd/worker-controller.socket"; }
];
};
rspamd_proxy = {
enable = true;
type = "rspamd_proxy";
count = 5; # TODO: match with postfix limits
extraConfig = ''
upstream "local" {
self_scan = yes; # Enable self-scan
}
'';
bindSockets = [
{ socket = config.variables.rspamdMilterSocket; mode = "0600"; owner = config.services.postfix.user; group = config.services.rspamd.group; }
];
};
};
};
}

20
sieve-after.nix Normal file
View file

@ -0,0 +1,20 @@
{ stdenv, dovecot_pigeonhole}:
stdenv.mkDerivation rec {
name = "sieve-after";
src = ./sieve-after;
phases = [ "copyPhase" "compilePhase" ];
copyPhase = ''
cd $src
mkdir $out
cp -Rv $src/. $out/
find $out -type d -exec chmod -c 0755 {} \;
set +x
'';
compilePhase = ''
find $out -iname '*.sieve' -print0 | xargs -t -0 -n1 ${dovecot_pigeonhole}/bin/sievec -c /dev/null
'';
}

View file

@ -0,0 +1,6 @@
require ["fileinto","mailbox"];
if header :contains "X-Spam" "Yes" {
fileinto :create "Junk";
stop;
}

14
sieve-pipe-bin-dir.nix Normal file
View file

@ -0,0 +1,14 @@
{ stdenv}:
stdenv.mkDerivation rec {
name = "sieve-pipe-bin-dir";
src = ./sieve-pipe-bin-dir;
phases = [ "copyPhase" "fixupPhase" ];
copyPhase = ''
mkdir $out
cp -Rv $src/. $out/
'';
}

View file

@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
exec /run/current-system/sw/bin/rspamc -h /run/rspamd/worker-controller.socket learn_ham

View file

@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
exec /run/current-system/sw/bin/rspamc -h /run/rspamd/worker-controller.socket learn_spam

30
sieve-report-spam-ham.nix Normal file
View file

@ -0,0 +1,30 @@
{ stdenv, dovecot_pigeonhole}:
stdenv.mkDerivation rec {
name = "sieve-report-spam-ham";
src = ./sieve-report-spam-ham;
phases = [ "copyPhase" "compilePhase" ];
copyPhase = ''
mkdir $out
cp -Rv $src/. $out/
find $out -type d -exec chmod -c 0755 {} \;
set +x
'';
# Yeah, need a specific dovecot.conf to enable the necessary plugins...
# taking the one used by the dovecot that actually executes the sieve scripts should
# work as well, but passing it through isn't worth my time.
compilePhase = ''
dc=$(pwd)/dovecot.conf
cat > $dc <<-EOF
plugin {
sieve_plugins = sieve_imapsieve sieve_extprograms
sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.environment
}
EOF
find $out -iname '*.sieve' -print0 | xargs -t -0 -n1 ${dovecot_pigeonhole}/bin/sievec -c $dc
'';
}

View file

@ -0,0 +1,15 @@
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}";
}
pipe :copy "learn-ham.sh" [ "${username}" ];

View file

@ -0,0 +1,7 @@
require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"];
if environment :matches "imap.user" "*" {
set "username" "${1}";
}
pipe :copy "learn-spam.sh" [ "${username}" ];

65
variables.nix Normal file
View file

@ -0,0 +1,65 @@
{ config, lib, pkgs, ... }:
{
options = {
variables = lib.mkOption {
type = lib.types.attrs;
default = { };
};
};
config.variables = {
dovecotGroup = "dovecot2";
dovecotUser = "dovecot2";
dovecotAuthSocket = "/run/dovecot2/dovecot-auth";
dovecotLmtpSocket = "/run/dovecot2/dovecot-lmtp";
rspamdMilterSocket = "/run/rspamd/milter";
myFQDN = "${config.networking.hostName}.${config.networking.domain}";
pfadminDataDir = "/var/lib/postfixadmin";
pfaGroup = "pfadmin";
pfaPhpfpmHostPort = "127.0.0.1:9000";
pfaUser = "pfadmin";
pfaDomain = "pfa.${config.variables.myFQDN}";
roundcubeFQDN = config.variables.myFQDN;
roundcubeDataDir = "/var/lib/roundcube";
roundcubePhpfpmHostPort = "127.0.0.1:9001";
roundcubeUser = "roundcube";
useSSL = false;
vmailBaseDir = "/var/vmail";
vmailGID = 10000;
vmailGroup = "vmail";
vmailUID = 10000;
vmailUser = "vmail";
postfixadminpkgCacheDir = "/var/cache/postfixadmin";
postfixadminpkg = (pkgs.callPackage ./pkg-postfixadmin.nix {
config = (pkgs.writeText "postfixadmin-config.local.php" ''
<?php
$CONF['configured'] = true;
$CONF['setup_password'] = '!';
$CONF['database_type'] = 'sqlite';
$CONF['database_name'] = '${config.variables.pfadminDataDir}/postfixadmin.db';
$CONF['password_expiration'] = 'NO';
$CONF['encrypt'] = 'dovecot:BLF-CRYPT';
$CONF['dovecotpw'] = "${pkgs.dovecot}/bin/doveadm pw -r 12";
$CONF['generate_password'] = 'YES';
$CONF['show_password'] = 'NO';
$CONF['quota'] = 'NO';
$CONF['fetchmail'] = 'NO';
$CONF['recipient_delimiter'] = "+";
$CONF['forgotten_user_password_reset'] = false;
$CONF['forgotten_admin_password_reset'] = false;
$CONF['aliases'] = '0';
$CONF['mailboxes'] = '0';
$CONF['default_aliases'] = array (
'abuse' => '${config.variables.mailAdmin}',
'hostmaster' => '${config.variables.mailAdmin}',
'postmaster' => '${config.variables.mailAdmin}',
'webmaster' => '${config.variables.mailAdmin}'
);
$CONF['footer_text'] = "";
$CONF['footer_link'] = "";
?>
'');
cacheDir = config.variables.postfixadminpkgCacheDir;
} );
};
}