mirror of https://github.com/itflow-org/itflow
Merge pull request #592 from wrongecho/email-parsing-lib
Email ticket parsing enhancements
This commit is contained in:
commit
17f88f88a0
|
|
@ -66,6 +66,7 @@
|
||||||
* MariaDB
|
* MariaDB
|
||||||
* PHPMailer
|
* PHPMailer
|
||||||
* HTML Purifier
|
* HTML Purifier
|
||||||
|
* PHP Mime Mail Parser
|
||||||
|
|
||||||
* CSS
|
* CSS
|
||||||
* Bootstrap
|
* 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)
|
1. Install a LAMP stack (Linux, Apache, MariaDB, PHP)
|
||||||
```sh
|
```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
|
2. Clone the repo
|
||||||
```sh
|
```sh
|
||||||
|
|
|
||||||
|
|
@ -10,58 +10,73 @@ TODO:
|
||||||
- Process unregistered contacts/clients into an inbox to allow a ticket to be created/ignored
|
- Process unregistered contacts/clients into an inbox to allow a ticket to be created/ignored
|
||||||
- Better handle replying to closed tickets
|
- Better handle replying to closed tickets
|
||||||
- Support for authenticating with OAuth
|
- Support for authenticating with OAuth
|
||||||
- Documentation
|
|
||||||
- Separate Mailbox Account for tickets 2022-12-14 - JQ
|
- 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
|
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
|
// Get ITFlow config & helper functions
|
||||||
include_once("config.php");
|
require_once("config.php");
|
||||||
include_once("functions.php");
|
require_once("functions.php");
|
||||||
|
|
||||||
// Get settings for the "default" company
|
// Get settings for the "default" company
|
||||||
$company_id = 1;
|
$company_id = 1;
|
||||||
$session_company_id = 1;
|
$session_company_id = 1;
|
||||||
include_once("get_settings.php");
|
require_once("get_settings.php");
|
||||||
|
|
||||||
// Check setting enabled
|
// Check setting enabled
|
||||||
if ($config_ticket_email_parse == 0) {
|
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')) {
|
if (!function_exists('imap_open')) {
|
||||||
echo "PHP IMAP extension is not installed, quitting..";
|
exit("Email Parser: PHP IMAP extension is not installed. See https://wiki.itflow.org/doku.php?id=wiki:ticket_email_parse -- Quitting..");
|
||||||
exit();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 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
|
// 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
|
// Prep ticket details
|
||||||
$message = nl2br(htmlentities(strip_tags($message)));
|
$message = nl2br(htmlentities(strip_tags($message)));
|
||||||
$message = trim(mysqli_real_escape_string($mysqli,"<i>Email from: $contact_email at $date:-</i> <br><br>$message"));
|
$message = trim(mysqli_real_escape_string($mysqli, "<i>Email from: $contact_email at $date:-</i> <br><br>$message"));
|
||||||
|
|
||||||
// Get the next Ticket Number and add 1 for the new ticket number
|
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");
|
||||||
$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");
|
|
||||||
$id = mysqli_insert_id($mysqli);
|
$id = mysqli_insert_id($mysqli);
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
echo "Created new ticket.<br>";
|
echo "Created new ticket.<br>";
|
||||||
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
|
// 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);
|
$row = mysqli_fetch_array($sql);
|
||||||
$company_phone = formatPhoneNumber($row['company_phone']);
|
$company_phone = formatPhoneNumber($row['company_phone']);
|
||||||
$company_name = $row['company_name'];
|
$company_name = $row['company_name'];
|
||||||
|
|
@ -79,8 +94,8 @@ function createTicket($contact_id, $contact_name, $contact_email, $client_id, $c
|
||||||
$email_subject, $email_body);
|
$email_subject, $email_body);
|
||||||
|
|
||||||
if ($mail !== true) {
|
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 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 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 = "<i>Email from: $from_email at $date:-</i> <br><br>$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 = "<b>WARNING: Contact email mismatch</b><br>$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.<br>";
|
||||||
|
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/<blank>)
|
// Prepare connection string with encryption (TLS/SSL/<blank>)
|
||||||
$imap_mailbox = "$config_imap_host:$config_imap_port/imap/$config_imap_encryption";
|
$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) {
|
if (!$imap) {
|
||||||
// Logging
|
// Logging
|
||||||
$extended_log_description = var_export(imap_errors(), true);
|
$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");
|
exit("Could not connect to IMAP");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -124,108 +216,66 @@ if ($emails) {
|
||||||
// Default false
|
// Default false
|
||||||
$email_processed = false;
|
$email_processed = false;
|
||||||
|
|
||||||
// Get message details
|
// Get details from message and invoke PHP Mime Mail Parser
|
||||||
$metadata = imap_fetch_overview($imap, $email,0); // Date, Subject, Size
|
$msg_to_parse = imap_fetchheader($imap, $email, FT_PREFETCHTEXT) . imap_body($imap, $email);
|
||||||
$header = imap_headerinfo($imap, $email); // To get the From as an email, not a contact name
|
$parser = new PhpMimeMailParser\Parser();
|
||||||
$message = (imap_fetchbody($imap, $email, 1)); // Body
|
$parser->setText($msg_to_parse);
|
||||||
|
|
||||||
$from = trim(mysqli_real_escape_string($mysqli, htmlentities(strip_tags($header->from[0]->mailbox . "@" . $header->from[0]->host))));
|
// Process message attributes
|
||||||
$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))));
|
$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)
|
// Check if we can identify a ticket number (in square brackets)
|
||||||
if (preg_match("/\[$config_ticket_prefix\d+\]/", $subject, $ticket_number)) {
|
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)
|
// Get the actual ticket number (without the brackets)
|
||||||
preg_match('/\d+/', $ticket_number[0], $ticket_number);
|
preg_match('/\d+/', $ticket_number[0], $ticket_number);
|
||||||
$ticket_number = intval($ticket_number[0]);
|
$ticket_number = intval($ticket_number[0]);
|
||||||
|
|
||||||
// Split the email into just the latest reply, with some metadata
|
if (addReply($from_email, $date, $subject, $ticket_number, $message)) {
|
||||||
// We base this off the string "#--itflow--#" that we prepend the outgoing emails with (similar to the old school --reply above this line--)
|
$email_processed = true;
|
||||||
$message = explode("#--itflow--#", $message);
|
|
||||||
$message = nl2br(htmlentities(strip_tags($message[0])));
|
|
||||||
$message = "<i>Email from: $from at $date:-</i> <br><br>$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 = "<b>WARNING: Contact email mismatch</b><br>$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.<br>";
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Couldn't match this email to an existing ticket
|
// Couldn't match this email to an existing ticket
|
||||||
|
|
||||||
// Check if we can match the sender to a pre-existing contact
|
// 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);
|
$row = mysqli_fetch_array($any_contact_sql);
|
||||||
|
|
||||||
$contact_name = $row['contact_name'];
|
if ($row) {
|
||||||
$contact_id = $row['contact_id'];
|
// Sender exists as a contact
|
||||||
$contact_email = $row['contact_email'];
|
$contact_name = $row['contact_name'];
|
||||||
$client_id = $row['contact_client_id'];
|
$contact_id = $row['contact_id'];
|
||||||
$company_id = $row['company_id'];
|
$contact_email = $row['contact_email'];
|
||||||
|
$client_id = $row['contact_client_id'];
|
||||||
|
$company_id = $row['company_id'];
|
||||||
|
|
||||||
if ($from == $contact_email) {
|
if (addTicket($contact_id, $contact_name, $contact_email, $client_id, $company_id, $date, $subject, $message)) {
|
||||||
|
$email_processed = true;
|
||||||
createTicket($contact_id, $contact_name, $contact_email, $client_id, $company_id, $date, $subject, $message);
|
}
|
||||||
$email_processed = true;
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
// Couldn't match this email to an existing ticket or an existing client contact
|
// 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
|
// 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
|
// We found a match - create a contact under this client and raise a ticket for them
|
||||||
|
|
||||||
|
|
@ -236,22 +286,22 @@ if ($emails) {
|
||||||
// Contact details
|
// Contact details
|
||||||
$password = password_hash(randomString(), PASSWORD_DEFAULT);
|
$password = password_hash(randomString(), PASSWORD_DEFAULT);
|
||||||
$contact_name = $from_name;
|
$contact_name = $from_name;
|
||||||
$contact_email = $from;
|
$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");
|
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);
|
$contact_id = mysqli_insert_id($mysqli);
|
||||||
|
|
||||||
// Logging for contact creation
|
// Logging for contact creation
|
||||||
echo "Created new contact.<br>";
|
echo "Created new contact.<br>";
|
||||||
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);
|
if (addTicket($contact_id, $contact_name, $contact_email, $client_id, $company_id, $date, $subject, $message)) {
|
||||||
|
$email_processed = true;
|
||||||
$email_processed = true;
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
// Couldn't match this email to an existing ticket, existing contact or an existing client via the "from" domain
|
// 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) {
|
if ($email_processed) {
|
||||||
imap_mail_move($imap, $email, $imap_folder);
|
imap_mail_move($imap, $email, $imap_folder);
|
||||||
} else {
|
} else {
|
||||||
|
echo "Failed to process email - flagging for manual review.";
|
||||||
imap_setflag_full($imap, $email, "\\Flagged");
|
imap_setflag_full($imap, $email, "\\Flagged");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -271,6 +322,5 @@ if ($emails) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
imap_expunge($imap);
|
imap_expunge($imap);
|
||||||
imap_close($imap);
|
imap_close($imap);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,276 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace PhpMimeMailParser;
|
||||||
|
|
||||||
|
use function var_dump;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attachment of php-mime-mail-parser
|
||||||
|
*
|
||||||
|
* Fully Tested Mailparse Extension Wrapper for PHP 5.4+
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
class Attachment
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string $filename Filename
|
||||||
|
*/
|
||||||
|
protected $filename;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string $contentType Mime Type
|
||||||
|
*/
|
||||||
|
protected $contentType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string $content File Content
|
||||||
|
*/
|
||||||
|
protected $content;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string $contentDisposition Content-Disposition (attachment or inline)
|
||||||
|
*/
|
||||||
|
protected $contentDisposition;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string $contentId Content-ID
|
||||||
|
*/
|
||||||
|
protected $contentId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array $headers An Array of the attachment headers
|
||||||
|
*/
|
||||||
|
protected $headers;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var resource $stream
|
||||||
|
*/
|
||||||
|
protected $stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string $mimePartStr
|
||||||
|
*/
|
||||||
|
protected $mimePartStr;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var integer $maxDuplicateNumber
|
||||||
|
*/
|
||||||
|
public $maxDuplicateNumber = 100;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attachment constructor.
|
||||||
|
*
|
||||||
|
* @param string $filename
|
||||||
|
* @param string $contentType
|
||||||
|
* @param resource $stream
|
||||||
|
* @param string $contentDisposition
|
||||||
|
* @param string $contentId
|
||||||
|
* @param array $headers
|
||||||
|
* @param string $mimePartStr
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
$filename,
|
||||||
|
$contentType,
|
||||||
|
$stream,
|
||||||
|
$contentDisposition = 'attachment',
|
||||||
|
$contentId = '',
|
||||||
|
$headers = [],
|
||||||
|
$mimePartStr = ''
|
||||||
|
) {
|
||||||
|
$this->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.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,370 @@
|
||||||
|
<?php namespace PhpMimeMailParser;
|
||||||
|
|
||||||
|
use PhpMimeMailParser\Contracts\CharsetManager;
|
||||||
|
|
||||||
|
class Charset implements CharsetManager
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Charset Aliases
|
||||||
|
*/
|
||||||
|
private $charsetAlias = [
|
||||||
|
'ascii' => '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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?php namespace PhpMimeMailParser\Contracts;
|
||||||
|
|
||||||
|
interface CharsetManager
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode the string from Charset
|
||||||
|
*
|
||||||
|
* @param string $encodedString The string in its original encoded state
|
||||||
|
* @param string $charset The Charset header of the part.
|
||||||
|
*
|
||||||
|
* @return string The decoded string
|
||||||
|
*/
|
||||||
|
public function decodeCharset($encodedString, $charset);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get charset alias
|
||||||
|
*
|
||||||
|
* @param string $charset .
|
||||||
|
*
|
||||||
|
* @return string The charset alias
|
||||||
|
*/
|
||||||
|
public function getCharsetAlias($charset);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace PhpMimeMailParser\Contracts;
|
||||||
|
|
||||||
|
use PhpMimeMailParser\MimePart;
|
||||||
|
use PhpMimeMailParser\MiddlewareStack;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process Mime parts by either:
|
||||||
|
* processing the part or calling the $next MiddlewareStack
|
||||||
|
*/
|
||||||
|
interface Middleware
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Process a mime part, optionally delegating parsing to the $next MiddlewareStack
|
||||||
|
*
|
||||||
|
* @param MimePart $part
|
||||||
|
* @param MiddlewareStack $next
|
||||||
|
*
|
||||||
|
* @return MimePart
|
||||||
|
*/
|
||||||
|
public function parse(MimePart $part, MiddlewareStack $next);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace PhpMimeMailParser;
|
||||||
|
|
||||||
|
class Exception extends \RuntimeException
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace PhpMimeMailParser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps a callable as a Middleware
|
||||||
|
*/
|
||||||
|
class Middleware implements Contracts\Middleware
|
||||||
|
{
|
||||||
|
protected $parser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a middleware using a callable $fn
|
||||||
|
*
|
||||||
|
* @param callable $fn
|
||||||
|
*/
|
||||||
|
public function __construct(callable $fn)
|
||||||
|
{
|
||||||
|
$this->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace PhpMimeMailParser;
|
||||||
|
|
||||||
|
use PhpMimeMailParser\Contracts\MiddleWare as MiddleWareContracts;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A stack of middleware chained together by (MiddlewareStack $next)
|
||||||
|
*/
|
||||||
|
class MiddlewareStack
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Next MiddlewareStack in chain
|
||||||
|
*
|
||||||
|
* @var MiddlewareStack
|
||||||
|
*/
|
||||||
|
protected $next;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware in this MiddlewareStack
|
||||||
|
*
|
||||||
|
* @var Middleware
|
||||||
|
*/
|
||||||
|
protected $middleware;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct the first middleware in this MiddlewareStack
|
||||||
|
* The next middleware is chained through $MiddlewareStack->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace PhpMimeMailParser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mime Part
|
||||||
|
* Represents the results of mailparse_msg_get_part_data()
|
||||||
|
*
|
||||||
|
* Note ArrayAccess::offsetSet() cannot modify deeply nestated arrays.
|
||||||
|
* When modifying use getPart() and setPart() for deep nested data modification
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* $MimePart['headers']['from'] = 'modified@example.com' // fails
|
||||||
|
*
|
||||||
|
* // correct
|
||||||
|
* $part = $MimePart->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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,923 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace PhpMimeMailParser;
|
||||||
|
|
||||||
|
use PhpMimeMailParser\Contracts\CharsetManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parser of php-mime-mail-parser
|
||||||
|
*
|
||||||
|
* Fully Tested Mailparse Extension Wrapper for PHP 5.4+
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
class Parser
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Attachment filename argument option for ->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue