Skip to content

Commit 1018c34

Browse files
authored
Merge pull request #38 from dyne/sieve-import
feat: import Jaromail sieve rules
2 parents afdbbfa + c422ff5 commit 1018c34

7 files changed

Lines changed: 293 additions & 8 deletions

File tree

.github/workflows/pr-tests.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ jobs:
2727
chmod +x extras/test/run-source.sh
2828
chmod +x extras/test/test-addressbook-parse.sh
2929
chmod +x extras/test/test-filtering.sh
30+
chmod +x extras/test/test-sieve-import.sh
3031
chmod +x extras/test/test-helpers.sh
3132
3233
- name: Build
@@ -41,5 +42,8 @@ jobs:
4142
- name: Filtering tests
4243
run: extras/test/test-filtering.sh
4344

45+
- name: Sieve import tests
46+
run: extras/test/test-sieve-import.sh
47+
4448
- name: Helper binary tests
4549
run: extras/test/test-helpers.sh

extras/shell_completion/_jaromail

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ _jaromail() {
1616

1717
case $state in
1818
commands)
19-
_arguments '1:Commands:(open compose fetch send peek search passwd abook extract import backup merge update filter)'
19+
_arguments '1:Commands:(open compose fetch send peek search passwd abook extract import backup merge update sieve sieve-import filter)'
2020
;;
2121
*)
2222
_last=$(( ${#words} - 1 ))
@@ -55,6 +55,10 @@ _jaromail() {
5555
_multi_parts . md
5656
;;
5757

58+
sieve-import)
59+
_files -g '*.sieve(-.)'
60+
;;
61+
5862
compose)
5963
_adrs=("${(@f)$(jaro search addr . 2>/dev/null)}")
6064
compadd -X "Recipient addresses:" -a _adrs

extras/shell_completion/jaromail.bash

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ _jaro() { #+ starts with an underscore.
1616

1717
case "$cur" in
1818
*)
19-
COMPREPLY=( $( compgen -W 'fetch send peek compose open' -- $cur ) )
19+
COMPREPLY=( $( compgen -W 'fetch send peek compose open update sieve sieve-import filter' -- $cur ) )
2020
;;
2121

2222
open)
@@ -39,4 +39,3 @@ echo "current array: ${COMP_WORDS[@]}"
3939
}
4040

4141
complete -F _jaro -o filenames jaro
42-

extras/test/test-sieve-import.sh

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
#!/usr/bin/env zsh
2+
3+
set -euo pipefail
4+
5+
script_dir="${0:A:h}"
6+
source "${script_dir}/lib/test_helpers.zsh"
7+
test_setup "${0}"
8+
9+
cleanup() {
10+
test_cleanup
11+
}
12+
trap cleanup EXIT INT TERM
13+
14+
missing_cmds=()
15+
for req in pinentry fetchmail gpg msmtp notmuch abook; do
16+
command -v "${req}" >/dev/null 2>&1 || missing_cmds+=("${req}")
17+
done
18+
if (( ${#missing_cmds[@]} > 0 )); then
19+
print -- "SKIP test-sieve-import.sh: missing runtime commands: ${missing_cmds[*]}"
20+
exit 0
21+
fi
22+
23+
jaro_source init >/dev/null
24+
: > "${mail_root}/Filters.txt"
25+
: > "${mail_root}/Aliases.txt"
26+
27+
sieve_file="${tmp_root}/import.sieve"
28+
cat > "${sieve_file}" <<'EOF'
29+
# mailbox supports fileinto :create
30+
require ["fileinto","mailbox","variables"];
31+
32+
# zz.blacklist
33+
if header :contains "From" [
34+
"blocked@example.org",
35+
"blocked@example.org"
36+
]
37+
{ fileinto :create "zz.blacklist"; stop; }
38+
39+
# bounces
40+
if header :contains "Sender" "mailman-bounce" {
41+
fileinto :create "zz.bounces";
42+
stop;
43+
}
44+
45+
#############
46+
# own filters
47+
48+
if header :contains [ "To","Cc" ] "team@example.org" { fileinto :create "team"; stop; }
49+
if header :contains [ "From","Sender" ] "list@example.org" { fileinto :create "lists"; stop; }
50+
51+
# INBOX
52+
if header :contains [ "From","Sender" ] [
53+
"Known Person <known@example.org>",
54+
"known@example.org"
55+
]
56+
{ fileinto :create "INBOX"; stop; }
57+
58+
# spam
59+
if header :is "X-Spam-Flag" "YES" {
60+
fileinto :create "Spam"; stop;
61+
}
62+
63+
# priv
64+
if header :contains [ "To","Cc" ] [
65+
"alias@example.org",
66+
"unknown@gmail.com"
67+
]
68+
{ fileinto :create "priv"; stop; }
69+
70+
fileinto :create "unsorted";
71+
EOF
72+
73+
jaro_source sieve-import "${sieve_file}" >/dev/null
74+
jaro_source sieve-import "${sieve_file}" >/dev/null
75+
76+
filters="$(grep -v '^#' "${mail_root}/Filters.txt" | grep -v '^$' | sort)"
77+
assert_equal "${filters}" $'from list@example.org move lists\nto team@example.org move team' "import filters"
78+
79+
alias_lines="$(grep -v '^#' "${mail_root}/Aliases.txt" | grep -v '^$' | sort)"
80+
assert_equal "${alias_lines}" "alias@example.org" "import aliases"
81+
82+
whitelist="$(jaro_source addr 2>/dev/null | sort)"
83+
assert_equal "${whitelist}" "Known Person <known@example.org>" "import whitelist"
84+
85+
blacklist="$(jaro_source -l blacklist addr 2>/dev/null | sort)"
86+
assert_equal "${blacklist}" "blocked <blocked@example.org>" "import blacklist"
87+
88+
jaro_source update >/dev/null
89+
90+
generated="$(cat "${mail_root}/Filters.sieve")"
91+
assert_contains "${generated}" '"team@example.org" { fileinto :create "team"; stop; }' "round-trip to filter"
92+
assert_contains "${generated}" '"list@example.org" { fileinto :create "lists"; stop; }' "round-trip from filter"
93+
assert_contains "${generated}" '"alias@example.org"' "round-trip alias"
94+
assert_contains "${generated}" '"known@example.org"' "round-trip whitelist"
95+
assert_contains "${generated}" '"blocked@example.org"' "round-trip blacklist"

src/jaro

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,13 @@ a pipe | in front indicate they take an email body from stdin
108108
update updates all filter engine according to Filters.txt
109109
(also generates Sieve format rules ready for server use)
110110
111+
sieve regenerate $MAILDIRS/Filters.sieve from local filters,
112+
aliases, and addressbooks without updating the mail index
113+
114+
sieve-import FILE
115+
import a Jaromail-generated Sieve file into Filters.txt,
116+
Aliases.txt, whitelist.abook, and blacklist.abook
117+
111118
filter process a maildir distributing emails according to Filters.txt
112119
(if none specified, processes incoming/ - called by fetch)
113120
@@ -136,6 +143,7 @@ main() {
136143

137144
option_subcommands[update]=""
138145
option_subcommands[sieve]=""
146+
option_subcommands[sieve-import]=""
139147

140148
option_subcommands[stat]=""
141149

@@ -269,6 +277,8 @@ main() {
269277

270278
sieve) cmd_sieve ;;
271279

280+
sieve-import) cmd_sieve_import ;;
281+
272282
help) cmd_help ;;
273283

274284
index) cmd_index ;;

src/zlibs/filters

Lines changed: 173 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -105,12 +105,16 @@ update_filters() {
105105
newlock "$ff"
106106
cat <<EOF >> "$ff"
107107
# generated on `date`
108-
typeset -Al filter_from
109-
typeset -alU filter_own
110-
typeset -Al filter_to
108+
typeset -gAl filter_from
109+
typeset -galU filter_own
110+
typeset -gAl filter_to
111+
typeset -galU filter_whitelist
112+
typeset -galU filter_blacklist
111113
filter_from=()
112114
filter_own=()
113115
filter_to=()
116+
filter_whitelist=()
117+
filter_blacklist=()
114118
115119
EOF
116120

@@ -587,10 +591,16 @@ if header :contains "Sender" "mailman-bounce" {
587591
588592
EOF
589593

590-
set -A sieve_filter_map ${(kv)filter_to}
594+
sieve_filter_map=()
595+
for fil in ${(k)filter_to}; do
596+
sieve_filter_map[$fil]="${filter_to[$fil]}"
597+
done
591598
sieve_complex_filter 'if header :contains [ "To","Cc" ] '
592599

593-
set -A sieve_filter_map ${(kv)filter_from}
600+
sieve_filter_map=()
601+
for fil in ${(k)filter_from}; do
602+
sieve_filter_map[$fil]="${filter_from[$fil]}"
603+
done
594604
sieve_complex_filter 'if header :contains [ "From","Sender" ] '
595605

596606
##############################################################
@@ -635,6 +645,164 @@ EOF
635645
return 0
636646
} # end of update()
637647

648+
sieve_import_addressbook_entry() {
649+
local target_list="$1"
650+
local raw_address="$2"
651+
local old_list="$list"
652+
local old_addressbook="$ADDRESSBOOK"
653+
654+
list="$target_list"
655+
ADDRESSBOOK="${target_list}.abook"
656+
[[ -r "$MAILDIRS/$ADDRESSBOOK" ]] || create_addressbook
657+
658+
e_addr=()
659+
print - "From: $raw_address" | e_parse From > /dev/null || {
660+
list="$old_list"
661+
ADDRESSBOOK="$old_addressbook"
662+
return 1
663+
}
664+
665+
local email name
666+
for email in ${(k)e_addr}; do
667+
name="${e_addr[$email]}"
668+
[[ -z "$name" ]] && name="$email"
669+
lookup_email "$email"
670+
[[ $? = 0 ]] || insert_address "$email" "$name" > /dev/null
671+
done
672+
673+
list="$old_list"
674+
ADDRESSBOOK="$old_addressbook"
675+
return 0
676+
}
677+
678+
sieve_import_grouped_values() {
679+
local sieve_file="$1"
680+
awk '
681+
/^# / {
682+
section=substr($0, 3)
683+
next
684+
}
685+
/^[[:space:]]*"/ {
686+
value=$0
687+
sub(/^[[:space:]]*"/, "", value)
688+
sub(/".*/, "", value)
689+
print section "|" value
690+
}
691+
' "$sieve_file"
692+
}
693+
694+
sieve_import_route_rules() {
695+
local sieve_file="$1"
696+
awk '
697+
/header :contains/ && /fileinto :create/ {
698+
destination=$0
699+
sub(/^.*fileinto :create "/, "", destination)
700+
sub(/".*/, "", destination)
701+
702+
if (destination == "INBOX" || destination == "priv" ||
703+
destination == "Spam" || destination == "unsorted" ||
704+
destination == "zz.blacklist" || destination == "zz.bounces") {
705+
next
706+
}
707+
708+
if ($0 ~ /\[ "To","Cc" \]/) {
709+
header="to"
710+
} else if ($0 ~ /\[ "From","Sender" \]/) {
711+
header="from"
712+
} else {
713+
next
714+
}
715+
716+
pattern=$0
717+
sub(/^.*\][[:space:]]*"/, "", pattern)
718+
sub(/".*/, "", pattern)
719+
if (pattern != "") {
720+
print header " " pattern " move " destination
721+
}
722+
}
723+
' "$sieve_file"
724+
}
725+
726+
sieve_import_append_unique() {
727+
local target_file="$1"
728+
local line="$2"
729+
730+
touch "$target_file"
731+
grep -Fqx "$line" "$target_file" 2>/dev/null || print - "$line" >> "$target_file"
732+
}
733+
734+
sieve_import_is_account_address() {
735+
local needle="${1:l}"
736+
737+
[[ -d "$MAILDIRS/Accounts" ]] || return 1
738+
find "$MAILDIRS/Accounts" -type f -exec awk '/^email/ { print tolower($2) }' {} \; | \
739+
grep -Fqx "$needle"
740+
}
741+
742+
sieve_import_alias_entry() {
743+
local raw_address="$1"
744+
745+
e_addr=()
746+
print - "From: $raw_address" | e_parse From > /dev/null || return 1
747+
748+
local email
749+
for email in ${(k)e_addr}; do
750+
sieve_import_is_account_address "$email" && continue
751+
sieve_import_append_unique "$MAILDIRS/Aliases.txt" "$email"
752+
done
753+
return 0
754+
}
755+
756+
sieve_import() {
757+
fn sieve_import $*
758+
local sieve_file="$1"
759+
req=(MAILDIRS sieve_file)
760+
freq=($sieve_file)
761+
ckreq || return 1
762+
763+
[[ -r "$sieve_file" ]] || {
764+
error "Sieve file not readable: $sieve_file"
765+
return 1
766+
}
767+
768+
notice "Import Sieve filters from $sieve_file"
769+
local section value entry
770+
local imported_blacklist=0
771+
local imported_whitelist=0
772+
local imported_filters=0
773+
local imported_aliases=0
774+
775+
for entry in ${(f)"$(sieve_import_grouped_values "$sieve_file")"}; do
776+
section="${entry%%|*}"
777+
value="${entry#*|}"
778+
case "$section" in
779+
zz.blacklist)
780+
sieve_import_addressbook_entry blacklist "$value" && \
781+
imported_blacklist=$(( $imported_blacklist + 1 ))
782+
;;
783+
INBOX)
784+
sieve_import_addressbook_entry whitelist "$value" && \
785+
imported_whitelist=$(( $imported_whitelist + 1 ))
786+
;;
787+
priv)
788+
sieve_import_alias_entry "$value" && \
789+
imported_aliases=$(( $imported_aliases + 1 ))
790+
;;
791+
esac
792+
done
793+
794+
for entry in ${(f)"$(sieve_import_route_rules "$sieve_file")"}; do
795+
sieve_import_append_unique "$MAILDIRS/Filters.txt" "$entry"
796+
imported_filters=$(( $imported_filters + 1 ))
797+
done
798+
799+
act "blacklist addresses imported: $imported_blacklist"
800+
act "whitelist addresses imported: $imported_whitelist"
801+
act "aliases imported: $imported_aliases"
802+
act "filter rules imported: $imported_filters"
803+
return 0
804+
}
805+
638806
cmd_filter() {
639807
filter_maildir ${option_params}
640808
exitcode=$?

src/zlibs/setup

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,8 @@ cmd_sieve() {
4545
update_sieve
4646
notice "Sieve filters updated in $MAILDIRS"
4747
}
48+
49+
cmd_sieve_import() {
50+
sieve_import ${option_params}
51+
exitcode=$?
52+
}

0 commit comments

Comments
 (0)