{ pkgs, lib, config, ... }: let inherit (builtins) isString isList length head all isInt isAttrs isFloat isBool ; inherit (lib) concatStrings concatStringsSep splitString match replaceStrings reverseList elemAt mapAttrsToList ; mapConcat = f: xs: concatStrings (map f xs); mapConcatSep = sep: f: xs: concatStringsSep sep (map f xs); mapConcatLines = f: xs: mapConcatSep "\n" f xs; isListWhere = xs: f: (isList xs) && (all f xs); stringOrList = val: (isString val) || ((isListWhere val isString) && (length val) > 0); listify = val: if isList val then val else [ val ]; email_folders = [ "24nm-domain@shelvacu.com" "agora@shelvacu.com" "crys-weekly@shelvacu.com" "genshin@shelvacu.com" "paxful@shelvacu.com" "postgres-lists@shelvacu.com" "cpapsupplies@shelvacu.com" "jork@shelvacu.com" "bob@dis8.net" "fresh.avocado@dis8.net" "pickle-jar-right-here@dis8.net" "contact@jean-luc.org" "discord@jean-luc.org" "henhenry@jean-luc.org" "jean-luc@jean-luc.org" "mariceayukawa@jean-luc.org" "snow@jean-luc.org" "capt@in.jean-luc.org" ]; domain_folders = [ "dis8.net" "shelvacu.com" "jean-luc.org" "in.jean-luc.org" "mail.dis8.net" "liam.dis8.net" # no longer used: "tulpaudcast.info" "tulpae.info" "xn--tulp-yoa.info" ]; valid_ish_domain = domain: match "[a-z0-9][a-z0-9-]*(\\.[a-z0-9][a-z0-9-]*)+" domain != null; mk_domain_folder_name = domain: assert valid_ish_domain domain; concatStringsSep "." (reverseList (splitString "." domain)); mk_email_folder_name = email: let parts = splitString "@" email; domain_part = assert (length parts) == 2; elemAt parts 1; user_part = assert (length parts) == 2; elemAt parts 0; domain_folder = mk_domain_folder_name domain_part; folder_name = domain_folder + ".@" + user_part; in folder_name; is_match = regex: s: (match regex s) != null; is_not_match = regex: s: !(is_match regex s); only_printable_ascii = s: is_match "[ -~\r\n]*" s; has_vars = s: lib.hasInfix ("$" + "{") s; # is_quoteable = s: (only_printable_ascii s) && (!lib.hasInfix ("$" + "{") s); sieve_raw_escape_string = s: if !only_printable_ascii s then builtins.trace s throw "s failed only_printable_ascii check" else replaceStrings [ ''"'' ''\'' "\n" "\r" ] [ ''\"'' ''\\'' ''\n'' ''\r'' ] s; sieve_encode_string = { allow_vars, for_debug_comment, with_quotes, }: s: assert isString s; assert allow_vars || for_debug_comment || (!has_vars s); let a = sieve_raw_escape_string s; b = if for_debug_comment then replaceStrings [ ''*/'' ] [ ''*\/'' ] a else a; res = if with_quotes then ''"${b}"'' else b; in res; sieve_quote_string = sieve_encode_string { allow_vars = false; for_debug_comment = false; with_quotes = true; }; sieve_quote_string_with_interp = sieve_encode_string { allow_vars = true; for_debug_comment = false; with_quotes = true; }; is_valid_long_ident = is_match "[a-z_][a-z0-9_]*"; is_number_ident = is_match "[0-9]*"; is_valid_ident = s: (is_valid_long_ident s) || (is_number_ident s); interp = ident: assert isString ident; assert is_valid_ident ident; "$" + "{${ident}}"; dest = "envelope_to"; dest_domain = "envelope_to_domain"; set_envelope = '' #set_envelope START if header :index 1 :matches "X-Envelope-To" "*" { set ${sieve_quote_string dest} "''${1}"; } if header :index 1 :matches "X-Envelope-To" "*@*" { set ${sieve_quote_string dest_domain} "''${2}"; } #set_envelope END ''; envelope_is = key: ''string :is "${interp dest}" ${sieve_quote_string key}''; envelope_domain_is = key: ''string :is "${interp dest_domain}" ${sieve_quote_string key}''; sieve_encode_list = xs: assert isListWhere xs isString; "[ ${mapConcatSep ", " sieve_encode xs} ]"; sieve_encode = val: if isString val then sieve_quote_string val else if isList val then sieve_encode_list val else assert "dunno what to do with this"; null; sieve_debug_list = xs: "[ ${mapConcat (s: (sieve_debug s) + " ") xs}]"; sieve_debug_attrs = attrs: let toPairStr = name: val: "${sieve_debug name} = ${sieve_debug val}; "; pairStrs = mapAttrsToList toPairStr attrs; pairsStr = concatStrings pairStrs; in "{ ${pairsStr}}"; sieve_debug = val: if isString val then sieve_encode_string { allow_vars = true; for_debug_comment = true; with_quotes = true; } val else if (isInt val) || (isFloat val) then toString val else if (isBool val) then (if val then "true" else "false") else if isNull val then "null" else if isList val then sieve_debug_list val else if isAttrs val then sieve_debug_attrs val else assert "dunno what to do with this"; null; pure_flags_impl = flags: conditions: assert isListWhere flags isString; assert isListWhere conditions isString; assert (length flags) > 0; assert (length conditions) > 0; let argAttrs = { inherit flags conditions; }; firstFlag = head flags; combined_condition = if (length conditions) == 1 then head conditions else (allof conditions); in '' # pure_flags ${sieve_debug argAttrs}; removeflag ${sieve_quote_string firstFlag}; if ${combined_condition} { ${record_action "pure_flags ${concatStringsSep " " flags}"} ${concatStringsSep "\n" (map (flag: ''addflag ${sieve_quote_string flag};'') flags)} } # pure_flags end ''; pure_flags = flags: conditions: assert stringOrList flags; assert stringOrList conditions; pure_flags_impl (listify flags) (listify conditions); exists_impl = headers: assert isListWhere headers isString; if headers == [ ] then "/* exists START: called with empty array */ false /* exists END */" else "/* exists START */ exists ${sieve_encode_list headers} /* exists END */"; exists = headers: assert stringOrList headers; exists_impl (listify headers); header_generic = match_kind: header_s: match_es: assert stringOrList header_s; assert stringOrList match_es; ''/* header_generic START */ header ${match_kind} ${sieve_encode header_s} ${sieve_encode match_es} /* header_generic END */''; header_matches = header_generic ":matches"; header_is = header_generic ":is"; subject_generic = match_kind: match_es: header_generic match_kind "Subject" match_es; subject_matches = subject_generic ":matches"; subject_is = subject_generic ":is"; environment_generic = match_kind: environment_name_s: match_es: assert stringOrList environment_name_s; assert stringOrList match_es; "environment ${match_kind} ${sieve_encode environment_name_s} ${sieve_encode match_es}"; environment_matches = environment_generic ":matches"; environment_is = environment_generic ":is"; from_is = addr_list: assert stringOrList addr_list; ''/* from_is START */ address :is :all "From" ${sieve_encode addr_list} /* from_is END */''; var_is = var_name: rhs: assert isString var_name; assert stringOrList rhs; ''string :is "''${${var_name}}" ${sieve_encode rhs}''; var_is_true = var_name: var_is var_name "1"; var_is_false = var_name: not (var_is_true var_name); set_with_interp = var_name: new_val: assert isString var_name; assert is_valid_ident var_name; assert isString new_val; "set ${sieve_encode var_name} ${sieve_quote_string_with_interp new_val};"; set = var_name: new_val: assert isString var_name; assert is_valid_ident var_name; assert isString new_val; "set ${sieve_encode var_name} ${sieve_encode new_val};"; set_bool_var = var_name: bool_val: assert isBool bool_val; set var_name (if bool_val then "1" else "0"); over_test_list = name: test_list: assert isListWhere test_list isString; '' ${name}( ${concatStringsSep ",\n" test_list} ) ''; anyof = over_test_list "anyof"; allof = over_test_list "allof"; not = test: "not ${test}"; record_action = action_desc: assert isString action_desc; ''addheader "X-Vacu-Action" ${sieve_encode action_desc};''; fileinto = folder: assert isString folder; '' ${record_action "fileinto ${folder}"} fileinto :create ${sieve_encode folder}; ''; ihave = extension_name_s: assert stringOrList extension_name_s; "ihave ${sieve_encode extension_name_s}"; email_filters = map (e: '' elsif ${envelope_is e} { # item of email_filters ${record_action "email_filters fileinto ${mk_email_folder_name e}"} fileinto :create ${sieve_quote_string (mk_email_folder_name e)}; } '') email_folders; domain_filters = map (d: '' elsif ${envelope_domain_is d} { # item of domain_filters ${record_action "domain_filters fileinto ${mk_domain_folder_name d}"} fileinto :create ${sieve_quote_string (mk_domain_folder_name d)}; } '') domain_folders; set_from = { condition, var, default ? "-", warn_if_unset ? false, }@args: '' # set_from ${sieve_debug args} if ${condition} { ${set_with_interp var (interp "1")} } else { ${lib.optionalString warn_if_unset ( maybe_debug "info: Could not set ${var} from condition ${condition}, setting to default(${default})" )} ${set var default} } # set_from END ''; set_var_from_environment = item: var: '' # set_var_from_environment '' + set_from { condition = ''environment :matches ${sieve_quote_string item} "*"''; inherit var; }; maybe_debug = msg: '' if ${ihave "vnd.dovecot.debug"} { debug_log ${sieve_quote_string_with_interp msg}; } ''; sieve_text = '' require [ "fileinto", "mailbox", "imap4flags", "editheader", "environment", "variables", "date", "index", "ihave" ]; if ${ allof [ (ihave "imapsieve") (environment_matches "imap.user" "*") (environment_matches "location" "MS") (environment_matches "phase" "post") ] } { ${set_bool_var "in_imap" true} } else { ${set_bool_var "in_imap" false} } if ${var_is_true "in_imap"} { if ${ not (allof [ (environment_is "imap.cause" [ "APPEND" "COPY" "" ]) (environment_is "imap.mailbox" [ "MagicRefilter" "" ]) ]) } { ${maybe_debug "NOT doing anything cuz imap.cause and/or imap.mailbox isn't right"} stop; } } ${set_envelope} ${set_var_from_environment "location" "env_location"} ${set_var_from_environment "phase" "env_phase"} ${set_var_from_environment "imap.user" "env_imap_user"} ${set_var_from_environment "imap.email" "env_imap_email"} ${set_var_from_environment "imap.cause" "env_imap_cause"} ${set_var_from_environment "imap.mailbox" "env_imap_mailbox"} ${set_var_from_environment "imap.changedflags" "env_imap_changedflags"} ${set_from { condition = ''currentdate :matches "iso8601" "*"''; var = "datetime"; }} ${set_with_interp "sieved_message" ''at ''${datetime} by ${config.vacu.versionId} loc ''${env_location} phase ''${env_phase} user ''${env_imap_user} email ''${env_imap_email} cause ''${env_imap_cause} mailbox ''${env_imap_mailbox} changedflags ''${env_imap_changedflags} envelope ''${dest}''} ${maybe_debug ''X-Vacu-Sieved: ''${sieved_message}''} if ${ihave "envelope"} { if envelope :all :matches "to" "*@*" { ${set_with_interp "userfor" (interp "1")} } else { error "i dunno what to do, theres no envelope"; } } elsif ${var_is_true "in_imap"} { ${set_with_interp "userfor" (interp "env_imap_user")} } else { error "dont have envelope or imapsieve, dunno what to do"; } if ${var_is "userfor" "shelvacu"} { addheader "X-Vacu-Sieved" "''${sieved_message}"; removeflag "ignore"; removeflag "not-spamish"; ${pure_flags [ "amazon-ignore" "ignore" ] [ (envelope_is "amznbsns@shelvacu.com") (subject_matches [ "Your Amazon.com order has shipped*" "Your Amazon.com order of * has shipped!" ]) ] } ${pure_flags [ "bandcamp-ignore" "ignore" ] [ (envelope_is "bandcamp@shelvacu.com") (subject_matches [ "* just announced a listening party on Bandcamp" "New items from *" "Starting in *" "New from *" ]) ] } ${pure_flags [ "ika-ignore" "ignore" ] (envelope_is "ika@dis8.net")} ${pure_flags [ "ally-statement" "ignore" ] [ (envelope_is "ally@shelvacu.com") (subject_is "Your latest statement is ready to view.") ] } ${pure_flags "bloomberg" (envelope_is "bloomberg@shelvacu.com")} ${pure_flags [ "money-stuff" "not-spamish" ] [ (envelope_is "bloomberg@shelvacu.com") ''header :matches "From" "\"Matt Levine\" *"'' ] } ${pure_flags [ "git" "not-spamish" ] (exists [ "X-GitHub-Reason" "X-GitLab-Project" ])} ${pure_flags [ "git-uninsane" "git" "not-spamish" ] (envelope_is "git-uninsane@shelvacu.com")} ${pure_flags [ "discourse" "not-spamish" ] (exists "X-Discourse-Post-Id")} ${pure_flags [ "agora" "not-spamish" ] (envelope_is "agora@shelvacu.com")} ${pure_flags [ "postgres-list" "not-spamish" ] ( header_matches "List-Id" "<*.lists.postgresql.org>" )} ${pure_flags [ "secureaccesswa" "not-spamish" ] (from_is "help@secureaccess.wa.gov")} ${pure_flags [ "letsencrypt-mailing-list" "not-spamish" ] ( envelope_is "lets-encrypt-mailing-list@shelvacu.com" )} ${pure_flags [ "jmp-news" "not-spamish" ] (header_matches "List-Id" "*")} ${pure_flags [ "tf2wiki" "not-spamish" ] [ (envelope_is "tf2wiki@shelvacu.com") (from_is "noreply@wiki.teamfortress.com") ] } ${pure_flags "gmail-fwd" (envelope_is "gmailfwd-fc2e10bec8b2@shelvacu.com")} ${pure_flags "aliexpress" (from_is [ "transaction@notice.aliexpress.com" "aliexpress@notice.aliexpress.com" ])} removeflag "auto-marked-read"; if hasflag "ignore" { ${record_action "auto-mark-read"} addflag "\\Seen"; addflag "auto-marked-read"; } ${pure_flags "spamish" [ (anyof [ (header_is "Precedence" "bulk") (exists "List-Unsubscribe") (exists "List-Unsubscribe-Post") ]) ''not hasflag "not-spamish"'' ]} if ${envelope_is "brandcrowd@shelvacu.com"} { discard; } elsif allof ( ${envelope_domain_is "shelvacu.com"}, hasflag "spamish" ) { ${fileinto "com.shelvacu.#spamish"} } elsif hasflag "aliexpress" { ${fileinto "aliexpress"} } elsif hasflag "gmail-fwd" { ${fileinto "gmail"} } elsif hasflag "money-stuff" { ${fileinto "com.shelvacu.#money-stuff"} } ${concatStrings email_filters} ${concatStrings domain_filters} else { keep; } } # disable any sieve scripts that might want to run after this one stop; ''; pigeonhole_pkg = pkgs.dovecot_pigeonhole; in { imports = [ # Allow running a sieve filter when a message gets moved to another folder in imap # see https://doc.dovecot.org/2.3/configuration_manual/sieve/plugins/imapsieve/ { services.dovecot2 = { sieve.plugins = [ "sieve_imapsieve" ]; mailPlugins.perProtocol.imap.enable = [ "imap_sieve" ]; }; } ]; options.vacu.checkSieve = lib.mkOption { readOnly = true; default = pkgs.writeScriptBin "check-liam-sieve" '' set -xev ${lib.escapeShellArgs [ (lib.getExe' pigeonhole_pkg "sieve-test") "-c" config.services.dovecot2.configFile "-C" # force compilation "-D" # enable sieve debugging "-f" "some-rando@example.com" "-a" "shelvacu@liam.dis8.net" config.services.dovecot2.sieve.scripts.before "/dev/null" ]} ''; }; options.vacu.liam-sieve-script = lib.mkOption { readOnly = true; default = pkgs.writeText "mainsieve" sieve_text; }; config = { services.dovecot2.modules = [ pigeonhole_pkg ]; services.dovecot2.sieve = { extensions = [ "fileinto" "mailbox" "editheader" "vnd.dovecot.debug" ]; scripts.before = config.vacu.liam-sieve-script; }; services.dovecot2.imapsieve.mailbox = [ { name = "*"; causes = [ "APPEND" "COPY" "FLAG" ]; before = config.vacu.liam-sieve-script; } ]; # services.dovecot2.mailboxes."magic-refilter".auto = "create"; }; }