598 lines
17 KiB
Nix
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";
|
|
};
|
|
}
|