mail() Hacks

This article was first published in the “Tips & Tricks” column in php|architect magazine.

How do you send e-mail on a server in which there is no mail server installed? How do you redirect e-mail messages in a testing environment so they don’t go to your users? This edition of Tips & Tricks addresses these two questions, highlighting some useful tricks to redefine or redirect mail().

PHP provides an awesome built-in feature with the mail() function. I refer to it as “awesome” because I originally came to this language from the background of ASP and VBScript, and to successfully send an e-mail message from an ASP script, one had to purchase a third-party COM object and successfully install and register the object on a Windows server. PHP has mail capabilities built right into the language, providing developers with a powerful and easy way to send e-mail.

Sometimes, however, whether for purposes of security (in which the server doesn’t have access to a local mail server) or debugging (mail should be trapped and not sent to users), it becomes necessary to redefine the mail() function, or redirect it. In this edition of Tips & Tricks, we’ll explore how to do both.

Redefining mail()

There might be times in which server administrators do not wish to provide access to mail functionality. For example, they are unwilling to install sendmail, postfix, or any other mail servers. There are valid security reasons for disallowing mail servers, such as the fear of a Web server being used as a spam relay, but this lack of functionality can put a damper on Web application features. Furthermore, while applications can be written in such a way as to get around this limitation (e.g. using sockets and SMTP), there are many third-party applications and tools that rely on PHP’s mail() function, and it is far too time consuming to rework these applications to use your own mail function. Thus, for full compatibility, it becomes necessary to hack away at PHP’s mail() command and create your own, but, as difficult as this sounds, it’s actually quite simple to do.

To completely redefine the mail() function, it is necessary to recompile PHP without support for the function. Afterwards, we’ll create a new mail() function using PHP, and your applications will be none the wiser.

First, to compile PHP without mail(), run the configure command as normal, including all desired parameters. Then, before running make, edit main/php_config.h. Find the line that reads:

#define HAVE_SENDMAIL 1

Comment out this line, so that it now reads:

/* #define HAVE_SENDMAIL 1 */

Now, run make and make install as usual.

This will essentially disable the mail() function, and it will no longer be available to your scripts. So, our next step is to create a mail() function at the application level.

Listing 1 shows one such example mail() function using the PEAR::Mail package. This function implements the same exact parameters as the native PHP mail() function to ensure compatibility with any applications that require the use of mail(); it does not use the $additional_parameters parameter since that is primarily used to pass additional arguments to the sendmail (or other mailer) binary. This new function should also behave in exactly the same way as the native function and all parameters passed to it should follow the rules for mail() as defined in the PHP manual.

NOTE: Using this method, you cannot simply create a new mail() function using the PEAR::Mail “mail” driver, as this driver also utilizes PHP’s built-in mail() function to send mail. Thus, redefining the mail() function to use the PEAR::Mail “smtp” driver should also work for any applications that use PEAR::Mail with the mail driver. Using PEAR::Mail with the sendmail driver will not work if sendmail is not available on the system.

Now that we have defined a new mail() function, we need to make it accessible to the applications that require it. The quickest and easiest way to do this is to use the auto_prepend_file setting in php.ini:

auto_prepend_file = /path/to/new_mail.php

You may also set this in your Apache httpd.conf or .htaccess file:

php_value auto_prepend_file /path/to/new_mail.php

Now, we have a mail() function that will behave similarly to the built-in function, and all PHP applications on the system have access to use it. Keep in mind that other PHP mailing libraries could be used; you are not limited to PEAR::Mail.

Listing 1.
<?php
/* Use PEAR::Mail */
require_once 'Mail.php';
function mail($to, $subject, $message, $additional_headers = NULL, $additional_parameters = NULL)
{
/* Default From address */
$from = 'foo@example.org';
/* Set SMTP parameters */
$smtp_params = array(
'host' => 'mail.example.org',
'port' => 25,
'auth' => TRUE,
'username' => 'smtp_username',
'password' => 'smtp_password',
'persist' => FALSE
);
/* Parse headers */
$headers = array();
if (!is_null($additional_headers) &&
is_string($additional_headers))
{
$tmp_headers = explode("\r\n", $additional_headers);
foreach ($tmp_headers as $header)
{
list($h, $v) = explode(':', $header);
$headers[$h] = trim($v);
}
}
/* Set default headers, if not present */
$headers['Subject'] = $subject;
if (!isset($headers['To'])) $headers['To'] = $to;
if (!isset($headers['Date'])) $headers['Date'] = date('r');
if (!isset($headers['From'])) $headers['From'] = $from;
/* Send the mail message */
$mail_object =& Mail::factory('smtp', $smtp_params);
$e = $mail_object->send($to, $headers, $message);
if (PEAR::isError($e))
{
exit($e->getMessage());
}
}
?>

Redirecting mail()

At times it is preferable to turn off mail functionality altogether without recompiling PHP. This includes applications that are running on testing servers and need to use mail() for debugging purposes, but should not send any actual mail messages—or should send messages but only to the developers. In cases such as these, it is possible to redirect mail messages sent through mail() by modifying the php.ini sendmail_path value.

Modifying sendmail_path is a simple task. The complexities lie in the script to which all mail is redirected. This script may be as simple as directing all mail to a log file or redirecting it to the project developers, or it may be as complex as implementing a full-scale mail solution using a PHP command-line interface (CLI) script to both send mail, as illustrated in Listing 1, and log everything. We’ll examine all of these options.

If the goal is to temporarily turn off mail and redirect it to a log file, simply create a script named logmail, set the permissions level to 755 (chmod 755 logmail), and put the following line in the script:

cat >> /tmp/logmail.log

Then, set sendmail_path in php.ini to /path/to/logmail. Don’t forget to restart your web server. Now, all e-mail sent by applications will be stored in /tmp/logmail.log rather than reaching the recipient in the To header.

NOTE: The sendmail_path directive may be set only in php.ini or Apache’s httpd.conf. It cannot be set from an .htaccess file.

There may be times, however, when properly testing an application means that all e-mail messages generated by the application must be sent somewhere, but they shouldn’t go to any real users. Thus, we need to trap the mail, which is another fairly simple task.

Create a script named trapmail, set the permissions level, again, to 755, and place the following in the script (replacing developer@example.org with your choice of e- mail address, of course):

formail -R cc X-original-cc \
-R to X-original-to \
-R bcc X-original-bcc \
-f -A”To: developer@example.org” \
| /usr/sbin/sendmail -t –i

Then, as with earlier, set the sendmail_path directive to /path/to/trapmail. This will successfully redirect all e-mail messages sent by the application to developer@example.org, and the original To, Cc, and Bcc headers will be rewritten to X-original-to, X-original-cc, and X-original-bcc respectively.

To do this, sendmail must be available on the system, yet it is not required, since it is possible to create a PHP CLI script to combine this sort of redirecting with code from the custom mail() script of Listing 1 to redirect and log any messages sent by a PHP application. Listing 2 gives a glimpse into how this is possible.

Listing 2.
#!/usr/local/bin/php
<?php
$stdin = fopen('php://stdin', 'r');
$input = '';
while (!feof($stdin))
{
$input .= fread($stdin, 1024); // read 1kb at a time
}
list($headers, $body) = explode("\n\n", $input, 2);
// Now, use a mailer package and implement some logging
// using $headers and $body
?>

I would save the code in Listing 2 to a file such as /usr/local/bin/php_mailer and set its permissions to 755. Then, I would implement some form of logging, perhaps using PHP 5’s file_put_contents(), along with a mailer package to send mail to either the intended recipient (on a production server) or the developers (on a testing server).

Also, notice that the mail is received on standard input in Listing 2. The message is being received in exactly the same format that sendmail would receive it. Thus, this script must parse the received message, extract the headers and body (we have already done this), and send them to PEAR::Mail in the format it expects.

Finally, the sendmail_path directive must be set to /usr/local/bin/php_mailer to make use of it.

These suggestions are simple, yet effective, ways to either send e-mail when your server can’t support PHP’s native mail() function or you wish to redirect messages during development or testing. I hope you can see how these methods are versatile and can be extended to implement some rather complex mailing functionality.

I’d like to thank Sean Coates and Davey Shafik, who allowed me the use of content from their blogs to make this column possible.

Until next time, happy coding!