Sendmail support for Cyrus IMAP IGNOREQUOTA LMTP extension

Cyrus IMAP system supports IGNOREQUOTA LMTP extension but Sendmail does not. What is the purpose of this extension and can we modify Sendmail to support it?

Background

The relevant RFC can be found at:

LMTP Service Extension for Ignoring Recipient Quotas

The introduction states the rationale (emphasis by Kalevi Kolttonen):

In many cases, the [LMTP] protocol is used to transfer messages to a delivery agent which might impose some limits on the usage of system resources by individual recipients (e.g. disk space on a mailstore). Sometimes it may be desirable for an LMTP client to inject a message into the mailstore regardless of these quotas (e.g. an automated process notifying users that they are over quota).

The plan

So we need a way to tell Sendmail to use IGNOREQUOTA extension. I chose Sendmail 8.15.2 because as of this writing (October 14th, 2016), it is the latest version. I think it is the best solution to invent a new mailer flag that forces Sendmail to use IGNOREQUOTA. There is no need to construct a more complicated logic that would interpret the LHLO response from the server: We are doing local mail delivery after all and we already know in advance whether our LMTP server supports the IGNOREQUOTA extension or not.

The patch

Fortunately, and perhaps surprisingly, it turns out that the modifications to the Sendmail 8.15.2 source tree are very simple indeed. We only need to alter two files: sendmail/sendmail.h and sendmail/usersmtp.c. There is a total of 57 M_XXXXXXXX mailer flag #define definitions in sendmail.h, but two of them ('+' and '-') are characters that are reserved just for the Sendmail M4 macro configuration facility. What did I do?

  1. I chose an unused letter 'Q' to be the new mailer flag that forces Sendmail to use the IGNOREQUOTA extension
  2. I added one if-clause to check whether the 'Q' flag is set and to verify that there is enough space in the RCPT TO option buffer

You can download the gzipped Sendmail 8.15.2 patch from here.

The patch content looks like this:

diff -urp sendmail-8.15.2/sendmail/sendmail.h sendmail-8.15.2-patched/sendmail/sendmail.h
--- sendmail-8.15.2/sendmail/sendmail.h	2015-06-19 15:59:29.000000000 +0300
+++ sendmail-8.15.2-patched/sendmail/sendmail.h	2016-10-14 13:51:58.083469892 +0300
@@ -513,6 +513,7 @@ struct mailer
 #define M_PLUS		'+'	/* Reserved: Used in mc for adding new flags */
 #define M_MINUS		'-'	/* Reserved: Used in mc for removing flags */
 #define M_NOMHHACK	'!'	/* Don't perform HM hack dropping explicit from */
+#define M_IGNQUOTA	'Q'	/* Force IGNOREQUOTA RCPT TO LMTP extension */
 
 /* functions */
 extern void	initerrmailers __P((void));
diff -urp sendmail-8.15.2/sendmail/usersmtp.c sendmail-8.15.2-patched/sendmail/usersmtp.c
--- sendmail-8.15.2/sendmail/usersmtp.c	2014-12-05 17:42:28.000000000 +0200
+++ sendmail-8.15.2-patched/sendmail/usersmtp.c	2016-10-14 13:51:44.099380746 +0300
@@ -2376,6 +2376,15 @@ smtprcpt(to, m, mci, e, ctladdr, xstart)
 		}
 	}
 
+	/* Force IGNOREQUOTA RCPT TO LMTP extension? */
+	if (bitnset(M_IGNQUOTA, m->m_flags) &&
+	    SPACELEFT(optbuf, bufp) > 12)
+	{
+		(void) sm_snprintf(bufp, SPACELEFT(optbuf, bufp),
+			 " IGNOREQUOTA");
+		bufp += strlen(bufp);
+	}
+
 	smtpmessage("RCPT To:<%s>%s", m, mci, to->q_user, optbuf);
 	mci->mci_state = MCIS_RCPT;

Sendmail configuration

I assume your LMTP server is Cyrus lmtpd that supports IGNOREQUOTA. We must next modify Sendmail's Cyrus V2 mailer definition to include our new 'Q' flag. Make sure your sendmail.mc contains something like the following:

MODIFY_MAILER_FLAGS(`CYRUSV2', `+Q')dnl
define(`CYRUSV2_MAILER_ARGS', `TCP $h 24')dnl
MAILER(cyrusv2)dnl

The corresponding sendmail.cf mailer definition should then look like following, with the upper case 'Q' appended to the F= flags definition:

Mcyrusv2,	P=[IPC], F=lsDFMnqXzA@/:|mQ,
		S=EnvFromSMTP/HdrFromL, R=EnvToL/HdrToL, E=\r\n,
		T=DNS/RFC822/SMTP,
		A=TCP $h 24

Conclusion

Finally we should go through some notes about using the IGNOREQUOTA patched Sendmail. The simplicity comes with a price: Once you set the 'Q' mailer flag, every LMTP delivery succeeds provided that the recipient is valid and the LMTP server has not run out of disk space. That is why you should keep your regular Cyrus V2 Sendmail instance running in order to get the normal "over quota" responses, i.e. do not install the patched Sendmail binary as /usr/sbin/sendmail. Instead, you could put it elsewhere and when you need the IGNOREQUOTA patched Sendmail for sending out messages, you would just run it as client like this:

/path/to/patched/sendmail -Am -v -t < /path/to/your/message

Note that the IGNOREQUOTA patched Sendmail and the regular Sendmail could co-exist and even share the same configuration /etc/mail/sendmail.cf. However, that might get confusing. The patched Sendmail would interpret the new 'Q' mailer flag, but the regular Sendmail would simply ignore it and run the cyrusv2 mailer in the standard way. Using the same sendmail.cf could also lead to problems when the patched Sendmail could not deliver the message at the first try. Whatever reason for the initial failure, the message would end up in /var/spool/mqueue for later retries. The later retries would be done by the regular Sendmail, ignoring the 'Q' flag and failing due to over quota recipient boxes. When running two Sendmail instances on one host, they should have different configuration files and different mail queues to keep things working correctly.

Alternatively, you could just run the patched Sendmail on a special purpose host that is designed for sending out notifications even to those users whose quota has been exceeded. That way you would avoid the horrible hassle of managing two Sendmail instances on one host. This is definitely the option I would recommend.

Of course you could install the patched Sendmail as /usr/sbin/sendmail and control the LMTP IGNOREQUOTA behaviour by adding and removing the 'Q' mailer flag as needed. For example, you could leave it out for normal everyday use and enable it when in need of sending out important nofitications that should reach everyone no matter what.


IMPORTANT UPDATE: PLEASE READ ON IF YOU ARE INTERESTED IN THE IGNOREQUOTA LTMP EXTENSION

Rethinking IGNOREQUOTA LMTP extension

It is now October 13th, 2018. It is pretty clear that the decision whether to use IGNOREQUOTA LMTP extension could be more configurable. I released my original patch almost exactly two years ago in October 14th, 2016. The C code is of course just fine, because the changes are probably as small as they can be to implement IGNOREQUOTA. My idea to create a new mailer flag Q was also a valid choice.

Thinking back, I should also have provided a more complete, manageable, configurable way to use IGNOREQUOTA rather than just modifying the existing cyrusv2 mailer flags. I guess I was just too eager to see whether this would work out or not. When it did, I was happy that the solution was very simple and left it at that. But a few days ago I came up with an alternative idea. It works, but might be impractical, because it requires database managing to control IGNOREQUOTA usage. Running a dedicated virtual machine with Sendmail having IGNOREQUOTA unconditionally enabled would probably be a better idea in many circumstances.

Despite that, I thought that you should be able to choose IGNOREQUOTA based on the whole recipient address dynamically at runtime. In other words, you should be able to tell Sendmail that e.g. for the recipient john.smith@example.com, use IGNOREQUOTA extension, but for another recipient jokke.hamalainen@example.com do not use it. After that idea I checked out sendmail.cf, but could not find a hook to do that.

I remembered that hooks like Local_check_mail and Local_check_rcpt did exist, but I soon realized that selecting a mailer was not so simple. So the first stumbling block was find out how to insert a call to my custom ruleset that would consult a database map to do the appropriate mailer selection.

Sendmail guru to the rescue

Not wanting to duplicate work that somebody else had already done well, I tried to remember the name of a Polish Sendmail guru who used to hang in comp.mail.sendmail newsgroup, but could not recall the name exactly. I typed in something to do a Google search and it was close enough to find the right guy.

His name is Andrzej Filip and I knew that he had created a patch that would enable customized mailertable lookups. I went to his fine website and found Mailertable Rule Sets from there.

This seemed to be a very promising starting point, but the patch was pretty old. I think it was originally released in 2004, but fortunately the core email standards have not changed that much since, and what is more, Andrzej mentioned that the patch applies cleanly to Sendmail 8.13.0. I admit that 8.13 first release is quite old too. Time passes quickly.

I still decided to download the mrs.m4 patch to see what it was like. Well, that patch did not contain any of the logic I was searching - it had only one effective line and that is:

define(`_MRS_RELAY_', `')

So when you add:

FEATURE(`mrs')dnl

to your sendmail.mc it just enables Andrzej's custom Mailertable Rule Set feature, but the actual logic is implemented elsewhere. Sure enough his webpage says that we have to modify Sendmail's proto.m4.

Seeing the mrs-8.12.11.patch really blew my mind! Andrzej's idea is to do the following:

ifdef(`_MRS_RELAY_',`dnl
R< $={MRS} : $* > $*	$@ $> $1 < $2 > $3
dnl')

The sendmail.cf ruleset modification is only the second line. The first and third lines are m4 macro language and they choose whether to enable this feature or not. What is the meaning of the R line? It can look pretty weird and mysterious. R line always defines a rule.

First Andrzej checks whether we have a Sendmail token match that is contained in his custom class called MRS. If we have match, he calls the ruleset having the name that was the match, passing on tokens that are of format:

<param>user<@some.domain>

This solution is absolutely amazing, because rather than hardcoding anything, it provides us with a means to call our custom ruleset! So using this feature we are free to do pretty much anything we want, and we can even pass a parameter of our own choosing using mailertable. How cool is that? I was ecstatic, imagine that just one line of sendmail.cf code that did all that! Of course I know it is an addition to Sendmail's existing, larger mailertable processing ruleset, but this patch is very nice regardless of that.

I next downloaded Sendmail 8.15.2 and inspected proto.m4 to see if there were notable updates in that version. To my delight, the context into which Andrzej's patch should be applied looked just fine. It was of course in the mailertable logic like I had anticipated, and indeed the patch went in with no problems at all.

To get going on Fedora Linux 28, I just did:

cp -iv mrs.m4 /usr/share/sendmail-cf/feature
cp -iv mrs-8.12.11.patch /usr/share/sendmail-cf/m4
cd /usr/share/sendmail-cf/m4
patch -p3 < mrs-8.12.11.patch

To be clear, even though Andrzej's patch is essentially just one single line of sendmail.cf code, conditionally included, it must have taken some serious brain work to find the appropriate place to insert the extra rule and to make it as elegant as it is now! I am even willing to consider this patch a piece of art. It saved me from many hours of work, I know that. This hook is even better than I could have come up with. What a brilliant idea.

Implementing dynamic mailer selection logic

Having now extra confidence that this will work out, I proceeded to enable Mailertable Rule Set by having this in my sendmail.mc:

FEATURE(`mrs')dnl

Then I created /etc/mail/ignorequota and in that file, I inserted line:

john.smith@example.com		IGNOREQUOTA

Next I built a Berkeley DB database file using the familiar makemap command that is shipped with Sendmail:

makemap hash /etc/mail/ignorequota < /etc/mail/ignorequota

So the database lookup key is john.smith@example.com and its value is string literal IGNOREQUOTA. You probably guessed that a successful lookup returning value IGNOREQUOTA means that we want Sendmail to use our IGNOREQUOTA LMTP extension for that particular recipient address. Yes, that is our current plan.

Let's use cyrusv2 mailer again like we did in October 2016, but this time we do not modify the original mailer definition. We leave it as it is, and we will create an almost exact copy of it, with our Q flag added. The exact paths depend on your OS, but on Fedora Linux 28, we do the following:

cp -iv /usr/share/sendmail-cf/mailer/cyrusv2.m4 /usr/share/sendmail-cf/mailer/cyrusv2iq.m4

And this is the first modified line in /usr/share/sendmail-cf/mailer/cyrusv2iq.m4:

_DEFIFNOT(`CYRUSV2IQ_MAILER_FLAGS', `A@/:|mQ')

Note that we renamed CYRUSV2_MAILER_FLAGS to CYRUSV2IQ_MAILER_FLAGS and appended Q to the flags so that this copied mailer will use the IGNOREQUOTA patch that I wrote in October 2016.

Our second modification changed Mcyrusv2iq part to reflect the new mailer name. In addition CYRUSV2_MAILER_FLAGS was renamed to CYRUSV2IQ_MAILER_FLAGS. The result is like this:

Mcyrusv2iq,     P=[IPC], F=_MODMF_(CONCAT(_DEF_CYRUSV2_MAILER_FLAGS, CYRUSV2IQ_MAILER_FLAGS), `CYRUSV2'),

Our cyrusv2iq mailer definition can be downloaded using this link.

Knowing that it would be needed, I dug out my Batbook, meaning O'Reilly's Sendmail (4th edition). Here is a photo of the book's front cover:

Sendmail Batbook 4th edition

This book is legendary, informational and over 1300 pages long, packed with cleanly and clearly written technical Sendmail documentation. It is simply great job from the authors Bryan Costales, George Jansen, Claus Aßmann and Gregory Neil Shapiro. Claus is actually the lead developer of Sendmail nowadays, and he has been that for several years. I am sure the other authors are Sendmail experts, too. The quality of Sendmail documentation is exceptionally good. It is fantastic.

Although I am convinced that Sendmail's original creator Eric Allman is probably some kind of a mildy crazy genius (in a good way, I love him!), I must warn you that the early Batbook editions were not very well organized if you think of their pedagogical aspect. I am not sure if I remember right, but I think the second edition starts by introducing the sendmail.cf rulesets almost right away. Wow, what a shock introduction to most readers.

The later Batbook editions have adopted a much more down-to-earth attitude, and they start by introducing general, higher level concepts first. But of course the 4th edition also documents every single detail, including the rules, because advanced custom configurations are sometimes needed. I do not wish to go into the details concerning the configuration language here. And I certainly do not hate the language.

Eric's rationale for the sendmail.cf language is clear: In the early days, computers had much slower CPUs and had limited amount of RAM. But the sendmail.cf parsing had to be fast, because Sendmail reads the configuration file every time it starts - that means that the compact syntax was good, because it was fast to parse. The language itself was needed, because the email world was pretty heterogenous in those days. There was no single standard protocol or address format, so Eric Allman solved the problem with the best way he could: He invented the sendmail.cf language to transform addresses and to do many other things such as mailer selection.

Eric has jokingly said that he really thinks of sendmail.cf as comparable to assembly language and recommends using the m4 macro language configuration facilities. He is of course quite right, and editing sendmail.mc is the only sane way to configure Sendmail. It has been like that for almost 20 years. Despite that recommendation, ability to write sendmail.cf rulesets is sometimes useful. Yet even in those rare cases, we can usually embed our custom rulesets inside the sendmail.mc so we should never have to edit sendmail.cf directly.

However, there is one criticism I must make concerning the configuration language. It is not so good that the same syntactic parts such as $( and $: have a completely different meaning depending on where they occur. In my opinion the different meanings should have been defined with different syntactic parts. But I guess not many people remember the syntax anyway, so the Batbook must be at hand when writing or reading rules. I know at least I cannot remember the syntax, because it is rarely needed, but I have usually succeeded in doing what I want to achieve. For the most part, the m4 macro facilities are sufficient for everything.

In any case, sorry for that small digression. I am a Sendmail lover, not hater.

Now we are ready to write our custom ruleset. The following lines must be appended to the very end of your sendmail.mc, but do not copy and paste them from here. Instead download ignorequota.mc.gz so that the tab characters are preserved.

LOCAL_CONFIG
C{MRS}check_ignquota
Kignquota hash -T<TMPF> -o /etc/mail/ignorequota
LOCAL_RULESETS
Scheck_ignquota
R<$+>$+<@$+.>				$: $(ignquota $2@$3 $: DEFAULT $) <$1><$2><$3>
R$+<TMPF>				$#error $@ 4.3.0 "451 Temporary database failure"
RIGNOREQUOTA <$+><$+><$+>		$#cyrusv2iq $@ $1 $: $2 < @ $3 >
RDEFAULT <$+><$+><$+>			$#cyrusv2 $@ $1 $: $2 < @ $3 >

Let's analyze this short code, line by line.

  1. LOCAL_CONFIG is m4 macro language and it just tells the preprocessor that the following lines are local configurations. It does not insert any code into the resulting sendmail.cf.

  2. Using C line we insert the name of our check_ignquota ruleset into class MRS so that Mailertable Rule Set matching logic knows that check_ignquota must be called when selecting mailer/host/user triple. Remember that Andrzej's proto.m4 extension bases its matching on class MRS membership.

  3. Using K line we tell Sendmail that ignquota is our new database map's internal name. We need it so that we can refer to the database using our rules.

    Database type is hash and it is a Berkeley DB implementation.

    On temporary database failure, we want Sendmail to append the literal <TMPF> to the lookup key so that we can detect a temporary failure. So the key and <TMPF> will be returned on temporary Berkeley DB failure.

    Using -o we specify that the database file /etc/mail/ignorequota is optional, so if it is not present, Sendmail ignores that fact. Otherwise when not finding /etc/mail/ignorequota, Sendmail would refuse to run.

    Note that the actual Berkeley DB binary format database file is called /etc/mail/ignorequota.db. Sendmail uses only that file, so makemap utility must be used to create the database.

  4. Like LOCAL_CONFIG, LOCAL_RULESETS is m4 macro language and it informs the preprocessor that the following lines are local rulesets. This does not insert any code into the resulting sendmail.cf either.

  5. Scheck_ignquota tells Sendmail that this is the start of a new ruleset definition. The name of the new ruleset is check_ignquota.

    Sendmail rulesets more or less resemble functions in other programming languages. In case you did not know, sendmail.cf configuration language is actually Turing complete, because it includes looping (via kind of "automatic recursion") and it has conditional detection for selective branching. Those two facts are sufficient to prove it. But as you can see, the sendmail.cf computation model looks totally different from a universal Turing machine or ordinary procedural programming languages. Despite that, it is Turing complete - there are many ways to compute the same set of functions.

    For more information about computable functions, see Church–Turing thesis.

    By default, Sendmail always loops on an R rule for as long as it matches, repeatedly rewriting the workspace with RHS ("Right Hand Side"). "Workspace" refers to the LHS ("Left Hand Side"). In order to make an R rule to attempt matching only once, we must use $: in RHS. That is what we will do on the next R line when doing ignquota database lookup.

  6. This R can look scary if you are not used to sendmail.cf configuration language. But it is really only a matching rule that matches the parameter format that MRS feature passes to our ruleset. So we match whatever parameter we got, and use that to do ignquota database lookup (using e.g. john.smith@example.com).

    Note that in the LHS, we have a dot near the end before >. It is there because the email address is in Sendmail's internal canonical address format. That format has domains ending with a dot to signify that the domain part is complete, i.e. the last component is a top-level domain. But we want to strip the trailing dot away so that our database key does not have to have it, too.

    In addition to doing ignquota database lookup here, it is important that we have <$1><$2><$3> appended to the RHS of our matching rule. That enables preserving the tokens we got from mailertable ruleset so that we can use them in the following R lines.

  7. The following R detects whether a temporary database failure occurred. If so, it returns a special error mailer, specifying with Delivery Status Notification codes that this error is temporary. What this means in practice is that MTAs will usually put the message into a mail queue. Then they will try sending again later, say, for seven days before finally giving up.

    The text "Temporary database failure" is meant for human readers and for logging purposes. MTAs do not interpret the text as they rely on the numeric DSN codes only.

  8. Next we use R line to check whether the database lookup returned literal IGNOREQUOTA and other tokens that we have passed around. If we have a match, then we return cyrusv2iq mailer so that Sendmail will use our IGNOREQUOTA LMTP extension for this recipient.

  9. Finally we check for DEFAULT literal and our tokens. If there is a match, we will return the ordinary cyrusv2 mailer.

    In fact we know that this last rule will always match, because we have specified that database lookup failures must always return DEFAULT. We also could have left the DEFAULT literal out when we did the database lookup. If we had done so, we could just match using R$* ($* meaning "match zero or more tokens") in the last R rule. But I chose to use DEFAULT literal, because I think it makes this ruleset more readable.

As you may have noticed, my explanation above was a bit simplified. We cannot return just mailers, because the mailertable processing rules expect us to return a mailer/host/user triple in a strictly defined format. So that is what we did to make things work correctly.

In case you want to learn more about Sendmail rules and its internal workings, I very strongly recommend that you buy the latest edition of Batbook. It contains everything you need to know and the book is not that expensive. It is indeed rare to see documentation that is of such a high quality.

How do we enable our custom ruleset? Fortunately it is quite simple. Like its creator said, FEATURE(`mrs') augments mailertable syntax to include lines such as:

some.domain   mrs_rule_set:param

So we add the following line to /etc/mail/mailertable:

example.com		check_ignquota:[127.0.0.1]

It means that when Sendmail encounters recipient address like john.smith@example.com during its mailertable processing logic, it must call our new custom ruleset check_ignquota and pass [127.0.0.1] as the parameter to check_ignquota.

We used [127.0.0.1] just for testing purposes and there is no further significance in that choice. By the way, the square brackets are standard Sendmail syntax. They instruct Sendmail to skip MX-record lookups for the DNS-name or IP-address contained inside the square brackets.

Before we can test our new configuration, we must rebuild the mailertable database. We type:

makemap hash /etc/mail/mailertable < /etc/mail/mailertable

Sendmail must be aware of our new cyrusv2iq mailer so make sure that you have lines like these near the end of sendmail.mc, before the LOCAL_CONFIG line:

MAILER(cyrusv2)dnl
MAILER(cyrusv2iq)dnl

We are almost done. To be sure that the new settings are effective, we rebuild sendmail.cf:

cd /etc/mail
make sendmail.cf

It is finally time to do two important tests that will decide whether our work paid off or not. First let's see what happens when we tell Sendmail to parse address john.smith@example.com:

[root@localhost ~]# sendmail -bv john.smith@example.com
john.smith@example.com... deliverable: mailer cyrusv2iq, host [127.0.0.1], user john.smith

We see it worked like we wanted: mailer cyrusv2iq was selected, because /etc/mail/ignorequota.db contains key john.smith@example.com with our special value IGNOREQUOTA. Remember that we modified that mailer by adding Q mailer flag, so Sendmail will use our IGNOREQUOTA protocol extension when doing LMTP delivery to john.smith@example.com.

Next let's ask Sendmail to parse address jokke.hamalainen@example.com. This is what happens in our test environment:

[root@localhost ~]# sendmail -bv jokke.hamalainen@example.com
jokke.hamalainen@example.com... deliverable: mailer cyrusv2, host [127.0.0.1], user jokke.hamalainen

That case also worked as expected: mailer cyrusv2 was selected, because jokke.hamalainen@example.com was not in our database /etc/mail/ignorequota.db. The database search returned DEFAULT and so Sendmail chose the ordinary cyrusv2 mailer that has no Q flag defined. This means that no IGNOREQUOTA LTMP protocol extension will be used when delivering mail to jokke.hamalainen@example.com.

Sendmail is very flexible with database maps. I am not totally sure, but I am confident that we can change our database type from Berkeley DB to an LDAP-based map rather easily. When I have more time, I just might investigate that possibility and expand this article again. I will not make any promises, though.

Best regards,
Kalevi Kolttonen <kalevi@kolttonen.fi>