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.logThis 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 -iAnd 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.
gagan
2005 Aug 16 07:19
no comments
Chad
2006 Jan 12 17:34
Wow. Great solution. If only I could get it to work.
I changed the line in php.ini:
sendmail_path=/usr/local/bin/trapmail
And created the /usr/local/bin/trapmail file
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
But for some reason it still sends to the original to field. Is sendmail picking up the email before formail? Please let me know where I may be going wrong.
Chad
2006 Jan 17 16:46
Just wanted to update my last comment. It works perfectly. The script I was using to test was using sendmail, but not by using the php mail() function. When I changed the script to use mail() it worked. Thanks for the neat suggestion!
M
2006 Jun 12 09:49
Actually I would - too - love to have an overrideable or at least extendable mail()-function in php!
My Problem: I'd love to track scripts on my hosting-server an tag all mails with an extra header, containing vhost- & script-name in an encrypted manner!
This should happen, without changing users scripts ...
Beginning with a certain number of virtual hosts, it's just a matter of time that a form spammer finds a usable script, and it's quite a hassle to search log data of ~ 100MB / day for unusual events (although audit-logging from mod_security and common *nix-tools like grep help a little) ...
although, it wouldn't be nessecary to allow a replacement-function, since this would be - again - a potential risk to abuse ...
A first step would be to allow the configuration of mail-headers in PHP_INI_SYSTEM-style to reach this ;)
Michael
walter
2010 Mar 02 17:14
If you have installed reformail , then
reformail -R to: X-original-to: -R cc: X-original-cc: -R bcc: X-original-bcc: -f0 -A'To: to-devteam@example.com' | /usr/sbin/sendmail -t -i
Penetration Testing
2010 Apr 18 03:55
This is a useful hack. I would suggest the people don't use sendmail anyway as it has had a very bad history from a security perspective. Try Qmail or something else instead.