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 comments on “Configuring a Dev Box Mail Server”
This a a friendly example of how to help people. Thank you for the valuable information!
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)
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.
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 🙂