{ config, lib, pkgs, environment, ... }:

with lib;
let
  cfg = config.fudo.mail-server;

  state-directory = "${cfg.state-directory}/dovecot";

  pipe-bin = pkgs.stdenv.mkDerivation {
    name = "pipe_bin";
    src = ./dovecot/pipe_bin;
    buildInputs = with pkgs; [ makeWrapper coreutils bash rspamd ];
    buildCommand = ''
      mkdir -p $out/pipe/bin
      cp $src/* $out/pipe/bin/
      chmod a+x $out/pipe/bin/*
      patchShebangs $out/pipe/bin

      for file in $out/pipe/bin/*; do
        wrapProgram $file \
          --set PATH "${pkgs.coreutils}/bin:${pkgs.rspamd}/bin"
      done
    '';
  };

  ldap-conf = filename: config:
    let
      ssl-config = if config.ca == null then ''
        tls = no
        tls_require_cert = try
      '' else ''
        tls_ca_cert_file = ${config.ca}
        tls = yes
        tls_require_cert = try
      '';

    in
      pkgs.writeText filename ''
      uris = ${concatStringsSep " " config.server-urls}
      ldap_version = 3
      dn = ${config.reader-dn}
      dnpass = ${config.reader-passwd}
      auth_bind = yes
      auth_bind_userdn = uid=%u,ou=members,dc=fudo,dc=org
      base = dc=fudo,dc=org
      ${ssl-config}
    '';

  ldap-passwd-entry = ldap-config: ''
      passdb {
          driver = ldap
          args = ${ldap-conf "ldap-passdb.conf" ldap-config}
      }
    '';

  ldapOpts = {
    options = with types; {
      ca = mkOption {
        type = nullOr str;
        description = "The path to the CA cert used to sign the LDAP server certificate.";
        default = null;
      };

      server-urls = mkOption {
        type = listOf str;
        description = "A list of LDAP server URLs used for authentication.";
      };

      reader-dn = mkOption {
        type = str;
        description = ''
        DN to use for reading user information. Needs access to homeDirectory,
        uidNumber, gidNumber, and uid, but not password attributes.
      '';
      };

      reader-passwd = mkOption {
        type = str;
        description = ''
        Password for the user specified in ldap-reader-dn.
      '';
      };
    };
  };

  dovecot-user = config.services.dovecot2.user;

in {
  options.fudo.mail-server.dovecot = with types; {
    ssl-private-key = mkOption {
      type = str;
      description = "Location of the server SSL private key.";
    };

    ssl-certificate = mkOption {
      type = str;
      description = "Location of the server SSL certificate.";
    };

    ldap = mkOption {
      type = nullOr (submodule ldapOpts);
      default = null;
      description = ''
        LDAP auth server configuration. If omitted, the server will use local authentication.
      '';
    };
  };

  config = mkIf cfg.enable {

    services.prometheus.exporters.dovecot = mkIf cfg.monitoring {
      enable = true;
      scopes = ["user" "global"];
      listenAddress = "127.0.0.1";
      port = 9166;
      socketPath = "/var/run/dovecot2/old-stats";
    };

    services.dovecot2 = {
      enable = true;
      enableImap = true;
      enableLmtp = true;
      enablePop3 = true;
      enablePAM = cfg.dovecot.ldap == null;

      createMailUser = true;

      mailUser = cfg.mail-user;
      mailGroup = cfg.mail-group;
      mailLocation = "maildir:${cfg.mail-directory}/%u/";

      sslServerCert = cfg.dovecot.ssl-certificate;
      sslServerKey = cfg.dovecot.ssl-private-key;

      modules = [ pkgs.dovecot_pigeonhole ];
      protocols = [ "sieve" ];

      sieveScripts = {
        after = builtins.toFile "spam.sieve" ''
              require "fileinto";

              if header :is "X-Spam" "Yes" {
                fileinto "Junk";
                stop;
              }
            '';
      };

      mailboxes = cfg.mailboxes;

      extraConfig = ''
        #Extra Config

        ${optionalString cfg.monitoring ''
          # The prometheus exporter still expects an older style of metrics
          mail_plugins = $mail_plugins old_stats
          service old-stats {
            unix_listener old-stats {
              user = dovecot-exporter
              group = dovecot-exporter
            }
          }
        ''}

        ${lib.optionalString cfg.debug ''
          mail_debug = yes
          auth_debug = yes
          verbose_ssl = yes
        ''}

        protocol imap {
          mail_max_userip_connections = ${toString cfg.max-user-connections}
          mail_plugins = $mail_plugins imap_sieve
        }

        protocol pop3 {
          mail_max_userip_connections = ${toString cfg.max-user-connections}
        }

        protocol lmtp {
          mail_plugins = $mail_plugins sieve
        }

        mail_access_groups = ${cfg.mail-group}
        ssl = required

        # When looking up usernames, just use the name, not the full address
        auth_username_format = %n

        service lmtp {
          # Enable logging in debug mode
          ${optionalString cfg.debug "executable = lmtp -L"}

          # Unix socket for postfix to deliver messages via lmtp
          unix_listener dovecot-lmtp {
            user = "postfix"
            group = ${cfg.mail-group}
            mode = 0600
          }

          # Drop privs, since all mail is owned by one user
          # user = ${cfg.mail-user}
          # group = ${cfg.mail-group}
          user = root
        }

        auth_mechanisms = login plain

        ${optionalString (cfg.dovecot.ldap != null)
          (ldap-passwd-entry cfg.dovecot.ldap)}
        userdb {
          driver = static
          args = uid=${toString cfg.mail-user-id} home=${cfg.mail-directory}/%u
        }

        # Used by postfix to authorize users
        service auth {
          unix_listener auth {
            mode = 0660
            user = "${config.services.postfix.user}"
            group = ${cfg.mail-group}
          }

          unix_listener auth-userdb {
            mode = 0660
            user = "${config.services.postfix.user}"
            group = ${cfg.mail-group}
          }
        }

        service auth-worker {
          user = root
        }

        service imap {
          vsz_limit = 1024M
        }

        namespace inbox {
          separator = "/"
          inbox = yes
        }

        plugin {
          sieve_plugins = sieve_imapsieve sieve_extprograms
          sieve = file:/var/sieve/%u/scripts;active=/var/sieve/%u/active.sieve
          sieve_default = file:/var/sieve/%u/default.sieve
          sieve_default_name = default
          # From elsewhere to Spam folder
          imapsieve_mailbox1_name = Junk
          imapsieve_mailbox1_causes = COPY
          imapsieve_mailbox1_before = file:${state-directory}/imap_sieve/report-spam.sieve
          # From Spam folder to elsewhere
          imapsieve_mailbox2_name = *
          imapsieve_mailbox2_from = Junk
          imapsieve_mailbox2_causes = COPY
          imapsieve_mailbox2_before = file:${state-directory}/imap_sieve/report-ham.sieve
          sieve_pipe_bin_dir = ${pipe-bin}/pipe/bin
          sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.environment
        }

        recipient_delimiter = +

        lmtp_save_to_detail_mailbox = yes

        lda_mailbox_autosubscribe = yes
        lda_mailbox_autocreate = yes
      '';
    };

    systemd.services.dovecot2.preStart = ''
      mkdir -p '${state-directory}'
      chown ${dovecot-user}:${cfg.mail-group} '${state-directory}'
      rm -rf '${state-directory}/imap_sieve'
      mkdir '${state-directory}/imap_sieve'
      cp -p "${./dovecot/imap_sieve}"/*.sieve '${state-directory}/imap_sieve/'
      for k in "${state-directory}/imap_sieve"/*.sieve ; do
        ${pkgs.dovecot_pigeonhole}/bin/sievec "$k"
      done
      chown -R '${dovecot-user}:${cfg.mail-group}' '${state-directory}/imap_sieve'

      chown '${cfg.mail-user}:${cfg.mail-group}' ${cfg.mail-directory}
      chmod g+w ${cfg.mail-directory}
    '';
  };
}