I develop websites on my laptop using a local web server.  Often those sites have functions that send out email, and that needs to be tested, along with everything else.  It can be a problem when some function sends out lots of emails to customers, admins, affiliates – a bunch of people.  If I’m working with a copy of the “live” database to debug some problem, it might try to send emails to places I don’t want (real customers).  What would be nice is if it generated those emails, but just wrote them to a file on disk where I could look at them.

This is pretty easy to do with Postfix, my MTA of choice and the one that ships with my OSX laptop.  First, add a line to the end of /etc/postfix/master.cf:

fs_mail unix - n n - - pipe flags=F user=_www argv=tee /Users/haroldp/Documents/Projects/localmail/htdocs/spool/${queue_id}.${recipient}.txt

Let’s break that down.

  • I am adding a new service that I’m naming, fs_mail.
  • It accepts mail from the pipe service (works like a unix pipe).
  • It should run as user _www, which is the UID my web server runs as.  The mail files created with have 0600 permissions, so only the owner can read them.  More on that later.
  • The pipe argv is set to tee (tee has a man page you can read), to split output to a file.  And that file is in a directory in my websites folder.  Each file will be named using the postfix queue ID and the recipient.  I thought that would be sufficiently unique for my needs.

When that is saved, we need to create the directory to collect those emails and make it writable by the fs_mail process:

mkdir /Users/haroldp/Documents/Projects/localmail/htdocs/spool
chmod 777 /Users/haroldp/Documents/Projects/localmail/htdocs/spool

If you are setting this up on your own computer, you will want to adjust the directory location to your suit your needs.

Now we need to tell postfix to use our new service for all outgoing email. Edit /etc/postfix/mail.cf adding the following:

default_transport = fs_mail

That should do it. Restart postfix and check your mail log for any errors:

sudo postfix stop
sudo postfix start
tail /var/log/mail.log

If that all looks good we can test by sending an email from the command line:

% mail haroldp@internal.org
Subject: test #42
This is a test message. End it by typing a period (.) on its own line, and hitting return.
.
EOT

Check your mail.log again to see that it worked without error. Check your new spool directory to see if there is a mail file in there.

If that is working, then you are done! But remember that we saved those messages as UID _www? That is the default user ID of apache web processes on OSX, so my local web server can read those files. For extra credit build a web page to view the 10 newest emails in your spool dir:

<?
# number of messages to display:
$max_messages = 10;

if ( isset($_POST['filename']) ) {
    # deleting a file
    $filename = './spool/' . $_POST['filename'];
    if ( file_exists($filename) ) {
        unlink($filename);
    }
    else {
        die("File $filename not found");
    }
    
}

# get a list of all the files, then sort them by age, newest first
$files = array();
if ($handle = opendir('./spool/')) {
    while (false !== ($entry = readdir($handle))) {
        if ( $entry !== '.'  && $entry !== '..' ) {
            $stat = stat('./spool/' . $entry);
            $files[$stat['size']] = array(
                'filename' => $entry, 
                'lastmod'  => $stat['mtime'],
                'size'     => $stat['size']
            );
            $total_count++;
        }
    }
    closedir($handle);
}
usort($files, "sortinator");


# get To, From and Subject from each of our $max_messages files
$messages = array();
$display_count = 0;
foreach ($files as $file) {
    if ( $display_count < $max_messages ) {
        $file['subject'] = null;
        $file['to']      = null;
        $file['from']    = null;
        $handle = @fopen('./spool/' . $file['filename'], "r");
        if ($handle) {
            while (($buffer = fgets($handle, 4096)) !== false) {
                foreach ( array('Subject', 'To', 'From') as $header ) {
                    $h_len = strlen($header);
                    if ( substr($buffer, 0, $h_len + 1) == $header . ':' ) {
                        $file[strtolower($header)] = substr($buffer, $h_len + 2);
                    }
                }
                if (
                       ! is_null($file['subject']) 
                    && ! is_null($file['to']) 
                    && ! is_null($file['from']) 
                    ) {
                    break; # quit looking after we match all three
                }
            }
            fclose($handle);            
        }
        else {
            die("Couldn't open ./spool/" . $file['filename']);
        }
        
        $messages[] = $file;
        
        $display_count++;
    }
    else {
        break;
    }
    
}

function sortinator($a,$b) {
    return $a['lastmod'] < $b['lastmod'];
}

?>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <title>local mail</title>
    <link rel="stylesheet" href="/bootstrap/css/bootstrap.min.css">
    <meta http-equiv="refresh" content="60">
</head>
<body>

<div class="container">

    <div class="alert alert-warning">
        Total Emails <span class="badge"><?= HtmlSpecialChars($total_count); ?></span>
    </div>


    <table class="table table-striped">
    
    <thead>
    <tr>
        <th>To</th>
        <th>From</th>
        <th>Date</th>
        <th>Size</th>
        <th>Subject</th>
        <th>Delete</th>
    </tr>
    </thead>
    
    <tbody>
<? FOREACH ($messages as $message): ?>
    <tr>
        <td>
            <a href="/spool/<?= HtmlSpecialChars($message['filename']); ?>">
            <?= HtmlSpecialChars($message['to']); ?>
            </a>
        </td>
        <td>
            <?= HtmlSpecialChars($message['from']); ?>
        </td>
        <td><?= date('n/j/y h:i', $message['lastmod']); ?> </td>
        <td><?= HtmlSpecialChars($message['size']); ?> bytes</td>
        <td>
            <?= HtmlSpecialChars($message['subject']); ?>
        </td>
        <td>
            <form method="post">
            <button type="submit" class="btn btn-primary trash-msg" 
                name="filename" value="<?= HtmlSpecialChars($message['filename']); ?>">
            <span class="glyphicon glyphicon-trash"></span></button>
            </form>
        </td>
    </tr>
<? ENDFOREACH; ?>
    <tbody>

    </table>
</div>


<script src="/jquery-1.11.2.min.js"></script>
<script src="/bootstrap/js/bootstrap.min.js"></script>

</body>
</html>

You’ll end up with something that looks like this:

4 thoughts on “Configuring a Dev Box Mail Server

  1. Thank you. This was exactly what I needed. I still have an issue though. If I send email from my terminal using “mail”, a file i generated and I can read the contents. However when email is sent from the application (php), the file that is created is not correctly displayed. The header information is correct, but the text seems to be encoded wrong
    You can see the header below, and as you can see the subject is also not easy to read. Any idea what this might be? Mails look fine when they are delivered in the ‘right’ way

    From mymail@mail.mail Wed Feb 8 21:09:00 2017
    Received: by xxxxxxxxxxxxxxx (Postfix, from userid 33)
    id 85C64322347; Wed, 8 Feb 2017 21:09:00 +0100 (CET)
    To: recipient@mail.mail
    Subject: =?UTF-8?B?dGVzdCBhZiBtYWls?=
    X-PHP-Originating-Script: 1009:Message.php
    MIME-Version: 1.0
    From: xxxxxxxxxxxxxxxxxxxx
    Reply-To: xxxxxxxxxxxxxxxxxxxxx
    Content-type: text/html; charset=utf-8
    Content-Transfer-Encoding: base64
    Message-Id:
    Date: Wed, 8 Feb 2017 21:09:00 +0100 (CET)

    Reply
    • Looks like your emails are coming out base64 encoded. And that is totally reasonable, but my little “webmail” app isn’t doing anything to display it. In fact, specifically, I don’t want it to render HTML or decode anything. It’s not really useful for, you know, reading emails. I made it for like, checking that emails are getting sent to the right places, tokens are getting substituted, etc.

      Decoding MIME emails gets REALLY complicated really quickly, and if that is a feature you want, I would suggest sending all the messages to a mailbox of some sort and installing a real webmail app. You could still use postfix’s default_transport to swallow everything, but I guess you’ll need to run an imap server.

      Reply
      • Thanks. I installed mutt on my linux server, and it is able to properly decode the troublesome emails that standard ‘mail’ could not. So I am good 🙂

        Reply

Leave a reply

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong> 

required