MIMEDEFANG-FILTER(5)
๐ธ MIMEDEFANG-FILTER(5) โ the File Formats Manual page that's literally the blueprint for your mail filter ๐ธ
okay bestie, gather round the group chat ๐ this is the MIMEDEFANG-FILTER(5) manual page, and it's all about mimedefang-filter โ the Perl fragment that controls how mimedefang.pl disposes of various parts of a MIME message. It also contains some global variable settings that affect the operation of mimedefang.pl. No cap. โจ
๐ NAME ๐
mimedefang-filter โ Configuration file for MIMEDefang mail filter. โจ
๐ DESCRIPTION ๐
mimedefang-filter is a Perl fragment that controls how mimedefang.pl disposes of various parts of a MIME message. In addition, it contains some global variable settings that affect the operation of mimedefang.pl. ๐ซถ
๐ฆ CALLING SEQUENCE ๐ฆ
Incoming messages are scanned as follows, bestie:
- A temporary working directory is created. It is made the current working directory and the e-mail message is split into parts in this directory. Each part is represented internally as an instance of
MIME::Entity. ๐๏ธ - If the file
/etc/mail/mimedefang-filter.pldefines a Perl function calledfilter_begin, it is called with a single argument consisting of aMIME::Entityrepresenting the parsed e-mail message. Any return value is ignored. ๐ - For each leaf part of the mail message,
filteris called with four arguments:entity, aMIME::Entityobject;fname, the suggested filename taken from the MIME Content-Disposition header;ext, the file extension, andtype, the MIME Content-Type value. For each non-leaf part of the mail message,filter_multipartis called with the same four arguments asfilter. A non-leaf part of a message is a part that contains nested parts. Such a part has no useful body, but you should still perform filename checks to check for viruses that use malformed MIME to masquerade as non-leaf parts (likemessage/rfc822). In general, any action you perform infilter_multipartapplies to the part itself and any contained parts. ๐ซถ
Note that bothfilterandfilter_multipartare optional. If you do not define them, a default function that simply accepts each part is used. ๐
- After all parts have been processed, the function
filter_endis called if it has been defined. It is passed a single argument consisting of the (possibly modified)MIME::Entityobject representing the message about to be delivered. Withinfilter_end, you can call functions that modify the message headers body. โจ - After
filter_endreturns, the functionfilter_wrapupis called if it has been defined. It is passed a single argument consisting of the (possibly modified)MIME::Entityobject representing the message about to be delivered, including any modifications made infilter_end. Withinfilter_wrapup, you can not call functions that modify the message body, but you can still add or modify message headers. ๐ธ
๐ DISPOSITION ๐
mimedefang.pl examines each part of the MIME message and chooses a disposition for that part. (A disposition is selected by calling one of the following functions from filter and then immediately returning.) Available dispositions are:
action_acceptโ The part is passed through unchanged. If no disposition function is returned, this is the default. โaction_accept_with_warningโ The part is passed through unchanged, but a warning is added to the mail message. โ ๏ธaction_dropโ The part is deleted without any notification to the recipients. ๐action_drop_with_warningโ The part is deleted and a warning is added to the mail message. ๐ญaction_replace_with_warningโ The part is deleted and instead replaced with a text message. ๐action_quarantineโ The part is deleted and a warning is added to the mail message. In addition, a copy of the part is saved on the mail server in the directory/var/spool/MD-Quarantineand a notification is sent to the MIMEDefang administrator. ๐จaction_bounceโ The entire e-mail message is rejected and an error returned to the sender. The intended recipients are not notified. Note that in spite of the name, MIMEDefang does not generate and e-mail a failure notification. Rather, it causes the SMTP server to return a5XXSMTP failure code. ๐action_discardโ The entire e-mail message is discarded silently. Neither the sender nor the intended recipients are notified. ๐action_greylistโ The e-mail message is greylisted. If it's the first time we see this sender, the email will be tempfailed, when the sender will retry the email will be accepted. ๐ข
๐ฅ CONTROLLING RELAYING ๐ฅ
You can define a function called filter_relay in your filter. This lets you reject SMTP connection attempts early on in the SMTP dialog, rather than waiting until the whole message has been sent. Note that for this check to take place, you must use the -r flag with mimedefang. ๐
filter_relay is passed six arguments: $hostip is the IP address of the relay host (for example, "127.0.0.1"), $hostname is the host name if known (for example, "localhost.localdomain"). If the host name could not be determined, $hostname is $hostip enclosed in square brackets. (That is, ("$hostname" eq "[$hostip]") will be true.) ๐
The remaining four arguments to filter_relay are $port, $myip, $myport and $qid, which contain the client's TCP port, the Sendmail daemon's listening IP address, the Sendmail daemon's listening port, and the Sendmail Queue-ID, respectively. Note that the Queue-ID may not yet be available at this stage (for example, Postfix does not allocate a queue-ID this early.) If the Queue-ID is not available, the string NOQUEUE is passed instead. ๐
filter_relay must return a two-element list: ($code, $msg). $msg specifies the text message to use for the SMTP reply, but because of limitations in the Milter API, this message is for documentation purposes only---you cannot set the text of the SMTP message returned to the SMTP client from filter_relay. ๐ฌ
$code is a literal string, and can have one of the following values:
'REJECT'โ if the connection should be rejected. โ'CONTINUE'โ if the connection should be accepted. โ'TEMPFAIL'โ if a temporary failure code should be returned. โณ'DISCARD'โ if the message should be accepted and silently discarded. ๐๏ธ'ACCEPT_AND_NO_MORE_FILTERING'โ if the connection should be accepted and no further filtering done. ๐
Earlier versions of MIMEDefang used -1 for TEMPFAIL, 0 for REJECT and 1 for CONTINUE. These values still work, but are deprecated. ๐ต
In the case of REJECT or TEMPFAIL, $msg specifies the text part of the SMTP reply. $msg must not contain newlines. ๐ซ
For example, if you wish to reject connection attempts from any machine in the spammer.com domain, you could use this function:
sub filter_relay {
my ($ip, $name) = @_;
if ($name =~ /spammer\.com$/) {
return ('REJECT', "Sorry; spammer.com is blacklisted");
}
return ('CONTINUE', "ok");
}
๐ FILTERING BY HELO ๐
You can define a function called filter_helo in your filter. This lets you reject connections after the HELO/EHLO SMTP command. Note that for this function to be called, you must use the -H flag with mimedefang. ๐ฏ
filter_helo is passed seven arguments: $ip and $name are the IP address and name of the sending relay, as in filter_relay. The third argument, $helo, is the argument supplied in the HELO/EHLO command. ๐ฆ
The remaining four arguments to filter_helo are $port, $myip, $myport and $qid, which contain the client's TCP port, the Sendmail daemon's listening IP address, the Sendmail daemon's listening port, and the Sendmail Queue-ID, respectively. Note that the Queue-ID may not yet be available at this stage (for example, Postfix does not allocate a queue-ID this early.) If the Queue-ID is not available, the string NOQUEUE is passed instead. ๐
filter_helo must return a two-to-five element list: ($code, $msg, $smtp_code, $smtp_dsn, $delay). $code is a return code, with the same meaning as the $code return from filter_relay. $msg specifies the text message to use for the SMTP reply. If $smtp_code and $smtp_dsn are supplied, they become the SMTP numerical reply code and the enhanced status delivery code (DSN code). If they are not supplied, sensible defaults are used. $delay specifies a delay in seconds; the C milter code will sleep for $delay seconds before returning the reply to Sendmail. $delay defaults to zero. โฐ
(Note that the delay is implemented in the Milter C code; if you specify a delay of 30 seconds, that doesn't mean a Perl worker is tied up for the duration of the delay. The delay only costs one Milter thread.) ๐งต
๐ FILTERING BY SENDER ๐
Okay bestie, so you can define a function called filter_sender in your filter. This is giving early rejection energy โ it lets you bounce messages from certain senders before the whole message has even been sent. No waiting around for the full drama to unfold. But important note: for this check to actually happen, you must use the -s flag with mimedefang. Periodt.
filter_sender gets passed four arguments:
$senderโ the envelope e-mail address of the sender (for example,<dfs@roaringpenguin.com>). The address may or may not be surrounded by angle brackets. It's giving mystery brackets, iykyk.$ipand$nameโ the IP address and host name of the SMTP relay.$heloโ the argument to the SMTPHELOcommand.
Inside filter_sender, you can access any ESMTP arguments (such as SIZE=12345) in the array @ESMTPArgs. Each ESMTP argument occupies one array element. One arg, one slot, no crumbs.
filter_sender must return a two-to-five element list, with the same meaning as the return value from filter_helo. Same rules, different function, bestie.
For example, if you wish to reject messages from spammer@badguy.com, you could use this function:
sub filter_sender {
my ($sender, $ip, $hostname, $helo) = @_;
if ($sender =~ /^<?spammer\@badguy\.com>?$/i) {
return ('REJECT', 'Sorry; spammer@badguy.com is blacklisted.');
}
return ('CONTINUE', "ok");
}
As another example, some spammers identify their own machine as your machine in the SMTP HELO command. Not the flex they think it is. This function rejects a machine claiming to be in the roaringpenguin.com domain unless it really is a Roaring Penguin machine:
sub filter_sender {
my($sender, $ip, $hostname, $helo) = @_;
if ($helo =~ /roaringpenguin.com/i) {
if ($ip ne "127.0.0.1" and
$ip ne "216.191.236.23" and
$ip ne "216.191.236.30") {
return('REJECT', "Go away... $ip is not in roaringpenguin.com");
}
}
return ('CONTINUE', "ok");
}
As a third example, you may wish to prevent spoofs by requiring SMTP authentication when email is sent from some email addresses. This function rejects mail from king@example.com, unless the connecting user properly authenticated as elvisp. ๐ Elvis has left the building, but his auth did not. Note that this needs access to the %SendmailMacros global, that is not available in filter_sender until after a call to read_commands_file.
sub filter_sender {
my($sender, $ip, $hostname, $helo) = @_;
read_commands_file();
### notice: This assumes The King uses authentication without realm!
if ($sender =~ /^<?king\@example\.com>?$/i and
$SendmailMacros{auth_authen} ne "elvisp") {
return('REJECT', "Faking mail from the king is not allowed.");
}
return ('CONTINUE', "ok");
}
๐ FILTERING BY RECIPIENT ๐
Now we're checking the other side of the envelope, bestie. You can define a function called filter_recipient in your filter. This lets you reject messages to certain recipients, rather than waiting until the whole message has been sent. Same early-rejection vibe, different target. Note that for this check to take place, you must use the -t flag with mimedefang. Flag check: -t, no cap.
filter_recipient is passed nine arguments:
$recipientโ the envelope address of the recipient.$senderโ the envelope e-mail address of the sender (for example,<dfs@roaringpenguin.com>). The addresses may or may not be surrounded by angle brackets.$ipand$nameโ the IP address and host name of the SMTP relay.$firstโ the envelope address of the first recipient for this message.$heloโ the argument to the SMTPHELOcommand.$rcpt_mailer,$rcpt_hostand$rcpt_addrโ the Sendmail mailer, host and address triple for the recipient address.
For example, for local recipients, $rcpt_mailer is likely to be local, while for remote recipients, it is likely to be esmtp. Local vs remote, the mailer spill the tea.
Inside filter_recipient, you can access any ESMTP arguments (such as NOTIFY=never) in the array @ESMTPArgs. Each ESMTP argument occupies one array element. Yes, again. One per element. We love consistency.
filter_recipient must return a two-to-five element list whose interpretation is the same as for filter_sender. Note, however, that if filter_recipient returns 'DISCARD', then the entire message for all recipients is discarded. (It doesn't really make sense, but that's how Milter works.) โ ๏ธ Whole-message discard, not just one recipient. The Milter said so. We don't make the rules.
For example, if you wish to reject messages from spammer@badguy.com, unless they are to postmaster@mydomain.com, you could use this function:
sub filter_recipient {
my ($recipient, $sender, $ip, $hostname, $first, $helo,
$rcpt_mailer, $rcpt_host, $rcpt_addr) = @_;
if ($sender =~ /^<?spammer\@badguy\.com>?$/i) {
if ($recipient =~ /^<?postmaster\@mydomain\.com>?$/i) {
return ('CONTINUE', "ok");
}
return ('REJECT', 'Sorry; spammer@badguy.com is blacklisted.');
}
return ('CONTINUE', "ok");
}
๐งผ INITIALIZATION AND CLEANUP ๐งผ
Bestie, this is the before-and-after of worker life. Just before a worker begins processing messages, mimedefang.pl calls the functions filter_initialize (if it is defined) with no arguments. By the time filter_initialize is called, all the other initialization (such as setting up syslog facility and priority) has been done. Setup first, then your code. Order matters.
If you are not using an embedded Perl interpreter, then performing an action inside filter_initialize is practically the same as performing it directly in the filter file, outside any function definition. However, if you are using an embedded Perl interpreter, then anything you call directly from outside a function definition is executed once only in the parent process. Anything in filter_initialize is executed once per worker. Parent process vs worker process. Big difference. Pay attention.
If you use any code that opens a descriptor (for example, a connection to a database server), you must run that code inside filter_initialize and not directly from the filter, because the multiplexor closes all open descriptors when it activates a new worker. โ ๏ธ The multiplexor will absolutely close your descriptors. Do not fight it. Put that code in filter_initialize.
From within filter_initialize a configuration file could be loaded by calling read_config. read_config accepts a configuration file path and it can be used to overwrite global variables. Configuration file format is pure Perl code. Pure Perl. No secret JSON situation. We're not doing that.
When a worker is about to exit, mimedefang.pl calls the function filter_cleanup (if it is defined) with no arguments. This function can do whatever cleanup you like, such as closing file descriptors and cleaning up long-lived worker resources. The return value from filter_cleanup becomes the worker's exit status. (You should therefore ensure that filter_cleanup returns an integer suitable for a process exit status.) Integer exit status, bestie. Not vibes. An actual integer.
If filter_cleanup takes longer than 10 seconds to run, the worker is sent a SIGTERM signal. If that doesn't kill it (because you're catching signals, perhaps), then a further 10 seconds later, the worker is sent a SIGKILL signal. ๐ 10 seconds, then SIGTERM. Another 10 seconds, then SIGKILL. It's giving "we tried to be nice, now we're not."
๐ง CONTROLLING PARSING ๐ง
Okay so if you define a function called filter_create_parser taking no arguments, then mimedefang.pl will call it to create a MIME::Parser object for parsing mail messages. You get to build the parser. Main character energy.
Filter_create_parser is expected to return a MIME::Parser object (or an instance of a class derived from MIME::Parser). Subclass friendly. We love inheritance when it's not toxic.
You can use filter_create_parser to change the behavior of the MIME::Parser used by mimedefang.pl.
If you do not define a filter_create_parser function, then a built-in version equivalent to this is used:
sub filter_create_parser () {
my $parser = MIME::Parser->new();
$parser->extract_nested_messages(1);
$parser->extract_uuencode(1);
$parser->output_to_core(0);
$parser->tmp_to_core(0);
return $parser;
}
๐ EXTENDING MIMEDEFANG ๐
The man page for mimedefang-protocol(7) lists commands that are passed to workers in server mode (see "SERVER COMMANDS".) You can define a function called filter_unknown_cmd to extend the set of commands your filter can handle. Custom commands? We're expanding the lore.
If you define filter_unknown_cmd, it is passed the unknown command as a single argument. It should return a list of values as follows: The first element of the list must be either "ok" or "error:" (with the colon.) The remaining arguments are percent-encoded. All the resulting pieces are joined together with a single space between them, and the resulting string passed back as the reply to the multiplexor. Colon included. Do not forget the colon. It matters.
For example, the following function will make your filter reply to a "PING" command with "PONG":
sub filter_unknown_cmd ($) {
my($cmd) = @_;
if ($cmd eq "PING") {
return("ok", "PONG");
}
return("error:", "Unknown command");
}
You can test this filter by typing the following as root:
md-mx-ctrl PING
The response should be:
ok PONG
If you extend the set of commands using filter_unknown_cmd, you should make all your commands start with an upper-case letter to avoid clashes with future built-in commands. Upper-case first letter. Future-proof your vibes.
๐ซ REJECTING UNKNOWN USERS EARLY ๐ซ
okay bestie so picture this: super common setup where a MIMEDefang machine is out here being an SMTP proxy โ it accepts mail, scans it, then relays it to the actual mail server. sounds great right?? except ๐ the MIMEDefang box has no idea whether a local address is even real, so it just forwards everything for the domains it handles. and the moment mail shows up for a user who doesn't exist, the MIMEDefang machine is forced to spit out a bounce message when the relay attempt inevitably fails. embarrassing.
so what you actually want is for the MIMEDefang host to just reply with a "User unknown" SMTP response right away. you could do that by copying your whole local user list over to the MIMEDefang machine, but that's giving โจmaintenance nightmareโจ. instead, MIMEDefang ships a built-in function called md_check_against_smtp_server that goes and asks another relay host for you:
md_check_against_smtp_server($sender, $recip, $helo, $server, $port)
this function connects to the SMTP server $server and pretends to send mail from $sender to $recip. and the return value? always a two-element array, no cap. if the RCPT TO: command succeeds, you get back ("CONTINUE", "OK"). if the RCPT fails with a permanent failure, you get back ("REJECT", $msg), where $msg is the message the SMTP server handed you. and any temporary failures, connection errors, etc.? those come back as ("TEMPFAIL", $msg).
the optional argument $port is the TCP port you want to connect to. leave it out and it defaults to the classic SMTP port, 25.
if the server offers STARTTLS support, TLS step-up is attempted. and if TLS step-up fails, the check will fall-back to using clear text and log the failure. ๐
so like, say the machine filter.domain.tld is filtering mail destined for the real mail server mail.domain.tld. your filter_recipient function could look like this:
sub filter_recipient
{
my($recip, $sender, $ip, $host, $first, $helo,
$rcpt_mailer, $rcpt_host, $rcpt_addr) = @_;
return md_check_against_smtp_server($sender, $recip,
"filter.domain.tld",
"mail.domain.tld");
}
and for every single RCPT TO: command, MIMEDefang opens an SMTP connection to mail.domain.tld and checks whether that command would succeed. cute, right? ๐
โ ๏ธ ok pause, important tea: you should only usemd_check_against_smtp_serverif your mail server responds with a failure code for nonexistent users at theRCPT TO:level. also, this function may impose too much overhead if you receive a lot of e-mail, and it will generate lots of useless log entries on the real mail server (because of all thoseRCPT TO:probes). it may also significantly increase the load on the real mail server. so likeโฆ use it wisely, bestie. ๐
๐ธ GLOBAL VARIABLES YOU CAN SET ๐ธ
these Perl global variables should be set in mimedefang-filter:
$AdminAddressโ the e-mail address of the MIMEDefang administrator. ๐$DaemonAddressโ the e-mail address that MIMEDefang-originated notifications come from. like, the return address on all the daemon's little letters.$AddWarningsInlineโ if you set this to0, then all MIMEDefang warnings (the ones made byaction_quarantineoraction_drop_with_warning, for example) get collected together and added in a separate MIME part calledWARNING.TXT. if you set it to1instead, those warnings are added directly in the firsttext/plainandtext/htmlparts of the message. and if the message doesn't contain anytext/plainortext/htmlparts at all? then aWARNING.TXTMIME part gets added, same as before. no escaping it ๐$MaxMIMEPartsโ a message with a ton of MIME parts can makeMIME::Toolseat up huge amounts of memory and bring your whole system to its knees ๐ญ. so if you set$MaxMIMEPartsto a positive number, MIME parsing is terminated for messages with more than that many parts, and the message is bounced. and in that case, none of your filter functions is called. important!! by default,$MaxMIMEPartsis set to-1, meaning there is no limit on the number of parts in a message. also note: in order to use this variable, you must install the Roaring Penguin patched version ofMIME::Tools, version5.411a-RP-Patched-02or newer. ๐ง$Stupidity{"NoMultipleInlines"}โ set this to1if your e-mail is too stupid to display multiple MIME parts in-line. in that case, a nasty hack causes the first part of the original message to appear as an attachment if warnings are issued. mail clients that are not this stupid include Netscape Communicator and Pine. on the other hand, Microsoft Exchange and Microsoft Outlook are indeed this stupid. perhaps users of those clients should switch. periodt. ๐
the following global variables may optionally be set. if they are not set, sensible defaults are used:
$AddApparentlyToForSpamAssassinโ by default, MIMEDefang tries to pass SpamAssassin a message that looks exactly like one it would receive via procmail. that means adding aReceived:header, adding aMessage-IDheader if necessary, and adding aReturn-Path:header. if you set$AddApparentlyToForSpamAssassinto1, then MIMEDefang also adds anApparently-To:header with all the envelope recipients before passing the message to SpamAssassin. this lets SpamAssassin detect possibly whitelisted recipient addresses. the default value for$AddApparentlyToForSpamAssassinis0.$SyslogFacilityโ this specifies the logging facility used bymimedefang.pl. by default it's set to"mail", but you can set it to other possibilites. see theopenlog(3)man page for details. you should name facilities as all-lowercase without the leading"LOG_". that is, use"local3", not"LOG_LOCAL3". iykyk โจ$WarningLocation(default0) โ if set to0(the default), non-inline warnings are placed first. if you want the warning at the end of the e-mail, set$WarningLocationto-1.$DaemonName(default"MIMEDefang") โ the full name used when MIMEDefang sends out notifications.$AdminName(default"MIMEDefang Administrator") โ the full name of the MIMEDefang administrator. that's you, bestie ๐ซถ$SALocalTestsOnly(default1) โ if set to1, SpamAssassin calls will use only local tests. this is the default and recommended setting. it disables Received, RBL and Razor tests in an all or nothing fashion. to use Razor this MUST be set to0. you can add'skip_rbl_checks 1'to your SpamAssassin config file if you need to.$NotifySenderSubject(default"MIMEDefang Notification") โ the subject used when e-mail is sent out byaction_notify_sender(). if you set this, you should set it each time you callaction_notify_sender()to ensure consistency.$NotifyAdministratorSubject(default"MIMEDefang Notification") โ the subject used when e-mail is sent out byaction_notify_administrator(). if you set this, you should set it each time you callaction_notify_administrator()to ensure consistency.$QuarantineSubject(default"MIMEDefang Quarantine Report") โ the subject used when a quarantine notice is sent to the administrator. if you set this, you should set it each time you callaction_quarantine()oraction_quarantine_entire_message().$NotifyNoPreamble(default0) โ normally, notifications sent byaction_notify_sender()have a preamble warning about message modifications. if you do not want this, set$NotifyNoPreambleto1.$CSSHost(default127.0.0.1:7777:local) โ host and port for the Symantec CarrierScan Server virus scanner. this takes the formip_addr:port:local_or_nonlocal. theip_addrandportare the host and port on which CarrierScan Server is listening. if you want to scan local files, append:localto force the use of theAVSCANLOCALcommand. if the CarrierScan Server is on another host, append:nonlocalto force the file contents to be sent to the scanner over the socket. ๐$SophieSock(default/var/spool/MIMEDefang/sophie) โ socket used for Sophie daemon calls withinmessage_contains_virus_sophieandentity_contains_virus_sophieunless a socket is provided by the calling routine.$ClamdSock(default/var/spool/MIMEDefang/clamd.sock) โ socket used for clamd daemon calls withinmessage_contains_virus_clamdandentity_contains_virus_clamdunless a socket is provided by the calling routine.$TrophieSock(default/var/spool/MIMEDefang/trophie) โ socket used for Trophie daemon calls withinmessage_contains_virus_trophieandentity_contains_virus_trophieunless a socket is provided by the calling routine.
๐ FILTER ๐
Okay bestie, this is the moment. The absolute heart and soul of mimedefang-filter is the filter procedure. It's the main character of the whole page. ๐
Wanna learn how to write one? Peep the examples that shipped with MIMEDefang โ they're literally right there for you. โจ
The filter gets called with these arguments, in this exact order:
$entityโ theMIME::Entityobject. (Go read theMIME::toolsPerl module documentation for the full tea. โ)$fnameโ the suggested attachment filename, or""if none was supplied. ๐ซถ$extโ the file extension, which means all characters from the rightmost period to the end of the filename. ๐$typeโ the MIME type (like, for example,"text/plain"). ๐ธ
๐ Where the filename actually comes from ๐
The filename is derived like this, in order:
- First, if the
Content-Dispositionheader has a"filename"field, that one gets used. ๐ - Otherwise, if the
Content-Typeheader has a"name"field, that one gets used. โจ - Otherwise, the
Content-Descriptionheader value is used. ๐ฆ
Ngl, the truly paranoid among you will check all three of those fields for matches. Do with that what you will. ๐
๐ง The regex helpers ๐ง
Good news: the functions re_match and re_match_ext do the regular expression matching on all three of the fields named above for you, and they return 1 if any field matches. Check the sample filters for the deets. ๐
The calling sequence is exactly this:
re_match($entity, "regexp")
re_match_ext($entity, "regexp")
re_matchreturns true if any of the fields matches the regexp, without regard to case. ๐re_match_extreturns true if the extension in any field matches. An extension is defined as the last dot in a name plus all the remaining characters. โญ
๐๏ธ Peeking inside archives ๐๏ธ
A third function called re_match_in_zip_directory will look inside zip files and return true if any of the file names inside the zip archive match the regular expression. Call it like this, fr fr:
my $bh = $entity->bodyhandle();
my $path = (defined($bh)) ? $bh->path() : undef;
if (defined($path) and re_match_in_zip_directory($path, "regexp")) {
# Take action...
}
๐จ Bestie, listen: you should not call re_match_in_zip_directory unless you know that the entity is a zip file attachment. Like, actually know know. ๐
Another function called re_match_in_rar_directory will look inside rar files and return true if any of the file names inside the rar archive match the regular expression. It's very similar to re_match_in_zip_directory, but heads up โ the unrar binary is required and it must be specified in $Features{"unrar"}. ๐ฅ
And another one, re_match_in_7z_directory, will look inside 7zip files and return true if any of the file names inside the 7zip archive match the regular expression. Same energy as re_match_in_zip_directory, but the 7z binary is required and must be specified in $Features{"7zip"}. ๐
๐ GLOBAL VARIABLES SET BY MIMEDEFANG.PL ๐
okay bestie so mimedefang.pl sets a whole lil squad of global variables and they're all yours to use in your filter ๐ซถ. Every single one of these is always available to filter_begin, filter, filter_multipart and filter_end. And some of them โ the overachievers โ also show up in filter_relay, filter_sender or filter_recipient. Whenever that's the case, we say so below, mmkay? ๐
%Featuresโ this hash is your little runtime vibe check ๐ . It lets you figure out, while the thing is actually running, whether certain functionality is even available. It's there for you at all times, assumingdetect_and_load_perl_modules()got called. Here's the whole roster of defined features:$Features{"SpamAssassin"}is 1 if SpamAssassin 1.6 or better is installed; 0 otherwise. โจ$Features{"HTML::Parser"}is 1 if HTML::Parser is installed; 0 otherwise.$Features{"Virus:FPROTD"}is currently always 0. Set it to 1 in your filter file if you have F-Risk's FPROTD scanner earlier than version 6.$Features{"Virus:FPROTD6"}is currently always 0. Set it to 1 in your filter file if you have version 6 of F-Risk's FPROTD scanner.$Features{"Virus:SymantecCSS"}is currently always 0. Set it to 1 in your filter file if you have the Symantec CarrierScan Server virus scanner.$Features{"Virus:NAI"}is the full path to NAI uvscan if it is installed; 0 if it is not.$Features{"Virus:BDC"}is the full path to Bitdefender bdc if it is installed; 0 if it is not.$Features{"Virus:NVCC"}is the full path to Norman Virus Control nvcc if it is installed; 0 if it is not.$Features{"Virus:HBEDV"}is the full path to H+BEDV AntiVir if it is installed; 0 if it is not.$Features{"Virus:VEXIRA"}is the full path to Central Command Vexira if it is installed; 0 if it is not.$Features{"Virus:SOPHOS"}is the full path to Sophos sweep if it is installed; 0 if it is not.$Features{"Virus:SAVSCAN"}is the full path to Sophos savscan if it is installed; 0 if it is not.$Features{"Virus:CLAMAV"}is the full path to Clam AV clamscan if it is installed; 0 if it is not.$Features{"Virus:AVP"}is the full path to AVP AvpLinux if it is installed; 0 if it is not.$Features{"Virus:AVP5"}is the full path to Kaspersky "aveclient" if it is installed; 0 if it is not.$Features{"Virus:CSAV"}is the full path to Command csav if it is installed; 0 if it is not.$Features{"Virus:FSAV"}is the full path to F-Secure fsav if it is installed; 0 if it is not.$Features{"Virus:FPROT"}is the full path to F-Risk f-prot if it is installed; 0 if it is not.$Features{"Virus:FPSCAN"}is the full path to F-Risk fpscan if it is installed; 0 if it is not.$Features{"Virus:SOPHIE"}is the full path to Sophie if it is installed; 0 if it is not.$Features{"Virus:CLAMD"}is the full path to clamd if it is installed; 0 if it is not.$Features{"Virus:CLAMDSCAN"}is the full path to clamdscan if it is installed; 0 if it is not.$Features{"Virus:TROPHIE"}is the full path to Trophie if it is installed; 0 if it is not.$Features{"Virus:NOD32"}is the full path to ESET NOD32 nod32cli if it is installed; 0 if it is not.$Features{"Path:RSPAMC"}is the full path torspamc(1)if it is installed (deprecated); 0 if it is not. ๐
NOTE: Perl-module based features such as SpamAssassin are determined at runtime and may change as these are added and removed. Most Virus features are predetermined at the time of configuration and do not adapt to runtime availability unless changed by the filter rules. ๐ซก
$CWDโ this one holds the working directory for the current message. While filter processing is happening,mimedefang.plchdir's into this directory before calling any of thefilter_functions. Heads up though: this variable is set correctly infilter_senderandfilter_recipient, but not infilter_relay. ๐$SuspiciousCharsInHeadersโ if this is true, mimedefang found some suspicious characters chilling in the message headers. This might be an exploit for bugs in MIME-parsing routines in some badly-written mail user agents (e.g. Microsoft Outlook.) You should always drop such messages. no cap. ๐ฉ$SuspiciousCharsInBodyโ same energy, but for the message body. If it's true, mimedefang found suspicious characters in the body. Might be an exploit for bugs in MIME-parsing routines in some badly-written mail user agents (e.g. Microsoft Outlook.) You should always drop such messages too. ๐ฉ$RelayHostnameโ the host name of the relay. Like, the name of the host that is attempting to send e-mail to your host. May beundefif the host name could not be determined. This variable is available infilter_relay,filter_senderandfilter_recipientin addition to the body filtering functions. ๐ฆ$RelayAddrโ the IP address of the sending relay (as a string consisting of four dot-separated decimal numbers.) One potential use of$RelayAddris to limit mailing to certain lists to people within your organization. This variable is available infilter_relay,filter_senderandfilter_recipientin addition to the body filtering functions. ๐ธ$Heloโ the argument given to the SMTP"HELO"command. This variable is available infilter_senderandfilter_recipient, but not infilter_relay. ๐ญ$Subjectโ the contents of the"Subject:"header. โจ$Senderโ the sender of the e-mail. This variable is set infilter_senderandfilter_recipientin addition to the body filtering functions.@Recipientsโ a list of the recipients. Infilter_recipient, it is set to the single recipient currently under consideration. Or, after callingread_commands_filewithinfilter_recipient, the current recipient under consideration is in the final position of the array, at$Recipients[-1], while any previous (and accepted) recipients are at the beginning of the array, that is, in@Recipients[0 .. $#Recipients-1]. ๐$MessageIDโ the contents of the"Message-ID:"header if one is present. Otherwise, contains the string"NOQUEUE".$QueueIDโ the Sendmail queue identifier if it could be determined. This variable is set correctly infilter_relay,filter_helo,filter_senderandfilter_recipient. Note, however, that Postfix may not allocate a queue ID untilfilter_recipienttime. If a Queue-ID has not yet been allocated,$QueueIDis set to"NOQUEUE".$MsgIDโ set to$QueueIDif the queue ID could be determined; otherwise, set to$MessageID. This identifier should be used in logging, because it matches the identifier used by Sendmail to log messages. Note that this variable is set correctly infilter_senderandfilter_recipient, but it is not available infilter_relay. ๐$VirusScannerMessagesโ every time a virus-scanning function is called, messages (if any) from the virus scanner get accumulated in this variable. You can use it infilter_endto formulate a notification (if you wish.) ๐$VirusNameโ if a virus-scanning function found a virus, this variable will hold the virus name (if it could be determined.) ๐ฆ$SASpamTesterโ if defined, this is the configuredMail::SpamAssassinobject used for mail tests. It may be initialized with a call tospam_assassin_initwhich also returns it.%SendmailMacrosโ this hash contains the values of some Sendmail macros. The hash elements exist only for macros defined by Sendmail. See the Sendmail documentation for the meanings of the macros.
By default, mimedefang passes the values of the following macros: ${daemon_name}, ${daemon_port}, ${if_name}, ${if_addr}, $j, $_, $i, ${tls_version}, ${cipher}, ${cipher_bits}, ${cert_subject}, ${cert_issuer}, ${auth_type}, ${auth_authen}, ${auth_ssf}, ${auth_author}, ${mail_mailer}, ${mail_host} and ${mail_addr}. In addition, ${client_port} is set to the client's TCP port.
If any macro is not set or not passed to milter, it will be unavailable. To access the value of a macro, use:
$SendmailMacros{"macro_name"}
Do not place curly brackets around the macro name. This variable is available in filter_sender and filter_recipient after a call to read_commands_file. ๐ฅ
@SenderESMTPArgsโ this array contains all the ESMTP arguments supplied in theMAIL FROM:command. For example:
sub print_sender_esmtp_args {
foreach (@SenderESMTPArgs) {
print STDERR "Sender ESMTP arg: $_0;
}
}
%RecipientESMTPArgsโ this hash contains all the ESMTP arguments supplied in eachRCPT TO:command. For example:
sub print_recip_esmtp_args {
foreach my $recip (@Recipients) {
foreach(@{$RecipientESMTPArgs{$recip}}) {
print STDERR "Recip ESMTP arg for $recip: $_0;
}
}
}
%RecipientMailersโ this hash contains the Sendmail"mailer-host-address"triple for each recipient. Here's an example of how to use it:
sub print_mailer_info {
my($recip, $mailer, $host, $addr);
foreach $recip (@Recipients) {
$mailer = ${RecipientMailers{$recip}}[0];
$host = ${RecipientMailers{$recip}}[1];
$addr = ${RecipientMailers{$recip}}[2];
print STDERR "$recip: mailer=$mailer, host=$host, addr=$addr\n";
}
}
In filter_recipient, this variable by default only contains information on the recipient currently under investigation. Information on all recipients is available after calling read_commands_file. ๐
๐ฌ ACTIONS ๐ฌ
so when your filter procedure decides how it's gonna dispose of a part, it should call one or more action_ subroutines. these are them, bestie โ go off:
action_accept()โ Accept the part. ๐action_rebuild()โ bestie, this one forces a rebuild of the mail body even when mimedefang thinks nothing changed. Normally mimedefang is like "no edits, no rebuild, we're good ๐ " but if you're out here changing entities directly (say, by messing withMIME::Headwith your own two hands), mimedefang has no idea. It won't know unless you callaction_rebuild. Heads up though โ every single built-inaction_...routine that changes a message already callsaction_rebuildimplicitly, so this is only for your DIY moments. โจaction_add_header($hdr, $val)โ adds a header to the message. Works infilter_beginorfilter_end.$hdris the header name without the colon, and$valis the header value. So say you want to add:
X-MyHeader: A nice piece of text
you'd do:
action_add_header("X-MyHeader", "A nice piece of text");
action_change_header($hdr, $val, $index)โ changes a header that's already in the message. Also fine infilter_beginorfilter_end.$hdris the header name without the colon,$valis the value. And if that header doesn't exist? No drama, it just gets added with that name and value. ๐ซถ
The $index parameter is optional and defaults to 1. If you pass it, then the $index'th occurrence of the header gets changed โ relevant when there's more than one header with the same name (super common with the Received: header, iykyk).
action_insert_header($hdr, $val, $index)โ adds a header into the message at position$index. A position of0means "prepend it before the existing headers, main character entrance โจ". Works infilter_beginorfilter_end.$hdris the header name without the colon,$valis the header value.action_delete_header($hdr, $index)โ deletes an existing header from the message. Yep,filter_beginorfilter_endboth chill.$hdris the header name without the colon.
$index is optional, defaults to 1. Pass it and the $index'th occurrence gets deleted, for when there's more than one header sharing a name. Bye bestie ๐
action_delete_all_headers($hdr)โ deletes every header with that name.filter_beginorfilter_end, your call.$hdris the header name without the colon. Scorched earth, no survivors, periodt.action_drop()โ drops the part. And if you call it fromfilter_multipart, it drops all the contained parts too. Nested carnage, love that for us. ๐ฆaction_drop_with_warning($msg)โ drops the part, but tacks the warning$msgonto the e-mail message so somebody knows. Called fromfilter_multipart? All contained parts get dropped too.action_accept_with_warning($msg)โ accepts the part (it stays!), but adds the warning$msgto the e-mail message.action_replace_with_warning($msg)โ drops the part and swaps it out for a text part containing$msg. Fromfilter_multipart, all contained parts get dropped too.action_replace_with_url($entity, $doc_root, $base_url, $msg, [$cd_data, $salt])โ drops the part, but saves it in a unique location under$doc_root. The part gets replaced with the text message$msg. And here's the cute part: the string"_URL_"inside$msggets replaced with$base_url/something, which you can use to go retrieve the message. ๐
Do not use this function in filter_multipart.
The whole vibe of this action is stripping big chunky parts out of a message and replacing them with a link on a Web server. Here's how you'd use it in filter():
$size = (stat($entity->bodyhandle->path))[7];
if ($size > 1000000) {
return action_replace_with_url($entity,
"/home/httpd/html/mail_parts",
"http://mailserver.company.com/mail_parts",
"The attachment was larger than 1,000,000 bytes.\n" .
"It was removed, but may be accessed at this URL:\n\n" .
"\t_URL_\n");
}
So this example moves attachments bigger than 1,000,000 bytes into /home/httpd/html/mail_parts and replaces them with a link. That directory should be reachable via a Web server at http://mailserver.company.com/mail_parts.
The generated name comes from running a SHA1 hash of the part and adding the extension onto the ASCII-HEX representation of that hash. And the genuinely iconic side effect: if tons of different e-mails carry an identical large part, only one copy of the part gets stored โ no matter how many senders or recipients there are. Deduplication queen. ๐
For privacy reasons you must turn off Web server indexing in the directory where you stash mail parts, otherwise literally anyone can read them. With indexing disabled, an attacker would have to guess the SHA1 hash of a part to read it. Keep it locked. ๐
Optionally, a fifth argument can supply data to be saved into a hidden dot filename based on the generated name. A CGI script or mod_perl module can then read that data in on the fly before serving the file to a web client, and use it to add info to the response โ stuff like Content-Disposition data.
A sixth optional argument, $salt, gets mixed into the SHA1 hash. The salt can be any string and should be kept confidential. Why? It stops people from guessing whether or not a particular attachment has been received on your server by messing with the SHA1 hash calculation. Secret seasoning, fr fr. ๐ง
action_defang($entity, $name, $fname, $type)โ accepts the part, but renames it: its name becomes$name, its suggested filename becomes$fname, and its MIME type becomes$type. If$nameor$fnameare"", thenmimedefang.plgenerates generic names for you. Do not use this action infilter_multipart.
If you use action_defang, you must define a subroutine called defang_warning in your filter. It takes two arguments: $oldfname (the original name of an attachment) and $fname (the defanged version). It should return a message telling the user what happened. Like so:
sub defang_warning {
my($oldfname, $fname) = @_;
return "The attachment '$oldfname' was renamed to '$fname'\n";
}
action_external_filter($entity, $cmd)โ runs an external UNIX command$cmd. That command must read the part from the file./FILTERINPUTand leave the result in./FILTEROUTPUT. If it executes successfully, returns1; otherwise0. You can check the return value and call anotheraction_if the filter failed. Do not use this action infilter_multipart.action_quarantine($entity, $msg)โ drops and quarantines the part, but adds the warning$msgto the e-mail message. Gone but not forgotten. ๐action_quarantine_entire_message($msg)โ quarantines the entire message into a quarantine directory on the mail server, but otherwise doesn't touch how the message is disposed of. If"$msg"is non-empty, it gets included in any administrator notification.action_sm_quarantine($reason)โ quarantines a message in the Sendmail mail queue using the newQUARANTINEfacility of Sendmail 8.13. Go read the Sendmail docs for the deets on that facility. And if you useaction_sm_quarantinewith a version of Sendmail that lacks theQUARANTINEfacility, mimedefang will log an error message and not quarantine the message. No silent fails, at least. ๐ญaction_bounce($reply, $code, $dsn)โ rejects the entire e-mail message with an SMTP failure code and the one-line error message$reply. If the optional$codeand$dsnarguments are supplied, they set the numerical SMTP reply code and the extended status code (DSN code). If the codes you hand over don't make sense for a bounce, they get swapped for"554"and"5.7.1"respectively. The code police said no. ๐
action_bounce just makes a note that the message is to be bounced โ the remaining parts still get processed. If action_bounce is called for more than one part, the mail is bounced with the message from the final call to action_bounce. You can profitably call action_quarantine followed by action_bounce if you want to keep a copy of the offending part. And note: the message is not bounced immediately โ remaining parts get processed first, and the bounce happens after all parts have been processed.
Also, despite the name, action_bounce does not generate a "bounce message". It merely rejects the message with an SMTP failure code. Naming is a scam sometimes. ๐
WARNING: action_bounce() may cause the sending relay to generate spurious bounce messages if the sender address is faked. This is a particular problem with viruses. However, we believe that on balance, it's better to bounce a virus than to silently discard it. It's almost never a good idea to hide a problem.
action_tempfail($msg, $code, $dsn)โ causes an SMTP "temporary failure" code to come back, so the sending mail relay requeues the message and tries again later. The message$msgis included with the temporary failure code. If the optional$codeand$dsnarguments are supplied, they set the numerical SMTP reply code and the extended status code (DSN code). If the codes you supply don't make sense for a temporary failure, they get replaced with"450"and"4.7.1"respectively.action_discard()โ silently discards the message and notifies absolutely nobody. ๐ป You can profitably callaction_quarantinefollowed byaction_discardif you want to keep a copy of the offending part. And note: the message isn't discarded immediately โ remaining parts are processed, and the message is discarded after all parts have been processed.action_notify_sender($message)โ sends an e-mail back to the original sender with the indicated message. You may call another action after this one. Ifaction_notify_senderis called more than once, the messages accumulate into a single e-mail message โ at most one notification message is sent per incoming message. The message should be terminated with a newline.
The notification is delivered in deferred mode, so you should run a client-queue runner if you're using Sendmail 8.12. Don't leave it hanging bestie ๐ซถ
๐ real talk bestie: viruses be out here faking sender addresses like it's nothing. so if a virus-scanner already caught a virus, action_notify_sender gets disabled and will literally just log an error message at you if you try to use it. not a vibe, but it is what it is. ๐
action_notify_administrator($message)โ this one e-mails the MIMEDefang administrator whatever$messageyou hand it. ๐ and yes, you can call another action right after this one, becauseaction_notify_administratordoes not mess with mail processing at all. if you call it more than once, all the messages get accumulated into a single e-mail message โ so at most one notification message goes out per incoming message. make sure$messageends with a newline, okay?
โจ heads up: the notification is delivered in deferred mode, so if you're running Sendmail 8.12 you should run a client-queue runner. no cap.
append_text_boilerplate($entity, $boilerplate, $all)โ this one is strictly afilter_endgirlie, so only call it from there. it appends the text"\n$boilerplate\n"to the firsttext/plainpart (when$allis0) or to all thetext/plainparts (when$allis1). slay. ๐append_html_boilerplate($entity, $boilerplate, $all)โ also only fromfilter_end, bestie. it adds the text"\n$boilerplate\n"to the firsttext/htmlpart (if$allis0) or to all thetext/htmlparts (if$allis1). and the way this function is smart about inserting the boilerplateโฆ ๐ง it usesHTML::Parserto detect closing tags, then inserts the boilerplate before the</body>tag if there is one, or before the</html>tag if there's no</body>. if there's no</body>or</html>tag at all, it just appends the boilerplate to the end of the part.
๐จ do not useappend_html_boilerplateunless you have theHTML::ParserPerl module installed. fr fr.
here's a lil example showing how to use the boilerplate functions, iconically:
sub filter_end {
my($entity) = @_;
append_text_boilerplate($entity,
"Lame text disclaimer", 0);
append_html_boilerplate($entity,
"<em>Lame</em> HTML disclaimer", 0);
}
action_add_part($entity, $type, $encoding, $data, $fname, $disposition [, $offset])โfilter_endonly, babes, don't try this anywhere else. it adds a new part to the message, converting the original message to multipart if it needs to. ๐ the function returns the part so you can set extra mime attributes on it. here's the example:
sub filter_end {
my($entity) = @_;
action_add_part($entity, "text/plain", "-suggest",
"This e-mail does not represent" .
"the official policy of FuBar, Inc.\n",
"disclaimer.txt", "inline");
}
the $entity parameter must be the argument passed in to filter_end. the $offset parameter is optional; if you omit it, it defaults to -1, which adds the new part at the end. for what $offset actually means, go check the MIME::Entity man page and the add_part member function. ๐
also note that action_add_part tries to be more intelligent than just calling $entity->add_part. the decision process goes like this:
- if the top-level entity is
multipart/mixed, then the part is simply added. - otherwise, a new top-level
multipart/mixedcontainer is generated, and the original top-level entity is made the first part of themultipart/mixedcontainer. the new part then gets added to themultipart/mixedcontainer. ๐ฆ
action_add_entity($entity [, $offset])โ this is similar toaction_add_part, but it takes a pre-builtMIME::Entityobject instead of constructing one from the$type,$encoding,$data,$fnameand$dispositionarguments. cute, right? โจ
๐ USEFUL ROUTINES ๐
okay bestie so mimedefang.pl comes with a whole lil toolkit of functions you can justโฆ call from your filter? like they're already there for you. no cap. let's go through them one by one ๐ซถ
detect_and_load_perl_modules()โ okay so unless you genuinely know what you're doing (and I mean fr fr know), you must call this bad boy as the very first thing in your filter file. it makesmimedefang.plgo detect and load Perl modules likeMail::SpamAssassin,Net::DNS, etc., and it populates the%Featureshash. it's giving "wake up babe, we're loading modules" โจsend_quarantine_notifications()โ this one gets called fromfilter_end. if any parts were quarantined, a quarantine notification gets sent to the MIMEDefang administrator. and like, important: if you don't callsend_quarantine_notifications, then no quarantine notifications get sent. periodt. so don't forget her ๐get_quarantine_dir()โ returns the full path name of the quarantine directory. and if you haven't quarantined any parts of the message yet? it just creates a quarantine directory and hands you its pathname. she's proactive like that ๐change_sender($sender)โ changes the envelope sender to$sender. you can only call this fromfilter_beginor any later function. also heads up: this is only supported with Sendmail/Milter 8.14.0 or newer. if you're running older versions it literally has no effect. sad but true ๐ญadd_recipient($recip)โ adds$recipto the list of envelope recipients. a copy of the message (after any modifications by MIMEDefang) gets sent to$recipin addition to the original recipients. now here's the tea:add_recipientdoes not modify the@Recipientsarray โ it just makes a note to Sendmail to add the recipient. it's a whisper, not a rewrite ๐คซdelete_recipient($recip)โ deletes$recipfrom the list of recipients, so that person won't get a copy of the mail.$recipshould exactly match an entry in the@Recipientsarray fordelete_recipient()to actually work. and again:delete_recipientdoes not modify the@Recipientsarray โ it just makes a note to Sendmail to delete the recipient. same energy as above, opposite direction ๐resend_message($recip1, $recip2, ...)orresend_message(@recips)โ immediately resends the original, unmodified mail message to each of the named recipients. the sender's address is preserved.
โ ๏ธ bestie be so careful with this one. it resends the original message, which may contain undesired attachments. and you should not call it fromfilter(), because it resends the message each time it's called โ which may result in multiple copies being sent if you're not careful. call it fromfilter_begin()orfilter_end()to be safe. i'm begging ๐ฅบ
the function returns true on success, or false if it fails.
also note: resend_message delivers the mail in deferred mode (using Sendmail's -odd flag). you must run a client-submission queue processor if you use Sendmail 8.12. we recommend executing this command as part of the Sendmail startup sequence:
sendmail -Ac -q5m
remove_redundant_html_parts($entity)โ only call this fromfilter_end. it removes redundant HTML parts from the message. how? it deletes any part of typetext/htmlfrom the message if (1) it's a sub-part of amultipart/alternativepart, and (2) there's another part of typetext/plainunder themultipart/alternativepart. decluttering queen ๐งนโจreplace_entire_message($entity)โ can only be called fromfilter_end. it replaces the entire message with$entity, aMIME::Entityobject that you have constructed. you can use any of theMIME::Toolsfunctions to build that entity. main character energy, you're literally rebuilding the whole message ๐read_commands_file()โ should only be called fromfilter_senderandfilter_recipient. this reads theCOMMANDSfile (as described inmimedefang-protocol(7)) and fills or updates the following global variables:$Sender,@Recipients,%RecipientMailers,$RelayAddr,$RealRelayAddr,$RelayHostname,$RealRelayHostname,$QueueID,$Helo,%SendmailMacros.
and like, if you don't call read_commands_file, then the only info available in filter_sender and filter_recipient is whatever got passed as an argument to the function. that's it. that's the whole vibe check ๐ธ
stream_by_domain()โ okay so do not use this function unless you have Sendmail 8.12 and locally-submitted e-mail is submitted using SMTP.
this one should only be called at the very beginning of filter_begin(), like this:
sub filter_begin {
if (stream_by_domain()) {
return;
}
# Rest of filter_begin
}
stream_by_domain() looks at all the recipients of the message, and if they belong to the same domain (e.g., joe@domain.com, jane@domain.com and sue@domain.com), it returns 0 and sets the global variable $Domain to the domain (domain.com in this example.)
if users are in different domains, stream_by_domain() resends the message (once to each domain) and returns 1 For example, if the original recipients are joe@abc.net, jane@xyz.net and sue@abc.net, the original message is resent twice: one copy to joe@abc.net and sue@abc.net, and another copy to jane@xyz.net. also, any subsequent scanning is canceled (filter() and filter_end() will not be called for the original message) and the message is silently discarded. gone. poof ๐จ
if you have Sendmail 8.12, then locally-submitted messages are sent via SMTP, and MIMEDefang will be called for each resent message. it's possible to set up Sendmail 8.12 so locally-submitted messages are delivered directly โ but in that case, stream_by_domain will not work. so like, pick your setup wisely ๐
using stream_by_domain lets you customize your filter rules for each domain. if you use the function as described above, you can do this in your filter routine:
sub filter {
my($entity, $fname, $ext, $type) = @_;
if ($Domain eq "abc.com") {
# Filter actions for abc.com
} elsif ($Domain eq "xyz.com") {
# Filter actions for xyz.com
} else {
# Default filter actions
}
}
you cannot rely on $Domain being set unless you have called stream_by_domain(). don't assume, bestie โ
stream_by_recipient()โ same deal: do not use this function unless you have Sendmail 8.12 and locally-submitted e-mail is submitted using SMTP.
this function should only be called at the very beginning of filter_begin(), like this:
sub filter_begin {
if (stream_by_recipient()) {
return;
}
# Rest of filter_begin
}
if there's more than one recipient, stream_by_recipient() resends the message once to each recipient. that way you can customize your filter rules on a per-recipient basis. lowkey though โ this may increase the load on your mail server considerably. she's thorough but she's heavy ๐ฎโ๐จ
also, a "recipient" is determined before alias expansion. so "all@mydomain.com" is considered a single recipient, even if Sendmail delivers to a list. one name, one recipient, that's the rule ๐ฆ
if you have Sendmail 8.12, then locally-submitted messages are sent via SMTP, and MIMEDefang will be called for each resent message. it's possible to set up Sendmail 8.12 so locally-submitted messages are delivered directly โ but in this case, stream_by_recipient() will not work. same as her sister function fr fr ๐
stream_by_recipient() lets you customize your filter rules for each recipient in a manner similar to stream_by_domain(). iconic duo honestly ๐
๐ธ LOGGING ๐ธ
ok so bestie, sometimes you just wanna know what's going on, right? this section is all about writing stuff to syslog so your filter can have a lil diary ๐
md_graphdefang_log_enable($facility, $enum_recips)โ this turns ON themd_graphdefang_logfunction (yes, the one right below, keep reading ๐ ). it logs to syslog using whatever facility you pass in. if you leave out$facility, it defaults to'mail'. and here's the tea: if you never callmd_graphdefang_log_enablein your filter at all, then every single call tomd_graphdefang_logjust... does nothing. silently. zero notes. ๐ซฅ
if you pass $enum_recips as 1, then you get a whole line of logging for each recipient of a mail message. if it's zero, you get just one single line per message. and if you don't pass $enum_recips at all, it defaults to 1.
md_graphdefang_log($event, $v1, $v2, $utf8_decode, $per_message)โ logs an event, with up to two optional extra params. the log message has this very specific format that graphing tools love, and it looks like this:
MDLOG,msgid,event,v1,v2,sender,recipient,subj
MDLOG is literal text, like, just those characters, no thoughts. msgid is the Sendmail queue identifier. event is the event name, and v1 and v2 are the extra params. sender is the sender's e-mail address, recipient is the recipient's e-mail address, and subj is the message subject. now if a message has more than one recipient, md_graphdefang_log might log an event message for each recipient โ depends on how you called md_graphdefang_log_enable, iykyk.
the optional $per_message param (that's the fifth argument, default 0) overrides per-recipient enumeration for one single call. when you set it to 1, exactly one syslog line gets emitted no matter what the $EnumerateRecipients setting says โ the actual recipient address is used when there's a single recipient, otherwise "rcpts=N" is used. this is the move for message-level events like mail_in, mail_out, and non-spam, which are counters for the whole message rather than per-user stats. example time:
md_graphdefang_log('mail_in', undef, undef, 0, 1);
โ ๏ธ heads up bestie:md_graphdefang_logshould not be used infilter_relay,filter_senderorfilter_recipient. the global variables it relies on are just not valid in that context. it's giving "wrong place, wrong time" ๐
and if you just want to log general text strings? do not use md_graphdefang_log. use md_syslog instead (it's right below, don't worry ๐).
md_graphdefang_log_array($event, $utf8_decode, \$values, $per_message)โ a variant ofmd_graphdefang_logthat takes an array reference instead of two fixed positional values, so you can log literally any number of extra fields. obsessed.
the log line has the same format as md_graphdefang_log, but the comma-separated value fields come from the array:
MDLOG,msgid,event,val1,val2,...,valN,sender,recipient,subj
the parameters are:
$eventโ the event name (e.g.'virus','spam').$utf8_decodeโ if set to1, MIME-decode and UTF-8 encode the event name, subject, and each value before logging. defaults to0.\$valuesโ a reference to an array of additional values associated with the event. each element is percent-encoded. pass a reference to an empty array if there are no additional values.$per_messageโ optional; defaults to0. when set to1, exactly one syslog line is emitted regardless of the$EnumerateRecipientssetting, the actual recipient address is used when there's a single recipient, otherwise"rcpts=N"is used.
Examples:
my @info = ($virus_name, $scanner);
md_graphdefang_log_array('virus', 0, \@info);
my @info = ($score, $tests);
md_graphdefang_log_array('spam', 1, \@info, 0);
โ ๏ธ same warning as before, no cap:md_graphdefang_log_arrayshould not be used infilter_relay,filter_senderorfilter_recipient. the global variables it relies on are not valid in that context. โจ
md_syslog($level, $msg)โ logs the message$msgto syslog, using level$level. the level is a literal string, and it should be one of'err','debug','warning',ยดemerg','crit','notice'or'info'. (peepsyslog(3)for the details ๐)
ok important note: md_syslog does not do %-subsitutions the way syslog(3) does. and depending on your Perl installation, md_syslog boils down to a call to Unix::Syslog::syslog or Sys::Syslog::syslog. check the Unix::Syslog or Sys::Syslog man pages for more deets.
md_openlog($tag, $facility)โ sets the tag used in syslog messages to$tag, and sends the logs to the$facilityfacility. and here's the thing: if you never callmd_openlogbefore you callmd_syslog, then it gets called implicitly anyway, with$tagset tomimedefang.pland$facilityset tomail. it literally does it for you, main character behavior ๐
๐ RBL LOOKUP FUNCTIONS ๐
mimedefang.pl includes these functions for looking up IP addresses in DNS-based real-time blacklists. and listen โ the "relay_is_blacklisted" functions are deprecated and might get removed in a future release. instead, you should use the module Net::DNSBL::Client from CPAN. the way that deprecated function is still here but on thin ice ๐ญ
relay_is_blacklisted($relay, $domain)โ this checks a DNS-based real-time spam blacklist, and returns true if the relay host is blacklisted, or false otherwise. (fr fr, the return value is whatever the blacklist returns as a resolved hostname, like"127.0.0.4")
ngl, relay_is_blacklisted uses the built-in gethostbyname function; this is usually pretty inefficient and it does not let you set a timeout on the lookup. instead, we recommend using one of the other DNS lookup functions described in this section. (tho quick note: the other functions require the Perl Net::DNS module, whereas relay_is_blacklisted doesn't need it. tradeoffs, bestie ๐ธ)
here's an example of how to use relay_is_blacklisted:
if (relay_is_blacklisted($RelayAddr, "rbl.spamhaus.org")) {
action_add_header("X-Blacklist-Warning",
"Relay $RelayAddr is blacklisted by Spamhaus");
}
relay_is_blacklisted_multi($relay, $timeout, $answers_wanted, [$domain1, $domain2, ...], $res)โ this one is similar torelay_is_blacklisted, except it takes a timeout argument (in seconds) and an array of domains to check. it checks all the domains in parallel, and it's guaranteed to return within$timeoutseconds. (ok actually, it might take up to one second longer. technicalities ๐)
the parameters are:
$relayโ the IP address you want to look up.$timeoutโ a timeout in seconds after which the function should return.$answers_wantedโ the maximum number of positive answers you care about. for example, if you're looking up an address in 10 different RBLs but you're gonna bounce it if it's on four or more, you can set$answers_wantedto4, and the function returns as soon as four "hits" are discovered. if you set$answers_wantedto zero, then the function does not return early.[$domain1, $domain2, ...]โ a reference to an array of strings, where each string is an RBL domain.$resโ aNet::DNS::Resolverobject. this argument is optional; if you don't supply it, thenrelay_is_blacklisted_multiconstructs its own resolver. it's very self-sufficient like that ๐
the return value is a reference to a hash; the keys of the hash are the original domains, and the corresponding values are either SERVFAIL, NXDOMAIN, or a list of IP addresses in dotted-quad notation.
here's an example:
$ans = relay_is_blacklisted_multi($RelayAddr, 8, 0,
["sbl.spamhaus.org", "relays.ordb.org"]);
foreach $domain (keys(%$ans)) {
$r = $ans->{$domain};
if (ref($r) eq "ARRAY") {
# It's an array -- it IS listed in RBL
print STDERR "Lookup in $domain yields [ ";
foreach $addr (@$r) {
print STDERR $addr . " ";
}
print STDERR "]\n";
} else {
# It is NOT listed in RBL
print STDERR "Lookup in $domain yields "
. $ans->{$domain} . "\n";
}
}
you should compare each $ans->{$domain} to "SERVFAIL" and "NXDOMAIN" to see if the relay is not listed. any other return value will be an array of IP addresses indicating that the relay is listed. ๐ฅ
any lookup that doesn't succeed within $timeout seconds has the corresponding return value set to SERVFAIL.
relay_is_blacklisted_multi_list($relay, $timeout, $answers_wanted, [$domain1, $domain2, ...], $res)โ this function is similar torelay_is_blacklisted_multiexcept that the return value is simply an array of RBL domains in which the relay was listed. simple, clean, iconic โจrelay_is_blacklisted_multi_count($relay, $timeout, $answers_wanted, [$domain1, $domain2, ...], $res)โ this function is similar torelay_is_blacklisted_multiexcept that the return value is an integer specifying the number of domains on which the relay was blacklisted. just a number, bestie. that's it ๐ขmd_get_bogus_mx_hosts($domain)
this function looks up all the MX records for the specified domain (or A records if there are no MX records) and returns a list of "bogus" IP addresses found amongst the records. a "bogus" IP address is an IP address in a private network (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), the loopback network (127.0.0.0/8), local-link for auto-DHCP (169.254.0.0/16), IPv4 multicast (224.0.0.0/4) or reserved (240.0.0.0/4). so many ways to be sus, we love to see the list ๐
here's how you might use the function in filter_sender:
sub filter_sender {
my ($sender, $ip, $hostname, $helo) = @_;
if ($sender =~ /@([^>]+)/) {
my $domain = $1;
my @bogushosts = md_get_bogus_mx_hosts($domain);
if (scalar(@bogushosts)) {
return('REJECT', "Domain $domain contains bogus MX record(s) " .
join(', ', @bogushosts));
}
}
return ('CONTINUE', 'ok');
}
โญ TEST FUNCTIONS โญ
mimedefang.pl includes some "test" functions, and yes they're basically the vibe-check squad ๐ง
md_version()โ returns the version of MIMEDefang as a string (for example,"3.7.1"). cute lil flex ๐message_rejected()โ returns true if any ofaction_tempfail,action_bounceoraction_discardhave been called for this message; returns false otherwise. so like, a lil "did we yeet it?" check ๐
okay bestie, sit down, hydrate ๐ซถ โ if you have the Mail::SpamAssassin Perl module installed (peep https://spamassassin.apache.org), you can call literally any of the spam_assassin_* functions. But like, lowkey important: only call them from filter_begin or filter_end, because they operate on the entire message at once. Doing it mid-filter is not the vibe, it's giving chaos. ๐ญ
Most of these take an optional config file. If you don't pass one, mimedefang.pl goes looking for one of four default SpamAssassin preference files, and the first one it finds wins:
/etc/mail/sa-mimedefang.cf/etc/mail/spamassassin/sa-mimedefang.cf/etc/mail/spamassassin/local.cf/etc/mail/spamassassin.cf
Important Note: MIMEDefang does not let SpamAssassin modify messages. Not a single character, not a single header. ๐ซ If you want to tag spam with special headers or alter the subject line, you have to do it with MIMEDefang functions yourself. Setting SpamAssassin configuration options to alter messages? Will. Not. Work. periodt.
spam_assassin_is_spam([ $config_file ])
Determines if the current message is SPAM/UCE as determined by SpamAssassin. It compares the score of the message against the threshold score (see below) and returns true if it is. Uses spam_assassin_check below. โจ
spam_assassin_check([ $config_file ])
Returns a four-element list, shaped like this:
($hits, $required, $tests, $report)
$hitsโ the "score" SpamAssassin gave the message (higher score = more likely SPAM).$requiredโ how many hits are required before SpamAssassin concludes the message is SPAM.$testsโ a comma-separated list of SpamAssassin test names.$reportโ text detailing which tests triggered and their point score.
Basically it hands you the receipts on why SpamAssassin said SPAM. ๐
Uses spam_assassin_status below.
spam_assassin_status([ $config_file ])
Returns a Mail::SpamAssasin::PerMsgStatus object. Read the SpamAssassin documentation for details about this object. You are responsible for calling the finish method when you are done with it โ no leaving it hanging, bestie, that's rude to your memory. ๐ง Uses spam_assassin_init and spam_assassin_mail below.
spam_assassin_init([ $config_file ])
Returns the new global Mail::SpamAssassin object with the specified or default config (the ones outlined above). If the global object is already defined, it just returns it โ it does not change config files! ๐ค The object can be used to perform other SpamAssassin related functions.
spam_assassin_mail()
Returns a Mail::SpamAssassin::NoMailAudit object with the current email message contained in it. You can use it to perform other SpamAssassin related functions.
rspamd_check([ $uri ]) experimental
โ ๏ธ experimental โ tread carefully, bestie.
Returns a six-element list, shaped like this:
($hits, $required, $tests, $report, $action, $is_spam)
$hitsโ the "score" Rspamd gave the message (higher score = more likely SPAM).$requiredโ the number of hits required before Rspamd concludes that the message is SPAM.$testsโ a list of Rspamd test names.$reportโ text detailing which tests triggered and their point score. If JSON and LWP modules are present,$reportwill be a json string.$actionโ the action thatrspamd(8)wants to apply.$is_spamโ a boolean value (true/false) that determines if the message is spam or not.
Gives you insight into why Rspamd concluded the message is SPAM. ๐ฅ
md_copy_orig_msg_to_work_dir()
Normally, virus-scanners are passed only the unpacked, decoded parts of a MIME message. If you want to pass the original, undecoded message in as well, call md_copy_orig_msg_to_work_dir prior to calling message_contains_virus.
md_copy_orig_msg_to_work_dir_as_mbox_file()
Same energy as above โ normally virus-scanners only get the unpacked, decoded parts of a MIME message. If you want to pass the original, undecoded message in as a UNIX-style "mbox" file, call md_copy_orig_msg_to_work_dir_as_mbox_file prior to calling message_contains_virus.
The only difference between this function and md_copy_orig_msg_to_work_dir is that this one prepends a From_ line so the message looks like a UNIX-style mbox file. That's required for some virus scanners (such as Clam AntiVirus) to recognize the file as an e-mail message. ๐ฆ
message_contains_virus()
This one runs every installed virus-scanner and returns the scanner results. Call it in list context; the return value is a three-element list:
($code, $category, $action)
$codeโ the actual return code from the virus scanner.$categoryโ a string categorizing the return code:"ok"โ no viruses detected. ๐"not-installed"โ indicated virus scanner is not installed."cannot-execute"โ for some reason, the scanner could not be executed."virus"โ a virus was found. ๐"suspicious"โ a "suspicious" file was found."interrupted"โ scanning was interrupted."swerr"โ an internal scanner software error occurred.
$actionโ a string containing the recommended action:"ok"โ allow the message through unmolested."quarantine"โ a virus was detected; quarantine it."tempfail"โ something went wrong; tempfail the message.
The scanner-specific message_contains_virus_* squad ๐
message_contains_virus_trend()message_contains_virus_nai()message_contains_virus_bdc()message_contains_virus_nvcc()message_contains_virus_csav()message_contains_virus_fsav()message_contains_virus_hbedv()message_contains_virus_vexira()message_contains_virus_sophos()message_contains_virus_clamav()message_contains_virus_clamdscan()message_contains_virus_avp()message_contains_virus_avp5()message_contains_virus_fprot()message_contains_virus_fpscan()message_contains_virus_fprotd()message_contains_virus_fprotd_v6()message_contains_virus_nod32()
These functions should be called in list context. They use the indicated anti-virus software to scan the message for viruses. They're intended for use in filter_begin() to make an initial scan of the e-mail message. ๐
The supported virus scanners are:
naiโ NAI "uvscan" โhttp://www.nai.com/Bitdefender "bdc"โhttp://www.bitdefender.com/csavโ Command Anti-Virus โhttp://www.commandsoftware.com/fsavโ F-Secure Anti-Virus โhttp://www.f-secure.com/hbedvโ H+BEDV "AntiVir" โhttp://www.hbedv.com/vexiraโ Vexira "Vexira" โhttp://www.centralcommand.com/sophosโ Sophos AntiVirus โhttp://www.sophos.com/avpโ Kaspersky AVP and aveclient (AVP5) โhttp://www.avp.ru/clamavโ Clam AntiVirus โhttps://www.clamav.net/f-protโ F-RISK F-PROT โhttp://www.f-prot.com/nod32cliโ ESET NOD32 โhttp://www.eset.com/
message_contains_virus_carrier_scan([$host])
Connects to the specified host:port:local_or_nonlocal (default $CSSHost), where the Symantec CarrierScan Server daemon is expected to be listening. Return values are the same as the other message_contains_virus functions.
message_contains_virus_sophie([$sophie_sock])
Connects to the specified socket (default $SophieSock), where the Sophie daemon is expected to be listening. Return values are the same as the other message_contains_virus functions.
message_contains_virus_clamd([$clamd_sock])
Connects to the specified socket (default $ClamdSock), where the clamd daemon is expected to be listening. Return values are the same as the other message_contains_virus functions.
message_contains_virus_trophie([$trophie_sock])
Connects to the specified socket (default $TrophieSock), where the Trophie daemon is expected to be listening. Return values are the same as the other message_contains_virus functions.
entity_contains_virus($entity)
This function runs the specified MIME::Entity through every installed virus-scanner and returns the scanner results. The return values are the same as for message_contains_virus(). ๐ฆ
The per-entity entity_contains_virus_* squad ๐
entity_contains_virus_trend($entity)entity_contains_virus_nai($entity)entity_contains_virus_bdc($entity)entity_contains_virus_nvcc($entity)entity_contains_virus_csav($entity)entity_contains_virus_fsav($entity)entity_contains_virus_hbedv($entity)entity_contains_virus_sophos($entity)entity_contains_virus_clamav($entity)entity_contains_virus_clamdscan($entity)entity_contains_virus_avp($entity)entity_contains_virus_avp5($entity)entity_contains_virus_fprot($entity)entity_contains_virus_fpscan($entity)entity_contains_virus_fprotd($entity)entity_contains_virus_fprotd_v6($entity)entity_contains_virus_nod32($entity)
These functions, meant to be called from filter(), are similar to the message_contains_virus functions except they scan only the current part. They should be called from list context, and their return values are as described for the message_contains_virus functions. โจ
entity_contains_virus_carrier_scan($entity[, $host])
Connects to the specified host:port:local_or_nonlocal (default $CSSHost), where the Symantec CarrierScan Server daemon is expected to be listening. Return values are the same as the other entity_contains_virus functions.
entity_contains_virus_sophie($entity[, $sophie_sock])
Connects to the specified socket (default $SophieSock), where the Sophie daemon is expected to be listening. Return values are the same as the other entity_contains_virus functions.
entity_contains_virus_trophie($entity[, $trophie_sock])
Connects to the specified socket (default $TrophieSock), where the Trophie daemon is expected to be listening. Return values are the same as the other entity_contains_virus functions.
entity_contains_virus_clamd($entity[, $clamd_sock])
Connects to the specified socket (default $ClamdSock), where the clamd daemon is expected to be listening. Return values are the same as the other entity_contains_virus functions. ๐ธ
โจ SMTP FLOW โจ
Bestie, here's the whole journey a message takes through MIMEDefang, step by step, no cap:
- INITIAL CONNECTION โ if you started
mimedefangwith the-roption and you've defined afilter_relayroutine, then boom, it gets called. ๐ - SMTP HELO COMMAND โ the HELO string gets stashed internally, but literally zero filter functions get called. Just vibes. ๐ฆ
- SMTP MAIL FROM: COMMAND โ if you launched
mimedefangwith the-soption and you have afilter_senderroutine defined, it's called. - SMTP RCPT TO: COMMAND โ if you launched
mimedefangwith the-toption and you have afilter_recipientroutine defined, it's called. - END OF SMTP DATA โ
filter_beginis called. Then for each MIME part,filteris called. Thenfilter_endis called. That's the whole arc, iconic. ๐
๐ PRESERVING RELAY INFORMATION ๐
Okay so most orgs have more than one machine handling internet e-mail. If the primary machine is down, mail gets routed to a secondary (or tertiary, etc.) MX server, which just holds onto it until the primary MX host comes back up. Then the mail is relayed to the primary MX host. ๐ซถ
Here's the sad part: relaying from a secondary to a primary MX host has the unfortunate side effect of losing the original relay's IP address information. ๐ญ MIMEDefang lets you preserve that info though, slay. One way around the problem is to run MIMEDefang on all the secondary MX hosts and use the same filter. But lowkey you might not have control over the secondary MX hosts. If you can persuade the owners of the secondary MX hosts to run MIMEDefang with a simple filter that only preserves relay information and does no other scanning, your primary MX host can obtain relay information and make decisions using $RelayAddr and $RelayHostname.
When you configure MIMEDefang, supply the --with-ipheader argument to the ./configure script. When you install MIMEDefang, a file called /etc/mail/mimedefang-ip-key gets created, and it contains a randomly-generated header name. Copy this file to all of your mail relays. It is important that all of your MX hosts have the same key. The key should be kept confidential, but it's not disastrous if it leaks out. ๐
On your secondary MX hosts, add this line to filter_end:
add_ip_validation_header();
Note: You should only add the validation header to mail destined for one of your other MX hosts! Otherwise, the validation header will leak out. ๐
When the secondary MX hosts relay to the primary MX host, $RelayAddr and $RelayHostname will be set based on the IP validation header. If MIMEDefang notices this header, it sets the global variable $WasResent to 1. Since you don't want to trust the header unless it was set by one of your secondary MX hosts, you should put this code in filter_begin:
if ($WasResent) {
if ($RealRelayAddr ne "ip.of.secondary.mx" and
$RealRelayAddr ne "ip.of.tertiary.mx") {
$RelayAddr = $RealRelayAddr;
$RelayHostname = $RealRelayHostname;
}
}
This resets the relay address and hostname to the actual relay address and hostname, unless the message is coming from one of your other MX hosts.
On the primary MX host, you should add this in filter_begin:
delete_ip_validation_header();
This prevents the validation header from leaking out to recipients.
Note: The IP validation header works only in message-oriented functions. It (obviously) has no effect onfilter_relay,filter_senderandfilter_recipient, because no header information is available yet. You must take this into account when writing your filter; you must defer relay-based decisions to the message filter for mail arriving from your other MX hosts.
๐ GLOBAL VARIABLE LIFETIME ๐
The following list describes the lifetime of global variables (thanks to Tony Nugent for providing this documentation, king behavior ๐).
If you set a global variable:
- Outside a subroutine in your filter file โ It is available to all functions, all the time.
- In
filter_relay,filter_senderorfilter_recipientโ Not guaranteed to be available to any other function, not even from onefilter_recipientcall to the next, when receiving a multi-recipient email message. ๐ - In
filter_beginโ Available tofilter_begin,filterandfilter_end - In
filterโ Available tofilterandfilter_end - In
filter_endโ Available withinfilter_end
The "built-in" globals like $Subject, $Sender, etc. are always available to filter_begin, filter and filter_end. Some are available to filter_relay, filter_sender or filter_recipient, but you should check the documentation of the variable above for details. ๐
๐ฆ MAINTAINING STATE ๐ฆ
There are four basic groups of filtering functions:
filter_relayfilter_senderfilter_recipientfilter_begin,filter,filter_multipart,filter_end
In general, for a given mail message, these groups of functions may be called in completely different Perl processes. So bestie, there is no way to maintain state inside Perl between groups of functions. Like, you cannot set a variable in filter_relay and expect it to be available in filter_sender, because the filter_sender invocation might take place in a completely different process. Fr fr. ๐ญ
For a given mail message, it is always the case that filter_begin, filter, filter_multipart and filter_end are called in the same Perl process. So you can use global variables to carry state among those functions. You should be very careful to initialize such variables in filter_begin to ensure no data leaks from one message to another. ๐
Also for a given mail message, the $CWD global variable holds the message spool directory, and the current working directory is set to $CWD. So you can store state in files inside $CWD. If filter_sender stores data in a file inside $CWD, then filter_recipient can retrieve that data. โจ
Since filter_relay is called directly after a mail connection is established, there is no message context yet, no per-message mimedefang spool directory, and the $CWD global is not set. Therefore, it is not possible to share information from filter_relay to one of the other filter functions. The only thing that filter_relay has in common with the other functions are the values in the globals $RelayAddr, and $RelayHostname. These could be used to access per-remote-host information in some database.
Inside $CWD, we reserve filenames beginning with upper-case letters for internal MIMEDefang use. If you want to create files to store state, name them beginning with a lower-case letter to avoid clashes with future releases of MIMEDefang. ๐
๐ SOCKET MAPS ๐
If you have Sendmail 8.13 or later, and have compiled it with the SOCKETMAP option, then you can use a special map type that communicates over a socket with another program (rather than looking up a key in a Berkeley database, for example).
mimedefang-multiplexor implements the Sendmail SOCKETMAP protocol if you supply the -N option. In that case, you can define a function called filter_map to implement map lookups. filter_map takes two arguments: $mapname is the name of the Sendmail map (as given in the K sendmail configuration directive), and $key is the key to be looked up.
filter_map must return a two-element list: ($code, $val). $code can be one of:
OKโ The lookup was successful. In this case,$valmust be the result of the lookupNOTFOUNDโ The lookup was unsuccessful โ the key was not found. In this case,$valshould be the empty string.TEMPโ There was a temporary failure of some kind.$valcan be an explanatory error message.TIMEOUTโ There was a timeout of some kind.$valcan be an explanatory error message.PERMโ There was a permanent failure. This is not the same as an unsuccessful lookup; it should be used only to indicate a serious misconfiguration. As before,$valcan be an explanatory error message.
Consider this small example. Here is a minimal Sendmail configuration file:
V10/Berkeley
Kmysock socket unix:/var/spool/MIMEDefang/map.sock
kothersock socket unix:/var/spool/MIMEDefang/map.sock
If mimedefang-multiplexor is invoked with the arguments -N unix:/var/spool/MIMEDefang/map.sock, and the filter defines filter_map as follows:
sub filter_map ($$) {
my($mapname, $key) = @_;
my $ans;
if($mapname ne "mysock") {
return("PERM", "Unknown map $mapname");
}
$ans = reverse($key);
return ("OK", $ans);
}
Then in Sendmail's testing mode, we see the following:
> /map mysock testing123
map_lookup: mysock (testing123) returns 321gnitset (0)
> /map othersock foo
map_lookup: othersock (foo) no match (69)
(The return code of 69 means EX_UNAVAILABLE or Service Unavailable)
A real-world example could do map lookups in an LDAP directory or SQL database, or perform other kinds of processing. You can even implement standard Sendmail maps like virtusertable, mailertable, access_db, etc. using SOCKETMAP. So many possibilities, we love to see it. ๐ฅ
โญ TICK REQUESTS โญ
If you supply the -X option to mimedefang-multiplexor, then every so often, a "tick" request is sent to a free worker. If your filter defines a function called filter_tick, then this function is called with a single argument: the tick type. If you run multiple parallel ticks, then each tick has a type ranging from 0 to n-1, where n is the number of parallel ticks. If you're only running one tick request, then the argument to filter_tick is always 0.
You can use this facility to run periodic tasks from within MIMEDefang. Note, however, that you have no control over which worker is picked to run filter_tick. Also, at most one filter_tick call with a particular "type" argument will be active at any time, and if there are no free workers when a tick would occur, the tick is skipped. ๐
๐ฆ SUPPORTED VIRUS SCANNERS ๐ฆ
Okay bestie, gather round, because MIMEDefang does not play when it comes to malware. It can hook you up with all of these scanners, and honestly? The lineup is giving range. ๐
- Symantec CarrierScan Server โ (http://www.symantec.com/region/can/eng/product/scs/)
- Trend Micro vscan โ (http://www.antivirus.com/)
- Sophos Sweep โ (http://www.sophos.com/products/antivirus/savunix.html)
- H+BEDV AntiVir โ (http://www.hbedv.com/)
- Central Command Vexira โ (http://www.centralcommand.com/)
- NAI uvscan โ (http://www.nai.com)
- Bitdefender bdc โ (http://www.bitdefender.com)
- Norman Virus Control (NVCC) โ (http://www.norman.no/)
- Command csav โ (http://www.commandsoftware.com)
- F-Secure fsav โ (http://www.f-secure.com)
- The
clamscanandclamdscancommand-line scanners, plus theclamddaemon from Clam AntiVirus โ (https://www.clamav.net/) ๐ฆ - Kaspersky Anti-Virus (AVP) โ (http://www.kaspersky.com/)
- F-Risk F-Prot โ (http://www.f-prot.com/)
- F-Risk F-Prot v6 โ (http://www.f-prot.com/) โจ yes, they're separate entries, and yes, you need the right one for your version, no cap
- F-Risk FPROTD โ the daemonized version of F-Prot ๐
- Symantec CarrierScan Server โ (http://www.symantec.ca/region/can/eng/product/scs/buymenu.html) not a typo bestie, Symantec really is in here twice with two different URLs. Iconic behaviour fr fr.
- Sophie โ (http://www.vanja.com/tools/sophie/), which uses the
libsavilibrary from Sophos, and is supported in daemon-scanning mode ๐ง - Trophie โ (http://www.vanja.com/tools/trophie/), which uses the
libvsapilibrary from Trend Micro, and is also supported in daemon-scanning mode ๐ธ - ESET NOD32 โ (http://www.eset.com/) ๐
๐ AUTHORS ๐
So who do we have to thank for this whole masterpiece? mimedefang was written by Dianne Skoll <dfs@roaringpenguin.com>. ๐ซถ
And the mimedefang home page lives at https://www.mimedefang.org/ โ go pay your respects, bestie. ๐
๐ญ SEE ALSO ๐ญ
The crossover episode, aka the other pages you're gonna want open in a second terminal:
And that's a wrap on the whole thing, bestie!! ๐ You made it through every single section, every flag, every env var, every dramatic warning. Main character energy, honestly. Go forth and filter those mails. ๐ โจ
This lil manual page comes from the 4th Berkeley Distribution, dated 8 February 2005, and it's the MIMEDEFANG-FILTER(5) page. Mwah. ๐ ๐
๐ฆ where this page comes from
- epel 10: mimedefang 3.7.1-1.el10_3 ยท GPL-2.0-only AND GPL-2.0-or-later
- epel 9: mimedefang 3.7.1-1.el9 ยท GPL-2.0-only AND GPL-2.0-or-later
- fedora 43: mimedefang 3.7.1-1.fc43 ยท GPL-2.0-only AND GPL-2.0-or-later
- fedora 44: mimedefang 3.7.1-1.fc44 ยท GPL-2.0-only AND GPL-2.0-or-later
- ๐ girlified by deepseek-ai/DeepSeek-V4.1-Flash on : a language model's rewrite of the original, under its terms