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
|
||||
* PHPMailer
|
||||
* HTML Purifier
|
||||
* PHP Mime Mail Parser
|
||||
|
||||
* CSS
|
||||
* Bootstrap
|
||||
|
|
@ -88,7 +89,7 @@ ITFlow is self-hosted. There is a full installation guide in the [docs](https://
|
|||
|
||||
1. Install a LAMP stack (Linux, Apache, MariaDB, PHP)
|
||||
```sh
|
||||
sudo apt install git apache2 php libapache2-mod-php php-intl php-imap php-mysqli php-curl mariadb-server
|
||||
sudo apt install git apache2 php libapache2-mod-php php-intl php-imap php-mailparse php-mysqli php-curl mariadb-server
|
||||
```
|
||||
2. Clone the repo
|
||||
```sh
|
||||
|
|
|
|||
|
|
@ -10,58 +10,73 @@ TODO:
|
|||
- Process unregistered contacts/clients into an inbox to allow a ticket to be created/ignored
|
||||
- Better handle replying to closed tickets
|
||||
- Support for authenticating with OAuth
|
||||
- Documentation
|
||||
- Separate Mailbox Account for tickets 2022-12-14 - JQ
|
||||
- Properly parse base64 encoded emails (if an Outlook user sends a smiley everything breaks :( - https://electrictoolbox.com/php-imap-message-parts/)
|
||||
|
||||
Relate PRs to https://github.com/itflow-org/itflow/issues/225 & https://forum.itflow.org/d/11-road-map & https://forum.itflow.org/d/31-tickets-from-email
|
||||
*/
|
||||
|
||||
// Get ITFlow config & helper functions
|
||||
include_once("config.php");
|
||||
include_once("functions.php");
|
||||
require_once("config.php");
|
||||
require_once("functions.php");
|
||||
|
||||
// Get settings for the "default" company
|
||||
$company_id = 1;
|
||||
$session_company_id = 1;
|
||||
include_once("get_settings.php");
|
||||
require_once("get_settings.php");
|
||||
|
||||
// Check setting enabled
|
||||
if ($config_ticket_email_parse == 0) {
|
||||
exit("Feature is not enabled - see Settings > Ticketing > Email-to-ticket parsing");
|
||||
exit("Email Parser: Feature is not enabled - check Settings > Ticketing > Email-to-ticket parsing. See https://wiki.itflow.org/doku.php?id=wiki:ticket_email_parse -- Quitting..");
|
||||
}
|
||||
|
||||
// Check IMAP function exists
|
||||
// Check IMAP extension works/installed
|
||||
if (!function_exists('imap_open')) {
|
||||
echo "PHP IMAP extension is not installed, quitting..";
|
||||
exit();
|
||||
exit("Email Parser: PHP IMAP extension is not installed. See https://wiki.itflow.org/doku.php?id=wiki:ticket_email_parse -- Quitting..");
|
||||
}
|
||||
|
||||
// Check mailparse extension works/installed
|
||||
if (!function_exists('mailparse_msg_parse_file')) {
|
||||
exit("Email Parser: PHP mailparse extension is not installed. See https://wiki.itflow.org/doku.php?id=wiki:ticket_email_parse -- Quitting..");
|
||||
}
|
||||
|
||||
// PHP Mail Parser
|
||||
require_once("plugins/php-mime-mail-parser/src/Contracts/CharsetManager.php");
|
||||
require_once("plugins/php-mime-mail-parser/src/Contracts/Middleware.php");
|
||||
require_once("plugins/php-mime-mail-parser/src/Attachment.php");
|
||||
require_once("plugins/php-mime-mail-parser/src/Charset.php");
|
||||
require_once("plugins/php-mime-mail-parser/src/Exception.php");
|
||||
require_once("plugins/php-mime-mail-parser/src/Middleware.php");
|
||||
require_once("plugins/php-mime-mail-parser/src/MiddlewareStack.php");
|
||||
require_once("plugins/php-mime-mail-parser/src/MimePart.php");
|
||||
require_once("plugins/php-mime-mail-parser/src/Parser.php");
|
||||
|
||||
|
||||
// Function to raise a new ticket for a given contact and email them confirmation (if configured)
|
||||
function createTicket($contact_id, $contact_name, $contact_email, $client_id, $company_id, $date, $subject, $message) {
|
||||
function addTicket($contact_id, $contact_name, $contact_email, $client_id, $company_id, $date, $subject, $message)
|
||||
{
|
||||
|
||||
// Access global variables
|
||||
global $mysqli, $config_ticket_next_number, $config_ticket_prefix, $config_ticket_client_general_notifications, $config_base_url, $config_ticket_from_name, $config_ticket_from_email, $config_smtp_host, $config_smtp_port, $config_smtp_encryption, $config_smtp_username, $config_smtp_password;
|
||||
global $mysqli, $config_ticket_prefix, $config_ticket_client_general_notifications, $config_base_url, $config_ticket_from_name, $config_ticket_from_email, $config_smtp_host, $config_smtp_port, $config_smtp_encryption, $config_smtp_username, $config_smtp_password;
|
||||
|
||||
// Get the next Ticket Number and add 1 for the new ticket number
|
||||
$ticket_number_sql = mysqli_fetch_array(mysqli_query($mysqli, "SELECT config_ticket_next_number FROM settings WHERE company_id = $company_id"));
|
||||
$ticket_number = intval($ticket_number_sql['config_ticket_next_number']);
|
||||
$new_config_ticket_next_number = $ticket_number + 1;
|
||||
mysqli_query($mysqli, "UPDATE settings SET config_ticket_next_number = $new_config_ticket_next_number WHERE company_id = $company_id");
|
||||
|
||||
// Prep ticket details
|
||||
$message = nl2br(htmlentities(strip_tags($message)));
|
||||
$message = trim(mysqli_real_escape_string($mysqli,"<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
|
||||
$ticket_number = $config_ticket_next_number;
|
||||
$new_config_ticket_next_number = $config_ticket_next_number + 1;
|
||||
mysqli_query($mysqli,"UPDATE settings SET config_ticket_next_number = $new_config_ticket_next_number WHERE company_id = $company_id");
|
||||
|
||||
mysqli_query($mysqli,"INSERT INTO tickets SET ticket_prefix = '$config_ticket_prefix', ticket_number = $ticket_number, ticket_subject = '$subject', ticket_details = '$message', ticket_priority = 'Low', ticket_status = 'Open', ticket_created_at = NOW(), ticket_created_by = '0', ticket_contact_id = $contact_id, ticket_client_id = $client_id, company_id = $company_id");
|
||||
mysqli_query($mysqli, "INSERT INTO tickets SET ticket_prefix = '$config_ticket_prefix', ticket_number = $ticket_number, ticket_subject = '$subject', ticket_details = '$message', ticket_priority = 'Low', ticket_status = 'Open', ticket_created_at = NOW(), ticket_created_by = '0', ticket_contact_id = $contact_id, ticket_client_id = $client_id, company_id = $company_id");
|
||||
$id = mysqli_insert_id($mysqli);
|
||||
|
||||
// Logging
|
||||
echo "Created new ticket.<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
|
||||
$sql = mysqli_query($mysqli,"SELECT company_name, company_phone FROM companies WHERE company_id = $company_id");
|
||||
$sql = mysqli_query($mysqli, "SELECT company_name, company_phone FROM companies WHERE company_id = $company_id");
|
||||
$row = mysqli_fetch_array($sql);
|
||||
$company_phone = formatPhoneNumber($row['company_phone']);
|
||||
$company_name = $row['company_name'];
|
||||
|
|
@ -79,8 +94,8 @@ function createTicket($contact_id, $contact_name, $contact_email, $client_id, $c
|
|||
$email_subject, $email_body);
|
||||
|
||||
if ($mail !== true) {
|
||||
mysqli_query($mysqli,"INSERT INTO notifications SET notification_type = 'Mail', notification = 'Failed to send email to $contact_email', notification_timestamp = NOW(), company_id = $company_id");
|
||||
mysqli_query($mysqli,"INSERT INTO logs SET log_type = 'Mail', log_action = 'Error', log_description = 'Failed to send email to $contact_email regarding $subject. $mail', company_id = $company_id");
|
||||
mysqli_query($mysqli, "INSERT INTO notifications SET notification_type = 'Mail', notification = 'Failed to send email to $contact_email', notification_timestamp = NOW(), company_id = $company_id");
|
||||
mysqli_query($mysqli, "INSERT INTO logs SET log_type = 'Mail', log_action = 'Error', log_description = 'Failed to send email to $contact_email regarding $subject. $mail', company_id = $company_id");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -89,6 +104,83 @@ function createTicket($contact_id, $contact_name, $contact_email, $client_id, $c
|
|||
|
||||
}
|
||||
|
||||
function addReply($from_email, $date, $subject, $ticket_number, $message)
|
||||
{
|
||||
// Add email as a comment/reply to an existing ticket
|
||||
|
||||
// Access global variables
|
||||
global $mysqli, $config_ticket_prefix, $config_base_url, $config_ticket_from_name, $config_ticket_from_email, $config_smtp_host, $config_smtp_port, $config_smtp_encryption, $config_smtp_username, $config_smtp_password;
|
||||
|
||||
// Set default reply type
|
||||
$ticket_reply_type = 'Client';
|
||||
|
||||
// Capture just the latest/most recent email reply content
|
||||
// based off the "#--itflow#" line that we prepend the outgoing emails with (similar to the old school --reply above this line--)
|
||||
$message = explode("#--itflow--#", $message);
|
||||
$message = nl2br(htmlentities(strip_tags($message[0])));
|
||||
$message = "<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>)
|
||||
$imap_mailbox = "$config_imap_host:$config_imap_port/imap/$config_imap_encryption";
|
||||
|
||||
|
|
@ -99,7 +191,7 @@ $imap = imap_open("{{$imap_mailbox}}INBOX", $config_smtp_username, $config_smtp_
|
|||
if (!$imap) {
|
||||
// Logging
|
||||
$extended_log_description = var_export(imap_errors(), true);
|
||||
mysqli_query($mysqli,"INSERT INTO logs SET log_type = 'Mail', log_action = 'Error', log_description = 'Email parser: Failed to connect to IMAP. Details: $extended_log_description', company_id = $company_id");
|
||||
mysqli_query($mysqli, "INSERT INTO logs SET log_type = 'Mail', log_action = 'Error', log_description = 'Email parser: Failed to connect to IMAP. Details: $extended_log_description', company_id = $company_id");
|
||||
exit("Could not connect to IMAP");
|
||||
}
|
||||
|
||||
|
|
@ -124,108 +216,66 @@ if ($emails) {
|
|||
// Default false
|
||||
$email_processed = false;
|
||||
|
||||
// Get message details
|
||||
$metadata = imap_fetch_overview($imap, $email,0); // Date, Subject, Size
|
||||
$header = imap_headerinfo($imap, $email); // To get the From as an email, not a contact name
|
||||
$message = (imap_fetchbody($imap, $email, 1)); // Body
|
||||
// Get details from message and invoke PHP Mime Mail Parser
|
||||
$msg_to_parse = imap_fetchheader($imap, $email, FT_PREFETCHTEXT) . imap_body($imap, $email);
|
||||
$parser = new PhpMimeMailParser\Parser();
|
||||
$parser->setText($msg_to_parse);
|
||||
|
||||
$from = trim(mysqli_real_escape_string($mysqli, htmlentities(strip_tags($header->from[0]->mailbox . "@" . $header->from[0]->host))));
|
||||
$subject = trim(mysqli_real_escape_string($mysqli, htmlentities(strip_tags($metadata[0]->subject))));
|
||||
$date = trim(mysqli_real_escape_string($mysqli, htmlentities(strip_tags($metadata[0]->date))));
|
||||
// Process message attributes
|
||||
|
||||
$from_array = $parser->getAddresses('from')[0];
|
||||
$from_name = trim(mysqli_real_escape_string($mysqli, htmlentities(strip_tags($from_array['display']))));
|
||||
$from_email = trim(mysqli_real_escape_string($mysqli, htmlentities(strip_tags($from_array['address']))));
|
||||
$from_domain = explode("@", $from_array['address']);
|
||||
$from_domain = trim(mysqli_real_escape_string($mysqli, htmlentities(strip_tags(end($from_domain))))); // Use the final element in the array (as technically legal to have multiple @'s)
|
||||
|
||||
$subject = trim(mysqli_real_escape_string($mysqli, htmlentities(strip_tags($parser->getHeader('subject')))));
|
||||
$date = trim(mysqli_real_escape_string($mysqli, htmlentities(strip_tags($parser->getHeader('date')))));
|
||||
|
||||
$message = $parser->getMessageBody('text');
|
||||
|
||||
$domain = trim(mysqli_real_escape_string($mysqli, $header->from[0]->host));
|
||||
$from_name = trim(mysqli_real_escape_string($mysqli, $header->from[0]->mailbox));
|
||||
|
||||
// Check if we can identify a ticket number (in square brackets)
|
||||
if (preg_match("/\[$config_ticket_prefix\d+\]/", $subject, $ticket_number)) {
|
||||
|
||||
// Looks like there's a ticket number in the subject line (e.g. [TCK-091]
|
||||
// Process as a ticket reply
|
||||
|
||||
// Get the actual ticket number (without the brackets)
|
||||
preg_match('/\d+/', $ticket_number[0], $ticket_number);
|
||||
$ticket_number = intval($ticket_number[0]);
|
||||
|
||||
// Split the email into just the latest reply, with some metadata
|
||||
// We base this off the string "#--itflow--#" that we prepend the outgoing emails with (similar to the old school --reply above this line--)
|
||||
$message = explode("#--itflow--#", $message);
|
||||
$message = nl2br(htmlentities(strip_tags($message[0])));
|
||||
$message = "<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;
|
||||
}
|
||||
|
||||
if (addReply($from_email, $date, $subject, $ticket_number, $message)) {
|
||||
$email_processed = true;
|
||||
}
|
||||
|
||||
|
||||
} else {
|
||||
// Couldn't match this email to an existing ticket
|
||||
|
||||
// Check if we can match the sender to a pre-existing contact
|
||||
$any_contact_sql = mysqli_query($mysqli, "SELECT * FROM contacts WHERE contact_email = '$from' LIMIT 1");
|
||||
$any_contact_sql = mysqli_query($mysqli, "SELECT * FROM contacts WHERE contact_email = '$from_email' LIMIT 1");
|
||||
$row = mysqli_fetch_array($any_contact_sql);
|
||||
|
||||
$contact_name = $row['contact_name'];
|
||||
$contact_id = $row['contact_id'];
|
||||
$contact_email = $row['contact_email'];
|
||||
$client_id = $row['contact_client_id'];
|
||||
$company_id = $row['company_id'];
|
||||
if ($row) {
|
||||
// Sender exists as a contact
|
||||
$contact_name = $row['contact_name'];
|
||||
$contact_id = $row['contact_id'];
|
||||
$contact_email = $row['contact_email'];
|
||||
$client_id = $row['contact_client_id'];
|
||||
$company_id = $row['company_id'];
|
||||
|
||||
if ($from == $contact_email) {
|
||||
|
||||
createTicket($contact_id, $contact_name, $contact_email, $client_id, $company_id, $date, $subject, $message);
|
||||
$email_processed = true;
|
||||
if (addTicket($contact_id, $contact_name, $contact_email, $client_id, $company_id, $date, $subject, $message)) {
|
||||
$email_processed = true;
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
// Couldn't match this email to an existing ticket or an existing client contact
|
||||
// Checking to see if the sender domain matches a client website
|
||||
|
||||
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT * FROM clients WHERE client_website = '$domain' LIMIT 1"));
|
||||
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT * FROM clients WHERE client_website = '$from_domain' LIMIT 1"));
|
||||
|
||||
if ($row && $domain == $row['client_website']) {
|
||||
if ($row && $from_domain == $row['client_website']) {
|
||||
|
||||
// We found a match - create a contact under this client and raise a ticket for them
|
||||
|
||||
|
|
@ -236,22 +286,22 @@ if ($emails) {
|
|||
// Contact details
|
||||
$password = password_hash(randomString(), PASSWORD_DEFAULT);
|
||||
$contact_name = $from_name;
|
||||
$contact_email = $from;
|
||||
mysqli_query($mysqli,"INSERT INTO contacts SET contact_name = '$contact_name', contact_email = '$contact_email', contact_notes = 'Added automatically via email parsing.', contact_password_hash = '$password', contact_client_id = $client_id, company_id = $company_id");
|
||||
$contact_email = $from_email;
|
||||
mysqli_query($mysqli, "INSERT INTO contacts SET contact_name = '$contact_name', contact_email = '$contact_email', contact_notes = 'Added automatically via email parsing.', contact_password_hash = '$password', contact_client_id = $client_id, company_id = $company_id");
|
||||
$contact_id = mysqli_insert_id($mysqli);
|
||||
|
||||
// Logging for contact creation
|
||||
echo "Created new contact.<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);
|
||||
|
||||
$email_processed = true;
|
||||
if (addTicket($contact_id, $contact_name, $contact_email, $client_id, $company_id, $date, $subject, $message)) {
|
||||
$email_processed = true;
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
// Couldn't match this email to an existing ticket, existing contact or an existing client via the "from" domain
|
||||
// In the future we might make a page where these can be nicely viewed / managed, but for now we'll just flag them as needing attention
|
||||
// In the future we might make a page where these can be nicely viewed / managed, but for now we'll just flag them in the Inbox as needing attention
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -259,10 +309,11 @@ if ($emails) {
|
|||
|
||||
}
|
||||
|
||||
// Deal with the message
|
||||
// Deal with the message (move it if processed, flag it if not)
|
||||
if ($email_processed) {
|
||||
imap_mail_move($imap, $email, $imap_folder);
|
||||
} else {
|
||||
echo "Failed to process email - flagging for manual review.";
|
||||
imap_setflag_full($imap, $email, "\\Flagged");
|
||||
}
|
||||
|
||||
|
|
@ -271,6 +322,5 @@ if ($emails) {
|
|||
|
||||
}
|
||||
|
||||
|
||||
imap_expunge($imap);
|
||||
imap_close($imap);
|
||||
|
|
|
|||
|
|
@ -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