Sendmail on OpenBSD

OpenBSD is delivered with sendmail as the standard MTA, but I discovered it also ships with a planned replacement OpenSMTPD which is being developed by the OPenBSD team.

I thought I'd see how easy it was to setup OPenSMTPD instead of my usual sendmail. Reading the man pages and also a how-to it seemed quite simple. First, I disabled sendmail and enabled OpenSMTPD in /usr/etc/rc.conf.local:

  sendmail_flags=NO
  smtpd_flags=

Then I created /etc/mail/smtpd.conf.local with the following content:

  # cobalt.hydrus.org.uk local smtpd configuration file
  #
  # 26th November, 2011 MPW
  #

  # listen for mail on external interface, normal smtp port 
  listen on fxp0 port 25

  # expire time for bounce back
  expire 4h

  # accept mail from the local machine (lo0) to localhost accounts and
  # pass to the recipient's procmail rules. Address mapping is derived
  # from the aliases file.  This rule is for internal machine mail only.
  accept from local for local alias aliases deliver to mda "procmail -f -"

  # Handle outgoing mail to a machine on the local network
  # It seems impossible to generalise this; all machines must be
  # specified.
  accept from local for domain "topaz" relay via "topaz.hydrus.org.uk"

  # accept mail from any ip address to the hostnames defined. Address
  # mapping is derived from the aliases file.  This rule is for external
  # mail being sent to a local account.
  accept from all for domain "*.hydrus.org.uk" alias aliases deliver to mbox

  # outgoing mail is accepted from localhost only and relayed through
  # my ISP.  This rule is for local users _only_ to send mail through
  # external mail gateway. No open relays!
  accept from local for all relay via "smtp.isp.com" \
     as "@hydrus.org.uk"

This is included in the main /etc/mail/smtpd.conf file:

  #       $OpenBSD: smtpd.conf,v 1.2 2009/11/03 22:32:10 gilles Exp $

  # This is the smtpd server system-wide configuration file.
  # See smtpd.conf(5) for more information.

  listen on lo0

  map "aliases" { source db "/etc/mail/aliases.db" }

  # cobalt local configuration
  include "/etc/mail/smtpd.conf.local"

#  accept for local alias aliases deliver to mbox
#  accept for all relay

The configuration is simple, but because of that, there are some things you can't do. As you can see from the comment in the /etc/mail/smtpd.conf.local file, it is not possible to have a general rule to allow mailing to other machines within the local area network. You have to enable it on a machine by machine basis.

It is also (apparently) not possible to re-write the sender address in the body of the mail to omit the host name (i.e cobalt in this case), so that mail send outside of the local network appears to come from the hydrus.org.uk domain rather than a specific host within the domain.

I initally thought the relay ... as rule would let me handle the latter issue, but that only re-writes the address in the envelope. But I found that didn't work properly. With the as rule defined for outgoing mail, the envelope FROM field should be re-written with the @domain specified, leaving the username untouched. However, the MAIL FROM field was null, i.e.

  MAIL FROM:<>

After digging around in the innards of smtpd, I found the cause of the problem, in lka_session.c:

--- lka_session_orig.c  Sun Nov 27 12:24:01 2011
+++ lka_session.c       Sun Nov 27 12:23:30 2011
@@ -393,9 +393,15 @@
         }
     }
     else if (new_ep->delivery.type == D_MTA) {
-        if (ep->rule.r_as)
-            new_ep->delivery.from = *ep->rule.r_as;
-
+        if (ep->rule.r_as) {
+            if ((*ep->rule.r_as).user[0] != '\0')
+                strlcpy(new_ep->delivery.from.user,(*ep->rule.r_as).user,
+                        sizeof(new_ep->delivery.from.user));
+            if ((*ep->rule.r_as).domain[0] != '\0')
+                strlcpy(new_ep->delivery.from.domain,(*ep->rule.r_as).domain,
+                        sizeof(new_ep->delivery.from.domain));
+        }
+    }
     TAILQ_INSERT_TAIL(&lks->deliverylist, new_ep, entry);
 }

In the original code, the entire as specification overwrote the envelope from field, even if the user field was empty. I also found another bug in chasing this down, in the parse.y code, which caused smptd to error exit, but without an error message, if the as field just contained a user name e.g. as "mark". The source of the error was missing block braces (I also fixed a incorrect error message while I was there):

  --- parse_orig.y        Sun Nov 27 17:14:53 2011
  +++ parse.y     Sun Nov 27 17:12:32 2011
  @@ -870,10 +870,11 @@
                           p = strrchr($2, '@');
                           if (p == NULL) {
                                   if (strlcpy(maddr.user, $2, sizeof (maddr.user))
  -                                    >= sizeof (maddr.user))
  +                                    >= sizeof (maddr.user)) {
                                           yyerror("user-part too long");
                                           free($2);
                                           YYERROR;
  +                }
               }
                           else {
                                   if (p == $2) {
  @@ -881,7 +882,7 @@
                                           p++;
                                           if (strlcpy(maddr.domain, p, sizeof (maddr.domain))
                                               >= sizeof (maddr.domain)) {
  -                                                yyerror("user-part too long");
  +                                                yyerror("domain-part too long");
                                                   free($2);
                                                   YYERROR;
                                           }
  @@ -902,6 +903,7 @@
                                           }
                                   }
                           }
  +
                           if (maddr.user[0] == '\0' && maddr.domain[0] == '\0') {
                                   yyerror("invalid 'relay as' value");
                                   free($2);
  @@ -918,6 +920,7 @@
                                           YYERROR;
                                   }
                           }
  +
                           maddrp = calloc(1, sizeof (*maddrp));
                           if (maddrp == NULL)
                                   fatal("calloc");

Of course, I should have looked at the current source code before I began digging, because the first issue is now fixed. The second still exists, however. Anyway, good to have OpenSMTPD working as documented.