. "$BST_LIB" # procmail 3.22 -- the mail filter whose whole acceptance path is a message file # plus an rcfile in, a folder and a log out. No MTA is involved here and no live # spool is locked: every run below is `procmail -m ` reading one message # on stdin, which is procmail's documented general-filter mode. # # THE PACKAGE SHIPS FOUR PROGRAMS AND THREE OF THEM ARE THE TICKET'S: procmail, # formail (the header/stream editor that also splits an mbox), mailstat (a # /bin/sh script that summarises procmail's LOGFILE), and lockfile, which comes # along with them. # # WHAT IS ASSERTED ABOUT DELIVERY IS "AND NOWHERE ELSE", NOT ONLY "THERE". A # filter that copied every message to every folder would satisfy a suite that # only looked in the folder it expected, so each delivery run below reads the # WHOLE maildir back and pins the exact set of files in it. That is also what # makes the fall-through case mean anything: the non-matching message has to # reach DEFAULT *and* leave the matched folder's contents untouched. # # THE TWO LOCKING MODES ARE SEPARATED ON PURPOSE. `:0:` asks procmail for a # folder lockfile, which it takes by creating a unique file and link()ing it onto # .lock; `:0` asks for none. The guest root is a cloud virtio-fs, and # link() is exactly the operation a filesystem of that shape is most likely not # to have -- so a red that lands on the locked cases while the unlocked ones stay # green is a filesystem finding with a name, not "procmail is broken". Both modes # deliver the same fixture so the pair is its own control. # # EVERY EXPECTATION WAS MEASURED, 2026-08-19, against a native x86_64 control: # the same procmail-3.22 source under the same seven meta-oe patches, driven with # the same two fixtures and the same rcfiles. The folder byte counts, the log # lines and mailstat's table below are that build's output. # # THE BYTE COUNTS ARE CROSS-CHECKED, NOT ONLY PINNED. mailstat's per-folder # totals are asserted against the folders' own sizes on disk as well as against # the measured constants, because the constants go stale the day the fixture is # edited and the cross-check does not -- and because a mailstat that echoed the # log without reading it would satisfy the constants alone. # # autoconf.h IS THE BUILD HOST'S ANSWERS AND THAT IS UPSTREAM'S DESIGN, NOT A # DEFECT OF THIS PORT. procmail has no GNU configure: it generates autoconf.h by # compiling and RUNNING probe programs, so the meta-oe recipe builds them with # BUILD_CC and the file records what the x86_64 builder answered. Only two of # those answers reach the target at all -- the locking strategies and # MAILSPOOLDIR -- and both are asserted below rather than assumed, which is the # only way a wrong one would ever be visible. The user-facing PATH is NOT among # them: the Debian patch series hardcodes DEFPATH in config.h, so the probed # defPATH is compiled in and never read. # # THE FIXTURES ARE COPIED TO TMPFS BEFORE ANYTHING READS THEM, AND THAT IS NOT # TIDINESS. A top-level shell redirection whose source is on the guest's # virtio-fs root can trap a hwjs lane; containment then returns a restart code # and glibc leaks the kernel's ERESTARTNOINTR (513) into errno, so the shell # reports "Unknown error 513" on a redirection it could not set up and, being # non-interactive, EXITS -- taking the rest of the file with it. That is # linux-in-the-browser#169, and it cost this suite 34 of its 42 cases on the # first two on-target runs. # # Measured on target, 2026-08-19, with tmpfs as the null control: twenty # top-level `cat < ` redirections against a TMPFS copy failed 0 times, # while the same twenty against the virtio-fs original ended the script. So the # fixtures are staged once through `cat src > dst` -- where the virtio-fs read # happens in a CHILD, which is not the affected path -- and every redirection # below reads the tmpfs copy. `cat "$MSG" | procmail` would also work and is how # an MTA invokes it, but the redirection is what the ticket asks for and, from # tmpfs, it is what runs. # # SPAWN BUDGET. exec on this arm is a fresh wasm module instantiation. This file # spends 4 procmail runs, 9 formail runs and 1 mailstat -- and mailstat is a # shell script that spawns a dozen more of its own, which is why it is the last # thing this suite does and why its cost is called out here rather than # discovered later. require procmail procmail/present tmp=${TMPDIR:-/tmp} W=$tmp/bst-procmail.$$ MD=$W/mail rm -rf "$W"; mkdir -p "$MD" "$W/split" "$W/nolock" # Staged to tmpfs -- see the note above; nothing below reads $BST_DATA directly. MSG=$W/msg.txt MBOX=$W/mbox.txt cat "$BST_DATA/procmail-msg.txt" > "$MSG" 2>/dev/null cat "$BST_DATA/procmail-mbox.txt" > "$MBOX" 2>/dev/null # pm_bail ... : emit each id as red under one reason. # # A suite that RETURNS EARLY reports a shorter suite, and a shorter suite is # indistinguishable in the summary from a smaller one -- the reviewer comparing # totals sees 20 of 34 and cannot tell a bail-out from a file that never had # those cases. So every exit below goes through this and the count stays 34. pm_bail() { _reason=$1; shift for _id in "$@"; do bst_notok "$_id" "$_reason"; done } # pm_ls : the sorted, space-separated basenames in . This is what the # "and nowhere else" cases compare, so it must not fork per entry. pm_ls() { _out= for _f in "$1"/* "$1"/.*; do case "$_f" in *'/*'|*'/.*'|*/.|*/..) continue ;; esac _b=${_f##*/} if [ -z "$_out" ]; then _out=$_b; else _out="$_out $_b"; fi done printf '%s' "$_out" } PM_DELIVER_CASES="procmail/match-rc procmail/match-folder-nonempty procmail/match-only-alpha procmail/match-headers-kept procmail/match-body-kept procmail/match-message-id-kept procmail/nomatch-rc procmail/nomatch-to-default procmail/nomatch-not-in-alpha procmail/nomatch-body-kept procmail/deliver-set-is-alpha-and-inbox" PM_LOG_CASES="procmail/log-created procmail/log-abstract-alpha procmail/log-abstract-default procmail/log-two-deliveries" PM_STAT_CASES="procmail/mailstat-rc procmail/mailstat-header procmail/mailstat-alpha-count procmail/mailstat-inbox-count procmail/mailstat-total-count procmail/mailstat-alpha-size-matches-disk procmail/mailstat-inbox-size-matches-disk procmail/mailstat-total-is-sum" PM_NOLOCK_CASES="procmail/nolock-rc procmail/nolock-delivered procmail/nolock-only-alpha" # --- what the port installs -------------------------------------------------- check_rc procmail/procmail-executable 0 test -x /usr/bin/procmail check_rc procmail/formail-executable 0 test -x /usr/bin/formail check_rc procmail/mailstat-executable 0 test -x /usr/bin/mailstat check_rc procmail/lockfile-executable 0 test -x /usr/bin/lockfile # --- the compiled-in identity and the two probed answers that reach the target - ver=$(procmail -v 2>&1) case "$ver" in *"procmail v3.22 2001/09/10"*) bst_ok procmail/version ;; *) bst_notok procmail/version "got:'$(_bst_1line "$ver")'" ;; esac # The locking strategies procmail was built with. Pinned because they are an # autoconf.h answer taken on the BUILDER, and because the `:0:` cases below mean # nothing unless a folder-locking strategy is actually compiled in. case "$ver" in *"Locking strategies:"*"dotlocking, fcntl()"*) bst_ok procmail/locking-strategies ;; *) bst_notok procmail/locking-strategies "want 'dotlocking, fcntl()' got:'$(_bst_1line "$ver")'" ;; esac # MAILSPOOLDIR, the other probed answer that reaches the target. The builder # found /var/spool/mail; asserted as a prefix rather than a whole path because # the trailing component is the invoking user, which is a property of the guest. case "$ver" in *"Your system mailbox:"*"/var/spool/mail/"*) bst_ok procmail/system-mailbox ;; *) bst_notok procmail/system-mailbox "want '/var/spool/mail/' got:'$(_bst_1line "$ver")'" ;; esac # --- delivery: file by header match, with a folder lock ---------------------- cat > "$W/rc" </dev/null) case "$got" in *"Subject: BST-ROUTE-ALPHA fixture"*) bst_ok procmail/match-headers-kept ;; *) bst_notok procmail/match-headers-kept "the Subject header is not in the folder" ;; esac case "$got" in *"alpha body line two"*) bst_ok procmail/match-body-kept ;; *) bst_notok procmail/match-body-kept "the body is not in the folder" ;; esac case "$got" in *""*) bst_ok procmail/match-message-id-kept ;; *) bst_notok procmail/match-message-id-kept "the Message-ID is not in the folder" ;; esac alpha_size=$(wc -c < "$MD/alpha" 2>/dev/null | tr -d ' ') # --- delivery: a rule that matches nothing falls through to DEFAULT ---------- # The same rcfile, a message whose X-Bst-Route matches neither rule. It must # reach DEFAULT -- not be dropped, and not be filed under a rule that did not # match it. sed 's/^X-Bst-Route: alpha/X-Bst-Route: nomatch/ s/BST-ROUTE-ALPHA/BST-ROUTE-NOMATCH/ s/bst-alpha-0001/bst-nomatch-0001/' "$MSG" > "$W/nomatch.txt" procmail -m "$W/rc" < "$W/nomatch.txt"; nrc=$? check_eq procmail/nomatch-rc 0 "$nrc" check_rc procmail/nomatch-to-default 0 test -s "$MD/inbox" # ...and the matched folder is untouched: byte-for-byte the size it had before, # which is the check that separates "fell through" from "was also copied". check_eq procmail/nomatch-not-in-alpha "$alpha_size" "$(wc -c < "$MD/alpha" 2>/dev/null | tr -d ' ')" inbox=$(cat "$MD/inbox" 2>/dev/null) case "$inbox" in *"BST-ROUTE-NOMATCH"*) bst_ok procmail/nomatch-body-kept ;; *) bst_notok procmail/nomatch-body-kept "the non-matching message is not in DEFAULT" ;; esac check_eq procmail/deliver-set-is-alpha-and-inbox "alpha inbox" "$(pm_ls "$MD")" inbox_size=$(wc -c < "$MD/inbox" 2>/dev/null | tr -d ' ') # --- delivery with NO folder lock -------------------------------------------- # # The control for the two `:0:` runs above. Same fixture, same rule, `:0` in # place of `:0:` -- so if the locked runs go red and this one stays green, the # finding is that the guest's root filesystem cannot take a procmail dotlock, # and not that the filter mis-files. cat > "$W/rc-nolock" < " line per # delivery. Those lines are mailstat's whole input, so they are pinned here # before mailstat is asked to summarise them. falpha=$(grep -c '^ Folder: alpha' "$W/log" 2>/dev/null) check_eq procmail/log-abstract-alpha 1 "$falpha" finbox=$(grep -c '^ Folder: inbox' "$W/log" 2>/dev/null) check_eq procmail/log-abstract-default 1 "$finbox" check_eq procmail/log-two-deliveries 2 "$(grep -c '^ Folder: ' "$W/log" 2>/dev/null)" # --- mailstat over that log ------------------------------------------------- # # mailstat is a /bin/sh script, not a wasm binary -- it drives test, expr, sed, # sort, awk, cat, mv and ls through its own hardcoded PATH=/bin:/usr/bin. So # this case is as much a check on the image's shell environment as on procmail, # and a red here is read that way. stat_out=$(mailstat -k "$W/log" 2>&1); src=$? check_eq procmail/mailstat-rc 0 "$src" case "$stat_out" in *"Total"*"Number"*"Folder"*) bst_ok procmail/mailstat-header ;; *) bst_notok procmail/mailstat-header "no table header: '$(_bst_1line "$stat_out")'" ;; esac # One row per folder, each carrying . a_row=$(printf '%s\n' "$stat_out" | awk '$3=="alpha"{print $1" "$2}') i_row=$(printf '%s\n' "$stat_out" | awk '$3=="inbox"{print $1" "$2}') t_row=$(printf '%s\n' "$stat_out" | awk 'NF==2 && $1 ~ /^[0-9]+$/ && $2 ~ /^[0-9]+$/{print $1" "$2}' | tail -1) check_eq procmail/mailstat-alpha-count 1 "${a_row#* }" check_eq procmail/mailstat-inbox-count 1 "${i_row#* }" check_eq procmail/mailstat-total-count 2 "${t_row#* }" # The cross-check that makes the counts mean something: the bytes mailstat # reports per folder are the bytes the folder actually holds. check_eq procmail/mailstat-alpha-size-matches-disk "$alpha_size" "${a_row%% *}" check_eq procmail/mailstat-inbox-size-matches-disk "$inbox_size" "${i_row%% *}" check_eq procmail/mailstat-total-is-sum "$((alpha_size + inbox_size))" "${t_row%% *}" fi # --- formail: split a multi-message mbox -------------------------------------- cd "$W/split" || exit 0 formail -s /bin/sh -c 'cat > msg.$FILENO' < "$MBOX"; frc=$? check_eq procmail/split-rc 0 "$frc" # Three messages in, three files out -- and the set is pinned, so a formail that # emitted four (a mis-detected From_ line) is as red as one that emitted two. check_eq procmail/split-count "msg.000 msg.001 msg.002" "$(pm_ls "$W/split")" # Each split message keeps ITS OWN headers. Asserted as one joined string so a # formail that wrote the same message three times cannot pass. subs=$(for f in msg.000 msg.001 msg.002; do formail -zx Subject: < "$f" 2>/dev/null; done | tr '\n' ',') check_eq procmail/split-subjects "BST-SPLIT-ONE,BST-SPLIT-TWO,BST-SPLIT-THREE," "$subs" seqs=$(for f in msg.000 msg.001 msg.002; do formail -zx X-Bst-Seq: < "$f" 2>/dev/null; done | tr '\n' ',') check_eq procmail/split-seq "1,2,3," "$seqs" # ...and each keeps its own body, which the header check above cannot say. bodies=0 grep -q '^body of message one$' msg.000 2>/dev/null && bodies=$((bodies + 1)) grep -q '^with a second line$' msg.001 2>/dev/null && bodies=$((bodies + 1)) grep -q '^body of message three$' msg.002 2>/dev/null && bodies=$((bodies + 1)) check_eq procmail/split-bodies 3 "$bodies" # --- formail as a header extractor -------------------------------------------- check_eq procmail/extract-message-id "" \ "$(formail -zx Message-ID: < "$MSG" 2>/dev/null)" check_eq procmail/extract-subject "BST-ROUTE-ALPHA fixture" \ "$(formail -zx Subject: < "$MSG" 2>/dev/null)" # formail -s over the mbox without splitting to files: one `wc -l` per message, # which is the same message boundaries stated a second way. check_eq procmail/formail-count-lines "9 10 9" \ "$(formail -s wc -l < "$MBOX" 2>/dev/null | tr -d ' ' | tr '\n' ' ' | sed 's/ $//')" cd "$tmp" || : rm -rf "$W"