Send mail from Docker container with host's Postfix

Solution 1:

Because you have a working solution, here I will try to explain different behavior when you telnet to postfix (SMTP) and when you use sendmail (non-SMTP).

FYI, OpenDKIM will invoked by postfix with Milter mechanism. You can get some info how milter implementation in postfix via this official documentation. Here the diagram of milter hook in postfix.

             SMTP-only       non-SMTP
             filters         filters
                ^ |            ^ |
                | v            | |
Network ->  smtpd(8)           | |
                       \       | V
Network ->  qmqpd(8)    ->  cleanup(8)  ->  incoming
                       /
            pickup(8)
               :
Local   ->  sendmail(1)

You can see that sendmail-way (non-SMTP) and telnet-way (SMTP) has different processing order.

  • The non-SMTP email will processed by cleanup before injected to milter. Cleanup daemon was responsible for adding missing headers: (Resent-) From:, To:, Message-Id:, and Date:. Therefore your email will have complete header when injected to OpenDKIM milter even original email had incomplete header.

  • The SMTP email will injected to OpenDKIM milter before any cleanup processing take place. Therefore, if your original email had incomplete header then opendkim may refuse to sign the email. The From: header was mandatory (see RFC 6376) and if an email doesn't have it, OpenDKIM will refuse to sign the email and give you a warning

    can't determine message sender; accepting
    

As I never use docker, than I don't know what limitation on sendmail/pickup inside the a container. I think David Grayson's workaround was safe enough to ensure that OpenDKIM signing the message.

Solution 2:

You have to point inet_interfaces to docker bridge (docker0) in postfix config located at set /etc/postfix/main.cf

inet_interfaces = <docker0_ip>

More internal working detail at sending-email-from-docker-through-postfix-installed-on-the-host


Solution 3:

This is a half-answer, or at least a half-tested one, since I'm currently working through the same problem. I'm hoping someone can help flesh out what I've missed.

The answer from the OP (David Grayson) sounds to me like a re-invention of the postdrop mail-spool, but using that mail spool sounds like a promising approach, so here's where I've gotten to.

The /usr/bin/sendmail compatibility interface provided by postfix passes mail to postdrop, which is sgid postdrop, allowing it to store mail into the maildrop queue at /var/spool/postfix/maildrop. This should occur in the docker container. The rest of postfix should hopefully not have to run in the container.

So, I'm host mounting /var/spool/postfix/maildrop and /var/spool/postfix/public. I can get mail delivered to /var/spool/postfix/maildrop in the host environment, since I have mounted the maildrop queue directory. Because I have mounted /var/spool/postfix/public, maildrop can signal pickup to collect the mail from the queue. Unfortunately, the uids and gids involved unless I take care of that, meaning that pickup in the host directory can't read the spool files, and worse the postfix installation messes up the permissions on the maildrop directory in the host environment.

Still, this seems to work:

$ cat Dockerfile 
FROM debian:jessie
# Ids from parent environment

    RUN groupadd -g 124 postfix && \
        groupadd -g 125 postdrop && \
    useradd -u 116 -g 124 postfix

    RUN apt-get update && \
      DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y \
        postfix \
        bsd-mailx

    CMD echo test mail | mail [email protected]

$ sudo docker build   .
...
Successfully built 16316fcd44b6

$ sudo docker run   -v /var/spool/postfix/maildrop:/var/spool/postfix/maildrop \
  -v /var/spool/postfix/public:/var/spool/postfix/public 16316fcd44b6

While it works, I'm not terribly happy with hard coding the uids and gids. This means that the same container can't be counted to run the same everywhere. I figure though that if instead of mounting the volume from the host I mount it from a container which runs postfix, then it won't ever conflict, and I only need one postfix installation to get mail out from many containers. I'd set those uids and gids in a base image which all my containers inherit from.

I do wonder though if this is really a good approach. With such a simple mail configuration, and no daemon in use on the container for re-trying delivery, a simpler local MTA like msmtp might be more appropriate. It would deliver via TCP to a relay on the same host, where spooling would occur.

Concerns with the msmtp approach include:

  • more possibility of losing mail if the smtp relay it sends to is not available. If that's a relay on the same host, then the chance of network problems is low, but I'd have to be careful about how I restarted the relay container.
  • performance?
  • If a large burst of mail goes through, does mail start to get dropped?

In general, the shared postfix spool approach seems more likely to be a fragile configuration to set up, but less likely to fail at run time (relay unavailable, so mail dropped).


Solution 4:

I decided that the way the container will send mail is to write it to a file in a particular directory, which will be accessible from both the container and the host as a Docker "volume".

I made a shell script called mailsender.sh that reads mails from a specified directory, sends them to sendmail, and then deletes them:

#!/bin/bash
# Runs on the host system, reading mails files from a directory
# and piping them to sendmail -t and then deleting them.

DIR=$1

if [ \! \( -d "$DIR" -a -w "$DIR" \) ]
then
  echo "Invalid directory given: $DIR"
  exit 1
fi

echo "`date`: Starting mailsender on directory $DIR"

cd $DIR

while :
do
  for file in `find . -maxdepth 1 -type f`
  do
    echo "`date`: Sending $file"
    sendmail -t < $file
    rm $file
  done
  sleep 1
done

Ubuntu uses upstart so I created a file named /etc/init/mailsender.conf to turn this script into a daemon:

description "sends mails from directory"
start on stopped rc RUNLEVEL=[2345]
stop on runlevel[!2345]
respawn
exec start-stop-daemon --start --make-pidfile --pidfile /var/run/mailsender.pid --exec
/path/to/mailsender.sh /var/mailsend

I can start the service with start mailsender and stop it with stop mailsender. I can look at its logs in /var/log/upstart/mailsender.log, and of course I can monitor it using the PID file.

You need to create the /var/mailsend directory and then make it accessible from the Docker container by adding the argument -v /var/mailsend:/var/mailsend to your docker run command.