mail() Function in PHP and Bounced E-Mails
Everyone’s favorite way of sending spam is the mail() function in PHP. The question of the day is what happens to the bounced e-mails that may result from this operation.
No, they’re not necessarily sent back to the “From” address of the original message. Mail servers return undeliverable messages to the envelope sender, which may or may not match the “From” field. There used to be a special header, Errors-To, but it was deprecated and as of today that is often ignored.
The envelope sender is set by the SMTP client when it connects to the server, so you might expect PHP’s mail() to handle it intelligently. First, let’s see how the function really works, starting with php.ini:
[mail function]
; For Win32 only.
SMTP = localhost
smtp_port = 25
; For Win32 only.
sendmail_from = me@example.com
; For Unix only. You may supply arguments as well (default: "sendmail -t -i").
sendmail_path = /usr/sbin/sendmail -t -i
On Windows, mail() appears to handle everything by itself. On Linux, the work is delegated to an external program: sendmail. The sendmail man page (the Postfix version) explains the two flags that are used here:
-i When reading a message from standard input, don’t treat a line containing only a
.character as the end of input.-t Extract recipients from the message headers. These are added to any recipients given on the command line.
So the mail() function actually prepares the message and pipes it to sendmail. How about the envelope sender, this elusive piece of data we were actually looking for? The man page above has the answer as well:
-f sender Set the envelope sender address. This is the address where delivery problems are sent. With Postfix versions before 2.1, the
Errors-To:message header overrides the error return address.
Since no -f parameter is being passed here, bounced e-mails end up being sent to one of the following:
- For older mail systems, the address in the
Errors-To:header (if you supplied one in the 4th parameter ofmail()). - The address in the
Return-Path:header (if you supplied one in the 4th parameter ofmail()). - The address automatically derived from the local user who ran the sendmail command (e.g.
apache,nobodyor a local user name). Postfix records all this in the automatically addedX-Postfix-Sender:header.
To avoid the third case, we need to to work our way around this. First, note that php.ini lets you replace the mail-sending program with your own script, which can do some additional processing before calling into sendmail:
sendmail_path = /usr/local/sbin/sendmail_wrapper
The script needs the right permissions to run:
rwxr-xr-x 1 root root 814 Oct 4 15:45 sendmail_wrapper
You could write the wrapper in Python or Ruby, but PHP works nicely too, as long as the script itself doesn’t call mail():
#!/usr/bin/php -q
<?php
set_time_limit(0);
$mtext = "";
$from = "";
$has_return_path = false;
// Feel free to replace the regex with a better e-mail match
$regex = "/[a-z0-9]+([_\\.-][a-z0-9]+)*@([a-z0-9]+([\.-][a-z0-9]+)*)+\\.[a-z]{2,}/i";
$stdin = fopen('php://stdin', 'r');
while (!feof($stdin))
{
$cline = fgets($stdin);
if (!((strpos($cline, "Return-Path:") === false))) {
if (preg_match_all($regex, $cline, $matches, PREG_PATTERN_ORDER)) {
$has_return_path = true;
}
}
if (!((strpos($cline, "From:") === false))) {
if (preg_match_all($regex, $cline, $matches, PREG_PATTERN_ORDER)) {
$from = $matches[0][0];
}
}
$mtext .= $cline;
}
fclose($stdin);
if (!empty($from) && !$has_return_path) {
$from_opt = ' -f' . $from;
}
$fd = popen('/usr/sbin/sendmail -t -i' . $from_opt, 'w');
fwrite($fd, $mtext);
pclose($fd);
This is all that it is. Hope you found it useful!