PHP Advent Calendar Day 1

01 Dec 2007

Welcome to the PHP Advent Calendar. If you are unfamiliar with the format of an Advent calendar, Wikipedia has a pretty good description. The PHP Advent Calendar is similar in spirit to the Perl Advent Calendar, a tradition the Perl community has sustained for several years.

Each day, starting today and ending on Christmas Day, a member of the PHP community will be sharing a PHP-related tip or trick. Today's entry is provided by Sean Coates.

Sean Coates

Sean Coates
Sean Coates a PHP developer who works primarily on keeping things together over at php|a by developing their software and organizing their conferences. He was formerly the Editor-in-Chief of php|architect Magazine, is the co-host of the Pro::PHP Podcast and contributes to the PHP documentation team.
Montréal, Canada

If you've ever developed a script that sends batch email to customers, you know that dreaded feeling of "what have I done?!" that hits seconds after you've launched the script, and the precise moment you remember that you forgot to turn the debug flag on, and hundreds of customers have been mailed your unfinished test template. It's an amateur mistake, but it's an easy one to make. With a little bit of clever configuration, you can mitigate the risk of stray email going to real customers from your development/staging environment.

When it comes to mail() (as well as many other things), PHP (on non-Windows, and by default) prefers to delegate the heavy lifting to another piece of software: sendmail (or a sendmail compatible command-line mail transport agent). By default, PHP will call your sendmail binary, and pass it the entire message, after composing it from the headers and body supplied by the developer.

One of the side-benefits to this system is the ability to override PHP's default, and seamlessly hook in your own sendmail-workalike binary or script. By setting the sendmail_path directive in php.ini, you can easily override the actual sending of email, and instead log it for easy review.

Here's an example from one of my development environments:

  1. $ cat /path/to/php/ini | grep sendmail_path
  2. sendmail_path=/usr/local/bin/logmail
  3. $ cat /usr/local/bin/logmail
  4. cat >> /tmp/logmail.log

This little bit of config code is extremely useful in a non-production environment. In the scenario above, you don't have to worry about flipping any flags or accidentally reading the "real" customer database when you meant to read the "fake" repository that contains only your own email address. Disaster avoided.

Left alone, that log file will get pretty big over time, quickly becoming unmanageable. With a little additional hackery, and with the help of the common formail app, an alternative might look like this:

  1. $ cat /path/to/php/ini | grep sendmail_path
  2. sendmail_path=/usr/local/bin/trapmail
  3. $ cat /usr/local/bin/trapmail
  4. formail -R cc X-original-cc \
  5.   -R to X-original-to \
  6.   -R bcc X-original-bcc \
  7.   -f -A"To:" \
  8. | /usr/sbin/sendmail -t -i

This script traps all mail that would normally go out (say, to a customer), and instead, delivers it to (with the original fields renamed for debugging purposes).

Sure, you could override this in your own framework (if you have a centralized mail object/function, for example), but the true beauty of this method is that it works for all calls to the mail() function, even those in third-party libraries. You do, however, still need to watch out for direct SMTP calls.