An Improved Quick and Dirty POP3 Toolkit in PHP

February 8th, 2011 Permalink

I recently found myself in need of yet another five minute PHP solution to pulling emails from a POP3 mail server. Since all of my previous efforts in that direction seem to have evaporated into the mists of time and hard drive crashes, I headed over to Google to run the usual quick searches for freely available code snippets. Interestingly, the best quick solutions involve the IMAP package - which caused me to initially skip over those pages while still in rapid search mode. That set of functions can be used for POP3 access, however, despite the fact that all the function names begin with "imap_" and the package itself is called "IMAP."

If you look at the IMAP package pages, you'll see that a kind soul has left a quick and dirty solution for POP3 access in the comments. (As an aside, the comments are frequently the most useful part of the PHP manual website; always read them if you happen to be there to look up information on some recalcitrant function or another). Unfortunately, it has a couple of issues, most important of which for my purposes is that it only works for multi-part messages when tested against the Courier POP3 server I was using: presented with a single part message, it won't download the body. Additional inline documentation on the format of the results returned by the functions would also help - I spent a little too much time running IMAP package functions against the mail server just to see what came back under various circumstances.

So at the end of all that, I fixed up the wrapper code, tinkered with the function names and formatting, added some documentation, that sort of thing. You'll find the resulting slightly improved quick and dirty POP3 solution presented below. These wrapper functions use only a tenth of the IMAP package functionality, if that, and are focused on walking through the following actions:

  • Log in to a POP3 account.
  • Obtain data on the contents.
  • Download all the email messages in the account.
  • Delete the messages.
  • Finish up by logging out.

For more than that, you'll have to explore the IMAP package yourself - which you should, as it's a good piece of work. But on with the code:

// this set of functions needs the php-imap package installed:
// See: http://www.php.net/manual/en/book.imap.php
// On a Fedora system, "yum install php-imap" will do the trick

// a set of fairly short timeouts - you may want to lengthen them
// if you are not communicating with a server in the local network
imap_timeout(IMAP_OPENTIMEOUT, 2);
imap_timeout(IMAP_READTIMEOUT, 5);
imap_timeout(IMAP_WRITETIMEOUT, 5);
imap_timeout(IMAP_CLOSETIMEOUT, 2);

function &_get_pop3_connection() {
   global $_imap_stream_resource;
   return $_imap_stream_resource;
}

/**
 * Log in to a POP3 server. If this fails, use imap_errors() or imap_last_error()
 * to find out what went wrong.
 *
 * @return true on success, false on error.
 */
function pop3_login() {

   // replace this block with your own global configuration reader
   // or change the function definition to accept these values as parameters
   $config = get_config();
   $host = $config['host']; // mail.domain.com
   $ssl = $config['using_ssl']; // true | false
   $folder = $config['mail_folder']; // INBOX is the default - all caps
   $user = $config['my_account']; // user@domain.com
   $pass = $config['my_account_password'];

   // your mail server is probably self-signed if it is using SSL,
   // so that "novalidate-cert" is necessary. It might even be needed
   // for non-SSL connections, depending on how the server is
   // configured - and it doesn't hurt to have it there regardless.
   $sslpart = ($ssl) ? "/ssl/novalidate-cert": "/novalidate-cert";
   $port = ($ssl) ? 995 : 110;
   global $_imap_stream_resource;
   $_imap_stream_resource = imap_open(
      "{"."$host:$port/pop3$sslpart"."}$folder", $user, $pass
   );
   if( ! $_imap_stream_resource ) {
      return false;
   } else {
      return true;
   }
}

/**
 * Log out of the currently active POP3 server connection.
 */
function pop3_logout() {
   global $_imap_stream_resource;
   if( $_imap_stream_resource ) {
      // close connection and delete all messages flagged for deletion
      imap_close($_imap_stream_resource, CL_EXPUNGE);
      $_imap_stream_resource = false;
   }
}

/*
   Obtain information on the POP3 folder you are presently logged in to.
   The function returns a result of this form:

   Array
   (
       [Unread] => 0
       [Deleted] => 0
       [Nmsgs] => 0
       [Size] => 0
       [Date] => Mon, 7 Feb 2011 22:28:23 -0500 (EST)
       [Driver] => pop3
       [Mailbox] => {m.host.com:995/pop3/ssl/user="us@host.com"}INBOX
       [Recent] => 0
   )

   Where Nmsgs is the total number of messages in the folder, and the other
   parameters should be fairly self-explanatory.
 */
function pop3_get_mailbox_info() {
   $connection = &_get_pop3_connection();
   $check = imap_mailboxmsginfo($connection);
   return ((array)$check);
}

/*
   List messages in the presently active POP3 mail folder.
   The function produces results of this form: a list of message
   information:

   Array
   (
       [1] => Array
           (
               [subject] => test w/ attachment
               [from] => name
               [to] => mail@domain.com
               [date] => Mon, 7 Feb 2011 19:37:07 -0800
               [message_id] => <longstringhere@com>
               [size] => 962
               [uid] => 1
               [msgno] => 1
               [recent] => 1
               [flagged] => 0
               [answered] => 0
               [deleted] => 0
               [seen] => 0
               [draft] => 0
               [udate] => 1297136227
           )

       [2] => ...

   )

   @param range - a string in the form x:y e.g. 1:3 obtains messages 1, 2, and 3.
*/
function pop3_list_messages($range = "") {
   $connection = &_get_pop3_connection();
   if( !$range ) {
      $MC = imap_check($connection);
      $range = "1:".$MC->Nmsgs;
   }
   $response = imap_fetch_overview($connection,$range);
   $result = array();
   foreach( $response as $msg ) {
      $result[$msg->msgno]=(array)$msg;
   }
   return $result;
}

/**
 * Return the headers of a message, either as raw text or an array
 * indexed by header name.
 *
 * @param $message_id - an integer index key from the array
 * returned by pop3_list_messages
 * @param $headers_as_array - if true, return an array rather than the
 * raw header string
 * @return either a raw header string or the headers formed up as an array
 */
function pop3_get_message_headers($message_id, $headers_as_array = false) {
   $connection = &_get_pop3_connection();
   $headers = imap_fetchheader($connection, $message_id, FT_PREFETCHTEXT);
   if( $headers_as_array ) {
      $headers = _parse_mail_headers_into_array($headers);
   }
   return $headers;
}

/**
 * Mark the the indicated message for deletion. It will be deleted
 * when pop3_logout() is called.
 *
 * @param $message_id - an integer index key from the array
 * returned by pop3_list_messages
 */
function pop3_mark_message_for_deletion($message_id) {
   $connection = &_get_pop3_connection();
   return imap_delete($connection, $message_id);
}


/*
   Return an array containing data from the various message parts.
   The results look much like this for a single part email, with the
   message headers in raw and parsed form contained in the first array field.

   Array
   (
       [0] => Array
           (
               [charset] => us-ascii
               [data] => Return-Path:
                  Delivered-To: account@domain.com
                  Reply-To:
                  From: "That Guy"
                  To:
                  Subject: test 1
                  Date: Mon, 7 Feb 2011 19:37:07 -0800
                  Message-ID: <longstringhere@com>
                  MIME-Version: 1.0
                  Content-Type: text/plain;
                     charset="us-ascii"
                  Content-Transfer-Encoding: 7bit
                  Content-Language: en-us
               [parsed] => Array
                   (
                       [Return-Path] =>
                       [Delivered-To] => account@domain.com
                       [Reply-To] =>
                       [From] => "That Guy"
                       [To] =>
                       [Subject] => test 1
                       [Date] => Mon, 7 Feb 2011 19:37:07 -0800
                       [Message-ID] => <longstringhere@com>
                       [MIME-Version] => 1.0
                       [Content-Type] => text/plain;charset="us-ascii"
                       [Content-Transfer-Encoding] => 7bit
                   )
           )

       [1] => Array
           (
               [data] => example mail body, probably much longer in reality
               [charset] => us-ascii
           )
   )

   Multi-part mails will contain more array entries of data, one for each part
   or file attachment.
*/
function pop3_get_message($message_id) {
   $connection = &_get_pop3_connection();
   $mail = imap_fetchstructure($connection, $message_id);

   // this will chase down all the components of a multi-part mail,
   // but only return the headers of a single part mail
   $mail = _mail_get_parts($message_id, $mail, 0);
   $mail[0]["parsed"] = _parse_mail_headers_into_array($mail[0]["data"]);

   // so if it is only a single part mail, we have to go and fetch the body
   if( count($mail) == 1) {
      $mail[] = array(
         'data' => imap_body($connection, $message_id),
         'charset' => $mail[0]['charset'],
      );
   }

   return $mail;
}

//------utility functions from here on down-------------------------------

// parse massage header text into an array.
function _parse_mail_headers_into_array($headers) {
   $headers = preg_replace('/\r\n\s+/m', '',$headers);
   preg_match_all('/([^:\s]+):\s(.+?(?:\r\n\s(?:.+?))*)?\r\n/m', $headers, $matches);
   $result = array();
   foreach( $matches[1] as $key => $value ) {
      $result[$value] = $matches[2][$key];
   }
   return $result;
}

// step through the parts of a multi-part mail, getting the content for each one.
function _mail_get_parts($message_id, $part, $prefix) {
   $attachments = array();
   $attachments[$prefix] = _mail_decode_part($message_id, $part, $prefix);
   if( isset($part->parts) ) // multipart
   {
      $prefix = ($prefix == "0")?"":"$prefix.";
      foreach( $part->parts as $number => $subpart ) {
         $attachments = array_merge(
            $attachments,
            _mail_get_parts($message_id, $subpart, $prefix.($number+1)));
      }
   }
   return $attachments;
}

// retrieve and decode the content for a single part.
function _mail_decode_part($message_id, $part, $prefix) {
   $connection = &_get_pop3_connection();
   $attachment = array();

   if( isset($part->ifdparameters) && $part->ifdparameters ) {
      foreach( $part->dparameters as $object ) {
         $attachment[strtolower($object->attribute)] = $object->value;
         if( strtolower($object->attribute) == 'filename' ) {
            $attachment['is_attachment'] = true;
            $attachment['filename'] = $object->value;
         }
      }
   }

   if( isset($part->ifparameters) && $part->ifparameters ) {
      foreach( $part->parameters as $object ) {
         $attachment[strtolower($object->attribute)] = $object->value;
         if( strtolower($object->attribute) == 'name' ) {
            $attachment['is_attachment'] = true;
            $attachment['name'] = $object->value;
         }
      }
   }

   $attachment['data'] = imap_fetchbody($connection, $message_id, $prefix);
   if( isset($part->encoding) ) {
      if( $part->encoding == 3 ) { // 3 = BASE64
         $attachment['data'] = base64_decode($attachment['data']);
      } elseif( $part->encoding == 4 ) { // 4 = QUOTED-PRINTABLE
         $attachment['data'] = quoted_printable_decode($attachment['data']);
      }
   }
   return $attachment;
}