{
  config,
  lib,
  shb,
  pkgs,
  ...
}:
let
  cfg = config.shb.mailserver;
in
{
  imports = [
    (
      builtins.fetchGit {
        url = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver.git";
        ref = "master";
        rev = "e33fbde199eaad513ef5d0746db19d5878150232";
      }
      + "/default.nix"
    )
    ../blocks/lldap.nix
  ];

  options.shb.mailserver = {
    enable = lib.mkEnableOption "SHB's nixos-mailserver module";

    subdomain = lib.mkOption {
      type = lib.types.str;
      description = "Subdomain under which imap and smtp functions will be served.";
      default = "imap";
    };

    domain = lib.mkOption {
      type = lib.types.str;
      description = "domain under which imap and smtp functions will be served.";
      example = "mydomain.com";
    };

    ssl = lib.mkOption {
      description = "Path to SSL files";
      type = lib.types.nullOr shb.contracts.ssl.certs;
      default = null;
    };

    adminUsername = lib.mkOption {
      type = lib.types.nullOr lib.types.str;
      default = null;
      description = ''
        Admin username.

        postmaster will be made an alias of this user.
      '';
      example = "admin";
    };

    adminPassword = lib.mkOption {
      description = "Admin user password.";
      default = null;
      type = lib.types.nullOr (
        lib.types.submodule {
          options = shb.contracts.secret.mkRequester {
            mode = "0400";
            owner = config.services.postfix.user;
            ownerText = "services.postfix.user";
            restartUnits = [ "dovecot.service" ];
          };
        }
      );
    };

    imapSync = lib.mkOption {
      description = ''
        Synchronize one or more email providers through IMAP
        to your dovecot instance.

        This allows you to backup that email provider
        and centralize your accounts in this dovecot instance.
      '';
      default = null;
      type = lib.types.nullOr (
        lib.types.submodule {
          options = {
            syncTimer = lib.mkOption {
              type = lib.types.str;
              default = "5m";
              description = ''
                Systemd timer for when imap sync job should happen.

                This timer is not scheduling the job at regular intervals.
                After a job finishes, the given amount of time is waited then the next job is started.

                The default is set deliberatily slow to not spam you when setting up your mailserver.
                When everything works, you will want to reduce it to 10s or something like that.
              '';
              example = "10s";
            };

            debug = lib.mkOption {
              type = lib.types.bool;
              default = false;
              description = "Enable verbose mbsync logging.";
            };

            accounts = lib.mkOption {
              description = ''
                Accounts to sync emails from using IMAP.

                Emails will be stored under `''${config.mailserver.storage.path}/''${name}/''${username}`
              '';
              type = lib.types.attrsOf (
                lib.types.submodule {
                  options = {
                    host = lib.mkOption {
                      type = lib.types.str;
                      description = "Hostname of the email's provider IMAP server.";
                      example = "imap.fastmail.com";
                    };

                    port = lib.mkOption {
                      type = lib.types.port;
                      description = "Port of the email's provider IMAP server.";
                      default = 993;
                    };

                    username = lib.mkOption {
                      type = lib.types.str;
                      description = "Username used to login to the email's provider IMAP server.";
                      example = "userA@fastmail.com";
                    };

                    password = lib.mkOption {
                      description = ''
                        Password used to login to the email's provider IMAP server.

                        The password could be an "app password" like for [Fastmail](https://www.fastmail.help/hc/en-us/articles/360058752854-App-passwords)
                      '';
                      type = lib.types.submodule {
                        options = shb.contracts.secret.mkRequester {
                          mode = "0400";
                          owner = config.mailserver.storage.owner;
                          restartUnits = [ "mbsync.service" ];
                        };
                      };
                    };

                    sslType = lib.mkOption {
                      description = "Connection security method.";
                      type = lib.types.enum [
                        "IMAPS"
                        "STARTTLS"
                      ];
                      default = "IMAPS";
                    };

                    timeout = lib.mkOption {
                      description = "Connect and data timeout.";
                      type = lib.types.int;
                      default = 120;
                    };

                    mapSpecialDrafts = lib.mkOption {
                      type = lib.types.str;
                      default = "Drafts";
                      description = ''
                        Drafts special folder name on far side.

                        You only need to change this if mbsync logs the following error:

                            Error: ... far side box Drafts cannot be opened
                      '';
                    };
                    mapSpecialSent = lib.mkOption {
                      type = lib.types.str;
                      default = "Sent";
                      description = ''
                        Sent special folder name on far side.

                        You only need to change this if mbsync logs the following error:

                            Error: ... far side box Sent cannot be opened
                      '';
                    };
                    mapSpecialTrash = lib.mkOption {
                      type = lib.types.str;
                      default = "Trash";
                      description = ''
                        Trash special folder name on far side.

                        You only need to change this if mbsync logs the following error:

                            Error: ... far side box Trash cannot be opened
                      '';
                    };
                    mapSpecialJunk = lib.mkOption {
                      type = lib.types.str;
                      default = "Junk";
                      description = ''
                        Junk special folder name on far side.

                        You only need to change this if mbsync logs the following error:

                            Error: ... far side box Junk cannot be opened
                      '';
                      example = "Spam";
                    };
                  };
                }
              );
            };
          };
        }
      );
    };

    smtpRelay = lib.mkOption {
      description = ''
        Proxy outgoing emails through an email provider.

        In short, this can help you avoid having your outgoing emails marked as spam.
        See the manual for a lengthier explanation.
      '';
      default = null;
      type = lib.types.nullOr (
        lib.types.submodule {
          options = {
            host = lib.mkOption {
              type = lib.types.str;
              description = "Hostname of the email's provider SMTP server.";
              example = "smtp.fastmail.com";
            };

            port = lib.mkOption {
              type = lib.types.port;
              description = "Port of the email's provider SMTP server.";
              default = 587;
            };

            username = lib.mkOption {
              description = "Username used to login to the email's provider SMTP server.";
              type = lib.types.str;
            };

            password = lib.mkOption {
              description = ''
                Password used to login to the email's provider IMAP server.

                The password could be an "app password" like for [Fastmail](https://www.fastmail.help/hc/en-us/articles/360058752854-App-passwords)
              '';
              type = lib.types.submodule {
                options = shb.contracts.secret.mkRequester {
                  mode = "0400";
                  owner = config.services.postfix.user;
                  ownerText = "services.postfix.user";
                  restartUnits = [ "postfix.service" ];
                };
              };
            };
          };
        }
      );
    };

    ldap = lib.mkOption {
      description = ''
        LDAP Integration.

        Enabling this app will create a new LDAP configuration or update one that exists with
        the given host.
      '';
      default = null;
      type = lib.types.nullOr (
        lib.types.submodule {
          options = {
            enable = lib.mkEnableOption "LDAP app.";

            host = lib.mkOption {
              type = lib.types.str;
              description = ''
                Host serving the LDAP server.
              '';
              default = "127.0.0.1";
            };

            port = lib.mkOption {
              type = lib.types.port;
              description = ''
                Port of the service serving the LDAP server.
              '';
              default = 389;
            };

            dcdomain = lib.mkOption {
              type = lib.types.str;
              description = "dc domain for ldap.";
              example = "dc=mydomain,dc=com";
            };

            account = lib.mkOption {
              type = lib.types.str;
              description = ''
                Select one account from those defined in `shb.mailserver.imapSync.accounts`
                to login with.

                Using LDAP, you can only connect to one account.
                This limitation could maybe be lifted, feel free to post an issue if you need this.
              '';
            };

            adminName = lib.mkOption {
              type = lib.types.str;
              description = "Admin user of the LDAP server.";
              default = "admin";
            };

            adminPassword = lib.mkOption {
              description = "LDAP server admin password.";
              type = lib.types.submodule {
                options = shb.contracts.secret.mkRequester {
                  mode = "0400";
                  owner = "nextcloud";
                  restartUnits = [ "dovecot.service" ];
                };
              };
            };

            userGroup = lib.mkOption {
              type = lib.types.str;
              description = "Group users must belong to to be able to use mails.";
              default = "mail_user";
            };
          };
        }
      );
    };

    stateVersion = lib.mkOption {
      type = lib.types.ints.positive;
      # SHB started at stateVersion 3.
      default = 4;
      description = ''
        Tracking stateful version changes as an incrementing number.

        When a new release comes out we may require manual migration steps to
        be completed, before the new version can be put into production.

        If your `stateVersion` is too low one or multiple assertions may
        trigger to give you instructions on what migrations steps are required
        to continue. Increase the `stateVersion` as instructed by the assertion
        message.

        See https://nixos-mailserver.readthedocs.io/en/latest/release-notes.html
        and https://nixos-mailserver.readthedocs.io/en/latest/migrations.html
      '';
    };

    backup = lib.mkOption {
      description = ''
        Backup emails, index and sieve.
      '';
      default = { };
      type = lib.types.submodule {
        options = shb.contracts.backup.mkRequester {
          user = config.mailserver.storage.owner;
          sourceDirectories = builtins.filter (x: x != null) [
            config.mailserver.indexDir
            config.mailserver.storage.path
            config.mailserver.sieveDirectory
          ];
          sourceDirectoriesText = ''
            [
              config.mailserver.indexDir
              config.mailserver.storage.path
              config.mailserver.sieveDirectory
            ]
          '';
        };
      };
    };

    backupDKIM = lib.mkOption {
      description = ''
        Backup dkim directory.
      '';
      default = { };
      type = lib.types.submodule {
        options = shb.contracts.backup.mkRequester {
          user = config.services.rspamd.user;
          userText = "services.rspamd.user";
          sourceDirectories = builtins.filter (x: x != null) [
            config.mailserver.dkim.keyDirectory
          ];
          sourceDirectoriesText = ''
            [
              config.mailserver.dkimKeyDirectory
            ]
          '';
        };
      };
    };

    impermanence = lib.mkOption {
      description = ''
        Path to save when using impermanence setup.
      '';
      type = lib.types.attrsOf lib.types.str;
      default = {
        index = config.mailserver.indexDir;
        mail = config.mailserver.storage.path;
        sieve = config.mailserver.sieveDirectory;
        dkim = config.mailserver.dkimKeyDirectory;
      };
      defaultText = lib.literalExpression ''
        {
          index = config.mailserver.indexDir;
          mail = config.mailserver.storage.path;
          sieve = config.mailserver.sieveDirectory;
          dkim = config.mailserver.dkimKeyDirectory;
        }
      '';
    };

    dashboard = lib.mkOption {
      description = ''
        Dashboard contract consumer
      '';
      default = { };
      type = lib.types.submodule {
        options = shb.contracts.dashboard.mkRequester {
          externalUrl = "https://${cfg.subdomain}.${cfg.domain}";
          externalUrlText = "https://\${config.shb.mailserver.subdomain}.\${config.shb.mailserver.domain}";
        };
      };
    };
  };

  config = lib.mkMerge [
    (lib.mkIf cfg.enable {
      mailserver = {
        enable = true;
        fqdn = "${cfg.subdomain}.${cfg.domain}";
        inherit (cfg) stateVersion;
        domains = [ cfg.domain ];

        localDnsResolver = false;

        enableImapSsl = true;
        enableSubmissionSsl = true;
        x509 = {
          certificateFile = cfg.ssl.paths.cert;
          privateKeyFile = cfg.ssl.paths.key;
        };

        # Using / is needed for iOS mail.
        # Both following options are used to organize subfolders in subdirectories.
        hierarchySeparator = "/";
        storage = {
          directoryLayout = "fs";
        };
      };

      services.postfix.settings.main = {
        smtpd_tls_security_level = lib.mkForce "encrypt";
      };

      # Is probably needed for iOS mail.
      services.dovecot2.settings = {
        ssl_min_protocol = "TLSv1.2";
        # This conflicts with the default setting which seems much better than this one.
        # So I'm leaving the default settings and we'll see if something breaks.
        # ssl_cipher_list = "HIGH:!aNULL:!MD5";
      };

      services.nginx = {
        enable = true;

        virtualHosts."${cfg.domain}" =
          let
            announce = pkgs.writeTextDir "config-v1.1.xml" ''
              <?xml version="1.0" encoding="UTF-8"?>
              <clientConfig version="1.1">
                <emailProvider id="${cfg.domain}">
                  <domain>${cfg.domain}</domain>
                  <displayName>${cfg.domain} Mailserver</displayName>

                  <!-- Incoming IMAP server -->
                  <incomingServer type="imap">
                    <hostname>${cfg.subdomain}.${cfg.domain}</hostname>
                    <port>993</port>
                    <socketType>SSL</socketType>
                    <authentication>password-cleartext</authentication>
                    <username>%EMAILADDRESS%</username>
                  </incomingServer>

                  <!-- Outgoing SMTP server -->
                  <outgoingServer type="smtp">
                    <hostname>${cfg.subdomain}.${cfg.domain}</hostname>
                    <port>465</port>
                    <socketType>SSL</socketType>
                    <authentication>password-cleartext</authentication>
                    <username>%EMAILADDRESS%</username>
                  </outgoingServer>

                </emailProvider>
              </clientConfig>
            '';
          in
          {
            forceSSL = true; # Redirect HTTP → HTTPS
            sslCertificate = cfg.ssl.paths.cert;
            sslCertificateKey = cfg.ssl.paths.key;
            root = "/var/www"; # Dummy root
            locations."/.well-known/autoconfig/mail/" = {
              alias = "${announce}/";
              extraConfig = ''
                default_type application/xml;
              '';
            };
          };
        virtualHosts."${cfg.subdomain}.${cfg.domain}" =
          let
            landingPage = pkgs.writeTextDir "index.html" ''
              <html><body>
              <p>Configuration of the mailserver is done automatically thanks to
              <a href="https://${cfg.domain}/.well-known/autoconfig/mail/config-v1.1.xml">${cfg.domain}/.well-known/autoconfig/mail/config-v1.1.xml</a>.</p>
              </body></html>
            '';
          in
          {
            forceSSL = true; # Redirect HTTP → HTTPS
            sslCertificate = cfg.ssl.paths.cert;
            sslCertificateKey = cfg.ssl.paths.key;
            root = "/var/www"; # Dummy root
            locations."/" = {
              alias = "${landingPage}/";
              extraConfig = ''
                default_type application/html;
              '';
            };
          };
      };
    })
    (lib.mkIf (cfg.enable && cfg.adminUsername != null) {
      assertions = [
        {
          assertion = cfg.adminPassword != null;
          message = "`shb.mailserver.adminPassword` must be not null if `shb.mailserver.adminUsername` is not null.";
        }
      ];

      mailserver = {
        # To create the password hashes, use:
        # nix run nixpkgs#mkpasswd -- --run 'mkpasswd -s'
        loginAccounts = {
          "${cfg.adminUsername}@${cfg.domain}" = {
            hashedPasswordFile = cfg.adminPassword.result.path;
            aliases = [ "postmaster@${cfg.domain}" ];
          };
        };
      };
    })
    (lib.mkIf (cfg.enable && cfg.ldap != null) {
      assertions = [
        {
          assertion = cfg.adminUsername == null;
          message = "`shb.mailserver.adminUsername` must be null `shb.mailserver.ldap` integration is set.";
        }
      ];

      shb.lldap.ensureGroups = {
        ${cfg.ldap.userGroup} = { };
      };

      mailserver = {
        ldap = {
          enable = true;
          uris = [
            "ldap://${cfg.ldap.host}:${toString cfg.ldap.port}"
          ];
          base = "ou=people,${cfg.ldap.dcdomain}";
          scope = "sub";
          bind = {
            dn = "uid=${cfg.ldap.adminName},ou=people,${cfg.ldap.dcdomain}";
            passwordFile = cfg.ldap.adminPassword.result.path;
          };
          dovecot =
            let
              filter = "(&(objectClass=inetOrgPerson)(mail=%{user})(memberOf=cn=${cfg.ldap.userGroup},ou=groups,${cfg.ldap.dcdomain}))";
            in
            {
              passFilter = filter;
              userFilter = filter;
            };
          # username needs to be set to mail so postfix maps correctly the mail address.
          # Otherwise we get error messages when sending
          #   Sender address rejected: not owned by user
          attributes = {
            username = "mail";
          };
          postfix = {
            filter = "(&(objectClass=inetOrgPerson)(mail=%s)(memberOf=cn=${cfg.ldap.userGroup},ou=groups,${cfg.ldap.dcdomain}))";
          };
        };
      };
      services.dovecot2.settings = {
        "userdb ldap" = {
          fields.home = lib.mkForce "${config.mailserver.storage.path}/${cfg.ldap.account}/%{user}";
          fields.mail_index_path = lib.mkForce "${config.mailserver.storage.path}/${cfg.ldap.account}/%{user}";
        };
        "passdb ldap" = {
          # LLDAP does not understand ldap user password setup.
          fields.password = lib.mkForce null;
          # We use bind DN auth.
          # https://doc.dovecot.org/2.3/configuration_manual/authentication/ldap_bind/#authentication-ldap-bind
          bind = true;
        };
      };
    })
    (lib.mkIf (cfg.enable && cfg.imapSync != null) {
      systemd.services.mbsync =
        let
          configFile =
            let
              mkAccount = name: acct: ''
                # ${name} account

                IMAPAccount ${name}
                Host ${acct.host}
                Port ${toString acct.port}
                User ${acct.username}
                PassCmd "cat ${acct.password.result.path}"
                TLSType ${acct.sslType}
                AuthMechs LOGIN
                Timeout ${toString acct.timeout}

                IMAPStore ${name}-remote
                Account ${name}

                MaildirStore ${name}-local
                INBOX ${config.mailserver.storage.path}/${name}/${acct.username}/mail/
                # Maps subfolders on far side to actual subfolders on disk.
                # The other option is Maildir++ but then the mailserver.hierarchySeparator must be set to a dot '.'
                SubFolders Verbatim
                Path ${config.mailserver.storage.path}/${name}/${acct.username}/mail/

                Channel ${name}-main
                Far :${name}-remote:
                Near :${name}-local:
                Patterns * !Drafts !Sent !Trash !Junk !${acct.mapSpecialDrafts} !${acct.mapSpecialSent} !${acct.mapSpecialTrash} !${acct.mapSpecialJunk}
                Create Both
                Expunge Both
                SyncState *
                Sync All
                CopyArrivalDate yes  # Preserve date from incoming message.

                Channel ${name}-drafts
                Far :${name}-remote:"${acct.mapSpecialDrafts}"
                Near :${name}-local:"Drafts"
                Create Both
                Expunge Both
                SyncState *
                Sync All
                CopyArrivalDate yes  # Preserve date from incoming message.

                Channel ${name}-sent
                Far :${name}-remote:"${acct.mapSpecialSent}"
                Near :${name}-local:"Sent"
                Create Both
                Expunge Both
                SyncState *
                Sync All
                CopyArrivalDate yes  # Preserve date from incoming message.

                Channel ${name}-trash
                Far :${name}-remote:"${acct.mapSpecialTrash}"
                Near :${name}-local:"Trash"
                Create Both
                Expunge Both
                SyncState *
                Sync All
                CopyArrivalDate yes  # Preserve date from incoming message.

                Channel ${name}-junk
                Far :${name}-remote:"${acct.mapSpecialJunk}"
                Near :${name}-local:"Junk"
                Create Both
                Expunge Both
                SyncState *
                Sync All
                CopyArrivalDate yes  # Preserve date from incoming message.

                Group ${name}
                Channel ${name}-main
                Channel ${name}-drafts
                Channel ${name}-sent
                Channel ${name}-trash
                Channel ${name}-junk

                # END ${name} account
              '';

            in
            pkgs.writeText "mbsync.conf" (
              lib.concatStringsSep "\n" (lib.mapAttrsToList mkAccount cfg.imapSync.accounts)
            );
        in
        {
          description = "Sync mailbox";
          serviceConfig = {
            Type = "oneshot";
            User = config.mailserver.storage.owner;
          };
          script =
            let
              debug = if cfg.imapSync.debug then "-V" else "";
            in
            ''
              ${pkgs.isync}/bin/mbsync --all ${debug} --config ${configFile}
            '';
        };

      systemd.tmpfiles.rules =
        let
          mkAccount =
            name: acct:
            # The equal sign makes sure parent directories have the corret user and group too.
            [
              "d '${config.mailserver.storage.path}/${name}' 0750 ${config.mailserver.storage.owner} ${config.mailserver.storage.group} - -"
              "d '${config.mailserver.storage.path}/${name}/${acct.username}' 0750 ${config.mailserver.storage.owner} ${config.mailserver.storage.group} - -"
            ];
        in
        lib.flatten (lib.mapAttrsToList mkAccount cfg.imapSync.accounts);

      systemd.timers.mbsync = {
        wantedBy = [ "timers.target" ];
        timerConfig = {
          OnBootSec = cfg.imapSync.syncTimer;
          OnUnitActiveSec = cfg.imapSync.syncTimer;
        };
      };
    })
    (lib.mkIf (cfg.enable && cfg.smtpRelay != null) (
      let
        url = "[${cfg.smtpRelay.host}]:${toString cfg.smtpRelay.port}";
      in
      {
        assertions = [
          {
            assertion = lib.hasAttr cfg.adminPassword != null;
            message = "`shb.mailserver.adminPassword` must be not null if `shb.mailserver.adminUsername` is not null.";
          }
        ];

        # Inspiration from https://www.brull.me/postfix/debian/fastmail/2016/08/16/fastmail-smtp.html
        services.postfix = {
          settings.main = {
            relayhost = [ url ];
            smtp_sasl_auth_enable = "yes";
            smtp_sasl_password_maps = "texthash:/run/secrets/postfix/postfix-smtp-relay-password";
            smtp_sasl_security_options = "noanonymous";
            smtp_use_tls = "yes";
          };
        };

        systemd.services.postfix-pre = {
          script = shb.replaceSecrets {
            userConfig = {
              inherit url;
              inherit (cfg.smtpRelay) username;
              password.source = cfg.smtpRelay.password.result.path;
            };
            generator =
              name:
              {
                url,
                username,
                password,
              }:
              pkgs.writeText "postfix-smtp-relay-password" ''
                ${url} ${username}:${password}
              '';
            resultPath = "/run/secrets/postfix/postfix-smtp-relay-password";
            user = config.services.postfix.user;
          };
          serviceConfig.Type = "oneshot";
          wantedBy = [ "multi-user.target" ];
          before = [ "postfix.service" ];
          requiredBy = [ "postfix.service" ];
        };
      }
    ))
  ];
}
