General-Purpose Mail Filter

3 Tutorial

This chapter contains a tutorial introduction, guiding you
through various mailfromd configurations, starting from the
simplest ones and proceeding up to more advanced forms. It omits
most complicated details, concentrating mainly on the
common practical tasks.

If you are familiar to mailfromd, you can skip this
chapter and go directly to the next one (see MFL), which contains
detailed discussion of the mail filtering language and
mailfromd interaction with the Mail Transport Agent.

3.1 Start Up

The mailfromd utility runs as a standalone daemon
program and listens on a predefined communication channel for requests
from the Mail Transfer Agent (MTA, for short). When
processing each message, the MTA installs communication with
mailfromd, and goes through several states, collecting the
necessary data from the sender. At each state it sends the relevant
information to mailfromd, and waits for it to reply. The
mailfromd filter receives the message data through
Sendmail macros and runs a handler program
defined for the given state. The result of this run is a response
code, that it returns to the MTA. The following response
codes are defined:

continue

Continue message processing.

accept

Accept this message for delivery. After receiving this code the
MTA continues processing this message without
further consulting mailfromd filter.

reject

Reject this message. The message processing stops at this stage, and the
sender receives the reject reply (‘5xx’ reply code). No
further mailfromd handlers are called for this message.

discard

Silently discard the message. This means that MTA will
continue processing this message as if it were going to deliver it,
but will discard it after receiving. No further interaction with
mailfromd occurs.

tempfail

Temporarily reject the message. The message processing stops at this
stage, and the sender receives the ‘temporary failure’ reply
(‘4xx’ reply code). No further mailfromd
handlers are called for this message.

The instructions on how to process the message are supplied to
mailfromd in its filter script file. It is normally
called /usr/local/etc/mailfromd.mf (but can be located
elsewhere, see Invocation) and contains a set of milter
state handlers, or subroutines to be executed in various
SMTP states. Each interaction state can
be supplied its own handling procedure. A missing procedure implies
continue response code.

The filter script can define up to nine milter state handlers,
called after the names of milter states: ‘connect’, ‘helo’,
‘envfrom’, ‘envrcpt’, ‘data’, ‘header’, ‘eoh’,
‘body’, and ‘eom’. The ‘data’ handler is invoked only
if MTA uses Milter protocol version 3 or later. Two special
handlers are available for initialization and clean-up purposes:
‘begin’ is called before the processing starts, and ‘end’ is
called after it is finished. The diagram below shows the control flow
when processing an SMTP transaction. Lines marked with
C: show SMTP commands issued by the remote machine (the
client), those marked with ‘⇒’ show called handlers
with their arguments. An ‘[R]’ appearing at the start of a line
indicates that this part of the transaction can be repeated any number
of times:

This control flow is maintained for as long as each called handler
returns continue (see Actions). Otherwise, if
any handler returns accept or discard, the message
processing continues, but no other handler is called. In the case
of accept, the MTA will accept the message for
delivery, in the case of discard it will silently discard it.

If any of the handlers returns reject or tempfail, the
result depends on the handler. If this code is returned by
envrcpt handler, it causes this particular recipient address to
be rejected. When returned by any other handler,
it causes the whole message will be rejected.

The reject and tempfail actions executed by
helo handler do not take effect immediately. Instead, their
action is deferred until the next SMTP command from the
client, which is usually MAIL FROM.

3.2 Simplest Configurations

The mailfromd script file contains a
series of declarations of the handler procedures. Each
declaration has the form:

prog name
do
…
done

where prog, do and done are the keywords,
and name is the state name for this handler. The dots in the
above example represent the actual code, or a set of
commands, instructing mailfromd how to process the message.

For example, the declaration:

prog envfrom
do
accept
done

installs a handler for ‘envfrom’ state, which always approves the
message for delivery, without any further interaction with
mailfromd.

The word accept in the above example is an action.
Action is a special language statement that instructs the
run-time engine to stop execution of the program and to return a
response code to the Sendmail. There are five actions, one
for each response code: continue, accept, reject,
discard, and tempfail. Among these, reject and
discard can optionally take one to three arguments. There are
two ways of supplying the arguments.

In the first form, called literal or traditional notation,
the arguments are supplied as additional words after the action name,
separated by whitespace. The first argument is a three-digit
RFC 2821 reply code. It must begin with ‘5’ for
reject and with ‘4’ for tempfail. If two arguments
are supplied, the second argument must be either an extended
reply code (RFC 1893/2034) or a textual string to be
returned along with the SMTP reply. Finally, if all three
arguments are supplied, then the second one must be an extended reply
code and the third one must supply the textual string. The following
examples illustrate all possible ways of using the reject
statement in literal notation:

Another form for these action is called functional notation,
because it resembles the function syntax. When used in this form, the
action word is followed by a parenthesized group of exactly three
arguments, separated by commas. The meaning and ordering of the
argument is the same as in literal form. Any of three arguments may
be absent, in which case it will be replaced by the default value. To
illustrate this, here are the statements from the previous example,
written in functional notation:

3.3 Conditional Execution

Programs consisting of a single action are rarely useful. In most
cases you will want to do some checking and decide whether to process
the message depending on its result. For example, if you do not want
to accept messages from the address ‘<badguy@some.net>’, you
could write the following program:

prog envfrom
do
if $f = "badguy@some.net"
reject
else
accept
fi
done

This example illustrates several important concepts. First or
all, $f in the third line is a Sendmail macro
reference. Sendmail macros are referenced the same way as in
sendmail.cf, with the only difference that curly braces around
macro names are optional, even if the name consists of several
letters. The value of a macro reference is always a string.

The equality operator (‘=’) compares its left and right
arguments and evaluates to true if the two strings are exactly the
same, or to false otherwise. Apart from equality, you can use the
regular relational operators: ‘!=’, ‘>’, ‘>=’, ‘<’
and ‘<=’. Notice that string comparison in mailfromd
is always case sensitive. To do case-insensitive comparison,
translate both operands to upper or lower case (See tolower, and
see toupper).

The if statement decides what actions to execute depending
on the value its condition evaluates to. Its usual form is:

if expressionthen-body [else else-body] fi

The then-body is executed if the expression evaluates to
true (i.e. to any non-zero value). The optional
else-body is executed if the expression yields
false (i.e. zero). Both then-body and else-body can contain
other if statements, their nesting depth is not limited. To
facilitate writing complex conditional statements, the elif
keyword can be used to introduce alternative conditions, for example:

3.4 Functions and Modules

As any programming language, MFL supports a concept of
function, i.e. a body of code that is assigned a unique name and
can be invoked elsewhere as many times as needed.

All functions have a definition that introduces types and
names of the formal parameters and the result type, if the function is
to return a meaningful value (function definitions in MFL
are discussed in detail in see User-Defined Functions).

A function is invoked using a special construct, a function
call:

name (arg-list)

where name is the function name, and arg-list is a
comma-separated list of expressions. Each expression in
arg-list is evaluated, and its type is compared with that of the
corresponding formal argument. If the types differ, the expression is
converted to the formal argument type. Finally, a copy of its value
is passed to the function as a corresponding argument. The order in
which the expressions are evaluated is not defined. The compiler
checks that the number of elements in arg-list match the
number of mandatory arguments for function name.

If the function does not deliver a result, it should only be called
as a statement.

Functions may be recursive, even mutually recursive.

Mailfromd comes with a rich set of predefined functions
for various purposes. There are two basic function classes:
built-in functions, that are implemented by the MFL
runtime environment in mailfromd, and library
functions, that are implemented in MFL. The built-in
functions are always available and no preparatory work is needed before
calling them. In contrast, the library functions are defined in
modules, special MFL source files that contain functions
designed for a particular task. In order to access a library
function, you must first require a module it is defined in.
This is done using require statement. For example, the
function hostname looks up in the DNS the name
corresponding to the IP address specified as its argument. This
function is defined in module dns.mf, so before calling it you
must require this module:

require dns

The require statement takes a single argument: the name of the
requested module (without the ‘.mf’ suffix). It looks up the
module on disk and loads it if it is available.

3.5 Domain Name System

Site administrators often do not wish to accept mail from hosts that
do not have a proper reverse delegation in the Domain Name System.
In the previous section we introduced the library function
hostname, that looks up in the DNS the name corresponding to
the IP address specified as its argument. If there is no
corresponding name, the function returns its argument unchanged. This
can be used to test if the IP was resolved, as illustrated in the
example below:

The on poll construct runs standard verification
(see standard verification) for the email address specified as its
argument (in the example above it is the value of the Sendmail macro
‘$f’). The check can result in the following conditions:

success

The address exists.

not_found

The address does not exist.

failure

Some error of permanent nature occurred during the check. The
existence of the address cannot be verified.

temp_failure

Some temporary failure occurred during the check. The
existence of the address cannot be verified at the moment.

The when branches of the on poll statement
introduce statements, that are executed depending on the actual
return condition. If any condition occurs that is not handled within
the on block, the run-time evaluator will signal an
exception4 and return temporary
failure, therefore it is advisable to always handle all four
conditions. In fact, the condition handling shown in the above
example is preferable for most normal configurations: the mail is
accepted if the sender address is proved to exist and rejected
otherwise. If a temporary failure occurs, the remote party is urged
to retry the transaction some time later.

The poll statement itself has a number of options that
control the type of the verification. These are discussed in detail
in poll.

It is worth noticing that there is one special email
address which is always available on any host, it is the null
address ‘<>’ used in error reporting. It is of no use verifying
its existence:

3.7 SMTP Timeouts

When using polling functions, it is important to take into account
possible delays, which can occur in SMTP transactions. Such
delays may be due to low network bandwidth or high load on the remote
server. Some sites impose them willingly, as a spam-fighting measure.

Ideally the callout verification should use the timeout values
defined in the RFC 2822, but this is impossible in practice, because
it would cause a timeout escalation, which consists in
propagating delays encountered in a callout SMTP session
back to the remote client whose session initiated the callout.

Consider, for example, the following scenario. An MFL
script performs a callout on ‘envfrom’ stage. The remote server
is overloaded and delays heavily in responding, so that the initial response
arrives 3 minutes after establishing the connection, and processing
the ‘EHLO’ command takes another 3 minutes. These delays are
OK according to the RFC, which imposes a 5 minute limit for each stage,
but while waiting for the remote reply our SMTP server
remains in the ‘envfrom’ state with the client waiting
for a response to its ‘MAIL’ command more than 6 minutes, which is
intolerable, because of the same 5 minute limit. Thus, the client
will almost certainly break the session.

To avoid this, mailfromd uses a special instance, called
callout server, which is responsible for running callout
SMTP sessions asynchronously. The usual sender verification
is performed using so-called soft timeout values, which
are set to values short enough to not disturb the incoming session
(e.g. a timeout for ‘HELO’ response is 3 seconds, instead of 5
minutes). If this verification yields a definite answer, that answer
is stored in the cache database and returned to the calling procedure
immediately. If, however, the verification is aborted due to a timeout,
the caller procedure is returned an ‘e_temp_failure’ exception, and
the callout is scheduled for processing by a callout server. This
exception normally causes the milter session to return a temporary
error to the sender, urging it to retry the connection later.

In the meantime, the callout server runs the sender verification
again using another set of timeouts, called hard timeouts, which
are normally much longer than ‘soft’ ones (they default to the
values required by RFC 2822). If it gets a definitive result (e.g.
‘email found’ or ‘email not found’), the server stores it
in the cache database. If the callout ends due to a timeout, a
‘not_found’ result is stored in the database.

Some time later, the remote server retries the delivery, and the
mailfromd script is run again. This time, the callout
function will immediately obtain the already cached result from the
database and proceed accordingly. If the callout server has not
finished the request by the time the sender retries the connection,
the latter is again returned a temporary error, and the process
continues until the callout is finished.

Usually, callout server is just another instance of
mailfromd itself, which is started automatically to
perform scheduled SMTP callouts. It is also possible to set up
a separate callout server on another machine. This is discussed in
calloutd.

For a detailed information about callout timeouts and their
configuration, see conf-timeout.

For a description of how to configure mailfromd to use callout
servers, see conf-server.

3.8 Avoiding Verification Loops

An envfrom program consisting only of the on poll
statement will work smoothly for incoming mails, but will create
infinite loops for outgoing mails. This is because upon sending an outgoing
message mailfromd will start the verification procedure, which
will initiate an SMTP transaction with the same mail server
that runs it. This transaction will in turn trigger execution of
on poll statement, etc. ad infinitum. To avoid this, any
properly written filter script should not run the verification
procedure on the email addresses in those domains that are relayed by
the server it runs on. This can be achieved using relayed
function. The function returns true if its argument is
contained in one of the predefined domain list files. These
files correspond to Sendmail plain text files used in
F class definition forms (see Sendmail Installation and
Operation Guide, chapter 5.3), i.e. they contain one domain name per
line, with empty lines and lines started with ‘#’ being ignored.
The domain files consulted by relayed function are defined
in the relayed-domain-file configuration file statement
(see relayed-domain-file):

If you feel that your Sendmail’s relayed domains are not restrictive
enough for mailfromd filters (for example you are relaying
mails from some third-party servers), you can use a database of
trusted mail server addresses. If the number of such servers is small
enough, a single ‘or’ statement can be used, e.g.:

or combine both methods. Finally, you can keep a DBM
database of relayed addresses and use dbmap or dbget
function for checking (see Database functions).

elif dbmap("%__statedir__/relay.db", ${client_addr})
accept
…

3.9 HELO Domain

Some of the mail filtering conditions may depend on the value of
helo domain name, i.e. the argument to the SMTPEHLO (or
HELO) command. If you ever need such conditions, take into
account the following caveats. Firstly, although Sendmail
passes the helo domain in $s macro, it does not do this
consistently. In fact, the $s macro is available only to
the helo handler, all other handlers won’t see it, no matter what
the value of the corresponding Milter.macros.handler
statement. So, if you wish to access its value from any
handler, other than helo, you will have to store it in a
variable in the helo handler and then use this variable
value in the other handler. This approach is also recommended for
another MTAs. This brings us to the concept of
variables in mailfromd scripts.

A variable is declared using the following syntax:

typename

where variable is the variable name and type is
‘string’, if the variable is to hold a string value, and
‘number’, if it is supposed to have a numeric value.

A variable is assigned a value using the set statement:

set nameexpr

where expr is any valid MFL expression.

The set statement can occur within handler or function
declarations as well as outside of them.

There are two kinds of Mailfromd variables: global
variables, that are visible to all handlers and functions, and
automatic variables, that are available only within the handler
or function where they are declared. For our purpose we need a global
variable (See Variable classes, for detailed descriptions
of both kinds of variables).

The following example illustrates an approach that allows to use
the HELO domain name in any handler:

Notice, that for this approach to work, your MTA
must export the ‘s’ macro (e.g., in case of Sendmail, the
Milter.macros.helo statement in the sendmail.cf file must contain
‘s’. see Sendmail). This requirement can be removed by
using the handler argument of helo. Each
mailfromd handler is given one or several arguments. The
exact number of arguments and their meaning are handler-specific and are
described in Handlers, and Figure 3.1.
The arguments are referenced by their ordinal number, using the notation
$n. The helo handler takes one argument, whose
value is the helo domain. Using this information, the helo
handler from the example above can be rewritten as follows:

prog helo
do
# Save the host name for further use
set helohost $1
done

3.10 SMTP RSET and Milter Abort Handling

In previous section we have used a global variable to hold certain
information and share it between handlers. In the majority of cases,
such information is session specific, and becomes invalid if the
remote party issues the SMTP RSET command. Therefore,
mailfromd clears all global variables when it receives a
Milter ‘abort’ request, which is normally generated by this
command.

However, you may need some variables that retain their values
even across SMTP session resets. In mailfromd terminology
such variables are called precious. Precious variables are
declared by prefixing their declaration with the keyword
precious. Consider, for example, this snippet of code:

Here, the variable ‘rcpt_counter’ is declared as precious and
its value is incremented each time the ‘envrcpt’ handler is
called. This way, ‘rcpt_counter’ will keep the total number of
SMTP RCPT commands issued during the session, no matter how
many times it was restarted using the RSET command.

3.11 Controlling Number of Recipients

Any MTA provides a way to limit the number of recipients
per message. For example, in Sendmail you may use the
MaxRecipientsPerMessage option7. However, such methods are not flexible, so
you are often better off using mailfromd for this purpose.

Mailfromd keeps the number of recipients collected so far
in variable rcpt_count, which can be controlled in
envrcpt handler as shown in the example below:

This filter will accept no more than 10 recipients per message.
You may achieve finer granularity by using additional conditions. For
example, the following code will allow any number of recipients if the
mail is coming from a domain relayed by the server, while limiting it
to 10 for incoming mail from other domains:

There are three important features to notice in the above code.
First of all, it introduces two boolean operators:
and, which evaluates to true only if both
left-side and right-side expressions are true, and not,
which reverses the value of its argument.

Secondly, the scope of an operation is determined by its
precedence, or binding strength. Not binds more
tightly than and, so its scope is limited by the next
expression between it and and. Using parentheses to underline the
operator scoping, the above if condition can be rewritten as
follows:

if (not (relayed(hostname($client_addr)))) and (%rcpt_count > 10)

Finally, it is important to notice that all boolean expressions
are computed using shortcut evaluation. To understand what it
is, let’s consider the following expression: x and
y. Its value is true only if both x and y
are true. Now suppose that we evaluate the expression from
left to right and we find that x is false. This means that
no matter what the value of y is, the resulting expression will be
false, therefore there is no need to compute y at all.
So, the boolean shortcut evaluation works as follows:

x and y

If x ⇒ false, do not evaluate y and
return false.

x or y

If x ⇒ true, do not evaluate y and
return true.

Thus, in the expression not relayed(hostname($client_addr)) and
rcpt_count > 10, the value of the rcpt_count variable will be
compared with ‘10’ only if the relayed function yielded
false.

To further enhance our sample filter, you may wish to make the
reject output more informative, to let the sender know what
the recipient limit is. To do so, you can use the concatenation
operator ‘.’ (a dot):

When evaluating the third argument to reject,
mailfromd will first convert max_rcpt to string and
then concatenate both strings together, producing string ‘Too many
recipients, max=10’.

3.12 Sending Rate

We have introduced the notion of mail sending rate in Rate Limit. Mailfromd keeps the computed rates in the special
rate database (see Databases). Each record in this
database consists of a key, for which the rate is computed, and
the rate value, in form of a double precision floating point number,
representing average number of messages per second sent by this
key within the last sampling interval. In the simplest case,
the sender email address can be used as a key, however we recommend
to use a conjunction email-sender_ip instead, so the
actual email owner won’t be blocked by actions of some spammer
abusing his/her address.

Two functions are provided to control and update sending rates. The
rateok function takes three mandatory arguments:

bool rateok(string key, number interval, number threshold)

The key meaning is described above. The interval is the
sampling interval, or the number of seconds to which the actual
sending rate value is converted. Remember that it is stored
internally as a floating point number, and thus cannot be directly
used in mailfromd filters, which operate only on integer
numbers. To use the rate value, it is first converted to messages per
given interval, which is an integer number. For example, the rate
0.138888 brought to 1-hour interval gives 500
(messages per hour).

When the rateok function is called, it recomputes
rate record for the given key. If the new rate value converted
to messages per given interval is less than threshold,
the function updates the database and returns True. Otherwise it
returns False and does not update the database.

This function must be required prior to use, by placing the
following statement somewhere at the beginning of your script:

require rateok

For example, the following code limits the mail sending rate for each
‘email address’-‘IP’ combination to 180 per hour.
If the actual rate value exceeds this limit, the sender is returned a
temporary failure response:

It is often inconvenient to specify intervals in seconds,
therefore a special interval function is provided. It
converts its argument, which is a textual string representing time
interval in English, to the corresponding number of seconds. Using
this function, the function invocation would be:

The rateok function begins computing the rate
as soon as it has collected enough data. By default, it needs at least
four mails. Since this may lead to a big number of false positives
(i.e. overestimated rates) at the beginning of sampling interval,
there is a way to specify a minimum number of samples rateok
must collect before starting to actually compute rates. This number of
samples is given as the optional fourth argument to the function. For
example, the following call will always return True for the
first 10 mails, no matter what the actual rate:

rateok($f . "-" . ${client_addr}, interval("1 hour"), 180, 10)

The tbf_rate function allows to exercise more control over
the mail rates. This function implements a token bucket filter
(TBF) algorithm.

The token bucket controls when the data can be transmitted based on
the presence of abstract entities called tokens in a container
called bucket. Each token represents some amount of data. The
algorithm works as follows:

A token is added to the bucket at a constant rate of 1 token per
t microseconds.

A bucket can hold at most m tokens. If a token arrives
when the bucket is full, that token is discarded.

When n items of data arrive (e.g. n mails), n
tokens are removed from the bucket and the data are accepted.

If fewer than n tokens are available, no tokens are
removed from the bucket and the data are not accepted.

This algorithm allows to keep the data traffic at a constant rate
t with bursts of up to m data items. Such bursts occur
when no data was being arrived for m*t or more
microseconds.

Mailfromd keeps buckets in a database ‘tbf’. Each
bucket is identified by a unique key. The tbf_rate
function is defined as follows:

bool tbf_rate(string key, number n, number t, number m)

The key identifies the bucket to operate upon. The rest of
arguments is described above. The tbf_rate function returns
‘True’ if the algorithm allows to accept the data and
‘False’ otherwise.

Depending on how the actual arguments are selected the tbf_rate
function can be used to control various types of flow rates. For
example, to control mail sending rate, assign the arguments as
follows: n to the number of mails and t to the control
interval in microseconds:

The example above permits to send at most one mail each 10 seconds.
The burst size is set to 20.

Another use for the tbf_rate function is to limit the total
delivered mail size per given interval of time. To do so, the
function must be used in prog eom handler, because it is the
only handler where the entire size of the message is known. The
n argument must contain the number of bytes in the email (or
email bytes * number of recipients), and the t must be set to
the number of bytes per microsecond a given user is allowed to send. The
m argument must be large enough to accommodate a couple of
large emails. E.g.:

3.13 Greylisting

Greylisting is a simple method of defending against the spam
proposed by Evan Harris. In few words, it consists in recording the
‘sender IP’-‘sender email’-‘recipient email’ triplet of
mail transactions. Each time the unknown triplet is seen, the
corresponding message is rejected with the tempfail code. If the
mail is legitimate, this will make the originating server retry
the delivery later, until the destination eventually accepts it. If,
however, the mail is a spam, it will probably never be retried, so
the users will not be bothered by it. Even if the spammer will retry
the delivery, the greylisting period will give spam-detection
systems, such as DNSBLs, enough time to detect and blacklist it,
so by the time the destination host starts accepting emails from this
triplet, it will already be blocked by other means.

The mailfromd implementation of greylisting is based on
greylist function. The function takes two arguments:
the key, identifying the greylisting triplet, and the
interval. The function looks up the key in the greylisting
database. If such a key is not found, a new entry is created for it
and the function returns true. If the key is
found, greylist returns false, if it was inserted to the
database more than interval seconds ago, and true otherwise.
In other words, from the point of view of the greylisting algorithm, the
function returns true when the message delivery should be
blocked. Thus, the simplest implementation of the algorithm would be:

However, the message returned by this example, is not informative
enough. In particular, it does not tell when the message will be
accepted. To help you produce more informative messages, greylist
function stores the number of seconds left to the end of the
greylisting period in the global variable
greylist_seconds_left, so the above example could be enhanced
as follows:

In real life you will have to avoid greylisting some messages, in
particular those coming from the ‘<>’ address and from the IP
addresses in your relayed domain. It can easily be done using the
techniques described in previous sections and is left as an exercise
to the reader.

Mailfromd provides two implementations of greylisting
primitives, which differ in the information stored in the database.
The one described above is called traditional. It keeps in the
database the time when the greylisting was activated for the given
key, so the greylisting function uses its second argument
(interval) and the current timestamp to decide whether the key
is still greylisted.

The second implementation is called by the name of its inventor
Con Tassios. This implementation stores in the database the
time when the greylisting period is set to expire, computed by the
greylist when it is first called for the given key, using the
formula ‘current_timestamp + interval’. Subsequent calls to
greylist compare the current timestamp with the one stored in
the database and ignore their second argument. This implementation is
enabled by one of the following pragmas:

#pragma greylist con-tassios

or

#pragma greylist ct

When Con Tassios implementation is used, yet another function
becomes available. The function is_greylisted
(see is_greylisted) returns
‘True’ if its argument is greylisted and ‘False’ otherwise.
It can be used to check for the greylisting status without actually
updating the database:

if is_greylisted("${client_addr}-$f-${rcpt_addr}")
…
fi

One special case is whitelisting, which is often used
together with greylisting. To implement it, mailfromd
provides the function dbmap, which takes two mandatory arguments:
dbmap(file, key) (it also allows an optional third
argument, see dbmap, for more information on it). The first argument is
the name of the DBM file where to search for the key, the second one
is the key to be searched. Assuming you keep your whitelist database
in file /var/run/whitelist.db, a more practical example will be:

3.14 Local Account Verification

In your filter script you may need to verify if the given
user name is served by your mail server, in other words, to verify if
it represents a local account. Notice that in this context, the word
local does not necessarily mean that the account is local for
the server running mailfromd, it simply means any account
whose mailbox is served by the mail servers using mailfromd.

The validuser function may be used for this purpose. It
takes one argument, the user name, and returns true if
this name corresponds to a local account. To verify this, the
function relies on libmuauth, a powerful authentication
library shipped with GNU mailutils. More precisely, it
invokes a list of authorization functions. Each function is
responsible for looking up the user name in a particular source of
information, such as system passwd database, an SQL database,
etc. The search is terminated when one of the functions finds
the name in question or the list is exhausted. In the former case, the
account is local, in the latter it is not. This concept is
discussed in detail in see Authorization and Authentication Principles in GNU Mailutils
Manual). Here we will give only some practical advices for
implementing it in mailfromd filters.

The actual list of available authorization modules depends on your
mailutils installation. Usually it includes, apart from
traditional UNIXpasswd database, the functions for verifying
PAM, RADIUS and SQL database accounts.
Each of the authorization methods is configured using special
configuration file statements. For the description of the Mailutils
configuration files, See Mailutils Configuration File in GNU Mailutils Manual.
You can obtain the template for mailfromd configuration by
running mailfromd --config-help.

For example, the following mailfromd.conf file:

auth {
authorization pam:system;
}
pam {
service mailfromd;
}

sets up the authorization using PAM and system passwd
database. The name of PAM service to use is ‘mailfromd’.

The function validuser is often used together with
dbmap, as in the example below:

3.15 Databases

Some mailfromd functions use DBM databases to save their
persistent state data. Each database has a unique identifier,
and is assigned several pieces of information for its maintenance: the
database file name and the expiration period, i.e. the
time after which a record is considered expired.

To obtain the list of available databases along with their
preconfigured settings, run mailfromd --show-defaults. You will
see an output similar to this:

The text below ‘optional features’ line describes the available
built-in databases. Notice that the ‘cache’ database, in
contrast to the rest of databases, has two expiration periods
associated with it. This is explained in the next subsection.

3.15.1 Database Formats

The version 8.7 runs the following database types (or
formats):

‘cache’

Cache database keeps the information about external emails,
obtained using sender verification functions (see Checking Sender Address). The key entry to this database is an email address or
email:sender-ip string, for addresses checked using strict
verification. The data its stores for each key are:

Address validity. This field can be either success or
not_found, meaning the address is confirmed to exists or it
is not.

The time when the entry was entered into the database. It is used to
check for expired entries.

The ‘cache’ database has two expiration periods: a
positive expiration period, that is applied to entries with
the first field set to success, and a negative expiration
period, applied to entries marked as not_found.

‘rate’

The mail sending rate data, maintained by rate function
(see Rate limiting functions). A record consists of the following fields:

timestamp

The time when the entry was entered into the database.

interval

Interval during which the rate was measured (seconds).

count

Number of mails sent during this interval.

‘tbf’

This database is maintained by tbf_rate function (see TBF).
Each record represents a single bucket and consists of the following
keys:

timestamp

Timestamp of most recent token, as a 64-bit unsigned integer
(microseconds resolution).

expirytime

Estimated time when this bucket expires (seconds since epoch).

tokens

Number of tokens in the bucket (size_t).

‘greylist’

This database is maintained by greylist function
(see Greylisting). Each record holds only the timestamp.
Its semantics depends on the greylisting implementation in
use (see greylisting types). In traditional implementation, it
is the time when the entry was entered into the database. In Con
Tassios implementation, it is the time when the greylisting period
expires.

3.15.2 Basic Database Operations

The mfdbtool utility is provided for performing various
operations on the mailfromd database.

To list the contents of a database, use --list option.
When used without any arguments it will list the ‘cache’
database:

The --format option can be used with any database
management option, described below.

Another useful operation you can do while listing ‘rate’
database is the prediction of estimated time of sending,
i.e. the time when the user will be able to send mail if currently his
mail sending rate has exceeded the limit. This is done using
--predict option. The option takes an argument, specifying
the mail sending rate limit, e.g. (the second line is split for readability):

Notice, that there is no need to use --list --format=rate
along with this option, although doing so is not an error.

To delete an entry from the database, use --delete option,
for example: mfdbtool --delete abrakat@mail.com. You can give
any number of keys to delete in the command line.

3.15.3 Database Maintenance

There are two principal operations of database management:
expiration and compaction. Expiration consists in removing
expired entries from the database. In fact, it is rarely needed,
since the expired entries are removed in the process of normal
mailfromd work. Nevertheless, a special option is provided
in case an explicit expiration is needed (for example, before dumping
the database to another format, to avoid transferring useless
information).

The command line option --expire instructs
mfdbtool to delete expired entries from the specified database. As
usual, the database is specified using --format option. If
it is not given explicitly, ‘cache’ is assumed.

While removing expired entries the space they occupied is marked as
free, so it can be used by subsequent inserts. The database does
not shrink after expiration is finished. To actually return the
unused space to the file system you should compact your
database.

This is done by running mfdbtool --compact (and, optionally,
specifying the database to operate upon with --format
option). Notice, that compacting a database needs roughly as
much disk space on the partition where the database resides as is
currently used by the database. Database compaction runs in three phases.
First, the database is scanned and all non-expired records are stored
in the memory. Secondly, a temporary database is created in the state
directory and all the cached entries are flushed into it. This
database is named after the PID of the running
mfdbtool process. Finally, the temporary database is
renamed to the source database.

Both --compact and --expire can be applied to all
databases by combining them with --all. It is useful, for
example, in crontab files. For example, I have the following
monthly job in my crontab:

0 1 1 * * /usr/bin/mfdbtool --compact --all

3.16 Testing Filter Scripts

It is important to check your filter script before actually starting
to use it. There are several ways to do so.

To test the syntax of your filter script, use the --lint
option. It will cause mailfromd to exit
immediately after attempting to compile the script file. If the
compilation succeeds, the program will exit with code 0. Otherwise,
it will exit with error code 78 (‘configuration error’). In the
latter case, mailfromd will also print a diagnostic message,
describing the error along with the exact location where the error was
diagnosed, for example:

mailfromd: /etc/mailfromd.mf:39: syntax error, unexpected reject

The error location is indicated by the name of the file and the
number of the line when the error occurred. By using the
--location-column option you instruct mailfromd to
also print the column number. E.g. with this option the above
error message may look like:

mailfromd: /etc/mailfromd.mf:39.12 syntax error, unexpected reject

Here, ‘39’ is the line and ‘12’ is the column number.

For complex scripts you may wish to obtain a listing of variables
used in the script. This can be achieved using --xref
command line option:

The output it produces consists of four columns:

Variable name

Data type

Either number or string.

Offset in data segment

Measured in words.

References

A comma-separated list of locations where the variable was
referenced. Each location is represented as file:line.
If several locations pertain to the same file, the file name is
listed only once.

If the script passes syntax check, the next step is often to test if
it works as you expect it to. This is done with --test
(-t) command line option. This option runs the
envfrom handler (or another one, see below) and prints the
result of its execution.

When running your script in test mode, you will need to supply the
values of Sendmail macros it needs. You do this by placing
the necessary assignments in the command line. For example, this is
how to supply initial values for f and client_addr
macros:

$ mailfromd --test f=gray@gnu.org client_addr=127.0.0.1

You may also need to alter initial values of some global variables
your script uses. To do so, use -v (--variable)
command line option. This option takes a single argument consisting
of the variable name and its initial value, separated by an equals
sign. For example, here is how to change the value of
ehlo_domain global variable:

$ mailfromd -v ehlo_domain=mydomain.org

The --test option is often useful in conjunction with options
--debug, --trace and --transcript
(see Logging and Debugging. The following example shows what the
author got while debugging the filter script described in
Filter Script Example:

To test any handler, other than ‘envfrom’, give its name as the
argument to --test option. Since this argument is optional,
it is important that it be given immediately after the option, without
any intervening white space, for example mailfromd --test=helo,
or mailfromd -thelo.

This method allows to test one handler at a time. To test the
script as a whole, use mtasim utility. When
started it enters interactive mode, similar to that of
sendmail -bs, where it expects SMTP commands on
its standard input and sends answers to the standard output. The
--port=auto command line option instructs it to start
mailfromd and to create a unique socket for communication
with it. For the detailed description of the program and the ways to
use it, See mtasim.

3.17 Run Mode

Mailfromd provides a special option that allows to run arbitrary
MFL scripts. This is an experimental feature, intended for
future use of MFL as a scripting language.

When given the --run command line option,
mailfromd loads the script given in its command line and
executes a function called ‘main’.

The function main must be declared as:

func main(...) returns number

Mailfromd passes all command line arguments that follow the script
name as arguments to that function. When the function returns, its
return value is used by mailfromd as exit code.

As an example, suppose the file script.mf contains the
following:

func main (...)
returns number
do
loop for number i 1,
while i <= $#,
set i i + 1
do
echo "arg %i=" . $(i)
done
done

This function prints all its arguments (See variadic functions, for
a detailed description of functions with variable
number of arguments). Now running:

$ mailfromd --run script.mf 1 file dest

displays the following:

arg 1=1
arg 2=file
arg 3=dest

Note, that MFL does not have a direct equivalent of
shell’s $0 argument. If your function needs to know the name
of the script that is being executed, use __file__ built-in
constant instead (see __file__.

You may name your start function with any name other than the default
‘main’. In this case, give its name as an argument to the
--run option. This argument is optional, therefore it must
be separated from the option by an equals sign (with no whitespace
from either side). For example, given the command line below,
mailfromd loads the file script.mf and execute the
function named ‘start’:

$ mailfromd --run=start script.mf

3.17.1 The Top of a Script File

The --run option makes it possible to use mailfromd
scripts as standalone programs. The traditional way to do so was to
set the executable bit on the script file and to begin the script with
the interpreter selector, i.e. the characters ‘#!’ followed by
the name of the mailfromd executable, e.g.:

#! /usr/sbin/mailfromd --run

This would cause the shell to invoke mailfromd with the
command line constructed from the --run option, the name
of the invoked script file itself, and any actual arguments from
the invocation. Once invoked, mailfromd would treat the
initial ‘#!’ line as a usual single-line comment
(see Comments).

However, the interpretation of the ‘#!’ by shells has various
deficiencies, which depend on the actual shell being used. For
example, some shells pass any characters following the whitespace
after the interpreter name as a single argument, some others silently
truncate the command line after some number of characters, etc. This
often make it impossible to pass additional arguments to
mailfromd. For example, a script which begins with the
following line would most probably fail to be executed properly:

#! /usr/sbin/mailfromd --no-config --run

To compensate for these deficiencies and to allow for more complex
invocation sequences, mailfromd handles initial ‘#’
in a special way. If the first line of a source file begins with
‘#!/’ or ‘#! /’ (with a single space between ‘!’ and
‘/’), it is treated as a start of a multi-line comment, which is
closed by the two characters ‘!#’ on a line by themselves.

Thus, the correct way to begin a mailfromd script is:

#! /usr/sbin/mailfromd --run
!#

Using this feature, you can start the mailfromd with
arbitrary shell code, provided it ends with an exec statement
invoking the interpreter itself. For example:

Note the use of ‘$0’ and ‘$@’ to pass the actual script file
name and command line arguments to mailfromd.

3.17.2 Parsing Command Line Arguments

A special function is provided to break (parse) options in command
lines, and to check for legal options. It uses the GNU getopt
routines (see getopt in The GNU C Library
Reference Manual).

Built-in Function: stringgetopt(number argc, pointer argv, ...)

The getopt function parses the command line arguments, as
supplied by argc and argv. The argc argument is the
argument count, and argv is an opaque data structure,
representing the array of arguments8. The operator vaptr (see vaptr) is
provided to initialize this argument.

An argument that starts with ‘-’ (and is not exactly ‘-’ or
‘--’), is an option element. An argument that starts with a
‘-’ is called short or traditional option. The
characters of this element, except for the initial ‘-’ are option
characters. Each option character represents a separate option. An
argument that starts with ‘--’ is called long or GNU
option. The characters of this element, except for the initial
‘--’ form the option name.

Options may have arguments. The argument to a short option is
supplied immediately after the option character, or as the next word
in command line. E.g., if option -f takes a mandatory
argument, then it may be given either as -farg or as
-f arg. The argument to a long option is either given
immediately after it and separated from the option name by an equals
sign (as --file=arg), or is given as the next word in the
command line (e.g. --file arg).

If the option argument is optional, i.e. it may not necessarily be
given, then only the first form is allowed (i.e. either -farg or
--file=arg.

The ‘--’ command line argument ends the option list. Any
arguments following it are not considered options, even if they begin
with a dash.

If getopt is called repeatedly, it returns successively each of
the option characters from each of the option elements (for short
options) and each option name (for long options). In this case, the
actual arguments are supplied only to the first invocation.
Subsequent calls must be given two nulls as arguments. Such
invocation instructs getopt to use the values saved on the
previous invocation.

When the function finds another option, it returns its character or name
updating the external variable optind (see below) so that the
next call to getopt can resume the scan with the following
option.

When there are no more options left, or a ‘--’ argument is
encountered, getopt returns an empty string. Then
optind gives the index in argv of the first element that
is not an option.

The legitimate options and their characteristics are supplied in
additional arguments to getopt. Each such argument is a string
consisting of two parts, separated by a vertical bar (‘|’). Any
one of these parts is optional, but at least one of them must be
present. The first part specifies short option character. If it is
followed by a colon, this character takes mandatory argument. If it
is followed by two colons, this character takes an optional argument.
If only the first part is present, the ‘|’ separator may be
omitted. Examples:

"c"

"c|"

Short option -c.

"f:"

"f:|"

Short option -f, taking a mandatory argument.

"f::"

"f::|"

Short option -f, taking an optional argument.

If the vertical bar is present and is followed by any characters, these
characters specify the name of a long option, synonymous to the short
one, specified by the first part. Any mandatory or optional arguments
to the short option remain mandatory or optional for the corresponding
long option. Examples:

"f:|file"

Short option -f, or long option --file, requiring an
argument.

"f::|file"

Short option -f, or long option --file, taking an
optional argument.

In any of the above cases, if this option appears in the command line,
getopt returns its short option character.

To define a long option without a short equivalent, begin it with a
bar, e.g.:

"|help"

If this option is to take an argument, this is specified using the
mechanism described above, except that the short option character
is replaced with a minus sign. For example:

"-:|output"

Long option --output, which takes a mandatory argument.

"-::|output"

Long option --output, which takes an optional argument.

If an option is returned that has an argument in the command line,
getopt stores this argument in the variable optarg.

After each invocation, getopt sets the variable optind
to the index of the next argv element to be parsed. Thus,
when the list of options is exhausted and the function returned an
empty string, optind contains the index of the the first
element that is not an option.

When getopt encounters an option that is not described in its
arguments or if it detects a missing option argument it prints an
error message using mailfromd logging facilities, stores
the offending option in the variable optopt, and returns ‘?’.

If printing error message is not desired (e.g. the application is going
to take care of error messaging), it can be disabled by setting the
variable opterr to ‘0’.

The third argument to getopt, called controlling argument,
may be used to control the behavior of the function. If it is a
colon, it disables printing the error message for unrecognized options
and missing option arguments (as setting opterr to ‘0’
does). In this case getopt returns ‘:’, instead of
‘?’ to indicate missing option argument.

If the controlling argument is a plus sign, or the environment
variable POSIXLY_CORRECT is set, then option processing stops as
soon as a non-option argument is encountered. By default, if options
and non optional arguments are intermixed in argv, getopt
permutes them so that the options go first, followed by non-optional
arguments.

If the controlling argument is ‘-’, then each non-option
element in argv is handled as if it were the argument of
an option with character code 1 (‘"\001"’, in MFL
notation. This can used by programs that are written to expect options and
other argv-elements in any order and that care about the
ordering of the two.

Any other value of the controlling argument is handled as an option
definition.

A special language construct is provided to supply the second
argument (argv) to getopt and similar functions:

vaptr(param)

where param is a positional parameter, from which to start the
array of argv. For example:

3.18 Logging and Debugging

Depending on its operation mode, mailfromd tries to guess
whether it is appropriate to print its diagnostics and informational
messages on standard error or to send them to syslog. Standard error
is assumed if the program is run with one of the following command
line options:

If none of these are used, mailfromd switches to syslog as
soon as it finishes its startup. There are two ways to communicate
with the syslogd daemon: using the syslog
function from the system libc library, which is a blocking
implementation in most cases, or via internal, asynchronous,
syslog implementation. Whether the latter is compiled in and which
of the implementation is used by default is determined while compiling
the package, as described in Using non-blocking syslog.

The --logger command line option allows you to manually
select the diagnostic channel:

--logger=stderr

Log everything to the standard error.

--logger=syslog

Log to syslog.

--logger=syslog:async

Log to syslog using the asynchronous syslog implementation.

Another way to select the diagnostic channel is by using the
logger statement in the configuration file. The statement
takes the same argument as its command line counterpart.

The rest of details regarding diagnostic output are controlled by
the logging configuration statement.

The default syslog facility is ‘mail’; it can be changed
using the --log-facility command line option or
facility statement. Argument in both cases is a valid
facility name, i.e. one of: ‘user’, ‘daemon’,
‘auth’, ‘authpriv’, ‘mail’, and ‘local0’
through ‘local7’. The argument can be given in upper, lower or
mixed cases, and it can be prefixed with ‘log_’:

Another syslog-related parameter that can be configured is the
tag, which identifies mailfromd messages. The default tag
is the program name. It is changed by the --log-tag
(-L command line option and the tag logging statement.

The following example configures both the syslog facility and tag:

logging {
facility local7;
tag "mfd";
}

As any other UNIX utility, mailfromd is very quiet unless it
has something important to communicate, such as, e.g. an error condition.
A set of command line options is provided for
controlling the verbosity of its output.

The --trace option enables tracing Sendmail actions
executed during message verifications. When this option is given,
any accept, discard, continue, etc. triggered
during execution of your filter program will leave their traces in
the log file. Here is an example of how it looks like (syslog time
stamp, tag and PID removed for readability):

This shows that while verifying the message with ID
‘k8DHxvO9030656’ the reject action was executed by filter
script /etc/mailfromd.mf at line 45.

The use of message ID in the log deserves a special
notice. The program will always identify its log messages with
the ‘Message-Id’, when it is available. Your responsibility as an
administrator is to make sure it is available by configuring
your MTA to export the macro ‘i’ to mailfromd.
The rule of thumb is: make ‘i’ available to the very first
handler mailfromd executes. It is not necessary to export
it to the rest of the handlers, since mailfromd will cache
it. For example, if your filter script contains ‘envfrom’ and
‘envrcpt’ handlers, export ‘i’ for ‘envfrom’.
The exact instructions on how to ensure it depend on the
MTA you use. For ‘Sendmail’, refer to Sendmail.
For MeTA1, see MeTA1, and pmult-macros. For
‘Postfix’, see Postfix.

The debugging output is controlled by a set of levels, each of which
can be set independently of others. Each debug level consists of a
category name, which identifies the part of package for which
additional debugging is desired, and a level number, which indicates
how verbose should its output be.

Valid debug levels are:

error

Displays error conditions which are normally not reported, but passed
to the caller layers for handling.

Implementation and applicability of each level of debugging differs
between various categories. Categories built-in to mailutils are
described in http://mailutils.org/wiki/Debug_level. Mailfromd
introduces the following additional categories:

db

trace0

Detailed debugging info about expiration and compaction.

trace5

List records being removed.

dns

trace8

Verbose information about attempted DNS queries and their results.

trace9

Enables ‘libadns’ internal debugging.

srvman

trace0

Additional information about normal conditions, such as subprocess
exiting successfully or a remote party being allowed access by ACL.

For example, the following invocation enables levels up to
‘trace2’ in category ‘engine’, all levels in category
‘savsrv’ and levels up to ‘trace0’ in category
‘srvman’:

$ mailfromd --debug='engine.trace2;savsrv;srvman.trace0'

You need to have sufficient knowledge about mailfromd
internal structure to use this form of the --debug option.

To control the execution of the sender verification functions
(see SMTP Callout functions), you may use
--transcript (-X) command line option which enables
transcripts of SMTP sessions in the logs. Here is an example
of the output produced running mailfromd --transcript:

3.19 Runtime Errors

A runtime error is a special condition encountered during
execution of the filter program, that makes further execution of
the program impossible. There are two kinds of runtime errors: fatal
errors, and uncaught exceptions. Whenever a runtime error occurs,
mailfromd writes into the log file the following message:

RUNTIME ERROR near file:line: text

where file:line indicates approximate source file location
where the error occurred and text gives the textual description
of the error.

Fatal runtime errors

Fatal runtime errors are caused by a condition that is impossible to
fix at run time. For version 8.7 these are:

Not enough memory

There is not enough memory for the execution of the program. Try to
make more memory available for mailfromd or to reduce
its memory requirements by rewriting your filter script.

Out of stack space; increase #pragma stacksize

Heap overrun; increase #pragma stacksize

memory chunk too big to fit into heap

These errors are reported when there is not enough space left on
stack to perform the requested operation, and the attempt to resize the stack
has failed. Usually mailfromd expands the stack when the need
arises (see automatic stack resizing). This runtime error
indicates that there were no more memory available for stack
expansion. Try to make more memory available for mailfromd
or to reduce its memory requirements by rewriting your filter script.

Stack underflow

Program attempted to pop a value off the stack but the stack was
already empty. This indicates an internal error in the
MFL compiler or mailfromd runtime engine. If you
ever encounter this error,
please report it to bug-mailfromd@gnu.org.ua. Include
the log fragment (about 10-15 lines before and after this log message)
and your filter script. See Reporting Bugs, for more
information about bug reporting.

pc out of range

The program counter is out of allowed range. This is a severe
error, indicating an internal inconsistency in mailfromd
runtime engine. If you encounter it, please report it to
bug-mailfromd@gnu.org.ua. Include the log fragment (about
10-15 lines before and after this log message) and your filter script.
See Reporting Bugs, for more information about how to report a
bug.

Programmatic runtime errors

These indicate a programmatic error in your filter script, which the
MFL compiler was unable to discover at compilation stage:

Invalid exception number: n

The throw statement used a not existent exception number n.
Fix the statement and restart mailfromd. See throw, for
the information about throw statement and see Exceptions,
for the list of available exception codes.

No previous regular expression

You have used a back-reference (see Back references), where there
is no previous regular expression to refer to. Fix this line in
your code and restart the program.

Invalid back-reference number

You have used a back-reference (see Back references), with a
number greater than the number of available groups in the previous
regular expression. For example:

if $f matches "(.*)@gnu.org"
# Wrong: there is only one group in the regexp above!
set x \2
…

Fix your code and restart the daemon.

Uncaught exceptions

Another kind of runtime errors are uncaught exceptions,
i.e. exceptional conditions for which no handler was installed
(See Exceptions, for information on exceptions and on how to
handle them). These errors mean that the programmer (i.e. you), made
no provision for some specific condition. For example, consider the
following code:

prog envfrom
do
if $f mx matches "yahoo.com"
foo()
fi
done

It is syntactically correct, but it overlooks the fact that mx
matches may generate e_temp_failure exception, if the underlying
DNS query has timed out (see Special comparisons). If
this happens, mailfromd has no instructions on what to do
next and reports an error. This can easily be fixed using a catch
statement, e.g.:

Another common case are undefined Sendmail macros. In this case the
e_macroundef exception is generated:

RUNTIME ERROR near foo.c:34: Macro not defined: {client_adr}

These can be caused either by misspelling the macro name (as in the
example message above) or by failing to export the required name in
Sendmail milter configuration (see exporting macros). This error
should be fixed either in your source code or in sendmail.cf
file, but if you wish to provide a special handling for it, you can
use the following catch statement:

catch e_macroundef
do
…
done

Sometimes the location indicated with the runtime error message is
not enough to trace the origin of the error. For example, an error
can be generated explicitly with throw statement
(see throw):

RUNTIME ERROR near match_cidr.mf:30: invalid CIDR (text)

If you look in module match_cidr.mf, you will see
the following code (line numbers added for reference):

Now, it is obvious that the value of cidr argument to
match_cidr was wrong, but how to find the caller that passed
the wrong value to it? The special command line option
--stack-trace is provided for this. This option enables
dumping stack traces when a fatal error occurs. The traces
contain information about function calls. Continuing our example,
using the --stack-trace option you will see the following diagnostics:

Each trace line describes one stack frame. The lines appear in the
order of most recently called to least recently called. Each frame
consists of:

Value of the program counter at the time of its execution;

Source code location, if available;

Name of the function called.

Thus, the example above can be read as: “the function
match_cidr was called by the function bar in file
test.mf at line 13. This function was called from
the function bar, in file test.mf at line 13. In its turn,
bar was called by the function foo, in file
mailfromd.mf at line 18”.

Examining caller functions will help you localize the source of
the error and fix it.

You can also request a stack trace any place in your code, by
calling the stack_trace function. This can be useful for
debugging, or in your catch statements.

3.20 Notes and Cautions

This section discusses some potential culprits in the
MFL.

It is important to execute special caution when writing format
strings for sprintf (see String formatting) and strftime
(see strftime) functions. They use ‘%’ as a character
introducing conversion specifiers, while the same character is used to
expand a MFL variable within a string. To prevent this
misinterpretation, always enclose format specification in single
quotes (see singe-vs-double). To illustrate this, let’s consider
the following example:

echo sprintf ("Mail from %s", $f)

If a variable s is not declared, this line will produce the
‘Variable s is not defined’ error message, which will allow you
to identify and fix the bug. The situation is considerably worse if
s is declared. In that case you will see no warning message,
as the statement is perfectly valid, but at the run-time the variable
s will be interpreted within the format string, and its value
will replace %s. To prevent this from happening, single quotes
must be used:

echo sprintf ('Mail from %s', $f)

This does not limit the functionality, since there is no need to fall
back to variable interpretation in format strings.

Yet another dangerous feature of the language is the way to refer to
variable and constant names within literal strings. To expand a
variable or a constant the same notation is used
(See Variables, and see Constants). Now, lets consider the
following code:

const x 2
string x "X"
prog envfrom
do
echo "X is %x"
done

Does %x in echo refers to the variable or to the
constant? The correct answer is ‘to the variable’. When executed, this
code will print ‘X is X’.

As of version 8.7, mailfromd will always print a
diagnostic message whenever it stumbles upon a variable having the
same name as a previously defined constant or vice versa. The
resolution of such name clashes is described in detail in
See variable--constant shadowing.

Future versions of the program may provide a non-ambiguous way of
referring to variables and constants from literal strings.