From 70f572daa22c53db6a9d810ef4a747d91ffd708d Mon Sep 17 00:00:00 2001 From: Keith Bare <105995606+keithbare2@users.noreply.github.com> Date: Sat, 25 Feb 2023 22:09:54 -0500 Subject: [PATCH] Avoid generating duplicate headers in ezmlm-send When processing a message's headers: - Make note if Reply-To was seen. - Save off and do not copy the Cc header to the output immediately. - Save off and do not copy the From header to the output immediately. At the end of the message headers, call rewrite_from(). When flagrewritefrom is set, either by the list's configuration or because a DMARC reject policy applies to the message sender's address: - The From header is rewritten as before. - If replytolist is not enabled, a Reply-To header is only added if the original message did not have one. Rationale is that the original Reply-To header indicated where the post author is interested in receiving responses, not the From header. - When replytolist is enabled and the original message had a Cc header, the From address is appended to the existing Cc header. - When replytolist is enabled and the original message did not have a Cc header, generate a new one using the From address. After rewrite_from() performs any of the applicable manipulations described above, it copies the resulting From and Cc headers to the output. Update tests/551-ezmlm-send-rewritefrom: - Confirm Reply-To and Cc headers are not duplicated. - Validate behavior when replytolist is configured. --- bin/ezmlm-send.c | 34 ++++++++++++++-- tests/02-functions | 21 ++++++++++ tests/550-ezmlm-send | 11 +++--- tests/551-ezmlm-send-rewritefrom | 66 ++++++++++++++++++++++++++++++++ 4 files changed, 124 insertions(+), 8 deletions(-) diff --git a/bin/ezmlm-send.c b/bin/ezmlm-send.c index 2d98995..104fb12 100644 --- a/bin/ezmlm-send.c +++ b/bin/ezmlm-send.c @@ -81,6 +81,7 @@ static stralloc lines = {0}; static stralloc subject = {0}; static stralloc from = {0}; static stralloc received = {0}; +static stralloc cc = {0}; static stralloc prefix = {0}; static stralloc content = {0}; stralloc boundary = {0}; @@ -111,6 +112,7 @@ static int flagfoundokpart; /* Found something to pass on. If multipart */ /* we set to 0 and then set to 1 for any */ /* acceptable mime part. If 0 -> reject */ static int flagsawreceived; +static int flaghavereplyto; static int flagprefixed; static unsigned int serial = 0; static int flagarchived; @@ -318,6 +320,8 @@ static void rewrite_from() unsigned int at; int r; + stralloc_copyb(&line,"",0); + /* If not unconditionally rewriting headers, turn it on for this * message if DMARC would prevent us from sending as-is. */ if (!flagrewritefrom) { @@ -347,9 +351,27 @@ static void rewrite_from() stralloc_catb(&line,"@",1); stralloc_catb(&line,outhost.s,outhost.len); stralloc_catb(&line,">\n",2); - stralloc_cats(&line,flagreplytolist ? "Cc:" : "Reply-To:"); + if (flagreplytolist) { + if (cc.s) { + --cc.len; /* remove '\n' */ + stralloc_catb(&cc,",\n",2); + } + stralloc_catb(&cc,from.s,from.len); + } else if (!flaghavereplyto) { + stralloc_catb(&line,"Reply-To:",9); + stralloc_catb(&line,from.s,from.len); + } + } else { + stralloc_copyb(&line,"From:",5); stralloc_catb(&line,from.s,from.len); } + + if (cc.s) { + stralloc_catb(&line,"Cc:",3); + stralloc_catb(&line,cc.s,cc.len); + } + + stralloc_catb(&line,"\n",1); } int main(int argc,char **argv) @@ -519,6 +541,7 @@ int main(int argc,char **argv) strerr_die2sys(111,FATAL,MSG(ERR_READ_INPUT)); if (flaginheader && match) { if (line.len == 1) { /* end of header */ + rewrite_from(); flaginheader = 0; if (flagindexed) /* std entry */ r = idx_copy_insertsubject(); /* all indexed lists */ @@ -624,9 +647,14 @@ int main(int argc,char **argv) else if (case_startb(cp,cpafter-cp,"Quoted-Printable")) encin = 'Q'; } else if (flaglistid && case_startb(line.s,line.len,"list-id:")) flagbadfield = 1; /* suppress if we added our own */ - else if (case_startb(line.s,line.len,"From:")) { + else if (!flagbadfield && case_startb(line.s,line.len,"Reply-To:")) + flaghavereplyto = 1; + else if (case_startb(line.s,line.len,"Cc:")) { + stralloc_copyb(&cc,line.s+3,line.len-3); + flagbadfield = 1; /* written/adjusted by rewrite_from() */ + } else if (case_startb(line.s,line.len,"From:")) { stralloc_copyb(&from,line.s+5,line.len-5); - rewrite_from(); + flagbadfield = 1; /* written/adjusted by rewrite_from() */ } else if (line.len == mydtline.len) if (!byte_diff(line.s,line.len,mydtline.s)) strerr_die2x(100,FATAL,MSG(ERR_LOOPING)); diff --git a/tests/02-functions b/tests/02-functions index e999279..ea0a27d 100644 --- a/tests/02-functions +++ b/tests/02-functions @@ -22,6 +22,16 @@ fatal() { exit 100; } +unfoldhdrs() { + (cat "$QQHDR"; echo) | + sed -e '/^\([^ \t]\|$\)/x' -e '# new header; swap hold/pattern' \ + -e '/^$/d' -e '# ignore initial empty hold' \ + -e '/^[ \t]/{' -e H -e d -e } -e '# append continuation to hold' \ + -e 's/[ \t]*\n[ \t]*/ /g' -e '# unfold concatenated header' \ + >"${TMP}hdr" + mv -f "${TMP}hdr" "$QQHDR" +} + grephdr() { # Search for the header line, and produce an error if it didn't match. egrep "^$*$" "$QQHDR" >/dev/null 2>&1 || \ @@ -36,6 +46,17 @@ grephdr() { mv -f "${TMP}hdr" "$QQHDR" } +singlehdr() { + # Search for the header and ensure it appears exactly once. + count=$(grep -i -c "^$1" "$QQHDR") + if [ "$count" -ne 1 ]; then + [ "$count" -eq 0 ] && echo "Missing $1 line:" + [ "$count" -gt 1 ] && echo "Multiple $1 lines:" && grep -i "^$1" "$QQHDR" + BUG="${BUG} headers" + prompt "..............: " + fi +} + grepbody() { egrep "^$*$" "$QQBODY" >/dev/null 2>&1 || \ { diff --git a/tests/550-ezmlm-send b/tests/550-ezmlm-send index a9e8a99..6a4cfdb 100644 --- a/tests/550-ezmlm-send +++ b/tests/550-ezmlm-send @@ -1,9 +1,11 @@ prompt "ezmlm-send: " sendfrom() { - cat >"$TMP" <"$TMP" + if [ -n "$2" ]; then + echo "Reply-To: $2" >>"$TMP" + fi + cat >>"$TMP" <" - grephdr Reply-To: test1@example.org grephdr_empty grepbody Local: "$LOCAL" @@ -40,7 +41,7 @@ EOF grephdr Subject: 'test post' touch "$DIR"/replytolist - sendfrom test2@example.org + sendfrom test2@example.org test2@example.org grephdr_list 1 grephdr Precedence: bulk diff --git a/tests/551-ezmlm-send-rewritefrom b/tests/551-ezmlm-send-rewritefrom index 95e979c..9bd061a 100644 --- a/tests/551-ezmlm-send-rewritefrom +++ b/tests/551-ezmlm-send-rewritefrom @@ -4,46 +4,112 @@ touch "$DIR"/rewritefrom sendfrom '"My Name 1" ' grephdr From: "\"My Name 1\" via ${LIST} <${LIST}@${HOST}>$" +singlehdr Reply-To: grephdr Reply-To: '"My Name 1" ' sendfrom '"My Name 2" test2@example.org' grephdr From: "\"My Name 2\" via ${LIST} <${LIST}@${HOST}>$" +singlehdr Reply-To: grephdr Reply-To: '"My Name 2" test2@example.org' sendfrom 'My Name 3 ' grephdr From: "My Name 3 via ${LIST} <${LIST}@${HOST}>$" +singlehdr Reply-To: grephdr Reply-To: 'My Name 3 ' sendfrom 'test4@example.org (My Name 4)' grephdr From: "\"My Name 4\" via ${LIST} <${LIST}@${HOST}>$" +singlehdr Reply-To: grephdr Reply-To: 'test4@example.org \(My Name 4\)' sendfrom 'test5@example.org' grephdr From: "\"test5@example.org\" via ${LIST} <${LIST}@${HOST}>$" +singlehdr Reply-To: grephdr Reply-To: 'test5@example.org' rm -f "$DIR"/rewritefrom sendfrom 'test6@yahoo.com' grephdr From: "\"test6@yahoo.com\" via ${LIST} <${LIST}@${HOST}>$" +singlehdr Reply-To: grephdr Reply-To: 'test6@yahoo.com' sendfrom '"My Name 7" ' grephdr From: "\"My Name 7\" via ${LIST} <${LIST}@${HOST}>$" +singlehdr Reply-To: grephdr Reply-To: '"My Name 7" ' sendfrom 'My Name 8 ' grephdr From: "My Name 8 via ${LIST} <${LIST}@${HOST}>$" +singlehdr Reply-To: grephdr Reply-To: 'My Name 8 ' touch "$DIR"/rewritefrom sendfrom '=?iso-8859-1?Q?My=20N=E4m=E9=209?= ' grephdr From: "=\\?iso-8859-1\\?Q\\?My=20N=E4m=E9=209\\?= via ${LIST} <${LIST}@${HOST}>$" +singlehdr Reply-To: grephdr Reply-To: '=\?iso-8859-1\?Q\?My=20N=E4m=E9=209\?= ' sendfrom '=?utf-8?B?77yt772ZIMORw6ZtIDEw?= ' grephdr From: "=\\?utf-8\\?B\\?77yt772ZIMORw6ZtIDEw\\?= via ${LIST} <${LIST}@${HOST}>$" +singlehdr Reply-To: grephdr Reply-To: '=\?utf-8\?B\?77yt772ZIMORw6ZtIDEw\?= ' +sendfrom 'My Name 11 ' 'Reply To 11 ' +grephdr From: "My Name 11 via ${LIST} <${LIST}@${HOST}>$" +singlehdr Reply-To: +grephdr Reply-To: 'Reply To 11 ' + +sendfrom test12@example.org test12@example.org +grephdr From: "\"test12@example.org\" via ${LIST} <${LIST}@${HOST}>$" +singlehdr Reply-To: +grephdr Reply-To: 'test12@example.org' + +touch "$DIR"/replytolist + +sendfrom '"My Name 13" ' +grephdr From: "\"My Name 13\" via ${LIST} <${LIST}@${HOST}>$" +singlehdr Reply-To: +grephdr Reply-To: "<${LIST}@${HOST}>" +singlehdr Cc: +grephdr Cc: '"My Name 13" ' + +cat >"$TMP" <"$ERR" 2>&1 || \ + fatal "failed to produce post" +unfoldhdrs +grephdr From: "\"test14@example.org\" via ${LIST} <${LIST}@${HOST}>$" +singlehdr Reply-To: +grephdr Reply-To: "<${LIST}@${HOST}>" +singlehdr Cc: +grephdr Cc: 'cc14@example.org, test14@example.org' + +cat >"$TMP" < +To: ${LIST}@${HOST} +Cc: "Carbon Copy 15" , + +Subject: carbon-copied test post + +message goes here +EOF +${EZBIN}/ezmlm-send "$DIR" <"$TMP" >"$ERR" 2>&1 || \ + fatal "failed to produce post" +unfoldhdrs +grephdr From: "My Test 15 via ${LIST} <${LIST}@${HOST}>$" +singlehdr Reply-To: +grephdr Reply-To: "<${LIST}@${HOST}>" +singlehdr Cc: +grephdr Cc: '"Carbon Copy 15" , , My Test 15 ' + +rm -f "$DIR"/rewritefrom "$DIR"/replytolist + echo OK