MAILMUNGE-PROTOCOL(7)
MAILMUNGE-PROTOCOL ๐ โจ
okay bestie, so this is the mailmunge-protocol(7) page โ yeah, the Perl one, from the User Contributed Perl Documentation corner of the universe ๐. It's the whole tea on the secret handshake between mailmunge and its filter programs. Buckle up, it's giving protocol lore ๐ฅ
โจ NAME โจ
mailmunge-protocol โ the conventions mailmunge uses to talk to filter programs ๐ฌ๐ซถ
๐ธ DESCRIPTION ๐ธ
So mailmunge and mailmunge-multiplexor team up to give you a whole mechanism for hooking scripts and programs into Sendmail's milter API. Iconic behavior honestly ๐
Here's the vibe: the milter API is multi-threaded and written in C. Kinda intimidating ngl ๐ญ. But mailmunge lets you write single-threaded filters in literally the language of your choice ๐ฆ. Yeah, you do sacrifice some of milter's flexibility and speed โ a little, a tiny bit โ but the ease of writing filters more than compensates for that slight loss. Worth it, fr fr ๐
This manual describes exactly how mailmunge communicates with the filter program, and it gives you enough info to write your own filter program as a replacement for the Mailmunge Perl modules, if that's your main character moment ๐
๐ฆ PROTOCOL OVERVIEW ๐ฆ
The protocol is a simple line-based protocol. No cap. The filter program runs in a loop, communicating with the mailmunge-multiplexor program on its standard input and standard output file descriptors ๐
Every time through the loop, the server reads a single line from standard input โ that line contains a command. Then the server executes the command and outputs a single-line response to standard output. Simple as that ๐
For the scan command there's extra lore: additional information gets placed in a temporary working directory ๐. When the filter processes the scan command (typically inside its filter_message function), it's expected to populate that working directory with more files โ those files are how the scan results get communicated back to mailmunge ๐ซถ
๐ FILTER INVOCATION ๐
The filter program may be invoked in one of four ways. Like we said above, the filter program is expected to run in a loop, accepting commands from standard input and writing responses to standard output. And it should exit if it receives end-of-file on standard input ๐ชโจ
filter_prog -server
If the program is invoked with the single argument -server, it's expected to run as a server as described above ๐ฅ๏ธ
filter_prog -serveru
If the program is invoked with the single argument -serveru, it's expected to run as a server. Plus, anything it prints to file descriptor 3 is used to update the "worker status" field in the multiplexor ๐. This lets the filter inform administrators exactly what it's doing. (See the -Z option to mailmunge-multiplexor.)
filter_prog -embserver
Similar to -server, but used by the embedded Perl interpreter ๐ช. The program should run any initialization routines and then exit. The multiplexor will subsequently call the Perl routine _mailmunge_do_main_loop when it's time for the worker to begin running โฐ
filter_prog -embserveru
Similar to -embserver with the additional magic of updating the worker status from data written to file descriptor 3 โจ
If you use the standard Mailmunge Perl modules, the Mailmunge::Filter class takes care of interpreting command-line arguments and running the filter in the appropriate mode. So you don't gotta do it yourself ๐
๐ฅ THE MAIN SERVER LOOP ๐ฅ
mailmunge-multiplexor runs the filter program, and that program is expected to run continuously in a server loop. The filter program reads commands from standard input, and writes results to standard output ๐
The filter program must exit shortly after it sees EOF on its standard input. If it doesn't exit within 10 seconds, it will be terminated with SIGTERM ๐. And if that still doesn't work? Then after a further 10 seconds, it is killed with SIGKILL. Not a vibe you want to experience ๐ญ
๐ PERCENT ENCODING ๐
Many parameters in commands and replies are percent-encoded ๐. Here's the rule, bestie: to percent-encode a sequence of bytes, replace any byte whose value is less than or equal to 32, greater than 126, or is one of %, \, ", or ' with the three-byte sequence %XY where XY is the hexadecimal value of the byte โจ
Here are some examples of percent-encoded strings:
Hello ==> Hello
Hello, there! ==> Hello,%20there!
"How\ are%you?" ==> %22How%5C%20are%25you?%22
๐ SERVER COMMANDS ๐
All server commands are single-line commands. Each command is followed by a space-separated list of arguments; each argument is percent-encoded ๐ . The commands defined are:
pingโ elicits a reply ofPONGfrom the server ๐scan queue_id dirโ run a scan for the mail identified by the Sendmail queue-ID queue_id in the directory dir. The command is terminated with a newline. The server must write a newline-terminatedokif the scan completed successfully, orerror: msgif something went wrong ๐ฌ Additional information is passed in a working directory as described in "THE SCAN COMMAND".relayok ip_addr hostname client_port daemon_ip daemon_port queue_idโ test whether or not to accept a connection from the specified host ๐. The server must write the following to standard output:ok 1โ accept the connection โok 0 error_message code dsnโ reject the connection with the specified code, dsn and text error_message โok -1 error_message code dsnโ tempfail the connection with the specified code, dsn and text error_message โณ
In all cases, error_message, code and dsn must be percent-encoded.
Note that even if the connection is accepted, a later scan can still reject the message based on other criteria ๐. ip_addr is the IP address of the relay, and hostname is the hostname (if it could be determined; otherwise, the IP address in square brackets).
helook ip_addr hostname helo_string client_port daemon_ip daemon_port queue_idโ test whether or not to accept the HELO/EHLO command ๐. The server must write the following to standard output:ok 1โ accept the HELO/EHLO command โok 0 error_message code dsnโ reject the command with the specified code, dsn and text error_message โok -1 error_message code dsnโ tempfail the command with the specified code, dsn and text error_message โณ
In all cases, error_message, code and dsn must be percent-encoded.
senderok sender_addr ip_addr hostname helo_string dir queue_id [esmtp_args...]โ test whether or not to accept mail from the specified sender ๐ฌ. The server must write the following to standard output:ok 1โ accept the sender โok 0 error_message code dsnโ reject the sender with the specified code, dsn and text error_message โok -1 error_message code dsnโ tempfail the sender with the specified code, dsn and text error_message โณ
In all cases, error_message, code and dsn must be percent-encoded.
Note that even if the sender is accepted, a later scan can still reject the message based on other criteria ๐. sender_addr is the sender's e-mail address. The ip_addr and hostname arguments are as in relayok. helo_string is the argument to the SMTP HELO/EHLO command. dir is the Mailmunge spool directory, and queue_id is the Sendmail or Postfix queue identifier ๐จ
The optional esmtp_args are space-separated, percent-encoded ESMTP arguments supplied with the MAIL FROM: command.
recipok recip_addr sender_addr ip_addr hostname first_recip helo_string dir queue_id [esmtp_args...]โ test whether or not to accept mail for the specified recipient ๐ฎ. The server must write the following to standard output:ok 1โ accept the recipient โok 0 error_message code dsnโ reject the recipient with the specified code, dsn and text error_message โok -1 error_message code dsnโ tempfail the recipient with the specified code, dsn and text error_message โณ
In all cases, error_message, code and dsn must be percent-encoded.
recip_addr is the argument to the RCPT TO: command, and first_recip is the argument to the first RCPT TO: command for this message. Other arguments are as in senderok ๐
map map_name keyโ if you're using a map socket (the-Noption tomailmunge-multiplexor), then the server should look up the key key in the map map_name ๐บ๏ธ. The server should print a single line to standard output. The first word on the line should be one ofOK,NOTFOUND,TEMP,TIMEOUTorPERM, indicating a successful lookup, absence of the key, a temporary failure, a timeout or a permanent failure, respectively. This should be followed by a space and a percent-encoded string representing the value of the key (if it was found) or an optional error message (if something went wrong) ๐ญtick bandโ the filter should execute itstickmethod with the specified band argument โฑ๏ธ. It should print a single line to standard output:tock band
๐ซถ Additional Commands ๐ซถ
If you use the standard Mailmunge perl modules, your filter can define a function filter_unknown_cmd that can extend the list of server commands ๐ง If you do this, make sure all of your commands start with an upper-case letter to avoid conflicts if more built-in commands are defined in the future. Future-proofing, bestie ๐
โญ SERVER REPLIES โญ
Your filter should not write anything to standard output except reply codes as documented above, or the multiplexor will become confused ๐ตโ๐ซ. And you should not terminate the program โ simply echo an error: reply and return to the server loop, okay? ๐ซถ
When you send a reply code back to the multiplexor, be sure to terminate it with a newline, and to flush standard output ๐ฟ. If your program uses the Standard I/O library, standard output may not be flushed immediately, and mailmunge-multiplexor will wait forever for the filter's reply, and eventually kill the filter on the assumption it has hung up ๐. So flush, babes.
If the filter program receives aSIGINTsignal, it must terminate. This is used bymailmunge-multiplexorto terminate workers after they have processed a given number of e-mail messages. Respect the signal ๐
okay that's part 1 bestie, keep scrolling because there's more lore coming ๐๐
๐ THE SCAN COMMAND ๐
Okay so the scan command is not like the other commands, bestie โ most of the info it needs gets dropped into a temporary working directory. Commands go into one file in that directory, and results should be written to a different file in that directory, as described below. Slay, but make it filesystem.
๐ FILE LAYOUT FOR THE SCAN COMMAND ๐
When the filter begins a scan, it should change directories to the working directory. Once it's in there, it will find these files waiting like little icon packs:
INPUTMSGโ The complete input e-mail message, including headers. The whole main-character message.HEADERSโ Just the headers, one per line. Headers that were continued over several lines in the original message get collapsed into a single line in this file. Clean, compact, lowkey.PRISTINE_HEADERSโ Just the headers, exactly as they were received from the SMTP client. UnlikeHEADERS, we do not unwrap the headers in this file, so a single header may extend over several lines. The untouched, no-filter receipt version.COMMANDSโ A list of commands. Each command is a single letter and may be followed by arguments. Each command is on its own line. Very one-command-per-line of it.
๐ THE COMMANDS FILE ๐
All commands in the COMMANDS file have their arguments percent-encoded. No exceptions, bestie.
The commands from the C to Perl filters are:
Ssenderโ The sender of the message.sesmtp_argโ An ESMTP argument associated with the sender (such asSIZE=54432). There is one"s"line for each ESMTP argument.Usubjectโ The message subject.Xmessage_idโ The Message-ID.Rrecipient mailer host addrโ A recipient. There is one"R"line for each recipient. The mailer, host and addr parts of the line are the values of the Sendmail{rcpt_mailer},{rcpt_host}and{rcpt_addr}macros if they are available, or"?"if not.resmtp_argโ An ESMTP argument associated with the most recent recipient (such asNOTIFY=never). There is one"r"line for each SMTP argument.!โ If this command is present, there are suspicious characters in the message headers. Red flag energy, fr fr.?โ If this command is present, there are suspicious characters in the message body. Also giving side-eye.Ihost_addrโ The SMTP relay host's IP address in dotted-quad notation.iidentifierโ An identifier generated by Mailmunge. On a given host, this identifier is very likely to be unique over a timespan of about 24 years. The way that is weirdly specific and also iconic.Hhost_nameโ The SMTP relay host name.Eargumentโ The argument to the SMTPEHLOorHELOcommand.Qqidโ The message's Sendmail or Postfix queue-ID.=macro valโ Set the value of the specified Sendmail macro toval. Bothmacroandvalare percent-encoded, but the single space character between them is not. Do not touch that space, periodt.
๐ซง SCAN OPERATION ๐ซง
When the filter performs a scan, it can make use of all the information in the files mentioned previously. If the filter needs temporary working files, it should create a subdirectory under the working directory for its own use. In this case, you do not have to clean up your working files, because mailmunge deletes the working directory when the filter returns. So yes, you can just let it vibe โ cleanup is handled.
๐ FINAL FILE LAYOUT ๐
The filter communicates the results of the scan back to mailmunge by creating additional files in the working directory. The most important file is called RESULTS, and it contains a list of one-letter, one-line commands back to mailmunge. As usual, command arguments are percent-escaped. The commands from the filter back to mailmunge are:
Bcode dsn reply_textโ Bounce (reject) the message with the specified SMTP reply code, DSN code and reply text. Not a soft launch โ an actual rejection.Dโ Silently discard the message and pretend it was delivered. Sneaky, but make it protocol.Tcode dsn reply_textโ Return an SMTP temporary failure code with the specified SMTP code, DSN and reply text.Cโ Replace the message body. If this command is present, the fileNEWBODYmust contain the new message body. The glow-up body goes inNEWBODY, no cap.Mheader_valโ Replace the MIME Content-Type header with a new value. Used to change MIME boundaries or convert non-MIME to MIME messages.Hheader valโ Add a new headerheaderwith valueval. The header should not contain a colon. Each ofheaderandvalis percent-escaped, but the single space between them is not.Nheader index valโ Adds a new headerheaderwith valuevalin positionindex. An index of zero specifies that the new header should be prepended before all existing headers. Main character header, first in line.Iheader index valโ Replace theindex'th occurrence ofheaderwith valueval. The index is 1-based. The header should not contain a colon. Each ofheader,indexandvalis percent-escaped, but the single space between them is not.Jheader indexโ Delete theindex'th occurrence ofheader. It's giving removal, bestie.Rrecipโ Add a new recipientrecipto the message.Srecipโ Deleterecipfrom the list of message recipients.fsenderโ Change the envelope sender tosender. This is only supported by Sendmail 8.14.0 and higher.Fโ Indicate that we have finished issuing commands. Anything after anFline is ignored. The final boss of the command list.
๐ง NON-PERL FILTERS ๐ง
In principle, you can replace the usual Mailmunge filter /etc/mailmunge/mailmunge.pl with any program you wish (using mailmunge-multiplexor's -f option) just as long as it obeys the protocol and command-line invocation options documented here. This would require significant effort and it's probably better to stick with the Perl code in most cases. Ngl, that's the practical take.
Obviously, if you want to write a non-Perl filter, you cannot use the embedded Perl interpreter (the -E mailmunge-multiplexor option.) Different lane, different limitations.
โจ SEE ALSO โจ
mailmungemailmunge-multiplexorMailmunge
๐ AUTHOR ๐
mailmunge was written by Dianne Skoll <dianne@skollsoft.com>. The mailmunge home page is <http://www.mailmunge.org/>. Crown-worthy work, honestly.
๐ LICENSE ๐
This code is licensed under the terms of the GNU General Public License, version 2.
Big kisses from the perl v5.42.2 era, dated 2025-10-05 โ this was MAILMUNGE-PROTOCOL(7), and that's the whole vibe. ๐โจ๐ซถ
๐ฆ where this page comes from
- alpine 3.24: mailmunge-doc 3.20-r0 ยท GPL-2.0-only
- ๐ girlified by deepseek-ai/DeepSeek-V4.1-Flash-1017 on : a language model's rewrite of the original, under its terms