Cover V13, i11

Article

nov2004.tar

Checking Email with PHP

Russell J.T. Dyer

As a consultant I'm often away from my office and will miss email from clients. With Web access to my email server, I can read and send mail; however, I don't have time to keep checking email when I'm working. Instead, I've written a program in PHP that checks email for me. I have cron run it every 15 minutes. If there's a message, PHP sends a text message to my cellular telephone letting me know. In this article, I will review this PHP script as a learning opportunity.

Mail Log in MySQL

To keep track of client email addresses and to prevent PHP from notifying me more than once for a message, I set up a couple of tables in MySQL. The first table contains client names and email addresses. From the mysql client program, I entered the following:

   CREATE DATABASE mail_log;

   USE mail_log;

   CREATE TABLE email_addresses
   (address_id INT AUTO_INCREMENT PRIMARY KEY,
   email_address CHAR(100) UNIQUE, name CHAR(100));
The first two lines create the database mail_log in MySQL and switches the session to it. It's not necessary to create a separate database, though. You could use an existing one instead. The next set of lines creates the table containing client information. The first column is an automatically incremented key field that will be referenced in queries along with the next table, which will contain a log of email. The second column will hold email addresses. Notice that I've required data in this column to be unique; duplicates are not allowed. The last column will hold the client's name. I've set all of these columns to fixed-width columns (no VARCHAR columns) so that the rows will be fixed. This will help speed up queries (not that it matters very much for a script run by cron, but it's a good habit to maintain). I could create a PHP script to enter data in this table through a Web interface, but to keep this article focused, I'll manually enter the email addresses of my few clients like so:

   INSERT INTO email_addresses
   (email_address, name)
   VALUES('russell@dyerhouse.com', 'Russell Dyer');
With the email address table in place, look at the table for logging in email:

   CREATE TABLE log
   (log_id INT AUTO_INCREMENT PRIMARY KEY,
   address_id INT NOT NULL, notified ENUM('y','n'),
   log_date DATETIME, subject CHAR(100),
   message CHAR(100),
   UNIQUE(address_id, log_date));
In this SQL statement, I'm creating the table log with another automatically incremented record identifier (i.e., log_id). This will be useful when updating rows later. The next column will contain the address_id number from the email_addresses table. To keep track of whether PHP has already notified me about an email message, I set up the column notified. I've enumerated the only possible answers to optimize queries.

Next is a column to record the date of each email message, followed by the subject and the first 100 characters of the message. On my cellular telephone, text messages are limited to only 100 characters. I end the SQL statement above with instructions for MySQL to create an index of both address_id and log_date. This will not only speed up queries, but it will also prevent a message from being recorded twice. If PHP attempts to insert data into log for an email address in which there already exists a related record with the exact same date and time, then the query will be rejected. That's all of the tables that are needed in MySQL. The next section introduces the PHP script that will check for mail sitting on the server.

Logging Mail with PHP

There are some nice functions for PHP that make examining a POP3 mailbox pretty easy. However, on my server, which runs with Red Hat Linux, I had to download an rpm to add the functions that I needed:

php-imap-xxxx-i386.rpm
With the imap functions in PHP, it can check the mail waiting for me on my email server.

#!/usr/bin/php -q

<?php
$mail_svr = 'mail.dyerhouse.com';
$to = 'russ_cell@dyerhouse.com';
$mail_box =
imap_open("{$mail_svr:110/pop3/notls}INBOX",'russell','password');

$headers = imap_headers($mail_box);
$msg_count = count($headers);
The first two lines start the PHP script. Next, PHP sets up two variables: one contains the address of the mail server, the other contains the email address of my cell phone. If my email server were on the same server, mail.dyerhouse.com could be replaced with localhost. The email address is an alias that I entered in /etc/aliases, which translates to the lengthy numeric address for my cellular telephone.

On the next line of code, I'm using the imap_open() function to open my inbox. Because I'm using POP3, I list port 110 and the notation about pop3. For IMAP servers, use port 143 and just leave off the forward-slash along with the pop3. The notation about notls is necessary because I'm using Red Hat Linux, but may not be required for your server. With a connection to the mailbox established, the next line retrieves an array of email headers using the imap_headers() function so that the number of messages may be determined. This will be useful in a for statement that is to follow in which PHP will loop through all of the messages. Before doing that, though, PHP needs to get a list of client email addresses from MySQL:

$link = mysql_connect('localhost', 'russell', 'password')
        or die("Couldn't connect to MySQL" . mysql_error() . "\n");
mysql_select_db('mail_log', $link);

$sql_stmnt = "SELECT address_id, email_address  
              FROM email_addresses";

$results = mysql_query($sql_stmnt)
           or die("Couldn't query server" . mysql_error() . "\n");

while ($row = mysql_fetch_assoc($results)){
   $addresses[$row['email_address']] = $row['address_id'];
}
After connecting to the MySQL server, PHP switches the session to the database mail_log using the mysql_select_db() function. Using the function mysql_query(), PHP queries the database with the SQL statement contained in $sql_stmnt to select client identifiers and their email addresses. Utilizing a while statement and the mysql_fetch_assoc() function, PHP loops through each row found in the email_addresses table, per the SQL statement. With each pass through the while statement, the results from each row is added to an associative array ($addresses[]) that will be referred to later when PHP looks through each email message. The syntax to extract an element using mysql_fetch_assoc() is to simply give the field name as the key, as shown above.

for ($c = 1; $c <= $msg_count; $c++) {
   $headerinfo = imap_headerinfo($mail, $c);
   $unixtime = $headerinfo->udate;
   $sender_username = $headerinfo->from[0]->mailbox;
   $sender_host = $headerinfo->from[0]->host;
   $sender_address = "$sender_username@$sender_host";
   $subject = $headerinfo->subject;
   $message = substr(imap_body($mail, $c), 0, 100);
In the first line here, the for statement mentioned previously is started. Before discussing its components, notice that the closing curly-brace that would end this statement does not appear above. It shows up later in the script. At the start of the for statement, PHP sets a counter ($c) to keep track of which email message is being processed (1 is first). The for statement will last as long as the count does not exceed the number of messages determined earlier ($msg_count).

The first set of lines in the statement block use the imap_headerinfo() function to retrieve various bits of data (e.g., sender, time, and subject) from the mailbox. The function returns an object and therefore requires the syntax that you see here to extract the values of each attribute of the object. Notice that the email address comes in pieces: the user's name is separated from the domain name. As a result, PHP has to join them.

In the last line above, PHP uses the function imap_body() to get the body of the message. Using the substr() function, PHP saves only the first 100 characters. Incidentally, if you're not the only person who has access to your MySQL server, you might not want to save even part of your messages. At a minimum, I have PHP delete the message from MySQL after emailing it to my cellular telephone.

Now that PHP has the salient bits of information from the message, it needs to check the email address against the associative array set up previously, which contains the client addresses. This is done with an if statement as follows:

   if($address_id = $addresses["$sender_address"]) {
      $sql_stmnt = "INSERT IGNORE INTO log
                    (address_id, notified, log_date,
                     subject, message)
                    VALUES('$address_id', 'n',
                    FROM_UNIXTIME($unixtime),
                    '$subject', '$message')";
      mysql_query($sql_stmnt);
   }
}
imap_close($mail);
The condition of the if statement here attempts to extract the address_id from the associative array $addresses[] based on the sender's email address. If it's successful, if the email message being examined is from an email address found in the email_addresses table, then it attempts to make an entry in the log table. The flag IGNORE is added so that MySQL will ignore any error messages that will be generated by attempting to insert data that's not unique (i.e., with the same address_id and log_date).

Using the FROM_UNIXTIME() function, MySQL converts the Unix time to the datetime column type. The next line of code invokes the SQL statement and the second closing curly-brace ends the for statement started earlier. This allows PHP to close the connection to the mailbox with the imap_close() function.

Now that the log has been updated for the new messages, PHP needs to figure out which ones to notify me about:

$sql_stmnt = "SELECT log_id, name, subject, message,
              DATE_FORMAT(log_date, '%b %d') AS date
              FROM log, email_addresses
              WHERE log.address_id = email_addresses.address_id
              AND notified = 'n'";

$results = mysql_query($sql_stmnt);

while($row = mysql_fetch_assoc($results)) {
   $log_id = $row["log_id"];
   $name = $row["name"];
   $subject = $row["subject"];
   $message = $row["message"];
   $date = $row["date"];

   $sql_stmnt = "UPDATE log
                 SET notified = 'y', message = ''
                 WHERE log_id = '$log_id'";
   mysql_query($sql_stmnt);

   mail($to, $subject, "$name ($date)\n$message");
}

mysql_close();
?>
Looking at the SQL statement above, MySQL retrieves the data needed to construct an email message to send my cellular telephone. It will only select rows where the column notified is equal to 'n', indicating that I have not been notified yet. Using the built-in MySQL function DATE_FORMAT(), MySQL gives me a shorter, simpler date entry (e.g., "Oct 15"). The while statement that follows uses the mysql_fetch_assoc() function to extract fields from the results set and to place the values in the appropriate variables.

With each pass through the while statement, MySQL is updating the table log to set the notified column to 'y', indicating that I've been notified. MySQL also erases the contents of the message column to maintain some privacy. The message is only needed for the duration of this script. There is the possibility that the email might not be sent successfully and PHP won't know to retry. However, transaction safety is not all that important for the use made of this script.

In the third from the last line of code above, PHP uses the mail() function to send an email informing me that I have a message from a client or friend waiting. The message is sent to the address contained in the variable $to, which was set up in the beginning of the script. The subject from the original email is used for this notice, as well. For the third argument of the mail() function, PHP will put together the sender's name and the date of the message on one line. Using \n, a line-break is added before the message. With each loop through the while statement, for each new client message, PHP will send me a text message. I could combine multiple message notices into one, but I only have 100 characters and I want to conserve the space by doing separate messages.

The closing curly-brace above ends the while statement. Since the script is ending, the connection to MySQL is closed using the mysql_close() function.

Final Thoughts

I think this script is very handy. It allows me to stay focused on my work and still be responsive to client emails. If you set up a script like this on your server, you may want to configure cron to run it more often, maybe every five minutes. Don't schedule it to run too frequently or it may sometimes notify you of messages even when you're at your desk with your email client program running. Incidentally, you will circumvent the script if you leave your email client running while you're away from your office. Finally, for tidiness, you may want to write a short script that cron runs once a week to delete rows in the log table from the previous week.

Russell Dyer is a Perl programmer, a MySQL developer, and a Web designer living and working on a consulting basis in New Orleans. He can be reached at: russell@dyerhouse.com.