739 lines
22 KiB
PHP
Executable File
739 lines
22 KiB
PHP
Executable File
<?php
|
|
|
|
/*
|
|
* This file is part of the Fetch package.
|
|
*
|
|
* (c) Robert Hafner <tedivm@tedivm.com>
|
|
*
|
|
* For the full copyright and license information, please view the LICENSE
|
|
* file that was distributed with this source code.
|
|
*/
|
|
|
|
namespace Fetch;
|
|
use Exception;
|
|
|
|
/**
|
|
* This library is a wrapper around the Imap library functions included in php. This class represents a single email
|
|
* message as retrieved from the Imap.
|
|
*
|
|
* @package Fetch
|
|
* @author Robert Hafner <tedivm@tedivm.com>
|
|
*/
|
|
class Message
|
|
{
|
|
/**
|
|
* This is the connection/mailbox class that the email came from.
|
|
*
|
|
* @var Server
|
|
*/
|
|
protected $imapConnection;
|
|
|
|
/**
|
|
* This is the unique identifier for the message. This corresponds to the imap "uid", which we use instead of the
|
|
* sequence number.
|
|
*
|
|
* @var int
|
|
*/
|
|
protected $uid;
|
|
|
|
/**
|
|
* This is a reference to the Imap stream generated by 'imap_open'.
|
|
*
|
|
* @var resource
|
|
*/
|
|
protected $imapStream;
|
|
|
|
/**
|
|
* This as an object which contains header information for the message.
|
|
*
|
|
* @var \stdClass
|
|
*/
|
|
protected $headers;
|
|
|
|
/**
|
|
* This is an object which contains various status messages and other information about the message.
|
|
*
|
|
* @var \stdClass
|
|
*/
|
|
protected $messageOverview;
|
|
|
|
/**
|
|
* This is an object which contains information about the structure of the message body.
|
|
*
|
|
* @var \stdClass
|
|
*/
|
|
protected $structure;
|
|
|
|
/**
|
|
* This is an array with the index being imap flags and the value being a boolean specifying whether that flag is
|
|
* set or not.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $status = array();
|
|
|
|
/**
|
|
* This is an array of the various imap flags that can be set.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected static $flagTypes = array('recent', 'flagged', 'answered', 'deleted', 'seen', 'draft');
|
|
|
|
/**
|
|
* This holds the plantext email message.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $plaintextMessage;
|
|
|
|
/**
|
|
* This holds the html version of the email.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $htmlMessage;
|
|
|
|
/**
|
|
* This is the date the email was sent.
|
|
*
|
|
* @var int
|
|
*/
|
|
protected $date;
|
|
|
|
/**
|
|
* This is the subject of the email.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $subject;
|
|
|
|
/**
|
|
* This is the size of the email.
|
|
*
|
|
* @var int
|
|
*/
|
|
protected $size;
|
|
|
|
/**
|
|
* This is an array containing information about the address the email came from.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $from;
|
|
|
|
/**
|
|
* This is an array containing information about the address the email was sent from.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $sender;
|
|
|
|
/**
|
|
* This is an array of arrays that contains information about the addresses the email was sent to.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $to;
|
|
|
|
/**
|
|
* This is an array of arrays that contains information about the addresses the email was cc'd to.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $cc;
|
|
|
|
/**
|
|
* This is an array of arrays that contains information about the addresses the email was bcc'd to.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $bcc;
|
|
|
|
/**
|
|
* This is an array of arrays that contain information about the addresses that should receive replies to the email.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $replyTo;
|
|
|
|
/**
|
|
* This is an array of ImapAttachments retrieved from the message.
|
|
*
|
|
* @var Attachment[]
|
|
*/
|
|
protected $attachments = array();
|
|
|
|
/**
|
|
* Contains the mailbox that the message resides in.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $mailbox;
|
|
|
|
/**
|
|
* This value defines the encoding we want the email message to use.
|
|
*
|
|
* @var string
|
|
*/
|
|
public static $charset = 'UTF-8';
|
|
|
|
/**
|
|
* This value defines the flag set for encoding if the mb_convert_encoding
|
|
* function can't be found, and in this case iconv encoding will be used.
|
|
*
|
|
* @var string
|
|
*/
|
|
public static $charsetFlag = '//TRANSLIT';
|
|
|
|
/**
|
|
* These constants can be used to easily access available flags
|
|
*/
|
|
const FLAG_RECENT = 'recent';
|
|
const FLAG_FLAGGED = 'flagged';
|
|
const FLAG_ANSWERED = 'answered';
|
|
const FLAG_DELETED = 'deleted';
|
|
const FLAG_SEEN = 'seen';
|
|
const FLAG_DRAFT = 'draft';
|
|
|
|
/**
|
|
* This constructor takes in the uid for the message and the Imap class representing the mailbox the
|
|
* message should be opened from. This constructor should generally not be called directly, but rather retrieved
|
|
* through the apprioriate Imap functions.
|
|
*
|
|
* @param int $messageUniqueId
|
|
* @param Server $mailbox
|
|
*/
|
|
public function __construct($messageUniqueId, Server $connection)
|
|
{
|
|
$this->imapConnection = $connection;
|
|
$this->mailbox = $connection->getMailBox();
|
|
$this->uid = $messageUniqueId;
|
|
$this->imapStream = $this->imapConnection->getImapStream();
|
|
if($this->loadMessage() !== true)
|
|
throw new \RuntimeException('Message with ID ' . $messageUniqueId . ' not found.');
|
|
}
|
|
|
|
/**
|
|
* This function is called when the message class is loaded. It loads general information about the message from the
|
|
* imap server.
|
|
*
|
|
*/
|
|
protected function loadMessage()
|
|
{
|
|
|
|
/* First load the message overview information */
|
|
|
|
if(!is_object($messageOverview = $this->getOverview()))
|
|
|
|
return false;
|
|
|
|
$this->subject = isset($messageOverview->subject) ? $messageOverview->subject : null;
|
|
$this->date = strtotime($messageOverview->date);
|
|
$this->size = $messageOverview->size;
|
|
|
|
foreach (self::$flagTypes as $flag)
|
|
$this->status[$flag] = ($messageOverview->$flag == 1);
|
|
|
|
/* Next load in all of the header information */
|
|
|
|
$headers = $this->getHeaders();
|
|
|
|
if (isset($headers->to))
|
|
$this->to = $this->processAddressObject($headers->to);
|
|
|
|
if (isset($headers->cc))
|
|
$this->cc = $this->processAddressObject($headers->cc);
|
|
|
|
if (isset($headers->bcc))
|
|
$this->bcc = $this->processAddressObject($headers->bcc);
|
|
|
|
if (isset($headers->sender))
|
|
$this->sender = $this->processAddressObject($headers->sender);
|
|
|
|
$this->from = isset($headers->from) ? $this->processAddressObject($headers->from) : array('');
|
|
$this->replyTo = isset($headers->reply_to) ? $this->processAddressObject($headers->reply_to) : $this->from;
|
|
|
|
/* Finally load the structure itself */
|
|
|
|
$structure = $this->getStructure();
|
|
|
|
if (!isset($structure->parts)) {
|
|
// not multipart
|
|
$this->processStructure($structure);
|
|
} else {
|
|
// multipart
|
|
foreach ($structure->parts as $id => $part)
|
|
$this->processStructure($part, $id + 1);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* This function returns an object containing information about the message. This output is similar to that over the
|
|
* imap_fetch_overview function, only instead of an array of message overviews only a single result is returned. The
|
|
* results are only retrieved from the server once unless passed true as a parameter.
|
|
*
|
|
* @param bool $forceReload
|
|
* @return \stdClass
|
|
*/
|
|
public function getOverview($forceReload = false)
|
|
{
|
|
if ($forceReload || !isset($this->messageOverview)) {
|
|
// returns an array, and since we just want one message we can grab the only result
|
|
$results = imap_fetch_overview($this->imapStream, $this->uid, FT_UID);
|
|
$this->messageOverview = array_shift($results);
|
|
if ( ! isset($this->messageOverview->date)) {
|
|
$this->messageOverview->date = null;
|
|
}
|
|
}
|
|
|
|
return $this->messageOverview;
|
|
}
|
|
|
|
/**
|
|
* This function returns an object containing the headers of the message. This is done by taking the raw headers
|
|
* and running them through the imap_rfc822_parse_headers function. The results are only retrieved from the server
|
|
* once unless passed true as a parameter.
|
|
*
|
|
* @param bool $forceReload
|
|
* @return \stdClass
|
|
*/
|
|
public function getHeaders($forceReload = false)
|
|
{
|
|
if ($forceReload || !isset($this->headers)) {
|
|
// raw headers (since imap_headerinfo doesn't use the unique id)
|
|
$rawHeaders = imap_fetchheader($this->imapStream, $this->uid, FT_UID);
|
|
|
|
// convert raw header string into a usable object
|
|
$headerObject = imap_rfc822_parse_headers($rawHeaders);
|
|
|
|
// to keep this object as close as possible to the original header object we add the udate property
|
|
if (isset($headerObject->date)) {
|
|
$headerObject->udate = strtotime($headerObject->date);
|
|
} else {
|
|
$headerObject->date = null;
|
|
$headerObject->udate = null;
|
|
}
|
|
|
|
$this->headers = $headerObject;
|
|
}
|
|
|
|
return $this->headers;
|
|
}
|
|
|
|
/**
|
|
* This function returns an object containing the structure of the message body. This is the same object thats
|
|
* returned by imap_fetchstructure. The results are only retrieved from the server once unless passed true as a
|
|
* parameter.
|
|
*
|
|
* @param bool $forceReload
|
|
* @return \stdClass
|
|
*/
|
|
public function getStructure($forceReload = false)
|
|
{
|
|
if ($forceReload || !isset($this->structure)) {
|
|
$this->structure = imap_fetchstructure($this->imapStream, $this->uid, FT_UID);
|
|
}
|
|
|
|
return $this->structure;
|
|
}
|
|
|
|
/**
|
|
* This function returns the message body of the email. By default it returns the plaintext version. If a plaintext
|
|
* version is requested but not present, the html version is stripped of tags and returned. If the opposite occurs,
|
|
* the plaintext version is given some html formatting and returned. If neither are present the return value will be
|
|
* false.
|
|
*
|
|
* @param bool $html Pass true to receive an html response.
|
|
* @return string|bool Returns false if no body is present.
|
|
*/
|
|
public function getMessageBody($html = false)
|
|
{
|
|
if ($html) {
|
|
if (!isset($this->htmlMessage) && isset($this->plaintextMessage)) {
|
|
$output = nl2br($this->plaintextMessage);
|
|
|
|
return $output;
|
|
|
|
} elseif (isset($this->htmlMessage)) {
|
|
return $this->htmlMessage;
|
|
}
|
|
} else {
|
|
if (!isset($this->plaintextMessage) && isset($this->htmlMessage)) {
|
|
$output = preg_replace('/\<br(\s*)?\/?\>/i', PHP_EOL, trim($this->htmlMessage) );
|
|
$output = strip_tags($output);
|
|
|
|
return $output;
|
|
} elseif (isset($this->plaintextMessage)) {
|
|
return $this->plaintextMessage;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* This function returns either an array of email addresses and names or, optionally, a string that can be used in
|
|
* mail headers.
|
|
*
|
|
* @param string $type Should be 'to', 'cc', 'bcc', 'from', 'sender', or 'reply-to'.
|
|
* @param bool $asString
|
|
* @return array|string|bool
|
|
*/
|
|
public function getAddresses($type, $asString = false)
|
|
{
|
|
$type = ( $type == 'reply-to' ) ? 'replyTo' : $type;
|
|
$addressTypes = array('to', 'cc', 'bcc', 'from', 'sender', 'replyTo');
|
|
|
|
if (!in_array($type, $addressTypes) || !isset($this->$type) || count($this->$type) < 1)
|
|
return false;
|
|
|
|
if (!$asString) {
|
|
if ($type == 'from')
|
|
return $this->from[0];
|
|
elseif ($type == 'sender')
|
|
return $this->sender[0];
|
|
|
|
return $this->$type;
|
|
} else {
|
|
$outputString = '';
|
|
foreach ($this->$type as $address) {
|
|
if (isset($set))
|
|
$outputString .= ', ';
|
|
if (!isset($set))
|
|
$set = true;
|
|
|
|
$outputString .= isset($address['name']) ?
|
|
$address['name'] . ' <' . $address['address'] . '>'
|
|
: $address['address'];
|
|
}
|
|
|
|
return $outputString;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This function returns the date, as a timestamp, of when the email was sent.
|
|
*
|
|
* @return int
|
|
*/
|
|
public function getDate()
|
|
{
|
|
return isset($this->date) ? $this->date : false;
|
|
}
|
|
|
|
/**
|
|
* This returns the subject of the message.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getSubject()
|
|
{
|
|
return isset($this->subject) ? $this->subject : null;
|
|
}
|
|
|
|
/**
|
|
* This function marks a message for deletion. It is important to note that the message will not be deleted form the
|
|
* mailbox until the Imap->expunge it run.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function delete()
|
|
{
|
|
return imap_delete($this->imapStream, $this->uid, FT_UID);
|
|
}
|
|
|
|
/**
|
|
* This function returns Imap this message came from.
|
|
*
|
|
* @return Server
|
|
*/
|
|
public function getImapBox()
|
|
{
|
|
return $this->imapConnection;
|
|
}
|
|
|
|
/**
|
|
* This function takes in a structure and identifier and processes that part of the message. If that portion of the
|
|
* message has its own subparts, those are recursively processed using this function.
|
|
*
|
|
* @param \stdClass $structure
|
|
* @param string $partIdentifier
|
|
*/
|
|
protected function processStructure($structure, $partIdentifier = null)
|
|
{
|
|
$parameters = self::getParametersFromStructure($structure);
|
|
|
|
if ((isset($parameters['name']) || isset($parameters['filename']))
|
|
|| (isset($structure->subtype) && strtolower($structure->subtype) == 'rfc822')
|
|
) {
|
|
$attachment = new Attachment($this, $structure, $partIdentifier);
|
|
$this->attachments[] = $attachment;
|
|
} elseif ($structure->type == 0 || $structure->type == 1) {
|
|
$messageBody = isset($partIdentifier) ?
|
|
imap_fetchbody($this->imapStream, $this->uid, $partIdentifier, FT_UID)
|
|
: imap_body($this->imapStream, $this->uid, FT_UID);
|
|
|
|
$messageBody = self::decode($messageBody, $structure->encoding);
|
|
|
|
if (!empty($parameters['charset']) && $parameters['charset'] !== self::$charset) {
|
|
$mb_converted = false;
|
|
if (function_exists('mb_convert_encoding')) {
|
|
try {
|
|
$messageBody = mb_convert_encoding($messageBody, self::$charset, $parameters['charset']);
|
|
$mb_converted = true;
|
|
} catch (Exception $e) {
|
|
// @TODO Handle exception
|
|
}
|
|
}
|
|
if (!$mb_converted) {
|
|
try {
|
|
$messageBody = iconv($parameters['charset'], self::$charset . self::$charsetFlag, $messageBody);
|
|
} catch (Exception $e) {
|
|
// @TODO Handle exception
|
|
}
|
|
}
|
|
}
|
|
|
|
if (strtolower($structure->subtype) === 'plain' || ($structure->type == 1 && strtolower($structure->subtype) !== 'alternative')) {
|
|
if (isset($this->plaintextMessage)) {
|
|
$this->plaintextMessage .= PHP_EOL . PHP_EOL;
|
|
} else {
|
|
$this->plaintextMessage = '';
|
|
}
|
|
|
|
$this->plaintextMessage .= trim($messageBody);
|
|
} elseif (strtolower($structure->subtype) === 'html') {
|
|
if (isset($this->htmlMessage)) {
|
|
$this->htmlMessage .= '<br><br>';
|
|
} else {
|
|
$this->htmlMessage = '';
|
|
}
|
|
|
|
$this->htmlMessage .= $messageBody;
|
|
}
|
|
}
|
|
|
|
if (isset($structure->parts)) { // multipart: iterate through each part
|
|
|
|
foreach ($structure->parts as $partIndex => $part) {
|
|
$partId = $partIndex + 1;
|
|
|
|
if (isset($partIdentifier))
|
|
$partId = $partIdentifier . '.' . $partId;
|
|
|
|
$this->processStructure($part, $partId);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This function takes in the message data and encoding type and returns the decoded data.
|
|
*
|
|
* @param string $data
|
|
* @param int|string $encoding
|
|
* @return string
|
|
*/
|
|
public static function decode($data, $encoding)
|
|
{
|
|
if (!is_numeric($encoding)) {
|
|
$encoding = strtolower($encoding);
|
|
}
|
|
|
|
switch (true) {
|
|
case $encoding === 'quoted-printable':
|
|
case $encoding === 4:
|
|
return quoted_printable_decode($data);
|
|
|
|
case $encoding === 'base64':
|
|
case $encoding === 3:
|
|
return base64_decode($data);
|
|
|
|
default:
|
|
return $data;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This function returns the body type that an imap integer maps to.
|
|
*
|
|
* @param int $id
|
|
* @return string
|
|
*/
|
|
public static function typeIdToString($id)
|
|
{
|
|
switch ($id) {
|
|
case 0:
|
|
return 'text';
|
|
|
|
case 1:
|
|
return 'multipart';
|
|
|
|
case 2:
|
|
return 'message';
|
|
|
|
case 3:
|
|
return 'application';
|
|
|
|
case 4:
|
|
return 'audio';
|
|
|
|
case 5:
|
|
return 'image';
|
|
|
|
case 6:
|
|
return 'video';
|
|
|
|
default:
|
|
case 7:
|
|
return 'other';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Takes in a section structure and returns its parameters as an associative array.
|
|
*
|
|
* @param \stdClass $structure
|
|
* @return array
|
|
*/
|
|
public static function getParametersFromStructure($structure)
|
|
{
|
|
$parameters = array();
|
|
if (isset($structure->parameters))
|
|
foreach ($structure->parameters as $parameter)
|
|
$parameters[strtolower($parameter->attribute)] = $parameter->value;
|
|
|
|
if (isset($structure->dparameters))
|
|
foreach ($structure->dparameters as $parameter)
|
|
$parameters[strtolower($parameter->attribute)] = $parameter->value;
|
|
|
|
return $parameters;
|
|
}
|
|
|
|
/**
|
|
* This function takes in an array of the address objects generated by the message headers and turns them into an
|
|
* associative array.
|
|
*
|
|
* @param array $addresses
|
|
* @return array
|
|
*/
|
|
protected function processAddressObject($addresses)
|
|
{
|
|
$outputAddresses = array();
|
|
if (is_array($addresses))
|
|
foreach ($addresses as $address) {
|
|
$currentAddress = array();
|
|
$currentAddress['address'] = $address->mailbox . '@' . $address->host;
|
|
if (isset($address->personal))
|
|
$currentAddress['name'] = $address->personal;
|
|
$outputAddresses[] = $currentAddress;
|
|
}
|
|
|
|
return $outputAddresses;
|
|
}
|
|
|
|
/**
|
|
* This function returns the unique id that identifies the message on the server.
|
|
*
|
|
* @return int
|
|
*/
|
|
public function getUid()
|
|
{
|
|
return $this->uid;
|
|
}
|
|
|
|
/**
|
|
* This function returns the attachments a message contains. If a filename is passed then just that ImapAttachment
|
|
* is returned, unless
|
|
*
|
|
* @param null|string $filename
|
|
* @return array|bool|Attachment[]
|
|
*/
|
|
public function getAttachments($filename = null)
|
|
{
|
|
if (!isset($this->attachments) || count($this->attachments) < 1)
|
|
return false;
|
|
|
|
if (!isset($filename))
|
|
return $this->attachments;
|
|
|
|
$results = array();
|
|
foreach ($this->attachments as $attachment) {
|
|
if ($attachment->getFileName() == $filename)
|
|
$results[] = $attachment;
|
|
}
|
|
|
|
switch (count($results)) {
|
|
case 0:
|
|
return false;
|
|
|
|
case 1:
|
|
return array_shift($results);
|
|
|
|
default:
|
|
return $results;
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This function checks to see if an imap flag is set on the email message.
|
|
*
|
|
* @param string $flag Recent, Flagged, Answered, Deleted, Seen, Draft
|
|
* @return bool
|
|
*/
|
|
public function checkFlag($flag = 'flagged')
|
|
{
|
|
return (isset($this->status[$flag]) && $this->status[$flag] === true);
|
|
}
|
|
|
|
/**
|
|
* This function is used to enable or disable a flag on the imap message.
|
|
*
|
|
* @param string $flag Flagged, Answered, Deleted, Seen, Draft
|
|
* @param bool $enable
|
|
* @throws \InvalidArgumentException
|
|
* @return bool
|
|
*/
|
|
public function setFlag($flag, $enable = true)
|
|
{
|
|
if (!in_array($flag, self::$flagTypes) || $flag == 'recent')
|
|
throw new \InvalidArgumentException('Unable to set invalid flag "' . $flag . '"');
|
|
|
|
$imapifiedFlag = '\\' . ucfirst($flag);
|
|
|
|
if ($enable === true) {
|
|
$this->status[$flag] = true;
|
|
|
|
return imap_setflag_full($this->imapStream, $this->uid, $imapifiedFlag, ST_UID);
|
|
} else {
|
|
unset($this->status[$flag]);
|
|
|
|
return imap_clearflag_full($this->imapStream, $this->uid, $imapifiedFlag, ST_UID);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This function is used to move a mail to the given mailbox.
|
|
*
|
|
* @param $mailbox
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function moveToMailBox($mailbox)
|
|
{
|
|
$currentBox = $this->imapConnection->getMailBox();
|
|
$this->imapConnection->setMailBox($this->mailbox);
|
|
|
|
$returnValue = imap_mail_copy($this->imapStream, $this->uid, $mailbox, CP_UID | CP_MOVE);
|
|
imap_expunge($this->imapStream);
|
|
|
|
$this->mailbox = $mailbox;
|
|
|
|
$this->imapConnection->setMailBox($currentBox);
|
|
|
|
return $returnValue;
|
|
}
|
|
}
|