*/ /** IMAP connection abstraction In the IMAP terminology, "mailbox" is a folder in the mailbox of the user */ class imap { /** The mailbox string */ private $mailbox = null; /** The current folder in UTF-8 */ private $curDir = "INBOX"; /** The auto expunge feature, after deleting/moving an email */ public $autoexpunge = true; /** Limit to one instance of the connection to the same database */ // Based on an idea of http://tonylandis.com/php/php5-pdo-singleton-class/ private static $instance = array (); /** The constructor The IMAP standard port is 143, but SSL tunnelled is 993 */ public function __construct ($imapserver = "localhost", $imapport = 143, $username = null, $password = null, $imapssl = false, $imapcertvalidate = true) { $this->connect ($imapserver, $imapport, $username, $password, $imapssl, $imapcertvalidate); } /** The connect can be used when extends the imap class. The constructor can be override by the child class. The IMAP standard port is 143, but SSL tunnelled is 993 */ public function connect ($imapserver = "localhost", $imapport = 143, $username = null, $password = null, $imapssl = false, $imapcertvalidate = true) { if (! function_exists ("imap_open")) throw new Exception ("PHP don't support IMAP. Please add it !", 500); if (! function_exists ("mb_convert_encoding")) throw new Exception ("PHP don't support MBString. Please add it !", 500); if ($username === null) throw new Exception ("No username provided for IMAP server", 500); if ($password === null) throw new Exception ("No password provided for IMAP server", 500); $imapssl = ($imapssl !== false) ? "/ssl" : ""; $imapcertvalidate = ($imapcertvalidate === false) ? "/novalidate-cert" : ""; $this->mailbox = "{"."$imapserver:$imapport/imap$imapssl$imapcertvalidate". "/user=$username}"; if (! array_key_exists ($this->mailbox, self::$instance)) { try { // Timeout authentication error to 1s (can't be less). By default, IMAP // wait 10s before returning an auth error imap_timeout (IMAP_READTIMEOUT, 1); self::$instance[$this->mailbox] = @imap_open ($this->mailbox, $username, $password); if (self::$instance[$this->mailbox] === FALSE) throw new Exception (imap_last_error()); } catch (Exception $e) { // imap_errors() takes the errors and clear the error stack $errors = imap_errors(); if (substr ($e->getMessage (), 0, 35) === "Can not authenticate to IMAP server") throw new Exception ("IMAP error : ".$e->getMessage(), 401); throw new Exception ("IMAP error : ".$e->getMessage(), 500); } } } /////////////////// /// FOLDERS /// /////////////////// /** Return an array of the existing folders. The sub-folders are with slash separator The names of folders are converted in UTF-8 */ public function foldersList () { if ($this->mailbox === null) throw new Exception ("IMAP server not connected", 500); $list = array_keys ($this->foldersListWithAttr ()); sort ($list); return $list; } /** Return an array with folder name in key and attributes in value. The attributes allow to see if there is new mails in folders */ public function foldersListWithAttr () { if ($this->mailbox === null) throw new Exception ("IMAP server not connected", 500); $list = imap_getmailboxes (self::$instance[$this->mailbox], $this->mailbox, "*"); $res = array (); foreach ($list as $val) { $dir = substr ($val->name, strlen ($this->mailbox)); $dir = mb_convert_encoding ($dir, "UTF8", "UTF7-IMAP"); if (isset ($val->delimiter)) $dir = str_replace ($val->delimiter, "/", $dir); $res[$dir] = $val->attributes; } return ($res); } /** Change to provided folder The folder name must be in UTF-8. The folder must be absolute */ public function changeFolder ($folder) { if ($this->mailbox === null) throw new Exception ("IMAP server not connected", 500); if (! in_array ($folder, $this->foldersList ())) throw new Exception ("Folder not found", 404); $folderUTF7 = mb_convert_encoding ($folder, "UTF7-IMAP","UTF-8"); $rc = @imap_reopen (self::$instance[$this->mailbox], $this->mailbox.$folderUTF7); if ($rc === true) $this->curDir = $folder; else throw new Exception ("Can't go in provided folder", 500); return $rc; } /** Return the current folder in UTF-8 */ public function getFolder () { if ($this->mailbox === null) throw new Exception ("IMAP server not connected", 500); return $this->curDir; } /** Create a new folder, provided in UTF-8. The folder must be absolute */ public function createFolder ($folder) { if ($this->mailbox === null) throw new Exception ("IMAP server not connected", 500); if ( in_array ($folder, $this->foldersList ())) throw new Exception ("Folder already exists", 406); $folderUTF7 = mb_convert_encoding ($folder, "UTF7-IMAP","UTF-8"); return imap_createmailbox (self::$instance[$this->mailbox], $this->mailbox.$folderUTF7); } /** Delete an existing folder provided in UTF-8. The folder must be absolute */ public function deleteFolder ($folder) { if ($this->mailbox === null) throw new Exception ("IMAP server not connected", 500); if (! in_array ($folder, $this->foldersList ())) throw new Exception ("Folder not found", 404); $folderUTF7 = mb_convert_encoding ($folder, "UTF7-IMAP","UTF-8"); return imap_deletemailbox (self::$instance[$this->mailbox], $this->mailbox.$folderUTF7); } /** Return the list of the folders substcribed by the user. The folders are in UTF-8 */ public function getSubscribe () { if ($this->mailbox === null) throw new Exception ("IMAP server not connected", 500); $subs = imap_getsubscribed (self::$instance[$this->mailbox], $this->mailbox, "*"); $res = array (); foreach ($subs as $sub) { $res [] = str_replace ($sub->delimiter, "/", substr ($sub->name, strlen ($this->mailbox))); } $res = array_map (function ($folder) { return mb_convert_encoding ($folder, "UTF-8", "UTF7-IMAP"); }, $res); sort ($res); return $res; } /** Add a subscription for a folder. The folder must be in UTF-8 */ public function addSubscribe ($folder) { if ($this->mailbox === null) throw new Exception ("IMAP server not connected", 500); $folderUTF7 = mb_convert_encoding ($folder, "UTF7-IMAP","UTF-8"); return imap_subscribe (self::$instance[$this->mailbox], $this->mailbox.$folder); } /** Remove a subscription for a folder. The folder must be in UTF-8 */ public function delSubscribe ($folder) { if ($this->mailbox === null) throw new Exception ("IMAP server not connected", 500); $folderUTF7 = mb_convert_encoding ($folder, "UTF7-IMAP","UTF-8"); return imap_unsubscribe (self::$instance[$this->mailbox], $this->mailbox.$folder); } /** Return the informations concerning a folder. It return an object with the following properties : Date date of last change (current datetime) Driver driver Mailbox name of the mailbox Nmsgs number of messages Recent number of recent messages Unread number of unread messages Deleted number of deleted messages Size mailbox size */ public function getFolderInfo ($folder) { if ($this->mailbox === null) throw new Exception ("IMAP server not connected", 500); $oldFolder = $this->curDir; $this->changeFolder ($folder); $rc = imap_mailboxmsginfo (self::$instance[$this->mailbox]); $this->changeFolder ($oldFolder); if ($rc === false) throw new Exception ("Can't read information for folder $folder", 500); return $rc; } ////////////////////// /// LIST MAILS /// ////////////////////// /** Return an array of mailHeaders order by $field and by order ASC or DESC */ public function imapSortMail ($mailHeaders, $field, $orderAsc = TRUE) { if ($this->mailbox === null) throw new Exception ("IMAP server not connected", 500); $this->changeFolder ($this->curDir); $sortList = array (); $sortInc = array (); foreach ($mailHeaders as $mail) { // Permit to have to mails with the same comparator field. Add an // increment at the end of field if (!isset ($sortInc[$mail->$field])) $inc = 1; else $inc = $sortInc[$mail->$field] + 1; $sortInc[$mail->$field] = $inc; $sortList[$mail->$field.$inc] = $mail; } ksort ($sortList, SORT_NATURAL); return array_values ($sortList); } /** Fetch the headers for all messages in the current folder sorted by date Return an array of mail object containing information like the subject, the date, if the message is already read (recent), answered... (see http://www.php.net/manual/en/function.imap-fetch-overview.php) If the $from is negative, take the LAST $from mails If from is zero, it's value is override to 1 For information, takes 0.4s to select 30 mails on 1552 **/ public function mailsDate ($from = 1, $nbmails = 30) { if ($this->mailbox === null) throw new Exception ("IMAP server not connected", 500); $this->changeFolder ($this->curDir); if ($from === null) $from = 1; $MC = imap_check (self::$instance[$this->mailbox]); if ($MC->Nmsgs === 0) return array (); if ($nbmails > $MC->Nmsgs) $nbmails = $MC->Nmsgs; if ($from < 0) { $from = abs ($from); if ($from < 1) $from = 1; if ($from > $MC->Nmsgs) throw new Exception ("Mail start is higher than the number of mails", 500); $from = $MC->Nmsgs - $from + 1; $to = $from + $nbmails - 1; if ($to > $MC->Nmsgs) $to = $MC->Nmsgs; } else { if ($from > $MC->Nmsgs) throw new Exception ("Mail start is higher than the number of mails", 500); if ($from < 1) $from = 1; $to = $from + $nbmails - 1; if ($to > $MC->Nmsgs) $to = $MC->Nmsgs; } $headers = array (); // Adding the FT_UID options cost 1.1s $result = imap_fetch_overview (self::$instance[$this->mailbox], "$from:$to", 0); // imap_errors() takes the errors and clear the error stack $errors = imap_errors(); return $result; } /** Return all the mails numbers order by thread in an array. [] => array ("msgno"=>msgno, "depth"=>depth) */ public function mailsThread () { if ($this->mailbox === null) throw new Exception ("IMAP server not connected", 500); $this->changeFolder ($this->curDir); $threads = @imap_thread (self::$instance[$this->mailbox]); // imap_errors() takes the errors and clear the error stack $errors = imap_errors(); $thread = array (); $depth = 0; foreach ($threads as $key => $val) { $tree = explode('.', $key); if ($tree[1] == 'num') { // If the mail unknown (the mails starts at 1), skip the thread record if ($val === 0) continue; $thread[] = array ("msgno"=>$val, "depth"=>$depth); $depth++; } elseif ($tree[1] == 'branch' && $depth > 0) { $depth--; } } return $thread; } /** Send back the number of mails in the mailbox */ public function mailsNumber () { if ($this->mailbox === null) throw new Exception ("IMAP server not connected", 500); $this->changeFolder ($this->curDir); $MC = imap_check (self::$instance[$this->mailbox]); return $MC->Nmsgs; } /** Return an array containing the msgno corresponding to the criteria */ public function mailsSearch ($criteria) { if ($this->mailbox === null) throw new Exception ("IMAP server not connected", 500); $this->changeFolder ($this->curDir); return imap_search (self::$instance[$this->mailbox], $criteria); } /** Move the mail provided in the $folder in UTF-8. If $msgno is an array, all the mails with the contain msgno are deleted Expunge automatically the current folder to remove the old emails */ public function mailMove ($msgno, $folder) { if ($this->mailbox === null) throw new Exception ("IMAP server not connected", 500); $this->changeFolder ($this->curDir); if (is_array ($msgno)) $msgno = implode (",", $msgno); $folderUTF7 = mb_convert_encoding ($folder, "UTF7-IMAP","UTF-8"); $rc = imap_mail_move (self::$instance[$this->mailbox], $msgno, $folderUTF7); if ($rc !== TRUE) { return FALSE; } if ($this->autoexpunge) return imap_expunge (self::$instance[$this->mailbox]); return true; } /** Copy the mail provided in the $folder in UTF-8. If $msgno is an array, all the mails with the contain msgno are copied */ public function mailCopy ($msgno, $folder) { if ($this->mailbox === null) throw new Exception ("IMAP server not connected", 500); $this->changeFolder ($this->curDir); if (is_array ($msgno)) $msgno = implode (",", $msgno); $folderUTF7 = mb_convert_encoding ($folder, "UTF7-IMAP","UTF-8"); $rc = imap_mail_copy (self::$instance[$this->mailbox], $msgno, $folderUTF7); if ($rc !== TRUE) { return FALSE; } return true; } /** Expunge the mailbox. If the autoexpunge is activated, it is normally not needed */ public function expunge () { if ($this->mailbox === null) throw new Exception ("IMAP server not connected", 500); $this->changeFolder ($this->curDir); return imap_expunge (self::$instance[$this->mailbox]); } ///////////////////////////// /// GET/SET/DEL EMAIL /// ///////////////////////////// /** Get an existing email in the current folder in raw format */ public function getEmailRaw ($msgno) { if ($this->mailbox === null) throw new Exception ("IMAP server not connected", 500); $this->changeFolder ($this->curDir); // Clear the errors imap_errors(); $content = @imap_fetchheader (self::$instance[$this->mailbox], $msgno)."\n". @imap_body (self::$instance[$this->mailbox], $msgno); $errors = imap_errors (); if ($errors !== false) throw new Exception ("Mail not found", 404); return $content; } /** Get the headers of the email (in raw format) */ public function getEmailHeadersRaw ($msgno) { if ($this->mailbox === null) throw new Exception ("IMAP server not connected", 500); $this->changeFolder ($this->curDir); // Clear the errors imap_errors(); $content = @imap_fetchheader (self::$instance[$this->mailbox], $msgno); $errors = imap_errors (); if ($errors !== false) throw new Exception ("Mail not found", 404); return $content; } /** Get all the body (and attached files) of an email in raw format */ public function getEmailBodyRaw ($msgno) { if ($this->mailbox === null) throw new Exception ("IMAP server not connected", 500); $this->changeFolder ($this->curDir); // Clear the errors imap_errors(); $content = @imap_body (self::$instance[$this->mailbox], $msgno); $errors = imap_errors (); if ($errors !== false) throw new Exception ("Mail not found", 404); return $content; } /** Return email structure of the body */ public function getStructure ($msgno) { if ($this->mailbox === null) throw new Exception ("IMAP server not connected", 500); $this->changeFolder ($this->curDir); // Clear the errors imap_errors(); $structure = @imap_fetchstructure (self::$instance[$this->mailbox], $msgno); $errors = imap_errors (); if ($errors !== false) throw new Exception ("Mail not found", 404); return $structure; } /** Return the structure of the mail body with the associated content */ public function getStructureWithContent ($msgno) { if ($this->mailbox === null) throw new Exception ("IMAP server not connected", 500); $this->changeFolder ($this->curDir); // Clear the errors imap_errors(); $structure = @imap_fetchstructure (self::$instance[$this->mailbox], $msgno); $errors = imap_errors (); if ($errors !== false) throw new Exception ("Mail not found", 404); if (! isset ($structure->parts)) { // In case of PLAIN text, there is no parts $content = imap_fetchbody (self::$instance[$this->mailbox], $msgno, 1); if ($structure->encoding === 4) $content = quoted_printable_decode ($content); elseif ($structure->encoding === 3) $content = base64_decode ($content); foreach ($structure->parameters as $param) { if ($param->attribute === "charset") $content = iconv ($param->value, "utf-8", $content); } $structure->content = $content; return $structure; } foreach ($structure->parts as $part1=>$struct1) { if (isset ($struct1->parts)) { foreach ($struct1->parts as $part2=>$struct2) { $content = imap_fetchbody (self::$instance[$this->mailbox], $msgno, ($part1+1).".".($part2+1)); if ($struct2->encoding === 4) $content = quoted_printable_decode ($content); elseif ($struct2->encoding === 3) $content = base64_decode ($content); foreach ($struct2->parameters as $param) { if ($param->attribute === "charset") $content = iconv ($param->value, "utf-8", $content); } $structure->parts[$part1]->parts[$part2]->content = $content; // Add the MIME type if ($struct2->type === 0) $structure->parts[$part1]->parts[$part2]->mimetype = "text/". strtolower ($struct2->subtype); elseif ($struct2->type === 1) $structure->parts[$part1]->parts[$part2]->mimetype = "multipart/". strtolower ($struct2->subtype); elseif ($struct2->type === 2) $structure->parts[$part1]->parts[$part2]->mimetype = "message/". strtolower ($struct2->subtype); elseif ($struct2->type === 3) $structure->parts[$part1]->parts[$part2]->mimetype = "application/". strtolower ($struct2->subtype); elseif ($struct2->type === 4) $structure->parts[$part1]->parts[$part2]->mimetype = "audio/". strtolower ($struct2->subtype); elseif ($struct2->type === 5) $structure->parts[$part1]->parts[$part2]->mimetype = "image/". strtolower ($struct2->subtype); elseif ($struct2->type === 6) $structure->parts[$part1]->parts[$part2]->mimetype = "video/". strtolower ($struct2->subtype); elseif ($struct2->type === 7) $structure->parts[$part1]->parts[$part2]->mimetype = "other/". strtolower ($struct2->subtype); else throw new Exception (sprintf ( _("Unknown type in imap_fetchstructure : %s"), $struct2->type), 500); } } else { $content = imap_fetchbody (self::$instance[$this->mailbox], $msgno, $part1+1); if ($struct1->encoding === 4) $content = quoted_printable_decode ($content); elseif ($struct1->encoding === 3) $content = base64_decode ($content); foreach ($struct1->parameters as $param) { if ($param->attribute === "charset") $content = iconv ($param->value, "utf-8", $content); } $structure->parts[$part1]->content = $content; // Add the MIME type if ($struct1->type === 0) $structure->parts[$part1]->mimetype = "text/". strtolower ($struct1->subtype); elseif ($struct1->type === 1) $structure->parts[$part1]->mimetype = "multipart/". strtolower ($struct1->subtype); elseif ($struct1->type === 2) $structure->parts[$part1]->mimetype = "message/". strtolower ($struct1->subtype); elseif ($struct1->type === 3) $structure->parts[$part1]->mimetype = "application/". strtolower ($struct1->subtype); elseif ($struct1->type === 4) $structure->parts[$part1]->mimetype = "audio/". strtolower ($struct1->subtype); elseif ($struct1->type === 5) $structure->parts[$part1]->mimetype = "image/". strtolower ($struct1->subtype); elseif ($struct1->type === 6) $structure->parts[$part1]->mimetype = "video/". strtolower ($struct1->subtype); elseif ($struct1->type === 7) $structure->parts[$part1]->mimetype = "other/". strtolower ($struct1->subtype); else throw new Exception (sprintf ( _("Unknown type in imap_fetchstructure : %s"), $struct1->type), 500); } } return $structure; } /** Return the content of a part of the mail body defined in the structure in an object, with the associated mimetype, the parameters like the charset if they are defined, the number of lines associated to this part in the mail and some other info */ public function getStructureContent ($msgno, $part) { if ($this->mailbox === null) throw new Exception ("IMAP server not connected", 500); $structure = $this->getStructureWithContent ($msgno); if (isset ($structure->parts[$part])) return $structure->parts[$part]; throw new Exception ("Part not found in the mail", 404); } /** Return the part identifiers of the structure of the mail body. To be used in getStructureContent */ public function getStructureParts ($msgno) { if ($this->mailbox === null) throw new Exception ("IMAP server not connected", 500); $structure = $this->getStructure ($msgno); if (! isset ($structure->parts)) return array (); return array_keys ($structure->parts); } /** Delete all the mailIDs (msgno) provided in an array or a single mail if $msgno is not an array DO NOT MOVE THE MAIL IN TRASH, DESTROY THE MAIL REALLY Expunge the mails at the end of the operation */ public function mailsDel ($msgno) { if ($this->mailbox === null) throw new Exception ("IMAP server not connected", 500); $this->changeFolder ($this->curDir); if (is_array ($msgno)) $msgno = implode (",", $msgno); $rc = @imap_delete (self::$instance[$this->mailbox], $msgno); imap_errors(); if ($rc === FALSE) throw new Exception ("No mailID provided can be found : ABORT"); if ($this->autoexpunge) return imap_expunge(self::$instance[$this->mailbox]); return $rc; } /** Add a new mail in the current folder. The content must be a string containing all the mail (header and body). If the content is invalid, the directory listing can provide erroneous data */ public function mailAdd ($content) { if ($this->mailbox === null) throw new Exception ("IMAP server not connected", 500); $folderUTF7 = mb_convert_encoding ($this->curDir, "UTF7-IMAP","UTF-8"); $rc = imap_append (self::$instance[$this->mailbox], $this->mailbox.$folderUTF7, $content); $errors = imap_errors(); if ($rc === FALSE) throw new Exception ("Error when saving the mail in folder : ". implode (" ", $errors), 500); return true; } ///////////////// /// QUOTA /// ///////////////// /** Return the quota used by the user in Mo */ public function getQuota () { if ($this->mailbox === null) throw new Exception ("IMAP server not connected", 500); $quota = @imap_get_quotaroot (self::$instance[$this->mailbox], "INBOX"); imap_errors(); if (! isset ($quota["STORAGE"])) return array (); return array_map (function ($n) {return intval ($n/1000);}, $quota["STORAGE"]); } ///////////////// /// FLAGS /// ///////////////// /** Set the flags of the msgno. If msgno is an array, the flags will be write on the list of mails. The others flags of the email are not modified. The flags must be an array containing : \Seen Message has been read \Answered Message has been answered \Flagged Message is "flagged" for urgent/special attention \Deleted Message is "deleted" for removal by later EXPUNGE \Draft Message has not completed composition (marked as a draft). */ public function setFlag ($msgno, $flags) { if ($this->mailbox === null) throw new Exception ("IMAP server not connected", 500); $this->changeFolder ($this->curDir); if (is_array ($msgno)) $msgno = implode (",", $msgno); $rc = @imap_setflag_full (self::$instance[$this->mailbox], $msgno, implode (" ", $flags)); imap_errors(); if ($rc === FALSE) throw new Exception ("Can't define the flags", 500); return true; } /** Unset the flags of the msgno. If msgno is an array, the flags will be write on the list of mails. The others flags of the email are not modified. The flags must be an array containing : \Seen Message has been read \Answered Message has been answered \Flagged Message is "flagged" for urgent/special attention \Deleted Message is "deleted" for removal by later EXPUNGE \Draft Message has not completed composition (marked as a draft). */ public function unsetFlag ($msgno, $flags) { if ($this->mailbox === null) throw new Exception ("IMAP server not connected", 500); $this->changeFolder ($this->curDir); if (is_array ($msgno)) $msgno = implode (",", $msgno); $rc = @imap_clearflag_full (self::$instance[$this->mailbox], $msgno, implode (" ", $flags)); imap_errors(); if ($rc === FALSE) throw new Exception ("Can't define the flags", 500); return true; } /** Mark mail(s) as read. If msgno is an array, a list of mails will be modified. If msgno is an integer, only one mail will be modified */ public function markMailAsRead ($msgno) { if ($this->mailbox === null) throw new Exception ("IMAP server not connected", 500); $this->changeFolder ($this->curDir); if (is_array ($msgno)) $msgno = implode (",", $msgno); $rc = @imap_setflag_full (self::$instance[$this->mailbox], $msgno, "\\Seen"); imap_errors(); if ($rc === FALSE) throw new Exception ("Can't mark mail as read", 500); return true; } /** Mark mail(s) as unread. If msgno is an array, a list of mails will be modified. If msgno is an integer, only one mail will be modified */ public function markMailAsUnread ($msgno) { if ($this->mailbox === null) throw new Exception ("IMAP server not connected", 500); $this->changeFolder ($this->curDir); if (is_array ($msgno)) $msgno = implode (",", $msgno); $rc = @imap_clearflag_full (self::$instance[$this->mailbox], $msgno, "\\Seen"); imap_errors(); if ($rc === FALSE) throw new Exception ("Can't mark mail as read", 500); return true; } }