diff --git a/README.md b/README.md
index 2b6f273d..a4b84cc7 100644
--- a/README.md
+++ b/README.md
@@ -66,6 +66,7 @@
* MariaDB
* PHPMailer
* HTML Purifier
+ * PHP Mime Mail Parser
* CSS
* Bootstrap
@@ -88,7 +89,7 @@ ITFlow is self-hosted. There is a full installation guide in the [docs](https://
1. Install a LAMP stack (Linux, Apache, MariaDB, PHP)
```sh
- sudo apt install git apache2 php libapache2-mod-php php-intl php-imap php-mysqli php-curl mariadb-server
+ sudo apt install git apache2 php libapache2-mod-php php-intl php-imap php-mailparse php-mysqli php-curl mariadb-server
```
2. Clone the repo
```sh
diff --git a/cron_ticket_email_parser.php b/cron_ticket_email_parser.php
index 73ff869d..67881aa2 100644
--- a/cron_ticket_email_parser.php
+++ b/cron_ticket_email_parser.php
@@ -10,58 +10,73 @@ TODO:
- Process unregistered contacts/clients into an inbox to allow a ticket to be created/ignored
- Better handle replying to closed tickets
- Support for authenticating with OAuth
- - Documentation
- Separate Mailbox Account for tickets 2022-12-14 - JQ
- - Properly parse base64 encoded emails (if an Outlook user sends a smiley everything breaks :( - https://electrictoolbox.com/php-imap-message-parts/)
Relate PRs to https://github.com/itflow-org/itflow/issues/225 & https://forum.itflow.org/d/11-road-map & https://forum.itflow.org/d/31-tickets-from-email
*/
// Get ITFlow config & helper functions
-include_once("config.php");
-include_once("functions.php");
+require_once("config.php");
+require_once("functions.php");
// Get settings for the "default" company
$company_id = 1;
$session_company_id = 1;
-include_once("get_settings.php");
+require_once("get_settings.php");
// Check setting enabled
if ($config_ticket_email_parse == 0) {
- exit("Feature is not enabled - see Settings > Ticketing > Email-to-ticket parsing");
+ exit("Email Parser: Feature is not enabled - check Settings > Ticketing > Email-to-ticket parsing. See https://wiki.itflow.org/doku.php?id=wiki:ticket_email_parse -- Quitting..");
}
-// Check IMAP function exists
+// Check IMAP extension works/installed
if (!function_exists('imap_open')) {
- echo "PHP IMAP extension is not installed, quitting..";
- exit();
+ exit("Email Parser: PHP IMAP extension is not installed. See https://wiki.itflow.org/doku.php?id=wiki:ticket_email_parse -- Quitting..");
}
+// Check mailparse extension works/installed
+if (!function_exists('mailparse_msg_parse_file')) {
+ exit("Email Parser: PHP mailparse extension is not installed. See https://wiki.itflow.org/doku.php?id=wiki:ticket_email_parse -- Quitting..");
+}
+
+// PHP Mail Parser
+require_once("plugins/php-mime-mail-parser/src/Contracts/CharsetManager.php");
+require_once("plugins/php-mime-mail-parser/src/Contracts/Middleware.php");
+require_once("plugins/php-mime-mail-parser/src/Attachment.php");
+require_once("plugins/php-mime-mail-parser/src/Charset.php");
+require_once("plugins/php-mime-mail-parser/src/Exception.php");
+require_once("plugins/php-mime-mail-parser/src/Middleware.php");
+require_once("plugins/php-mime-mail-parser/src/MiddlewareStack.php");
+require_once("plugins/php-mime-mail-parser/src/MimePart.php");
+require_once("plugins/php-mime-mail-parser/src/Parser.php");
+
// Function to raise a new ticket for a given contact and email them confirmation (if configured)
-function createTicket($contact_id, $contact_name, $contact_email, $client_id, $company_id, $date, $subject, $message) {
+function addTicket($contact_id, $contact_name, $contact_email, $client_id, $company_id, $date, $subject, $message)
+{
// Access global variables
- global $mysqli, $config_ticket_next_number, $config_ticket_prefix, $config_ticket_client_general_notifications, $config_base_url, $config_ticket_from_name, $config_ticket_from_email, $config_smtp_host, $config_smtp_port, $config_smtp_encryption, $config_smtp_username, $config_smtp_password;
+ global $mysqli, $config_ticket_prefix, $config_ticket_client_general_notifications, $config_base_url, $config_ticket_from_name, $config_ticket_from_email, $config_smtp_host, $config_smtp_port, $config_smtp_encryption, $config_smtp_username, $config_smtp_password;
+
+ // Get the next Ticket Number and add 1 for the new ticket number
+ $ticket_number_sql = mysqli_fetch_array(mysqli_query($mysqli, "SELECT config_ticket_next_number FROM settings WHERE company_id = $company_id"));
+ $ticket_number = intval($ticket_number_sql['config_ticket_next_number']);
+ $new_config_ticket_next_number = $ticket_number + 1;
+ mysqli_query($mysqli, "UPDATE settings SET config_ticket_next_number = $new_config_ticket_next_number WHERE company_id = $company_id");
// Prep ticket details
$message = nl2br(htmlentities(strip_tags($message)));
- $message = trim(mysqli_real_escape_string($mysqli,"Email from: $contact_email at $date:-
$message"));
+ $message = trim(mysqli_real_escape_string($mysqli, "Email from: $contact_email at $date:-
$message"));
- // Get the next Ticket Number and add 1 for the new ticket number
- $ticket_number = $config_ticket_next_number;
- $new_config_ticket_next_number = $config_ticket_next_number + 1;
- mysqli_query($mysqli,"UPDATE settings SET config_ticket_next_number = $new_config_ticket_next_number WHERE company_id = $company_id");
-
- mysqli_query($mysqli,"INSERT INTO tickets SET ticket_prefix = '$config_ticket_prefix', ticket_number = $ticket_number, ticket_subject = '$subject', ticket_details = '$message', ticket_priority = 'Low', ticket_status = 'Open', ticket_created_at = NOW(), ticket_created_by = '0', ticket_contact_id = $contact_id, ticket_client_id = $client_id, company_id = $company_id");
+ mysqli_query($mysqli, "INSERT INTO tickets SET ticket_prefix = '$config_ticket_prefix', ticket_number = $ticket_number, ticket_subject = '$subject', ticket_details = '$message', ticket_priority = 'Low', ticket_status = 'Open', ticket_created_at = NOW(), ticket_created_by = '0', ticket_contact_id = $contact_id, ticket_client_id = $client_id, company_id = $company_id");
$id = mysqli_insert_id($mysqli);
// Logging
echo "Created new ticket.
";
- mysqli_query($mysqli,"INSERT INTO logs SET log_type = 'Ticket', log_action = 'Create', log_description = 'Email parser: Client contact $contact_email created ticket $config_ticket_prefix$ticket_number ($subject)', log_created_at = NOW(), log_client_id = $client_id, company_id = $company_id");
+ mysqli_query($mysqli, "INSERT INTO logs SET log_type = 'Ticket', log_action = 'Create', log_description = 'Email parser: Client contact $contact_email created ticket $config_ticket_prefix$ticket_number ($subject) ($id)', log_created_at = NOW(), log_client_id = $client_id, company_id = $company_id");
// Get company name & phone
- $sql = mysqli_query($mysqli,"SELECT company_name, company_phone FROM companies WHERE company_id = $company_id");
+ $sql = mysqli_query($mysqli, "SELECT company_name, company_phone FROM companies WHERE company_id = $company_id");
$row = mysqli_fetch_array($sql);
$company_phone = formatPhoneNumber($row['company_phone']);
$company_name = $row['company_name'];
@@ -79,8 +94,8 @@ function createTicket($contact_id, $contact_name, $contact_email, $client_id, $c
$email_subject, $email_body);
if ($mail !== true) {
- mysqli_query($mysqli,"INSERT INTO notifications SET notification_type = 'Mail', notification = 'Failed to send email to $contact_email', notification_timestamp = NOW(), company_id = $company_id");
- mysqli_query($mysqli,"INSERT INTO logs SET log_type = 'Mail', log_action = 'Error', log_description = 'Failed to send email to $contact_email regarding $subject. $mail', company_id = $company_id");
+ mysqli_query($mysqli, "INSERT INTO notifications SET notification_type = 'Mail', notification = 'Failed to send email to $contact_email', notification_timestamp = NOW(), company_id = $company_id");
+ mysqli_query($mysqli, "INSERT INTO logs SET log_type = 'Mail', log_action = 'Error', log_description = 'Failed to send email to $contact_email regarding $subject. $mail', company_id = $company_id");
}
}
@@ -89,6 +104,83 @@ function createTicket($contact_id, $contact_name, $contact_email, $client_id, $c
}
+function addReply($from_email, $date, $subject, $ticket_number, $message)
+{
+ // Add email as a comment/reply to an existing ticket
+
+ // Access global variables
+ global $mysqli, $config_ticket_prefix, $config_base_url, $config_ticket_from_name, $config_ticket_from_email, $config_smtp_host, $config_smtp_port, $config_smtp_encryption, $config_smtp_username, $config_smtp_password;
+
+ // Set default reply type
+ $ticket_reply_type = 'Client';
+
+ // Capture just the latest/most recent email reply content
+ // based off the "#--itflow#" line that we prepend the outgoing emails with (similar to the old school --reply above this line--)
+ $message = explode("#--itflow--#", $message);
+ $message = nl2br(htmlentities(strip_tags($message[0])));
+ $message = "Email from: $from_email at $date:-
$message";
+
+ // Lookup the ticket ID
+ $row = mysqli_fetch_array(mysqli_query($mysqli, "SELECT ticket_id, ticket_subject, ticket_status, ticket_contact_id, ticket_client_id, tickets.company_id, contact_email
+ FROM tickets
+ LEFT JOIN contacts on tickets.ticket_contact_id = contacts.contact_id
+ WHERE ticket_number = '$ticket_number' LIMIT 1"));
+
+ if ($row) {
+
+ // Get ticket details
+ $ticket_id = $row['ticket_id'];
+ $ticket_status = $row['ticket_status'];
+ $ticket_reply_contact = $row['ticket_contact_id'];
+ $ticket_contact_email = $row['contact_email'];
+ $client_id = $row['ticket_client_id'];
+ $company_id = $row['company_id'];
+
+ // Check ticket isn't closed
+ if ($ticket_status == "Closed") {
+ mysqli_query($mysqli, "INSERT INTO notifications SET notification_type = 'Ticket', notification = 'Email parser: $from_email attempted to re-open ticket $config_ticket_prefix$ticket_number (ID $ticket_id) - check inbox manually to see email', notification_timestamp = NOW(), notification_client_id = '$client_id', company_id = '$company_id'");
+ return false;
+ }
+
+ // Check WHO replied (was it the owner of the ticket or someone else on CC?)
+ if (empty($ticket_contact_email) || $ticket_contact_email !== $from_email) {
+
+ // It wasn't the contact currently assigned to the ticket, check if it's another registered contact for that client
+
+ $row = mysqli_fetch_array(mysqli_query($mysqli, "SELECT contact_id FROM contacts WHERE contact_email = '$from_email' AND contact_client_id = $client_id LIMIT 1"));
+ if ($row) {
+
+ // Contact is known - we can keep the reply type as client
+ $ticket_reply_contact = $row['contact_id'];
+
+ } else {
+ // Mark the reply as internal as we don't recognise the contact (so the actual contact doesn't see it, and the tech can edit/delete if needed)
+ $ticket_reply_type = 'Internal';
+ $ticket_reply_contact = '0';
+ $message = "WARNING: Contact email mismatch
$message"; // Add a warning at the start of the message - for the techs benefit (think phishing/scams)
+ }
+ }
+
+ // Sanitize ticket reply
+ $comment = trim(mysqli_real_escape_string($mysqli, $message));
+
+ // Add the comment
+ mysqli_query($mysqli, "INSERT INTO ticket_replies SET ticket_reply = '$comment', ticket_reply_type = '$ticket_reply_type', ticket_reply_time_worked = '00:00:00', ticket_reply_created_at = NOW(), ticket_reply_by = '$ticket_reply_contact', ticket_reply_ticket_id = '$ticket_id', company_id = '$company_id'");
+
+ // Update Ticket Last Response Field & set ticket to open as client has replied
+ mysqli_query($mysqli, "UPDATE tickets SET ticket_status = 'Open', ticket_updated_at = NOW() WHERE ticket_id = $ticket_id AND ticket_client_id = '$client_id' LIMIT 1");
+
+ echo "Updated existing ticket.
";
+ mysqli_query($mysqli, "INSERT INTO logs SET log_type = 'Ticket', log_action = 'Update', log_description = 'Email parser: Client contact $from_email updated ticket $config_ticket_prefix$ticket_number ($subject)', log_created_at = NOW(), log_client_id = $client_id, company_id = $company_id");
+
+ return true;
+
+ } else {
+ // Invalid ticket number
+ return false;
+ }
+}
+
// Prepare connection string with encryption (TLS/SSL/)
$imap_mailbox = "$config_imap_host:$config_imap_port/imap/$config_imap_encryption";
@@ -99,7 +191,7 @@ $imap = imap_open("{{$imap_mailbox}}INBOX", $config_smtp_username, $config_smtp_
if (!$imap) {
// Logging
$extended_log_description = var_export(imap_errors(), true);
- mysqli_query($mysqli,"INSERT INTO logs SET log_type = 'Mail', log_action = 'Error', log_description = 'Email parser: Failed to connect to IMAP. Details: $extended_log_description', company_id = $company_id");
+ mysqli_query($mysqli, "INSERT INTO logs SET log_type = 'Mail', log_action = 'Error', log_description = 'Email parser: Failed to connect to IMAP. Details: $extended_log_description', company_id = $company_id");
exit("Could not connect to IMAP");
}
@@ -124,108 +216,66 @@ if ($emails) {
// Default false
$email_processed = false;
- // Get message details
- $metadata = imap_fetch_overview($imap, $email,0); // Date, Subject, Size
- $header = imap_headerinfo($imap, $email); // To get the From as an email, not a contact name
- $message = (imap_fetchbody($imap, $email, 1)); // Body
+ // Get details from message and invoke PHP Mime Mail Parser
+ $msg_to_parse = imap_fetchheader($imap, $email, FT_PREFETCHTEXT) . imap_body($imap, $email);
+ $parser = new PhpMimeMailParser\Parser();
+ $parser->setText($msg_to_parse);
- $from = trim(mysqli_real_escape_string($mysqli, htmlentities(strip_tags($header->from[0]->mailbox . "@" . $header->from[0]->host))));
- $subject = trim(mysqli_real_escape_string($mysqli, htmlentities(strip_tags($metadata[0]->subject))));
- $date = trim(mysqli_real_escape_string($mysqli, htmlentities(strip_tags($metadata[0]->date))));
+ // Process message attributes
+
+ $from_array = $parser->getAddresses('from')[0];
+ $from_name = trim(mysqli_real_escape_string($mysqli, htmlentities(strip_tags($from_array['display']))));
+ $from_email = trim(mysqli_real_escape_string($mysqli, htmlentities(strip_tags($from_array['address']))));
+ $from_domain = explode("@", $from_array['address']);
+ $from_domain = trim(mysqli_real_escape_string($mysqli, htmlentities(strip_tags(end($from_domain))))); // Use the final element in the array (as technically legal to have multiple @'s)
+
+ $subject = trim(mysqli_real_escape_string($mysqli, htmlentities(strip_tags($parser->getHeader('subject')))));
+ $date = trim(mysqli_real_escape_string($mysqli, htmlentities(strip_tags($parser->getHeader('date')))));
+
+ $message = $parser->getMessageBody('text');
- $domain = trim(mysqli_real_escape_string($mysqli, $header->from[0]->host));
- $from_name = trim(mysqli_real_escape_string($mysqli, $header->from[0]->mailbox));
// Check if we can identify a ticket number (in square brackets)
if (preg_match("/\[$config_ticket_prefix\d+\]/", $subject, $ticket_number)) {
+ // Looks like there's a ticket number in the subject line (e.g. [TCK-091]
+ // Process as a ticket reply
+
// Get the actual ticket number (without the brackets)
preg_match('/\d+/', $ticket_number[0], $ticket_number);
$ticket_number = intval($ticket_number[0]);
- // Split the email into just the latest reply, with some metadata
- // We base this off the string "#--itflow--#" that we prepend the outgoing emails with (similar to the old school --reply above this line--)
- $message = explode("#--itflow--#", $message);
- $message = nl2br(htmlentities(strip_tags($message[0])));
- $message = "Email from: $from at $date:-
$message";
-
- // Lookup the ticket ID to add the reply to (just to check in-case the ID is different from the number).
- $ticket_sql = mysqli_query($mysqli, "SELECT * FROM tickets WHERE ticket_number = '$ticket_number' LIMIT 1");
- $row = mysqli_fetch_array($ticket_sql);
- $ticket_id = $row['ticket_id'];
- $ticket_reply_contact = $row['ticket_contact_id'];
- $ticket_assigned_to = $row['ticket_assigned_to'];
- $client_id = $row['ticket_client_id'];
- $company_id = $row['company_id'];
- $ticket_reply_type = 'Client'; // Setting to client as a default value
-
- // Check the ticket ID is valid
- if (intval($ticket_id) && $ticket_id !== '0') {
-
- // Check that ticket is open
- if ($row['ticket_status'] == "Closed") {
-
- // It's closed - let's notify someone that a client tried to reply
- mysqli_query($mysqli,"INSERT INTO notifications SET notification_type = 'Ticket', notification = 'Email parser: $from attempted to re-open ticket ID $ticket_id ($config_ticket_prefix$ticket_number) - check inbox manually to see email', notification_timestamp = NOW(), notification_client_id = '$client_id', company_id = '$company_id'");
-
- } else {
-
- // Ticket is open, proceed.
-
- // Check the email matches the contact's email - if it doesn't then mark the reply as internal (so the contact doesn't see it, and the tech can edit/delete if needed)
- // Niche edge case - possibly where CC's on an email reply to a ticket?
- $contact_sql = mysqli_query($mysqli, "SELECT contact_email FROM contacts WHERE contact_id = '$ticket_reply_contact'");
- $row = mysqli_fetch_array($contact_sql);
- if ($from !== $row['contact_email']) {
- $ticket_reply_type = 'Internal';
- $ticket_reply_contact = '0';
- $message = "WARNING: Contact email mismatch
$message"; // Add a warning at the start of the message - for the techs benefit (think phishing/scams)
- }
-
- // Sanitize ticket reply
- $comment = trim(mysqli_real_escape_string($mysqli, $message));
-
- // Add the comment
- mysqli_query($mysqli, "INSERT INTO ticket_replies SET ticket_reply = '$comment', ticket_reply_type = '$ticket_reply_type', ticket_reply_time_worked = '00:00:00', ticket_reply_created_at = NOW(), ticket_reply_by = '$ticket_reply_contact', ticket_reply_ticket_id = '$ticket_id', company_id = '$company_id'");
-
- // Update Ticket Last Response Field & set ticket to open as client has replied
- mysqli_query($mysqli,"UPDATE tickets SET ticket_status = 'Open', ticket_updated_at = NOW() WHERE ticket_id = $ticket_id AND ticket_client_id = '$client_id' LIMIT 1");
-
- echo "Updated existing ticket.
";
- mysqli_query($mysqli,"INSERT INTO logs SET log_type = 'Ticket', log_action = 'Update', log_description = 'Email parser: Client contact $from updated ticket $config_ticket_prefix$ticket_number ($subject)', log_created_at = NOW(), log_client_id = $client_id, company_id = $company_id");
-
- $email_processed = true;
- }
-
+ if (addReply($from_email, $date, $subject, $ticket_number, $message)) {
+ $email_processed = true;
}
-
} else {
// Couldn't match this email to an existing ticket
// Check if we can match the sender to a pre-existing contact
- $any_contact_sql = mysqli_query($mysqli, "SELECT * FROM contacts WHERE contact_email = '$from' LIMIT 1");
+ $any_contact_sql = mysqli_query($mysqli, "SELECT * FROM contacts WHERE contact_email = '$from_email' LIMIT 1");
$row = mysqli_fetch_array($any_contact_sql);
- $contact_name = $row['contact_name'];
- $contact_id = $row['contact_id'];
- $contact_email = $row['contact_email'];
- $client_id = $row['contact_client_id'];
- $company_id = $row['company_id'];
+ if ($row) {
+ // Sender exists as a contact
+ $contact_name = $row['contact_name'];
+ $contact_id = $row['contact_id'];
+ $contact_email = $row['contact_email'];
+ $client_id = $row['contact_client_id'];
+ $company_id = $row['company_id'];
- if ($from == $contact_email) {
-
- createTicket($contact_id, $contact_name, $contact_email, $client_id, $company_id, $date, $subject, $message);
- $email_processed = true;
+ if (addTicket($contact_id, $contact_name, $contact_email, $client_id, $company_id, $date, $subject, $message)) {
+ $email_processed = true;
+ }
} else {
// Couldn't match this email to an existing ticket or an existing client contact
// Checking to see if the sender domain matches a client website
- $row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT * FROM clients WHERE client_website = '$domain' LIMIT 1"));
+ $row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT * FROM clients WHERE client_website = '$from_domain' LIMIT 1"));
- if ($row && $domain == $row['client_website']) {
+ if ($row && $from_domain == $row['client_website']) {
// We found a match - create a contact under this client and raise a ticket for them
@@ -236,22 +286,22 @@ if ($emails) {
// Contact details
$password = password_hash(randomString(), PASSWORD_DEFAULT);
$contact_name = $from_name;
- $contact_email = $from;
- mysqli_query($mysqli,"INSERT INTO contacts SET contact_name = '$contact_name', contact_email = '$contact_email', contact_notes = 'Added automatically via email parsing.', contact_password_hash = '$password', contact_client_id = $client_id, company_id = $company_id");
+ $contact_email = $from_email;
+ mysqli_query($mysqli, "INSERT INTO contacts SET contact_name = '$contact_name', contact_email = '$contact_email', contact_notes = 'Added automatically via email parsing.', contact_password_hash = '$password', contact_client_id = $client_id, company_id = $company_id");
$contact_id = mysqli_insert_id($mysqli);
// Logging for contact creation
echo "Created new contact.
";
- mysqli_query($mysqli,"INSERT INTO logs SET log_type = 'Contact', log_action = 'Create', log_description = 'Email parser: created contact $contact_name', log_client_id = $client_id, company_id = $company_id");
+ mysqli_query($mysqli, "INSERT INTO logs SET log_type = 'Contact', log_action = 'Create', log_description = 'Email parser: created contact $contact_name', log_client_id = $client_id, company_id = $company_id");
- createTicket($contact_id, $contact_name, $contact_email, $client_id, $company_id, $date, $subject, $message);
-
- $email_processed = true;
+ if (addTicket($contact_id, $contact_name, $contact_email, $client_id, $company_id, $date, $subject, $message)) {
+ $email_processed = true;
+ }
} else {
// Couldn't match this email to an existing ticket, existing contact or an existing client via the "from" domain
- // In the future we might make a page where these can be nicely viewed / managed, but for now we'll just flag them as needing attention
+ // In the future we might make a page where these can be nicely viewed / managed, but for now we'll just flag them in the Inbox as needing attention
}
@@ -259,10 +309,11 @@ if ($emails) {
}
- // Deal with the message
+ // Deal with the message (move it if processed, flag it if not)
if ($email_processed) {
imap_mail_move($imap, $email, $imap_folder);
} else {
+ echo "Failed to process email - flagging for manual review.";
imap_setflag_full($imap, $email, "\\Flagged");
}
@@ -271,6 +322,5 @@ if ($emails) {
}
-
imap_expunge($imap);
imap_close($imap);
diff --git a/plugins/php-mime-mail-parser/src/Attachment.php b/plugins/php-mime-mail-parser/src/Attachment.php
new file mode 100644
index 00000000..1a731635
--- /dev/null
+++ b/plugins/php-mime-mail-parser/src/Attachment.php
@@ -0,0 +1,276 @@
+filename = $filename;
+ $this->contentType = $contentType;
+ $this->stream = $stream;
+ $this->content = null;
+ $this->contentDisposition = $contentDisposition;
+ $this->contentId = $contentId;
+ $this->headers = $headers;
+ $this->mimePartStr = $mimePartStr;
+ }
+
+ /**
+ * retrieve the attachment filename
+ *
+ * @return string
+ */
+ public function getFilename()
+ {
+ return $this->filename;
+ }
+
+ /**
+ * Retrieve the Attachment Content-Type
+ *
+ * @return string
+ */
+ public function getContentType()
+ {
+ return $this->contentType;
+ }
+
+ /**
+ * Retrieve the Attachment Content-Disposition
+ *
+ * @return string
+ */
+ public function getContentDisposition()
+ {
+ return $this->contentDisposition;
+ }
+
+ /**
+ * Retrieve the Attachment Content-ID
+ *
+ * @return string
+ */
+ public function getContentID()
+ {
+ return $this->contentId;
+ }
+
+ /**
+ * Retrieve the Attachment Headers
+ *
+ * @return array
+ */
+ public function getHeaders()
+ {
+ return $this->headers;
+ }
+
+ /**
+ * Get a handle to the stream
+ *
+ * @return resource
+ */
+ public function getStream()
+ {
+ return $this->stream;
+ }
+
+ /**
+ * Rename a file if it already exists at its destination.
+ * Renaming is done by adding a duplicate number to the file name. E.g. existingFileName_1.ext.
+ * After a max duplicate number, renaming the file will switch over to generating a random suffix.
+ *
+ * @param string $fileName Complete path to the file.
+ * @return string The suffixed file name.
+ */
+ protected function suffixFileName(string $fileName): string
+ {
+ $pathInfo = pathinfo($fileName);
+ $dirname = $pathInfo['dirname'].DIRECTORY_SEPARATOR;
+ $filename = $pathInfo['filename'];
+ $extension = empty($pathInfo['extension']) ? '' : '.'.$pathInfo['extension'];
+
+ $i = 0;
+ do {
+ $i++;
+
+ if ($i > $this->maxDuplicateNumber) {
+ $duplicateExtension = uniqid();
+ } else {
+ $duplicateExtension = $i;
+ }
+
+ $resultName = $dirname.$filename."_$duplicateExtension".$extension;
+ } while (file_exists($resultName));
+
+ return $resultName;
+ }
+
+ /**
+ * Read the contents a few bytes at a time until completed
+ * Once read to completion, it always returns false
+ *
+ * @param int $bytes (default: 2082)
+ *
+ * @return string|bool
+ */
+ public function read($bytes = 2082)
+ {
+ return feof($this->stream) ? false : fread($this->stream, $bytes);
+ }
+
+ /**
+ * Retrieve the file content in one go
+ * Once you retrieve the content you cannot use MimeMailParser_attachment::read()
+ *
+ * @return string
+ */
+ public function getContent()
+ {
+ if ($this->content === null) {
+ fseek($this->stream, 0);
+ while (($buf = $this->read()) !== false) {
+ $this->content .= $buf;
+ }
+ }
+
+ return $this->content;
+ }
+
+ /**
+ * Get mime part string for this attachment
+ *
+ * @return string
+ */
+ public function getMimePartStr()
+ {
+ return $this->mimePartStr;
+ }
+
+ /**
+ * Save the attachment individually
+ *
+ * @param string $attach_dir
+ * @param string $filenameStrategy
+ *
+ * @return string
+ */
+ public function save(
+ $attach_dir,
+ $filenameStrategy = Parser::ATTACHMENT_DUPLICATE_SUFFIX
+ ) {
+ $attach_dir = rtrim($attach_dir, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR;
+ if (!is_dir($attach_dir)) {
+ mkdir($attach_dir);
+ }
+
+ // Determine filename
+ switch ($filenameStrategy) {
+ case Parser::ATTACHMENT_RANDOM_FILENAME:
+ $fileInfo = pathinfo($this->getFilename());
+ $extension = empty($fileInfo['extension']) ? '' : '.'.$fileInfo['extension'];
+ $attachment_path = $attach_dir.uniqid().$extension;
+ break;
+ case Parser::ATTACHMENT_DUPLICATE_THROW:
+ case Parser::ATTACHMENT_DUPLICATE_SUFFIX:
+ $attachment_path = $attach_dir.$this->getFilename();
+ break;
+ default:
+ throw new Exception('Invalid filename strategy argument provided.');
+ }
+
+ // Handle duplicate filename
+ if (file_exists($attachment_path)) {
+ switch ($filenameStrategy) {
+ case Parser::ATTACHMENT_DUPLICATE_THROW:
+ throw new Exception('Could not create file for attachment: duplicate filename.');
+ case Parser::ATTACHMENT_DUPLICATE_SUFFIX:
+ $attachment_path = $this->suffixFileName($attachment_path);
+ break;
+ }
+ }
+
+ /** @var resource $fp */
+ if ($fp = fopen($attachment_path, 'w')) {
+ while ($bytes = $this->read()) {
+ fwrite($fp, $bytes);
+ }
+ fclose($fp);
+ return realpath($attachment_path);
+ } else {
+ throw new Exception('Could not write attachments. Your directory may be unwritable by PHP.');
+ }
+ }
+}
diff --git a/plugins/php-mime-mail-parser/src/Charset.php b/plugins/php-mime-mail-parser/src/Charset.php
new file mode 100644
index 00000000..cd219f22
--- /dev/null
+++ b/plugins/php-mime-mail-parser/src/Charset.php
@@ -0,0 +1,370 @@
+ 'us-ascii',
+ 'us-ascii' => 'us-ascii',
+ 'ansi_x3.4-1968' => 'us-ascii',
+ '646' => 'us-ascii',
+ 'iso-8859-1' => 'iso-8859-1',
+ 'iso-8859-2' => 'iso-8859-2',
+ 'iso-8859-3' => 'iso-8859-3',
+ 'iso-8859-4' => 'iso-8859-4',
+ 'iso-8859-5' => 'iso-8859-5',
+ 'iso-8859-6' => 'iso-8859-6',
+ 'iso-8859-6-i' => 'iso-8859-6-i',
+ 'iso-8859-6-e' => 'iso-8859-6-e',
+ 'iso-8859-7' => 'iso-8859-7',
+ 'iso-8859-8' => 'iso-8859-8',
+ 'iso-8859-8-i' => 'iso-8859-8',
+ 'iso-8859-8-e' => 'iso-8859-8-e',
+ 'iso-8859-9' => 'iso-8859-9',
+ 'iso-8859-10' => 'iso-8859-10',
+ 'iso-8859-11' => 'iso-8859-11',
+ 'iso-8859-13' => 'iso-8859-13',
+ 'iso-8859-14' => 'iso-8859-14',
+ 'iso-8859-15' => 'iso-8859-15',
+ 'iso-8859-16' => 'iso-8859-16',
+ 'iso-ir-111' => 'iso-ir-111',
+ 'iso-2022-cn' => 'iso-2022-cn',
+ 'iso-2022-cn-ext' => 'iso-2022-cn',
+ 'iso-2022-kr' => 'iso-2022-kr',
+ 'iso-2022-jp' => 'iso-2022-jp',
+ 'utf-16be' => 'utf-16be',
+ 'utf-16le' => 'utf-16le',
+ 'utf-16' => 'utf-16',
+ 'windows-1250' => 'windows-1250',
+ 'windows-1251' => 'windows-1251',
+ 'windows-1252' => 'windows-1252',
+ 'windows-1253' => 'windows-1253',
+ 'windows-1254' => 'windows-1254',
+ 'windows-1255' => 'windows-1255',
+ 'windows-1256' => 'windows-1256',
+ 'windows-1257' => 'windows-1257',
+ 'windows-1258' => 'windows-1258',
+ 'ibm866' => 'ibm866',
+ 'ibm850' => 'ibm850',
+ 'ibm852' => 'ibm852',
+ 'ibm855' => 'ibm855',
+ 'ibm857' => 'ibm857',
+ 'ibm862' => 'ibm862',
+ 'ibm864' => 'ibm864',
+ 'utf-8' => 'utf-8',
+ 'utf-7' => 'utf-7',
+ 'shift_jis' => 'shift_jis',
+ 'big5' => 'big5',
+ 'euc-jp' => 'euc-jp',
+ 'euc-kr' => 'euc-kr',
+ 'gb2312' => 'gb2312',
+ 'gb18030' => 'gb18030',
+ 'viscii' => 'viscii',
+ 'koi8-r' => 'koi8-r',
+ 'koi8_r' => 'koi8-r',
+ 'cskoi8r' => 'koi8-r',
+ 'koi' => 'koi8-r',
+ 'koi8' => 'koi8-r',
+ 'koi8-u' => 'koi8-u',
+ 'tis-620' => 'tis-620',
+ 't.61-8bit' => 't.61-8bit',
+ 'hz-gb-2312' => 'hz-gb-2312',
+ 'big5-hkscs' => 'big5-hkscs',
+ 'gbk' => 'gbk',
+ 'cns11643' => 'x-euc-tw',
+ 'x-imap4-modified-utf7' => 'x-imap4-modified-utf7',
+ 'x-euc-tw' => 'x-euc-tw',
+ 'x-mac-ce' => 'macce',
+ 'x-mac-turkish' => 'macturkish',
+ 'x-mac-greek' => 'macgreek',
+ 'x-mac-icelandic' => 'macicelandic',
+ 'x-mac-croatian' => 'maccroatian',
+ 'x-mac-romanian' => 'macromanian',
+ 'x-mac-cyrillic' => 'maccyrillic',
+ 'x-mac-ukrainian' => 'macukrainian',
+ 'x-mac-hebrew' => 'machebrew',
+ 'x-mac-arabic' => 'macarabic',
+ 'x-mac-farsi' => 'macfarsi',
+ 'x-mac-devanagari' => 'macdevanagari',
+ 'x-mac-gujarati' => 'macgujarati',
+ 'x-mac-gurmukhi' => 'macgurmukhi',
+ 'armscii-8' => 'armscii-8',
+ 'x-viet-tcvn5712' => 'x-viet-tcvn5712',
+ 'x-viet-vps' => 'x-viet-vps',
+ 'iso-10646-ucs-2' => 'utf-16be',
+ 'x-iso-10646-ucs-2-be' => 'utf-16be',
+ 'x-iso-10646-ucs-2-le' => 'utf-16le',
+ 'x-user-defined' => 'x-user-defined',
+ 'x-johab' => 'x-johab',
+ 'latin1' => 'iso-8859-1',
+ 'iso_8859-1' => 'iso-8859-1',
+ 'iso8859-1' => 'iso-8859-1',
+ 'iso8859-2' => 'iso-8859-2',
+ 'iso8859-3' => 'iso-8859-3',
+ 'iso8859-4' => 'iso-8859-4',
+ 'iso8859-5' => 'iso-8859-5',
+ 'iso8859-6' => 'iso-8859-6',
+ 'iso8859-7' => 'iso-8859-7',
+ 'iso8859-8' => 'iso-8859-8',
+ 'iso8859-9' => 'iso-8859-9',
+ 'iso8859-10' => 'iso-8859-10',
+ 'iso8859-11' => 'iso-8859-11',
+ 'iso8859-13' => 'iso-8859-13',
+ 'iso8859-14' => 'iso-8859-14',
+ 'iso8859-15' => 'iso-8859-15',
+ 'iso_8859-1:1987' => 'iso-8859-1',
+ 'iso-ir-100' => 'iso-8859-1',
+ 'l1' => 'iso-8859-1',
+ 'ibm819' => 'iso-8859-1',
+ 'cp819' => 'iso-8859-1',
+ 'csisolatin1' => 'iso-8859-1',
+ 'latin2' => 'iso-8859-2',
+ 'iso_8859-2' => 'iso-8859-2',
+ 'iso_8859-2:1987' => 'iso-8859-2',
+ 'iso-ir-101' => 'iso-8859-2',
+ 'l2' => 'iso-8859-2',
+ 'csisolatin2' => 'iso-8859-2',
+ 'latin3' => 'iso-8859-3',
+ 'iso_8859-3' => 'iso-8859-3',
+ 'iso_8859-3:1988' => 'iso-8859-3',
+ 'iso-ir-109' => 'iso-8859-3',
+ 'l3' => 'iso-8859-3',
+ 'csisolatin3' => 'iso-8859-3',
+ 'latin4' => 'iso-8859-4',
+ 'iso_8859-4' => 'iso-8859-4',
+ 'iso_8859-4:1988' => 'iso-8859-4',
+ 'iso-ir-110' => 'iso-8859-4',
+ 'l4' => 'iso-8859-4',
+ 'csisolatin4' => 'iso-8859-4',
+ 'cyrillic' => 'iso-8859-5',
+ 'iso_8859-5' => 'iso-8859-5',
+ 'iso_8859-5:1988' => 'iso-8859-5',
+ 'iso-ir-144' => 'iso-8859-5',
+ 'csisolatincyrillic' => 'iso-8859-5',
+ 'arabic' => 'iso-8859-6',
+ 'iso_8859-6' => 'iso-8859-6',
+ 'iso_8859-6:1987' => 'iso-8859-6',
+ 'iso-ir-127' => 'iso-8859-6',
+ 'ecma-114' => 'iso-8859-6',
+ 'asmo-708' => 'iso-8859-6',
+ 'csisolatinarabic' => 'iso-8859-6',
+ 'csiso88596i' => 'iso-8859-6-i',
+ 'csiso88596e' => 'iso-8859-6-e',
+ 'greek' => 'iso-8859-7',
+ 'greek8' => 'iso-8859-7',
+ 'sun_eu_greek' => 'iso-8859-7',
+ 'iso_8859-7' => 'iso-8859-7',
+ 'iso_8859-7:1987' => 'iso-8859-7',
+ 'iso-ir-126' => 'iso-8859-7',
+ 'elot_928' => 'iso-8859-7',
+ 'ecma-118' => 'iso-8859-7',
+ 'csisolatingreek' => 'iso-8859-7',
+ 'hebrew' => 'iso-8859-8',
+ 'iso_8859-8' => 'iso-8859-8',
+ 'visual' => 'iso-8859-8',
+ 'iso_8859-8:1988' => 'iso-8859-8',
+ 'iso-ir-138' => 'iso-8859-8',
+ 'csisolatinhebrew' => 'iso-8859-8',
+ 'csiso88598i' => 'iso-8859-8',
+ 'iso-8859-8i' => 'iso-8859-8',
+ 'logical' => 'iso-8859-8',
+ 'csiso88598e' => 'iso-8859-8-e',
+ 'latin5' => 'iso-8859-9',
+ 'iso_8859-9' => 'iso-8859-9',
+ 'iso_8859-9:1989' => 'iso-8859-9',
+ 'iso-ir-148' => 'iso-8859-9',
+ 'l5' => 'iso-8859-9',
+ 'csisolatin5' => 'iso-8859-9',
+ 'unicode-1-1-utf-8' => 'utf-8',
+ 'utf8' => 'utf-8',
+ 'x-sjis' => 'shift_jis',
+ 'shift-jis' => 'shift_jis',
+ 'ms_kanji' => 'shift_jis',
+ 'csshiftjis' => 'shift_jis',
+ 'windows-31j' => 'shift_jis',
+ 'cp932' => 'shift_jis',
+ 'sjis' => 'shift_jis',
+ 'cseucpkdfmtjapanese' => 'euc-jp',
+ 'x-euc-jp' => 'euc-jp',
+ 'csiso2022jp' => 'iso-2022-jp',
+ 'iso-2022-jp-2' => 'iso-2022-jp',
+ 'csiso2022jp2' => 'iso-2022-jp',
+ 'csbig5' => 'big5',
+ 'cn-big5' => 'big5',
+ 'x-x-big5' => 'big5',
+ 'zh_tw-big5' => 'big5',
+ 'cseuckr' => 'euc-kr',
+ 'ks_c_5601-1987' => 'euc-kr',
+ 'iso-ir-149' => 'euc-kr',
+ 'ks_c_5601-1989' => 'euc-kr',
+ 'ksc_5601' => 'euc-kr',
+ 'ksc5601' => 'euc-kr',
+ 'korean' => 'euc-kr',
+ 'csksc56011987' => 'euc-kr',
+ '5601' => 'euc-kr',
+ 'windows-949' => 'euc-kr',
+ 'gb_2312-80' => 'gb2312',
+ 'iso-ir-58' => 'gb2312',
+ 'chinese' => 'gb2312',
+ 'csiso58gb231280' => 'gb2312',
+ 'csgb2312' => 'gb2312',
+ 'zh_cn.euc' => 'gb2312',
+ 'gb_2312' => 'gb2312',
+ 'x-cp1250' => 'windows-1250',
+ 'x-cp1251' => 'windows-1251',
+ 'x-cp1252' => 'windows-1252',
+ 'x-cp1253' => 'windows-1253',
+ 'x-cp1254' => 'windows-1254',
+ 'x-cp1255' => 'windows-1255',
+ 'x-cp1256' => 'windows-1256',
+ 'x-cp1257' => 'windows-1257',
+ 'x-cp1258' => 'windows-1258',
+ 'windows-874' => 'windows-874',
+ 'ibm874' => 'windows-874',
+ 'dos-874' => 'windows-874',
+ 'macintosh' => 'macintosh',
+ 'x-mac-roman' => 'macintosh',
+ 'mac' => 'macintosh',
+ 'csmacintosh' => 'macintosh',
+ 'cp866' => 'ibm866',
+ 'cp-866' => 'ibm866',
+ '866' => 'ibm866',
+ 'csibm866' => 'ibm866',
+ 'cp850' => 'ibm850',
+ '850' => 'ibm850',
+ 'csibm850' => 'ibm850',
+ 'cp852' => 'ibm852',
+ '852' => 'ibm852',
+ 'csibm852' => 'ibm852',
+ 'cp855' => 'ibm855',
+ '855' => 'ibm855',
+ 'csibm855' => 'ibm855',
+ 'cp857' => 'ibm857',
+ '857' => 'ibm857',
+ 'csibm857' => 'ibm857',
+ 'cp862' => 'ibm862',
+ '862' => 'ibm862',
+ 'csibm862' => 'ibm862',
+ 'cp864' => 'ibm864',
+ '864' => 'ibm864',
+ 'csibm864' => 'ibm864',
+ 'ibm-864' => 'ibm864',
+ 't.61' => 't.61-8bit',
+ 'iso-ir-103' => 't.61-8bit',
+ 'csiso103t618bit' => 't.61-8bit',
+ 'x-unicode-2-0-utf-7' => 'utf-7',
+ 'unicode-2-0-utf-7' => 'utf-7',
+ 'unicode-1-1-utf-7' => 'utf-7',
+ 'csunicode11utf7' => 'utf-7',
+ 'csunicode' => 'utf-16be',
+ 'csunicode11' => 'utf-16be',
+ 'iso-10646-ucs-basic' => 'utf-16be',
+ 'csunicodeascii' => 'utf-16be',
+ 'iso-10646-unicode-latin1' => 'utf-16be',
+ 'csunicodelatin1' => 'utf-16be',
+ 'iso-10646' => 'utf-16be',
+ 'iso-10646-j-1' => 'utf-16be',
+ 'latin6' => 'iso-8859-10',
+ 'iso-ir-157' => 'iso-8859-10',
+ 'l6' => 'iso-8859-10',
+ 'csisolatin6' => 'iso-8859-10',
+ 'iso_8859-15' => 'iso-8859-15',
+ 'csisolatin9' => 'iso-8859-15',
+ 'l9' => 'iso-8859-15',
+ 'ecma-cyrillic' => 'iso-ir-111',
+ 'csiso111ecmacyrillic' => 'iso-ir-111',
+ 'csiso2022kr' => 'iso-2022-kr',
+ 'csviscii' => 'viscii',
+ 'zh_tw-euc' => 'x-euc-tw',
+ 'iso88591' => 'iso-8859-1',
+ 'iso88592' => 'iso-8859-2',
+ 'iso88593' => 'iso-8859-3',
+ 'iso88594' => 'iso-8859-4',
+ 'iso88595' => 'iso-8859-5',
+ 'iso88596' => 'iso-8859-6',
+ 'iso88597' => 'iso-8859-7',
+ 'iso88598' => 'iso-8859-8',
+ 'iso88599' => 'iso-8859-9',
+ 'iso885910' => 'iso-8859-10',
+ 'iso885911' => 'iso-8859-11',
+ 'iso885912' => 'iso-8859-12',
+ 'iso885913' => 'iso-8859-13',
+ 'iso885914' => 'iso-8859-14',
+ 'iso885915' => 'iso-8859-15',
+ 'tis620' => 'tis-620',
+ 'cp1250' => 'windows-1250',
+ 'cp1251' => 'windows-1251',
+ 'cp1252' => 'windows-1252',
+ 'cp1253' => 'windows-1253',
+ 'cp1254' => 'windows-1254',
+ 'cp1255' => 'windows-1255',
+ 'cp1256' => 'windows-1256',
+ 'cp1257' => 'windows-1257',
+ 'cp1258' => 'windows-1258',
+ 'x-gbk' => 'gbk',
+ 'windows-936' => 'gbk',
+ 'ansi-1251' => 'windows-1251',
+ ];
+
+ /**
+ * {@inheritdoc}
+ */
+ public function decodeCharset($encodedString, $charset)
+ {
+ $charset = $this->getCharsetAlias($charset);
+
+ if ($charset == 'utf-8' || $charset == 'us-ascii') {
+ return $encodedString;
+ }
+
+ if (function_exists('mb_convert_encoding')) {
+ if ($charset == 'iso-2022-jp') {
+ return mb_convert_encoding($encodedString, 'utf-8', 'iso-2022-jp-ms');
+ }
+
+ if (array_search($charset, $this->getSupportedEncodings())) {
+ return mb_convert_encoding($encodedString, 'utf-8', $charset);
+ }
+ }
+
+ return iconv($charset, 'utf-8//translit//ignore', $encodedString);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCharsetAlias($charset)
+ {
+ $charset = strtolower($charset);
+
+ if (array_key_exists($charset, $this->charsetAlias)) {
+ return $this->charsetAlias[$charset];
+ }
+
+ return 'us-ascii';
+ }
+
+ private function getSupportedEncodings()
+ {
+ return
+ array_map(
+ 'strtolower',
+ array_unique(
+ array_merge(
+ $enc = mb_list_encodings(),
+ call_user_func_array(
+ 'array_merge',
+ array_map(
+ "mb_encoding_aliases",
+ $enc
+ )
+ )
+ )
+ )
+ );
+ }
+}
diff --git a/plugins/php-mime-mail-parser/src/Contracts/CharsetManager.php b/plugins/php-mime-mail-parser/src/Contracts/CharsetManager.php
new file mode 100644
index 00000000..660ec00c
--- /dev/null
+++ b/plugins/php-mime-mail-parser/src/Contracts/CharsetManager.php
@@ -0,0 +1,24 @@
+parser = $fn;
+ }
+
+ /**
+ * Process a mime part, optionally delegating parsing to the $next MiddlewareStack
+ */
+ public function parse(MimePart $part, MiddlewareStack $next)
+ {
+ return call_user_func($this->parser, $part, $next);
+ }
+}
diff --git a/plugins/php-mime-mail-parser/src/MiddlewareStack.php b/plugins/php-mime-mail-parser/src/MiddlewareStack.php
new file mode 100644
index 00000000..3ef6da93
--- /dev/null
+++ b/plugins/php-mime-mail-parser/src/MiddlewareStack.php
@@ -0,0 +1,89 @@
+add($Middleware)
+ *
+ * @param Middleware $middleware
+ */
+ public function __construct(MiddleWareContracts $middleware = null)
+ {
+ $this->middleware = $middleware;
+ }
+
+ /**
+ * Creates a chained middleware in MiddlewareStack
+ *
+ * @param Middleware $middleware
+ * @return MiddlewareStack Immutable MiddlewareStack
+ */
+ public function add(MiddleWareContracts $middleware)
+ {
+ $stack = new static($middleware);
+ $stack->next = $this;
+ return $stack;
+ }
+
+ /**
+ * Parses the MimePart by passing it through the Middleware
+ * @param MimePart $part
+ * @return MimePart
+ */
+ public function parse(MimePart $part)
+ {
+ if (!$this->middleware) {
+ return $part;
+ }
+ $part = call_user_func(array($this->middleware, 'parse'), $part, $this->next);
+ return $part;
+ }
+
+ /**
+ * Creates a MiddlewareStack based on an array of middleware
+ *
+ * @param Middleware[] $middlewares
+ * @return MiddlewareStack
+ */
+ public static function factory(array $middlewares = array())
+ {
+ $stack = new static;
+ foreach ($middlewares as $middleware) {
+ $stack = $stack->add($middleware);
+ }
+ return $stack;
+ }
+
+ /**
+ * Allow calling MiddlewareStack instance directly to invoke parse()
+ *
+ * @param MimePart $part
+ * @return MimePart
+ */
+ public function __invoke(MimePart $part)
+ {
+ return $this->parse($part);
+ }
+}
diff --git a/plugins/php-mime-mail-parser/src/MimePart.php b/plugins/php-mime-mail-parser/src/MimePart.php
new file mode 100644
index 00000000..d2211b7c
--- /dev/null
+++ b/plugins/php-mime-mail-parser/src/MimePart.php
@@ -0,0 +1,119 @@
+getPart();
+ * $part['headers']['from'] = 'modified@example.com';
+ * $MimePart->setPart($part);
+ */
+class MimePart implements \ArrayAccess
+{
+ /**
+ * Internal mime part
+ *
+ * @var array
+ */
+ protected $part = array();
+
+ /**
+ * Immutable Part Id
+ *
+ * @var string
+ */
+ private $id;
+
+ /**
+ * Create a mime part
+ *
+ * @param array $part
+ * @param string $id
+ */
+ public function __construct($id, array $part)
+ {
+ $this->part = $part;
+ $this->id = $id;
+ }
+
+ /**
+ * Retrieve the part Id
+ *
+ * @return string
+ */
+ public function getId()
+ {
+ return $this->id;
+ }
+
+ /**
+ * Retrieve the part data
+ *
+ * @return array
+ */
+ public function getPart()
+ {
+ return $this->part;
+ }
+
+ /**
+ * Set the mime part data
+ *
+ * @param array $part
+ * @return void
+ */
+ public function setPart(array $part)
+ {
+ $this->part = $part;
+ }
+
+ /**
+ * ArrayAccess
+ */
+ #[\ReturnTypeWillChange]
+ public function offsetSet($offset, $value)
+ {
+ if (is_null($offset)) {
+ $this->part[] = $value;
+ return;
+ }
+ $this->part[$offset] = $value;
+ }
+
+ /**
+ * ArrayAccess
+ */
+ #[\ReturnTypeWillChange]
+ public function offsetExists($offset)
+ {
+ return isset($this->part[$offset]);
+ }
+
+ /**
+ * ArrayAccess
+ */
+ #[\ReturnTypeWillChange]
+ public function offsetUnset($offset)
+ {
+ unset($this->part[$offset]);
+ }
+
+ /**
+ * ArrayAccess
+ */
+ #[\ReturnTypeWillChange]
+ public function offsetGet($offset)
+ {
+ return isset($this->part[$offset]) ? $this->part[$offset] : null;
+ }
+}
diff --git a/plugins/php-mime-mail-parser/src/Parser.php b/plugins/php-mime-mail-parser/src/Parser.php
new file mode 100644
index 00000000..6502b57e
--- /dev/null
+++ b/plugins/php-mime-mail-parser/src/Parser.php
@@ -0,0 +1,923 @@
+saveAttachments().
+ */
+ const ATTACHMENT_DUPLICATE_THROW = 'DuplicateThrow';
+ const ATTACHMENT_DUPLICATE_SUFFIX = 'DuplicateSuffix';
+ const ATTACHMENT_RANDOM_FILENAME = 'RandomFilename';
+
+ /**
+ * PHP MimeParser Resource ID
+ *
+ * @var resource $resource
+ */
+ protected $resource;
+
+ /**
+ * A file pointer to email
+ *
+ * @var resource $stream
+ */
+ protected $stream;
+
+ /**
+ * A text of an email
+ *
+ * @var string $data
+ */
+ protected $data;
+
+ /**
+ * Parts of an email
+ *
+ * @var array $parts
+ */
+ protected $parts;
+
+ /**
+ * @var CharsetManager object
+ */
+ protected $charset;
+
+ /**
+ * Valid stream modes for reading
+ *
+ * @var array
+ */
+ protected static $readableModes = [
+ 'r', 'r+', 'w+', 'a+', 'x+', 'c+', 'rb', 'r+b', 'w+b', 'a+b',
+ 'x+b', 'c+b', 'rt', 'r+t', 'w+t', 'a+t', 'x+t', 'c+t'
+ ];
+
+ /**
+ * Stack of middleware registered to process data
+ *
+ * @var MiddlewareStack
+ */
+ protected $middlewareStack;
+
+ /**
+ * Parser constructor.
+ *
+ * @param CharsetManager|null $charset
+ */
+ public function __construct(CharsetManager $charset = null)
+ {
+ if ($charset == null) {
+ $charset = new Charset();
+ }
+
+ $this->charset = $charset;
+ $this->middlewareStack = new MiddlewareStack();
+ }
+
+ /**
+ * Free the held resources
+ *
+ * @return void
+ */
+ public function __destruct()
+ {
+ // clear the email file resource
+ if (is_resource($this->stream)) {
+ fclose($this->stream);
+ }
+ // clear the MailParse resource
+ if (is_resource($this->resource)) {
+ mailparse_msg_free($this->resource);
+ }
+ }
+
+ /**
+ * Set the file path we use to get the email text
+ *
+ * @param string $path File path to the MIME mail
+ *
+ * @return Parser MimeMailParser Instance
+ */
+ public function setPath($path)
+ {
+ if (is_writable($path)) {
+ $file = fopen($path, 'a+');
+ fseek($file, -1, SEEK_END);
+ if (fread($file, 1) != "\n") {
+ fwrite($file, PHP_EOL);
+ }
+ fclose($file);
+ }
+
+ // should parse message incrementally from file
+ $this->resource = mailparse_msg_parse_file($path);
+ $this->stream = fopen($path, 'r');
+ $this->parse();
+
+ return $this;
+ }
+
+ /**
+ * Set the Stream resource we use to get the email text
+ *
+ * @param resource $stream
+ *
+ * @return Parser MimeMailParser Instance
+ * @throws Exception
+ */
+ public function setStream($stream)
+ {
+ // streams have to be cached to file first
+ $meta = @stream_get_meta_data($stream);
+ if (!$meta || !$meta['mode'] || !in_array($meta['mode'], self::$readableModes, true) || $meta['eof']) {
+ throw new Exception(
+ 'setStream() expects parameter stream to be readable stream resource.'
+ );
+ }
+
+ /** @var resource $tmp_fp */
+ $tmp_fp = tmpfile();
+ if ($tmp_fp) {
+ while (!feof($stream)) {
+ fwrite($tmp_fp, fread($stream, 2028));
+ }
+
+ if (fread($tmp_fp, 1) != "\n") {
+ fwrite($tmp_fp, PHP_EOL);
+ }
+
+ fseek($tmp_fp, 0);
+ $this->stream = &$tmp_fp;
+ } else {
+ throw new Exception(
+ 'Could not create temporary files for attachments. Your tmp directory may be unwritable by PHP.'
+ );
+ }
+ fclose($stream);
+
+ $this->resource = mailparse_msg_create();
+ // parses the message incrementally (low memory usage but slower)
+ while (!feof($this->stream)) {
+ mailparse_msg_parse($this->resource, fread($this->stream, 2082));
+ }
+ $this->parse();
+
+ return $this;
+ }
+
+ /**
+ * Set the email text
+ *
+ * @param string $data
+ *
+ * @return Parser MimeMailParser Instance
+ */
+ public function setText($data)
+ {
+ if (empty($data)) {
+ throw new Exception('You must not call MimeMailParser::setText with an empty string parameter');
+ }
+
+ if (substr($data, -1) != "\n") {
+ $data = $data.PHP_EOL;
+ }
+
+ $this->resource = mailparse_msg_create();
+ // does not parse incrementally, fast memory hog might explode
+ mailparse_msg_parse($this->resource, $data);
+ $this->data = $data;
+ $this->parse();
+
+ return $this;
+ }
+
+ /**
+ * Parse the Message into parts
+ *
+ * @return void
+ */
+ protected function parse()
+ {
+ $structure = mailparse_msg_get_structure($this->resource);
+ $this->parts = [];
+ foreach ($structure as $part_id) {
+ $part = mailparse_msg_get_part($this->resource, $part_id);
+ $part_data = mailparse_msg_get_part_data($part);
+ $mimePart = new MimePart($part_id, $part_data);
+ // let each middleware parse the part before saving
+ $this->parts[$part_id] = $this->middlewareStack->parse($mimePart)->getPart();
+ }
+ }
+
+ /**
+ * Retrieve a specific Email Header, without charset conversion.
+ *
+ * @param string $name Header name (case-insensitive)
+ *
+ * @return string|bool
+ * @throws Exception
+ */
+ public function getRawHeader($name)
+ {
+ $name = strtolower($name);
+ if (isset($this->parts[1])) {
+ $headers = $this->getPart('headers', $this->parts[1]);
+
+ return isset($headers[$name]) ? $headers[$name] : false;
+ } else {
+ throw new Exception(
+ 'setPath() or setText() or setStream() must be called before retrieving email headers.'
+ );
+ }
+ }
+
+ /**
+ * Retrieve a specific Email Header
+ *
+ * @param string $name Header name (case-insensitive)
+ *
+ * @return string|bool
+ */
+ public function getHeader($name)
+ {
+ $rawHeader = $this->getRawHeader($name);
+ if ($rawHeader === false) {
+ return false;
+ }
+
+ return $this->decodeHeader($rawHeader);
+ }
+
+ /**
+ * Retrieve all mail headers
+ *
+ * @return array
+ * @throws Exception
+ */
+ public function getHeaders()
+ {
+ if (isset($this->parts[1])) {
+ $headers = $this->getPart('headers', $this->parts[1]);
+ foreach ($headers as &$value) {
+ if (is_array($value)) {
+ foreach ($value as &$v) {
+ $v = $this->decodeSingleHeader($v);
+ }
+ } else {
+ $value = $this->decodeSingleHeader($value);
+ }
+ }
+
+ return $headers;
+ } else {
+ throw new Exception(
+ 'setPath() or setText() or setStream() must be called before retrieving email headers.'
+ );
+ }
+ }
+
+ /**
+ * Retrieve the raw mail headers as a string
+ *
+ * @return string
+ * @throws Exception
+ */
+ public function getHeadersRaw()
+ {
+ if (isset($this->parts[1])) {
+ return $this->getPartHeader($this->parts[1]);
+ } else {
+ throw new Exception(
+ 'setPath() or setText() or setStream() must be called before retrieving email headers.'
+ );
+ }
+ }
+
+ /**
+ * Retrieve the raw Header of a MIME part
+ *
+ * @return String
+ * @param $part Object
+ * @throws Exception
+ */
+ protected function getPartHeader(&$part)
+ {
+ $header = '';
+ if ($this->stream) {
+ $header = $this->getPartHeaderFromFile($part);
+ } elseif ($this->data) {
+ $header = $this->getPartHeaderFromText($part);
+ }
+ return $header;
+ }
+
+ /**
+ * Retrieve the Header from a MIME part from file
+ *
+ * @return String Mime Header Part
+ * @param $part Array
+ */
+ protected function getPartHeaderFromFile(&$part)
+ {
+ $start = $part['starting-pos'];
+ $end = $part['starting-pos-body'];
+ fseek($this->stream, $start, SEEK_SET);
+ $header = fread($this->stream, $end - $start);
+ return $header;
+ }
+
+ /**
+ * Retrieve the Header from a MIME part from text
+ *
+ * @return String Mime Header Part
+ * @param $part Array
+ */
+ protected function getPartHeaderFromText(&$part)
+ {
+ $start = $part['starting-pos'];
+ $end = $part['starting-pos-body'];
+ $header = substr($this->data, $start, $end - $start);
+ return $header;
+ }
+
+ /**
+ * Checks whether a given part ID is a child of another part
+ * eg. an RFC822 attachment may have one or more text parts
+ *
+ * @param string $partId
+ * @param string $parentPartId
+ * @return bool
+ */
+ protected function partIdIsChildOfPart($partId, $parentPartId)
+ {
+ $parentPartId = $parentPartId.'.';
+ return substr($partId, 0, strlen($parentPartId)) == $parentPartId;
+ }
+
+ /**
+ * Whether the given part ID is a child of any attachment part in the message.
+ *
+ * @param string $checkPartId
+ * @return bool
+ */
+ protected function partIdIsChildOfAnAttachment($checkPartId)
+ {
+ foreach ($this->parts as $partId => $part) {
+ if ($this->getPart('content-disposition', $part) == 'attachment') {
+ if ($this->partIdIsChildOfPart($checkPartId, $partId)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns the email message body in the specified format
+ *
+ * @param string $type text, html or htmlEmbedded
+ *
+ * @return string Body
+ * @throws Exception
+ */
+ public function getMessageBody($type = 'text')
+ {
+ $mime_types = [
+ 'text' => 'text/plain',
+ 'html' => 'text/html',
+ 'htmlEmbedded' => 'text/html',
+ ];
+
+ if (in_array($type, array_keys($mime_types))) {
+ $part_type = $type === 'htmlEmbedded' ? 'html' : $type;
+ $inline_parts = $this->getInlineParts($part_type);
+ $body = empty($inline_parts) ? '' : $inline_parts[0];
+ } else {
+ throw new Exception(
+ 'Invalid type specified for getMessageBody(). Expected: text, html or htmlEmbeded.'
+ );
+ }
+
+ if ($type == 'htmlEmbedded') {
+ $attachments = $this->getAttachments();
+ foreach ($attachments as $attachment) {
+ if ($attachment->getContentID() != '') {
+ $body = str_replace(
+ '"cid:'.$attachment->getContentID().'"',
+ '"'.$this->getEmbeddedData($attachment->getContentID()).'"',
+ $body
+ );
+ }
+ }
+ }
+
+ return $body;
+ }
+
+ /**
+ * Returns the embedded data structure
+ *
+ * @param string $contentId Content-Id
+ *
+ * @return string
+ */
+ protected function getEmbeddedData($contentId)
+ {
+ foreach ($this->parts as $part) {
+ if ($this->getPart('content-id', $part) == $contentId) {
+ $embeddedData = 'data:';
+ $embeddedData .= $this->getPart('content-type', $part);
+ $embeddedData .= ';'.$this->getPart('transfer-encoding', $part);
+ $embeddedData .= ','.$this->getPartBody($part);
+ return $embeddedData;
+ }
+ }
+ return '';
+ }
+
+ /**
+ * Return an array with the following keys display, address, is_group
+ *
+ * @param string $name Header name (case-insensitive)
+ *
+ * @return array
+ */
+ public function getAddresses($name)
+ {
+ $value = $this->getRawHeader($name);
+ $value = (is_array($value)) ? $value[0] : $value;
+ $addresses = mailparse_rfc822_parse_addresses($value);
+ foreach ($addresses as $i => $item) {
+ $addresses[$i]['display'] = $this->decodeHeader($item['display']);
+ }
+ return $addresses;
+ }
+
+ /**
+ * Returns the attachments contents in order of appearance
+ *
+ * @return Attachment[]
+ */
+ public function getInlineParts($type = 'text')
+ {
+ $inline_parts = [];
+ $mime_types = [
+ 'text' => 'text/plain',
+ 'html' => 'text/html',
+ ];
+
+ if (!in_array($type, array_keys($mime_types))) {
+ throw new Exception('Invalid type specified for getInlineParts(). "type" can either be text or html.');
+ }
+
+ foreach ($this->parts as $partId => $part) {
+ if ($this->getPart('content-type', $part) == $mime_types[$type]
+ && $this->getPart('content-disposition', $part) != 'attachment'
+ && !$this->partIdIsChildOfAnAttachment($partId)
+ ) {
+ $headers = $this->getPart('headers', $part);
+ $encodingType = array_key_exists('content-transfer-encoding', $headers) ?
+ $headers['content-transfer-encoding'] : '';
+ $undecoded_body = $this->decodeContentTransfer($this->getPartBody($part), $encodingType);
+ $inline_parts[] = $this->charset->decodeCharset($undecoded_body, $this->getPartCharset($part));
+ }
+ }
+
+ return $inline_parts;
+ }
+
+ /**
+ * Returns the attachments contents in order of appearance
+ *
+ * @return Attachment[]
+ */
+ public function getAttachments($include_inline = true)
+ {
+ $attachments = [];
+ $dispositions = $include_inline ? ['attachment', 'inline'] : ['attachment'];
+ $non_attachment_types = ['text/plain', 'text/html'];
+ $nonameIter = 0;
+
+ foreach ($this->parts as $part) {
+ $disposition = $this->getPart('content-disposition', $part);
+ $filename = 'noname';
+
+ if (isset($part['disposition-filename'])) {
+ $filename = $this->decodeHeader($part['disposition-filename']);
+ } elseif (isset($part['content-name'])) {
+ // if we have no disposition but we have a content-name, it's a valid attachment.
+ // we simulate the presence of an attachment disposition with a disposition filename
+ $filename = $this->decodeHeader($part['content-name']);
+ $disposition = 'attachment';
+ } elseif (in_array($part['content-type'], $non_attachment_types, true)
+ && $disposition !== 'attachment') {
+ // it is a message body, no attachment
+ continue;
+ } elseif (substr($part['content-type'], 0, 10) !== 'multipart/'
+ && $part['content-type'] !== 'text/plain; (error)') {
+ // if we cannot get it by getMessageBody(), we assume it is an attachment
+ $disposition = 'attachment';
+ }
+ if (in_array($disposition, ['attachment', 'inline']) === false && !empty($disposition)) {
+ $disposition = 'attachment';
+ }
+
+ if (in_array($disposition, $dispositions) === true) {
+ if ($filename == 'noname') {
+ $nonameIter++;
+ $filename = 'noname'.$nonameIter;
+ } else {
+ // Escape all potentially unsafe characters from the filename
+ $filename = preg_replace('((^\.)|\/|[\n|\r|\n\r]|(\.$))', '_', $filename);
+ }
+
+ $headersAttachments = $this->getPart('headers', $part);
+ $contentidAttachments = $this->getPart('content-id', $part);
+
+ $attachmentStream = $this->getAttachmentStream($part);
+ $mimePartStr = $this->getPartComplete($part);
+
+ $attachments[] = new Attachment(
+ $filename,
+ $this->getPart('content-type', $part),
+ $attachmentStream,
+ $disposition,
+ $contentidAttachments,
+ $headersAttachments,
+ $mimePartStr
+ );
+ }
+ }
+
+ return $attachments;
+ }
+
+ /**
+ * Save attachments in a folder
+ *
+ * @param string $attach_dir directory
+ * @param bool $include_inline
+ * @param string $filenameStrategy How to generate attachment filenames
+ *
+ * @return array Saved attachments paths
+ * @throws Exception
+ */
+ public function saveAttachments(
+ $attach_dir,
+ $include_inline = true,
+ $filenameStrategy = self::ATTACHMENT_DUPLICATE_SUFFIX
+ ) {
+ $attachments = $this->getAttachments($include_inline);
+
+ $attachments_paths = [];
+ foreach ($attachments as $attachment) {
+ $attachments_paths[] = $attachment->save($attach_dir, $filenameStrategy);
+ }
+
+ return $attachments_paths;
+ }
+
+ /**
+ * Read the attachment Body and save temporary file resource
+ *
+ * @param array $part
+ *
+ * @return resource Mime Body Part
+ * @throws Exception
+ */
+ protected function getAttachmentStream(&$part)
+ {
+ /** @var resource $temp_fp */
+ $temp_fp = tmpfile();
+
+ $headers = $this->getPart('headers', $part);
+ $encodingType = array_key_exists('content-transfer-encoding', $headers) ?
+ $headers['content-transfer-encoding'] : '';
+
+ if ($temp_fp) {
+ if ($this->stream) {
+ $start = $part['starting-pos-body'];
+ $end = $part['ending-pos-body'];
+ fseek($this->stream, $start, SEEK_SET);
+ $len = $end - $start;
+ $written = 0;
+ while ($written < $len) {
+ $write = $len;
+ $data = fread($this->stream, $write);
+ fwrite($temp_fp, $this->decodeContentTransfer($data, $encodingType));
+ $written += $write;
+ }
+ } elseif ($this->data) {
+ $attachment = $this->decodeContentTransfer($this->getPartBodyFromText($part), $encodingType);
+ fwrite($temp_fp, $attachment, strlen($attachment));
+ }
+ fseek($temp_fp, 0, SEEK_SET);
+ } else {
+ throw new Exception(
+ 'Could not create temporary files for attachments. Your tmp directory may be unwritable by PHP.'
+ );
+ }
+
+ return $temp_fp;
+ }
+
+ /**
+ * Decode the string from Content-Transfer-Encoding
+ *
+ * @param string $encodedString The string in its original encoded state
+ * @param string $encodingType The encoding type from the Content-Transfer-Encoding header of the part.
+ *
+ * @return string The decoded string
+ */
+ protected function decodeContentTransfer($encodedString, $encodingType)
+ {
+ if (is_array($encodingType)) {
+ $encodingType = $encodingType[0];
+ }
+
+ $encodingType = strtolower($encodingType);
+ if ($encodingType == 'base64') {
+ return base64_decode($encodedString);
+ } elseif ($encodingType == 'quoted-printable') {
+ return quoted_printable_decode($encodedString);
+ } else {
+ return $encodedString;
+ }
+ }
+
+ /**
+ * $input can be a string or array
+ *
+ * @param string|array $input
+ *
+ * @return string
+ */
+ protected function decodeHeader($input)
+ {
+ //Sometimes we have 2 label From so we take only the first
+ if (is_array($input)) {
+ return $this->decodeSingleHeader($input[0]);
+ }
+
+ return $this->decodeSingleHeader($input);
+ }
+
+ /**
+ * Decodes a single header (= string)
+ *
+ * @param string $input
+ *
+ * @return string
+ */
+ protected function decodeSingleHeader($input)
+ {
+ // For each encoded-word...
+ while (preg_match('/(=\?([^?]+)\?(q|b)\?([^?]*)\?=)((\s+)=\?)?/i', $input, $matches)) {
+ $encoded = $matches[1];
+ $charset = $matches[2];
+ $encoding = $matches[3];
+ $text = $matches[4];
+ $space = isset($matches[6]) ? $matches[6] : '';
+
+ switch (strtolower($encoding)) {
+ case 'b':
+ $text = $this->decodeContentTransfer($text, 'base64');
+ break;
+
+ case 'q':
+ $text = str_replace('_', ' ', $text);
+ preg_match_all('/=([a-f0-9]{2})/i', $text, $matches);
+ foreach ($matches[1] as $value) {
+ $text = str_replace('='.$value, chr(hexdec($value)), $text);
+ }
+ break;
+ }
+
+ $text = $this->charset->decodeCharset($text, $this->charset->getCharsetAlias($charset));
+ $input = str_replace($encoded.$space, $text, $input);
+ }
+
+ return $input;
+ }
+
+ /**
+ * Return the charset of the MIME part
+ *
+ * @param array $part
+ *
+ * @return string
+ */
+ protected function getPartCharset($part)
+ {
+ if (isset($part['charset'])) {
+ return $this->charset->getCharsetAlias($part['charset']);
+ } else {
+ return 'us-ascii';
+ }
+ }
+
+ /**
+ * Retrieve a specified MIME part
+ *
+ * @param string $type
+ * @param array $parts
+ *
+ * @return string|array
+ */
+ protected function getPart($type, $parts)
+ {
+ return (isset($parts[$type])) ? $parts[$type] : false;
+ }
+
+ /**
+ * Retrieve the Body of a MIME part
+ *
+ * @param array $part
+ *
+ * @return string
+ */
+ protected function getPartBody(&$part)
+ {
+ $body = '';
+ if ($this->stream) {
+ $body = $this->getPartBodyFromFile($part);
+ } elseif ($this->data) {
+ $body = $this->getPartBodyFromText($part);
+ }
+
+ return $body;
+ }
+
+ /**
+ * Retrieve the Body from a MIME part from file
+ *
+ * @param array $part
+ *
+ * @return string Mime Body Part
+ */
+ protected function getPartBodyFromFile(&$part)
+ {
+ $start = $part['starting-pos-body'];
+ $end = $part['ending-pos-body'];
+ $body = '';
+ if ($end - $start > 0) {
+ fseek($this->stream, $start, SEEK_SET);
+ $body = fread($this->stream, $end - $start);
+ }
+
+ return $body;
+ }
+
+ /**
+ * Retrieve the Body from a MIME part from text
+ *
+ * @param array $part
+ *
+ * @return string Mime Body Part
+ */
+ protected function getPartBodyFromText(&$part)
+ {
+ $start = $part['starting-pos-body'];
+ $end = $part['ending-pos-body'];
+
+ return substr($this->data, $start, $end - $start);
+ }
+
+ /**
+ * Retrieve the content of a MIME part
+ *
+ * @param array $part
+ *
+ * @return string
+ */
+ protected function getPartComplete(&$part)
+ {
+ $body = '';
+ if ($this->stream) {
+ $body = $this->getPartFromFile($part);
+ } elseif ($this->data) {
+ $body = $this->getPartFromText($part);
+ }
+
+ return $body;
+ }
+
+ /**
+ * Retrieve the content from a MIME part from file
+ *
+ * @param array $part
+ *
+ * @return string Mime Content
+ */
+ protected function getPartFromFile(&$part)
+ {
+ $start = $part['starting-pos'];
+ $end = $part['ending-pos'];
+ $body = '';
+ if ($end - $start > 0) {
+ fseek($this->stream, $start, SEEK_SET);
+ $body = fread($this->stream, $end - $start);
+ }
+
+ return $body;
+ }
+
+ /**
+ * Retrieve the content from a MIME part from text
+ *
+ * @param array $part
+ *
+ * @return string Mime Content
+ */
+ protected function getPartFromText(&$part)
+ {
+ $start = $part['starting-pos'];
+ $end = $part['ending-pos'];
+
+ return substr($this->data, $start, $end - $start);
+ }
+
+ /**
+ * Retrieve the resource
+ *
+ * @return resource resource
+ */
+ public function getResource()
+ {
+ return $this->resource;
+ }
+
+ /**
+ * Retrieve the file pointer to email
+ *
+ * @return resource stream
+ */
+ public function getStream()
+ {
+ return $this->stream;
+ }
+
+ /**
+ * Retrieve the text of an email
+ *
+ * @return string data
+ */
+ public function getData()
+ {
+ return $this->data;
+ }
+
+ /**
+ * Retrieve the parts of an email
+ *
+ * @return array parts
+ */
+ public function getParts()
+ {
+ return $this->parts;
+ }
+
+ /**
+ * Retrieve the charset manager object
+ *
+ * @return CharsetManager charset
+ */
+ public function getCharset()
+ {
+ return $this->charset;
+ }
+
+ /**
+ * Add a middleware to the parser MiddlewareStack
+ * Each middleware is invoked when:
+ * a MimePart is retrieved by mailparse_msg_get_part_data() during $this->parse()
+ * The middleware will receive MimePart $part and the next MiddlewareStack $next
+ *
+ * Eg:
+ *
+ * $Parser->addMiddleware(function(MimePart $part, MiddlewareStack $next) {
+ * // do something with the $part
+ * return $next($part);
+ * });
+ *
+ * @param callable $middleware Plain Function or Middleware Instance to execute
+ * @return void
+ */
+ public function addMiddleware(callable $middleware)
+ {
+ if (!$middleware instanceof Middleware) {
+ $middleware = new Middleware($middleware);
+ }
+ $this->middlewareStack = $this->middlewareStack->add($middleware);
+ }
+}