diff --git a/MailLibrary/Connection.php b/MailLibrary/Connection.php index 190cb18..22ac3d4 100644 --- a/MailLibrary/Connection.php +++ b/MailLibrary/Connection.php @@ -7,8 +7,9 @@ use greeny\MailLibrary\Drivers\IDriver; -class Connection { - /** @var \greeny\MailLibrary\Drivers\IDriver */ +class Connection +{ + /** @var IDriver */ protected $driver; /** @var bool */ diff --git a/MailLibrary/Drivers/IDriver.php b/MailLibrary/Drivers/IDriver.php index 6ee2345..4b8fb3b 100644 --- a/MailLibrary/Drivers/IDriver.php +++ b/MailLibrary/Drivers/IDriver.php @@ -11,133 +11,145 @@ use greeny\MailLibrary\Structures\IStructure; interface IDriver { - /** - * Connects to server - * @throws DriverException - */ - function connect(); - - /** - * Flushes changes to server - * @throws DriverException - */ - function flush(); - - /** - * Gets all mailboxes - * @return array of string - * @throws DriverException - */ - function getMailboxes(); - - /** - * Creates new mailbox - * @param string $name - * @throws DriverException - */ - function createMailbox($name); - - /** - * Renames mailbox - * @param string $from - * @param string $to - * @throws DriverException - */ - function renameMailbox($from, $to); - - /** - * Deletes mailbox - * @param string $name - * @throws DriverException - */ - function deleteMailbox($name); - - /** - * Switches current mailbox - * @param string $name - * @throws DriverException - */ - function switchMailbox($name); - - /** - * Finds UIDs of mails by filter - * - * @param array $filters - * @param int $limit - * @param int $offset - * @param int $orderBy - * @param string $orderType - * @return array of UIDs - */ - function getMailIds(array $filters, $limit = 0, $offset = 0, $orderBy = Mail::ORDER_DATE, $orderType = 'ASC'); - - /** - * Checks if filter is applicable for this driver - * @param string $key - * @param mixed $value - * @throws DriverException - */ - function checkFilter($key, $value = NULL); - - /** - * Gets mail headers - * @param int $mailId - * @return array of name => value - */ - function getHeaders($mailId); - - /** - * Creates structure for mail - * @param int $mailId - * @param Mailbox $mailbox - * @return IStructure - */ - function getStructure($mailId, Mailbox $mailbox); - - /** - * Gets part of body - * @param int $mailId - * @param array $data - * @return string - */ - function getBody($mailId, array $data); - - /** - * Gets flags for mail - * @param int $mailId - * @return array - */ - function getFlags($mailId); - - /** - * Sets one flag for mail - * @param int $mailId - * @param string $flag - * @param bool $value - * @throws DriverException - */ - function setFlag($mailId, $flag, $value); - - /** - * Copies mail to another mailbox - * @param int $mailId - * @param string $toMailbox - * @throws DriverException - */ - function copyMail($mailId, $toMailbox); - - /** - * Moves mail to another mailbox - * @param int $mailId - * @param string $toMailbox - * @throws DriverException - */ - function moveMail($mailId, $toMailbox); - - /** - * Deletes mail - * @param int $mailId - * @throws DriverException - */ - function deleteMail($mailId); -} \ No newline at end of file + /** + * Connects to server + * @throws DriverException + */ + function connect(); + + /** + * Flushes changes to server + * @throws DriverException + */ + function flush(); + + /** + * Gets all mailboxes + * @return array of string + * @throws DriverException + */ + function getMailboxes(); + + /** + * Creates new mailbox + * @param string $name + * @throws DriverException + */ + function createMailbox($name); + + /** + * Renames mailbox + * @param string $from + * @param string $to + * @throws DriverException + */ + function renameMailbox($from, $to); + + /** + * Deletes mailbox + * @param string $name + * @throws DriverException + */ + function deleteMailbox($name); + + /** + * Switches current mailbox + * @param string $name + * @throws DriverException + */ + function switchMailbox($name); + + /** + * Finds UIDs of mails by filter + * + * @param array $filters + * @param int $limit + * @param int $offset + * @param int $orderBy + * @param string $orderType + * @return array of UIDs + */ + function getMailIds(array $filters, $limit = 0, $offset = 0, $orderBy = Mail::ORDER_DATE, $orderType = 'ASC'); + + /** + * Checks if filter is applicable for this driver + * @param string $key + * @param mixed $value + * @throws DriverException + */ + function checkFilter($key, $value = NULL); + + /** + * Gets mail headers + * @param int $mailId + * @return array of name => value + */ + function getHeaders($mailId); + + /** + * Creates structure for mail + * @param int $mailId + * @param Mailbox $mailbox + * @return IStructure + */ + function getStructure($mailId, Mailbox $mailbox); + + /** + * Gets part of body + * @param int $mailId + * @param array $data + * @return string + */ + function getBody($mailId, array $data); + + /** + * Gets flags for mail + * @param int $mailId + * @return array + */ + function getFlags($mailId); + + /** + * Sets one flag for mail + * @param int $mailId + * @param string $flag + * @param bool $value + * @throws DriverException + */ + function setFlag($mailId, $flag, $value); + + /** + * Copies mail to another mailbox + * @param int $mailId + * @param string $toMailbox + * @throws DriverException + */ + function copyMail($mailId, $toMailbox); + + /** + * Moves mail to another mailbox + * @param int $mailId + * @param string $toMailbox + * @throws DriverException + */ + function moveMail($mailId, $toMailbox); + + /** + * Deletes mail + * @param int $mailId + * @throws DriverException + */ + function deleteMail($mailId); + + /** + * @param $mailId + * @return array + */ + function getOverview($mailId); + + /** + * @param $str + * @return string + */ + function mimeDecode($str); +} \ No newline at end of file diff --git a/MailLibrary/Drivers/ImapDriver.php b/MailLibrary/Drivers/ImapDriver.php index eb059e7..64520de 100644 --- a/MailLibrary/Drivers/ImapDriver.php +++ b/MailLibrary/Drivers/ImapDriver.php @@ -15,455 +15,494 @@ class ImapDriver implements IDriver { - /** @var string */ - protected $username; - - /** @var string */ - protected $password; - - /** @var resource */ - protected $resource; - - /** @var string */ - protected $server; - - /** @var string */ - protected $currentMailbox = NULL; - - protected static $filterTable = array( - Mail::ANSWERED => '%bANSWERED', - Mail::BCC => 'BCC "%s"', - Mail::BEFORE => 'BEFORE "%d"', - Mail::BODY => 'BODY "%s"', - Mail::CC => 'CC "%s"', - Mail::DELETED => '%bDELETED', - Mail::FLAGGED => '%bFLAGGED', - Mail::FROM => 'FROM "%s"', - Mail::KEYWORD => 'KEYWORD "%s"', - Mail::NEW_MESSAGES => 'NEW', - Mail::NOT_KEYWORD => 'UNKEYWORD "%s"', - Mail::OLD_MESSAGES => 'OLD', - Mail::ON => 'ON "%d"', - Mail::RECENT => 'RECENT', - Mail::SEEN => '%bSEEN', - Mail::SINCE => 'SINCE "%d"', - Mail::SUBJECT => 'SUBJECT "%s"', - Mail::TEXT => 'TEXT "%s"', - Mail::TO => 'TO "%s"', - ); - - protected static $contactHeaders = array( - 'to', - 'from', - 'cc', - 'bcc', - ); - - public function __construct($username, $password, $host, $port = 993, $ssl = TRUE) - { - $ssl = $ssl ? '/ssl' : '/novalidate-cert'; - $this->server = '{'.$host.':'.$port.'/imap'.$ssl.'}'; - $this->username = $username; - $this->password = $password; - } - - /** - * Connects to server - * - * @throws DriverException if connecting fails - */ - public function connect() - { - if(!$this->resource = @imap_open($this->server, $this->username, $this->password, CL_EXPUNGE)) { // @ - to allow throwing exceptions - throw new DriverException("Cannot connect to IMAP server: " . imap_last_error()); - } - } - - /** - * Flushes changes to server - * - * @throws DriverException if flushing fails - */ - public function flush() - { - imap_expunge($this->resource); - } - - /** - * Gets all mailboxes - * - * @return array of string - * @throws DriverException - */ - public function getMailboxes() - { - $mailboxes = array(); - $foo = imap_list($this->resource, $this->server, '*'); - if(!$foo) { - throw new DriverException("Cannot get mailboxes from server: " . imap_last_error()); - } - foreach($foo as $mailbox) { - $mailboxes[] = mb_convert_encoding(str_replace($this->server, '', $mailbox), 'UTF8', 'UTF7-IMAP'); - } - return $mailboxes; - } - - /** - * Creates new mailbox - * - * @param string $name - * @throws DriverException - */ - public function createMailbox($name) - { - if(!imap_createmailbox($this->resource, $this->server . $name)) { - throw new DriverException("Cannot create mailbox '$name': " . imap_last_error()); - } - } - - /** - * Renames mailbox - * - * @param string $from - * @param string $to - * @throws DriverException - */ - public function renameMailbox($from, $to) - { - if(!imap_renamemailbox($this->resource, $this->server . $from, $this->server . $to)) { - throw new DriverException("Cannot rename mailbox from '$from' to '$to': " . imap_last_error()); - } - } - - /** - * Deletes mailbox - * - * @param string $name - * @throws DriverException - */ - public function deleteMailbox($name) - { - if(!imap_deletemailbox($this->resource, $this->server . $name)) { - throw new DriverException("Cannot delete mailbox '$name': " . imap_last_error()); - } - } - - /** - * Switches current mailbox - * - * @param string $name - * @throws DriverException - */ - public function switchMailbox($name) - { - if($name !== $this->currentMailbox) { - $this->flush(); - if(!imap_reopen($this->resource, $this->server . $name)) { - throw new DriverException("Cannot switch to mailbox '$name': " . imap_last_error()); - } - $this->currentMailbox = $name; - } - } - - /** - * Finds UIDs of mails by filter - * - * @param array $filters - * @param int $limit - * @param int $offset - * @param int $orderBy - * @param string $orderType - * @throws \greeny\MailLibrary\DriverException - * @return array of UIDs - */ - public function getMailIds(array $filters, $limit = 0, $offset = 0, $orderBy = Mail::ORDER_DATE, $orderType = 'ASC') - { - $filter = $this->buildFilters($filters); - - $orderType = $orderType === 'ASC' ? 1 : 0; - - if(!is_array($ids = imap_sort($this->resource, $orderBy, $orderType, SE_UID | SE_NOPREFETCH, $filter, 'UTF-8'))) { - throw new DriverException("Cannot get mails: " . imap_last_error()); - } - - return $limit === 0 ? $ids : array_slice($ids, $offset, $limit); - } - - /** - * Checks if filter is applicable for this driver - * - * @param string $key - * @param mixed $value - * @throws DriverException - */ - public function checkFilter($key, $value = NULL) { - if(!in_array($key, array_keys(self::$filterTable))) { - throw new DriverException("Invalid filter key '$key'."); - } - $filtered = self::$filterTable[$key]; - if(strpos($filtered, '%s') !== FALSE) { - if(!is_string($value)) { - throw new DriverException("Invalid value type for filter '$key', expected string, got ".gettype($value)."."); - } - } else if(strpos($filtered, '%d') !== FALSE) { - if(!($value instanceof DateTime) && !is_int($value) && !strtotime($value)) { - throw new DriverException("Invalid value type for filter '$key', expected DateTime or timestamp, or textual representation of date, got ".gettype($value)."."); - } - } else if(strpos($filtered, '%b') !== FALSE) { - if(!is_bool($value)) { - throw new DriverException("Invalid value type for filter '$key', expected bool, got ".gettype($value)."."); - } - } else if($value !== NULL) { - throw new DriverException("Cannot assign value to filter '$key'."); - } - } - - /** - * Gets mail headers - * - * @param int $mailId - * @return array of name => value - */ - public function getHeaders($mailId) - { - $raw = imap_fetchheader($this->resource, $mailId, FT_UID); - $lines = explode("\n", $raw); - $headers = array(); - $lastHeader = NULL; - foreach($lines as $line) { - if(mb_substr($line, 0, 1, 'UTF-8') === " ") { - $headers[$lastHeader] .= $line; - } else { - $parts = explode(':', $line); - $name = $parts[0]; - unset($parts[0]); - - $headers[$name] = implode(':', $parts); - $lastHeader = $name; - } - } - - foreach($headers as $key => $header) { - if(trim($key) === '') { - unset($headers[$key]); - continue; - } - if(strtolower($key) === 'subject') { - $decoded = imap_mime_header_decode($header); - - $text = ''; - foreach($decoded as $part) { - if($part->charset !== 'UTF-8' && $part->charset !== 'default') { - $text .= mb_convert_encoding($part->text, 'UTF-8', $part->charset); - } else { - $text .= $part->text; - } - } - - $headers[$key] = trim($text); - } else if(in_array(strtolower($key), self::$contactHeaders)) { - $contacts = imap_rfc822_parse_adrlist(imap_utf8(trim($header)), 'UNKNOWN_HOST'); - $list = new ContactList(); - foreach($contacts as $contact) { - $list->addContact( - isset($contact->mailbox) ? $contact->mailbox : NULL, - isset($contact->host) ? $contact->host : NULL, - isset($contact->personal) ? $contact->personal : NULL, - isset($contact->adl) ? $contact->adl : NULL - ); - } - $list->build(); - $headers[$key] = $list; - } else { - $headers[$key] = trim(imap_utf8($header)); - } - } - return $headers; - } - - /** - * Creates structure for mail - * - * @param int $mailId - * @param Mailbox $mailbox - * @return IStructure - */ - public function getStructure($mailId, Mailbox $mailbox) - { - return new ImapStructure($this, imap_fetchstructure($this->resource, $mailId, FT_UID), $mailId, $mailbox); - } - - /** - * Gets part of body - * - * @param int $mailId - * @param array $data - * @return string - */ - public function getBody($mailId, array $data) - { - $body = array(); - foreach($data as $part) { - $data = ($part['id'] == 0) ? imap_body($this->resource, $mailId, FT_UID | FT_PEEK) : imap_fetchbody($this->resource, $mailId, $part['id'], FT_UID | FT_PEEK); - $encoding = $part['encoding']; - if($encoding === ImapStructure::ENCODING_BASE64) { - $data = base64_decode($data); - } else if($encoding === ImapStructure::ENCODING_QUOTED_PRINTABLE) { - $data = quoted_printable_decode($data); - } - - $body[] = $data; - } - return implode('\n\n', $body); - } - - /** - * Gets flags for mail - * - * @param $mailId - * @return array - */ - public function getFlags($mailId) - { - $data = imap_fetch_overview($this->resource, (string)$mailId, FT_UID); - reset($data); - $data = current($data); - $return = array( - Mail::FLAG_ANSWERED => FALSE, - Mail::FLAG_DELETED => FALSE, - Mail::FLAG_DRAFT => FALSE, - Mail::FLAG_FLAGGED => FALSE, - Mail::FLAG_SEEN => FALSE, - ); - if($data->answered) { - $return[Mail::FLAG_ANSWERED] = TRUE; - } - if($data->deleted) { - $return[Mail::FLAG_DELETED] = TRUE; - } - if($data->draft) { - $return[Mail::FLAG_DRAFT] = TRUE; - } - if($data->flagged) { - $return[Mail::FLAG_FLAGGED] = TRUE; - } - if($data->seen) { - $return[Mail::FLAG_SEEN] = TRUE; - } - return $return; - } - - /** - * Sets one flag for mail - * - * @param int $mailId - * @param string $flag - * @param bool $value - * @throws DriverException - */ - public function setFlag($mailId, $flag, $value) - { - if($value) { - if(!imap_setflag_full($this->resource, $mailId, $flag, ST_UID)) { - throw new DriverException("Cannot set flag '$flag': ".imap_last_error()); - } - } else { - if(!imap_clearflag_full($this->resource, $mailId, $flag, ST_UID)) { - throw new DriverException("Cannot unset flag '$flag': ".imap_last_error()); - } - } - } - - /** - * Copies mail to another mailbox - * @param int $mailId - * @param string $toMailbox - * @throws DriverException - */ - public function copyMail($mailId, $toMailbox) { - if(!imap_mail_copy($this->resource, $mailId, $this->server . $this->encodeMailboxName($toMailbox), CP_UID)) { - throw new DriverException("Cannot copy mail to mailbox '$toMailbox': ".imap_last_error()); - } - } - - /** - * Moves mail to another mailbox - * @param int $mailId - * @param string $toMailbox - * @throws DriverException - */ - public function moveMail($mailId, $toMailbox) { - if(!imap_mail_move($this->resource, $mailId, $this->server . $this->encodeMailboxName($toMailbox), CP_UID)) { - throw new DriverException("Cannot copy mail to mailbox '$toMailbox': ".imap_last_error()); - } - } - - /** - * Deletes mail - * @param int $mailId - * @throws DriverException - */ - public function deleteMail($mailId) { - if(!imap_delete($this->resource, $mailId, FT_UID)) { - throw new DriverException("Cannot delete mail: ".imap_last_error()); - } - } - - /** - * Builds filter string from filters - * - * @param array $filters - * @return string - */ - protected function buildFilters(array $filters) - { - $return = array(); - foreach($filters as $filter) { - $key = self::$filterTable[$filter['key']]; - $value = $filter['value']; - - if(strpos($key, '%s') !== FALSE) { - $data = str_replace('%s', str_replace('"', '', (string)$value), $key); - } else if(strpos($key, '%d') !== FALSE) { - if($value instanceof DateTime) { - $timestamp = $value->getTimestamp(); - } else if(is_string($value)) { - $timestamp = strtotime($value) ?: Time(); - } else { - $timestamp = (int)$value; - } - $data = str_replace('%d', date("d M Y", $timestamp), $key); - } else if(strpos($key, '%b') !== FALSE) { - $data = str_replace('%b', ((bool)$value ? '' : 'UN'), $key); - } else { - $data = $key; - } - $return[] = $data; - } - return implode(' ', $return); - } - - /** - * Builds list from ids array - * - * @param array $ids - * @return string - */ - protected function buildIdList(array $ids) - { - sort($ids); - return implode(',', $ids); - } - - /** - * Converts mailbox name encoding as defined in IMAP RFC 2060. - * - * @param $name - * @return string - */ - protected function encodeMailboxName($name) - { - return mb_convert_encoding($name, 'UTF7-IMAP', 'UTF-8'); - } - + /** @var string */ + protected $username; + + /** @var string */ + protected $password; + + /** @var resource */ + protected $resource; + + /** @var string */ + protected $server; + + /** @var string */ + protected $currentMailbox = NULL; + + protected static $filterTable = array( + Mail::ANSWERED => '%bANSWERED', + Mail::BCC => 'BCC "%s"', + Mail::BEFORE => 'BEFORE "%d"', + Mail::BODY => 'BODY "%s"', + Mail::CC => 'CC "%s"', + Mail::DELETED => '%bDELETED', + Mail::FLAGGED => '%bFLAGGED', + Mail::FROM => 'FROM "%s"', + Mail::KEYWORD => 'KEYWORD "%s"', + Mail::NEW_MESSAGES => 'NEW', + Mail::NOT_KEYWORD => 'UNKEYWORD "%s"', + Mail::OLD_MESSAGES => 'OLD', + Mail::ON => 'ON "%d"', + Mail::RECENT => 'RECENT', + Mail::SEEN => '%bSEEN', + Mail::SINCE => 'SINCE "%d"', + Mail::SUBJECT => 'SUBJECT "%s"', + Mail::TEXT => 'TEXT "%s"', + Mail::TO => 'TO "%s"', + ); + + protected static $contactHeaders = array( + 'to', + 'from', + 'cc', + 'bcc', + ); + + public function __construct($username, $password, $host, $port = 993, $ssl = TRUE) + { + $ssl = $ssl ? '/ssl' : '/novalidate-cert'; + $this->server = '{'.$host.':'.$port.'/imap'.$ssl.'}'; + $this->username = $username; + $this->password = $password; + } + + /** + * Connects to server + * + * @throws DriverException if connecting fails + */ + public function connect() + { + if(!$this->resource = @imap_open($this->server, $this->username, $this->password, CL_EXPUNGE)) { // @ - to allow throwing exceptions + throw new DriverException("Cannot connect to IMAP server: " . imap_last_error()); + } + } + + /** + * Flushes changes to server + * + * @throws DriverException if flushing fails + */ + public function flush() + { + imap_expunge($this->resource); + } + + /** + * Gets all mailboxes + * + * @return array of string + * @throws DriverException + */ + public function getMailboxes() + { + $mailboxes = array(); + $foo = imap_list($this->resource, $this->server, '*'); + if(!$foo) { + throw new DriverException("Cannot get mailboxes from server: " . imap_last_error()); + } + foreach($foo as $mailbox) { + $mailboxes[] = mb_convert_encoding(str_replace($this->server, '', $mailbox), 'UTF8', 'UTF7-IMAP'); + } + return $mailboxes; + } + + /** + * Creates new mailbox + * + * @param string $name + * @throws DriverException + */ + public function createMailbox($name) + { + if(!imap_createmailbox($this->resource, $this->server . $name)) { + throw new DriverException("Cannot create mailbox '$name': " . imap_last_error()); + } + } + + /** + * Renames mailbox + * + * @param string $from + * @param string $to + * @throws DriverException + */ + public function renameMailbox($from, $to) + { + if(!imap_renamemailbox($this->resource, $this->server . $from, $this->server . $to)) { + throw new DriverException("Cannot rename mailbox from '$from' to '$to': " . imap_last_error()); + } + } + + /** + * Deletes mailbox + * + * @param string $name + * @throws DriverException + */ + public function deleteMailbox($name) + { + if(!imap_deletemailbox($this->resource, $this->server . $name)) { + throw new DriverException("Cannot delete mailbox '$name': " . imap_last_error()); + } + } + + /** + * Switches current mailbox + * + * @param string $name + * @throws DriverException + */ + public function switchMailbox($name) + { + if($name !== $this->currentMailbox) { + $this->flush(); + if(!imap_reopen($this->resource, $this->server . $name)) { + throw new DriverException("Cannot switch to mailbox '$name': " . imap_last_error()); + } + $this->currentMailbox = $name; + } + } + + /** + * Finds UIDs of mails by filter + * + * @param array $filters + * @param int $limit + * @param int $offset + * @param int $orderBy + * @param string $orderType + * @throws DriverException + * @return array of UIDs + */ + public function getMailIds(array $filters, $limit = 0, $offset = 0, $orderBy = Mail::ORDER_DATE, $orderType = 'ASC') + { + $filter = $this->buildFilters($filters); + + $orderType = $orderType === 'ASC' ? 1 : 0; + + if(!is_array($ids = imap_sort($this->resource, $orderBy, $orderType, SE_UID | SE_NOPREFETCH, $filter, 'UTF-8'))) { + throw new DriverException("Cannot get mails: " . imap_last_error()); + } + + return $limit === 0 ? $ids : array_slice($ids, $offset, $limit); + } + + /** + * Checks if filter is applicable for this driver + * + * @param string $key + * @param mixed $value + * @throws DriverException + */ + public function checkFilter($key, $value = NULL) { + if(!in_array($key, array_keys(self::$filterTable))) { + throw new DriverException("Invalid filter key '$key'."); + } + $filtered = self::$filterTable[$key]; + if(strpos($filtered, '%s') !== FALSE) { + if(!is_string($value)) { + throw new DriverException("Invalid value type for filter '$key', expected string, got ".gettype($value)."."); + } + } else if(strpos($filtered, '%d') !== FALSE) { + if(!($value instanceof DateTime) && !is_int($value) && !strtotime($value)) { + throw new DriverException("Invalid value type for filter '$key', expected DateTime or timestamp, or textual representation of date, got ".gettype($value)."."); + } + } else if(strpos($filtered, '%b') !== FALSE) { + if(!is_bool($value)) { + throw new DriverException("Invalid value type for filter '$key', expected bool, got ".gettype($value)."."); + } + } else if($value !== NULL) { + throw new DriverException("Cannot assign value to filter '$key'."); + } + } + + /** + * Gets mail headers + * + * @param int $mailId + * @return array of name => value + */ + public function getHeaders($mailId) + { + $raw = imap_fetchheader($this->resource, $mailId, FT_UID); + preg_match_all('/([-\w]+):\s(.+)(?=\s[-\w]+:|$)/sU', $raw, $matches); + $headers = array(); + + foreach($matches[1] as $i => $key) { + $headers[$key] = trim($matches[2][$i]); + } + + foreach($headers as $key => $header) { + + if(trim($key) === '') { + unset($headers[$key]); + continue; + } + if(strtolower($key) === 'subject') { + $headers[$key] = trim($this->mimeDecode($header)); + } else if(in_array(strtolower($key), self::$contactHeaders)) { + $contacts = imap_rfc822_parse_adrlist(trim($this->mimeDecode($header)), 'UNKNOWN_HOST'); + $list = new ContactList(); + foreach($contacts as $contact) { + $list->addContact( + isset($contact->mailbox) ? $contact->mailbox : NULL, + isset($contact->host) ? $contact->host : NULL, + isset($contact->personal) ? $contact->personal : NULL, + isset($contact->adl) ? $contact->adl : NULL + ); + } + $list->build(); + $headers[$key] = $list; + } else { + $headers[$key] = trim(imap_utf8($header)); + } + } + return $headers; + } + + public function mimeDecode($str) + { + // Remove spaces between two encoded lines. + $str = preg_replace('/(=\?[^?]+\?[A-Z]\?[^?]+\?=)\s+(?=(=\?[^?]+\?[A-Z]\?[^?]+\?=))/', '$1', $str); + + // =?UTF-8?Q?=D0=9A=D0=BE=D0=BC=D0=B0=D0=BD=D0=B4=D0=B0?= + return preg_replace_callback('/=\?[^?]+\?[A-z]\?[^?]+\?=/', array($this, 'mimeDecodeReplaceCallback'), $str); + } + + /** + * Creates structure for mail + * + * @param int $mailId + * @param Mailbox $mailbox + * @return IStructure + */ + public function getStructure($mailId, Mailbox $mailbox) + { + return new ImapStructure($this, imap_fetchstructure($this->resource, $mailId, FT_UID), $mailId, $mailbox); + } + + /** + * Gets part of body + * + * @param int $mailId + * @param array $data + * @return string + */ + public function getBody($mailId, array $data) + { + $body = array(); + foreach($data as $part) { + $data = ($part['id'] == 0) + ? imap_body($this->resource, $mailId, FT_UID | FT_PEEK) + : imap_fetchbody($this->resource, $mailId, $part['id'], FT_UID | FT_PEEK); + + switch($part['encoding']) + { + case(1): + if(preg_match_all('/=[\dA-Z]{2}=/', $data) > 10) { + $data = quoted_printable_decode($data); + } + break; + + case(2): + $data = imap_binary($data); + break; + + case(ImapStructure::ENCODING_BASE64): + $data = base64_decode($data); + break; + + case(ImapStructure::ENCODING_QUOTED_PRINTABLE): + $data = quoted_printable_decode($data); + break; + } + + $body[] = $data; + } + return implode('\n\n', $body); + } + + /** + * Gets flags for mail + * + * @param $mailId + * @return array + */ + public function getFlags($mailId) + { + $data = imap_fetch_overview($this->resource, (string)$mailId, FT_UID); + reset($data); + $data = current($data); + $return = array( + Mail::FLAG_ANSWERED => FALSE, + Mail::FLAG_DELETED => FALSE, + Mail::FLAG_DRAFT => FALSE, + Mail::FLAG_FLAGGED => FALSE, + Mail::FLAG_SEEN => FALSE, + ); + if($data->answered) { + $return[Mail::FLAG_ANSWERED] = TRUE; + } + if($data->deleted) { + $return[Mail::FLAG_DELETED] = TRUE; + } + if($data->draft) { + $return[Mail::FLAG_DRAFT] = TRUE; + } + if($data->flagged) { + $return[Mail::FLAG_FLAGGED] = TRUE; + } + if($data->seen) { + $return[Mail::FLAG_SEEN] = TRUE; + } + return $return; + } + + /** + * Sets one flag for mail + * + * @param int $mailId + * @param string $flag + * @param bool $value + * @throws DriverException + */ + public function setFlag($mailId, $flag, $value) + { + if($value) { + if(!imap_setflag_full($this->resource, $mailId, $flag, ST_UID)) { + throw new DriverException("Cannot set flag '$flag': ".imap_last_error()); + } + } else { + if(!imap_clearflag_full($this->resource, $mailId, $flag, ST_UID)) { + throw new DriverException("Cannot unset flag '$flag': ".imap_last_error()); + } + } + } + + /** + * Copies mail to another mailbox + * @param int $mailId + * @param string $toMailbox + * @throws DriverException + */ + public function copyMail($mailId, $toMailbox) { + if(!imap_mail_copy($this->resource, $mailId, $this->server . $this->encodeMailboxName($toMailbox), CP_UID)) { + throw new DriverException("Cannot copy mail to mailbox '$toMailbox': ".imap_last_error()); + } + } + + /** + * Moves mail to another mailbox + * @param int $mailId + * @param string $toMailbox + * @throws DriverException + */ + public function moveMail($mailId, $toMailbox) { + if(!imap_mail_move($this->resource, $mailId, $this->server . $this->encodeMailboxName($toMailbox), CP_UID)) { + throw new DriverException("Cannot copy mail to mailbox '$toMailbox': ".imap_last_error()); + } + } + + /** + * Deletes mail + * @param int $mailId + * @throws DriverException + */ + public function deleteMail($mailId) { + if(!imap_delete($this->resource, $mailId, FT_UID)) { + throw new DriverException("Cannot delete mail: ".imap_last_error()); + } + } + + public function getOverview($mailId) + { + return current(imap_fetch_overview($this->resource, $mailId, FT_UID)); + } + + public function __destruct() + { + imap_errors(); + } + + + /** + * Builds filter string from filters + * + * @param array $filters + * @return string + */ + protected function buildFilters(array $filters) + { + $return = array(); + foreach($filters as $filter) { + $key = self::$filterTable[$filter['key']]; + $value = $filter['value']; + + if(strpos($key, '%s') !== FALSE) { + $data = str_replace('%s', str_replace('"', '', (string)$value), $key); + } else if(strpos($key, '%d') !== FALSE) { + if($value instanceof DateTime) { + $timestamp = $value->getTimestamp(); + } else if(is_string($value)) { + $timestamp = strtotime($value) ?: Time(); + } else { + $timestamp = (int)$value; + } + $data = str_replace('%d', date("d M Y", $timestamp), $key); + } else if(strpos($key, '%b') !== FALSE) { + $data = str_replace('%b', ((bool)$value ? '' : 'UN'), $key); + } else { + $data = $key; + } + $return[] = $data; + } + return implode(' ', $return); + } + + /** + * Builds list from ids array + * + * @param array $ids + * @return string + */ + protected function buildIdList(array $ids) + { + sort($ids); + return implode(',', $ids); + } + + /** + * Converts mailbox name encoding as defined in IMAP RFC 2060. + * + * @param $name + * @return string + */ + protected function encodeMailboxName($name) + { + return mb_convert_encoding($name, 'UTF7-IMAP', 'UTF-8'); + } + + /** + * @param array $m + * @return string + */ + private function mimeDecodeReplaceCallback($m) + { + $str = $m[0]; + + if($mime = strval(@iconv_mime_decode($str, 1, 'utf-8'))) { + return $mime; + } + + foreach(imap_mime_header_decode($str) as $header) { + if(strtolower($header->charset) != 'utf-8') { + $header->text = mb_convert_encoding($header->text, 'utf-8', $header->charset); + } + $mime .= $header->text; + } + + return $mime; + } } diff --git a/MailLibrary/Mail.php b/MailLibrary/Mail.php index 0431948..50e4d68 100644 --- a/MailLibrary/Mail.php +++ b/MailLibrary/Mail.php @@ -5,257 +5,287 @@ namespace greeny\MailLibrary; +use greeny\MailLibrary\Structures\IStructure; + class Mail { - const ANSWERED = 'ANSWERED'; - const BCC = 'BCC'; - const BEFORE = 'BEFORE'; - const BODY = 'BODY'; - const CC = 'CC'; - const DELETED = 'DELETED'; - const FLAGGED = 'FLAGGED'; - const FROM = 'FROM'; - const KEYWORD = 'KEYWORD'; - const NEW_MESSAGES = 'NEW'; - const NOT_KEYWORD = 'UNKEYWORD'; - const OLD_MESSAGES = 'OLD'; - const ON = 'ON'; - const RECENT = 'RECENT'; - const SEEN = 'SEEN'; - const SINCE = 'SINCE'; - const SUBJECT = 'SUBJECT'; - const TEXT = 'TEXT'; - const TO = 'TO'; - - const FLAG_ANSWERED = "\\ANSWERED"; - const FLAG_DELETED = "\\DELETED"; - const FLAG_DRAFT = "\\DRAFT"; - const FLAG_FLAGGED = "\\FLAGGED"; - const FLAG_SEEN = "\\SEEN"; - - const ORDER_DATE = SORTARRIVAL; - const ORDER_FROM = SORTFROM; - const ORDER_SUBJECT = SORTSUBJECT; - const ORDER_TO = SORTTO; - const ORDER_CC = SORTCC; - const ORDER_SIZE = SORTSIZE; - - /** @var \greeny\MailLibrary\Connection */ - protected $connection; - - /** @var \greeny\MailLibrary\Mailbox */ - protected $mailbox; - - /** @var int */ - protected $id; - - /** @var array */ - protected $headers = NULL; - - /** @var \greeny\MailLibrary\Structures\IStructure */ - protected $structure = NULL; - - /** @var array */ - protected $flags = NULL; - - /** - * @param Connection $connection - * @param Mailbox $mailbox - * @param int $id - */ - public function __construct(Connection $connection, Mailbox $mailbox, $id) - { - $this->connection = $connection; - $this->mailbox = $mailbox; - $this->id = $id; - } - - /** - * Header checker - * - * @param $name - * @return bool - */ - public function __isset($name) - { - $this->headers !== NULL || $this->initializeHeaders(); - return isset($this->headers[$this->formatHeaderName($name)]); - } - - /** - * Header getter - * - * @param string $name - * @return mixed - */ - public function __get($name) - { - return $this->getHeader($name); - } - - /** - * @return int - */ - public function getId() - { - return $this->id; - } - - /** - * @return Mailbox - */ - public function getMailbox() - { - return $this->mailbox; - } - - /** - * @return string[] - */ - public function getHeaders() - { - $this->headers !== NULL || $this->initializeHeaders(); - return $this->headers; - } - - /** - * @param string $name - * @return string - */ - public function getHeader($name) - { - $this->headers !== NULL || $this->initializeHeaders(); - return $this->headers[$this->formatHeaderName($name)]; - } - - /** - * @return Contact|null - */ - public function getSender() { - $from = $this->getHeader('from'); - if($from) { - $contacts = $from->getContactsObjects(); - return (count($contacts) ? $contacts[0] : NULL); - } else { - return NULL; - } - } - - /** - * @return string - */ - public function getBody() - { - $this->structure !== NULL || $this->initializeStructure(); - return $this->structure->getBody(); - } - - /** - * @return string - */ - public function getHtmlBody() - { - $this->structure !== NULL || $this->initializeStructure(); - return $this->structure->getHtmlBody(); - } - - /** - * @return string - */ - public function getTextBody() - { - $this->structure !== NULL || $this->initializeStructure(); - return $this->structure->getTextBody(); - } - - /** - * @return Attachment[] - */ - public function getAttachments() - { - $this->structure !== NULL || $this->initializeStructure(); - return $this->structure->getAttachments(); - } - - /** - * @return array - */ - public function getFlags() - { - $this->flags !== NULL || $this->initializeFlags(); - return $this->flags; - } - - public function setFlags(array $flags, $autoFlush = FALSE) - { - $this->connection->getDriver()->switchMailbox($this->mailbox->getName()); - foreach(array( - Mail::FLAG_ANSWERED, - Mail::FLAG_DELETED, - Mail::FLAG_DELETED, - Mail::FLAG_FLAGGED, - Mail::FLAG_SEEN, - ) as $flag) { - if(isset($flags[$flag])) { - $this->connection->getDriver()->setFlag($this->id, $flag, $flags[$flag]); - } - } - if($autoFlush) { - $this->connection->getDriver()->flush(); - } - } - - public function move($toMailbox) - { - $this->connection->getDriver()->switchMailbox($this->mailbox->getName()); - $this->connection->getDriver()->moveMail($this->id, $toMailbox); - } - - public function copy($toMailbox) - { - $this->connection->getDriver()->switchMailbox($this->mailbox->getName()); - $this->connection->getDriver()->copyMail($this->id, $toMailbox); - } - - public function delete() - { - $this->connection->getDriver()->switchMailbox($this->mailbox->getName()); - $this->connection->getDriver()->deleteMail($this->id); - } - - /** - * Initializes headers - */ - protected function initializeHeaders() - { - $this->headers = array(); - $this->connection->getDriver()->switchMailbox($this->mailbox->getName()); - foreach($this->connection->getDriver()->getHeaders($this->id) as $key => $value) { - $this->headers[$this->formatHeaderName($key)] = $value; - } - } - - protected function initializeStructure() - { - $this->connection->getDriver()->switchMailbox($this->mailbox->getName()); - $this->structure = $this->connection->getDriver()->getStructure($this->id, $this->mailbox); - } - - protected function initializeFlags() - { - $this->connection->getDriver()->switchMailbox($this->mailbox->getName()); - $this->flags = $this->connection->getDriver()->getFlags($this->id); - } - - /** - * Formats header name (X-Received-From => xReceivedFrom) - * - * @param string $name - * @return string - */ - protected function formatHeaderName($name) - { - return lcfirst(preg_replace_callback("~-.~", function($matches){ - return ucfirst(substr($matches[0], 1)); - }, $name)); - } + const ANSWERED = 'ANSWERED'; + const BCC = 'BCC'; + const BEFORE = 'BEFORE'; + const BODY = 'BODY'; + const CC = 'CC'; + const DELETED = 'DELETED'; + const FLAGGED = 'FLAGGED'; + const FROM = 'FROM'; + const KEYWORD = 'KEYWORD'; + const NEW_MESSAGES = 'NEW'; + const NOT_KEYWORD = 'UNKEYWORD'; + const OLD_MESSAGES = 'OLD'; + const ON = 'ON'; + const RECENT = 'RECENT'; + const SEEN = 'SEEN'; + const SINCE = 'SINCE'; + const SUBJECT = 'SUBJECT'; + const TEXT = 'TEXT'; + const TO = 'TO'; + + const FLAG_ANSWERED = "\\ANSWERED"; + const FLAG_DELETED = "\\DELETED"; + const FLAG_DRAFT = "\\DRAFT"; + const FLAG_FLAGGED = "\\FLAGGED"; + const FLAG_SEEN = "\\SEEN"; + + const ORDER_DATE = SORTARRIVAL; + const ORDER_FROM = SORTFROM; + const ORDER_SUBJECT = SORTSUBJECT; + const ORDER_TO = SORTTO; + const ORDER_CC = SORTCC; + const ORDER_SIZE = SORTSIZE; + + /** @var Connection */ + protected $connection; + + /** @var Mailbox */ + protected $mailbox; + + /** @var int */ + protected $id; + + /** @var array */ + protected $headers = NULL; + + /** @var IStructure */ + protected $structure = NULL; + + /** @var array */ + protected $flags = NULL; + + /** + * @var array + */ + private $overview; + + /** + * @param Connection $connection + * @param Mailbox $mailbox + * @param int $id + */ + public function __construct(Connection $connection, Mailbox $mailbox, $id) + { + $this->connection = $connection; + $this->mailbox = $mailbox; + $this->id = $id; + + $this->overview = $this->connection->getDriver()->getOverview($id); + } + + /** + * @return \DateTimeImmutable|false + */ + public function getDate() + { + return (new \DateTimeImmutable())->setTimestamp($this->overview->udate); + } + + /** + * @return string|null + */ + public function getSubject() + { + if(isset($this->overview->subject)) { + return $this->connection->getDriver()->mimeDecode($this->overview->subject); + } + + return null; + } + + /** + * Header checker + * + * @param $name + * @return bool + */ + public function __isset($name) + { + $this->headers !== NULL || $this->initializeHeaders(); + return isset($this->headers[$this->formatHeaderName($name)]); + } + + /** + * Header getter + * + * @param string $name + * @return mixed + */ + public function __get($name) + { + return $this->getHeader($name); + } + + /** + * @return int + */ + public function getId() + { + return $this->id; + } + + /** + * @return Mailbox + */ + public function getMailbox() + { + return $this->mailbox; + } + + /** + * @return string[] + */ + public function getHeaders() + { + $this->headers !== NULL || $this->initializeHeaders(); + return $this->headers; + } + + /** + * @param string $name + * @return string + */ + public function getHeader($name) + { + $this->headers !== NULL || $this->initializeHeaders(); + return $this->headers[$this->formatHeaderName($name)]; + } + + /** + * @return Contact|null + */ + public function getSender() { + /** @var ContactList $from */ + $from = $this->getHeader('from'); + if($from) { + $contacts = $from->getContactsObjects(); + return (count($contacts) ? $contacts[0] : NULL); + } else { + return NULL; + } + } + + /** + * @return string + */ + public function getBody() + { + $this->structure !== NULL || $this->initializeStructure(); + return $this->structure->getBody(); + } + + /** + * @return string + */ + public function getHtmlBody() + { + $this->structure !== NULL || $this->initializeStructure(); + return $this->structure->getHtmlBody(); + } + + /** + * @return string + */ + public function getTextBody() + { + $this->structure !== NULL || $this->initializeStructure(); + return $this->structure->getTextBody(); + } + + /** + * @return Attachment[] + */ + public function getAttachments() + { + $this->structure !== NULL || $this->initializeStructure(); + return $this->structure->getAttachments(); + } + + /** + * @return array + */ + public function getFlags() + { + $this->flags !== NULL || $this->initializeFlags(); + return $this->flags; + } + + public function setFlags(array $flags, $autoFlush = FALSE) + { + $this->connection->getDriver()->switchMailbox($this->mailbox->getName()); + foreach(array( + Mail::FLAG_ANSWERED, + Mail::FLAG_DELETED, + Mail::FLAG_DELETED, + Mail::FLAG_FLAGGED, + Mail::FLAG_SEEN, + ) as $flag) { + if(isset($flags[$flag])) { + $this->connection->getDriver()->setFlag($this->id, $flag, $flags[$flag]); + } + } + if($autoFlush) { + $this->connection->getDriver()->flush(); + } + } + + public function move($toMailbox) + { + $this->connection->getDriver()->switchMailbox($this->mailbox->getName()); + $this->connection->getDriver()->moveMail($this->id, $toMailbox); + } + + public function copy($toMailbox) + { + $this->connection->getDriver()->switchMailbox($this->mailbox->getName()); + $this->connection->getDriver()->copyMail($this->id, $toMailbox); + } + + public function delete() + { + $this->connection->getDriver()->switchMailbox($this->mailbox->getName()); + $this->connection->getDriver()->deleteMail($this->id); + } + + /** + * Initializes headers + */ + protected function initializeHeaders() + { + $this->headers = array(); + $this->connection->getDriver()->switchMailbox($this->mailbox->getName()); + foreach($this->connection->getDriver()->getHeaders($this->id) as $key => $value) { + $this->headers[$this->formatHeaderName($key)] = $value; + } + } + + protected function initializeStructure() + { + $this->connection->getDriver()->switchMailbox($this->mailbox->getName()); + $this->structure = $this->connection->getDriver()->getStructure($this->id, $this->mailbox); + } + + protected function initializeFlags() + { + $this->connection->getDriver()->switchMailbox($this->mailbox->getName()); + $this->flags = $this->connection->getDriver()->getFlags($this->id); + } + + /** + * Formats header name (X-Received-From => xReceivedFrom) + * + * @param string $name + * @return string + */ + protected function formatHeaderName($name) + { + return lcfirst(preg_replace_callback("~-.~", function($matches){ + return ucfirst(substr($matches[0], 1)); + }, $name)); + } } diff --git a/MailLibrary/Structures/ImapStructure.php b/MailLibrary/Structures/ImapStructure.php index ec9ed2e..8795cc2 100644 --- a/MailLibrary/Structures/ImapStructure.php +++ b/MailLibrary/Structures/ImapStructure.php @@ -152,12 +152,13 @@ protected function addStructurePart($structure, $partId) } if(isset($parameters['filename']) || isset($parameters['name'])) { - $this->attachmentsIds[] = array( - 'id' => $partId, - 'encoding' => $encoding, - 'name' => isset($parameters['filename']) ? $parameters['filename'] : $parameters['name'], - 'type' => self::$typeTable[$type]. '/' . $subtype, - ); + $name = isset($parameters['filename']) ? $parameters['filename'] : $parameters['name']; + $this->attachmentsIds[] = array( + 'id' => $partId, + 'encoding' => $encoding, + 'name' => iconv_mime_decode($name, 0, 'utf-8'), + 'type' => self::$typeTable[$type]. '/' . $subtype, + ); } else if($type === self::TYPE_TEXT) { if($subtype === 'HTML') { $this->htmlBodyIds[] = array('id' => $partId, 'encoding' => $encoding); diff --git a/composer.json b/composer.json index 23370ab..ae18057 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "php-mail-client/client", + "name": "aivazoff/php-mail-client", "description": "Full featured PHP mail client. Create, edit, delete, move and set flags to messages with ease! Drivers for IMAP4 and Microsoft Exchange Web Services available.", "license": "MIT", "authors": [ @@ -7,6 +7,10 @@ "name": "Tomáš Blatný", "email": "blatny.tomas@seznam.cz", "homepage": "http://tomasblatny.eu" + }, { + "name": "Arthur Aivazov", + "email": "arthur.aivazoff@gmail.com", + "homepage": "https://github.com/aivazoff" } ], "autoload": { @@ -15,7 +19,10 @@ ] }, "require": { - "php": ">= 5.4.0" + "php": ">= 5.4.0", + "ext-imap": "*", + "ext-mbstring": "*", + "ext-iconv": "*" }, "require-dev": { "nette/tester": "@dev"