Skip to main content

mail() replacement -- a better hack

This morning, I read Davey's post about how to compile PHP in a way that allows you ro specify your own mail() function. This is kind of a cool hack, but I've been using a different approach for a while, now, that allows much better control. Read on if you're interested.

Davey's hack, if you didn't read his post, yet, centers around defining your OWN mail function, after you have instructed PHP not to build the default one.

My hack doesn't require editing of the PHP source, or even a recompile. It doesn't require an auto-prepend, either, but it does require a small change to php.ini.

So, where's the magic? It lies in the sendmail_path directive.

When it comes to mail() (as well as many other things), PHP 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 sendmailesque binary or script.

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

sendmail_path=/usr/local/bin/logmail sean@sarcosm:~$ cat /usr/local/bin/logmail cat >> /tmp/logmail.log

This little bit of config & code is extremely useful in a non-production environment. How many of us have accidentally sent emails to actual customers from the development server? This little bit of trickery avoids this, and instead of sending the email (as PHP normally would), mail is instead logged to the /tmp/logmail.log file. Disaster avoided.

But, that file gets pretty big over time... it becomes unmanageable very quickly. So, in a different environment, I have an alternative:

sendmail_path=/usr/local/bin/trapmail sean@sarcosm:~$ cat /usr/local/bin/trapmail formail -R cc X-original-cc \ -R to X-original-to \ -R bcc X-original-bcc \ -f -A"To: devteam@example.com" \ | /usr/sbin/sendmail -t -i

And what does this do? It traps all mail that would normall go OUT (say, to a customer), and instead, delivers it to devteam@example.com (with the original fields renamed for debugging purposes).

So, how does all of this solve Davey's problem?

This is something I whipped up after work, today, so it's pretty new code that likely has a few bugs lurking in it, but it's a good start:sendmail_path=/usr/local/bin/mail_proxy.php

<?php //---CONFIG $config = array( 'host' => 'localhost', 'port' => 25, 'auth' => FALSE, ); $logDir = '/www/logs/mail'; $logFile = 'mail_proxy.log'; $failPrefix = 'fail_'; $EOL = "\n"; // change to \r\n if you send broken mail $defaultFrom = '"example.net Webserver" <www@example.net>'; //---END CONFIG if (!$log = fopen("{$logDir}/{$logFile}", 'a')) { die("ERROR: cannot open log file!\n"); } require('Mail.php'); // PEAR::Mail if (PEAR::isError($Mailer = Mail::factory('SMTP', $config))) { fwrite($log, ts() . "Failed to create PEAR::Mail object\n"); fclose($log); die(); } // get headers/body $stdin = fopen('php://stdin', 'r'); $in = ''; while (!feof($stdin)) { $in .= fread($stdin, 1024); // read 1kB at a time } list ($headers, $body) = explode("$EOL$EOL", $in, 2); $recipients = array(); $headers = explode($EOL, $headers); $mailHdrs = array(); $lastHdr = false; $recipFields = array('to','cc','bcc'); foreach ($headers AS $h) { if (!preg_match('/^[a-z]/i', $h)) { if ($lastHdr) { $lastHdr .= "\n$h"; } // skip this line, doesn't start with a letter continue; } list($field, $val) = explode(': ', $h, 2); if (isset($mailHdrs[$field])) { $mailHdrs[$field] = (array) $mailHdrs[$field]; $mailHdrs[$field][] = $val; } else { $mailHdrs[$field] = $val; } if (in_array(strtolower($field), $recipFields)) { if (preg_match_all('/[^ ;,]+@[^ ;,]+/', $val, $m)) { $recipients = array_merge($recipients, $m[0]);; } } } if (!isset($mailHdrs['From'])) { $mailHdrs['From'] = $defaultFrom; } $recipients = array_unique($recipients); // remove dupes // send if (PEAR::isError($send = $Mailer->send($recipients, $mailHdrs, $body))) { $fn = uniqid($failPrefix); file_put_contents("{$logDir}/{$fn}", $in); fwrite($log, ts() ."Error sending mail: $fn (". $send->getMessage() .")\n"); $ret = 1; // fail } else { fwrite($log, ts() ."Mail sent ". count($recipients) ." recipients.\n"); $ret = 0; // success } fclose($log); return $ret; ////////////////////////////// function ts() { return '['. date('y.m.d H:i:s') .'] '; } ?>

Voila. SMTP mail from a unix box that may or may not have a MTA (like sendmail) installed.

Don't forget to change the CONFIG block.