Files
nix-stuff/liam/sieve.nix
Shelvacu 9bad53f188 nix fmt
2025-04-19 13:17:36 -07:00

598 lines
17 KiB
Nix

{
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" "*<jmp-news.soprani.ca>")}
${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";
};
}