Merge branch 'docs-update' into 'master'

docs: update how-to guides, split off integrations

See merge request simple-nixos-mailserver/nixos-mailserver!490
This commit is contained in:
Martin Weinelt
2026-03-11 00:54:08 +00:00
21 changed files with 289 additions and 147 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ SNM branch corresponding to your NixOS version.
## Features
* [x] Continous Integration Testing
* [x] Continuous Integration Testing
* [x] Multiple Domains
* Postfix
* [x] SMTP on port 25
+4 -3
View File
@@ -336,7 +336,7 @@ in
default = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt";
defaultText = literalMD "see [source](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/blob/master/default.nix)";
description = ''
Certifificate trust anchors used to verify the LDAP server certificate.
Certificate trust anchors used to verify the LDAP server certificate.
'';
};
@@ -948,10 +948,11 @@ in
];
default = "rsa";
description = ''
The key type used for generating DKIM keys. ED25519 was introduced in RFC6376 (2018).
The key type used for generating DKIM keys. Ed25519 support was
introduced in RFC6376 (2018).
:::{warning}
ED25519 DKIM keys are currently not recommended for primary use, as
Ed25519 DKIM keys are currently not recommended for primary use, as
various DKIM validators out there lack support and consider the keypair invalid.
:::
-55
View File
@@ -1,55 +0,0 @@
Add Radicale
============
Configuration by @dotlambda
Starting with Radicale 3 (first introduced in NixOS 20.09) the traditional
crypt passwords are no longer supported. Instead bcrypt passwords
have to be used. These can still be generated using `mkpasswd -m bcrypt`.
.. code:: nix
{ config, pkgs, lib, ... }:
with lib;
let
mailAccounts = config.mailserver.loginAccounts;
htpasswd = pkgs.writeText "radicale.users" (concatStrings
(flip mapAttrsToList mailAccounts (mail: user:
mail + ":" + user.hashedPassword + "\n"
))
);
in {
services.radicale = {
enable = true;
settings = {
auth = {
type = "htpasswd";
htpasswd_filename = "${htpasswd}";
htpasswd_encryption = "bcrypt";
};
};
};
services.nginx = {
enable = true;
virtualHosts = {
"cal.example.com" = {
forceSSL = true;
enableACME = true;
locations."/" = {
proxyPass = "http://localhost:5232/";
extraConfig = ''
proxy_set_header X-Script-Name /;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass_header Authorization;
'';
};
};
};
};
networking.firewall.allowedTCPPorts = [ 80 443 ];
}
-31
View File
@@ -1,31 +0,0 @@
Add Roundcube, a webmail
========================
The NixOS module for roundcube nearly works out of the box with SNM. By
default, it sets up a nginx virtual host to serve the webmail, other web
servers may require more work.
.. code:: nix
{ config, pkgs, lib, ... }:
with lib;
{
services.roundcube = {
enable = true;
# this is the url of the vhost, not necessarily the same as the fqdn of
# the mailserver
hostName = "webmail.example.com";
extraConfig = ''
$config['imap_host'] = "ssl://${config.mailserver.fqdn}";
$config['smtp_host'] = "ssl://${config.mailserver.fqdn}";
$config['smtp_user'] = "%u";
$config['smtp_pass'] = "%p";
'';
};
services.nginx.enable = true;
networking.firewall.allowedTCPPorts = [ 80 443 ];
}
+67 -12
View File
@@ -1,18 +1,73 @@
Autodiscovery
=============
`RFC6186 <https://www.rfc-editor.org/rfc/rfc6186>`_ allows supporting email clients to automatically discover SMTP / IMAP addresses
of the mailserver. For that, the following records are required:
`RFC6186`_ defines how email clients can automatically discover a mail server's
SMTP and IMAP endpoints. To enable this, the following DNS records must be
configured:
================= ==== ==== ======== ====== ==== =================
Record TTL Type Priority Weight Port Value
================= ==== ==== ======== ====== ==== =================
_submission._tcp 3600 SRV 5 0 587 mail.example.com.
_submissions._tcp 3600 SRV 5 0 465 mail.example.com.
_imap._tcp 3600 SRV 5 0 143 mail.example.com.
_imaps._tcp 3600 SRV 5 0 993 mail.example.com.
================= ==== ==== ======== ====== ==== =================
.. csv-table:: Resource record set
:header: "Name", "TTL", "Type", "Priority", "Weight", "Port", "Value"
:widths: 30, 5, 5, 5, 5, 5, 20
Please note that only a few MUAs currently implement this. For vendor-specific
discovery mechanisms `automx <https://github.com/rseichter/automx2>`_ can be used instead.
_submissions._tcp.example.com., 3600, SRV, 10, 1, 465, mail.example.com.
_imaps._tcp.example.com., 3600, SRV, 10, 1, 993, mail.example.com.
Legacy records
^^^^^^^^^^^^^^
The following DNS records are only supported with
:option:`mailserver.enableSubmission` and :option:`mailserver.enableImap`,
because they only support connections with explicit TLS. These services are
disabled by default because they are deprecated through `RFC8314 4.1`_.
.. csv-table:: Resource record set
:header: "Name", "TTL", "Type", "Priority", "Weight", "Port", "Value"
:widths: 30, 5, 5, 5, 5, 5, 20
_submission._tcp.example.com., 3600, SRV, 20, 1, 587, mail.example.com.
_imap._tcp.example.com., 3600, SRV, 20, 1, 143, mail.example.com.
Client support
^^^^^^^^^^^^^^
*As researched in March 2026*
Only a small number of MUAs currently implement this. The most common concern
from the bigger and security-conscious vendors is lack of widespread DNSSEC
propagation that could be used to authenticate these SRV records.
- Aerc: since 0.20.1
- ``_submissions._tcp`` support submitted in https://lists.sr.ht/~rjarry/aerc-devel/patches/68173
- Evolution: Since 3.49.3 for mail accounts
- https://gitlab.gnome.org/GNOME/evolution/-/wikis/Autoconfig
- https://gitlab.gnome.org/GNOME/evolution/-/issues/941
Unsupported
***********
- DeltaChat:
- https://github.com/chatmail/core/issues/1508
- Thunderbird:
- Desktop: https://bugzilla.mozilla.org/show_bug.cgi?id=342242
- Android: https://github.com/thunderbird/thunderbird-android/issues/4721
Vendor-specific autoconfig
^^^^^^^^^^^^^^^^^^^^^^^^^^
The `automx2`_ service can provide autoconfig support for Apple's
`mobileconfig`_, Microsoft's `Autodiscover`_ and Mozilla's `Autoconfig`_
standards. It does however lack support for multiple mail domains and isn't open for
contributions due to copyright concerns.
.. _mobileconfig: https://support.apple.com/de-de/guide/profile-manager/pmdbd71ebc9/mac
.. _Autodiscover: https://learn.microsoft.com/en-us/exchange/architecture/client-access/autodiscover?view=exchserver-2019
.. _Autoconfig: https://benbucksch.github.io/autoconfig-spec/draft-ietf-mailmaint-autoconfig.html
.. _automx2: https://github.com/rseichter/automx2
.. _RFC6186: https://www.rfc-editor.org/rfc/rfc6186
.. _RFC8314 4.1: https://www.rfc-editor.org/rfc/rfc8314#section-4.1
+1 -1
View File
@@ -12,7 +12,7 @@ anything works, as it is simply a folder with plenty of files in it. If
your backup solution does not preserve the owner of the files dont
forget to ``chown`` them to ``virtualMail:virtualMail`` if you copy them
back (or whatever you specified as ``vmailUserName``, and
``vmailGoupName``).
``vmailGroupName``).
If you enabled ``enableManageSieve`` then you also may want to backup
``/var/sieve`` or whatever you have specified as ``sieveDirectory``.
+35
View File
@@ -0,0 +1,35 @@
{
description = "NixOS configuration";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11-small";
simple-nixos-mailserver.url = "gitlab:simple-nixos-mailserver/nixos-mailserver/nixos-25.11";
simple-nixos-mailserver.inputs.nixpkgs.follows = "nixpkgs";
};
outputs =
{
nixpkgs,
simple-nixos-mailserver,
...
}:
{
nixosConfigurations = {
hostname = nixpkgs.lib.nixosSystem {
system = "x86_64-linux"; # or aarch64-linux
modules = [
simple-nixos-mailserver.nixosModule
{
mailserver = {
enable = true;
# Check the setup guide if you have no idea how to continue
# from here!
};
}
];
};
};
};
}
+13 -25
View File
@@ -1,30 +1,18 @@
Nix Flakes
==========
Flakes
======
If you're using `flakes <https://wiki.nixos.org/wiki/Flakes>`__, you can use
the following minimal ``flake.nix`` as an example:
To use NixOS mailserver `Nix flakes`_, the following minimal ``flake.nix`` can
serve as an example to get started:
.. code:: nix
.. _Nix flakes: https://wiki.nixos.org/wiki/Flakes
{
description = "NixOS configuration";
.. literalinclude:: ./flakes.nix
:language: nix
inputs.simple-nixos-mailserver.url = "gitlab:simple-nixos-mailserver/nixos-mailserver/nixos-20.09";
outputs = { self, nixpkgs, simple-nixos-mailserver }: {
nixosConfigurations = {
hostname = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
simple-nixos-mailserver.nixosModule
{
mailserver = {
enable = true;
# ...
};
}
];
};
};
};
}
Lock the inputs and deploy the system closure:
.. code-block:: console
nix flake lock
nixos-rebuild --target-host root@mail.example.com --flake .#hostname switch
+1 -1
View File
@@ -48,7 +48,7 @@ recommended to move indices in a different location, such as
When the value of the ``indexDir`` option is changed, all dovecot
indices needs to be recreated: clients would need to resynchronize.
Indexation itself is rather resouces intensive, in CPU, and for emails with
Indexation itself is rather resource intensive, in CPU, and for emails with
large headers, in memory as well. Initial indexation of existing emails can take
hours. If the indexer worker is killed or segfaults during indexation, it can
be that it tried to allocate more memory than allowed. You can increase the memory
+2 -2
View File
@@ -38,8 +38,8 @@ You can then run the testsuite via
$ nix flake check -L
Since Nix doesn't garantee your machine have enough resources to run
all test VMs in parallel, some tests can fail. You would then haev to
Since Nix doesn't guarantee your machine have enough resources to run
all test VMs in parallel, some tests can fail. You would then have to
run tests manually. For instance:
::
+10 -5
View File
@@ -33,12 +33,17 @@ Welcome to NixOS Mailserver's documentation!
:maxdepth: 0
:caption: How-to
backup-guide
add-radicale
add-roundcube
rspamd-tuning
flakes
autodiscovery
backup-guide
flakes
rspamd-tuning
.. toctree::
:maxdepth: 0
:caption: Integrations
radicale
roundcube
Indices and tables
==================
+55
View File
@@ -0,0 +1,55 @@
{
config,
pkgs,
lib,
...
}:
let
inherit (lib)
concatStrings
flip
mapAttrsToList
;
mailAccounts = config.mailserver.loginAccounts;
htpasswd = pkgs.writeText "radicale.users" (
concatStrings (flip mapAttrsToList mailAccounts (mail: user: "${mail}+:${user.hashedPassword}\n"))
);
in
{
services.radicale = {
enable = true;
settings = {
auth = {
type = "htpasswd";
htpasswd_filename = "${htpasswd}";
htpasswd_encryption = "bcrypt";
};
};
};
services.nginx = {
enable = true;
virtualHosts = {
"cal.example.com" = {
forceSSL = true;
enableACME = true;
locations."/" = {
proxyPass = "http://localhost:5232/";
extraConfig = ''
proxy_set_header X-Script-Name /;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass_header Authorization;
'';
};
};
};
};
networking.firewall.allowedTCPPorts = [
80
443
];
}
+29
View File
@@ -0,0 +1,29 @@
Radicale
========
Radicale is a lightweight open-source CalDAV/CardDAV server that stores
calendars and contacts as plain files on the filesystem, enabling simple
self-hosted synchronization with standard clients.
Limitations
^^^^^^^^^^^
Radicale since the 3.x release (introduced in NixOS 20.09) does not support
traditional crypt() password hashes any longer. To establish access for
existing :option:`mailserver.loginAccounts`, the hashing method used
for ``hashedPassword`` needs to be compatible with one of the available
`htpasswd_encryption`_ methods. Such hashes can for example be created using
.. code-block:: console
nix-shell -p mkpasswd --command "mkpasswd -m bcrypt"
.. _htpasswd_encryption: https://radicale.org/v3.html#htpasswd_encryption
Code
^^^^
Configuration contributed by Robert Schütz (@dotlambda).
.. literalinclude:: ./radicale.nix
:language: nix
+3 -3
View File
@@ -35,14 +35,14 @@ NixOS 25.11
1024 bit keys should not be considered valid any longer.
- IMAP access over port ``143/tcp`` is now default disabled in line
with `RFC 8314 4.1`_. Use IMAP over implicit TLS on port ``993/tcp``
instead. If you still require this feature you can reenable it using
instead. If you still require this feature you can re-enable it using
``mailserver.enableImap``, but it is scheduled for removal after the 25.11
release.
- SMTP server and client now support and prefer a hybrid key exchange
(X25519MLKEM768)
- SMTP access over STARTTLS on port ``587/tcp`` is now default disabled in line
with `RFC 8314 3.3`_. If you still require this feature you can renable it using
``mailserver.enableSubmission``.
with `RFC 8314 3.3`_. If you still require this feature you can re-enable it
using ``mailserver.enableSubmission``.
- DMARC reports are now sent with the ``noreply-dmarc`` localpart from the
system domain.
- DANE and MTA-STS are now validated for outgoing SMTP connections using
+41
View File
@@ -0,0 +1,41 @@
{
config,
pkgs,
...
}:
{
services.roundcube = {
enable = true;
hostName = "webmail.example.com"; # the nginx vhost
package = pkgs.roundcube.withPlugins (
plugins: with plugins; [
# https://search.nixos.org/packages?query=roundcubePlugins
persistent_login
]
);
dicts = with pkgs.aspellDicts; [
# https://search.nixos.org/packages?query=aspellDicts
en
];
# Account for ~30% size increase due to base64 encoding of attachments
# https://github.com/roundcube/roundcubemail/issues/7979
maxAttachmentSize = config.mailserver.messageSizeLimit / 1024 / 1024 / 1.37;
extraConfig = ''
$config['imap_host'] = "ssl://${config.mailserver.fqdn}";
$config['smtp_host'] = "ssl://${config.mailserver.fqdn}";
$config['smtp_user'] = "%u";
$config['smtp_pass'] = "%p";
'';
};
services.nginx.virtualHosts.${config.services.rounducbe.hostName} = {
enableACME = true;
forceSSL = true;
};
networking.firewall.allowedTCPPorts = [
80
443
];
}
+19
View File
@@ -0,0 +1,19 @@
Roundcube
=========
Roundcube is a browser-based open-source webmail client that provides a
full-featured email interface with support for IMAP, SMTP, address books, and
extensible plugins.
Code
^^^^
The NixOS module for Roundcube integrates almost immediately with NixOS
mailserver, automatically configuring an Nginx virtual host and ACME-managed
TLS for secure webmail access; using other web servers may require additional
manual setup.
Once set up you can login with your login account credentials.
.. literalinclude:: ./roundcube.nix
:language: nix
+1 -1
View File
@@ -94,7 +94,7 @@ With an nginx reverse-proxy
If you have a secured nginx reverse proxy set on the host, you can use it to expose the socket.
**Keep in mind the UI is unsecured by default, you need to setup an authentication scheme**, for
exemple with `basic auth <https://docs.nginx.com/nginx/admin-guide/security-controls/configuring-http-basic-authentication/>`_:
example with `basic auth <https://docs.nginx.com/nginx/admin-guide/security-controls/configuring-http-basic-authentication/>`_:
.. code:: nix
+1 -1
View File
@@ -155,7 +155,7 @@
markdownlint = {
enable = true;
settings.configuration = {
# Max line length, doesn't seem to correclty account for lines containing links
# Max line length, doesn't seem to correctly account for lines containing links
# https://github.com/DavidAnson/markdownlint/blob/main/doc/md013.md
MD013 = false;
};
+2 -2
View File
@@ -145,7 +145,7 @@ let
lib.filterAttrs (_: v: v ? "specialUse" && v.specialUse == "Junk") cfg.mailboxes
);
junkMailboxNumber = builtins.length junkMailboxes;
# The assertion garantees there is exactly one Junk mailbox.
# The assertion guarantees there is exactly one Junk mailbox.
junkMailboxName = if junkMailboxNumber == 1 then builtins.elemAt junkMailboxes 0 else "";
mkLdapSearchScope =
@@ -206,7 +206,7 @@ in
};
};
# for sieve-test. Shelling it in on demand usually doesnt' work, as it reads
# for sieve-test. Shelling it in on demand usually doesn't work, as it reads
# the global config and tries to open shared libraries configured in there,
# which are usually not compatible.
environment.systemPackages = [
+3 -3
View File
@@ -430,9 +430,9 @@
# fetchmail returns EXIT_CODE 0 when it retrieves mail
client.succeed("fetchmail --nosslcertck -v")
with subtest("catchAlls"):
with subtest("domain catch-all"):
client.execute("rm ~/mail/*")
# send email from chuck to non exsitent account
# send email from chuck to non-existent account
client.succeed(
"msmtp -a test3 --tls=on --tls-certcheck=off --auth=on lol@example.com < /etc/root/email2 >&2"
)
@@ -447,7 +447,7 @@
)
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
# fetchmail returns EXIT_CODE 1 when no new mail
# if this succeeds, it means that user1 recieved the mail that was intended for chuck.
# if this succeeds, it means that user1 received the mail that was intended for chuck.
client.fail("fetchmail --nosslcertck -v")
with subtest("extraVirtualAliases"):
+1 -1
View File
@@ -135,7 +135,7 @@ in
# This function retrieves the ldap table file from a postconf
# command.
# A key lookup is achived and the returned value is compared
# A key lookup is achieved and the returned value is compared
# to the expected value.
def test_lookup(postconf_cmdline, key, expected):
conf = machine.succeed(postconf_cmdline).rstrip()