How to hook PHP mail() function

There are times when you may need to hook some PHP function for one reason or another. PHP makes it an easy task to accomplish using override_function which overrides built-in functions. This is useful only in the context of your own code or the code you can easily change in order to work with the aforementioned function. So, this solution might not be always at your disposal.

Specifically, let’s take a deeper look into the PHP mail function which is frequently used and misused in various scripts.

It is possible to globally hook PHP mail function for different purposes, like for logging or filtering. For logging, the best course of action would be to activate logging option in the PHP configuration file (usually php.ini). Just uncomment the mail.log line and add the path to the log file like this:

mail.log = /var/log/php_mail.log

Log entries include the full path of the script, line number, To address and headers. So for debugging purpose, we will have enough information to address potential issues.

On the other side, for mail filtering mail.log won’t be very useful. It doesn’t answer the question from the title how to hook mail function. Hooking by definition usually gives the option to interfere with the data if we need to or to completely ‘choke’ the request in case some condition is met. This is what we need for proper filtering, especially when it’s not easy to wrap mail function with our own classes. Moreover, it gives system administrators the power to protect the system from malicious code.

In this tutorial, we will focus only on the hooking of the PHP mail function. Keep in mind that this function is not the only method for sending an email via PHP, so don’t rely on this as the final solution for monitoring your PHP scripts email activity. PHP scripts could access SMTP service directly and in that case we will need to deploy different solutions for monitoring and filtering. But hooking mail function is one of the useful tools in the arsenal.

Let’s look at how to do it. It’s quite simple.

Since we’re hooking PHP mail function we are going to make a little utility script in PHP. We could do the same in the bash scripting language but when you get the idea of how to do it implementation will become irrelevant.

The basic script:

#!/usr/bin/php
<?php

$sendmail_path = '/usr/sbin/sendmail';
$handle = fopen('php://stdin', 'r');
$mail = '';

while ($mail_line = fgets($handle)) {
    $mail .= $mail_line;
}

$cmd = 'echo ' . escapeshellarg($mail) . ' | ' . $sendmail_path . ' -t -i';
return shell_exec($cmd);

?>

Save the script in some directory, for example: /usr/local/bin/ with a name ‘sendmail_wrapper’. At this point, you probably already sense the rest of the solution. Anyway, let’s provide a short explanation since there’s one more step you need to do to make the script work. Go open php.ini, PHP configuration, and find the line ‘sendmail_path’. Uncomment that line (if it’s commented) and write the name of your wrapper script including the full path. Reload PHP service.

While editing php.ini, notice comments on the sendmail command parameters which sometimes differ on different Linux systems and edit the script accordingly.

The script at its own provides a simple mail relay between PHP mail function and sendmail service, (which is usually another relay to the real email service, like Postfix or Exim – nowadays nobody uses Sendmail any more), which can be used for logging mail function activity. We can also decide not to relay email if some condition is met. For example, we can filter out an email if we found it is going to a specific email address:

....

while ($mail_line = fgets($handle)) { 
    if (preg_match('/^to: some@email.com/i', $mail_line)) {
        return TRUE;
    }
    $mail .= $mail_line; 
} 
....

We can also modify the script to send alerts, make frequency caps, log some parts of an email when some condition occurs, remove ‘X-Php-Originating-Script’ header and many other useful things.

Leave a Reply

Your email address will not be published. Required fields are marked *