David NĂ©grier CTO

In this tutorial, we will see how to trigger a PHP script each time your web server is receiving some e-mails.
Why would you want to do that? Well you could want to do that to develop a custom mailing list system, to archive your mail in database, trigger commands in your website by sending mails... your imagination is the limit.

Getting started with a basic Postfix filter

Configuring Postfix

To trigger a PHP script, we are going to configure Postfix so that it delivers the mail to our script. First of all, you might want to have a global understanding of Postfix architecture. If you haven't read this article, read it now, it helps understand what's going on.

Now, let's add our filter. We do that in the master.cf file that contains the list of all processes run when a mail is delivered.

Let's add a single line:

myhook unix - n n - - pipe
  flags=F user=www-data argv=/path/to/postfix.php ${sender} ${size} ${recipient}

We have registered a script named "myhook".
Let's stay in master.cf. We must now tell Postfix when to run that script.

To do this, let's edit the smtp line and change it this way:

smtp      inet  n       -       -       -       -       smtpd
        -o content_filter=myhook:dummy

The -o content_filter=myhook:dummy tells Postfix to run the filter for any mail arriving via the SMTP delivery. Please note that is you are sending mails using the "sendmail" command, the filter will not trigger. In this case, add the option after the "pickup" delivery method:

pickup    fifo  n       -       -       60      1       pickup
    -o content_filter=myhook:dummy

Do not forget: after changing a configuration file, you must run the command:

postfix reload

Writing a basic filter

Now, let's write the script. Let's name it "postfix.php"
The first thing you want to do is make the script readable and executable by anybody (we can strengthen the rights later):

chmod +rx postfix.php

The script most be runnable from the command line, so it should start with: #!/usr/bin/php
(assuming your PHP CLI is in /usr/bin/php)

We will first write a script that does nothing, except writing a line in a temporary file at "/tmp/postfixtest".

#!/usr/bin/php
<?php
$file = fopen("/tmp/postfixtest", "a");
fwrite($file, "Script successfully ran at ".date("Y-m-d H:i:s")."n");
fclose($file);
?>

Try running that script manually using

su www-data
./postfix.php

and check that it works. If you see lines adding in /tmp/postfixtest, the script is correct.

It is important to "su" into the user that will run the script, since this is the only way to know if there are any security problems that might occur.

If the script is running fine, you can now try sending a mail.
The mail should be completely redirected to our filter.

You will notice that your PHP script is running (by looking at the "/tmp/postfixtest" file, and you will also notice that the mail you send is not normally delivered.

If you are facing a problem, the first place to look is in the postfix logs.
In Ubuntu and Debian, the default location for the logs is: /var/log/mail.log

Now, let's build up a slightly more useful script:

#!/usr/bin/php
<?php
$file = fopen("/tmp/postfixtest", "a");
fwrite($file, "Script successfully ran at ".date("Y-m-d H:i:s")."n");

// read from stdin
$fd = fopen("php://stdin", "r");
$email = "";
while (!feof($fd)) {
    $line = fread($fd, 1024);
    $email .= $line;
}
fclose($fd);

fwrite($file, $email);
fclose($file);

?>

This script does read the mail message that is sent over STDIN (the standard unix input).
It writes the output in our "/tmp/postfixtest" file. So now, you can see the content of the mail in the file.

What have we done so far?

So far, we have developed a system that triggers a script when a mail is received. The script prevents the mail from being normally delivered and instead, routes the content of the mail to another file (but we could make it analyse the mail and do various actions on it)

Now, try to send a mail to someone that does not exist on your domain. For instance, if your mail isjohn.doe@example.com, try to send a mail at does_not_exist@example.com.

You see? The script is triggered. What this means is that our filter is always triggered, whatever the receiver is!

This might be useful in some case, but might be a bit of an overkill. Let's have a look at fine-tuning the Postfix configuration to trigger the script only for some recipients.

Tuning Postfix configuration

Running the script only for some mail adresses

To do this, we will need to write an "access" configuration file. In this configuration file, we can write down which mail address should be filtered;

Here is a sample "access" file.

david@example.com FILTER myhook:dummy

This file says that the mails to "david@example.com" should be delivered to our filter.
Postfix does not know how to read a simple "access" file. We must "hash" it so that postfix can read it:

postmap /etc/postfix/access

The postmap command should create a /etc/postfix/access.db file. It is a "hashed" version of the "access" configuration file that can be quickly read by Postfix.

Finally, we must reference the "access" file in the main.cf file.
Look for the smtpd_recipient_restrictions directive and add at the beginning of the directive:check_recipient_access hash:/etc/postfix/access

Of the directive is not present in main.cf, add the following line:

smtpd\_recipient\_restrictions = check\_recipient\_access hash:/etc/postfix/access, permit\_mynetworks, reject\_unauth_destination

This will make sure that the "access" file is used for any mail received by SMTP.

This also means that the filter will not work if you send mails locally, using sendmail

Finally, let's reload the configuration.

postfix reload

Test this new setup by sending a mail to the address you put in the "access" file. Your script should run, only for this address!

There are many things you can do in the "access" file.
You can learn more by reading the "access" file Postfix documentation.

Among the many things you can do, you can use regular expressions, or you can replace the "access" file with a lookup in a SQL or LDAP database.

Analysing the mail in PHP

So far, we have seen how we can trigger a PHP script when a mail is received. We also know that the mail is sent on "STDIN" to PHP.
If you have a look at what is sent, here is a typical test mail:

From david@localhost  Thu Jan  5 10:04:48 2012
Received: from \[127.0.0.1\] (localhost.localdomain \[127.0.0.1\])
        by DavidUbuntu (Postfix) with ESMTPS id 740AD380597
        for <david@localhost>; Thu,  5 Jan 2012 10:04:48 +0100 (CET)
Message-ID: <1325754288.4989.6.camel@DavidUbuntu>
Subject: my title
From: David Negrier <david@localhost>
To: david@localhost
Date: Thu, 05 Jan 2012 10:04:48 +0100
Content-Type: text/plain
X-Mailer: Evolution 3.2.0- 
Content-Transfer-Encoding: 7bit
Mime-Version: 1.0

my mail content

We can see a section with headers, and the content of the mail at the bottom. This is not very easy to analyse, and it will get even trickier if we have attachements. So parsing this by hand is not really a good idea. Instead, we might want to use one of the numerous PHP classes written to parse MIME content:

Here is a sample using the Zend Framework Zend_Mail_Message class;

#!/usr/bin/php
<?php
require_once "Zend/Mime.php";
require_once "Zend/Mail/Message.php";
require_once "Zend/Mail.php";

$file = fopen("/tmp/postfixtest", "a");
fwrite($file, "Script successfully ran at ".date("Y-m-d H:i:s")."n");

// read from stdin
$fd = fopen("php://stdin", "r");
$email = "";
while (!feof($fd)) {
    $line = fread($fd, 1024);
    $email .= $line;
}
fclose($fd);

// Initialize email object using Zend
$emailObj = new Zend\_Mail\_Message(array('raw' => $email));

// Let's fetch the title of the mail
fwrite($file, "Received a mail whose subject is: ".$emailObj->subject."n");

// Let's extract the body from the text/plain piece of the email
if($emailObj->isMultipart()) {
  foreach (new RecursiveIteratorIterator($emailObj) as $part) {
    try {
      if (strtok($part->contentType, ';') == 'text/plain') {
        $body = trim($part);
        break;
      }
    } catch (Zend\_Mail\_Exception $e) { 
    // ignore 
    }
  }
  if(!$body) {
    fwrite($file, "An error occured: no body found");
  }
} else {
  $body = trim($emailObj->getContent());
}

// Let's write the body of the mail in our "/tmp/postfixtest" file
fwrite($file, "Body of the mail:n".$body."n");
fclose($file);
?>

About the author

David is CTO and co-founder of TheCodingMachine and WorkAdventure. He is the co-editor of PSR-11, the standard that provides interoperability between dependency injection containers. He is also the lead developper of GraphQLite, a framework-agnostic PHP library to implement a GraphQL API easily.