Using PHP and PEAR::Mail_mime To Mail-Merge Multipart/Alternative Emails

Fake-Adam-Savage-Cockney-accent: Cor! So, like, wha's my mo'ivation 'ere?

So, okay, like, yer wants ter send perfectly formatted HTML email to yer many admirers - but yer also wants your text-only email readers ter be able to read it! Horrors - stuck on th' horns of a dilemma!

PEAR::Mail_mime to the rescue!

  • I assume you already have a web server and PHP installed and some idea of how to use them. (If you don't, I recommend Practical PHP Programming.)
  • Make sure the PEAR package manager is installed on your development server. If you are running Windows and have PHP 5.2.0+ already installed, you can open a command shell (Start -> Run, 'cmd'+Enter), go to your PHP directory ('cd "c:\Program Files\PHP"' or wherever you put it), go to the PEAR directory ('cd PEAR') and run the self-installing archive ('php -q go-pear.phar'); otherwise get the latest version and follow the installation directions inside.
  • Get the Mail_mime package: open a command shell, go to the PEAR directory, and do 'pear install --alldeps pear/Mail_mime'.
  • If you're tired of opening command windows and cd'ing around to get where you need to be, I recommend Microsoft's Open Command Window Here powertoy. Vista has this built-in: shift-right-click a folder to get the Open Command Window Here option.

Okay, it's time to make it happen! Let's start a new script:

<?php

/********************
* A sample email-merge script using the PEAR Mail_mime class
*
* @author    Hugh Bothwell  hugh_bothwell@hotmail.com
* @version    1.0
*/

require_once 'Mail.php';        // The base email class
require_once 'Mail/mime.php';    // ... extended to do multipart/mime emails
require_once 'Net/SMTP.php';    // Talk directly to your mail server

    /***************
    * Get mailhost parameters
    * @see http://pear.php.net/package/Mail/docs/latest/Mail/Mail_smtp.html#methodMail_smtp
    * @return array Associative array containing mailhost data {name, email, ...}
    */
    
function get_params() {
        
// In practice this information should be include()d
        // from a configuration file in a private directory, ie
        //   $params = parse_ini_file('/var/www/private/mailhost.ini');
        // where
        // === begin /var/www/private/mailhost.ini ===
        // [mailhost]
        // host = "mail.ibm.com"
        // auth = true
        // username = "twatson@ibm.com"
        // password = "8ig8!u3"
        // === end ===
    
        
$params = array(
            
'host'        =>    'mail.ibm.com',
            
'auth'        =>    true,
            
'username'    =>    'twatson@ibm.com',
            
'password'    =>    '8ig8!u3',
            
'debug'        =>    true
        
);
        
        return 
$params;
    }

    
/***************
    * Get sender information
    * @return array Associative arrays containing sender data {fromname, fromemail, ...}
    */
    
function get_from() {
        
$arr = array(
            
'fromname'    =>    'Thomas J Watson',
            
'fromemail'    =>    'twatson@ibm.com',
        );
        
        return 
$arr;
    }

    
/***************
    * Get a list of people to send to
    * @return array Array of associative arrays containing recipient data {name, email, ...}
    */
    
function get_recipients() {
        
// In practice this information would
        // almost certainly be pulled from a database, ie
        //
        // $conn = mysql_query('SELECT name, email, where, item FROM person');
        //
        // $arr = array();
        // while( ($res = mysql_fetch_assoc($conn)) !== false )
        //     $arr[] = $res;
        
        
$arr = array(
            array(
                
'name'    =>    'Julius Caesar',
                
'email'    =>    'jcaesar@praetorian.org',
                
'where'    =>    'at the Coliseum',
                
'item'    =>    'some information',
                
'title'    =>    'Pontifex Maximus'
            
),
            array(
                
'name'    =>    'Hirohito',
                
'email'    =>    'emperor@risingsun.jp',
                
'where'    =>    'in your cherry orchard',
                
'item'    =>    'your father\'s sword',
                
'title'    =>    'Emperor Showa'
            
),
            array(
                
'name'    =>    'Sequoyah',
                
'email'    =>    'george@cherokee.org',
                
'where'    =>    'on the way to Oklahoma',
                
'item'    =>    'a dictionary',
                
'title'    =>    'Teacher'
            
)
        );
    
        return 
$arr;
    }

    
/***************
    * Get the message subject line
    * @return string Subject-line text
    */
    
function get_subject() {
        
// In practice this would probably come from a textarea form input, ie
        // $str = stripslashes($_POST['subject']);
        
$str "About [item]";

        return 
$str;
    }

    
/***************
    * Get the plain-text message to send
    * @return string Text of message to send
    */
    
function get_text() {
        
// In practice this would probably come from a textarea form input, ie
        // $str = stripslashes($_POST['text']);
        
        
$str = <<<EOF
Hello, [name]!
When we last met [where], you were looking for [item].  I think I have located it for you.
Hail, [title]!
EOF;

        return 
$str;
    }

    
/***************
    * Get the HTML message to send
    * @return string HTML of message to send
    */
    
function get_html() {
        
// In practice this would probably come from a richtext form input, ie
        // $str = stripslashes($_POST['html']);
        
        
$str = <<<EOF
<html>
 <head>
  <meta http-equiv="Content-Type" content="text/html; charset=iso-8559-1" />
  <title>Greetings, [name]</title>
  <style type="text/css">body { font-family:Verdana,Helvetica,Arial,sans-serif; }</style>
 </head>
 <body>
  <p>Hello, [name]!</p>
  <p>When we last met [where], you were looking for [item].  I think I have located it for you.</p>
  <p>Hail, [title]!</p>
 </body>
</html>
EOF;

        return 
$str;
    }

        
/***************
        * Encode mail-merge field names
        * @param string $str Field name to encode (ie 'name')
        * @return string Encoded field name (ie '[name]')
        */
        
function makeMergeCode($str) {
            return 
'['.trim($str).']';
        }

    
/***************
    * Do mail-merge substitution
    * @param string $str Text to do mail-merge against
    * @param array $data Associative array of mail-merge data
    * @return string Mail-merged result text
    */
    
function mergeText($str$data) {
        
$search array_keys($data);                    // Get data field names
        
$search array_map('makeMergeCode'$search);    //  and turn them into merge codes
        
        
$replace array_values($data);                    // Get data with which to replace merge codes
        
        
return str_replace($search$replace$str);
    }


/********************
* Mail-merge and send emails
* @param array $params Associative array of mail-host settings
* @param array $from Associative array containing sender information {fromname, fromemail, ...}
* @param array $toAll Array of associative arrays containing recipient information {name, email, ...}
* @param string $subj Email subject line
* @param string $text Plain-text mail content
* @param string $html HTML mail content
*/
function SendMultipartAlternativeMails($params$from$toAll$subj$text$html) {
    
$Mailer =& Mail::factory('smtp'$params);
    
$Msg = new Mail_mime();

    
// prevent the script from timing out while sending emails
    
set_time_limit(0);

    foreach(
$toAll as $pers) {
        
// Consolidate mail-merge data
        //
        // NOTE: a named var in $from will take precedence
        //  over one of the same name in $pers -
        //  for best results, do not overlap namespaces!
        //
        
$data array_merge($pers$from);
        
        
// Do mail-merge
        
$_subject mergeText($subj$data);
        
$_text mergeText($text$data);
        
$_html mergeText($html$data);
        
        
// Load message values...
        
$Msg->setSubject($_subject);
        
$Msg->setFrom("{$from['fromname']} <{$from['fromemail']}>");
        
$_to $Msg->encodeRecipients$pers['email'] );
        
$Msg->setTXTBody($_text);
        
$Msg->setHTMLBody($_text);
        
        
// NOTE: (as of Mail_mime 1.5.2):
        //   calling get() finalizes the object
        //   and sets the Content-type header;
        //   for this reason, it is imperative that
        //   get() be called before headers()!
        
$_msg $Msg->get();
        
$_head $Msg->headers();
        
        
$Mailer->send$_to$_head$_msg );
    }
                    
    
// reset the timeout counter
    
set_time_limit(30);    
}


SendMultipartAlternativeMails(
    
get_params(),
    
get_from(),
    
get_recipients(),
    
get_subject(),
    
get_text(),
    
get_html()
);

?>

Notes:
  1. The first two-thirds of the script - all the get_* functions - is just set-up, giving sample data to demonstrate the expected formats.
  2. mergeText() is kind of neat - it automatically figures out what merge codes are available from the provided data.
  3. SendMultipartAlternativeMails() is where the meat is - for each recipient, it mail-merges the email contents then constructs the email and sends it.
  4. Mail_mime accepts whatever you give it - text, HTML, attachments, etc - then automatically decides what sort of multipart email to generate. If you give it both text and HTML - as I have done - the resulting email is Multipart/Alternative, exactly as desired.
  5. As commented, the Mail_mime class has a gotcha - you must call get() to make headers() valid. This is remarkably ungraceful behavior, especially as send() expects its parameters in the reverse order! Took me 3/4hr to figure that one out. I am going to submit a patch on this.
Creative Commons License
This sample script is licensed under a
Creative Commons Attribution 2.5 Canada License.