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

Want to support HowtoForge? Become a subscriber!
 
Submitted by till (Contact Author) (Forums) on Wed, 2009-08-05 17:41. :: Debian | Apache | Email | PHP | Security

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 = me@example.com

; 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 = me@example.com

; 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('yourname@yourdomain.com','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


Please do not use the comment function to ask for help! If you need help, please use our forum.
Comments will be published after administrator approval.
Submitted by crazeeEyez (not registered) on Tue, 2013-05-07 21:42.

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!

Submitted by Jae (not registered) on Thu, 2013-02-28 13:32.
readable log :)

/* Write the log
file_put_contents($logfile, date('Y-m-d H:i:s') . ' ' . $_ENV['PWD'] . ' ' . $logline.PHP_EOL, FILE_APPEND);
Submitted by Ian Dunn (not registered) on Mon, 2012-05-28 19:22.
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.
Submitted by Aas (not registered) on Wed, 2014-02-12 11:16.
Thank you. Your comment should precede the article itself!
Submitted by Justin G. (not registered) on Thu, 2013-09-12 02:44.
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.
Submitted by abdi (registered user) on Wed, 2013-05-22 08:11.
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 ...
Submitted by Nick Lachey (not registered) on Sun, 2012-03-04 22:05.
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
Submitted by Unai Rodriguez (not registered) on Sat, 2011-02-12 18:49.

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

Submitted by Anonymous (not registered) on Mon, 2013-04-29 12:58.

Just add this on the beginning of the script:

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

Submitted by Anonymous (not registered) on Fri, 2012-06-22 16:07.

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

Submitted by Jamen (not registered) on Sun, 2011-02-06 21:37.

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!

Submitted by Mike (not registered) on Mon, 2010-07-19 23:00.

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.

Submitted by itransition (not registered) on Tue, 2011-12-06 21:27.
Thanks. It helped me.
Submitted by letezo (registered user) on Wed, 2010-05-19 01:17.

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!

Submitted by peterpallesen (registered user) on Tue, 2011-07-12 07:40.

maketemp doesn't exist on Centos 5.  

Replace line

 TMPFP=`tempfile --prefix=lsm_`

 with

 TMPFP=`mktemp`

 

Submitted by Joao (not registered) on Mon, 2010-12-27 17:23.

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

Submitted by Debian Dude (not registered) on Tue, 2010-12-07 23:53.
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.
Submitted by guldi (not registered) on Wed, 2011-08-31 16:54.

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. :)

Submitted by Bretticus (not registered) on Wed, 2013-04-17 17:52.
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.
Submitted by Sean Dempsey (not registered) on Mon, 2012-01-30 06:28.
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?
Submitted by roben (not registered) on Thu, 2010-01-21 17:04.

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.

Submitted by roben (not registered) on Fri, 2010-01-22 21:55.

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"]);
?>

Submitted by Brooks (not registered) on Sat, 2009-11-21 02:16.

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

Submitted by till (registered user) on Mon, 2009-12-14 17:14.
Thank you for sharing your script!
Submitted by Anonymous (not registered) on Sun, 2009-11-01 20:45.
thank you very much, it worked perfectly, very useful article.
Submitted by David Goodwin (not registered) on Thu, 2009-09-17 12:57.

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....)

Submitted by till (registered user) on Tue, 2009-09-29 09:24.
Thanks for the modified script. I tested it and updated it in the tutorial.
Submitted by Jason Clifford (not registered) on Sat, 2009-08-22 18:25.

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.

Submitted by David F. Skoll (not registered) on Tue, 2009-08-11 21:49.

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

 

Submitted by Jm (not registered) on Sat, 2009-08-15 11:09.

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

 

Submitted by binaryrogue (registered user) on Thu, 2009-08-13 01:12.

is this really a security hole?

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

Submitted by George (not registered) on Fri, 2009-08-14 09:46.

Yes, this is a gaping security hole.

Read up on command line injection to understand why.

Submitted by George (not registered) on Tue, 2009-08-11 10:27.

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!

 

Submitted by jm (not registered) on Sat, 2009-08-15 11:10.

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

Submitted by till (registered user) on Fri, 2009-08-07 11:04.
Thank you for the hint! I modified the tutorial to correct the problems.
Submitted by hakanpolatkan (not registered) on Wed, 2009-08-05 22:35.

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

Submitted by Anonymous (not registered) on Wed, 2009-08-05 20:44.
Thanks ;)
Submitted by Anonymous (not registered) on Sat, 2009-09-19 12:14.
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.
Submitted by Brooks (not registered) on Sat, 2009-11-21 02:05.

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@address.com

 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.

Submitted by shantanuo (registered user) on Thu, 2010-11-18 11:03.
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.

Submitted by till (registered user) on Tue, 2010-11-23 14:14.

Change the line:

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

 to:

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