How To Log Emails Sent With PHP's mail() Function To Detect Form Spam

Version 1.0
Author: Till Brehm <t [dot] brehm [at] ispconfig [dot] com>
Last edited 07/10/2009

If you are running a webserver you might have faced the problem already: somewhere on your server is a vulnerable contact form or CMS system written in PHP that gets abused by spammers to send emails trough your server. If you have more than a few websites, it is a pain to detect which of the sites is vulnerable and sends the spam emails. This tutorial explains the installation of a small wrapper script which logs email messages sent trough the PHP mail() function.

I'm using Debian Linux here for this tutorial but the script should work on any Linux distribution.

 

1 Installing the wrapper script

Open a new file /usr/local/bin/phpsendmail...

vi /usr/local/bin/phpsendmail

... and insert the following script code:

#!/usr/bin/php
<?php

/**
  This script is a sendmail wrapper for php to log calls of the php mail() function.
  Author: Till Brehm, www.ispconfig.org
  (Hopefully) secured by David Goodwin <david @ _palepurple_.co.uk>
*/

$sendmail_bin = '/usr/sbin/sendmail';
$logfile = '/tmp/mail_php.log';

//* Get the email content
$logline = '';
$pointer = fopen('php://stdin', 'r');

while ($line = fgets($pointer)) {
        if(preg_match('/^to:/i', $line) || preg_match('/^from:/i', $line)) {
                $logline .= trim($line).' ';
        }
        $mail .= $line;
}

//* compose the sendmail command
$command = 'echo ' . escapeshellarg($mail) . ' | '.$sendmail_bin.' -t -i';
for ($i = 1; $i < $_SERVER['argc']; $i++) {
        $command .= escapeshellarg($_SERVER['argv'][$i]).' ';
}



//* Write the log
file_put_contents($logfile, date('Y-m-d H:i:s') . ' ' . $_ENV['PWD'] . ' ' . $logline, FILE_APPEND);
//* Execute the command
return shell_exec($command);
?>

If you use a different Linux distribution than Debian, the sendmail binary might be in a different location than /usr/sbin/sendmail and you have to change the sendmail path in the line $sendmail_bin = '/usr/sbin/sendmail'; of the script.

Now make the script executable...

chmod +x /usr/local/bin/phpsendmail

... and create the logfile and make it writable:

touch /var/log/mail.form
chmod 777 /var/log/mail.form

 

2 Modifying the php.ini

Now we reconfigure PHP so that it uses our wrapper script to send the emails.

Open the php.ini file...

vi /etc/php5/apache2/php.ini

... and change the lines...

[mail function]
; For Win32 only.
SMTP = localhost
smtp_port = 25

; For Win32 only.
;sendmail_from = [email protected]

; For Unix only.  You may supply arguments as well (default: "sendmail -t -i").
;sendmail_path =

... to:

[mail function]
; For Win32 only.
;SMTP = localhost
;smtp_port = 25

; For Win32 only.
;sendmail_from = [email protected]

; For Unix only.  You may supply arguments as well (default: "sendmail -t -i").
sendmail_path = /usr/local/bin/phpsendmail

If you use php as cgi, with suphp or as fcgi, then change the same lines in the file /etc/php5/cgi/php.ini, too.

Restart the Apache webserver to apply the changes.

/etc/init.d/apache2 restart

 

3 Test the setup

To test this setup, create a new php file with the name mailtest.php in one of your websites with the content:

<?php
mail('[email protected]','This is a test message subject','This is a test message body');
echo 'Mail sent.'; 
?>

Then open the file in a webbrowser to execute it. The test message should be logged now into the logfile. Check this with the command:

cat /var/log/mail.form

Share this page:

43 Comment(s)

Add comment

Comments

From: at: 2009-08-07 10:04:14

Thank you for the hint! I modified the tutorial to correct the problems.

From: George at: 2009-08-11 09:27:46

The suggestion in this article is a malicious user's dream! The code contains several deficiencies which will readily grant a user shell access to the server!

 

From: jm at: 2009-08-15 10:10:51

Please submit corrections instead of just cry "do not use this"

From: Anonymous at: 2009-08-05 19:44:41

Thanks ;)

From: Anonymous at: 2009-09-19 11:14:50

I can't find out the logs being generated in the file mail.form file .. But the mail reaches the mail id specified in the file.

From: hakanpolatkan at: 2009-08-05 21:35:58

Hi Till,

First, thank you very much for sharing your howto document and it definetely seems that it'll make my work easier.

But i think it might need to be made some little corrections about the phpsendmail file. The code doesnt run unless the correction below is made.

Line 36: return shell_exec($cmd); -> return shell_exec($command);

 And;

I get the message below when the system runs the script.

sendmail: fatal: Recipient addresses must be specified on the command line or via the -t option

Line 20: $command = 'echo "'.$mail.'" | '.$sendmail_bin.' ';

But it runs when i change the line above as 

$command = 'echo "'.$mail.'" | '.$sendmail_bin.' -t';

 

 

best regards

From: George at: 2009-08-14 08:46:34

Yes, this is a gaping security hole.

Read up on command line injection to understand why.

From: David F. Skoll at: 2009-08-11 20:49:36

This script is one huge security hole.  Do not use it!  It invokes shell_exec() and system() on untrusted input without sanitization.

 

From: at: 2009-08-13 00:12:54

is this really a security hole?

 anyways, I'm not getting anything logged in the logs. am I missing something?

From: Jm at: 2009-08-15 10:09:07

Instead of criticize, you should better submit a patch...

 

From: Jason Clifford at: 2009-08-22 17:25:17

Instead of trying to bolt the barn door after the horse has bolted it is far better to patch PHP to prevent anyone using it to inject implicit recipients (http://www.lancs.ac.uk/~steveb/patches/php-mail-reject-implicit-recipients-patch/)

 You can also patch mail() to specify in the email which website actually sent the message to make it easier to trace who is actually responsible for any spam -  http://www.lancs.ac.uk/~steveb/patches/php-mail-header-patch/

 I've used both these patches and they really help a lot.

From: at: 2009-12-14 16:14:01

Thank you for sharing your script!

From: Brooks at: 2009-11-21 01:05:41

The log, if you copied the script exactly, will be in /tmp/mail_php.log, also assuming that you set this to 777.  If you have a test mail script and it is not writing to the log, you will want to make sure the script is hitting your mail server as well, to ensure it is running through the entire script and not erroring out, a simple way to do this is to grep the email that you are sending to out of your mail.log, usually in /var/log/maillog, /var/log/mail.log, or if you have plesk, /usr/local/psa/var/log/maillog

 so in the case of plesk:

 tail /usr/local/psa/var/log/maillog -f | grep [email protected]

 Enter that, then submit the form, if you don't see the shell window start to show results, the mail is not going through, and you may need to start choppping the code a bit.

From: at: 2010-11-18 10:03:50

I changed the logfile location from temp directory to log directory in the phpsendmail file.

//$logfile = '/tmp/mail_php.log';
$logfile = '/var/log/mail.form';

I got the log correctly in the mail.form file, but all the records are on the same line. How to add a line break at the end of the line? Logging the mail sent through PHP seems to be really a good idea.

From: at: 2010-11-23 13:14:49

Change the line:

 $logline .= trim($line).' ';

 to:

 $logline .= trim($line)." \n";

From: Brooks at: 2009-11-21 01:16:50

Hello,

    Excellent script, I've had to modify this for a couple of my older servers that are still running PHP4 (Plesk does not really enjoy when you update...anything).  And, I could think of nothing better to do than to share this.

 

Touch /usr/local/bin/phpsendmail

Touch /var/log/php_mail_log

chmod +x /usr/local/bin/phpsendmail

chmod 777 /var/log/php_mail_log

 

The content of /usr/local/bin/phpsendmail:

 

#!/usr/bin/php
<?php
$sendmail_bin='/usr/sbin/sendmailauth';
$logfile='/var/log/php_mail_log';

//* Get the email content
$logline = '';
$pointer = fopen('php://stdin', 'r');

//Open the log file
$handle = fopen($logfile,"a+");

//Prepare the date
$date = date('m-d-y :: h:i:s A --');

// get email content functionality
while ($line = fgets($pointer)) {
        if(preg_match('/^to:/i', $line) || preg_match('/^from:/i', $line)) {
                $logline .= trim($line).' ';
        }
        $mail .= $line;
}

//* compose the sendmail command
$command = 'echo ' . escapeshellarg($mail) . ' | '.$sendmail_bin.' -t -i';
for ($i = 1; $i < $_SERVER['argc']; $i++) {
        $command .= escapeshellarg($_SERVER['argv'][$i]).' ';
}

$content = "$date " . $_ENV["PWD"]  . " $logline " . "\n";
fwrite($handle,$content);
fclose($handle);


//* Execute the command
return shell_exec($command);

?>
 

   Please note the sendmail bin, yours will probably just be /usr/sbin/sendmail and not /usr/sbin/sendmailauth, this is a customized sendmail script for SMTP Authentication.   The reason for this  entry would be PHP4 does not have the ability to use file_put_contents, there are ways to add it to the code, but add a lot more lines of code when fopen and fwrite can do the same thing.  I have also varied the date command a little bit for ease of reading, and added a line break so that everything does not string into one long line, once again for ease of reading. Below is a sample of the output:

 09-11-20 :: 07:01:50 -- /var/www/vhosts/domain/ To: address From: address

   Thank you very much for the original code as it makes my job much easier, and I hope this contribution is well accepted.

    Brooks

     Nexpoint Web Hosting

     http://www.nexpoint.net

From: Anonymous at: 2009-11-01 19:45:50

thank you very much, it worked perfectly, very useful article.

From: at: 2009-09-29 08:24:01

Thanks for the modified script. I tested it and updated it in the tutorial.

From: David Goodwin at: 2009-09-17 11:57:10

As mentioned above; the code is quite insecure.

I've hopefully fixed the problems with it, and include the code below.

thanks

 David.

 <code>

#!/usr/bin/php
<?php

/**
  This script is a sendmail wrapper for php to log calls of the php mail() function.
  Author: Till Brehm, www.ispconfig.org
  (Hopefully) secured by David Goodwin <david @ _palepurple_.co.uk>
*/

$sendmail_bin = '/usr/sbin/sendmail';
$logfile = '/tmp/mail_php.log';

//* Get the email content
$logline = '';
$pointer = fopen('php://stdin', 'r');

while ($line = fgets($pointer)) {
        if(preg_match('/^to:/i', $line) || preg_match('/^from:/i', $line)) {
                $logline .= trim($line).' ';
        }
        $mail .= $line;
}

//* compose the sendmail command
$command = 'echo ' . escapeshellarg($mail) . ' | '.$sendmail_bin.' -t -i';
for ($i = 1; $i < $_SERVER['argc']; $i++) {
        $command .= escapeshellarg($_SERVER['argv'][$i]).' ';
}



//* Write the log
file_put_contents($logfile, date('Y-m-d H:i:s') . ' ' . $_ENV['PWD'] . ' ' . $logline, FILE_APPEND);
//* Execute the command
return shell_exec($command);

</code>

(I've not yet been able to fully test this, so your mileage may vary....)

From: roben at: 2010-01-21 16:04:37

Thanks for this, this is just what I needed!

 

If you also want to log some of the php environment variables from the request you need to put something like

in /etc/php5/set_php_headers.php. You can choose whatever variables you want. I added the "__" to them to prevent overriding. Now modify php.ini and set: auto_prepend_file = "/etc/php5/set_php_headers.php" This executes the above file for each request. You can use these variables in the /usr/local/bin/phpsendmail script for logging.

From: roben at: 2010-01-22 20:55:23

Grrr... Something broke my reply.

What you have to add is:

<?php
        putenv("__PATH_INFO=". $_SERVER["PATH_INFO"]);
        putenv("__SCRIPT_NAME=". $_SERVER["SCRIPT_NAME"]);
        putenv("__SCRIPT_FILENAME=". $_SERVER["SCRIPT_FILENAME"]);
        putenv("__REMOTE_ADDR=". $_SERVER["REMOTE_ADDR"]);
        putenv("__HTTP_HOST=". $_SERVER["HTTP_HOST"]);
?>

From: at: 2010-05-19 00:17:35

1. Login as root, then create script file: /usr/local/bin/logging_sendmail

#!/bin/sh
# Logging sendmail wrapper

SENDMAIL="/usr/sbin/sendmail"
LOGFILE="/var/log/sendmail.log"

DT=`date "+%Y-%m-%d %H:%M:%S"`
DTFN=`date "+%Y%m%d-%H%M%S"`
TMPFP=`tempfile --prefix=lsm_`

cat | tee "$TMPFP" | $SENDMAIL $*
RETVAL=$?

TO=`grep "To:" <"$TMPFP"`
rm -f "$TMPFP"

echo "$DT: $PWD sends e-mail $TO" >>$LOGFILE

exit $RETVAL

2. Make the script executable:

chmod +x /usr/local/bin/logging_sendmail

3. Create /var/log/sendmail.log:

touch /var/log/sendmail.log

chown root:adm  /var/log/sendmail.log

chmod 662  /var/log/sendmail.log

4. Add logrotate configuration as /etc/logrotate.d/logging_sendmail:

/var/log/sendmail.log {
    weekly
    rotate 4
    compress
    delaycompress
    missingok
    create 662 root adm
}

5. Configure the /usr/local/bin/logging_sendmail script instead of phpsendmail as the article describes above (php.ini modifications).

6. Restart Apache as the article describes

You can look at the logfile with the same command, but the filename passed has to be: /var/log/sendmail.log

It works perfectly on our server.

Good luck!

From: Debian Dude at: 2010-12-07 22:53:00

This shell solution doesn't work at least on Debian. Logging works but mail was not sent. And yes i copy/paste the script and send mail location is correct.

From: guldi at: 2011-08-31 15:54:02

yes it does. you have to adjust this line:

SENDMAIL="/usr/sbin/sendmail"

And add the -t:

SENDMAIL="/usr/sbin/sendmail -t"

this just worked great for me. yeah. :)

From: Sean Dempsey at: 2012-01-30 05:28:11

The shell option works great for me too (for some reason the PHP version did not).  The only problem I'm having is that TO is not capturing the to field. Could capitalization of "To" be causing the problem? Any ideas, anyone?

From: Bretticus at: 2013-04-17 16:52:29

Email did not send for me until I used the -t switch in the sendmail path also (I  am using CentOS release 5.9 with sendmail linked to exim.) Thanks for the awesome wrapper. We have so many old email forms on this particular server. What a relief to be able to find a troublespot (Wordpress comments currently!) and take quick measures.

From: Joao at: 2010-12-27 16:23:20

Hey, this solution is very good thanks.

But the >> is not appending :/ probably permissions problem. I have tried chmod 777 but nothing.

 I removed the rm command and append >> to the temp file and it works.
But how can i get the output to go to my logfile?

this is what i have:

#!/bin/sh
# Logging sendmail wrapper

SENDMAIL="/usr/sbin/sendmail -t"
LOGFILE="/var/log/sendmail.log"

DT=`date "+%Y-%m-%d %H:%M:%S"`
DTFN=`date "+%Y%m%d-%H%M%S"`
TMPFP=`mktemp`

cat | tee "$TMPFP" | $SENDMAIL $*
RETVAL=$?

TO=`grep "To:" < "$TMPFP"`
#rm -f "$TMPFP"        (this should be uncommented ofc)
echo "$DT: $PWD sends e-mail $TO" >> $TMPFP      (this should be $LOGFILE)
exit $RETVAL


And the ls on the log file

-rwxrwxrwx 1 root         apache              8 2010-12-27 16:08 sendmail.log


Thanks for the help

From: at: 2011-07-12 06:40:50

maketemp doesn't exist on Centos 5.  

Replace line

 TMPFP=`tempfile --prefix=lsm_`

 with

 TMPFP=`mktemp`

 

From: Mike at: 2010-07-19 22:00:47

My server has been attacked by some guys using fishing and mail() function.

Your tutorial allow me not to secure my server, but to warn victims.

 Sure your tutorial is strongly useful.

From: itransition at: 2011-12-06 20:27:51

Thanks. It helped me.

From: Jamen at: 2011-02-06 20:37:33

I have tried both versions of the script, php and shell only versions and both work fine.

But NEITHER show the actual file of the script completing the mail function.  They only show the PWD aka Working directory of the script! 

How do you make the darn log file show the actual php script file itself running the mail function!  Not just the working directory!  There are thousands of php files in the directories listed.

Thank you!

From: Unai Rodriguez at: 2011-02-12 17:49:26

Dear All,

We have had issues sending UTF-8 encoded emails when using this phpsendmail wrapper instead of the normal sendmail method. Basically the contents get all turned into unreadable characters.

 Has anyone experienced similar issues? Would there be a way of fixing this?

 Thank you so much for the excellent information.

With Best Wishes,

Unai Rodriguez

From: Anonymous at: 2012-06-22 15:07:08

php function escapeshellarg() removes internetional characters. Simply quote string- do not use this function

From: Anonymous at: 2013-04-29 11:58:00

Just add this on the beginning of the script:

 setlocale(LC_CTYPE"en_US.UTF-8");

From: Nick Lachey at: 2012-03-04 21:05:28

Fantastic post. Here’s a tool that lets you build all types of web forms with email alerts fast and without coding. Just point and click http://www.caspio.com/online-database/features/web-forms-online.aspx

From: Ian Dunn at: 2012-05-28 18:22:27

PHP 5.3 added two configuration directives to solve this problem.
 
mail.add_x_header - Adds an extra header to the e-mail showing which script made the call to mail()
mail.log - Sets up a log file for all mail() calls. Contains the originating script and To address

See http://php.net/manual/en/mail.configuration.php for more info.

From: at: 2013-05-22 07:11:45

You are the best, thanks so much for this information ...With this new feature / support then the wrapper is not necessary for PHP 5.3 and above ...

From: Justin G. at: 2013-09-12 01:44:30

THANK YOU.  This provides so much more information (including the full path to the calling script and even the line number of the call).  Only took a few seconds to hunt down the offender once I enabled this.

From: Aas at: 2014-02-12 10:16:37

Thank you. Your comment should precede the article itself!

From: Jae at: 2013-02-28 12:32:32

readable log :)

/* Write the log
file_put_contents($logfile, date('Y-m-d H:i:s') . ' ' . $_ENV['PWD'] . ' ' . $logline.PHP_EOL, FILE_APPEND);

From: crazeeEyez at: 2013-05-07 20:42:57

blah blah blah security hole blah blah blah.  This is very useful to troubleshoot when you know there is mail being sent through your sendmail but can't track down the origin.  If you only use it when you need it and revert back when you're done, this can be really helpful.

 Thank you for the very useful tool!

From: Anonymous at: 2014-11-14 22:30:48

THANK YOU!!!!!!

From: zayo at: 2015-04-06 16:32:49

how can a get script filename path for write it to logfile?

$_SERVER["SCRIPT_FILENAME"]  return me /usr/local/bin/phpsendmail

thanx