Support -a - to read addresses from stdin (one per line).
Lines starting with # or empty are ignored.
Invalid addresses (no @) are skipped with a warning.
README: document $posteraddr$ and DKIM/From munging recipe
Explain the $posteraddr$ variable available in control/customheaders
and provide a complete recipe for DKIM-friendly From: rewriting using
only existing control files (customheaders, delheaders, replyto).
This makes PR #102 (From munging via control/munge) unnecessary.
Declare action_strs, modreason_strs, subtype_strs, subreason_strs,
and subtypes arrays as const char * const to put them in .rodata
and allow the compiler to optimize better.
The valuecount field in struct mailhdr was int but is used as a loop
counter and array index. Change it to size_t and update the loops
in free_mailhdrs() and mlmmj-process.c to use size_t consistently,
eliminating signed/unsigned comparison warnings.
mlmmj-send: fix gotsigterm to be volatile sig_atomic_t
Per POSIX, variables shared between signal handlers and the main
execution context must be volatile sig_atomic_t, otherwise their
read/write is not guaranteed to be atomic in a signal context.
Replace the goto-based fallback pattern with a natural if/else flow.
The subject is now parsed inside an if (txt != NULL) block, and the
fallback is generated when subject remains NULL. This also fixes a
memory leak: when "too many headers" occurred, the old code jumped
to fallback_subject without freeing the last get_processed_text_line
allocation.
The confsub and confunsub blocks were identical except for the directory
name ('subconf' vs 'unsubconf'). Merge them into a single block that
derives the directory name from the identifier string.
Simplify do_access action parsing with lookup table
Replace the if/else chain for action keyword matching with a struct-based
lookup table and a parse_action() helper. This makes adding new actions
easier (just add to action_keywords[]) and eliminates the ACT_DENY
sentinel-overload issue.
Simplify initsmtp: replace do/while(1) with for(;;) and direct returns
The old code used a do/while(1) loop with scattered break/continue and
a retval variable set before each break. The new code returns each error
code directly where the decision is made, making the EHLO→HELO fallback
flow much easier to follow.
Also fix: the old code had a misleading comment 'EHLO successful don't
try more' on the HELO break path.
If fopen() on the queue file fails, send_mail() treats NULL fp as "no
mail to send" and returns 0 (success). send_single_mail() then returns
true, causing the queue file to be unlink()ed without ever being
delivered — silent data loss.
Add explicit fopen return checks in all functions that previously
omitted them.
Fix do_access: add missing NULL check on msg parameter
The final fallthrough path (no rules matched) called xasprintf(msg, ...)
without checking if msg != NULL, unlike all other xasprintf calls in the
same function. If a caller passes NULL for msg, this would crash in
vasprintf(NULL, ...).
Fix send_probe: detect fopen failure and return false early
If fopen(queuefilename, "r") fails, mail.fp stays NULL. send_mail()
treats NULL fp as "no mail to send" and returns success, causing
send_probe() to silently report success without sending anything.
Now we check for NULL fp immediately and return failure.
Fix fdopendir UB: use dirfd() instead of raw fd after fdopendir
POSIX states that after fdopendir(), the file descriptor is under the
control of the system and must not be used directly. Use dirfd() to
retrieve the fd from the DIR stream for subsequent openat() calls.
Rename parameter in is_subbed_in() from "dirfd" to "subddir_fd" to
avoid naming collision with the POSIX dirfd() function.
Fix textcontent: return early when openat fails instead of calling close(-1)
When openat() for the "text" subdirectory fails, fd is -1. The old code
passed fd=-1 to ctrlval() which would return NULL, but then closed(-1)
which is a no-op that sets errno=EBADF unnecessarily.
Fix process_headers: stop at blank line instead of skipping it
process_headers() used "continue" for empty/whitespace-only lines,
causing it to keep reading past the end-of-headers marker into body
content. Changed to "break" so it properly terminates at the blank
line as per RFC 5322.
Add test to verify body content after blank line is not processed.
Fix write_mailbody: eliminate double ungetc and EOF pushback in dot-stuffing
The old code called ungetc() twice without an intervening read in the
dot-stuffing path (C standard guarantees only one ungetc between reads),
and also passed EOF to ungetc() when the dot was the last character in
the file (undefined behavior).
Fix by outputting the original dot character directly via fputc() after
dot-stuffing instead of pushing it back via ungetc(), and only calling
ungetc() when the character is not EOF.
Fix random_int: validate readn() return to prevent using uninitialized data
If readn() fails (returns -1) or reads fewer than 4 bytes, val would
have been returned with uninitialized stack data. Now we initialize val
to 0 and only use it when readn() reports exactly 4 bytes read.
The old code returned immediately when a string started with \n or \r,
only stripping the first character. For inputs like "\n\n" or "\r\n\r\n"
this left trailing newlines intact.
Rewrite to strip trailing newlines from the end of the string,
which correctly handles all cases including multi-newline strings.
mlmmj-send: replace vec_pop_front with index-based iteration
In send_mail_many_list(), vec_pop_front() is O(n) because it shifts
every remaining element on each pop. Replace it with a simple index
that walks the strlist array in place, giving O(1) per element.
On failure (SIGTERM or send error), a temporary strlist pointing to
the remaining entries is passed to requeuemail(). The remaining
entries are freed after requeuemail() has written them to disk.
Test: verify only one Reply-To header with multiple From
Add test voodoo_replyto_once to verify that when the input
has multiple From: headers, only one Reply-To header is
generated (regression test for the previous fix).
When a subscriber file only contains the address to unsubscribe,
the file is deleted but the mmap and file descriptor are not
unmapped/closed before continue, leaking resources.
Fix: memory leak in prepstdreply_to() and copy-paste bug
Free headers[] on all error paths in prepstdreply_to().
Also fix headers[3] which was set to msgid instead of gendatestr()
when a msgid parameter was provided.
- Replace configure with latest bbuild version (adds write_if_changed,
HAVE_DECL_* handling, --includedir, --libdir, pkgconfigdir default)
- Update mk/defs.mk.in with PACKAGE_NAME and VERSION
- Update mk/prog.mk and mk/common.mk to latest bbuild
mlmmj.c: In function ‘bouncemail’:
mlmmj.c:450:21: warning: unused variable ‘st’ [-Wunused-variable]
450 | struct stat st;
| ^~
In file included from mlmmj.c:23:
mlmmj.c: In function ‘atfu_find_in_list_body’:
/tmp/mlmmj-devel.git/include/vec.h:93:15: warning: value computed is not used [-Wunused-value]
93 | (v)->d[--(v)->len]
| ~~~~~~^~~~~~~~~~~~
mlmmj.c:3611:9: note: in expansion of macro ‘vec_pop’
3611 | vec_pop(&bla);
| ^~~~~~~
ArchLinux: clang version 22.1.5
mlmmj-process.c:829:19: warning: variable 'txt' is used uninitialized whenever 'if' condition is false [-Wsometimes-uninitialized]
829 | } else if (modonlypost) {
| ^~~~~~~~~~~
mlmmj-process.c:833:18: note: uninitialized use occurs here
833 | MY_ASSERT(txt);
| ^~~
mlmmj-process.c:829:15: note: remove the 'if' if its condition is always true
829 | } else if (modonlypost) {
| ^~~~~~~~~~~~~~~~
mlmmj-process.c:287:11: note: initialize the variable 'txt' to silence this warning
287 | text *txt;
| ^
| = NULL
Add MIME type detection/rejection to mlmmj-receive
When control/mimedeny exists, parse MIME headers and body to detect
unwanted MIME types in both top-level and multipart sub-parts. Adds
an X-ThisMailContainsUnwantedMimeParts header (Y/N) without stripping
any content.
New functions: read_headers(), write_headers(),
update_unwanted_mime_header(), parse_body(), process_mail().
If control/mimedeny is absent, behavior is unchanged (dumpfd2fd).
Fixes from contrib/receivestrip/:
- use-after-free in header reading (free after vec_push)
- boundary detection missing "--" prefix in MIME body
add optional YYYY/MM archive partitioning via control/archivepartition
Large mailing lists accumulate thousands of files in a single archive/
directory, degrading filesystem performance. When control/archivepartition
exists, new archives are written to archive/YYYY/MM/N and mlmmj-maintd
automatically migrates existing flat archives based on file mtime.
Reading always tries flat layout first then falls back to scanning
subdirectories, so archives work regardless of layout.
generate RFC 2919/2369 List-* and Precedence headers natively
Mailing list messages without List-Id/Precedence headers cause vacation
autoreplies to trigger bounces leading to unsubscriptions, prevent mail
clients like Delta Chat from detecting list messages, and hurt
deliverability with major providers.
Generate List-Id, List-Post, List-Help, List-Subscribe, List-Unsubscribe,
and Precedence headers by default in do_all_the_voodoo_here(). List-Owner
is included only when control/owner exists. All headers can be disabled
via control/nolistheaders.
mlmmj-make-ml: use full email address for default owner
The default owner "postmaster" lacks a domain part, causing
mlmmj-send to reject it with "No @ in address". Use the FQDN
already collected earlier in the script to build a proper
postmaster@FQDN default.
send_mail: add X-Forwarded-To and X-Signed-Recipient headers
Add per-recipient headers to improve deliverability and support DARA
(draft ARC replay-resistant authentication):
- X-Forwarded-To: helps Gmail recognize legitimate forwarding
- X-Signed-Recipient: used in ARC signatures to prove the message
was intended for a specific recipient
Both are enabled independently via control files (xforwardedto and dara).
Like addtohdr, these are incompatible with VERP since they require
per-recipient header injection.