Mailserver with NixOS

Posted on 2021-02-06

In this blog post I will go through how I setup a mailserver using the excellent nixos-mailserver module. If you want to follow along, you obviously need to setup a domain with a domain registrar.

This post will only cover the details for the server, I will make a follow-up post on how I configure my laptop to deal with mail.

What you get

Before we actually use the module, it is probably a good idea to go over what it is that you get when using the module. The module will configure the following things for you

  • Dovecot for delivering mail using IMAP or POP3
  • Postfix for sending mail
  • Rspamd for filtering spam and greylisting
  • Let’ Encrypt for SSL certificate
  • Opendkim for DKIM signing
  • Sieve for filtering mail

Download the modules

The nixos-mailserver module is not part of the official Nixpkgs repository, this means that we have to download it separately to use it. You can do this in many different ways, I am currently using the experimental nix-flakes feature so I will put the following in my flake.nix:

nixos-mailserver = {
  url = "gitlab:simple-nixos-mailserver/nixos-mailserver/nixos-20.09";

If you do not use flakes, you can put the following in your configuration.nix:

{ config, pkgs, ... }:
let release = "nixos-20.09";
in {
  imports = [
    (builtins.fetchTarball {
      url = "${release}/nixos-mailserver-${release}.tar.gz";
      # This hash needs to be updated
      sha256 = "0000000000000000000000000000000000000000000000000000";

  # The rest of the config


All you have to configure is the domain for your server, the mailboxes you want to use and the user accounts, and that is it! Settings certificateScheme to 3 means that my SSL certificate will be created automatically with Let’s Encrypt. For this to work, you also have to create an “A” record for the FQDN to point to your IP address.

The account password should be a hash of the real password, this can be generated by running mkpasswd -m sha-512 in the shell. I will go into more detail on the sieve script I am using in the next section.

  mailserver = {
    enable = true;
    fqdn = "";
    domains = [ "" ];

    loginAccounts = {
      "" = {
        # mkpasswd -m sha-512
        hashedPassword = "<hashed-password>";
        sieveFilter = builtins.readFile ./filters.sieve;

    mailboxes = {
      Trash = {
        auto = "no";
        specialUse = "Trash";
      Junk = {
        auto = "subscribe";
        specialUse = "Junk";
      Drafts = {
        auto = "subscribe";
        specialUse = "Drafts";
      Sent = {
        auto = "subscribe";
        specialUse = "Sent";
      Archive = {
        auto = "subscribe";
        specialUse = "Archive";

    certificateScheme = 3;

  networking.firewall.allowedTCPPorts = [ 465 993 ];


I am subscribed to a few mailing lists, to better keep track of them I have a sieve filter on the server that refiles the emails to different folders. Every mailing list will get its own folder and the folder structure looks something like this (it follows the Maildir++ spec):

├── .Archive
├── .Sent
├── .lists.emacs.git-email
├── .lists.emacs.piem
├── .lists.nix.nixpkgs-dev
├── .lists.mail.public-inbox
├── cur
├── new
└── tmp

I could write all of the sieve filters manually, but that would be very tedious. John Wiegley has a really nice Haskell script which generates the sieve script from a Haskell lookup table. I have modified it to filter the relevant mailing lists, and the result looks like this:

elsif anyof (header :contains ["List-Id"]
          header :contains ["Sender","From","To","Reply-To","Cc"]
            "") {
  fileinto "lists.emacs.piem";

elsif anyof (header :contains ["List-Id"]
          header :contains ["Sender","From","To","Reply-To","Cc"]
            "~yoctocell/") {
  fileinto "lists.emacs.git-email";

elsif anyof (header :contains ["List-Id"]
          header :contains ["Sender","From","To","Reply-To","Cc"]
            "") {
  fileinto "lists.mail.public-inbox";

Then I just import this file in my mailserver config with sieveFilter = builtins.readFile ./filters.sieve;.

This is pretty much it for the server-side. There will be a follow up blog post about the client-side configuration (it is quite complicated). You can find the relevant configurations here.

Articles from blogs I follow...

Generated by openring

Outreachy 'guix git log' internship wrap-up

Magali Lemes joined Guix in December for a three-month internship with Outreachy . Magali implemented a guix git log command to browse the history of packaging changes, with mentoring from Simon Tournier and Gábor Boskovits. In this blog post, Magali…

via GNU Guix — Blog April 8, 2021

What should the next chat app look like?

As you’re surely aware, Signal has officially jumped the shark with the introduction of cryptocurrency to their chat app. Back in 2018, I wrote about my concerns with Signal, and those concerns were unfortunately validated by this week’s announcement. Moxie’…

via Drew DeVault's blog April 7, 2021

Uphold Marxism-Leninism-Maoism-Stallmanism!

Chairman Stallman has been under fire lately from the reactionary forces that have gathered mainly on the American propaganda machine called Twitter. Parties interested in the demise of the ideological advances of Free Software want to sabotage the movement,…

via brown121407 March 25, 2021