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

# TODO: use blacklists

with lib;
let
  cfg = config.fudo.mail.rspamd;
  mailCfg = config.fudo.mail;

in {
  options.fudo.mail.rspamd = with types; {
    enable = mkEnableOption "Enable rspamd spam test server.";

    ports = {
      metrics = mkOption {
        type = port;
        default = 7573;
      };
      controller = mkOption {
        type = port;
        default = 11334;
      };
      milter = mkOption {
        type = port;
        default = 11335;
      };
    };

    antivirus = {
      host = mkOption {
        type = str;
        description = "Host of the ClamAV server.";
      };

      port = mkOption {
        type = port;
        description = "Port at which to reach ClamAV";
      };
    };

    redis.password = mkOption {
      type = str;
      description = "Password with which to connect to Redis.";
    };
  };

  config = mkIf cfg.enable {
    networking.firewall = {
      enable = true;
      allowedTCPPorts = with cfg.ports; [ metrics controller milter ];
      allowedUDPPorts = with cfg.ports; [ controller milter ];
    };

    services = {
      prometheus.exporters.rspamd = {
        enable = true;
        listenAddress = "127.0.0.1";
        port = cfg.ports.metrics;
      };

      rspamd = {
        enable = true;

        locals = {
          "milter_headers.conf".text = "extended_spam_headers = yes;";

          "antivirus.conf".text = ''
            clamav {
              action = "reject";
              symbol = "CLAM_VIRUS";
              type = "clamav";
              log_clean = true;
              servers = "${cfg.antivirus.host}:${toString cfg.antivirus.port}";
              scan_mime_parts = false; # scan mail as a whole unit, not parts. seems to be needed to work at all
            }
          '';

          "neural.conf".text = ''
            symbols = {
              "NEURAL_SPAM" = {
                weight = 3.0;
                description = "Neural network spam";
              }
              "NEURAL_HAM" = {
                weight = -3.0;
                description = "Neural network ham";
              }
            }
          '';

          "dmark.conf".text = ''
            dmarc = {
              servers = "redis";
              password = "${cfg.redis.password}";
            }
          '';

          "mx_check.conf".text = ''
            enabled = true;

            servers = "redis";
            password = "${cfg.redis.password}";

            timeout = 10.0;

            exclude_domains = [
              "https://maps.rspamd.com/freemail/disposable.txt.zst",
              "https://maps.rspamd.com/freemail/free.txt.zst",
            ];
          '';

          "reputation.conf".text = ''
            rules {
              ip_reputation = {
                selector "ip" {
                }
                backend "redis" {
                  servers = "redis";
                  password = "${cfg.redis.password}";
                }

                symbol = "IP_REPUTATION";
              }
              spf_reputation =  {
                selector "spf" {
                }
                backend "redis" {
                  servers = "redis";
                  password = "${cfg.redis.password}";
                }

                symbol = "SPF_REPUTATION";
              }
              dkim_reputation =  {
                selector "dkim" {
                }
                backend "redis" {
                  servers = "redis";
                  password = "${cfg.redis.password}";
                }

                symbol = "DKIM_REPUTATION"; # Also adjusts scores for DKIM_ALLOW, DKIM_REJECT
              }
              generic_reputation =  {
                selector "generic" {
                  selector = "ip"; # see https://rspamd.com/doc/configuration/selectors.html
                }
                backend "redis" {
                  servers = "redis";
                  password = "${cfg.redis.password}";
                }

                symbol = "GENERIC_REPUTATION";
              }
            }
          '';

          # SURBL checks URLs in emails against known-bad urls
          "rbl.conf".text = ''
            surbl {
              rules {
                "SURBL_MULTI" {
                  ignore_defaults = true; # for compatibility with old defaults
                  rbl = "multi.surbl.org";
                  checks = ['emails', 'dkim', 'urls'];
                  emails_domainonly = true;
                  urls = true;

                  returnbits = {
                    CRACKED_SURBL = 128; # From February 2016
                    ABUSE_SURBL = 64;
                    MW_SURBL_MULTI = 16;
                    PH_SURBL_MULTI = 8;
                    SURBL_BLOCKED = 1;
                  }
                }

                "URIBL_MULTI" {
                  ignore_defaults = true; # for compatibility with old defaults
                  rbl = "multi.uribl.com";
                  checks = ['emails', 'dkim', 'urls'];
                  emails_domainonly = true;

                  returnbits = {
                    URIBL_BLOCKED = 1;
                    URIBL_BLACK = 2;
                    URIBL_GREY = 4;
                    URIBL_RED = 8;
                  }
                }

                "RSPAMD_URIBL" {
                  ignore_defaults = true; # for compatibility with old defaults
                  rbl = "uribl.rspamd.com";
                  checks = ['emails', 'dkim', 'urls'];
                  # Also check images
                  images = true;
                  # Check emails for URLs
                  emails_domainonly = true;
                  # Hashed BL
                  hash = 'blake2';
                  hash_len = 32;
                  hash_format = 'base32';

                  returncodes = {
                    RSPAMD_URIBL = [
                      "127.0.0.2",
                    ];
                  }
                }

                "DBL" {
                  ignore_defaults = true; # for compatibility with old defaults
                  rbl = "dbl.spamhaus.org";
                  no_ip = true;
                  checks = ['emails', 'dkim', 'urls'];
                  emails_domainonly = true;

                  returncodes = {
                    # spam domain
                    DBL_SPAM = "127.0.1.2";
                    # phish domain
                    DBL_PHISH = "127.0.1.4";
                    # malware domain
                    DBL_MALWARE = "127.0.1.5";
                    # botnet C&C domain
                    DBL_BOTNET = "127.0.1.6";
                    # abused legit spam
                    DBL_ABUSE = "127.0.1.102";
                    # abused spammed redirector domain
                    DBL_ABUSE_REDIR = "127.0.1.103";
                    # abused legit phish
                    DBL_ABUSE_PHISH = "127.0.1.104";
                    # abused legit malware
                    DBL_ABUSE_MALWARE = "127.0.1.105";
                    # abused legit botnet C&C
                    DBL_ABUSE_BOTNET = "127.0.1.106";
                    # error - IP queries prohibited!
                    DBL_PROHIBIT = "127.0.1.255";
                    # issue #3074
                    DBL_BLOCKED_OPENRESOLVER = "127.255.255.254";
                    DBL_BLOCKED = "127.255.255.255";
                  }
                }

                "SEM_URIBL_UNKNOWN" {
                  ignore_defaults = true; # for compatibility with old defaults
                  rbl = "uribl.spameatingmonkey.net";
                  no_ip = true;
                  checks = ['emails', 'dkim', 'urls'];
                  emails_domainonly = true;
                  returnbits {
                    SEM_URIBL = 2;
                  }
                }
              }
            }
          '';
        };

        overrides."milter_headers.conf".text = "extended_spam_headers = true;";

        workers = {
          rspamd_proxy = {
            type = "rspamd_proxy";
            bindSockets = [ "*:${toString cfg.ports.milter}" ];
            count = 4;
            extraConfig = ''
              milter = yes;
              timeout = 120s;

              upstream "local" {
                default = yes;
                self_scan = yes;
              }
            '';
          };

          controller = {
            type = "controller";
            count = 4;
            bindSockets = [ "*:${toString cfg.ports.controller}" ];
            includes = [ ];
          };
        };
      };
    };
  };
}