User

You are logged in as Anonymous.

Want to log out?

My friend Paul has a cool service called Wonderproxy that lets you test and develop GeoIP-based apps without the normal headaches. If you need to simulate remote, international traffic, you should check it out.

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.


6 Responses to mail() replacement -- a better hack

  1. 137 News and Information 2005-08-03 06:55

    Months ago I was looking for a solution to log outgoing mails instead of sending them. This week I found a solution mentioned in Sean's PHP-Related Weblog.

    For me only this part was interesting:

    PHP:
    <?php

    $stdin = fopen('php://stdin',

  2. 138 gagan 2005-08-16 07:19

    no comments

  3. 140 Chad 2006-01-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.

  4. 141 Chad 2006-01-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!

  5. 139 M 2006-06-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

  6. 22856 walter 2010-03-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

Leave a Reply



Clicky Web Analytics