Sendmail-Wrapper for PHP

March 24th, 2014 by Philip Iezzi 5 min read
cover image

Years ago I wrote about the extensive sendmail wrapper with sender throttling, a pretty simple Perl script. It reliably provided throttling of the email volume per day by the sender’s original UID (user id). It also logged the pathes of scripts that sent emails directly via sendmail (e.g. via PHP’s mail() function). The main flaw in the original sendmail wrapper was security, though. As in Linux, every executable script must be readable by the user that calls it, the throttle table in MySQL was basically open and every customer could manipulate it. Every customer could raise his own throttling limit and circumvent it.

Today, I’m publishing my new sendmail-wrapper that is going to fix all the flaws of the previous version and add some nice extras. The new sendmail-wrapper is written entirely in PHP and does not require any external libraries. It is a complete rewrite and has pretty much nothing in common with the old Perl version.

Features

  • Lets you monitor any mail traffic from PHP scripts
  • Allows throttling (limiting) emails sent by PHP’s mail() function
  • Throttle by sent email and/or recipient count per day
  • Logs both to syslog and database with message metadata
  • Logs common mail headers like From, To, Cc, Bcc, Subject
  • Fixes Return-Path header on the fly for users who did not correctly set it
  • Highly secured setup, customers cannot access the logging/throttling database
  • Standalone PHP application without any external library dependencies
  • Built for shared webhosting environment where PHP runs as cgi/FastCGI/suPHP
  • No cronjobs required, sendmail-wrapper will reset counters automatically every day

The idea behind this secure setup

Logging is done only via syslog, no customer can read or modify syslog entries. The sendmail-wrapper is split into two components: sendmail-wrapper.php and sendmail-throttle.php. The wrapper component will only do some syslogging and analyze some headers. It will also fix the Return-Path header if applicable as most customers forget to correctly set it in PHP. sendmail-wrapper.php is readable and executable by every customer. This is a strong requirement as our customers all run their script under their own UID, PHP runs as cgi/FastCGI/suPHP. The throttle component is where it gets more interesting. The sendmail-throttle.php script can only be executed by user sendmailwrapper which is an equally unprivileged system user like every other customer user. The throttling script will NOT be world readable and it will read it’s configuration from an extra file config.private.ini which contains the database password. The correct permissions are set by our installation script.

Return-Path fixing

Most customers just forget to set a Return-Path when they use PHP’s built-in mail() function. Even if they try to set it in the $additional_headers variable, it will not get correctly set on most systems. The only way ist to set it via the $additional_parameters variable, using the -f or -r switches. 99% of our customers simply can’t manage this, seriously! This sendmail-wrapper is extracting the email address from the Return-Path header, and if it doesn’t exist, it will just take the sender’s address (from the From header) as return-path. This is actually one of the most important features of my sendmail-wrapper, even if it might get overlooked and you’re installing the wrapper because of it’s logging and throttling capabilities.

Installation

(for detailed instructions, please consult the README on Github)

Clone repository from GitHub:

$ cd /opt/
$ git clone https://github.com/onlime/sendmail-wrapper.git sendmail-wrapper

Set up system user for sendmail-wrapper:

$ adduser --system --home /no/home --no-create-home --uid 6000 --group --disabled-password --disabled-login sendmailwrapper
$ adduser sendmailwrapper customers

The installer script install.sh will correctly set up permissions and symlink the wrapper scripts:

$ cd /opt/sendmail-wrapper/
$ ./install.sh

Add the following lines to your /etc/sudoers:

/etc/sudoers
www-data        ALL = (sendmailwrapper) NOPASSWD:/usr/sbin/sendmail-throttle [0-9]*
%customers      ALL = (sendmailwrapper) NOPASSWD:/usr/sbin/sendmail-throttle [0-9]*

Add/modify the following in your php.ini:

php.ini
sendmail_path = /usr/sbin/sendmail-wrapper
auto_prepend_file = /var/www/shared/prepend.php

Import the sendmailwrapper database schema:

$ mysql -u root -p < schema/schema.mysql.sql

Create a MySQL user with the following permissions:

GRANT USAGE ON *.* TO sendmailwrapper@'localhost' IDENTIFIED BY '********';
GRANT SELECT, INSERT, UPDATE ON sendmailwrapper.throttle TO sendmailwrapper@'localhost';
GRANT INSERT ON sendmailwrapper.messages TO sendmailwrapper@'localhost';

Configuration

Default configuration can be found in config.ini:

config.ini
[global]
defaultTZ = Europe/Zurich
adminTo = hostmaster@example.com
adminFrom = hostmaster@example.com

[wrapper]
sendmailCmd = "/usr/sbin/sendmail -t -i"
throttleCmd ="sudo -u sendmailwrapper /usr/sbin/sendmail-throttle"
throttleOn = true
defaultHost = "example.com"
syslogPrefix = sendmail-wrapper-php
xHeaderPrefix = "X-Example-"

[throttle]
countMax = 1000
rcptMax = 1000
syslogPrefix = sendmail-throttle-php
adminSubject = "Sendmail limit exceeded"

[db]
dsn = "mysql:host=localhost;dbname=sendmailwrapper"
user = sendmailwrapper
pass = "xxxxxxxxxxxxxxxxxxxxx"

You should not change any of the above values. Create your own config.local.ini instead to overwrite some values, e.g.:

config.local.ini
[global]
adminTo = hostmaster@mydomain.com
adminFrom = hostmaster@mydomain.com

[wrapper]
defaultHost = "mydomain.com"
xHeaderPrefix = "X-MyCompany-"

Never put your database password in any of the above configuration files. Use another configuration file called config.private.ini instead, e.g.:

config.private.ini
[db]
pass = "mySuper-SecurePassword/826.4287+foo"

Sample syslog output:

Mar 24 11:50:21 web1 php: sendmail-throttle-php: user=web4111 (4111:4111), rcpts=1, status=0, command=/usr/sbin/sendmail-throttle 1, count_max=1000, count_cur=3, count_tot=9, rcpt_max=1000, rcpt_cur=3, rcpt_tot=9
Mar 24 11:50:21 web1 php: sendmail-wrapper-php: uid=web4111#012, msgid=20140324115021.7xp40a@example.com, from=foo@example.com, to="bar@example.com", cc="", bcc="", subject="Just testing", site=www.example.com, client=8.8.1.1, script=/var/www/example/public_html/www/index.php, throttleStatus=0

MySQL database sendmailwrapper:

You may access both MySQL tables directly: throttle to tweak each system user’s limits (they will be persistent! Only counters are gettingn reset every day), messages for detailed message header reviewing or for statistical purposes.