diff --git a/legacy_cron_ticket_email_parser.php b/legacy_cron_ticket_email_parser.php
new file mode 100644
index 00000000..3d013da1
--- /dev/null
+++ b/legacy_cron_ticket_email_parser.php
@@ -0,0 +1,556 @@
+ Ticketing > Email-to-ticket parsing. See https://docs.itflow.org/ticket_email_parse -- Quitting..");
+}
+
+$argv = $_SERVER['argv'];
+
+// Check Cron Key
+if ( $argv[1] !== $config_cron_key ) {
+ exit("Cron Key invalid -- Quitting..");
+}
+
+// Check IMAP extension works/installed
+if (!function_exists('imap_open')) {
+ exit("Email Parser: PHP IMAP extension is not installed. See https://docs.itflow.org/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://docs.itflow.org/ticket_email_parse -- Quitting..");
+}
+
+// Get system temp directory
+$temp_dir = sys_get_temp_dir();
+
+// Create the path for the lock file using the temp directory
+$lock_file_path = "{$temp_dir}/itflow_legacy_email_parser_{$installation_id}.lock";
+
+// Check for lock file to prevent concurrent script runs
+if (file_exists($lock_file_path)) {
+ $file_age = time() - filemtime($lock_file_path);
+
+ // If file is older than 3 minutes (180 seconds), delete and continue
+ if ($file_age > 300) {
+ unlink($lock_file_path);
+ mysqli_query($mysqli, "INSERT INTO logs SET log_type = 'Cron-Email-Parser', log_action = 'Delete', log_description = 'Cron Email Parser detected a lock file was present but was over 10 minutes old so it removed it'");
+ } else {
+ mysqli_query($mysqli, "INSERT INTO logs SET log_type = 'Cron-Email-Parser', log_action = 'Locked', log_description = 'Cron Email Parser attempted to execute but was already executing, so instead it terminated.'");
+ exit("Script is already running. Exiting.");
+ }
+}
+
+// Create a lock file
+file_put_contents($lock_file_path, "Locked");
+
+// PHP Mail Parser
+use PhpMimeMailParser\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";
+
+
+// Allowed attachment extensions
+$allowed_extensions = array('jpg', 'jpeg', 'gif', 'png', 'webp', 'pdf', 'txt', 'md', 'doc', 'docx', 'csv', 'xls', 'xlsx', 'xlsm', 'zip', 'tar', 'gz');
+
+// Function to raise a new ticket for a given contact and email them confirmation (if configured)
+function addTicket($contact_id, $contact_name, $contact_email, $client_id, $date, $subject, $message, $attachments, $original_message_file) {
+
+ // Access global variables
+ global $mysqli,$config_app_name, $company_name, $company_phone, $config_ticket_prefix, $config_ticket_client_general_notifications, $config_ticket_new_ticket_notification_email, $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, $allowed_extensions;
+
+ // 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 = 1"));
+ $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 = 1");
+
+ // Prep ticket details
+ $message = nl2br($message);
+ $message = mysqli_escape_string($mysqli, "Email from: $contact_email at $date:-
$message");
+
+ 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 = 1, ticket_created_by = 0, ticket_contact_id = $contact_id, ticket_client_id = $client_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) ($id)', log_client_id = $client_id");
+
+ // -- Process attachments (after ticket is logged as created because we save to the folder named after the ticket ID) --
+
+ mkdirMissing('uploads/tickets/'); // Create tickets dir
+
+ // Setup directory for this ticket ID
+ $att_dir = "uploads/tickets/" . $id . "/";
+ mkdirMissing($att_dir);
+
+ // Save original email message as ticket attachment
+ rename("uploads/tmp/{$original_message_file}", "{$att_dir}/{$original_message_file}");
+ mysqli_query($mysqli, "INSERT INTO ticket_attachments SET ticket_attachment_name = 'Original-parsed-email.eml', ticket_attachment_reference_name = '$original_message_file', ticket_attachment_ticket_id = $id");
+
+ // Process each attachment
+ foreach ($attachments as $attachment) {
+
+ // Get name and extension
+ $att_name = $attachment->getFileName();
+ $att_extarr = explode('.', $att_name);
+ $att_extension = strtolower(end($att_extarr));
+
+ // Check the extension is allowed
+ if (in_array($att_extension, $allowed_extensions)) {
+
+ // Save attachment with a random name
+ $att_saved_path = $attachment->save($att_dir, Parser::ATTACHMENT_RANDOM_FILENAME);
+
+ // Access the random name to add into the database (this won't work on Windows)
+ $att_tmparr = explode($att_dir, $att_saved_path);
+
+ $ticket_attachment_name = sanitizeInput($att_name);
+ $ticket_attachment_reference_name = sanitizeInput(end($att_tmparr));
+
+ mysqli_query($mysqli, "INSERT INTO ticket_attachments SET ticket_attachment_name = '$ticket_attachment_name', ticket_attachment_reference_name = '$ticket_attachment_reference_name', ticket_attachment_ticket_id = $id");
+
+ } else {
+ $ticket_attachment_name = sanitizeInput($att_name);
+ mysqli_query($mysqli, "INSERT INTO logs SET log_type = 'Ticket', log_action = 'Update', log_description = 'Email parser: Blocked attachment $ticket_attachment_name from Client contact $contact_email for ticket $config_ticket_prefix$ticket_number', log_client_id = $client_id");
+ }
+
+ }
+
+ $data = [];
+ // E-mail client notification that ticket has been created
+ if ($config_ticket_client_general_notifications == 1) {
+
+ $subject_email = "Ticket created - [$config_ticket_prefix$ticket_number] - $subject";
+ $body = "##- Please type your reply above this line -##
Hello $contact_name,
Thank you for your email. A ticket regarding \"$subject\" has been automatically created for you.
Ticket: $config_ticket_prefix$ticket_number
Subject: $subject
Status: New
https://$config_base_url/portal/ticket.php?id=$id
--
$company_name - Support
$config_ticket_from_email
$company_phone";
+
+ $data[] = [
+ 'from' => $config_ticket_from_email,
+ 'from_name' => $config_ticket_from_name,
+ 'recipient' => $contact_email,
+ 'recipient_name' => $contact_name,
+ 'subject' => $subject_email,
+ 'body' => $body
+ ];
+ }
+
+ // Notify agent DL of the new ticket, if populated with a valid email
+ if ($config_ticket_new_ticket_notification_email) {
+
+ // Get client info
+ $client_sql = mysqli_query($mysqli, "SELECT client_name FROM clients WHERE client_id = $client_id");
+ $client_row = mysqli_fetch_array($client_sql);
+ $client_name = sanitizeInput($client_row['client_name']);
+
+ $email_subject = "$config_app_name - New Ticket - $client_name: $subject";
+ $email_body = "Hello,
This is a notification that a new ticket has been raised in ITFlow.
Client: $client_name
Priority: Low (email parsed)
Link: https://$config_base_url/ticket.php?ticket_id=$id
--------------------------------
$subject
$details";
+
+ $data[] = [
+ 'from' => $config_ticket_from_email,
+ 'from_name' => $config_ticket_from_name,
+ 'recipient' => $config_ticket_new_ticket_notification_email,
+ 'recipient_name' => $config_ticket_from_name,
+ 'subject' => $email_subject,
+ 'body' => $email_body
+ ];
+ }
+
+ addToMailQueue($mysqli, $data);
+
+ return true;
+
+}
+// End Add Ticket Function
+
+// Add Reply Function
+function addReply($from_email, $date, $subject, $ticket_number, $message, $attachments) {
+ // Add email as a comment/reply to an existing ticket
+
+ // Access global variables
+ global $mysqli, $config_app_name, $company_name, $company_phone, $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, $allowed_extensions;
+
+ // Set default reply type
+ $ticket_reply_type = 'Client';
+
+ // Capture just the latest/most recent email reply content
+ // based off the "##- Please type your reply above this line -##" line that we prepend the outgoing emails with
+ $message = explode("##- Please type your reply above this line -##", $message);
+ $message = nl2br($message[0]);
+ $message = mysqli_escape_string($mysqli, "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, contact_email, client_name
+ FROM tickets
+ LEFT JOIN contacts on tickets.ticket_contact_id = contacts.contact_id
+ LEFT JOIN clients on tickets.ticket_client_id = clients.client_id
+ WHERE ticket_number = $ticket_number LIMIT 1"));
+
+ if ($row) {
+
+ // Get ticket details
+ $ticket_id = intval($row['ticket_id']);
+ $ticket_subject = sanitizeInput($row['ticket_subject']);
+ $ticket_status = sanitizeInput($row['ticket_status']);
+ $ticket_reply_contact = intval($row['ticket_contact_id']);
+ $ticket_contact_email = sanitizeInput($row['contact_email']);
+ $client_id = intval($row['ticket_client_id']);
+ $client_name = sanitizeInput($row['client_name']);
+
+ // Check ticket isn't closed - tickets can't be re-opened
+ if ($ticket_status == 5) {
+ 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_action = 'ticket.php?ticket_id=$ticket_id', notification_client_id = $client_id");
+
+ $email_subject = "Action required: This ticket is already closed";
+ $email_body = "Hi there,
You\'ve tried to reply to a ticket that is closed - we won\'t see your response.
Please raise a new ticket by sending a fresh e-mail to our support address below.
--
$company_name - Support
$config_ticket_from_email
$company_phone";
+
+ $data = [
+ [
+ 'from' => $config_ticket_from_email,
+ 'from_name' => $config_ticket_from_name,
+ 'recipient' => $from_email,
+ 'recipient_name' => $from_email,
+ 'subject' => $email_subject,
+ 'body' => $email_body
+ ]
+ ];
+
+ addToMailQueue($mysqli, $data);
+
+ return true;
+ }
+
+ // 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 = intval($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)
+ }
+ }
+
+ // Add the comment
+ mysqli_query($mysqli, "INSERT INTO ticket_replies SET ticket_reply = '$message', ticket_reply_type = '$ticket_reply_type', ticket_reply_time_worked = '00:00:00', ticket_reply_by = $ticket_reply_contact, ticket_reply_ticket_id = $ticket_id");
+
+ $reply_id = mysqli_insert_id($mysqli);
+
+ // Process attachments
+ mkdirMissing('uploads/tickets/');
+ foreach ($attachments as $attachment) {
+
+ // Get name and extension
+ $att_name = $attachment->getFileName();
+ $att_extarr = explode('.', $att_name);
+ $att_extension = strtolower(end($att_extarr));
+
+ // Check the extension is allowed
+ if (in_array($att_extension, $allowed_extensions)) {
+
+ // Setup directory for this ticket ID
+ $att_dir = "uploads/tickets/" . $ticket_id . "/";
+ mkdirMissing($att_dir);
+
+ // Save attachment with a random name
+ $att_saved_path = $attachment->save($att_dir, Parser::ATTACHMENT_RANDOM_FILENAME);
+
+ // Access the random name to add into the database (this won't work on Windows)
+ $att_tmparr = explode($att_dir, $att_saved_path);
+
+ $ticket_attachment_name = sanitizeInput($att_name);
+ $ticket_attachment_reference_name = sanitizeInput(end($att_tmparr));
+
+ mysqli_query($mysqli, "INSERT INTO ticket_attachments SET ticket_attachment_name = '$ticket_attachment_name', ticket_attachment_reference_name = '$ticket_attachment_reference_name', ticket_attachment_reply_id = $reply_id, ticket_attachment_ticket_id = $ticket_id");
+
+ } else {
+ $ticket_attachment_name = sanitizeInput($att_name);
+ mysqli_query($mysqli, "INSERT INTO logs SET log_type = 'Ticket', log_action = 'Update', log_description = 'Email parser: Blocked attachment $ticket_attachment_name from Client contact $from_email for ticket $config_ticket_prefix$ticket_number', log_client_id = $client_id");
+ }
+
+ }
+
+ // E-mail techs assigned to the ticket to notify them of the reply
+ $ticket_assigned_to = mysqli_query($mysqli, "SELECT ticket_assigned_to FROM tickets WHERE ticket_id = $ticket_id LIMIT 1");
+
+ if ($ticket_assigned_to) {
+
+ $row = mysqli_fetch_array($ticket_assigned_to);
+ $ticket_assigned_to = intval($row['ticket_assigned_to']);
+
+ if ($ticket_assigned_to) {
+
+ // Get tech details
+ $tech_sql = mysqli_query($mysqli, "SELECT user_email, user_name FROM users WHERE user_id = $ticket_assigned_to LIMIT 1");
+ $tech_row = mysqli_fetch_array($tech_sql);
+ $tech_email = sanitizeInput($tech_row['user_email']);
+ $tech_name = sanitizeInput($tech_row['user_name']);
+
+ $email_subject = "$config_app_name - Ticket updated - [$config_ticket_prefix$ticket_number] $ticket_subject";
+ $email_body = "Hello $tech_name,
A new reply has been added to the below ticket, check ITFlow for full details.
Client: $client_name
Ticket: $config_ticket_prefix$ticket_number
Subject: $ticket_subject
https://$config_base_url/ticket.php?ticket_id=$ticket_id";
+
+ $data = [
+ [
+ 'from' => $config_ticket_from_email,
+ 'from_name' => $config_ticket_from_name,
+ 'recipient' => $tech_email,
+ 'recipient_name' => $tech_name,
+ 'subject' => $email_subject,
+ 'body' => $email_body
+ ]
+ ];
+
+ addToMailQueue($mysqli, $data);
+
+ }
+
+ }
+
+ // Update Ticket Last Response Field & set ticket to open as client has replied
+ mysqli_query($mysqli, "UPDATE tickets SET ticket_status = 2 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_client_id = $client_id");
+
+ return true;
+
+ } else {
+ // Invalid ticket number
+ return false;
+ }
+}
+// END ADD REPLY FUNCTION -------------------------------------------------
+
+// Prepare connection string with encryption (TLS/SSL/)
+$imap_mailbox = "$config_imap_host:$config_imap_port/imap/$config_imap_encryption";
+
+// Connect to host via IMAP
+$imap = imap_open("{{$imap_mailbox}}INBOX", $config_imap_username, $config_imap_password);
+
+// Check connection
+if (!$imap) {
+ // Logging
+ //$extended_log_description = var_export(imap_errors(), true);
+ // Remove the lock file
+ unlink($lock_file_path);
+ mysqli_query($mysqli, "INSERT INTO logs SET log_type = 'Mail', log_action = 'Error', log_description = 'Email parser: Failed to connect to IMAP. Details'");
+ exit("Could not connect to IMAP");
+}
+
+// Check for the ITFlow mailbox that we move messages to once processed
+$imap_folder = 'ITFlow';
+$list = imap_list($imap, "{{$imap_mailbox}}", "*");
+if (array_search("{{$imap_mailbox}}$imap_folder", $list) === false) {
+ imap_createmailbox($imap, imap_utf7_encode("{{$imap_mailbox}}$imap_folder"));
+ imap_subscribe($imap, imap_utf7_encode("{{$imap_mailbox}}$imap_folder"));
+}
+
+// Search for unread ("UNSEEN") emails
+$emails = imap_search($imap, 'UNSEEN');
+
+if ($emails) {
+
+ // Sort
+ rsort($emails);
+
+ // Loop through each email
+ foreach ($emails as $email) {
+
+ // Default false
+ $email_processed = false;
+
+ // Save the original email (to be moved later)
+ mkdirMissing('uploads/tmp/'); // Create tmp dir
+ $original_message_file = "processed-eml-" . randomString(200) . ".eml";
+ imap_savebody($imap, "uploads/tmp/{$original_message_file}", $email);
+
+ // Get details from message and invoke PHP Mime Mail Parser
+ $msg_to_parse = imap_fetchheader($imap, $email, FT_PREFETCHTEXT) . imap_body($imap, $email, FT_PEEK);
+ $parser = new PhpMimeMailParser\Parser();
+ $parser->setText($msg_to_parse);
+
+ // Process message attributes
+
+ $from_array = $parser->getAddresses('from')[0];
+ $from_name = sanitizeInput($from_array['display']);
+
+ // Handle blank 'From' emails
+ $from_email = "itflow-guest@example.com";
+ if (filter_var($from_array['address'], FILTER_VALIDATE_EMAIL)) {
+ $from_email = sanitizeInput($from_array['address']);
+ }
+
+ $from_domain = explode("@", $from_array['address']);
+ $from_domain = sanitizeInput(end($from_domain));
+
+ $subject = sanitizeInput($parser->getHeader('subject'));
+ $date = sanitizeInput($parser->getHeader('date'));
+ $attachments = $parser->getAttachments();
+
+ // Get the message content
+ // (first try HTML parsing, but switch to plain text if the email is empty/plain-text only)
+// $message = $parser->getMessageBody('htmlEmbedded');
+// if (empty($message)) {
+// echo "DEBUG: Switching to plain text parsing for this message ($subject)";
+// $message = $parser->getMessageBody('text');
+// }
+
+ // TODO: Default to getting HTML and fallback to plaintext, but HTML emails seem to break the forward/agent notifications
+
+ $message = $parser->getMessageBody('text');
+
+ // 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]);
+
+ if (addReply($from_email, $date, $subject, $ticket_number, $message, $attachments)) {
+ $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_email' LIMIT 1");
+ $row = mysqli_fetch_array($any_contact_sql);
+
+ if ($row) {
+ // Sender exists as a contact
+ $contact_name = sanitizeInput($row['contact_name']);
+ $contact_id = intval($row['contact_id']);
+ $contact_email = sanitizeInput($row['contact_email']);
+ $client_id = intval($row['contact_client_id']);
+
+ if (addTicket($contact_id, $contact_name, $contact_email, $client_id, $date, $subject, $message, $attachments, $original_message_file)) {
+ $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 domains WHERE domain_name = '$from_domain' LIMIT 1"));
+
+ if ($row && $from_domain == $row['domain_name']) {
+
+ // We found a match - create a contact under this client and raise a ticket for them
+
+ // Client details
+ $client_id = intval($row['domain_client_id']);
+
+ // Contact details
+ $password = password_hash(randomString(), PASSWORD_DEFAULT);
+ $contact_name = $from_name; // This was already Sanitized above
+ $contact_email = $from_email; // This was already Sanitized above
+ 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");
+ $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");
+
+ if (addTicket($contact_id, $contact_name, $contact_email, $client_id, $date, $subject, $message, $attachments, $original_message_file)) {
+ $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 in the Inbox as needing attention
+
+ }
+
+ }
+
+ }
+
+ // Deal with the message (move it if processed, flag it if not)
+ if ($email_processed) {
+ imap_setflag_full($imap, $email, "\\Seen");
+ imap_mail_move($imap, $email, $imap_folder);
+ } else {
+ // Basically just flags all emails to be manually checked
+ echo "Failed to process email - flagging for manual review.";
+ imap_setflag_full($imap, $email, "\\Flagged");
+ }
+
+ // Remove temp original message if still there
+ if (file_exists("uploads/tmp/{$original_message_file}")) {
+ unlink("uploads/tmp/{$original_message_file}");
+ }
+
+ }
+
+}
+
+imap_expunge($imap);
+imap_close($imap);
+
+// Remove the lock file
+unlink($lock_file_path);
diff --git a/plugins/php-mime-mail-parser/Attachment.php b/plugins/php-mime-mail-parser/Attachment.php
new file mode 100644
index 00000000..1a731635
--- /dev/null
+++ b/plugins/php-mime-mail-parser/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/Charset.php b/plugins/php-mime-mail-parser/Charset.php
new file mode 100644
index 00000000..0824f947
--- /dev/null
+++ b/plugins/php-mime-mail-parser/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 = array_diff(mb_list_encodings(), ['BASE64', 'UUENCODE', 'HTML-ENTITIES', 'Quoted-Printable']),
+ call_user_func_array(
+ 'array_merge',
+ array_map(
+ "mb_encoding_aliases",
+ $enc
+ )
+ )
+ )
+ )
+ );
+ }
+}
diff --git a/plugins/php-mime-mail-parser/Contracts/CharsetManager.php b/plugins/php-mime-mail-parser/Contracts/CharsetManager.php
new file mode 100644
index 00000000..660ec00c
--- /dev/null
+++ b/plugins/php-mime-mail-parser/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/MiddlewareStack.php b/plugins/php-mime-mail-parser/MiddlewareStack.php
new file mode 100644
index 00000000..3ef6da93
--- /dev/null
+++ b/plugins/php-mime-mail-parser/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/MimePart.php b/plugins/php-mime-mail-parser/MimePart.php
new file mode 100644
index 00000000..d2211b7c
--- /dev/null
+++ b/plugins/php-mime-mail-parser/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/Parser.php b/plugins/php-mime-mail-parser/Parser.php
new file mode 100644
index 00000000..94c2a300
--- /dev/null
+++ b/plugins/php-mime-mail-parser/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|array|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|false
+ */
+ 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 of associative arrays 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)' && $disposition != 'inline') {
+ // 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);
+ }
+}