diff --git a/cron_ticket_email_parser.php b/cron_ticket_email_parser.php index 1dc1f6b0..5b77363c 100644 --- a/cron_ticket_email_parser.php +++ b/cron_ticket_email_parser.php @@ -4,23 +4,17 @@ * Process emails and create/update tickets */ -/* -TODO: - - Process unregistered contacts/clients into an inbox to allow a ticket to be created/ignored - - Support for authenticating with OAuth - - Separate Mailbox Account for tickets 2022-12-14 - JQ - -*/ - // Set working directory to the directory this cron script lives at. chdir(dirname(__FILE__)); +// Autoload Composer dependencies +require_once __DIR__ . '/plugins/php-imap/vendor/autoload.php'; + // Get ITFlow config & helper functions require_once "config.php"; // Set Timezone require_once "inc_set_timezone.php"; - require_once "functions.php"; // Get settings for the "default" company @@ -43,7 +37,7 @@ if ($config_ticket_email_parse == 0) { $argv = $_SERVER['argv']; // Check Cron Key -if ( $argv[1] !== $config_cron_key ) { +if ($argv[1] !== $config_cron_key) { exit("Cron Key invalid -- Quitting.."); } @@ -80,116 +74,83 @@ if (file_exists($lock_file_path)) { // Create a lock file file_put_contents($lock_file_path, "Locked"); -// PHP Mail Parser -use PhpMimeMailParser\Parser; - -require_once "plugins/php-mime-mail-parser/src/Contracts/CharsetManager.php"; - -require_once "plugins/php-mime-mail-parser/src/Contracts/Middleware.php"; - -require_once "plugins/php-mime-mail-parser/src/Attachment.php"; - -require_once "plugins/php-mime-mail-parser/src/Charset.php"; - -require_once "plugins/php-mime-mail-parser/src/Exception.php"; - -require_once "plugins/php-mime-mail-parser/src/Middleware.php"; - -require_once "plugins/php-mime-mail-parser/src/MiddlewareStack.php"; - -require_once "plugins/php-mime-mail-parser/src/MimePart.php"; - -require_once "plugins/php-mime-mail-parser/src/Parser.php"; - +// Webklex PHP-IMAP +use Webklex\PHPIMAP\ClientManager; +use Webklex\PHPIMAP\Message\Attachment; // Allowed attachment extensions $allowed_extensions = array('jpg', 'jpeg', 'gif', 'png', 'webp', 'pdf', 'txt', 'md', 'doc', 'docx', 'csv', 'xls', 'xlsx', 'xlsm', 'zip', 'tar', 'gz'); // Function to raise a new ticket for a given contact and email them confirmation (if configured) function addTicket($contact_id, $contact_name, $contact_email, $client_id, $date, $subject, $message, $attachments, $original_message_file) { + global $mysqli, $config_app_name, $company_name, $company_phone, $config_ticket_prefix, $config_ticket_client_general_notifications, $config_ticket_new_ticket_notification_email, $config_base_url, $config_ticket_from_name, $config_ticket_from_email, $config_smtp_host, $config_smtp_port, $config_smtp_encryption, $config_smtp_username, $config_smtp_password, $allowed_extensions; - // Access global variables - global $mysqli,$config_app_name, $company_name, $company_phone, $config_ticket_prefix, $config_ticket_client_general_notifications, $config_ticket_new_ticket_notification_email, $config_base_url, $config_ticket_from_name, $config_ticket_from_email, $config_smtp_host, $config_smtp_port, $config_smtp_encryption, $config_smtp_username, $config_smtp_password, $allowed_extensions; - - // Get the next Ticket Number and add 1 for the new ticket number $ticket_number_sql = mysqli_fetch_array(mysqli_query($mysqli, "SELECT config_ticket_next_number FROM settings WHERE company_id = 1")); $ticket_number = intval($ticket_number_sql['config_ticket_next_number']); $new_config_ticket_next_number = $ticket_number + 1; mysqli_query($mysqli, "UPDATE settings SET config_ticket_next_number = $new_config_ticket_next_number WHERE company_id = 1"); - // Prep ticket details - $message = nl2br($message); - $message = mysqli_escape_string($mysqli, "Email from: $contact_email at $date:-

$message"); + $message = "Email from: $contact_email at $date:-

$message"; - mysqli_query($mysqli, "INSERT INTO tickets SET ticket_prefix = '$config_ticket_prefix', ticket_number = $ticket_number, ticket_subject = '$subject', ticket_details = '$message', ticket_priority = 'Low', ticket_status = 1, ticket_created_by = 0, ticket_contact_id = $contact_id, ticket_client_id = $client_id"); + $ticket_prefix_esc = mysqli_real_escape_string($mysqli, $config_ticket_prefix); + $subject_esc = mysqli_real_escape_string($mysqli, $subject); + $message_esc = mysqli_real_escape_string($mysqli, $message); + $contact_email_esc = mysqli_real_escape_string($mysqli, $contact_email); + $client_id_esc = intval($client_id); + + mysqli_query($mysqli, "INSERT INTO tickets SET ticket_prefix = '$ticket_prefix_esc', ticket_number = $ticket_number, ticket_subject = '$subject_esc', ticket_details = '$message_esc', ticket_priority = 'Low', ticket_status = 1, ticket_created_by = 0, ticket_contact_id = $contact_id, ticket_client_id = $client_id_esc"); $id = mysqli_insert_id($mysqli); - // Logging echo "Created new ticket.
"; - mysqli_query($mysqli, "INSERT INTO logs SET log_type = 'Ticket', log_action = 'Create', log_description = 'Email parser: Client contact $contact_email created ticket $config_ticket_prefix$ticket_number ($subject) ($id)', log_client_id = $client_id"); + mysqli_query($mysqli, "INSERT INTO logs SET log_type = 'Ticket', log_action = 'Create', log_description = 'Email parser: Client contact $contact_email_esc created ticket $ticket_prefix_esc$ticket_number ($subject_esc) ($id)', log_client_id = $client_id_esc"); - // -- Process attachments (after ticket is logged as created because we save to the folder named after the ticket ID) -- - - mkdirMissing('uploads/tickets/'); // Create tickets dir - - // Setup directory for this ticket ID + mkdirMissing('uploads/tickets/'); $att_dir = "uploads/tickets/" . $id . "/"; mkdirMissing($att_dir); - // Save original email message as ticket attachment rename("uploads/tmp/{$original_message_file}", "{$att_dir}/{$original_message_file}"); - mysqli_query($mysqli, "INSERT INTO ticket_attachments SET ticket_attachment_name = 'Original-parsed-email.eml', ticket_attachment_reference_name = '$original_message_file', ticket_attachment_ticket_id = $id"); + $original_message_file_esc = mysqli_real_escape_string($mysqli, $original_message_file); + mysqli_query($mysqli, "INSERT INTO ticket_attachments SET ticket_attachment_name = 'Original-parsed-email.eml', ticket_attachment_reference_name = '$original_message_file_esc', ticket_attachment_ticket_id = $id"); - // Process each attachment foreach ($attachments as $attachment) { - - // Get name and extension - $att_name = $attachment->getFileName(); + $att_name = $attachment->getName(); $att_extarr = explode('.', $att_name); $att_extension = strtolower(end($att_extarr)); - // Check the extension is allowed if (in_array($att_extension, $allowed_extensions)) { - - // Save attachment with a random name - $att_saved_path = $attachment->save($att_dir, Parser::ATTACHMENT_RANDOM_FILENAME); - - // Access the random name to add into the database (this won't work on Windows) - $att_tmparr = explode($att_dir, $att_saved_path); + $att_saved_filename = md5(uniqid(rand(), true)) . '.' . $att_extension; + $att_saved_path = $att_dir . $att_saved_filename; + $attachment->save($att_dir); // Save the attachment to the directory + rename($att_dir . $attachment->getName(), $att_saved_path); // Rename the saved file to the hashed name $ticket_attachment_name = sanitizeInput($att_name); - $ticket_attachment_reference_name = sanitizeInput(end($att_tmparr)); - - mysqli_query($mysqli, "INSERT INTO ticket_attachments SET ticket_attachment_name = '$ticket_attachment_name', ticket_attachment_reference_name = '$ticket_attachment_reference_name', ticket_attachment_ticket_id = $id"); + $ticket_attachment_reference_name = sanitizeInput($att_saved_filename); + $ticket_attachment_name_esc = mysqli_real_escape_string($mysqli, $ticket_attachment_name); + $ticket_attachment_reference_name_esc = mysqli_real_escape_string($mysqli, $ticket_attachment_reference_name); + mysqli_query($mysqli, "INSERT INTO ticket_attachments SET ticket_attachment_name = '$ticket_attachment_name_esc', ticket_attachment_reference_name = '$ticket_attachment_reference_name_esc', ticket_attachment_ticket_id = $id"); } else { - $ticket_attachment_name = sanitizeInput($att_name); - mysqli_query($mysqli, "INSERT INTO logs SET log_type = 'Ticket', log_action = 'Update', log_description = 'Email parser: Blocked attachment $ticket_attachment_name from Client contact $contact_email for ticket $config_ticket_prefix$ticket_number', log_client_id = $client_id"); + $ticket_attachment_name_esc = mysqli_real_escape_string($mysqli, $att_name); + mysqli_query($mysqli, "INSERT INTO logs SET log_type = 'Ticket', log_action = 'Update', log_description = 'Email parser: Blocked attachment $ticket_attachment_name_esc from Client contact $contact_email_esc for ticket $ticket_prefix_esc$ticket_number', log_client_id = $client_id_esc"); } - } $data = []; - // E-mail client notification that ticket has been created if ($config_ticket_client_general_notifications == 1) { - $subject_email = "Ticket created - [$config_ticket_prefix$ticket_number] - $subject"; - $body = "##- Please type your reply above this line -##

Hello $contact_name,

Thank you for your email. A ticket regarding \"$subject\" has been automatically created for you.

Ticket: $config_ticket_prefix$ticket_number
Subject: $subject
Status: New
https://$config_base_url/portal/ticket.php?id=$id

--
$company_name - Support
$config_ticket_from_email
$company_phone"; + $body = "##- Please type your reply above this line -##

Hello $contact_name,

Thank you for your email. A ticket regarding \"$subject\" has been automatically created for you.

Ticket: $config_ticket_prefix$ticket_number
Subject: $subject
Status: New
https://$config_base_url/portal/ticket.php?id=$id

--
$company_name - Support
$config_ticket_from_email
$company_phone"; $data[] = [ 'from' => $config_ticket_from_email, 'from_name' => $config_ticket_from_name, 'recipient' => $contact_email, 'recipient_name' => $contact_name, - 'subject' => $subject_email, - 'body' => $body + 'subject' => mysqli_real_escape_string($mysqli, $subject_email), + 'body' => mysqli_real_escape_string($mysqli, $body) ]; } - // Notify agent DL of the new ticket, if populated with a valid email if ($config_ticket_new_ticket_notification_email) { - - // Get client info $client_sql = mysqli_query($mysqli, "SELECT client_name FROM clients WHERE client_id = $client_id"); $client_row = mysqli_fetch_array($client_sql); $client_name = sanitizeInput($client_row['client_name']); @@ -202,44 +163,36 @@ function addTicket($contact_id, $contact_name, $contact_email, $client_id, $date 'from_name' => $config_ticket_from_name, 'recipient' => $config_ticket_new_ticket_notification_email, 'recipient_name' => $config_ticket_from_name, - 'subject' => $email_subject, - 'body' => $email_body + 'subject' => mysqli_real_escape_string($mysqli, $email_subject), + 'body' => mysqli_real_escape_string($mysqli, $email_body) ]; } addToMailQueue($mysqli, $data); return true; - } -// End Add Ticket Function // Add Reply Function function addReply($from_email, $date, $subject, $ticket_number, $message, $attachments) { - // Add email as a comment/reply to an existing ticket - - // Access global variables global $mysqli, $config_app_name, $company_name, $company_phone, $config_ticket_prefix, $config_base_url, $config_ticket_from_name, $config_ticket_from_email, $config_smtp_host, $config_smtp_port, $config_smtp_encryption, $config_smtp_username, $config_smtp_password, $allowed_extensions; - // Set default reply type $ticket_reply_type = 'Client'; - - // Capture just the latest/most recent email reply content - // based off the "##- Please type your reply above this line -##" line that we prepend the outgoing emails with $message = explode("##- Please type your reply above this line -##", $message); $message = nl2br($message[0]); - $message = mysqli_escape_string($mysqli, "Email from: $from_email at $date:-

$message"); + $message = "Email from: $from_email at $date:-

$message"; + + $ticket_number_esc = intval($ticket_number); + $message_esc = mysqli_real_escape_string($mysqli, $message); + $from_email_esc = mysqli_real_escape_string($mysqli, $from_email); - // Lookup the ticket ID $row = mysqli_fetch_array(mysqli_query($mysqli, "SELECT ticket_id, ticket_subject, ticket_status, ticket_contact_id, ticket_client_id, contact_email, client_name FROM tickets LEFT JOIN contacts on tickets.ticket_contact_id = contacts.contact_id LEFT JOIN clients on tickets.ticket_client_id = clients.client_id - WHERE ticket_number = $ticket_number LIMIT 1")); + WHERE ticket_number = $ticket_number_esc LIMIT 1")); if ($row) { - - // Get ticket details $ticket_id = intval($row['ticket_id']); $ticket_subject = sanitizeInput($row['ticket_subject']); $ticket_status = sanitizeInput($row['ticket_status']); @@ -248,12 +201,16 @@ function addReply($from_email, $date, $subject, $ticket_number, $message, $attac $client_id = intval($row['ticket_client_id']); $client_name = sanitizeInput($row['client_name']); - // Check ticket isn't closed - tickets can't be re-opened if ($ticket_status == 5) { - mysqli_query($mysqli, "INSERT INTO notifications SET notification_type = 'Ticket', notification = 'Email parser: $from_email attempted to re-open ticket $config_ticket_prefix$ticket_number (ID $ticket_id) - check inbox manually to see email', notification_action = 'ticket.php?ticket_id=$ticket_id', notification_client_id = $client_id"); + $config_ticket_prefix_esc = mysqli_real_escape_string($mysqli, $config_ticket_prefix); + $ticket_number_esc = mysqli_real_escape_string($mysqli, $ticket_number); + $ticket_id_esc = intval($ticket_id); + $client_id_esc = intval($client_id); + + mysqli_query($mysqli, "INSERT INTO notifications SET notification_type = 'Ticket', notification = 'Email parser: $from_email attempted to re-open ticket $config_ticket_prefix_esc$ticket_number_esc (ID $ticket_id_esc) - check inbox manually to see email', notification_action = 'ticket.php?ticket_id=$ticket_id_esc', notification_client_id = $client_id_esc"); $email_subject = "Action required: This ticket is already closed"; - $email_body = "Hi there,

You\'ve tried to reply to a ticket that is closed - we won\'t see your response.

Please raise a new ticket by sending a fresh e-mail to our support address below.

--
$company_name - Support
$config_ticket_from_email
$company_phone"; + $email_body = "Hi there,

You've tried to reply to a ticket that is closed - we won't see your response.

Please raise a new ticket by sending a fresh e-mail to our support address below.

--
$company_name - Support
$config_ticket_from_email
$company_phone"; $data = [ [ @@ -261,8 +218,8 @@ function addReply($from_email, $date, $subject, $ticket_number, $message, $attac 'from_name' => $config_ticket_from_name, 'recipient' => $from_email, 'recipient_name' => $from_email, - 'subject' => $email_subject, - 'body' => $email_body + 'subject' => mysqli_real_escape_string($mysqli, $email_subject), + 'body' => mysqli_real_escape_string($mysqli, $email_body) ] ]; @@ -271,75 +228,53 @@ function addReply($from_email, $date, $subject, $ticket_number, $message, $attac return true; } - // Check WHO replied (was it the owner of the ticket or someone else on CC?) if (empty($ticket_contact_email) || $ticket_contact_email !== $from_email) { - - // It wasn't the contact currently assigned to the ticket, check if it's another registered contact for that client - - $row = mysqli_fetch_array(mysqli_query($mysqli, "SELECT contact_id FROM contacts WHERE contact_email = '$from_email' AND contact_client_id = $client_id LIMIT 1")); + $from_email_esc = mysqli_real_escape_string($mysqli, $from_email); + $row = mysqli_fetch_array(mysqli_query($mysqli, "SELECT contact_id FROM contacts WHERE contact_email = '$from_email_esc' AND contact_client_id = $client_id LIMIT 1")); if ($row) { - - // Contact is known - we can keep the reply type as client $ticket_reply_contact = intval($row['contact_id']); - } else { - // Mark the reply as internal as we don't recognise the contact (so the actual contact doesn't see it, and the tech can edit/delete if needed) $ticket_reply_type = 'Internal'; $ticket_reply_contact = '0'; - $message = "WARNING: Contact email mismatch
$message"; // Add a warning at the start of the message - for the techs benefit (think phishing/scams) + $message = "WARNING: Contact email mismatch
$message"; + $message_esc = mysqli_real_escape_string($mysqli, $message); } } - // Add the comment - mysqli_query($mysqli, "INSERT INTO ticket_replies SET ticket_reply = '$message', ticket_reply_type = '$ticket_reply_type', ticket_reply_time_worked = '00:00:00', ticket_reply_by = $ticket_reply_contact, ticket_reply_ticket_id = $ticket_id"); - + mysqli_query($mysqli, "INSERT INTO ticket_replies SET ticket_reply = '$message_esc', ticket_reply_type = '$ticket_reply_type', ticket_reply_time_worked = '00:00:00', ticket_reply_by = $ticket_reply_contact, ticket_reply_ticket_id = $ticket_id"); $reply_id = mysqli_insert_id($mysqli); - // Process attachments mkdirMissing('uploads/tickets/'); foreach ($attachments as $attachment) { - - // Get name and extension - $att_name = $attachment->getFileName(); + $att_name = $attachment->getName(); $att_extarr = explode('.', $att_name); $att_extension = strtolower(end($att_extarr)); - // Check the extension is allowed if (in_array($att_extension, $allowed_extensions)) { - - // Setup directory for this ticket ID - $att_dir = "uploads/tickets/" . $ticket_id . "/"; - mkdirMissing($att_dir); - - // Save attachment with a random name - $att_saved_path = $attachment->save($att_dir, Parser::ATTACHMENT_RANDOM_FILENAME); - - // Access the random name to add into the database (this won't work on Windows) - $att_tmparr = explode($att_dir, $att_saved_path); + $att_saved_filename = md5(uniqid(rand(), true)) . '.' . $att_extension; + $att_saved_path = "uploads/tickets/" . $ticket_id . "/" . $att_saved_filename; + $attachment->save("uploads/tickets/" . $ticket_id); // Save the attachment to the directory + rename("uploads/tickets/" . $ticket_id . "/" . $attachment->getName(), $att_saved_path); // Rename the saved file to the hashed name $ticket_attachment_name = sanitizeInput($att_name); - $ticket_attachment_reference_name = sanitizeInput(end($att_tmparr)); - - mysqli_query($mysqli, "INSERT INTO ticket_attachments SET ticket_attachment_name = '$ticket_attachment_name', ticket_attachment_reference_name = '$ticket_attachment_reference_name', ticket_attachment_reply_id = $reply_id, ticket_attachment_ticket_id = $ticket_id"); + $ticket_attachment_reference_name = sanitizeInput($att_saved_filename); + $ticket_attachment_name_esc = mysqli_real_escape_string($mysqli, $ticket_attachment_name); + $ticket_attachment_reference_name_esc = mysqli_real_escape_string($mysqli, $ticket_attachment_reference_name); + mysqli_query($mysqli, "INSERT INTO ticket_attachments SET ticket_attachment_name = '$ticket_attachment_name_esc', ticket_attachment_reference_name = '$ticket_attachment_reference_name_esc', ticket_attachment_reply_id = $reply_id, ticket_attachment_ticket_id = $ticket_id"); } else { - $ticket_attachment_name = sanitizeInput($att_name); - mysqli_query($mysqli, "INSERT INTO logs SET log_type = 'Ticket', log_action = 'Update', log_description = 'Email parser: Blocked attachment $ticket_attachment_name from Client contact $from_email for ticket $config_ticket_prefix$ticket_number', log_client_id = $client_id"); + $ticket_attachment_name_esc = mysqli_real_escape_string($mysqli, $att_name); + mysqli_query($mysqli, "INSERT INTO logs SET log_type = 'Ticket', log_action = 'Update', log_description = 'Email parser: Blocked attachment $ticket_attachment_name_esc from Client contact $from_email_esc for ticket $config_ticket_prefix$ticket_number_esc', log_client_id = $client_id"); } - } - // E-mail techs assigned to the ticket to notify them of the reply $ticket_assigned_to = mysqli_query($mysqli, "SELECT ticket_assigned_to FROM tickets WHERE ticket_id = $ticket_id LIMIT 1"); if ($ticket_assigned_to) { - $row = mysqli_fetch_array($ticket_assigned_to); $ticket_assigned_to = intval($row['ticket_assigned_to']); if ($ticket_assigned_to) { - - // Get tech details $tech_sql = mysqli_query($mysqli, "SELECT user_email, user_name FROM users WHERE user_id = $ticket_assigned_to LIMIT 1"); $tech_row = mysqli_fetch_array($tech_sql); $tech_email = sanitizeInput($tech_row['user_email']); @@ -354,203 +289,124 @@ function addReply($from_email, $date, $subject, $ticket_number, $message, $attac 'from_name' => $config_ticket_from_name, 'recipient' => $tech_email, 'recipient_name' => $tech_name, - 'subject' => $email_subject, - 'body' => $email_body + 'subject' => mysqli_real_escape_string($mysqli, $email_subject), + 'body' => mysqli_real_escape_string($mysqli, $email_body) ] ]; addToMailQueue($mysqli, $data); - } - } - // Update Ticket Last Response Field & set ticket to open as client has replied mysqli_query($mysqli, "UPDATE tickets SET ticket_status = 2 WHERE ticket_id = $ticket_id AND ticket_client_id = $client_id LIMIT 1"); echo "Updated existing ticket.
"; - mysqli_query($mysqli, "INSERT INTO logs SET log_type = 'Ticket', log_action = 'Update', log_description = 'Email parser: Client contact $from_email updated ticket $config_ticket_prefix$ticket_number ($subject)', log_client_id = $client_id"); + mysqli_query($mysqli, "INSERT INTO logs SET log_type = 'Ticket', log_action = 'Update', log_description = 'Email parser: Client contact $from_email_esc updated ticket $config_ticket_prefix$ticket_number_esc ($subject)', log_client_id = $client_id"); return true; - } else { - // Invalid ticket number return false; } } -// END ADD REPLY FUNCTION ------------------------------------------------- -// Prepare connection string with encryption (TLS/SSL/) -$imap_mailbox = "$config_imap_host:$config_imap_port/imap/$config_imap_encryption"; +// Initialize the client manager and create the client +$clientManager = new ClientManager(); +$client = $clientManager->make([ + 'host' => $config_imap_host, + 'port' => $config_imap_port, + 'encryption' => $config_imap_encryption, + 'validate_cert' => true, + 'username' => $config_imap_username, + 'password' => $config_imap_password, + 'protocol' => 'imap' +]); -// Connect to host via IMAP -$imap = imap_open("{{$imap_mailbox}}INBOX", $config_imap_username, $config_imap_password); +// Connect to the IMAP server +$client->connect(); -// Check connection -if (!$imap) { - // Logging - //$extended_log_description = var_export(imap_errors(), true); - // Remove the lock file - unlink($lock_file_path); - mysqli_query($mysqli, "INSERT INTO logs SET log_type = 'Mail', log_action = 'Error', log_description = 'Email parser: Failed to connect to IMAP. Details'"); - exit("Could not connect to IMAP"); -} +$inbox = $client->getFolder('INBOX'); +$messages = $inbox->query()->unseen()->get(); -// Check for the ITFlow mailbox that we move messages to once processed -$imap_folder = 'ITFlow'; -$list = imap_list($imap, "{{$imap_mailbox}}", "*"); -if (array_search("{{$imap_mailbox}}$imap_folder", $list) === false) { - imap_createmailbox($imap, imap_utf7_encode("{{$imap_mailbox}}$imap_folder")); - imap_subscribe($imap, imap_utf7_encode("{{$imap_mailbox}}$imap_folder")); -} - -// Search for unread ("UNSEEN") emails -$emails = imap_search($imap, 'UNSEEN'); - -if ($emails) { - - // Sort - rsort($emails); - - // Loop through each email - foreach ($emails as $email) { - - // Default false +if ($messages->count() > 0) { + foreach ($messages as $message) { $email_processed = false; - // Save the original email (to be moved later) - mkdirMissing('uploads/tmp/'); // Create tmp dir + mkdirMissing('uploads/tmp/'); $original_message_file = "processed-eml-" . randomString(200) . ".eml"; - imap_savebody($imap, "uploads/tmp/{$original_message_file}", $email); + file_put_contents("uploads/tmp/{$original_message_file}", $message->getRawMessage()); - // Get details from message and invoke PHP Mime Mail Parser - $msg_to_parse = imap_fetchheader($imap, $email, FT_PREFETCHTEXT) . imap_body($imap, $email, FT_PEEK); - $parser = new PhpMimeMailParser\Parser(); - $parser->setText($msg_to_parse); + $from_address = $message->getFrom(); + $from_name = sanitizeInput($from_address[0]->personal ?? 'Unknown'); + $from_email = sanitizeInput($from_address[0]->mail ?? 'itflow-guest@example.com'); - // Process message attributes - - $from_array = $parser->getAddresses('from')[0]; - $from_name = sanitizeInput($from_array['display']); - - // Handle blank 'From' emails - $from_email = "itflow-guest@example.com"; - if (filter_var($from_array['address'], FILTER_VALIDATE_EMAIL)) { - $from_email = sanitizeInput($from_array['address']); - } - - $from_domain = explode("@", $from_array['address']); + $from_domain = explode("@", $from_email); $from_domain = sanitizeInput(end($from_domain)); - $subject = sanitizeInput($parser->getHeader('subject')); - $date = sanitizeInput($parser->getHeader('date')); - $attachments = $parser->getAttachments(); + $subject = sanitizeInput($message->getSubject() ?? 'No Subject'); + $date = sanitizeInput($message->getDate() ?? date('Y-m-d H:i:s')); + $message_body = $message->getHtmlBody() ?? $message->getTextBody() ?? ''; - // Get the message content - // (first try HTML parsing, but switch to plain text if the email is empty/plain-text only) -// $message = $parser->getMessageBody('htmlEmbedded'); -// if (empty($message)) { -// echo "DEBUG: Switching to plain text parsing for this message ($subject)"; -// $message = $parser->getMessageBody('text'); -// } - - // TODO: Default to getting HTML and fallback to plaintext, but HTML emails seem to break the forward/agent notifications - - $message = $parser->getMessageBody('text'); - - // Check if we can identify a ticket number (in square brackets) if (preg_match("/\[$config_ticket_prefix\d+\]/", $subject, $ticket_number)) { - - // Looks like there's a ticket number in the subject line (e.g. [TCK-091] - // Process as a ticket reply - - // Get the actual ticket number (without the brackets) preg_match('/\d+/', $ticket_number[0], $ticket_number); $ticket_number = intval($ticket_number[0]); - if (addReply($from_email, $date, $subject, $ticket_number, $message, $attachments)) { + if (addReply($from_email, $date, $subject, $ticket_number, $message_body, $message->getAttachments())) { $email_processed = true; } - } else { - // Couldn't match this email to an existing ticket - - // Check if we can match the sender to a pre-existing contact - $any_contact_sql = mysqli_query($mysqli, "SELECT * FROM contacts WHERE contact_email = '$from_email' LIMIT 1"); + $from_email_esc = mysqli_real_escape_string($mysqli, $from_email); + $any_contact_sql = mysqli_query($mysqli, "SELECT * FROM contacts WHERE contact_email = '$from_email_esc' LIMIT 1"); $row = mysqli_fetch_array($any_contact_sql); if ($row) { - // Sender exists as a contact $contact_name = sanitizeInput($row['contact_name']); $contact_id = intval($row['contact_id']); $contact_email = sanitizeInput($row['contact_email']); $client_id = intval($row['contact_client_id']); - if (addTicket($contact_id, $contact_name, $contact_email, $client_id, $date, $subject, $message, $attachments, $original_message_file)) { + if (addTicket($contact_id, $contact_name, $contact_email, $client_id, $date, $subject, $message_body, $message->getAttachments(), $original_message_file)) { $email_processed = true; } - } else { - - // Couldn't match this email to an existing ticket or an existing client contact - // Checking to see if the sender domain matches a client website - - $row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT * FROM domains WHERE domain_name = '$from_domain' LIMIT 1")); + $from_domain_esc = mysqli_real_escape_string($mysqli, $from_domain); + $row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT * FROM domains WHERE domain_name = '$from_domain_esc' LIMIT 1")); if ($row && $from_domain == $row['domain_name']) { - - // We found a match - create a contact under this client and raise a ticket for them - - // Client details $client_id = intval($row['domain_client_id']); - // Contact details $password = password_hash(randomString(), PASSWORD_DEFAULT); - $contact_name = $from_name; // This was already Sanitized above - $contact_email = $from_email; // This was already Sanitized above - mysqli_query($mysqli, "INSERT INTO contacts SET contact_name = '$contact_name', contact_email = '$contact_email', contact_notes = 'Added automatically via email parsing.', contact_password_hash = '$password', contact_client_id = $client_id"); + $contact_name = $from_name; + $contact_email = $from_email; + mysqli_query($mysqli, "INSERT INTO contacts SET contact_name = '".mysqli_real_escape_string($mysqli, $contact_name)."', contact_email = '".mysqli_real_escape_string($mysqli, $contact_email)."', contact_notes = 'Added automatically via email parsing.', contact_password_hash = '$password', contact_client_id = $client_id"); $contact_id = mysqli_insert_id($mysqli); - // Logging for contact creation echo "Created new contact.
"; - mysqli_query($mysqli, "INSERT INTO logs SET log_type = 'Contact', log_action = 'Create', log_description = 'Email parser: created contact $contact_name', log_client_id = $client_id"); + mysqli_query($mysqli, "INSERT INTO logs SET log_type = 'Contact', log_action = 'Create', log_description = 'Email parser: created contact ".mysqli_real_escape_string($mysqli, $contact_name)."', log_client_id = $client_id"); - if (addTicket($contact_id, $contact_name, $contact_email, $client_id, $date, $subject, $message, $attachments, $original_message_file)) { + if (addTicket($contact_id, $contact_name, $contact_email, $client_id, $date, $subject, $message_body, $message->getAttachments(), $original_message_file)) { $email_processed = true; } - - } else { - - // Couldn't match this email to an existing ticket, existing contact or an existing client via the "from" domain - // In the future we might make a page where these can be nicely viewed / managed, but for now we'll just flag them in the Inbox as needing attention - } - } - } - // Deal with the message (move it if processed, flag it if not) if ($email_processed) { - imap_setflag_full($imap, $email, "\\Seen"); - imap_mail_move($imap, $email, $imap_folder); + $message->setFlag(['Seen']); + $message->move('ITFlow'); } else { - // Basically just flags all emails to be manually checked echo "Failed to process email - flagging for manual review."; - imap_setflag_full($imap, $email, "\\Flagged"); + $message->setFlag(['Flagged']); } - // Remove temp original message if still there if (file_exists("uploads/tmp/{$original_message_file}")) { unlink("uploads/tmp/{$original_message_file}"); } - } - } -imap_expunge($imap); -imap_close($imap); +$client->expunge(); +$client->disconnect(); // Remove the lock file unlink($lock_file_path); +?> diff --git a/plugins/php-imap/.github/FUNDING.yml b/plugins/php-imap/.github/FUNDING.yml new file mode 100644 index 00000000..c49bc6e3 --- /dev/null +++ b/plugins/php-imap/.github/FUNDING.yml @@ -0,0 +1,2 @@ +ko_fi: webklex +custom: ['https://www.buymeacoffee.com/webklex'] diff --git a/plugins/php-imap/.github/ISSUE_TEMPLATE/bug_report.md b/plugins/php-imap/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..a0571c33 --- /dev/null +++ b/plugins/php-imap/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,32 @@ +--- +name: Bug report +about: Create a report to help us improve + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**Used config** +Please provide the used config, if you are not using the package default config. + +**Code to Reproduce** +The troubling code section which produces the reported bug. +```php +echo "Bug"; +``` + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop / Server (please complete the following information):** +- OS: [e.g. Debian 10] +- PHP: [e.g. 5.5.9] +- Version [e.g. v2.3.1] +- Provider [e.g. Gmail, Outlook, Dovecot] + +**Additional context** +Add any other context about the problem here. diff --git a/plugins/php-imap/.github/ISSUE_TEMPLATE/feature_request.md b/plugins/php-imap/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..066b2d92 --- /dev/null +++ b/plugins/php-imap/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,17 @@ +--- +name: Feature request +about: Suggest an idea for this project + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/plugins/php-imap/.github/ISSUE_TEMPLATE/general-help-request.md b/plugins/php-imap/.github/ISSUE_TEMPLATE/general-help-request.md new file mode 100644 index 00000000..49809d14 --- /dev/null +++ b/plugins/php-imap/.github/ISSUE_TEMPLATE/general-help-request.md @@ -0,0 +1,12 @@ +--- +name: General help request +about: Feel free to ask about any project related stuff + +--- + +Please be aware that these issues will be closed if inactive for more then 14 days. + +Also make sure to use https://github.com/Webklex/php-imap/issues/new?template=bug_report.md if you want to report a bug +or https://github.com/Webklex/php-imap/issues/new?template=feature_request.md if you want to suggest a feature. + +Still here? Well clean this out and go ahead :) diff --git a/plugins/php-imap/.github/docker/Dockerfile b/plugins/php-imap/.github/docker/Dockerfile new file mode 100644 index 00000000..ff0f11e6 --- /dev/null +++ b/plugins/php-imap/.github/docker/Dockerfile @@ -0,0 +1,23 @@ +FROM ubuntu:latest +LABEL maintainer="Webklex " + +RUN apt-get update +RUN apt-get upgrade -y +RUN apt-get install -y sudo dovecot-imapd + +ENV LIVE_MAILBOX=true +ENV LIVE_MAILBOX_HOST=mail.example.local +ENV LIVE_MAILBOX_PORT=993 +ENV LIVE_MAILBOX_ENCRYPTION=ssl +ENV LIVE_MAILBOX_VALIDATE_CERT=true +ENV LIVE_MAILBOX_USERNAME=root@example.local +ENV LIVE_MAILBOX_PASSWORD=foobar +ENV LIVE_MAILBOX_QUOTA_SUPPORT=true + +EXPOSE 993 + +ADD dovecot_setup.sh /root/dovecot_setup.sh +RUN chmod +x /root/dovecot_setup.sh + +CMD ["/bin/bash", "-c", "/root/dovecot_setup.sh && tail -f /dev/null"] + diff --git a/plugins/php-imap/.github/docker/dovecot_setup.sh b/plugins/php-imap/.github/docker/dovecot_setup.sh new file mode 100644 index 00000000..25a51928 --- /dev/null +++ b/plugins/php-imap/.github/docker/dovecot_setup.sh @@ -0,0 +1,63 @@ +#!/bin/sh + +set -ex + +sudo apt-get -q update +sudo apt-get -q -y install dovecot-imapd + +{ + echo "127.0.0.1 $LIVE_MAILBOX_HOST" +} | sudo tee -a /etc/hosts + +SSL_CERT="/etc/ssl/certs/dovecot.crt" +SSL_KEY="/etc/ssl/private/dovecot.key" + +sudo openssl req -new -x509 -days 3 -nodes \ + -out "$SSL_CERT" \ + -keyout "$SSL_KEY" \ + -subj "/C=EU/ST=Europe/L=Home/O=Webklex/OU=Webklex DEV/CN=""$LIVE_MAILBOX_HOST" + +sudo chown root:dovecot "$SSL_CERT" "$SSL_KEY" +sudo chmod 0440 "$SSL_CERT" +sudo chmod 0400 "$SSL_KEY" + +DOVECOT_CONF="/etc/dovecot/local.conf" +MAIL_CONF="/etc/dovecot/conf.d/10-mail.conf" +IMAP_CONF="/etc/dovecot/conf.d/20-imap.conf" +QUOTA_CONF="/etc/dovecot/conf.d/90-quota.conf" +sudo touch "$DOVECOT_CONF" "$MAIL_CONF" "$IMAP_CONF" "$QUOTA_CONF" +sudo chown root:dovecot "$DOVECOT_CONF" "$MAIL_CONF" "$IMAP_CONF" "$QUOTA_CONF" +sudo chmod 0640 "$DOVECOT_CONF" "$MAIL_CONF" "$IMAP_CONF" "$QUOTA_CONF" + +{ + echo "ssl = required" + echo "disable_plaintext_auth = yes" + echo "ssl_cert = <""$SSL_CERT" + echo "ssl_key = <""$SSL_KEY" + echo "ssl_protocols = !SSLv2 !SSLv3" + echo "ssl_cipher_list = AES128+EECDH:AES128+EDH" +} | sudo tee -a "$DOVECOT_CONF" + +{ + echo "mail_plugins = \$mail_plugins quota" +} | sudo tee -a "$MAIL_CONF" + +{ + echo "protocol imap {" + echo " mail_plugins = \$mail_plugins imap_quota" + echo "}" +} | sudo tee -a "$IMAP_CONF" + +{ + echo "plugin {" + echo " quota = maildir:User quota" + echo " quota_rule = *:storage=1G" + echo "}" +} | sudo tee -a "$QUOTA_CONF" + +sudo useradd --create-home --shell /bin/false "$LIVE_MAILBOX_USERNAME" +echo "$LIVE_MAILBOX_USERNAME"":""$LIVE_MAILBOX_PASSWORD" | sudo chpasswd + +sudo service dovecot restart + +sudo doveadm auth test -x service=imap "$LIVE_MAILBOX_USERNAME" "$LIVE_MAILBOX_PASSWORD" \ No newline at end of file diff --git a/plugins/php-imap/.github/workflows/tests.yaml b/plugins/php-imap/.github/workflows/tests.yaml new file mode 100644 index 00000000..b441efd3 --- /dev/null +++ b/plugins/php-imap/.github/workflows/tests.yaml @@ -0,0 +1,50 @@ +name: Tests + +on: + push: + pull_request: + schedule: + - cron: '0 0 * * *' + +permissions: + contents: read + +jobs: + phpunit: + runs-on: ubuntu-latest + + strategy: + fail-fast: true + matrix: + php: ['8.0', 8.1, 8.2] + + name: PHP ${{ matrix.php }} + + env: + LIVE_MAILBOX: true + LIVE_MAILBOX_DEBUG: true + LIVE_MAILBOX_HOST: mail.example.local + LIVE_MAILBOX_PORT: 993 + LIVE_MAILBOX_USERNAME: root@example.local + LIVE_MAILBOX_ENCRYPTION: ssl + LIVE_MAILBOX_PASSWORD: foobar + LIVE_MAILBOX_QUOTA_SUPPORT: true + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: openssl, json, mbstring, iconv, fileinfo, libxml, zip + coverage: none + + - name: Install Composer dependencies + run: composer install --prefer-dist --no-interaction --no-progress + + - run: "sh .github/docker/dovecot_setup.sh" + + - name: Execute tests + run: vendor/bin/phpunit \ No newline at end of file diff --git a/plugins/php-imap/.gitignore b/plugins/php-imap/.gitignore new file mode 100644 index 00000000..d1816ad9 --- /dev/null +++ b/plugins/php-imap/.gitignore @@ -0,0 +1,7 @@ +vendor +composer.lock +.idea +/build/ +test.php +.phpunit.result.cache +phpunit.xml \ No newline at end of file diff --git a/plugins/php-imap/CHANGELOG.md b/plugins/php-imap/CHANGELOG.md new file mode 100755 index 00000000..edeea50d --- /dev/null +++ b/plugins/php-imap/CHANGELOG.md @@ -0,0 +1,911 @@ +# Changelog + +All notable changes to `webklex/php-imap` will be documented in this file. + +Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) principles. + +## [UNRELEASED] +### Fixed +- Fixed date issue if timezone is UT and a 2 digit year #429 (thanks @ferrisbuellers) +- Make the space optional after a comma separator #437 (thanks @marc0adam) +- Fix bug when multipart message getHTMLBody() method returns null #455 (thanks @michalkortas) +- Fix: Improve return type hints and return docblocks for query classes #470 (thanks @olliescase) +- Fix - Query - Chunked - Resolved infinite loop when start chunk > 1 #477 (thanks @NeekTheNook) + +### Added +- IMAP STATUS command support added `Folder::status()` #424 (thanks @InterLinked1) +- Add attributes and special flags #428 (thanks @sazanof) +- Better connection check for IMAP #449 (thanks @thin-k-design) +- Config handling moved into a new class `Config::class` to allow class serialization (sponsored by elb-BIT GmbH) +- Support for Carbon 3 added #483 + +### Breaking changes +- `Folder::getStatus()` no longer returns the results of `EXAMINE` but `STATUS` instead. If you want to use `EXAMINE` you can use the `Folder::examine()` method instead. +- `ClientManager::class` has now longer access to all configs. Config handling has been moved to its own class `Config::class`. If you want to access the config you can use the retriever method `::getConfig()` instead. Example: `$client->getConfig()` or `$message->getConfig()`, etc. +- `ClientManager::get` isn't available anymore. Use the regular config accessor instead. Example: `$cm->getConfig()` +- `M̀essage::getConfig()` now returns the client configuration instead of the fetching options configuration. Please use `$message->getOptions()` instead. +- `Attachment::getConfig()` now returns the client configuration instead of the fetching options configuration. Please use `$attachment->getOptions()` instead. +- `Header::getConfig()` now returns the client configuration instead of the fetching options configuration. Please use `$header->getOptions()` instead. +- `M̀essage::setConfig` now expects the client configuration instead of the fetching options configuration. Please use `$message->setOptions` instead. +- `Attachment::setConfig` now expects the client configuration instead of the fetching options configuration. Please use `$attachment->setOptions` instead. +- `Header::setConfig` now expects the client configuration instead of the fetching options configuration. Please use `$header->setOptions` instead. +- All protocol constructors now require a `Config::class` instance +- The `Client::class` constructors now require a `Config::class` instance +- The `Part::class` constructors now require a `Config::class` instance +- The `Header::class` constructors now require a `Config::class` instance +- The `Message::fromFile` method now requires a `Config::class` instance +- The `Message::fromString` method now requires a `Config::class` instance +- The `Message::boot` method now requires a `Config::class` instance + +## [5.5.0] - 2023-06-28 +### Fixed +- Error token length mismatch in `ImapProtocol::readResponse` #400 +- Attachment name parsing fixed #410 #421 (thanks @nuernbergerA) +- Additional Attachment name fallback added to prevent missing attachments +- Attachment id is now static (based on the raw part content) instead of random +- Always parse the attachment description if it is available + +### Added +- Attachment content hash added + + +## [5.4.0] - 2023-06-24 +### Fixed +- Legacy protocol support fixed (object to array conversion) #411 +- Header value decoding improved #410 +- Protocol exception handling improved (bad response message added) #408 +- Prevent fetching singular rfc partials from running indefinitely #407 +- Subject with colon ";" is truncated #401 +- Catching and handling iconv decoding exception #397 + +### Added +- Additional timestamp formats added #198 #392 (thanks @esk-ap) + + +## [5.3.0] - Security patch - 2023-06-20 +### Fixed +- Potential RCE through path traversal fixed #414 (special thanks @angelej) + +### Security Impact and Mitigation +Impacted are all versions below v5.3.0. +If possible, update to >= v5.3.0 as soon as possible. Impacted was the `Attachment::save` +method which could be used to write files to the local filesystem. The path was not +properly sanitized and could be used to write files to arbitrary locations. + +However, the `Attachment::save` method is not used by default and has to be called +manually. If you are using this method without providing a sanitized path, you are +affected by this vulnerability. +If you are not using this method or are providing a sanitized path, you are not affected +by this vulnerability and no immediate action is required. + +If you have any questions, please feel welcome to join this issue: https://github.com/Webklex/php-imap/issues/416 +#### Timeline +- 17.06.23 21:30: Vulnerability reported +- 18.06.23 19:14: Vulnerability confirmed +- 19.06.23 18:41: Vulnerability fixed via PR #414 +- 20.06.23 13:45: Security patch released +- 21.06.23 20:48: CVE-2023-35169 got assigned +- 21.06.23 20:58: Advisory released https://github.com/Webklex/php-imap/security/advisories/GHSA-47p7-xfcc-4pv9 + + +## [5.2.0] - 2023-04-11 +### Fixed +- Use all available methods to detect the attachment extension instead of just one +- Allow the `LIST` command response to be empty #393 +- Initialize folder children attributes on class initialization + +### Added +- Soft fail option added to all folder fetching methods. If soft fail is enabled, the method will return an empty collection instead of throwing an exception if the folder doesn't exist + + +## [5.1.0] - 2023-03-16 +### Fixed +- IMAP Quota root command fixed +- Prevent line-breaks in folder path caused by special chars +- Partial fix for #362 (allow overview response to be empty) +- `Message::setConfig()` config parameter type set to array +- Reset the protocol uid cache if the session gets expunged +- Set the "seen" flag only if the flag isn't set and the fetch option isn't `IMAP::FT_PEEK` +- `Message::is()` date comparison fixed +- `Message::$client` could not be set to null +- `in_reply_to` and `references` parsing fixed +- Prevent message body parser from injecting empty lines +- Don't parse regular inline message parts without name or filename as attachment +- `Message::hasTextBody()` and `Message::hasHtmlBody()` should return `false` if the body is empty +- Imap-Protocol "empty response" detection extended to catch an empty response caused by a broken resource stream +- `iconv_mime_decode()` is now used with `ICONV_MIME_DECODE_CONTINUE_ON_ERROR` to prevent the decoding from failing +- Date decoding rules extended to support more date formats +- Unset the currently active folder if it gets deleted (prevent infinite loop) +- Attachment name and filename parsing fixed and improved to support more formats +- Check if the next uid is available (after copying or moving a message) before fetching it #381 +- Default pagination `$total` attribute value set to 0 #385 (thanks @hhniao) +- Use attachment ID as fallback filename for saving an attachment +- Address decoding error detection added #388 + +### Added +- Extended UTF-7 support added (RFC2060) #383 +- `Protocol::sizes()` support added (fetch the message byte size via RFC822.SIZE). Accessible through `Message::getSize()` #379 (thanks @didi1357) +- `Message::hasFlag()` method added to check if a message has a specific flag +- `Message::getConfig()` method added to get the current message configuration +- `Folder::select()` method added to select a folder +- `Message::getAvailableFlags()` method added to get all available flags +- Live mailbox and fixture tests added +- `Attribute::map()` method added to map all attribute values +- `Header::has()` method added to check if a header attribute / value exist +- All part attributes are now accessible via linked attribute +- Restore a message from string `Message::fromString()` + + +## [5.0.1] - 2023-03-01 +### Fixed +- More unique ID generation to prevent multiple attachments with same ID #363 (thanks @Guite) +- Not all attachments are pushed to the collection #372 (thanks @AdrianKuriata) +- Partial fix for #362 (allow search response to be empty) +- Unsafe usage of switch case. #354 #366 (thanks @shuergab) +- Fix use of ST_MSGN as sequence method #356 (thanks @gioid) +- Prevent infinite loop in ImapProtocol #316 (thanks @thin-k-design) + + +## [5.0.0] - 2023-01-18 +### Fixed +- The message uid and message number will only be fetched if accessed and wasn't previously set #326 #285 (thanks @szymekjanaczek) +- Fix undefined attachment name when headers use "filename*=" format #301 (thanks @JulienChavee) +- Fixed `ImapProtocol::logout` always throws 'not connected' Exception after upgraded to 4.1.2 #351 +- Protocol interface and methods unified +- Strict attribute and return types introduced where ever possible +- Parallel messages during idle #338 +- Idle timeout / stale resource stream issue fixed +- Syntax updated to support php 8 features +- Get the attachment file extension from the filename if no mimetype detection library is available +- Prevent the structure parsing from parsing an empty part +- Convert all header keys to their lower case representation +- Restructure the decode function #355 (thanks @istid) + +### Added +- Unit tests added #347 #242 (thanks @sergiy-petrov, @boekkooi-lengoo) +- `Client::clone()` method added to clone a client instance +- Save an entire message (including its headers) `Message::save()` +- Restore a message from a local or remote file `Message::fromFile()` +- Protocol resource stream accessor added `Protocol::getStream()` +- Protocol resource stream meta data accessor added `Protocol::meta()` +- ImapProtocol resource stream reset method added `ImapProtocol::reset()` +- Protocol `Response::class` introduced to handle and unify all protocol requests +- Static mask config accessor added `ClientManager::getMask()` added +- An `Attribute::class` instance can be treated as array +- Get the current client account configuration via `Client::getConfig()` +- Delete a folder via `Client::deleteFolder()` + +### Breaking changes +- PHP ^8.0.2 required +- `nesbot/carbon` version bumped to ^2.62.1 +- `phpunit/phpunit` version bumped to ^9.5.10 +- `Header::get()` always returns an `Attribute::class` instance +- `Attribute::class` accessor methods renamed to shorten their names and improve the readability +- All protocol methods that used to return `array|bool` will now always return a `Response::class` instance. +- `ResponseException::class` gets thrown if a response is empty or contains errors +- Message client is optional and can be null (e.g. if used in combination with `Message::fromFile()`) +- The message text or html body is now "" if its empty and not `null` + + +## [4.1.2] - 2022-12-14 +### Fixed +- Attachment ID can return an empty value #318 +- Additional message date format added #345 (thanks @amorebietakoUdala) + + +## [4.1.1] - 2022-11-16 +### Fixed +- Fix for extension recognition #325 (thanks @pwoszczyk) +- Missing null check added #327 (thanks @spanjeta) +- Leading white-space in response causes an infinite loop #321 (thanks @thin-k-design) +- Fix error when creating folders with special chars #319 (thanks @thin-k-design) +- `Client::getFoldersWithStatus()` recursive loading fixed #312 (thanks @szymekjanaczek) +- Fix Folder name encoding error in `Folder::appendMessage()` #306 #307 (thanks @rskrzypczak) + + +## [4.1.0] - 2022-10-18 +### Fixed +- Fix assumedNextTaggedLine bug #288 (thanks @Blear) +- Fix empty response error for blank lines #274 (thanks @bierpub) +- Fix empty body #233 (thanks @latypoff) +- Fix imap_reopen folder argument #234 (thanks @latypoff) + +### Added +- Added possibility of loading a Folder status #298 (thanks @szymekjanaczek) + + +## [4.0.2] - 2022-08-26 +### Fixed +- RFC 822 3.1.1. long header fields regular expression fixed #268 #269 (thanks @hbraehne) + + +## [4.0.1] - 2022-08-25 +### Fixed +- Type casting added to several ImapProtocol return values #261 +- Remove IMAP::OP_READONLY flag from imap_reopen if POP3 or NNTP protocol is selected #135 (thanks @xianzhe18) +- Several statements optimized and redundant checks removed +- Check if the Protocol supports the fetch method if extensions are present +- Detect `NONEXISTENT` errors while selecting or examining a folder #266 +- Missing type cast added to `PaginatedCollection::paginate` #267 (thanks @rogerb87) +- Fix multiline header unfolding #250 (thanks @sulgie-eitea) +- Fix problem with illegal offset error #226 (thanks @szymekjanaczek) +- Typos fixed + +### Affected Classes +- [Query::class](src/Query/Query.php) +- [ImapProtocol::class](src/Connection/Protocols/ImapProtocol.php) +- [LegacyProtocol::class](src/Connection/Protocols/LegacyProtocol.php) +- [PaginatedCollection::class](src/Support/PaginatedCollection.php) + + +## [4.0.0] - 2022-08-19 +### Fixed +- PHP dependency updated to support php v8.0 #212 #214 (thanks @freescout-helpdesk) +- Method return and argument types added +- Imap `DONE` method refactored +- UID cache loop fixed +- `HasEvent::getEvent` return value set to mixed to allow multiple event types +- Protocol line reader changed to `fread` (stream_context timeout issue fixed) +- Issue setting the client timeout fixed +- IMAP Connection debugging improved +- `Folder::idle()` method reworked and several issues fixed #170 #229 #237 #249 #258 +- Datetime conversion rules extended #189 #173 + +### Affected Classes +- [Client::class](src/Client.php) +- [Folder::class](src/Folder.php) +- [ImapProtocol::class](src/Connection/Protocols/ImapProtocol.php) +- [HasEvents::class](src/Traits/HasEvents.php) + +### Breaking changes +- No longer supports php >=5.5.9 but instead requires at least php v7.0.0. +- `HasEvent::getEvent` returns a mixed result. Either an `Event` or a class string representing the event class. +- The error message, if the connection fails to read the next line, is now `empty response` instead of `failed to read - connection closed?`. +- The `$auto_reconnect` used with `Folder::indle()` is deprecated and doesn't serve any purpose anymore. + + +## [3.2.0] - 2022-03-07 +### Fixed +- Fix attribute serialization #179 (thanks @netpok) +- Use real tls instead of starttls #180 (thanks @netpok) +- Allow to fully overwrite default config arrays #194 (thanks @laurent-rizer) +- Query::chunked does not loop over the last chunk #196 (thanks @laurent-rizer) +- Fix isAttachment that did not properly take in consideration dispositions options #195 (thanks @laurent-rizer) +- Extend date parsing error message #173 +- Fixed 'Where' method replaces the content with uppercase #148 +- Don't surround numeric search values with quotes +- Context added to `InvalidWhereQueryCriteriaException` +- Redundant `stream_set_timeout()` removed + +### Added +- UID Cache added #204 (thanks @HelloSebastian) +- Query::class extended with `getByUidLower`, `getByUidLowerOrEqual` , `getByUidGreaterOrEqual` , `getByUidGreater` to fetch certain ranges of uids #201 (thanks @HelloSebastian) +- Check if IDLE is supported if `Folder::idle()` is called #199 (thanks @HelloSebastian) +- Fallback date support added. The config option `options.fallback_date` is used as fallback date is it is set. Otherwise, an exception will be thrown #198 +- UID filter support added +- Make boundary regex configurable #169 #150 #126 #121 #111 #152 #108 (thanks @EthraZa) +- IMAP ID support added #174 +- Enable debug mode via config +- Custom UID alternative support added +- Fetch additional extensions using `Folder::query(["FEATURE_NAME"])` +- Optionally move a message during "deletion" instead of just "flagging" it #106 (thanks @EthraZa) +- `WhereQuery::where()` accepts now a wide range of criteria / values. #104 + +### Affected Classes +- [Part::class](src/Part.php) +- [Query::class](src/Query/Query.php) +- [Client::class](src/Client.php) +- [Header::class](src/Header.php) +- [Protocol::class](src/Connection/Protocols/Protocol.php) +- [ClientManager::class](src/ClientManager.php) + +### Breaking changes +- If you are using the legacy protocol to search, the results no longer return false if the search criteria could not be interpreted but instead return an empty array. This will ensure it is compatible to the rest of this library and no longer result in a potential type confusion. +- `Folder::idle` will throw an `Webklex\PHPIMAP\Exceptions\NotSupportedCapabilityException` exception if IMAP isn't supported by the mail server +- All protocol methods which had a `boolean` `$uid` option no longer support a boolean value. Use `IMAP::ST_UID` or `IMAP::NIL` instead. If you want to use an alternative to `UID` just use the string instead. +- Default config option `options.sequence` changed from `IMAP::ST_MSGN` to `IMAP::ST_UID`. +- `Folder::query()` no longer accepts a charset string. It has been replaced by an extension array, which provides the ability to automatically fetch additional features. + + +## [3.1.0-alpha] - 2022-02-03 +### Fixed +- Fix attribute serialization #179 (thanks @netpok) +- Use real tls instead of starttls #180 (thanks @netpok) +- Allow to fully overwrite default config arrays #194 (thanks @laurent-rizer) +- Query::chunked does not loop over the last chunk #196 (thanks @laurent-rizer) +- Fix isAttachment that did not properly take in consideration dispositions options #195 (thanks @laurent-rizer) + +### Affected Classes +- [Header::class](src/Header.php) +- [Protocol::class](src/Connection/Protocols/Protocol.php) +- [Query::class](src/Query/Query.php) +- [Part::class](src/Part.php) +- [ClientManager::class](src/ClientManager.php) + +## [3.0.0-alpha] - 2021-11-04 +### Fixed +- Extend date parsing error message #173 +- Fixed 'Where' method replaces the content with uppercase #148 +- Don't surround numeric search values with quotes +- Context added to `InvalidWhereQueryCriteriaException` +- Redundant `stream_set_timeout()` removed + +### Added +- Make boundary regex configurable #169 #150 #126 #121 #111 #152 #108 (thanks @EthraZa) +- IMAP ID support added #174 +- Enable debug mode via config +- Custom UID alternative support added +- Fetch additional extensions using `Folder::query(["FEATURE_NAME"])` +- Optionally move a message during "deletion" instead of just "flagging" it #106 (thanks @EthraZa) +- `WhereQuery::where()` accepts now a wide range of criteria / values. #104 + +### Affected Classes +- [Header::class](src/Header.php) +- [Protocol::class](src/Connection/Protocols/Protocol.php) +- [Query::class](src/Query/Query.php) +- [WhereQuery::class](src/Query/WhereQuery.php) +- [Message::class](src/Message.php) + +### Breaking changes +- All protocol methods which had a `boolean` `$uid` option no longer support a boolean. Use `IMAP::ST_UID` or `IMAP::NIL` instead. If you want to use an alternative to `UID` just use the string instead. +- Default config option `options.sequence` changed from `IMAP::ST_MSGN` to `IMAP::ST_UID`. +- `Folder::query()` no longer accepts a charset string. It has been replaced by an extension array, which provides the ability to automatically fetch additional features. + +## [2.7.2] - 2021-09-27 +### Fixed +- Fixed problem with skipping last line of the response. #166 (thanks @szymekjanaczek) + +## [2.7.1] - 2021-09-08 +### Added +- Added `UID` as available search criteria #161 (thanks @szymekjanaczek) + +## [2.7.0] - 2021-09-04 +### Fixed +- Fixes handling of long header lines which are seperated by `\r\n\t` (thanks @Oliver-Holz) +- Fixes to line parsing with multiple addresses (thanks @Oliver-Holz) + +### Added +- Expose message folder path #154 (thanks @Magiczne) +- Adds mailparse_rfc822_parse_addresses integration (thanks @Oliver-Holz) +- Added moveManyMessages method (thanks @Magiczne) +- Added copyManyMessages method (thanks @Magiczne) + +### Affected Classes +- [Header::class](src/Header.php) +- [Message::class](src/Message.php) + +## [2.6.0] - 2021-08-20 +### Fixed +- POP3 fixes #151 (thanks @Korko) + +### Added +- Added imap 4 handling. #146 (thanks @szymekjanaczek) +- Added laravel's conditionable methods. #147 (thanks @szymekjanaczek) + +### Affected Classes +- [Query::class](src/Query/Query.php) +- [Client::class](src/Client.php) + +## [2.5.1] - 2021-06-19 +### Fixed +- Fix setting default mask from config #133 (thanks @shacky) +- Chunked fetch fails in case of less available mails than page size #114 +- Protocol::createStream() exception information fixed #137 +- Legacy methods (headers, content, flags) fixed #125 +- Legacy connection cycle fixed #124 (thanks @zssarkany) + +### Added +- Disable rfc822 header parsing via config option #115 + +## [2.5.0] - 2021-02-01 +### Fixed +- Attachment saving filename fixed +- Unnecessary parameter removed from `Client::getTimeout()` +- Missing encryption variable added - could have caused problems with unencrypted communications +- Prefer attachment filename attribute over name attribute #82 +- Missing connection settings added to `Folder:idle()` auto mode #89 +- Message move / copy expect a folder path #79 +- `Client::getFolder()` updated to circumvent special edge cases #79 +- Missing connection status checks added to various methods +- Unused default attribute `message_no` removed from `Message::class` + +### Added +- Dynamic Attribute access support added (e.g `$message->from[0]`) +- Message not found exception added #93 +- Chunked fetching support added `Query::chunked()`. Just in case you can't fetch all messages at once +- "Soft fail" support added +- Count method added to `Attribute:class` +- Convert an Attribute instance into a Carbon date object #95 + +### Affected Classes +- [Attachment::class](src/Attachment.php) +- [Attribute::class](src/Attribute.php) +- [Query::class](src/Query/Query.php) +- [Message::class](src/Message.php) +- [Client::class](src/Client.php) +- [Folder::class](src/Folder.php) + +### Breaking changes +- A new exception can occur if a message can't be fetched (`\Webklex\PHPIMAP\Exceptions\MessageNotFoundException::class`) +- `Message::move()` and `Message::copy()` no longer accept folder names as folder path +- A `Message::class` instance might no longer have a `message_no` attribute + +## [2.4.4] - 2021-01-22 +### Fixed +- Boundary detection simplified #90 +- Prevent potential body overwriting #90 +- CSV files are no longer regarded as plain body +- Boundary detection overhauled to support "related" and "alternative" multipart messages #90 #91 + +### Affected Classes +- [Structure::class](src/Structure.php) +- [Message::class](src/Message.php) +- [Header::class](src/Header.php) +- [Part::class](src/Part.php) + +## [2.4.3] - 2021-01-21 +### Fixed +- Attachment detection updated #82 #90 +- Timeout handling improved +- Additional utf-8 checks added to prevent decoding of unencoded values #76 + +### Added +- Auto reconnect option added to `Folder::idle()` #89 + +### Affected Classes +- [Folder::class](src/Folder.php) +- [Part::class](src/Part.php) +- [Client::class](src/Client.php) +- [Header::class](src/Header.php) + +## [2.4.2] - 2021-01-09 +### Fixed +- Attachment::save() return error 'A facade root has not been set' #87 +- Unused dependencies removed +- Fix PHP 8 error that changes null back in to an empty string. #88 (thanks @mennovanhout) +- Fix regex to be case insensitive #88 (thanks @mennovanhout) + +### Affected Classes +- [Attachment::class](src/Attachment.php) +- [Address::class](src/Address.php) +- [Attribute::class](src/Attribute.php) +- [Structure::class](src/Structure.php) + +## [2.4.1] - 2021-01-06 +### Fixed +- Debug line position fixed +- Handle incomplete address to string conversion #83 +- Configured message key gets overwritten by the first fetched message #84 + +### Affected Classes +- [Address::class](src/Address.php) +- [Query::class](src/Query/Query.php) + +## [2.4.0] - 2021-01-03 +### Fixed +- Get partial overview when `IMAP::ST_UID` is set #74 +- Unnecessary "'" removed from address names +- Folder referral typo fixed +- Legacy protocol fixed +- Treat message collection keys always as strings + +### Added +- Configurable supported default flags added +- Message attribute class added to unify value handling +- Address class added and integrated +- Alias `Message::attachments()` for `Message::getAttachments()` added +- Alias `Message::addFlag()` for `Message::setFlag()` added +- Alias `Message::removeFlag()` for `Message::unsetFlag()` added +- Alias `Message::flags()` for `Message::getFlags()` added +- New Exception `MessageFlagException::class` added +- New method `Message::setSequenceId($id)` added +- Optional Header attributizion option added + +### Affected Classes +- [Folder::class](src/Folder.php) +- [Header::class](src/Header.php) +- [Message::class](src/Message.php) +- [Address::class](src/Address.php) +- [Query::class](src/Query/Query.php) +- [Attribute::class](src/Attribute.php) + +### Breaking changes +- Stringified message headers are now separated by ", " instead of " ". +- All message header values such as subject, message_id, from, to, etc now consists of an `Àttribute::class` instance (should behave the same way as before, but might cause some problem in certain edge cases) +- The formal address object "from", "to", etc now consists of an `Address::class` instance (should behave the same way as before, but might cause some problem in certain edge cases) +- When fetching or manipulating message flags a `MessageFlagException::class` exception can be thrown if a runtime error occurs +- Learn more about the new `Attribute` class here: [www.php-imap.com/api/attribute](https://www.php-imap.com/api/attribute) +- Learn more about the new `Address` class here: [www.php-imap.com/api/address](https://www.php-imap.com/api/address) +- Folder attribute "referal" is now called "referral" + +## [2.3.1] - 2020-12-30 +### Fixed +- Missing RFC attributes added +- Set the message sequence when idling +- Missing UID commands added #64 + +### Added +- Get a message by its message number +- Get a message by its uid #72 #66 #63 + +### Affected Classes +- [Message::class](src/Message.php) +- [Folder::class](src/Folder.php) +- [Query::class](src/Query/Query.php) + +## [2.3.0] - 2020-12-21 +### Fixed +- Cert validation issue fixed +- Allow boundaries ending with a space or semicolon (thanks [@smartilabs](https://github.com/smartilabs)) +- Ignore IMAP DONE command response #57 +- Default `options.fetch` set to `IMAP::FT_PEEK` +- Address parsing fixed #60 +- Alternative rfc822 header parsing fixed #60 +- Parse more than one Received: header #61 +- Fetch folder overview fixed +- `Message::getTextBody()` fallback value fixed + +### Added +- Proxy support added +- Flexible disposition support added #58 +- New `options.message_key` option `uid` added +- Protocol UID support added +- Flexible sequence type support added + +### Affected Classes +- [Structure::class](src/Structure.php) +- [Query::class](src/Query/Query.php) +- [Client::class](src/Client.php) +- [Header::class](src/Header.php) +- [Folder::class](src/Folder.php) +- [Part::class](src/Part.php) + +### Breaking changes +- Depending on your configuration, your certificates actually get checked. Which can cause an aborted connection if the certificate can not be validated. +- Messages don't get flagged as read unless you are using your own custom config. +- All `Header::class` attribute keys are now in a snake_format and no longer minus-separated. +- `Message::getTextBody()` no longer returns false if no text body is present. `null` is returned instead. + +## [2.2.5] - 2020-12-11 +### Fixed +- Missing array decoder method added #51 (thanks [@lutchin](https://github.com/lutchin)) +- Additional checks added to prevent message from getting marked as seen #33 +- Boundary parsing improved #39 #36 (thanks [@AntonioDiPassio-AppSys](https://github.com/AntonioDiPassio-AppSys)) +- Idle operation updated #44 + +### Added +- Force a folder to be opened + +### Affected Classes +- [Header::class](src/Header.php) +- [Folder::class](src/Folder.php) +- [Query::class](src/Query/Query.php) +- [Message::class](src/Message.php) +- [Structure::class](src/Structure.php) + +## [2.2.4] - 2020-12-08 +### Fixed +- Search performance increased by fetching all headers, bodies and flags at once #42 +- Legacy protocol support updated +- Fix Query pagination. (#52 [@mikemiller891](https://github.com/mikemiller891)) + +### Added +- Missing message setter methods added +- `Folder::overview()` method added to fetch all headers of all messages in the current folder + +### Affected Classes +- [Message::class](src/Message.php) +- [Folder::class](src/Folder.php) +- [Query::class](src/Query/Query.php) +- [PaginatedCollection::class](src/Support/PaginatedCollection.php) + +## [2.2.3] - 2020-11-02 +### Fixed +- Text/Html body fetched as attachment if subtype is null #34 +- Potential header overwriting through header extensions #35 +- Prevent empty attachments #37 + +### Added +- Set fetch order during query #41 [@Max13](https://github.com/Max13) + +### Affected Classes +- [Message::class](src/Message.php) +- [Part::class](src/Part.php) +- [Header::class](src/Header.php) +- [Query::class](src/Query/Query.php) + + +## [2.2.2] - 2020-10-20 +### Fixed +- IMAP::FT_PEEK removing "Seen" flag issue fixed #33 + +### Affected Classes +- [Message::class](src/Message.php) + +## [2.2.1] - 2020-10-19 +### Fixed +- Header decoding problem fixed #31 + +### Added +- Search for messages by message-Id +- Search for messages by In-Reply-To +- Message threading added `Message::thread()` +- Default folder locations added + +### Affected Classes +- [Query::class](src/Query/Query.php) +- [Message::class](src/Message.php) +- [Header::class](src/Header.php) + + +## [2.2.0] - 2020-10-16 +### Fixed +- Prevent text bodies from being fetched as attachment #27 +- Missing variable check added to prevent exception while parsing an address [webklex/laravel-imap #356](https://github.com/Webklex/laravel-imap/issues/356) +- Missing variable check added to prevent exception while parsing a part subtype #27 +- Missing variable check added to prevent exception while parsing a part content-type [webklex/laravel-imap #356](https://github.com/Webklex/laravel-imap/issues/356) +- Mixed message header attribute `in_reply_to` "unified" to be always an array #26 +- Potential message moving / copying problem fixed #29 +- Move messages by using `Protocol::moveMessage()` instead of `Protocol::copyMessage()` and `Message::delete()` #29 + +### Added +- `Protocol::moveMessage()` method added #29 + +### Affected Classes +- [Message::class](src/Message.php) +- [Header::class](src/Header.php) +- [Part::class](src/Part.php) + +### Breaking changes +- Text bodies might no longer get fetched as attachment +- `Message::$in_reply_to` type changed from mixed to array + +## [2.1.13] - 2020-10-13 +### Fixed +- Boundary detection problem fixed (#28 [@DasTobbel](https://github.com/DasTobbel)) +- Content-Type detection problem fixed (#28 [@DasTobbel](https://github.com/DasTobbel)) + +### Affected Classes +- [Structure::class](src/Structure.php) + +## [2.1.12] - 2020-10-13 +### Fixed +- If content disposition is multiline, implode the array to a simple string (#25 [@DasTobbel](https://github.com/DasTobbel)) + +### Affected Classes +- [Part::class](src/Part.php) + +## [2.1.11] - 2020-10-13 +### Fixed +- Potential problematic prefixed white-spaces removed from header attributes + +### Added +- Expended `Client::getFolder($name, $deleimiter = null)` to accept either a folder name or path ([@DasTobbel](https://github.com/DasTobbel)) +- Special MS-Exchange header decoding support added + +### Affected Classes +- [Client::class](src/Client.php) +- [Header::class](src/Header.php) + +## [2.1.10] - 2020-10-09 +### Added +- `ClientManager::make()` method added to support undefined accounts + +### Affected Classes +- [ClientManager::class](src/ClientManager.php) + +## [2.1.9] - 2020-10-08 +### Fixed +- Fix inline attachments and embedded images (#22 [@dwalczyk](https://github.com/dwalczyk)) + +### Added +- Alternative attachment names support added (#20 [@oneFoldSoftware](https://github.com/oneFoldSoftware)) +- Fetch message content without leaving a "Seen" flag behind + +### Affected Classes +- [Attachment::class](src/Attachment.php) +- [Message::class](src/Message.php) +- [Part::class](src/Part.php) +- [Query::class](src/Query/Query.php) + +## [2.1.8] - 2020-10-08 +### Fixed +- Possible error during address decoding fixed (#16 [@Slauta](https://github.com/Slauta)) +- Flag event dispatching fixed #15 + +### Added +- Support multiple boundaries (#17, #19 [@dwalczyk](https://github.com/dwalczyk)) + +### Affected Classes +- [Structure::class](src/Structure.php) + +## [2.1.7] - 2020-10-03 +### Fixed +- Fixed `Query::paginate()` (#13 #14 by [@Max13](https://github.com/Max13)) + +### Affected Classes +- [Query::class](src/Query/Query.php) + +## [2.1.6] - 2020-10-02 +### Fixed +- `Message::getAttributes()` hasn't returned all parameters + +### Affected Classes +- [Message::class](src/Message.php) + +### Added +- Part number added to attachment +- `Client::getFolderByPath()` added (#12 by [@Max13](https://github.com/Max13)) +- `Client::getFolderByName()` added (#12 by [@Max13](https://github.com/Max13)) +- Throws exceptions if the authentication fails (#11 by [@Max13](https://github.com/Max13)) + +### Affected Classes +- [Client::class](src/Client.php) + +## [2.1.5] - 2020-09-30 +### Fixed +- Wrong message content property reference fixed (#10) + +## [2.1.4] - 2020-09-30 +### Fixed +- Fix header extension values +- Part header detection method changed (#10) + +### Affected Classes +- [Header::class](src/Header.php) +- [Part::class](src/Part.php) + +## [2.1.3] - 2020-09-29 +### Fixed +- Possible decoding problem fixed +- `Str::class` dependency removed from `Header::class` + +### Affected Classes +- [Header::class](src/Header.php) + +## [2.1.2] - 2020-09-28 +### Fixed +- Dependency problem in `Attachement::getExtension()` fixed (#9) + +### Affected Classes +- [Attachment::class](src/Attachment.php) + +## [2.1.1] - 2020-09-23 +### Fixed +- Missing default config parameter added + +### Added +- Default account config fallback added + +### Affected Classes +- [Client::class](src/Client.php) + +## [2.1.0] - 2020-09-22 +### Fixed +- Quota handling fixed + +### Added +- Event system and callbacks added + +### Affected Classes +- [Client::class](src/Client.php) +- [Folder::class](src/Folder.php) +- [Message::class](src/Message.php) + +## [2.0.1] - 2020-09-20 +### Fixed +- Carbon dependency fixed + +## [2.0.0] - 2020-09-20 +### Fixed +- Missing pagination item records fixed + +### Added +- php-imap module replaced by direct socket communication +- Legacy support added +- IDLE support added +- oAuth support added +- Charset detection method updated +- Decoding fallback charsets added + +### Affected Classes +- All + +## [1.4.5] - 2019-01-23 +### Fixed +- .csv attachement is not processed +- mail part structure property comparison changed to lowercase +- Replace helper functions for Laravel 6.0 #4 (@koenhoeijmakers) +- Date handling in Folder::appendMessage() fixed +- Carbon Exception Parse Data +- Convert sender name from non-utf8 to uf8 (@hwilok) +- Convert encoding of personal data struct + +### Added +- Path prefix option added to Client::getFolder() method +- Attachment size handling added +- Find messages by custom search criteria + +### Affected Classes +- [Query::class](src/Query/WhereQuery.php) +- [Mask::class](src/Support/Masks/Mask.php) +- [Attachment::class](src/Attachment.php) +- [Client::class](src/Client.php) +- [Folder::class](src/Folder.php) +- [Message::class](src/Message.php) + +## [1.4.2.1] - 2019-07-03 +### Fixed +- Error in Attachment::__construct #3 +- Examples added + +## [1.4.2] - 2019-07-02 +### Fixed +- Pagination count total bug #213 +- Changed internal message move and copy methods #210 +- Query::since() query returning empty response #215 +- Carbon Exception Parse Data #45 +- Reading a blank body (text / html) but only from this sender #203 +- Problem with Message::moveToFolder() and multiple moves #31 +- Problem with encoding conversion #203 +- Message null value attribute problem fixed +- Client connection path handling changed to be handled inside the calling method #31 +- iconv(): error suppressor for //IGNORE added #184 +- Typo Folder attribute fullName changed to full_name +- Query scope error fixed #153 +- Replace embedded image with URL #151 +- Fix sender name in non-latin emails sent from Gmail (#155) +- Fix broken non-latin characters in body in ASCII (us-ascii) charset #156 +- Message::getMessageId() returns wrong value #197 +- Message date validation extended #45 #192 +- Removed "-i" from "iso-8859-8-i" in Message::parseBody #146 + +### Added +- Message::getFolder() method +- Create a fast count method for queries #216 +- STARTTLS encryption alias added +- Mailbox fetching exception added #201 +- Message::moveToFolder() fetches new Message::class afterwards #31 +- Message structure accessor added #182 +- Shadow Imap const class added #188 +- Connectable "NOT" queries added +- Additional where methods added +- Message attribute handling changed +- Attachment attribute handling changed +- Message flag handling updated +- Message::getHTMLBody($callback) extended +- Masks added (take look at the examples for more information on masks) +- More examples added +- Query::paginate() method added +- Imap client timeout can be modified and read #186 +- Decoder config options added #175 +- Message search criteria "NOT" added #181 +- Invalid message date exception added +- Blade examples + +### Breaking changes +- Message::moveToFolder() returns either a Message::class instance or null and not a boolean +- Folder::fullName is now Folder::full_name +- Attachment::image_src might no longer work as expected - use Attachment::getImageSrc() instead + +### Affected Classes +- [Folder::class](src/Folder.php) +- [Client::class](src/Client.php) +- [Message::class](src/Message.php) +- [Attachment::class](src/Attachment.php) +- [Query::class](src/Query/Query.php) +- [WhereQuery::class](src/Query/WhereQuery.php) + +## 0.0.3 - 2018-12-02 +### Fixed +- Folder delimiter check added #137 +- Config setting not getting loaded +- Date parsing updated + +### Affected Classes +- [Folder::class](src/IMAP/Client.php) +- [Folder::class](src/IMAP/Message.php) + +## 0.0.1 - 2018-08-13 +### Added +- new php-imap package (fork from [webklex/laravel-imap](https://github.com/Webklex/laravel-imap)) diff --git a/plugins/php-imap/CODE_OF_CONDUCT.md b/plugins/php-imap/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..2ed07c83 --- /dev/null +++ b/plugins/php-imap/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at github@webklex.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/plugins/php-imap/ClientManager.php b/plugins/php-imap/ClientManager.php deleted file mode 100644 index 7f724ffe..00000000 --- a/plugins/php-imap/ClientManager.php +++ /dev/null @@ -1,293 +0,0 @@ -setConfig($config); - } - - /** - * Dynamically pass calls to the default account. - * @param string $method - * @param array $parameters - * - * @return mixed - * @throws Exceptions\MaskNotFoundException - */ - public function __call(string $method, array $parameters) { - $callable = [$this->account(), $method]; - - return call_user_func_array($callable, $parameters); - } - - /** - * Safely create a new client instance which is not listed in accounts - * @param array $config - * - * @return Client - * @throws Exceptions\MaskNotFoundException - */ - public function make(array $config): Client { - return new Client($config); - } - - /** - * Get a dotted config parameter - * @param string $key - * @param null $default - * - * @return mixed|null - */ - public static function get(string $key, $default = null): mixed { - $parts = explode('.', $key); - $value = null; - foreach ($parts as $part) { - if ($value === null) { - if (isset(self::$config[$part])) { - $value = self::$config[$part]; - } else { - break; - } - } else { - if (isset($value[$part])) { - $value = $value[$part]; - } else { - break; - } - } - } - - return $value === null ? $default : $value; - } - - /** - * Get the mask for a given section - * @param string $section section name such as "message" or "attachment" - * - * @return string|null - */ - public static function getMask(string $section): ?string { - $default_masks = ClientManager::get("masks"); - if (isset($default_masks[$section])) { - if (class_exists($default_masks[$section])) { - return $default_masks[$section]; - } - } - return null; - } - - /** - * Resolve a account instance. - * @param string|null $name - * - * @return Client - * @throws Exceptions\MaskNotFoundException - */ - public function account(string $name = null): Client { - $name = $name ?: $this->getDefaultAccount(); - - // If the connection has not been resolved we will resolve it now as all - // the connections are resolved when they are actually needed, so we do - // not make any unnecessary connection to the various queue end-points. - if (!isset($this->accounts[$name])) { - $this->accounts[$name] = $this->resolve($name); - } - - return $this->accounts[$name]; - } - - /** - * Resolve an account. - * @param string $name - * - * @return Client - * @throws Exceptions\MaskNotFoundException - */ - protected function resolve(string $name): Client { - $config = $this->getClientConfig($name); - - return new Client($config); - } - - /** - * Get the account configuration. - * @param string|null $name - * - * @return array - */ - protected function getClientConfig(?string $name): array { - if ($name === null || $name === 'null' || $name === "") { - return ['driver' => 'null']; - } - $account = self::$config["accounts"][$name] ?? []; - - return is_array($account) ? $account : []; - } - - /** - * Get the name of the default account. - * - * @return string - */ - public function getDefaultAccount(): string { - return self::$config['default']; - } - - /** - * Set the name of the default account. - * @param string $name - * - * @return void - */ - public function setDefaultAccount(string $name): void { - self::$config['default'] = $name; - } - - - /** - * Merge the vendor settings with the local config - * - * The default account identifier will be used as default for any missing account parameters. - * If however the default account is missing a parameter the package default account parameter will be used. - * This can be disabled by setting imap.default in your config file to 'false' - * - * @param array|string $config - * - * @return $this - */ - public function setConfig(array|string $config): ClientManager { - - if (is_array($config) === false) { - $config = require $config; - } - - $config_key = 'imap'; - $path = __DIR__ . '/config/' . $config_key . '.php'; - - $vendor_config = require $path; - $config = $this->array_merge_recursive_distinct($vendor_config, $config); - - if (is_array($config)) { - if (isset($config['default'])) { - if (isset($config['accounts']) && $config['default']) { - - $default_config = $vendor_config['accounts']['default']; - if (isset($config['accounts'][$config['default']])) { - $default_config = array_merge($default_config, $config['accounts'][$config['default']]); - } - - if (is_array($config['accounts'])) { - foreach ($config['accounts'] as $account_key => $account) { - $config['accounts'][$account_key] = array_merge($default_config, $account); - } - } - } - } - } - - self::$config = $config; - - return $this; - } - - /** - * Marge arrays recursively and distinct - * - * Merges any number of arrays / parameters recursively, replacing - * entries with string keys with values from latter arrays. - * If the entry or the next value to be assigned is an array, then it - * automatically treats both arguments as an array. - * Numeric entries are appended, not replaced, but only if they are - * unique - * - * @return array|mixed - * - * @link http://www.php.net/manual/en/function.array-merge-recursive.php#96201 - * @author Mark Roduner - */ - private function array_merge_recursive_distinct(): mixed { - - $arrays = func_get_args(); - $base = array_shift($arrays); - - // From https://stackoverflow.com/a/173479 - $isAssoc = function(array $arr) { - if (array() === $arr) return false; - return array_keys($arr) !== range(0, count($arr) - 1); - }; - - if (!is_array($base)) $base = empty($base) ? array() : array($base); - - foreach ($arrays as $append) { - - if (!is_array($append)) $append = array($append); - - foreach ($append as $key => $value) { - - if (!array_key_exists($key, $base) and !is_numeric($key)) { - $base[$key] = $value; - continue; - } - - if ( - ( - is_array($value) - && $isAssoc($value) - ) - || ( - is_array($base[$key]) - && $isAssoc($base[$key]) - ) - ) { - // If the arrays are not associates we don't want to array_merge_recursive_distinct - // else merging $baseConfig['dispositions'] = ['attachment', 'inline'] with $customConfig['dispositions'] = ['attachment'] - // results in $resultConfig['dispositions'] = ['attachment', 'inline'] - $base[$key] = $this->array_merge_recursive_distinct($base[$key], $value); - } else if (is_numeric($key)) { - if (!in_array($value, $base)) $base[] = $value; - } else { - $base[$key] = $value; - } - - } - - } - - return $base; - } -} \ No newline at end of file diff --git a/plugins/php-imap/LICENSE b/plugins/php-imap/LICENSE new file mode 100644 index 00000000..6c13191e --- /dev/null +++ b/plugins/php-imap/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 Webklex + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/php-imap/LICENSE.md b/plugins/php-imap/LICENSE.md new file mode 100644 index 00000000..feae5f32 --- /dev/null +++ b/plugins/php-imap/LICENSE.md @@ -0,0 +1,21 @@ +# The MIT License (MIT) + +Copyright (c) 2016 Malte Goldenbaum + +> Permission is hereby granted, free of charge, to any person obtaining a copy +> of this software and associated documentation files (the "Software"), to deal +> in the Software without restriction, including without limitation the rights +> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +> copies of the Software, and to permit persons to whom the Software is +> furnished to do so, subject to the following conditions: +> +> The above copyright notice and this permission notice shall be included in +> all copies or substantial portions of the Software. +> +> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +> THE SOFTWARE. diff --git a/plugins/php-imap/README.md b/plugins/php-imap/README.md new file mode 100755 index 00000000..2eb4ef40 --- /dev/null +++ b/plugins/php-imap/README.md @@ -0,0 +1,235 @@ + +# IMAP Library for PHP + +[![Latest release on Packagist][ico-release]][link-packagist] +[![Latest prerelease on Packagist][ico-prerelease]][link-packagist] +[![Software License][ico-license]][link-license] +[![Total Downloads][ico-downloads]][link-downloads] +[![Hits][ico-hits]][link-hits] +[![Discord][ico-discord]][link-discord] +[![Snyk][ico-snyk]][link-snyk] + + +## Description +PHP-IMAP is a wrapper for common IMAP communication without the need to have the php-imap module installed / enabled. +The protocol is completely integrated and therefore supports IMAP IDLE operation and the "new" oAuth authentication +process as well. +You can enable the `php-imap` module in order to handle edge cases, improve message decoding quality and is required if +you want to use legacy protocols such as pop3. + +Official documentation: [php-imap.com](https://www.php-imap.com/) + +Laravel wrapper: [webklex/laravel-imap](https://github.com/Webklex/laravel-imap) + +Discord: [discord.gg/rd4cN9h6][link-discord] + +## Table of Contents +- [Documentations](#documentations) +- [Compatibility](#compatibility) +- [Basic usage example](#basic-usage-example) +- [Sponsors](#sponsors) +- [Testing](#testing) +- [Known issues](#known-issues) +- [Support](#support) +- [Features & pull requests](#features--pull-requests) +- [Security](#security) +- [Credits](#credits) +- [License](#license) + + +## Documentations +- Legacy (< v2.0.0): [legacy documentation](https://github.com/Webklex/php-imap/tree/1.4.5) +- Core documentation: [php-imap.com](https://www.php-imap.com/) + + +## Compatibility +| Version | PHP 5.6 | PHP 7 | PHP 8 | +|:--------|:-------:|:-----:|:-----:| +| v5.x | / | / | X | +| v4.x | / | X | X | +| v3.x | / | X | / | +| v2.x | X | X | / | +| v1.x | X | / | / | + +## Basic usage example +This is a basic example, which will echo out all Mails within all imap folders +and will move every message into INBOX.read. Please be aware that this should not be +tested in real life and is only meant to give an impression on how things work. + +```php +use Webklex\PHPIMAP\ClientManager; + +require_once "vendor/autoload.php"; + +$cm = new ClientManager('path/to/config/imap.php'); + +/** @var \Webklex\PHPIMAP\Client $client */ +$client = $cm->account('account_identifier'); + +//Connect to the IMAP Server +$client->connect(); + +//Get all Mailboxes +/** @var \Webklex\PHPIMAP\Support\FolderCollection $folders */ +$folders = $client->getFolders(); + +//Loop through every Mailbox +/** @var \Webklex\PHPIMAP\Folder $folder */ +foreach($folders as $folder){ + + //Get all Messages of the current Mailbox $folder + /** @var \Webklex\PHPIMAP\Support\MessageCollection $messages */ + $messages = $folder->messages()->all()->get(); + + /** @var \Webklex\PHPIMAP\Message $message */ + foreach($messages as $message){ + echo $message->getSubject().'
'; + echo 'Attachments: '.$message->getAttachments()->count().'
'; + echo $message->getHTMLBody(); + + //Move the current Message to 'INBOX.read' + if($message->move('INBOX.read') == true){ + echo 'Message has been moved'; + }else{ + echo 'Message could not be moved'; + } + } +} +``` + +## Sponsors +[![elb-BIT][ico-sponsor-elb-bit]][link-sponsor-elb-bit] +[![Feline][ico-sponsor-feline]][link-sponsor-feline] + + +## Testing +To run the tests, please execute the following command: +```bash +composer test +``` + +### Quick-Test / Static Test +To disable all test which require a live mailbox, please copy the `phpunit.xml.dist` to `phpunit.xml` and adjust the configuration: +```xml + + + +``` + +### Full-Test / Live Mailbox Test +To run all tests, you need to provide a valid imap configuration. + +To provide a valid imap configuration, please copy the `phpunit.xml.dist` to `phpunit.xml` and adjust the configuration: +```xml + + + + + + + + + + + +``` + +The test account should **not** contain any important data, as it will be deleted during the test. +Furthermore, the test account should be able to create new folders, move messages and should **not** be used by any other +application during the test. + +It's recommended to use a dedicated test account for this purpose. You can use the provided `Dockerfile` to create an imap server used for testing purposes. + +Build the docker image: +```bash +cd .github/docker + +docker build -t php-imap-server . +``` +Run the docker image: +```bash +docker run --name imap-server -p 993:993 --rm -d php-imap-server +``` +Stop the docker image: +```bash +docker stop imap-server +``` + + +### Known issues +| Error | Solution | +|:---------------------------------------------------------------------------|:----------------------------------------------------------------------------------------| +| Kerberos error: No credentials cache file found (try running kinit) (...) | Uncomment "DISABLE_AUTHENTICATOR" inside your config and use the `legacy-imap` protocol | + + +## Support +If you encounter any problems or if you find a bug, please don't hesitate to create a new [issue](https://github.com/Webklex/php-imap/issues). +However, please be aware that it might take some time to get an answer. +Off-topic, rude or abusive issues will be deleted without any notice. + +If you need **commercial** support, feel free to send me a mail at github@webklex.com. + + +##### A little notice +If you write source code in your issue, please consider to format it correctly. This makes it so much nicer to read +and people are more likely to comment and help :) + +```php + +echo 'your php code...'; + +``` + +will turn into: +```php +echo 'your php code...'; +``` + + +## Features & pull requests +Everyone can contribute to this project. Every pull request will be considered, but it can also happen to be declined. +To prevent unnecessary work, please consider to create a [feature issue](https://github.com/Webklex/php-imap/issues/new?template=feature_request.md) +first, if you're planning to do bigger changes. Of course, you can also create a new [feature issue](https://github.com/Webklex/php-imap/issues/new?template=feature_request.md) +if you're just wishing a feature ;) + + +## Change log +Please see [CHANGELOG][link-changelog] for more information what has changed recently. + + +## Security +If you discover any security related issues, please email github@webklex.com instead of using the issue tracker. + + +## Credits +- [Webklex][link-author] +- [All Contributors][link-contributors] + + +## License +The MIT License (MIT). Please see [License File][link-license] for more information. + + +[ico-release]: https://img.shields.io/packagist/v/Webklex/php-imap.svg?style=flat-square&label=version +[ico-prerelease]: https://img.shields.io/github/v/release/webklex/php-imap?include_prereleases&style=flat-square&label=pre-release +[ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square +[ico-downloads]: https://img.shields.io/packagist/dt/Webklex/php-imap.svg?style=flat-square +[ico-hits]: https://hits.webklex.com/svg/webklex/php-imap +[ico-snyk]: https://snyk-widget.herokuapp.com/badge/composer/webklex/php-imap/badge.svg +[ico-discord]: https://img.shields.io/static/v1?label=discord&message=open&color=5865f2&style=flat-square + +[link-packagist]: https://packagist.org/packages/Webklex/php-imap +[link-downloads]: https://packagist.org/packages/Webklex/php-imap +[link-author]: https://github.com/webklex +[link-contributors]: https://github.com/Webklex/php-imap/graphs/contributors +[link-license]: https://github.com/Webklex/php-imap/blob/master/LICENSE +[link-changelog]: https://github.com/Webklex/php-imap/blob/master/CHANGELOG.md +[link-hits]: https://hits.webklex.com +[link-snyk]: https://snyk.io/vuln/composer:webklex%2Fphp-imap +[link-discord]: https://discord.gg/rd4cN9h6 + + +[ico-sponsor-feline]: https://cdn.feline.dk/public/feline.png +[link-sponsor-feline]: https://www.feline.dk +[ico-sponsor-elb-bit]: https://www.elb-bit.de/user/themes/deliver/images/logo_small.png +[link-sponsor-elb-bit]: https://www.elb-bit.de?ref=webklex/php-imap \ No newline at end of file diff --git a/plugins/php-imap/VERSION b/plugins/php-imap/VERSION deleted file mode 100644 index c7ba1e87..00000000 --- a/plugins/php-imap/VERSION +++ /dev/null @@ -1 +0,0 @@ -5.5.0 \ No newline at end of file diff --git a/plugins/php-imap/_config.yml b/plugins/php-imap/_config.yml new file mode 100644 index 00000000..c4192631 --- /dev/null +++ b/plugins/php-imap/_config.yml @@ -0,0 +1 @@ +theme: jekyll-theme-cayman \ No newline at end of file diff --git a/plugins/php-imap/composer.json b/plugins/php-imap/composer.json new file mode 100644 index 00000000..4956e549 --- /dev/null +++ b/plugins/php-imap/composer.json @@ -0,0 +1,61 @@ +{ + "name": "webklex/php-imap", + "type": "library", + "description": "PHP IMAP client", + "keywords": [ + "webklex", + "imap", + "pop3", + "php-imap", + "mail" + ], + "homepage": "https://github.com/webklex/php-imap", + "license": "MIT", + "authors": [ + { + "name": "Malte Goldenbaum", + "email": "github@webklex.com", + "role": "Developer" + } + ], + "require": { + "php": "^8.0.2", + "ext-openssl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "ext-iconv": "*", + "ext-libxml": "*", + "ext-zip": "*", + "ext-fileinfo": "*", + "nesbot/carbon": "^2.62.1|^3.2.4", + "symfony/http-foundation": ">=2.8.0", + "illuminate/pagination": ">=5.0.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5.10" + }, + "suggest": { + "symfony/mime": "Recomended for better extension support", + "symfony/var-dumper": "Usefull tool for debugging" + }, + "autoload": { + "psr-4": { + "Webklex\\PHPIMAP\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "Tests\\": "tests" + } + }, + "scripts": { + "test": "phpunit" + }, + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "minimum-stability": "dev", + "prefer-stable": true +} diff --git a/plugins/php-imap/examples/custom_attachment_mask.php b/plugins/php-imap/examples/custom_attachment_mask.php new file mode 100644 index 00000000..eb4973e3 --- /dev/null +++ b/plugins/php-imap/examples/custom_attachment_mask.php @@ -0,0 +1,57 @@ +id, $this->getMessage()->getUid(), $this->name]); + } + + /** + * Custom attachment saving method + * @return bool + */ + public function custom_save(): bool { + $path = "foo".DIRECTORY_SEPARATOR."bar".DIRECTORY_SEPARATOR; + $filename = $this->token(); + + return file_put_contents($path.$filename, $this->getContent()) !== false; + } + +} + +$cm = new \Webklex\PHPIMAP\ClientManager('path/to/config/imap.php'); + +/** @var \Webklex\PHPIMAP\Client $client */ +$client = $cm->account('default'); +$client->connect(); +$client->setDefaultAttachmentMask(CustomAttachmentMask::class); + +/** @var \Webklex\PHPIMAP\Folder $folder */ +$folder = $client->getFolder('INBOX'); + +/** @var \Webklex\PHPIMAP\Message $message */ +$message = $folder->query()->limit(1)->get()->first(); + +/** @var \Webklex\PHPIMAP\Attachment $attachment */ +$attachment = $message->getAttachments()->first(); + +/** @var CustomAttachmentMask $masked_attachment */ +$masked_attachment = $attachment->mask(); + +echo 'Token for uid ['.$masked_attachment->getMessage()->getUid().']: '.$masked_attachment->token(); + +$masked_attachment->custom_save(); \ No newline at end of file diff --git a/plugins/php-imap/examples/custom_message_mask.php b/plugins/php-imap/examples/custom_message_mask.php new file mode 100644 index 00000000..0463c65b --- /dev/null +++ b/plugins/php-imap/examples/custom_message_mask.php @@ -0,0 +1,51 @@ +message_id, $this->uid, $this->message_no]); + } + + /** + * Get number of message attachments + * @return integer + */ + public function getAttachmentCount(): int { + return $this->getAttachments()->count(); + } + +} + +$cm = new \Webklex\PHPIMAP\ClientManager('path/to/config/imap.php'); + +/** @var \Webklex\PHPIMAP\Client $client */ +$client = $cm->account('default'); +$client->connect(); + +/** @var \Webklex\PHPIMAP\Folder $folder */ +$folder = $client->getFolder('INBOX'); + +/** @var \Webklex\PHPIMAP\Message $message */ +$message = $folder->query()->limit(1)->get()->first(); + +/** @var CustomMessageMask $masked_message */ +$masked_message = $message->mask(CustomMessageMask::class); + +echo 'Token for uid [' . $masked_message->uid . ']: ' . $masked_message->token() . ' @atms:' . $masked_message->getAttachmentCount(); + +$masked_message->setFlag('seen'); + diff --git a/plugins/php-imap/examples/folder_structure.blade.php b/plugins/php-imap/examples/folder_structure.blade.php new file mode 100644 index 00000000..a80dfb6c --- /dev/null +++ b/plugins/php-imap/examples/folder_structure.blade.php @@ -0,0 +1,42 @@ + + + + + + + + + + count() > 0): ?> + + + + + + + + + + + + +
FolderUnread messages
name; ?>search()->unseen()->setFetchBody(false)->count(); ?>
No folders found
+ +links(); ?> \ No newline at end of file diff --git a/plugins/php-imap/examples/message_table.blade.php b/plugins/php-imap/examples/message_table.blade.php new file mode 100644 index 00000000..c3bd7af8 --- /dev/null +++ b/plugins/php-imap/examples/message_table.blade.php @@ -0,0 +1,46 @@ + + + + + + + + + + + + count() > 0): ?> + + + + + + + + + + + + + + +
UIDSubjectFromAttachments
getUid(); ?>getSubject(); ?>getFrom()[0]->mail; ?>getAttachments()->count() > 0 ? 'yes' : 'no'; ?>
No messages found
+ +links(); ?> \ No newline at end of file diff --git a/plugins/php-imap/phpunit.xml.dist b/plugins/php-imap/phpunit.xml.dist new file mode 100644 index 00000000..02d56a89 --- /dev/null +++ b/plugins/php-imap/phpunit.xml.dist @@ -0,0 +1,35 @@ + + + + + src/ + + + + + + + + + + tests + tests/fixtures + tests/issues + tests/live + + + + + + + + + + + + + + + + + diff --git a/plugins/php-imap/Address.php b/plugins/php-imap/src/Address.php similarity index 100% rename from plugins/php-imap/Address.php rename to plugins/php-imap/src/Address.php diff --git a/plugins/php-imap/Attachment.php b/plugins/php-imap/src/Attachment.php similarity index 87% rename from plugins/php-imap/Attachment.php rename to plugins/php-imap/src/Attachment.php index 15b83f63..451dfe26 100755 --- a/plugins/php-imap/Attachment.php +++ b/plugins/php-imap/src/Attachment.php @@ -57,16 +57,23 @@ use Webklex\PHPIMAP\Support\Masks\AttachmentMask; class Attachment { /** - * @var Message $oMessage + * @var Message $message */ - protected Message $oMessage; + protected Message $message; /** * Used config * - * @var array $config + * @var Config $config */ - protected array $config = []; + protected Config $config; + + /** + * Attachment options + * + * @var array $options + */ + protected array $options = []; /** @var Part $part */ protected Part $part; @@ -100,23 +107,24 @@ class Attachment { /** * Attachment constructor. - * @param Message $oMessage + * @param Message $message * @param Part $part */ - public function __construct(Message $oMessage, Part $part) { - $this->config = ClientManager::get('options'); + public function __construct(Message $message, Part $part) { + $this->message = $message; + $this->config = $this->message->getConfig(); + $this->options = $this->config->get('options'); - $this->oMessage = $oMessage; $this->part = $part; $this->part_number = $part->part_number; - if ($this->oMessage->getClient()) { - $default_mask = $this->oMessage->getClient()?->getDefaultAttachmentMask(); + if ($this->message->getClient()) { + $default_mask = $this->message->getClient()?->getDefaultAttachmentMask(); if ($default_mask != null) { $this->mask = $default_mask; } } else { - $default_mask = ClientManager::getMask("attachment"); + $default_mask = $this->config->getMask("attachment"); if ($default_mask != "") { $this->mask = $default_mask; } @@ -205,7 +213,7 @@ class Attachment { $content = $this->part->content; $this->content_type = $this->part->content_type; - $this->content = $this->oMessage->decodeString($content, $this->part->encoding); + $this->content = $this->message->decodeString($content, $this->part->encoding); // Create a hash of the raw part - this can be used to identify the attachment in the message context. However, // it is not guaranteed to be unique and collisions are possible. @@ -292,7 +300,7 @@ class Attachment { } } - $decoder = $this->config['decoder']['message']; + $decoder = $this->options['decoder']['message']; if (preg_match('/=\?([^?]+)\?(Q|B)\?(.+)\?=/i', $name, $matches)) { $name = $this->part->getHeader()->decode($name); } elseif ($decoder === 'utf-8' && extension_loaded('imap')) { @@ -364,7 +372,7 @@ class Attachment { * @return Message */ public function getMessage(): Message { - return $this->oMessage; + return $this->message; } /** @@ -390,6 +398,45 @@ class Attachment { return $this->mask; } + /** + * Get the attachment options + * @return array + */ + public function getOptions(): array { + return $this->options; + } + + /** + * Set the attachment options + * @param array $options + * + * @return $this + */ + public function setOptions(array $options): Attachment { + $this->options = $options; + return $this; + } + + /** + * Get the used config + * + * @return Config + */ + public function getConfig(): Config { + return $this->config; + } + + /** + * Set the used config + * @param Config $config + * + * @return $this + */ + public function setConfig(Config $config): Attachment { + $this->config = $config; + return $this; + } + /** * Get a masked instance by providing a mask name * @param string|null $mask diff --git a/plugins/php-imap/Attribute.php b/plugins/php-imap/src/Attribute.php similarity index 100% rename from plugins/php-imap/Attribute.php rename to plugins/php-imap/src/Attribute.php diff --git a/plugins/php-imap/Client.php b/plugins/php-imap/src/Client.php similarity index 91% rename from plugins/php-imap/Client.php rename to plugins/php-imap/src/Client.php index 8027dc53..2ec9ebd9 100755 --- a/plugins/php-imap/Client.php +++ b/plugins/php-imap/src/Client.php @@ -46,6 +46,13 @@ class Client { */ public ?ProtocolInterface $connection = null; + /** + * Client configuration + * + * @var Config + */ + protected Config $config; + /** * Server hostname. * @@ -174,14 +181,14 @@ class Client { /** * Client constructor. - * @param array $config + * @param Config $config * * @throws MaskNotFoundException */ - public function __construct(array $config = []) { + public function __construct(Config $config) { $this->setConfig($config); - $this->setMaskFromConfig($config); - $this->setEventsFromConfig($config); + $this->setMaskFromConfig(); + $this->setEventsFromConfig(); } /** @@ -199,16 +206,17 @@ class Client { * Clone the current Client instance * * @return Client + * @throws MaskNotFoundException */ public function clone(): Client { - $client = new self(); + $client = new self($this->config); $client->events = $this->events; $client->timeout = $this->timeout; $client->active_folder = $this->active_folder; $client->default_account_config = $this->default_account_config; $config = $this->getAccountConfig(); foreach($config as $key => $value) { - $client->setAccountConfig($key, $config, $this->default_account_config); + $client->setAccountConfig($key, $this->default_account_config); } $client->default_message_mask = $this->default_message_mask; $client->default_attachment_mask = $this->default_message_mask; @@ -217,16 +225,17 @@ class Client { /** * Set the Client configuration - * @param array $config + * @param Config $config * * @return self */ - public function setConfig(array $config): Client { - $default_account = ClientManager::get('default'); - $default_config = ClientManager::get("accounts.$default_account"); + public function setConfig(Config $config): Client { + $this->config = $config; + $default_account = $this->config->get('default'); + $default_config = $this->config->get("accounts.$default_account"); foreach ($this->default_account_config as $key => $value) { - $this->setAccountConfig($key, $config, $default_config); + $this->setAccountConfig($key, $default_config); } return $this; @@ -235,27 +244,20 @@ class Client { /** * Get the current config * - * @return array + * @return Config */ - public function getConfig(): array { - $config = []; - foreach($this->default_account_config as $key => $value) { - $config[$key] = $this->$key; - } - return $config; + public function getConfig(): Config { + return $this->config; } /** * Set a specific account config * @param string $key - * @param array $config * @param array $default_config */ - private function setAccountConfig(string $key, array $config, array $default_config): void { + private function setAccountConfig(string $key, array $default_config): void { $value = $this->default_account_config[$key]; - if(isset($config[$key])) { - $value = $config[$key]; - }elseif(isset($default_config[$key])) { + if(isset($default_config[$key])) { $value = $default_config[$key]; } $this->$key = $value; @@ -278,10 +280,9 @@ class Client { /** * Look for a possible events in any available config - * @param $config */ - protected function setEventsFromConfig($config): void { - $this->events = ClientManager::get("events"); + protected function setEventsFromConfig(): void { + $this->events = $this->config->get("events"); if(isset($config['events'])){ foreach($config['events'] as $section => $events) { $this->events[$section] = array_merge($this->events[$section], $events); @@ -291,35 +292,35 @@ class Client { /** * Look for a possible mask in any available config - * @param $config * * @throws MaskNotFoundException */ - protected function setMaskFromConfig($config): void { + protected function setMaskFromConfig(): void { + $masks = $this->config->get("masks"); - if(isset($config['masks'])){ - if(isset($config['masks']['message'])) { - if(class_exists($config['masks']['message'])) { - $this->default_message_mask = $config['masks']['message']; + if(isset($masks)){ + if(isset($masks['message'])) { + if(class_exists($masks['message'])) { + $this->default_message_mask = $masks['message']; }else{ - throw new MaskNotFoundException("Unknown mask provided: ".$config['masks']['message']); + throw new MaskNotFoundException("Unknown mask provided: ".$masks['message']); } }else{ - $default_mask = ClientManager::getMask("message"); + $default_mask = $this->config->getMask("message"); if($default_mask != ""){ $this->default_message_mask = $default_mask; }else{ throw new MaskNotFoundException("Unknown message mask provided"); } } - if(isset($config['masks']['attachment'])) { - if(class_exists($config['masks']['attachment'])) { - $this->default_attachment_mask = $config['masks']['attachment']; + if(isset($masks['attachment'])) { + if(class_exists($masks['attachment'])) { + $this->default_attachment_mask = $masks['attachment']; }else{ - throw new MaskNotFoundException("Unknown mask provided: ". $config['masks']['attachment']); + throw new MaskNotFoundException("Unknown mask provided: ". $masks['attachment']); } }else{ - $default_mask = ClientManager::getMask("attachment"); + $default_mask = $this->config->getMask("attachment"); if($default_mask != ""){ $this->default_attachment_mask = $default_mask; }else{ @@ -327,14 +328,14 @@ class Client { } } }else{ - $default_mask = ClientManager::getMask("message"); + $default_mask = $this->config->getMask("message"); if($default_mask != ""){ $this->default_message_mask = $default_mask; }else{ throw new MaskNotFoundException("Unknown message mask provided"); } - $default_mask = ClientManager::getMask("attachment"); + $default_mask = $this->config->getMask("attachment"); if($default_mask != ""){ $this->default_attachment_mask = $default_mask; }else{ @@ -424,25 +425,25 @@ class Client { $protocol = strtolower($this->protocol); if (in_array($protocol, ['imap', 'imap4', 'imap4rev1'])) { - $this->connection = new ImapProtocol($this->validate_cert, $this->encryption); + $this->connection = new ImapProtocol($this->config, $this->validate_cert, $this->encryption); $this->connection->setConnectionTimeout($this->timeout); $this->connection->setProxy($this->proxy); }else{ if (extension_loaded('imap') === false) { throw new ConnectionFailedException("connection setup failed", 0, new ProtocolNotSupportedException($protocol." is an unsupported protocol")); } - $this->connection = new LegacyProtocol($this->validate_cert, $this->encryption); + $this->connection = new LegacyProtocol($this->config, $this->validate_cert, $this->encryption); if (str_starts_with($protocol, "legacy-")) { $protocol = substr($protocol, 7); } $this->connection->setProtocol($protocol); } - if (ClientManager::get('options.debug')) { + if ($this->config->get('options.debug')) { $this->connection->enableDebug(); } - if (!ClientManager::get('options.uid_cache')) { + if (!$this->config->get('options.uid_cache')) { $this->connection->disableUidCache(); } @@ -507,7 +508,7 @@ class Client { */ public function getFolder(string $folder_name, ?string $delimiter = null, bool $utf7 = false): ?Folder { // Set delimiter to false to force selection via getFolderByName (maybe useful for uncommon folder names) - $delimiter = is_null($delimiter) ? ClientManager::get('options.delimiter', "/") : $delimiter; + $delimiter = is_null($delimiter) ? $this->config->get('options.delimiter', "/") : $delimiter; if (str_contains($folder_name, (string)$delimiter)) { return $this->getFolderByPath($folder_name, $utf7); diff --git a/plugins/php-imap/src/ClientManager.php b/plugins/php-imap/src/ClientManager.php new file mode 100644 index 00000000..a63f3a4e --- /dev/null +++ b/plugins/php-imap/src/ClientManager.php @@ -0,0 +1,131 @@ +setConfig($config); + } + + /** + * Dynamically pass calls to the default account. + * @param string $method + * @param array $parameters + * + * @return mixed + * @throws Exceptions\MaskNotFoundException + */ + public function __call(string $method, array $parameters) { + $callable = [$this->account(), $method]; + + return call_user_func_array($callable, $parameters); + } + + /** + * Safely create a new client instance which is not listed in accounts + * @param array $config + * + * @return Client + * @throws Exceptions\MaskNotFoundException + */ + public function make(array $config): Client { + $name = $this->config->getDefaultAccount(); + $clientConfig = $this->config->all(); + $clientConfig["accounts"] = [$name => $config]; + return new Client(Config::make($clientConfig)); + } + + /** + * Resolve a account instance. + * @param string|null $name + * + * @return Client + * @throws Exceptions\MaskNotFoundException + */ + public function account(string $name = null): Client { + $name = $name ?: $this->config->getDefaultAccount(); + + // If the connection has not been resolved we will resolve it now as all + // the connections are resolved when they are actually needed, so we do + // not make any unnecessary connection to the various queue end-points. + if (!isset($this->accounts[$name])) { + $this->accounts[$name] = $this->resolve($name); + } + + return $this->accounts[$name]; + } + + /** + * Resolve an account. + * @param string $name + * + * @return Client + * @throws Exceptions\MaskNotFoundException + */ + protected function resolve(string $name): Client { + $config = $this->config->getClientConfig($name); + + return new Client($config); + } + + + /** + * Merge the vendor settings with the local config + * + * The default account identifier will be used as default for any missing account parameters. + * If however the default account is missing a parameter the package default account parameter will be used. + * This can be disabled by setting imap.default in your config file to 'false' + * + * @param array|string|Config $config + * + * @return $this + */ + public function setConfig(array|string|Config $config): ClientManager { + if (!$config instanceof Config) { + $config = Config::make($config); + } + $this->config = $config; + + return $this; + } + + /** + * Get the config instance + * @return Config + */ + public function getConfig(): Config { + return $this->config; + } +} \ No newline at end of file diff --git a/plugins/php-imap/src/Config.php b/plugins/php-imap/src/Config.php new file mode 100644 index 00000000..b5cf2599 --- /dev/null +++ b/plugins/php-imap/src/Config.php @@ -0,0 +1,266 @@ +config = $config; + } + + /** + * Get a dotted config parameter + * @param string $key + * @param null $default + * + * @return mixed|null + */ + public function get(string $key, $default = null): mixed { + $parts = explode('.', $key); + $value = null; + foreach ($parts as $part) { + if ($value === null) { + if (isset($this->config[$part])) { + $value = $this->config[$part]; + } else { + break; + } + } else { + if (isset($value[$part])) { + $value = $value[$part]; + } else { + break; + } + } + } + + return $value === null ? $default : $value; + } + + /** + * Set a dotted config parameter + * @param string $key + * @param string|array|mixed$value + * + * @return void + */ + public function set(string $key, mixed $value): void { + $parts = explode('.', $key); + $config = &$this->config; + + foreach ($parts as $part) { + if (!isset($config[$part])) { + $config[$part] = []; + } + $config = &$config[$part]; + } + + if(is_array($config) && is_array($value)){ + $config = array_merge($config, $value); + }else{ + $config = $value; + } + } + + /** + * Get the mask for a given section + * @param string $section section name such as "message" or "attachment" + * + * @return string|null + */ + public function getMask(string $section): ?string { + $default_masks = $this->get('masks', []); + if (isset($default_masks[$section])) { + if (class_exists($default_masks[$section])) { + return $default_masks[$section]; + } + } + return null; + } + + /** + * Get the account configuration. + * @param string|null $name + * + * @return self + */ + public function getClientConfig(?string $name): self { + $config = $this->all(); + $defaultName = $this->getDefaultAccount(); + $defaultAccount = $this->get('accounts.'.$defaultName, []); + + if ($name === null || $name === 'null' || $name === "") { + $account = $defaultAccount; + $name = $defaultName; + }else{ + $account = $this->get('accounts.'.$name, $defaultAccount); + } + + $config["default"] = $name; + $config["accounts"] = [ + $name => $account + ]; + + return new self($config); + } + + /** + * Get the name of the default account. + * + * @return string + */ + public function getDefaultAccount(): string { + return $this->get('default', 'default'); + } + + /** + * Set the name of the default account. + * @param string $name + * + * @return void + */ + public function setDefaultAccount(string $name): void { + $this->set('default', $name); + } + + /** + * Create a new instance of the Config class + * @param array|string $config + * @return Config + */ + public static function make(array|string $config = []): Config { + if (is_array($config) === false) { + $config = require $config; + } + + $config_key = 'imap'; + $path = __DIR__ . '/config/' . $config_key . '.php'; + + $vendor_config = require $path; + $config = self::array_merge_recursive_distinct($vendor_config, $config); + + if (isset($config['default'])) { + if (isset($config['accounts']) && $config['default']) { + + $default_config = $vendor_config['accounts']['default']; + if (isset($config['accounts'][$config['default']])) { + $default_config = array_merge($default_config, $config['accounts'][$config['default']]); + } + + if (is_array($config['accounts'])) { + foreach ($config['accounts'] as $account_key => $account) { + $config['accounts'][$account_key] = array_merge($default_config, $account); + } + } + } + } + + return new self($config); + } + + /** + * Marge arrays recursively and distinct + * + * Merges any number of arrays / parameters recursively, replacing + * entries with string keys with values from latter arrays. + * If the entry or the next value to be assigned is an array, then it + * automatically treats both arguments as an array. + * Numeric entries are appended, not replaced, but only if they are + * unique + * + * @return array + * + * @link http://www.php.net/manual/en/function.array-merge-recursive.php#96201 + * @author Mark Roduner + */ + private static function array_merge_recursive_distinct(): array { + $arrays = func_get_args(); + $base = array_shift($arrays); + + // From https://stackoverflow.com/a/173479 + $isAssoc = function(array $arr) { + if (array() === $arr) return false; + return array_keys($arr) !== range(0, count($arr) - 1); + }; + + if (!is_array($base)) $base = empty($base) ? array() : array($base); + + foreach ($arrays as $append) { + if (!is_array($append)) $append = array($append); + + foreach ($append as $key => $value) { + + if (!array_key_exists($key, $base) and !is_numeric($key)) { + $base[$key] = $value; + continue; + } + + if ((is_array($value) && $isAssoc($value)) || (is_array($base[$key]) && $isAssoc($base[$key]))) { + // If the arrays are not associates we don't want to array_merge_recursive_distinct + // else merging $baseConfig['dispositions'] = ['attachment', 'inline'] with $customConfig['dispositions'] = ['attachment'] + // results in $resultConfig['dispositions'] = ['attachment', 'inline'] + $base[$key] = self::array_merge_recursive_distinct($base[$key], $value); + } else if (is_numeric($key)) { + if (!in_array($value, $base)) $base[] = $value; + } else { + $base[$key] = $value; + } + + } + + } + + return $base; + } + + /** + * Get all configuration values + * @return array + */ + public function all(): array { + return $this->config; + } + + /** + * Check if a configuration value exists + * @param string $key + * @return bool + */ + public function has(string $key): bool { + return $this->get($key) !== null; + } + + /** + * Remove all configuration values + * @return $this + */ + public function clear(): static { + $this->config = []; + return $this; + } +} \ No newline at end of file diff --git a/plugins/php-imap/Connection/Protocols/ImapProtocol.php b/plugins/php-imap/src/Connection/Protocols/ImapProtocol.php similarity index 92% rename from plugins/php-imap/Connection/Protocols/ImapProtocol.php rename to plugins/php-imap/src/Connection/Protocols/ImapProtocol.php index 4d54579f..cb7cf4b4 100644 --- a/plugins/php-imap/Connection/Protocols/ImapProtocol.php +++ b/plugins/php-imap/src/Connection/Protocols/ImapProtocol.php @@ -13,6 +13,8 @@ namespace Webklex\PHPIMAP\Connection\Protocols; use Exception; +use Throwable; +use Webklex\PHPIMAP\Config; use Webklex\PHPIMAP\Exceptions\AuthFailedException; use Webklex\PHPIMAP\Exceptions\ConnectionFailedException; use Webklex\PHPIMAP\Exceptions\ImapBadRequestException; @@ -41,10 +43,12 @@ class ImapProtocol extends Protocol { /** * Imap constructor. + * @param Config $config * @param bool $cert_validation set to false to skip SSL certificate validation * @param mixed $encryption Connection encryption method */ - public function __construct(bool $cert_validation = true, mixed $encryption = false) { + public function __construct(Config $config, bool $cert_validation = true, mixed $encryption = false) { + $this->config = $config; $this->setCertValidation($cert_validation); $this->encryption = $encryption; } @@ -90,6 +94,24 @@ class ImapProtocol extends Protocol { return true; } + /** + * Check if the current session is connected + * + * @return bool + * @throws ImapBadRequestException + */ + public function connected(): bool { + if ((bool)$this->stream) { + try { + $this->requestAndResponse('NOOP'); + return true; + } catch (ImapServerErrorException|RuntimeException) { + return false; + } + } + return false; + } + /** * Enable tls on the current connection * @@ -98,7 +120,7 @@ class ImapProtocol extends Protocol { * @throws ImapServerErrorException * @throws RuntimeException */ - protected function enableStartTls() { + protected function enableStartTls(): void { $response = $this->requestAndResponse('STARTTLS'); $result = $response->successful() && stream_socket_enable_crypto($this->stream, true, $this->getCryptoMethod()); if (!$result) { @@ -114,7 +136,7 @@ class ImapProtocol extends Protocol { */ public function nextLine(Response $response): string { $line = ""; - while (($next_char = fread($this->stream, 1)) !== false && !in_array($next_char, ["","\n"])) { + while (($next_char = fread($this->stream, 1)) !== false && !in_array($next_char, ["", "\n"])) { $line .= $next_char; } if ($line === "" && ($next_char === false || $next_char === "")) { @@ -300,16 +322,35 @@ class ImapProtocol extends Protocol { $tokens = [trim(substr($tokens, 0, 3))]; } - $original = is_array($original)?$original : [$original]; + $original = is_array($original) ? $original : [$original]; + // last line has response code if ($tokens[0] == 'OK') { return $lines ?: [true]; } elseif ($tokens[0] == 'NO' || $tokens[0] == 'BAD' || $tokens[0] == 'BYE') { - throw new ImapServerErrorException(implode("\n", $original)); + throw new ImapServerErrorException($this->stringifyArray($original)); } - throw new ImapBadRequestException(implode("\n", $original)); + throw new ImapBadRequestException($this->stringifyArray($original)); + } + + /** + * Convert an array to a string + * @param array $arr array to stringify + * + * @return string stringified array + */ + private function stringifyArray(array $arr): string { + $string = ""; + foreach ($arr as $value) { + if (is_array($value)) { + $string .= "(" . $this->stringifyArray($value) . ")"; + } else { + $string .= $value . " "; + } + } + return $string; } /** @@ -492,7 +533,7 @@ class ImapProtocol extends Protocol { if (!$this->stream) { $this->reset(); return new Response(0, $this->debug); - }elseif ($this->meta()["timed_out"]) { + } elseif ($this->meta()["timed_out"]) { $this->reset(); return new Response(0, $this->debug); } @@ -501,7 +542,8 @@ class ImapProtocol extends Protocol { try { $result = $this->requestAndResponse('LOGOUT', [], true); fclose($this->stream); - } catch (\Throwable) {} + } catch (Throwable) { + } $this->reset(); @@ -549,7 +591,7 @@ class ImapProtocol extends Protocol { $result = []; $tokens = []; // define $tokens variable before first use - while (!$this->readLine($response, $tokens, $tag, false)) { + while (!$this->readLine($response, $tokens, $tag)) { if ($tokens[0] == 'FLAGS') { array_shift($tokens); $result['flags'] = $tokens; @@ -609,6 +651,42 @@ class ImapProtocol extends Protocol { return $this->examineOrSelect('EXAMINE', $folder); } + /** + * Get the status of a given folder + * + * @param string $folder + * @param string[] $arguments + * @return Response list of STATUS items + * + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws ResponseException + * @throws RuntimeException + */ + public function folderStatus(string $folder = 'INBOX', $arguments = ['MESSAGES', 'UNSEEN', 'RECENT', 'UIDNEXT', 'UIDVALIDITY']): Response { + $response = $this->requestAndResponse('STATUS', [$this->escapeString($folder), $this->escapeList($arguments)]); + $data = $response->validatedData(); + + if (!isset($data[0]) || !isset($data[0][2])) { + throw new RuntimeException("folder status could not be fetched"); + } + + $result = []; + $key = null; + foreach ($data[0][2] as $value) { + if ($key === null) { + $key = $value; + } else { + $result[strtolower($key)] = (int)$value; + $key = null; + } + } + + $response->setResult($result); + + return $response; + } + /** * Fetch one or more items of one or more messages * @param array|string $items items to fetch [RFC822.HEADER, FLAGS, RFC822.TEXT, etc] @@ -723,7 +801,7 @@ class ImapProtocol extends Protocol { } /** - * Fetch message headers + * Fetch message body (without headers) * @param int|array $uids * @param string $rfc * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use @@ -733,7 +811,7 @@ class ImapProtocol extends Protocol { * @throws RuntimeException */ public function content(int|array $uids, string $rfc = "RFC822", int|string $uid = IMAP::ST_UID): Response { - return $this->fetch(["$rfc.TEXT"], is_array($uids)?$uids:[$uids], null, $uid); + return $this->fetch(["$rfc.TEXT"], is_array($uids) ? $uids : [$uids], null, $uid); } /** @@ -747,7 +825,7 @@ class ImapProtocol extends Protocol { * @throws RuntimeException */ public function headers(int|array $uids, string $rfc = "RFC822", int|string $uid = IMAP::ST_UID): Response { - return $this->fetch(["$rfc.HEADER"], is_array($uids)?$uids:[$uids], null, $uid); + return $this->fetch(["$rfc.HEADER"], is_array($uids) ? $uids : [$uids], null, $uid); } /** @@ -760,7 +838,7 @@ class ImapProtocol extends Protocol { * @throws RuntimeException */ public function flags(int|array $uids, int|string $uid = IMAP::ST_UID): Response { - return $this->fetch(["FLAGS"], is_array($uids)?$uids:[$uids], null, $uid); + return $this->fetch(["FLAGS"], is_array($uids) ? $uids : [$uids], null, $uid); } /** @@ -773,7 +851,7 @@ class ImapProtocol extends Protocol { * @throws RuntimeException */ public function sizes(int|array $uids, int|string $uid = IMAP::ST_UID): Response { - return $this->fetch(["RFC822.SIZE"], is_array($uids)?$uids:[$uids], null, $uid); + return $this->fetch(["RFC822.SIZE"], is_array($uids) ? $uids : [$uids], null, $uid); } /** @@ -1186,7 +1264,7 @@ class ImapProtocol extends Protocol { * * @throws RuntimeException */ - public function idle() { + public function idle(): void { $response = $this->sendRequest("IDLE"); if (!$this->assumedNextLine($response, '+ ')) { throw new RuntimeException('idle failed'); @@ -1259,7 +1337,7 @@ class ImapProtocol extends Protocol { $headers = $this->headers($ids, "RFC822", $uid); $response->stack($headers); foreach ($headers->data() as $id => $raw_header) { - $result[$id] = (new Header($raw_header, false))->getAttributes(); + $result[$id] = (new Header($raw_header, $this->config))->getAttributes(); } } return $response->setResult($result)->setCanBeEmpty(true); diff --git a/plugins/php-imap/Connection/Protocols/LegacyProtocol.php b/plugins/php-imap/src/Connection/Protocols/LegacyProtocol.php similarity index 97% rename from plugins/php-imap/Connection/Protocols/LegacyProtocol.php rename to plugins/php-imap/src/Connection/Protocols/LegacyProtocol.php index 10bc9d9f..c6471a02 100644 --- a/plugins/php-imap/Connection/Protocols/LegacyProtocol.php +++ b/plugins/php-imap/src/Connection/Protocols/LegacyProtocol.php @@ -13,6 +13,7 @@ namespace Webklex\PHPIMAP\Connection\Protocols; use Webklex\PHPIMAP\ClientManager; +use Webklex\PHPIMAP\Config; use Webklex\PHPIMAP\Exceptions\AuthFailedException; use Webklex\PHPIMAP\Exceptions\ImapBadRequestException; use Webklex\PHPIMAP\Exceptions\MethodNotSupportedException; @@ -32,10 +33,12 @@ class LegacyProtocol extends Protocol { /** * Imap constructor. + * @param Config $config * @param bool $cert_validation set to false to skip SSL certificate validation * @param mixed $encryption Connection encryption method */ - public function __construct(bool $cert_validation = true, mixed $encryption = false) { + public function __construct(Config $config, bool $cert_validation = true, mixed $encryption = false) { + $this->config = $config; $this->setCertValidation($cert_validation); $this->encryption = $encryption; } @@ -52,7 +55,7 @@ class LegacyProtocol extends Protocol { * @param string $host * @param int|null $port */ - public function connect(string $host, int $port = null) { + public function connect(string $host, int $port = null): void { if ($this->encryption) { $encryption = strtolower($this->encryption); if ($encryption == "ssl") { @@ -81,7 +84,7 @@ class LegacyProtocol extends Protocol { $password, 0, $attempts = 3, - ClientManager::get('options.open') + $this->config->get('options.open') ); $response->addCommand("imap_open"); } catch (\ErrorException $e) { @@ -122,8 +125,6 @@ class LegacyProtocol extends Protocol { * @param string $token access token * * @return Response - * @throws AuthFailedException - * @throws RuntimeException */ public function authenticate(string $user, string $token): Response { return $this->login($user, $token); @@ -236,6 +237,16 @@ class LegacyProtocol extends Protocol { }); } + /** + * Get the status of a given folder + * + * @return Response list of STATUS items + * @throws MethodNotSupportedException + */ + public function folderStatus(string $folder = 'INBOX', $arguments = ['MESSAGES', 'UNSEEN', 'RECENT', 'UIDNEXT', 'UIDVALIDITY']): Response { + throw new MethodNotSupportedException(); + } + /** * Fetch message content * @param int|array $uids @@ -379,7 +390,7 @@ class LegacyProtocol extends Protocol { } /** - * Get a message number for a uid + * Get the message number of a given uid * @param string $id uid * * @return Response message number diff --git a/plugins/php-imap/Connection/Protocols/Protocol.php b/plugins/php-imap/src/Connection/Protocols/Protocol.php similarity index 95% rename from plugins/php-imap/Connection/Protocols/Protocol.php rename to plugins/php-imap/src/Connection/Protocols/Protocol.php index 6fe88ee9..a3886b55 100644 --- a/plugins/php-imap/Connection/Protocols/Protocol.php +++ b/plugins/php-imap/src/Connection/Protocols/Protocol.php @@ -12,6 +12,7 @@ namespace Webklex\PHPIMAP\Connection\Protocols; +use Webklex\PHPIMAP\Config; use Webklex\PHPIMAP\Exceptions\ConnectionFailedException; use Webklex\PHPIMAP\IMAP; @@ -38,10 +39,15 @@ abstract class Protocol implements ProtocolInterface { protected bool $enable_uid_cache = true; /** - * @var resource + * @var resource|mixed|boolean|null $stream */ public $stream = false; + /** + * @var Config $config + */ + protected Config $config; + /** * Connection encryption method * @var string $encryption @@ -268,7 +274,7 @@ abstract class Protocol implements ProtocolInterface { * * @param array|null $uids */ - public function setUidCache(?array $uids) { + public function setUidCache(?array $uids): void { if (is_null($uids)) { $this->uid_cache = []; return; @@ -330,7 +336,7 @@ abstract class Protocol implements ProtocolInterface { } /** - * Retrieves header/meta data from the resource stream + * Retrieves header/metadata from the resource stream * * @return array */ @@ -363,4 +369,13 @@ abstract class Protocol implements ProtocolInterface { public function getStream(): mixed { return $this->stream; } + + /** + * Set the Config instance + * + * @return Config + */ + public function getConfig(): Config { + return $this->config; + } } diff --git a/plugins/php-imap/Connection/Protocols/ProtocolInterface.php b/plugins/php-imap/src/Connection/Protocols/ProtocolInterface.php similarity index 97% rename from plugins/php-imap/Connection/Protocols/ProtocolInterface.php rename to plugins/php-imap/src/Connection/Protocols/ProtocolInterface.php index c02d7800..0a0dbfad 100644 --- a/plugins/php-imap/Connection/Protocols/ProtocolInterface.php +++ b/plugins/php-imap/src/Connection/Protocols/ProtocolInterface.php @@ -117,6 +117,17 @@ interface ProtocolInterface { */ public function examineFolder(string $folder = 'INBOX'): Response; + /** + * Get the status of a given folder + * + * @return Response list of STATUS items + * + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException + */ + public function folderStatus(string $folder = 'INBOX', $arguments = ['MESSAGES', 'UNSEEN', 'RECENT', 'UIDNEXT', 'UIDVALIDITY']): Response; + /** * Fetch message headers * @param int|array $uids diff --git a/plugins/php-imap/Connection/Protocols/Response.php b/plugins/php-imap/src/Connection/Protocols/Response.php similarity index 100% rename from plugins/php-imap/Connection/Protocols/Response.php rename to plugins/php-imap/src/Connection/Protocols/Response.php diff --git a/plugins/php-imap/EncodingAliases.php b/plugins/php-imap/src/EncodingAliases.php similarity index 100% rename from plugins/php-imap/EncodingAliases.php rename to plugins/php-imap/src/EncodingAliases.php diff --git a/plugins/php-imap/Events/Event.php b/plugins/php-imap/src/Events/Event.php similarity index 100% rename from plugins/php-imap/Events/Event.php rename to plugins/php-imap/src/Events/Event.php diff --git a/plugins/php-imap/Events/FlagDeletedEvent.php b/plugins/php-imap/src/Events/FlagDeletedEvent.php similarity index 100% rename from plugins/php-imap/Events/FlagDeletedEvent.php rename to plugins/php-imap/src/Events/FlagDeletedEvent.php diff --git a/plugins/php-imap/Events/FlagNewEvent.php b/plugins/php-imap/src/Events/FlagNewEvent.php similarity index 100% rename from plugins/php-imap/Events/FlagNewEvent.php rename to plugins/php-imap/src/Events/FlagNewEvent.php diff --git a/plugins/php-imap/Events/FolderDeletedEvent.php b/plugins/php-imap/src/Events/FolderDeletedEvent.php similarity index 100% rename from plugins/php-imap/Events/FolderDeletedEvent.php rename to plugins/php-imap/src/Events/FolderDeletedEvent.php diff --git a/plugins/php-imap/Events/FolderMovedEvent.php b/plugins/php-imap/src/Events/FolderMovedEvent.php similarity index 100% rename from plugins/php-imap/Events/FolderMovedEvent.php rename to plugins/php-imap/src/Events/FolderMovedEvent.php diff --git a/plugins/php-imap/Events/FolderNewEvent.php b/plugins/php-imap/src/Events/FolderNewEvent.php similarity index 100% rename from plugins/php-imap/Events/FolderNewEvent.php rename to plugins/php-imap/src/Events/FolderNewEvent.php diff --git a/plugins/php-imap/Events/MessageCopiedEvent.php b/plugins/php-imap/src/Events/MessageCopiedEvent.php similarity index 100% rename from plugins/php-imap/Events/MessageCopiedEvent.php rename to plugins/php-imap/src/Events/MessageCopiedEvent.php diff --git a/plugins/php-imap/Events/MessageDeletedEvent.php b/plugins/php-imap/src/Events/MessageDeletedEvent.php similarity index 100% rename from plugins/php-imap/Events/MessageDeletedEvent.php rename to plugins/php-imap/src/Events/MessageDeletedEvent.php diff --git a/plugins/php-imap/Events/MessageMovedEvent.php b/plugins/php-imap/src/Events/MessageMovedEvent.php similarity index 100% rename from plugins/php-imap/Events/MessageMovedEvent.php rename to plugins/php-imap/src/Events/MessageMovedEvent.php diff --git a/plugins/php-imap/Events/MessageNewEvent.php b/plugins/php-imap/src/Events/MessageNewEvent.php similarity index 100% rename from plugins/php-imap/Events/MessageNewEvent.php rename to plugins/php-imap/src/Events/MessageNewEvent.php diff --git a/plugins/php-imap/Events/MessageRestoredEvent.php b/plugins/php-imap/src/Events/MessageRestoredEvent.php similarity index 100% rename from plugins/php-imap/Events/MessageRestoredEvent.php rename to plugins/php-imap/src/Events/MessageRestoredEvent.php diff --git a/plugins/php-imap/Exceptions/AuthFailedException.php b/plugins/php-imap/src/Exceptions/AuthFailedException.php similarity index 100% rename from plugins/php-imap/Exceptions/AuthFailedException.php rename to plugins/php-imap/src/Exceptions/AuthFailedException.php diff --git a/plugins/php-imap/Exceptions/ConnectionFailedException.php b/plugins/php-imap/src/Exceptions/ConnectionFailedException.php similarity index 100% rename from plugins/php-imap/Exceptions/ConnectionFailedException.php rename to plugins/php-imap/src/Exceptions/ConnectionFailedException.php diff --git a/plugins/php-imap/Exceptions/EventNotFoundException.php b/plugins/php-imap/src/Exceptions/EventNotFoundException.php similarity index 100% rename from plugins/php-imap/Exceptions/EventNotFoundException.php rename to plugins/php-imap/src/Exceptions/EventNotFoundException.php diff --git a/plugins/php-imap/Exceptions/FolderFetchingException.php b/plugins/php-imap/src/Exceptions/FolderFetchingException.php similarity index 100% rename from plugins/php-imap/Exceptions/FolderFetchingException.php rename to plugins/php-imap/src/Exceptions/FolderFetchingException.php diff --git a/plugins/php-imap/Exceptions/GetMessagesFailedException.php b/plugins/php-imap/src/Exceptions/GetMessagesFailedException.php similarity index 100% rename from plugins/php-imap/Exceptions/GetMessagesFailedException.php rename to plugins/php-imap/src/Exceptions/GetMessagesFailedException.php diff --git a/plugins/php-imap/Exceptions/ImapBadRequestException.php b/plugins/php-imap/src/Exceptions/ImapBadRequestException.php similarity index 100% rename from plugins/php-imap/Exceptions/ImapBadRequestException.php rename to plugins/php-imap/src/Exceptions/ImapBadRequestException.php diff --git a/plugins/php-imap/Exceptions/ImapServerErrorException.php b/plugins/php-imap/src/Exceptions/ImapServerErrorException.php similarity index 100% rename from plugins/php-imap/Exceptions/ImapServerErrorException.php rename to plugins/php-imap/src/Exceptions/ImapServerErrorException.php diff --git a/plugins/php-imap/Exceptions/InvalidMessageDateException.php b/plugins/php-imap/src/Exceptions/InvalidMessageDateException.php similarity index 100% rename from plugins/php-imap/Exceptions/InvalidMessageDateException.php rename to plugins/php-imap/src/Exceptions/InvalidMessageDateException.php diff --git a/plugins/php-imap/Exceptions/InvalidWhereQueryCriteriaException.php b/plugins/php-imap/src/Exceptions/InvalidWhereQueryCriteriaException.php similarity index 100% rename from plugins/php-imap/Exceptions/InvalidWhereQueryCriteriaException.php rename to plugins/php-imap/src/Exceptions/InvalidWhereQueryCriteriaException.php diff --git a/plugins/php-imap/Exceptions/MaskNotFoundException.php b/plugins/php-imap/src/Exceptions/MaskNotFoundException.php similarity index 100% rename from plugins/php-imap/Exceptions/MaskNotFoundException.php rename to plugins/php-imap/src/Exceptions/MaskNotFoundException.php diff --git a/plugins/php-imap/Exceptions/MessageContentFetchingException.php b/plugins/php-imap/src/Exceptions/MessageContentFetchingException.php similarity index 100% rename from plugins/php-imap/Exceptions/MessageContentFetchingException.php rename to plugins/php-imap/src/Exceptions/MessageContentFetchingException.php diff --git a/plugins/php-imap/Exceptions/MessageFlagException.php b/plugins/php-imap/src/Exceptions/MessageFlagException.php similarity index 100% rename from plugins/php-imap/Exceptions/MessageFlagException.php rename to plugins/php-imap/src/Exceptions/MessageFlagException.php diff --git a/plugins/php-imap/Exceptions/MessageHeaderFetchingException.php b/plugins/php-imap/src/Exceptions/MessageHeaderFetchingException.php similarity index 100% rename from plugins/php-imap/Exceptions/MessageHeaderFetchingException.php rename to plugins/php-imap/src/Exceptions/MessageHeaderFetchingException.php diff --git a/plugins/php-imap/Exceptions/MessageNotFoundException.php b/plugins/php-imap/src/Exceptions/MessageNotFoundException.php similarity index 100% rename from plugins/php-imap/Exceptions/MessageNotFoundException.php rename to plugins/php-imap/src/Exceptions/MessageNotFoundException.php diff --git a/plugins/php-imap/Exceptions/MessageSearchValidationException.php b/plugins/php-imap/src/Exceptions/MessageSearchValidationException.php similarity index 100% rename from plugins/php-imap/Exceptions/MessageSearchValidationException.php rename to plugins/php-imap/src/Exceptions/MessageSearchValidationException.php diff --git a/plugins/php-imap/Exceptions/MessageSizeFetchingException.php b/plugins/php-imap/src/Exceptions/MessageSizeFetchingException.php similarity index 100% rename from plugins/php-imap/Exceptions/MessageSizeFetchingException.php rename to plugins/php-imap/src/Exceptions/MessageSizeFetchingException.php diff --git a/plugins/php-imap/Exceptions/MethodNotFoundException.php b/plugins/php-imap/src/Exceptions/MethodNotFoundException.php similarity index 100% rename from plugins/php-imap/Exceptions/MethodNotFoundException.php rename to plugins/php-imap/src/Exceptions/MethodNotFoundException.php diff --git a/plugins/php-imap/Exceptions/MethodNotSupportedException.php b/plugins/php-imap/src/Exceptions/MethodNotSupportedException.php similarity index 100% rename from plugins/php-imap/Exceptions/MethodNotSupportedException.php rename to plugins/php-imap/src/Exceptions/MethodNotSupportedException.php diff --git a/plugins/php-imap/Exceptions/NotSupportedCapabilityException.php b/plugins/php-imap/src/Exceptions/NotSupportedCapabilityException.php similarity index 100% rename from plugins/php-imap/Exceptions/NotSupportedCapabilityException.php rename to plugins/php-imap/src/Exceptions/NotSupportedCapabilityException.php diff --git a/plugins/php-imap/Exceptions/ProtocolNotSupportedException.php b/plugins/php-imap/src/Exceptions/ProtocolNotSupportedException.php similarity index 100% rename from plugins/php-imap/Exceptions/ProtocolNotSupportedException.php rename to plugins/php-imap/src/Exceptions/ProtocolNotSupportedException.php diff --git a/plugins/php-imap/Exceptions/ResponseException.php b/plugins/php-imap/src/Exceptions/ResponseException.php similarity index 100% rename from plugins/php-imap/Exceptions/ResponseException.php rename to plugins/php-imap/src/Exceptions/ResponseException.php diff --git a/plugins/php-imap/Exceptions/RuntimeException.php b/plugins/php-imap/src/Exceptions/RuntimeException.php similarity index 100% rename from plugins/php-imap/Exceptions/RuntimeException.php rename to plugins/php-imap/src/Exceptions/RuntimeException.php diff --git a/plugins/php-imap/Folder.php b/plugins/php-imap/src/Folder.php similarity index 87% rename from plugins/php-imap/Folder.php rename to plugins/php-imap/src/Folder.php index c9ca395d..8c9abd61 100755 --- a/plugins/php-imap/Folder.php +++ b/plugins/php-imap/src/Folder.php @@ -121,6 +121,22 @@ class Folder { /** @var array */ public array $status; + /** @var array */ + public array $attributes = []; + + + const SPECIAL_ATTRIBUTES = [ + 'haschildren' => ['\haschildren'], + 'hasnochildren' => ['\hasnochildren'], + 'template' => ['\template', '\templates'], + 'inbox' => ['\inbox'], + 'sent' => ['\sent'], + 'drafts' => ['\draft', '\drafts'], + 'archive' => ['\archive', '\archives'], + 'trash' => ['\trash'], + 'junk' => ['\junk', '\spam'], + ]; + /** * Folder constructor. * @param Client $client @@ -235,8 +251,8 @@ class Folder { */ protected function decodeName($name): string|array|bool|null { $parts = []; - foreach (explode($this->delimiter, $name) as $item) { - $parts[] = EncodingAliases::convert($item, "UTF7-IMAP", "UTF-8"); + foreach(explode($this->delimiter, $name) as $item) { + $parts[] = EncodingAliases::convert($item, "UTF7-IMAP"); } return implode($this->delimiter, $parts); @@ -264,6 +280,14 @@ class Folder { $this->marked = in_array('\Marked', $attributes); $this->referral = in_array('\Referral', $attributes); $this->has_children = in_array('\HasChildren', $attributes); + + array_map(function($el) { + foreach(self::SPECIAL_ATTRIBUTES as $key => $attribute) { + if(in_array(strtolower($el), $attribute)){ + $this->attributes[] = $key; + } + } + }, $attributes); } /** @@ -284,7 +308,7 @@ class Folder { public function move(string $new_name, bool $expunge = true): array { $this->client->checkConnection(); $status = $this->client->getConnection()->renameFolder($this->full_name, $new_name)->validatedData(); - if ($expunge) $this->client->expunge(); + if($expunge) $this->client->expunge(); $folder = $this->client->getFolder($new_name); $event = $this->getEvent("folder", "moved"); @@ -310,7 +334,7 @@ class Folder { public function overview(string $sequence = null): array { $this->client->openFolder($this->path); $sequence = $sequence === null ? "1:*" : $sequence; - $uid = ClientManager::get('options.sequence', IMAP::ST_MSGN); + $uid = $this->client->getConfig()->get('options.sequence', IMAP::ST_MSGN); $response = $this->client->getConnection()->overview($sequence, $uid); return $response->validatedData(); } @@ -336,7 +360,7 @@ class Folder { * date string that conforms to the rfc2060 specifications for a date_time value or be a Carbon object. */ - if ($internal_date instanceof Carbon) { + if($internal_date instanceof Carbon){ $internal_date = $internal_date->format('d-M-Y H:i:s O'); } @@ -377,11 +401,11 @@ class Folder { */ public function delete(bool $expunge = true): array { $status = $this->client->getConnection()->deleteFolder($this->path)->validatedData(); - if ($this->client->getActiveFolder() == $this->path){ - $this->client->setActiveFolder(null); + if($this->client->getActiveFolder() == $this->path){ + $this->client->setActiveFolder(); } - if ($expunge) $this->client->expunge(); + if($expunge) $this->client->expunge(); $event = $this->getEvent("folder", "deleted"); $event::dispatch($this); @@ -437,7 +461,7 @@ class Folder { public function idle(callable $callback, int $timeout = 300): void { $this->client->setTimeout($timeout); - if (!in_array("IDLE", $this->client->getConnection()->getCapabilities()->validatedData())) { + if(!in_array("IDLE", $this->client->getConnection()->getCapabilities()->validatedData())){ throw new Exceptions\NotSupportedCapabilityException("IMAP server does not support IDLE"); } @@ -448,17 +472,17 @@ class Folder { $last_action = Carbon::now()->addSeconds($timeout); - $sequence = ClientManager::get('options.sequence', IMAP::ST_MSGN); + $sequence = $this->client->getConfig()->get('options.sequence', IMAP::ST_MSGN); - while (true) { + while(true) { // This polymorphic call is fine - Protocol::idle() will throw an exception beforehand $line = $idle_client->getConnection()->nextLine(Response::empty()); - if (($pos = strpos($line, "EXISTS")) !== false) { + if(($pos = strpos($line, "EXISTS")) !== false){ $msgn = (int)substr($line, 2, $pos - 2); // Check if the stream is still alive or should be considered stale - if (!$this->client->isConnected() || $last_action->isBefore(Carbon::now())) { + if(!$this->client->isConnected() || $last_action->isBefore(Carbon::now())){ // Reset the connection before interacting with it. Otherwise, the resource might be stale which // would result in a stuck interaction. If you know of a way of detecting a stale resource, please // feel free to improve this logic. I tried a lot but nothing seem to work reliably... @@ -492,7 +516,7 @@ class Folder { } /** - * Get folder status information + * Get folder status information from the EXAMINE command * * @return array * @throws ConnectionFailedException @@ -502,20 +526,39 @@ class Folder { * @throws AuthFailedException * @throws ResponseException */ - public function getStatus(): array { - return $this->examine(); + public function status(): array { + return $this->client->getConnection()->folderStatus($this->path)->validatedData(); } /** + * Get folder status information from the EXAMINE command + * + * @return array + * @throws AuthFailedException * @throws ConnectionFailedException * @throws ImapBadRequestException * @throws ImapServerErrorException - * @throws RuntimeException - * @throws AuthFailedException * @throws ResponseException + * @throws RuntimeException + * + * @deprecated Use Folder::status() instead + */ + public function getStatus(): array { + return $this->status(); + } + + /** + * Load folder status information from the EXAMINE command + * @return Folder + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws ResponseException + * @throws RuntimeException */ public function loadStatus(): Folder { - $this->status = $this->getStatus(); + $this->status = $this->examine(); return $this; } @@ -563,8 +606,8 @@ class Folder { * @param $delimiter */ public function setDelimiter($delimiter): void { - if (in_array($delimiter, [null, '', ' ', false]) === true) { - $delimiter = ClientManager::get('options.delimiter', '/'); + if(in_array($delimiter, [null, '', ' ', false]) === true){ + $delimiter = $this->client->getConfig()->get('options.delimiter', '/'); } $this->delimiter = $delimiter; diff --git a/plugins/php-imap/Header.php b/plugins/php-imap/src/Header.php similarity index 93% rename from plugins/php-imap/Header.php rename to plugins/php-imap/src/Header.php index 9c8ae046..539bd05e 100644 --- a/plugins/php-imap/Header.php +++ b/plugins/php-imap/src/Header.php @@ -41,9 +41,16 @@ class Header { /** * Config holder * - * @var array $config + * @var Config $config */ - protected array $config = []; + protected Config $config; + + /** + * Config holder + * + * @var array $options + */ + protected array $options = []; /** * Fallback Encoding @@ -54,13 +61,15 @@ class Header { /** * Header constructor. + * @param Config $config * @param string $raw_header * * @throws InvalidMessageDateException */ - public function __construct(string $raw_header) { + public function __construct(string $raw_header, Config $config) { $this->raw = $raw_header; - $this->config = ClientManager::get('options'); + $this->config = $config; + $this->options = $this->config->get('options'); $this->parse(); } @@ -162,7 +171,7 @@ class Header { * @return string|null */ public function getBoundary(): ?string { - $regex = $this->config["boundary"] ?? "/boundary=(.*?(?=;)|(.*))/i"; + $regex = $this->options["boundary"] ?? "/boundary=(.*?(?=;)|(.*))/i"; $boundary = $this->find($regex); if ($boundary === null) { @@ -196,7 +205,7 @@ class Header { $this->set("subject", $this->decode($header->subject)); } if (property_exists($header, 'references')) { - $this->set("references", array_map(function ($item) { + $this->set("references", array_map(function($item) { return str_replace(['<', '>'], '', $item); }, explode(" ", $header->references))); } @@ -229,7 +238,7 @@ class Header { public function rfc822_parse_headers($raw_headers): object { $headers = []; $imap_headers = []; - if (extension_loaded('imap') && $this->config["rfc822"]) { + if (extension_loaded('imap') && $this->options["rfc822"]) { $raw_imap_headers = (array)\imap_rfc822_parse_headers($raw_headers); foreach ($raw_imap_headers as $key => $values) { $key = strtolower(str_replace("-", "_", $key)); @@ -418,7 +427,7 @@ class Header { return $this->decodeArray($value); } $original_value = $value; - $decoder = $this->config['decoder']['message']; + $decoder = $this->options['decoder']['message']; if ($value !== null) { if ($decoder === 'utf-8') { @@ -431,14 +440,14 @@ class Header { $value = $tempValue; } else if (extension_loaded('imap')) { $value = \imap_utf8($value); - }else if (function_exists('iconv_mime_decode')){ + } else if (function_exists('iconv_mime_decode')) { $value = iconv_mime_decode($value, ICONV_MIME_DECODE_CONTINUE_ON_ERROR, "UTF-8"); - }else{ + } else { $value = mb_decode_mimeheader($value); } - }elseif ($decoder === 'iconv') { + } elseif ($decoder === 'iconv') { $value = iconv_mime_decode($value, ICONV_MIME_DECODE_CONTINUE_ON_ERROR, "UTF-8"); - }else if ($this->is_uft8($value)) { + } else if ($this->is_uft8($value)) { $value = mb_decode_mimeheader($value); } @@ -490,7 +499,7 @@ class Header { private function decodeAddresses($values): array { $addresses = []; - if (extension_loaded('mailparse') && $this->config["rfc822"]) { + if (extension_loaded('mailparse') && $this->options["rfc822"]) { foreach ($values as $address) { foreach (\mailparse_rfc822_parse_addresses($address) as $parsed_address) { if (isset($parsed_address['address'])) { @@ -510,7 +519,7 @@ class Header { } foreach ($values as $address) { - foreach (preg_split('/, (?=(?:[^"]*"[^"]*")*[^"]*$)/', $address) as $split_address) { + foreach (preg_split('/, ?(?=(?:[^"]*"[^"]*")*[^"]*$)/', $address) as $split_address) { $split_address = trim(rtrim($split_address)); if (strpos($split_address, ",") == strlen($split_address) - 1) { @@ -722,7 +731,7 @@ class Header { $date = Carbon::createFromFormat("d M Y H:i:s O", trim(implode(',', $array))); break; case preg_match('/([0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ UT)+$/i', $date) > 0: - case preg_match('/([A-Z]{2,3}\,\ [0-9]{1,2}\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ UT)+$/i', $date) > 0: + case preg_match('/([A-Z]{2,3}\,\ [0-9]{1,2}\ [A-Z]{2,3}\ ([0-9]{2}|[0-9]{4})\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ UT)+$/i', $date) > 0: $date .= 'C'; break; case preg_match('/([A-Z]{2,3}\,\ [0-9]{1,2}[\,]\ [A-Z]{2,3}\ [0-9]{4}\ [0-9]{1,2}\:[0-9]{1,2}\:[0-9]{1,2}\ [\-|\+][0-9]{4})+$/i', $date) > 0: @@ -762,10 +771,10 @@ class Header { try { $parsed_date = Carbon::parse($date); } catch (\Exception $_e) { - if (!isset($this->config["fallback_date"])) { + if (!isset($this->options["fallback_date"])) { throw new InvalidMessageDateException("Invalid message date. ID:" . $this->get("message_id") . " Date:" . $header->date . "/" . $date, 1100, $e); } else { - $parsed_date = Carbon::parse($this->config["fallback_date"]); + $parsed_date = Carbon::parse($this->options["fallback_date"]); } } } @@ -800,9 +809,38 @@ class Header { * * @return Header */ - public function setConfig(array $config): Header { + public function setOptions(array $config): Header { + $this->options = $config; + return $this; + } + + /** + * Get the configuration used for parsing a raw header + * + * @return array + */ + public function getOptions(): array { + return $this->options; + } + + /** + * Set the configuration used for parsing a raw header + * @param Config $config + * + * @return Header + */ + public function setConfig(Config $config): Header { $this->config = $config; return $this; } + /** + * Get the configuration used for parsing a raw header + * + * @return Config + */ + public function getConfig(): Config { + return $this->config; + } + } diff --git a/plugins/php-imap/IMAP.php b/plugins/php-imap/src/IMAP.php similarity index 100% rename from plugins/php-imap/IMAP.php rename to plugins/php-imap/src/IMAP.php diff --git a/plugins/php-imap/Message.php b/plugins/php-imap/src/Message.php similarity index 95% rename from plugins/php-imap/Message.php rename to plugins/php-imap/src/Message.php index 09a534f2..10b96018 100755 --- a/plugins/php-imap/Message.php +++ b/plugins/php-imap/src/Message.php @@ -12,6 +12,7 @@ namespace Webklex\PHPIMAP; +use Exception; use ReflectionClass; use ReflectionException; use Webklex\PHPIMAP\Exceptions\AuthFailedException; @@ -87,7 +88,7 @@ class Message { * * @var ?Client */ - private ?Client $client = null; + private ?Client $client; /** * Default mask @@ -97,11 +98,18 @@ class Message { protected string $mask = MessageMask::class; /** - * Used config + * Used options * - * @var array $config + * @var array $options */ - protected array $config = []; + protected array $options = []; + + /** + * All library configs + * + * @var Config $config + */ + protected Config $config; /** * Attribute holder @@ -205,7 +213,7 @@ class Message { * @throws ResponseException */ public function __construct(int $uid, ?int $msglist, Client $client, int $fetch_options = null, bool $fetch_body = false, bool $fetch_flags = false, int $sequence = null) { - $this->boot(); + $this->boot($client->getConfig()); $default_mask = $client->getDefaultMessageMask(); if ($default_mask != null) { @@ -269,7 +277,7 @@ class Message { $reflection = new ReflectionClass(self::class); /** @var Message $instance */ $instance = $reflection->newInstanceWithoutConstructor(); - $instance->boot(); + $instance->boot($client->getConfig()); $default_mask = $client->getDefaultMessageMask(); if ($default_mask != null) { @@ -296,29 +304,8 @@ class Message { /** * Create a new message instance by reading and loading a file or remote location - * - * @throws RuntimeException - * @throws MessageContentFetchingException - * @throws ResponseException - * @throws ImapBadRequestException - * @throws InvalidMessageDateException - * @throws ConnectionFailedException - * @throws ImapServerErrorException - * @throws ReflectionException - * @throws AuthFailedException - * @throws MaskNotFoundException - */ - public static function fromFile($filename): Message { - $blob = file_get_contents($filename); - if ($blob === false) { - throw new RuntimeException("Unable to read file"); - } - return self::fromString($blob); - } - - /** - * Create a new message instance by reading and loading a string - * @param string $blob + * @param string $filename + * @param ?Config $config * * @return Message * @throws AuthFailedException @@ -332,13 +319,38 @@ class Message { * @throws ResponseException * @throws RuntimeException */ - public static function fromString(string $blob): Message { + public static function fromFile(string $filename, Config $config = null): Message { + $blob = file_get_contents($filename); + if ($blob === false) { + throw new RuntimeException("Unable to read file"); + } + return self::fromString($blob, $config); + } + + /** + * Create a new message instance by reading and loading a string + * @param string $blob + * @param ?Config $config + * + * @return Message + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws ReflectionException + * @throws ResponseException + * @throws RuntimeException + */ + public static function fromString(string $blob, Config $config = null): Message { $reflection = new ReflectionClass(self::class); /** @var Message $instance */ $instance = $reflection->newInstanceWithoutConstructor(); - $instance->boot(); + $instance->boot($config); - $default_mask = ClientManager::getMask("message"); + $default_mask = $instance->getConfig()->getMask("message"); if($default_mask != ""){ $instance->setMask($default_mask); }else{ @@ -361,15 +373,18 @@ class Message { /** * Boot a new instance + * @param ?Config $config */ - public function boot(): void { + public function boot(Config $config = null): void { $this->attributes = []; + $this->client = null; + $this->config = $config ?? Config::make(); - $this->config = ClientManager::get('options'); - $this->available_flags = ClientManager::get('flags'); + $this->options = $this->config->get('options'); + $this->available_flags = $this->config->get('flags'); - $this->attachments = AttachmentCollection::make([]); - $this->flags = FlagCollection::make([]); + $this->attachments = AttachmentCollection::make(); + $this->flags = FlagCollection::make(); } /** @@ -543,7 +558,7 @@ class Message { * @throws InvalidMessageDateException */ public function parseRawHeader(string $raw_header): void { - $this->header = new Header($raw_header); + $this->header = new Header($raw_header, $this->getConfig()); } /** @@ -551,7 +566,7 @@ class Message { * @param array $raw_flags */ public function parseRawFlags(array $raw_flags): void { - $this->flags = FlagCollection::make([]); + $this->flags = FlagCollection::make(); foreach ($raw_flags as $flag) { if (str_starts_with($flag, "\\")) { @@ -578,7 +593,7 @@ class Message { */ private function parseFlags(): void { $this->client->openFolder($this->folder_path); - $this->flags = FlagCollection::make([]); + $this->flags = FlagCollection::make(); $sequence_id = $this->getSequenceId(); try { @@ -614,7 +629,7 @@ class Message { try { $contents = $this->client->getConnection()->content([$sequence_id], "RFC822", $this->sequence)->validatedData(); } catch (Exceptions\RuntimeException $e) { - throw new MessageContentFetchingException("failed to fetch content", 0); + throw new MessageContentFetchingException("failed to fetch content", 0, $e); } if (!isset($contents[$sequence_id])) { throw new MessageContentFetchingException("no content found", 0); @@ -786,7 +801,7 @@ class Message { if (is_long($option) === true) { $this->fetch_options = $option; } elseif (is_null($option) === true) { - $config = ClientManager::get('options.fetch', IMAP::FT_UID); + $config = $this->config->get('options.fetch', IMAP::FT_UID); $this->fetch_options = is_long($config) ? $config : 1; } @@ -803,7 +818,7 @@ class Message { if (is_long($sequence)) { $this->sequence = $sequence; } elseif (is_null($sequence)) { - $config = ClientManager::get('options.sequence', IMAP::ST_MSGN); + $config = $this->config->get('options.sequence', IMAP::ST_MSGN); $this->sequence = is_long($config) ? $config : IMAP::ST_MSGN; } @@ -820,7 +835,7 @@ class Message { if (is_bool($option)) { $this->fetch_body = $option; } elseif (is_null($option)) { - $config = ClientManager::get('options.fetch_body', true); + $config = $this->config->get('options.fetch_body', true); $this->fetch_body = is_bool($config) ? $config : true; } @@ -837,7 +852,7 @@ class Message { if (is_bool($option)) { $this->fetch_flags = $option; } elseif (is_null($option)) { - $config = ClientManager::get('options.fetch_flags', true); + $config = $this->config->get('options.fetch_flags', true); $this->fetch_flags = is_bool($config) ? $config : true; } @@ -905,7 +920,7 @@ class Message { if (function_exists('iconv') && !EncodingAliases::isUtf7($from) && !EncodingAliases::isUtf7($to)) { try { return iconv($from, $to.'//IGNORE', $str); - } catch (\Exception $e) { + } catch (Exception) { return @iconv($from, $to, $str); } } else { @@ -971,9 +986,9 @@ class Message { * @throws ResponseException */ public function thread(Folder $sent_folder = null, MessageCollection &$thread = null, Folder $folder = null): MessageCollection { - $thread = $thread ?: MessageCollection::make([]); + $thread = $thread ?: MessageCollection::make(); $folder = $folder ?: $this->getFolder(); - $sent_folder = $sent_folder ?: $this->client->getFolderByPath(ClientManager::get("options.common_folders.sent", "INBOX/Sent")); + $sent_folder = $sent_folder ?: $this->client->getFolderByPath($this->config->get("options.common_folders.sent", "INBOX/Sent")); /** @var Message $message */ foreach ($thread as $message) { @@ -1547,11 +1562,11 @@ class Message { /** * Set the config - * @param array $config + * @param Config $config * * @return Message */ - public function setConfig(array $config): Message { + public function setConfig(Config $config): Message { $this->config = $config; return $this; @@ -1560,10 +1575,31 @@ class Message { /** * Get the config * + * @return Config + */ + public function getConfig(): Config { + return $this->config; + } + + /** + * Set the options + * @param array $options + * + * @return Message + */ + public function setOptions(array $options): Message { + $this->options = $options; + + return $this; + } + + /** + * Get the options + * * @return array */ - public function getConfig(): array { - return $this->config; + public function getOptions(): array { + return $this->options; } /** diff --git a/plugins/php-imap/Part.php b/plugins/php-imap/src/Part.php similarity index 93% rename from plugins/php-imap/Part.php rename to plugins/php-imap/src/Part.php index 1759b8de..1dbf9439 100644 --- a/plugins/php-imap/Part.php +++ b/plugins/php-imap/src/Part.php @@ -139,16 +139,23 @@ class Part { */ private ?Header $header; + /** + * @var Config $config + */ + protected Config $config; + /** * Part constructor. - * @param $raw_part + * @param string $raw_part + * @param Config $config * @param Header|null $header * @param integer $part_number * * @throws InvalidMessageDateException */ - public function __construct($raw_part, Header $header = null, int $part_number = 0) { + public function __construct(string $raw_part, Config $config, Header $header = null, int $part_number = 0) { $this->raw = $raw_part; + $this->config = $config; $this->header = $header; $this->part_number = $part_number; $this->parse(); @@ -211,7 +218,7 @@ class Part { $headers = substr($this->raw, 0, strlen($body) * -1); $body = substr($body, 0, -2); - $this->header = new Header($headers); + $this->header = new Header($headers, $this->config); return $body; } @@ -282,7 +289,7 @@ class Part { * @return bool */ public function isAttachment(): bool { - $valid_disposition = in_array(strtolower($this->disposition ?? ''), ClientManager::get('options.dispositions')); + $valid_disposition = in_array(strtolower($this->disposition ?? ''), $this->config->get('options.dispositions')); if ($this->type == IMAP::MESSAGE_TYPE_TEXT && ($this->ifdisposition == 0 || empty($this->disposition) || !$valid_disposition)) { if (($this->subtype == null || in_array((strtolower($this->subtype)), ["plain", "html"])) && $this->filename == null && $this->name == null) { @@ -305,4 +312,13 @@ class Part { return $this->header; } + /** + * Get the Config instance + * + * @return Config + */ + public function getConfig(): Config { + return $this->config; + } + } diff --git a/plugins/php-imap/Query/Query.php b/plugins/php-imap/src/Query/Query.php similarity index 91% rename from plugins/php-imap/Query/Query.php rename to plugins/php-imap/src/Query/Query.php index 727e641f..cade729d 100644 --- a/plugins/php-imap/Query/Query.php +++ b/plugins/php-imap/src/Query/Query.php @@ -18,7 +18,6 @@ use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Support\Collection; use ReflectionException; use Webklex\PHPIMAP\Client; -use Webklex\PHPIMAP\ClientManager; use Webklex\PHPIMAP\Exceptions\AuthFailedException; use Webklex\PHPIMAP\Exceptions\ConnectionFailedException; use Webklex\PHPIMAP\Exceptions\EventNotFoundException; @@ -93,18 +92,19 @@ class Query { */ public function __construct(Client $client, array $extensions = []) { $this->setClient($client); + $config = $this->client->getConfig(); - $this->sequence = ClientManager::get('options.sequence', IMAP::ST_MSGN); - if (ClientManager::get('options.fetch') === IMAP::FT_PEEK) $this->leaveUnread(); + $this->sequence = $config->get('options.sequence', IMAP::ST_MSGN); + if ($config->get('options.fetch') === IMAP::FT_PEEK) $this->leaveUnread(); - if (ClientManager::get('options.fetch_order') === 'desc') { + if ($config->get('options.fetch_order') === 'desc') { $this->fetch_order = 'desc'; } else { $this->fetch_order = 'asc'; } - $this->date_format = ClientManager::get('date_format', 'd M y'); - $this->soft_fail = ClientManager::get('options.soft_fail', false); + $this->date_format = $config->get('date_format', 'd M y'); + $this->soft_fail = $config->get('options.soft_fail', false); $this->setExtensions($extensions); $this->query = new Collection(); @@ -235,6 +235,7 @@ class Query { $uids = $available_messages->forPage($this->page, $this->limit)->toArray(); $extensions = $this->getExtensions(); if (empty($extensions) === false && method_exists($this->client->getConnection(), "fetch")) { + // this polymorphic call is fine - the method exists at this point $extensions = $this->client->getConnection()->fetch($extensions, $uids, null, $this->sequence)->validatedData(); } $flags = $this->client->getConnection()->flags($uids, $this->sequence)->validatedData(); @@ -314,7 +315,7 @@ class Query { if ($available_messages->count() > 0) { return $this->populate($available_messages); } - return MessageCollection::make([]); + return MessageCollection::make(); } catch (Exception $e) { throw new GetMessagesFailedException($e->getMessage(), 0, $e); } @@ -336,11 +337,12 @@ class Query { * @throws ResponseException */ protected function populate(Collection $available_messages): MessageCollection { - $messages = MessageCollection::make([]); + $messages = MessageCollection::make(); + $config = $this->client->getConfig(); $messages->total($available_messages->count()); - $message_key = ClientManager::get('options.message_key'); + $message_key = $config->get('options.message_key'); $raw_messages = $this->fetch($available_messages); @@ -395,8 +397,14 @@ class Query { * @throws ResponseException */ public function chunked(callable $callback, int $chunk_size = 10, int $start_chunk = 1): void { + $start_chunk = max($start_chunk,1); + $chunk_size = max($chunk_size,1); + $skipped_messages_count = $chunk_size * ($start_chunk-1); + $available_messages = $this->search(); - if (($available_messages_count = $available_messages->count()) > 0) { + $available_messages_count = max($available_messages->count() - $skipped_messages_count,0); + + if ($available_messages_count > 0) { $old_limit = $this->limit; $old_page = $this->page; @@ -640,7 +648,7 @@ class Query { * * @return $this */ - public function leaveUnread(): Query { + public function leaveUnread(): static { $this->setFetchOptions(IMAP::FT_PEEK); return $this; @@ -651,7 +659,7 @@ class Query { * * @return $this */ - public function markAsRead(): Query { + public function markAsRead(): static { $this->setFetchOptions(IMAP::FT_UID); return $this; @@ -663,7 +671,7 @@ class Query { * * @return $this */ - public function setSequence(int $sequence): Query { + public function setSequence(int $sequence): static { $this->sequence = $sequence; return $this; @@ -699,7 +707,7 @@ class Query { * * @return $this */ - public function limit(int $limit, int $page = 1): Query { + public function limit(int $limit, int $page = 1): static { if ($page >= 1) $this->page = $page; $this->limit = $limit; @@ -719,9 +727,9 @@ class Query { * Set all query parameters * @param array $query * - * @return Query + * @return $this */ - public function setQuery(array $query): Query { + public function setQuery(array $query): static { $this->query = new Collection($query); return $this; } @@ -739,9 +747,9 @@ class Query { * Set the raw query * @param string $raw_query * - * @return Query + * @return $this */ - public function setRawQuery(string $raw_query): Query { + public function setRawQuery(string $raw_query): static { $this->raw_query = $raw_query; return $this; } @@ -759,9 +767,9 @@ class Query { * Set all extensions that should be used * @param string[] $extensions * - * @return Query + * @return $this */ - public function setExtensions(array $extensions): Query { + public function setExtensions(array $extensions): static { $this->extensions = $extensions; if (count($this->extensions) > 0) { if (in_array("UID", $this->extensions) === false) { @@ -775,9 +783,9 @@ class Query { * Set the client instance * @param Client $client * - * @return Query + * @return $this */ - public function setClient(Client $client): Query { + public function setClient(Client $client): static { $this->client = $client; return $this; } @@ -795,9 +803,9 @@ class Query { * Set the fetch limit * @param int $limit * - * @return Query + * @return $this */ - public function setLimit(int $limit): Query { + public function setLimit(int $limit): static { $this->limit = $limit <= 0 ? null : $limit; return $this; } @@ -815,9 +823,9 @@ class Query { * Set the page * @param int $page * - * @return Query + * @return $this */ - public function setPage(int $page): Query { + public function setPage(int $page): static { $this->page = $page; return $this; } @@ -826,9 +834,9 @@ class Query { * Set the fetch option flag * @param int $fetch_options * - * @return Query + * @return $this */ - public function setFetchOptions(int $fetch_options): Query { + public function setFetchOptions(int $fetch_options): static { $this->fetch_options = $fetch_options; return $this; } @@ -837,9 +845,9 @@ class Query { * Set the fetch option flag * @param int $fetch_options * - * @return Query + * @return $this */ - public function fetchOptions(int $fetch_options): Query { + public function fetchOptions(int $fetch_options): static { return $this->setFetchOptions($fetch_options); } @@ -865,9 +873,9 @@ class Query { * Set the fetch body flag * @param boolean $fetch_body * - * @return Query + * @return $this */ - public function setFetchBody(bool $fetch_body): Query { + public function setFetchBody(bool $fetch_body): static { $this->fetch_body = $fetch_body; return $this; } @@ -876,9 +884,9 @@ class Query { * Set the fetch body flag * @param boolean $fetch_body * - * @return Query + * @return $this */ - public function fetchBody(bool $fetch_body): Query { + public function fetchBody(bool $fetch_body): static { return $this->setFetchBody($fetch_body); } @@ -895,9 +903,9 @@ class Query { * Set the fetch flag * @param bool $fetch_flags * - * @return Query + * @return $this */ - public function setFetchFlags(bool $fetch_flags): Query { + public function setFetchFlags(bool $fetch_flags): static { $this->fetch_flags = $fetch_flags; return $this; } @@ -906,9 +914,9 @@ class Query { * Set the fetch order * @param string $fetch_order * - * @return Query + * @return $this */ - public function setFetchOrder(string $fetch_order): Query { + public function setFetchOrder(string $fetch_order): static { $fetch_order = strtolower($fetch_order); if (in_array($fetch_order, ['asc', 'desc'])) { @@ -922,9 +930,9 @@ class Query { * Set the fetch order * @param string $fetch_order * - * @return Query + * @return $this */ - public function fetchOrder(string $fetch_order): Query { + public function fetchOrder(string $fetch_order): static { return $this->setFetchOrder($fetch_order); } @@ -940,36 +948,36 @@ class Query { /** * Set the fetch order to ascending * - * @return Query + * @return $this */ - public function setFetchOrderAsc(): Query { + public function setFetchOrderAsc(): static { return $this->setFetchOrder('asc'); } /** * Set the fetch order to ascending * - * @return Query + * @return $this */ - public function fetchOrderAsc(): Query { + public function fetchOrderAsc(): static { return $this->setFetchOrderAsc(); } /** * Set the fetch order to descending * - * @return Query + * @return $this */ - public function setFetchOrderDesc(): Query { + public function setFetchOrderDesc(): static { return $this->setFetchOrder('desc'); } /** * Set the fetch order to descending * - * @return Query + * @return $this */ - public function fetchOrderDesc(): Query { + public function fetchOrderDesc(): static { return $this->setFetchOrderDesc(); } @@ -977,9 +985,9 @@ class Query { * Set soft fail mode * @var boolean $state * - * @return Query + * @return $this */ - public function softFail(bool $state = true): Query { + public function softFail(bool $state = true): static { return $this->setSoftFail($state); } @@ -987,9 +995,9 @@ class Query { * Set soft fail mode * * @var boolean $state - * @return Query + * @return $this */ - public function setSoftFail(bool $state = true): Query { + public function setSoftFail(bool $state = true): static { $this->soft_fail = $state; return $this; diff --git a/plugins/php-imap/Query/WhereQuery.php b/plugins/php-imap/src/Query/WhereQuery.php similarity index 80% rename from plugins/php-imap/Query/WhereQuery.php rename to plugins/php-imap/src/Query/WhereQuery.php index b218e545..e76cbe85 100755 --- a/plugins/php-imap/Query/WhereQuery.php +++ b/plugins/php-imap/src/Query/WhereQuery.php @@ -132,7 +132,7 @@ class WhereQuery extends Query { * $query->where(["FROM" => "someone@email.tld", "SEEN"]); * $query->where("FROM", "someone@email.tld")->where("SEEN"); */ - public function where(mixed $criteria, mixed $value = null): WhereQuery { + public function where(mixed $criteria, mixed $value = null): static { if (is_array($criteria)) { foreach ($criteria as $key => $value) { if (is_numeric($key)) { @@ -155,7 +155,7 @@ class WhereQuery extends Query { * * @throws InvalidWhereQueryCriteriaException */ - protected function push_search_criteria(string $criteria, mixed $value){ + protected function push_search_criteria(string $criteria, mixed $value): void { $criteria = $this->validate_criteria($criteria); $value = $this->parse_value($value); @@ -171,7 +171,7 @@ class WhereQuery extends Query { * * @return $this */ - public function orWhere(Closure $closure = null): WhereQuery { + public function orWhere(Closure $closure = null): static { $this->query->push(['OR']); if ($closure !== null) $closure($this); @@ -183,7 +183,7 @@ class WhereQuery extends Query { * * @return $this */ - public function andWhere(Closure $closure = null): WhereQuery { + public function andWhere(Closure $closure = null): static { $this->query->push(['AND']); if ($closure !== null) $closure($this); @@ -191,38 +191,38 @@ class WhereQuery extends Query { } /** - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereAll(): WhereQuery { + public function whereAll(): static { return $this->where('ALL'); } /** - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereAnswered(): WhereQuery { + public function whereAnswered(): static { return $this->where('ANSWERED'); } /** * @param string $value * - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereBcc(string $value): WhereQuery { + public function whereBcc(string $value): static { return $this->where('BCC', $value); } /** * @param mixed $value - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException * @throws MessageSearchValidationException */ - public function whereBefore(mixed $value): WhereQuery { + public function whereBefore(mixed $value): static { $date = $this->parse_date($value); return $this->where('BEFORE', $date); } @@ -230,121 +230,121 @@ class WhereQuery extends Query { /** * @param string $value * - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereBody(string $value): WhereQuery { + public function whereBody(string $value): static { return $this->where('BODY', $value); } /** * @param string $value * - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereCc(string $value): WhereQuery { + public function whereCc(string $value): static { return $this->where('CC', $value); } /** - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereDeleted(): WhereQuery { + public function whereDeleted(): static { return $this->where('DELETED'); } /** * @param string $value * - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereFlagged(string $value): WhereQuery { + public function whereFlagged(string $value): static { return $this->where('FLAGGED', $value); } /** * @param string $value * - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereFrom(string $value): WhereQuery { + public function whereFrom(string $value): static { return $this->where('FROM', $value); } /** * @param string $value * - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereKeyword(string $value): WhereQuery { + public function whereKeyword(string $value): static { return $this->where('KEYWORD', $value); } /** - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereNew(): WhereQuery { + public function whereNew(): static { return $this->where('NEW'); } /** - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereNot(): WhereQuery { + public function whereNot(): static { return $this->where('NOT'); } /** - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereOld(): WhereQuery { + public function whereOld(): static { return $this->where('OLD'); } /** * @param mixed $value * - * @return WhereQuery + * @return $this * @throws MessageSearchValidationException * @throws InvalidWhereQueryCriteriaException */ - public function whereOn(mixed $value): WhereQuery { + public function whereOn(mixed $value): static { $date = $this->parse_date($value); return $this->where('ON', $date); } /** - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereRecent(): WhereQuery { + public function whereRecent(): static { return $this->where('RECENT'); } /** - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereSeen(): WhereQuery { + public function whereSeen(): static { return $this->where('SEEN'); } /** * @param mixed $value * - * @return WhereQuery + * @return $this * @throws MessageSearchValidationException * @throws InvalidWhereQueryCriteriaException */ - public function whereSince(mixed $value): WhereQuery { + public function whereSince(mixed $value): static { $date = $this->parse_date($value); return $this->where('SINCE', $date); } @@ -352,88 +352,88 @@ class WhereQuery extends Query { /** * @param string $value * - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereSubject(string $value): WhereQuery { + public function whereSubject(string $value): static { return $this->where('SUBJECT', $value); } /** * @param string $value * - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereText(string $value): WhereQuery { + public function whereText(string $value): static { return $this->where('TEXT', $value); } /** * @param string $value * - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereTo(string $value): WhereQuery { + public function whereTo(string $value): static { return $this->where('TO', $value); } /** * @param string $value * - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereUnkeyword(string $value): WhereQuery { + public function whereUnkeyword(string $value): static { return $this->where('UNKEYWORD', $value); } /** - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereUnanswered(): WhereQuery { + public function whereUnanswered(): static { return $this->where('UNANSWERED'); } /** - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereUndeleted(): WhereQuery { + public function whereUndeleted(): static { return $this->where('UNDELETED'); } /** - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereUnflagged(): WhereQuery { + public function whereUnflagged(): static { return $this->where('UNFLAGGED'); } /** - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereUnseen(): WhereQuery { + public function whereUnseen(): static { return $this->where('UNSEEN'); } /** - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereNoXSpam(): WhereQuery { + public function whereNoXSpam(): static { return $this->where("CUSTOM X-Spam-Flag NO"); } /** - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereIsXSpam(): WhereQuery { + public function whereIsXSpam(): static { return $this->where("CUSTOM X-Spam-Flag YES"); } @@ -442,10 +442,10 @@ class WhereQuery extends Query { * @param $header * @param $value * - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereHeader($header, $value): WhereQuery { + public function whereHeader($header, $value): static { return $this->where("CUSTOM HEADER $header $value"); } @@ -453,10 +453,10 @@ class WhereQuery extends Query { * Search for a specific message id * @param $messageId * - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereMessageId($messageId): WhereQuery { + public function whereMessageId($messageId): static { return $this->whereHeader("Message-ID", $messageId); } @@ -464,20 +464,20 @@ class WhereQuery extends Query { * Search for a specific message id * @param $messageId * - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereInReplyTo($messageId): WhereQuery { + public function whereInReplyTo($messageId): static { return $this->whereHeader("In-Reply-To", $messageId); } /** * @param $country_code * - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereLanguage($country_code): WhereQuery { + public function whereLanguage($country_code): static { return $this->where("Content-Language $country_code"); } @@ -486,10 +486,10 @@ class WhereQuery extends Query { * * @param int|string $uid * - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereUid(int|string $uid): WhereQuery { + public function whereUid(int|string $uid): static { return $this->where('UID', $uid); } @@ -498,10 +498,10 @@ class WhereQuery extends Query { * * @param array $uids * - * @return WhereQuery + * @return $this * @throws InvalidWhereQueryCriteriaException */ - public function whereUidIn(array $uids): WhereQuery { + public function whereUidIn(array $uids): static { $uids = implode(',', $uids); return $this->where('UID', $uids); } diff --git a/plugins/php-imap/Structure.php b/plugins/php-imap/src/Structure.php similarity index 86% rename from plugins/php-imap/Structure.php rename to plugins/php-imap/src/Structure.php index 745a6234..4907c542 100644 --- a/plugins/php-imap/Structure.php +++ b/plugins/php-imap/src/Structure.php @@ -50,11 +50,11 @@ class Structure { public array $parts = []; /** - * Config holder + * Options holder * - * @var array $config + * @var array $options */ - protected array $config = []; + protected array $options = []; /** * Structure constructor. @@ -67,7 +67,7 @@ class Structure { public function __construct($raw_structure, Header $header) { $this->raw = $raw_structure; $this->header = $header; - $this->config = ClientManager::get('options'); + $this->options = $header->getConfig()->get('options'); $this->parse(); } @@ -110,12 +110,17 @@ class Structure { $headers = substr($context, 0, strlen($body) * -1); $body = substr($body, 0, -2); - $headers = new Header($headers); + $config = $this->header->getConfig(); + $headers = new Header($headers, $config); if (($boundary = $headers->getBoundary()) !== null) { - return $this->detectParts($boundary, $body, $part_number); + $parts = $this->detectParts($boundary, $body, $part_number); + + if(count($parts) > 1) { + return $parts; + } } - return [new Part($body, $headers, $part_number)]; + return [new Part($body, $this->header->getConfig(), $headers, $part_number)]; } /** @@ -159,6 +164,6 @@ class Structure { return $this->detectParts($boundary, $this->raw); } - return [new Part($this->raw, $this->header)]; + return [new Part($this->raw, $this->header->getConfig(), $this->header)]; } } diff --git a/plugins/php-imap/Support/AttachmentCollection.php b/plugins/php-imap/src/Support/AttachmentCollection.php similarity index 100% rename from plugins/php-imap/Support/AttachmentCollection.php rename to plugins/php-imap/src/Support/AttachmentCollection.php diff --git a/plugins/php-imap/Support/FlagCollection.php b/plugins/php-imap/src/Support/FlagCollection.php similarity index 100% rename from plugins/php-imap/Support/FlagCollection.php rename to plugins/php-imap/src/Support/FlagCollection.php diff --git a/plugins/php-imap/Support/FolderCollection.php b/plugins/php-imap/src/Support/FolderCollection.php similarity index 100% rename from plugins/php-imap/Support/FolderCollection.php rename to plugins/php-imap/src/Support/FolderCollection.php diff --git a/plugins/php-imap/Support/Masks/AttachmentMask.php b/plugins/php-imap/src/Support/Masks/AttachmentMask.php similarity index 97% rename from plugins/php-imap/Support/Masks/AttachmentMask.php rename to plugins/php-imap/src/Support/Masks/AttachmentMask.php index d79b948a..2559c5b9 100644 --- a/plugins/php-imap/Support/Masks/AttachmentMask.php +++ b/plugins/php-imap/src/Support/Masks/AttachmentMask.php @@ -18,6 +18,7 @@ use Webklex\PHPIMAP\Attachment; * Class AttachmentMask * * @package Webklex\PHPIMAP\Support\Masks + * @mixin Attachment */ class AttachmentMask extends Mask { diff --git a/plugins/php-imap/Support/Masks/Mask.php b/plugins/php-imap/src/Support/Masks/Mask.php similarity index 100% rename from plugins/php-imap/Support/Masks/Mask.php rename to plugins/php-imap/src/Support/Masks/Mask.php diff --git a/plugins/php-imap/Support/Masks/MessageMask.php b/plugins/php-imap/src/Support/Masks/MessageMask.php similarity index 99% rename from plugins/php-imap/Support/Masks/MessageMask.php rename to plugins/php-imap/src/Support/Masks/MessageMask.php index 4cc3d5c0..aa3623f9 100644 --- a/plugins/php-imap/Support/Masks/MessageMask.php +++ b/plugins/php-imap/src/Support/Masks/MessageMask.php @@ -19,6 +19,7 @@ use Webklex\PHPIMAP\Message; * Class MessageMask * * @package Webklex\PHPIMAP\Support\Masks + * @mixin Message */ class MessageMask extends Mask { diff --git a/plugins/php-imap/Support/MessageCollection.php b/plugins/php-imap/src/Support/MessageCollection.php similarity index 100% rename from plugins/php-imap/Support/MessageCollection.php rename to plugins/php-imap/src/Support/MessageCollection.php diff --git a/plugins/php-imap/Support/PaginatedCollection.php b/plugins/php-imap/src/Support/PaginatedCollection.php similarity index 100% rename from plugins/php-imap/Support/PaginatedCollection.php rename to plugins/php-imap/src/Support/PaginatedCollection.php diff --git a/plugins/php-imap/Traits/HasEvents.php b/plugins/php-imap/src/Traits/HasEvents.php similarity index 100% rename from plugins/php-imap/Traits/HasEvents.php rename to plugins/php-imap/src/Traits/HasEvents.php diff --git a/plugins/php-imap/config/imap.php b/plugins/php-imap/src/config/imap.php similarity index 100% rename from plugins/php-imap/config/imap.php rename to plugins/php-imap/src/config/imap.php diff --git a/plugins/php-imap/tests/AddressTest.php b/plugins/php-imap/tests/AddressTest.php new file mode 100644 index 00000000..442462fb --- /dev/null +++ b/plugins/php-imap/tests/AddressTest.php @@ -0,0 +1,72 @@ + "Username", + "mailbox" => "info", + "host" => "domain.tld", + "mail" => "info@domain.tld", + "full" => "Username ", + ]; + + /** + * Address test + * + * @return void + */ + public function testAddress(): void { + $address = new Address((object)$this->data); + + self::assertSame("Username", $address->personal); + self::assertSame("info", $address->mailbox); + self::assertSame("domain.tld", $address->host); + self::assertSame("info@domain.tld", $address->mail); + self::assertSame("Username ", $address->full); + } + + /** + * Test Address to string conversion + * + * @return void + */ + public function testAddressToStringConversion(): void { + $address = new Address((object)$this->data); + + self::assertSame("Username ", (string)$address); + } + + /** + * Test Address serialization + * + * @return void + */ + public function testAddressSerialization(): void { + $address = new Address((object)$this->data); + + foreach($address as $key => $value) { + self::assertSame($this->data[$key], $value); + } + + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/AttributeTest.php b/plugins/php-imap/tests/AttributeTest.php new file mode 100644 index 00000000..8374894f --- /dev/null +++ b/plugins/php-imap/tests/AttributeTest.php @@ -0,0 +1,75 @@ +toString()); + self::assertSame("foo", $attribute->getName()); + self::assertSame("foos", $attribute->setName("foos")->getName()); + } + + /** + * Date Attribute test + * + * @return void + */ + public function testDateAttribute(): void { + $attribute = new Attribute("foo", "2022-12-26 08:07:14 GMT-0800"); + + self::assertInstanceOf(Carbon::class, $attribute->toDate()); + self::assertSame("2022-12-26 08:07:14 GMT-0800", $attribute->toDate()->format("Y-m-d H:i:s T")); + } + + /** + * Array Attribute test + * + * @return void + */ + public function testArrayAttribute(): void { + $attribute = new Attribute("foo", ["bar"]); + + self::assertSame("bar", $attribute->toString()); + + $attribute->add("bars"); + self::assertSame(true, $attribute->has(1)); + self::assertSame("bars", $attribute->get(1)); + self::assertSame(true, $attribute->contains("bars")); + self::assertSame("foo, bars", $attribute->set("foo", 0)->toString()); + + $attribute->remove(0); + self::assertSame("bars", $attribute->toString()); + + self::assertSame("bars, foos", $attribute->merge(["foos", "bars"], true)->toString()); + self::assertSame("bars, foos, foos, donk", $attribute->merge(["foos", "donk"], false)->toString()); + + self::assertSame(4, $attribute->count()); + + self::assertSame("donk", $attribute->last()); + self::assertSame("bars", $attribute->first()); + + self::assertSame(["bars", "foos", "foos", "donk"], array_values($attribute->all())); + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/ClientManagerTest.php b/plugins/php-imap/tests/ClientManagerTest.php new file mode 100644 index 00000000..e7910cf1 --- /dev/null +++ b/plugins/php-imap/tests/ClientManagerTest.php @@ -0,0 +1,94 @@ +cm = new ClientManager(); + } + + /** + * Test if the config can be accessed + * + * @return void + */ + public function testConfigAccessorAccount(): void { + $config = $this->cm->getConfig(); + self::assertInstanceOf(Config::class, $config); + self::assertSame("default", $config->get("default")); + self::assertSame("d-M-Y", $config->get("date_format")); + self::assertSame(IMAP::FT_PEEK, $config->get("options.fetch")); + self::assertSame([], $config->get("options.open")); + } + + /** + * Test creating a client instance + * + * @throws MaskNotFoundException + */ + public function testMakeClient(): void { + self::assertInstanceOf(Client::class, $this->cm->make([])); + } + + /** + * Test accessing accounts + * + * @throws MaskNotFoundException + */ + public function testAccountAccessor(): void { + self::assertSame("default", $this->cm->getConfig()->getDefaultAccount()); + self::assertNotEmpty($this->cm->account("default")); + + $this->cm->getConfig()->setDefaultAccount("foo"); + self::assertSame("foo", $this->cm->getConfig()->getDefaultAccount()); + $this->cm->getConfig()->setDefaultAccount("default"); + } + + /** + * Test setting a config + * + * @throws MaskNotFoundException + */ + public function testSetConfig(): void { + $config = [ + "default" => "foo", + "options" => [ + "fetch" => IMAP::ST_MSGN, + "open" => "foo" + ] + ]; + $cm = new ClientManager($config); + + self::assertSame("foo", $cm->getConfig()->getDefaultAccount()); + self::assertInstanceOf(Client::class, $cm->account("foo")); + self::assertSame(IMAP::ST_MSGN, $cm->getConfig()->get("options.fetch")); + self::assertSame(false, is_array($cm->getConfig()->get("options.open"))); + + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/ClientTest.php b/plugins/php-imap/tests/ClientTest.php new file mode 100644 index 00000000..315f3c42 --- /dev/null +++ b/plugins/php-imap/tests/ClientTest.php @@ -0,0 +1,314 @@ + [ + "default" => [ + 'protocol' => 'imap', + 'encryption' => 'ssl', + 'username' => 'foo@domain.tld', + 'password' => 'bar', + 'proxy' => [ + 'socket' => null, + 'request_fulluri' => false, + 'username' => null, + 'password' => null, + ], + ]] + ]); + $this->client = new Client($config); + } + + /** + * Client test + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws RuntimeException + */ + public function testClient(): void { + $this->createNewProtocolMockup(); + + self::assertInstanceOf(ImapProtocol::class, $this->client->getConnection()); + self::assertSame(true, $this->client->isConnected()); + self::assertSame(false, $this->client->checkConnection()); + self::assertSame(30, $this->client->getTimeout()); + self::assertSame(MessageMask::class, $this->client->getDefaultMessageMask()); + self::assertSame(AttachmentMask::class, $this->client->getDefaultAttachmentMask()); + self::assertArrayHasKey("new", $this->client->getDefaultEvents("message")); + } + + public function testClientLogout(): void { + $this->createNewProtocolMockup(); + + $this->protocol->expects($this->any())->method('logout')->willReturn(Response::empty()->setResponse([ + 0 => "BYE Logging out\r\n", + 1 => "OK Logout completed (0.001 + 0.000 secs).\r\n", + ])); + self::assertInstanceOf(Client::class, $this->client->disconnect()); + + } + + public function testClientExpunge(): void { + $this->createNewProtocolMockup(); + $this->protocol->expects($this->any())->method('expunge')->willReturn(Response::empty()->setResponse([ + 0 => "OK", + 1 => "Expunge", + 2 => "completed", + 3 => [ + 0 => "0.001", + 1 => "+", + 2 => "0.000", + 3 => "secs).", + ], + ])); + self::assertNotEmpty($this->client->expunge()); + + } + + public function testClientFolders(): void { + $this->createNewProtocolMockup(); + $this->protocol->expects($this->any())->method('expunge')->willReturn(Response::empty()->setResponse([ + 0 => "OK", + 1 => "Expunge", + 2 => "completed", + 3 => [ + 0 => "0.001", + 1 => "+", + 2 => "0.000", + 3 => "secs).", + ], + ])); + + $this->protocol->expects($this->any())->method('selectFolder')->willReturn(Response::empty()->setResponse([ + "flags" => [ + 0 => [ + 0 => "\Answered", + 1 => "\Flagged", + 2 => "\Deleted", + 3 => "\Seen", + 4 => "\Draft", + 5 => "NonJunk", + 6 => "unknown-1", + ], + ], + "exists" => 139, + "recent" => 0, + "unseen" => 94, + "uidvalidity" => 1488899637, + "uidnext" => 278, + ])); + self::assertNotEmpty($this->client->openFolder("INBOX")); + self::assertSame("INBOX", $this->client->getFolderPath()); + + $this->protocol->expects($this->any())->method('examineFolder')->willReturn(Response::empty()->setResponse([ + "flags" => [ + 0 => [ + 0 => "\Answered", + 1 => "\Flagged", + 2 => "\Deleted", + 3 => "\Seen", + 4 => "\Draft", + 5 => "NonJunk", + 6 => "unknown-1", + ], + ], + "exists" => 139, + "recent" => 0, + "unseen" => 94, + "uidvalidity" => 1488899637, + "uidnext" => 278, + ])); + self::assertNotEmpty($this->client->checkFolder("INBOX")); + + $this->protocol->expects($this->any())->method('folders')->with($this->identicalTo(""), $this->identicalTo("*"))->willReturn(Response::empty()->setResponse([ + "INBOX" => [ + "delimiter" => ".", + "flags" => [ + 0 => "\HasChildren", + ], + ], + "INBOX.new" => [ + "delimiter" => ".", + "flags" => [ + 0 => "\HasNoChildren", + ], + ], + "INBOX.9AL56dEMTTgUKOAz" => [ + "delimiter" => ".", + "flags" => [ + 0 => "\HasNoChildren", + ], + ], + "INBOX.U9PsHCvXxAffYvie" => [ + "delimiter" => ".", + "flags" => [ + 0 => "\HasNoChildren", + ], + ], + "INBOX.Trash" => [ + "delimiter" => ".", + "flags" => [ + 0 => "\HasNoChildren", + 1 => "\Trash", + ], + ], + "INBOX.processing" => [ + "delimiter" => ".", + "flags" => [ + 0 => "\HasNoChildren", + ], + ], + "INBOX.Sent" => [ + "delimiter" => ".", + "flags" => [ + 0 => "\HasNoChildren", + 1 => "\Sent", + ], + ], + "INBOX.OzDWCXKV3t241koc" => [ + "delimiter" => ".", + "flags" => [ + 0 => "\HasNoChildren", + ], + ], + "INBOX.5F3bIVTtBcJEqIVe" => [ + "delimiter" => ".", + "flags" => [ + 0 => "\HasNoChildren", + ], + ], + "INBOX.8J3rll6eOBWnTxIU" => [ + "delimiter" => ".", + "flags" => [ + 0 => "\HasNoChildren", + ], + ], + "INBOX.Junk" => [ + "delimiter" => ".", + "flags" => [ + 0 => "\HasNoChildren", + 1 => "\Junk", + ], + ], + "INBOX.Drafts" => [ + "delimiter" => ".", + "flags" => [ + 0 => "\HasNoChildren", + 1 => "\Drafts", + ], + ], + "INBOX.test" => [ + "delimiter" => ".", + "flags" => [ + 0 => "\HasNoChildren", + ], + ], + ])); + + $this->protocol->expects($this->any())->method('createFolder')->willReturn(Response::empty()->setResponse([ + 0 => "OK Create completed (0.004 + 0.000 + 0.003 secs).\r\n", + ])); + self::assertNotEmpty($this->client->createFolder("INBOX.new")); + + $this->protocol->expects($this->any())->method('deleteFolder')->willReturn(Response::empty()->setResponse([ + 0 => "OK Delete completed (0.007 + 0.000 + 0.006 secs).\r\n", + ])); + self::assertNotEmpty($this->client->deleteFolder("INBOX.new")); + + self::assertInstanceOf(Folder::class, $this->client->getFolderByPath("INBOX.new")); + self::assertInstanceOf(Folder::class, $this->client->getFolderByName("new")); + self::assertInstanceOf(Folder::class, $this->client->getFolder("INBOX.new", ".")); + self::assertInstanceOf(Folder::class, $this->client->getFolder("new")); + } + + public function testClientId(): void { + $this->createNewProtocolMockup(); + $this->protocol->expects($this->any())->method('ID')->willReturn(Response::empty()->setResponse([ + 0 => "ID (\"name\" \"Dovecot\")\r\n", + 1 => "OK ID completed (0.001 + 0.000 secs).\r\n" + + ])); + self::assertSame("ID (\"name\" \"Dovecot\")\r\n", $this->client->Id()[0]); + + } + + public function testClientConfig(): void { + $config = $this->client->getConfig()->get("accounts.".$this->client->getConfig()->getDefaultAccount()); + self::assertSame("foo@domain.tld", $config["username"]); + self::assertSame("bar", $config["password"]); + self::assertSame("localhost", $config["host"]); + self::assertSame(true, $config["validate_cert"]); + self::assertSame(993, $config["port"]); + + $this->client->getConfig()->set("accounts.".$this->client->getConfig()->getDefaultAccount(), [ + "host" => "domain.tld", + 'password' => 'bar', + ]); + $config = $this->client->getConfig()->get("accounts.".$this->client->getConfig()->getDefaultAccount()); + + self::assertSame("bar", $config["password"]); + self::assertSame("domain.tld", $config["host"]); + self::assertSame(true, $config["validate_cert"]); + } + + protected function createNewProtocolMockup() { + $this->protocol = $this->createMock(ImapProtocol::class); + + $this->protocol->expects($this->any())->method('connected')->willReturn(true); + $this->protocol->expects($this->any())->method('getConnectionTimeout')->willReturn(30); + + $this->protocol + ->expects($this->any()) + ->method('createStream') + //->will($this->onConsecutiveCalls(true)); + ->willReturn(true); + + $this->client->connection = $this->protocol; + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/HeaderTest.php b/plugins/php-imap/tests/HeaderTest.php new file mode 100644 index 00000000..65e31d7f --- /dev/null +++ b/plugins/php-imap/tests/HeaderTest.php @@ -0,0 +1,155 @@ +config = Config::make(); + } + + /** + * Test parsing email headers + * + * @throws InvalidMessageDateException + */ + public function testHeaderParsing(): void { + $email = file_get_contents(implode(DIRECTORY_SEPARATOR, [__DIR__, "messages", "1366671050@github.com.eml"])); + if (!str_contains($email, "\r\n")) { + $email = str_replace("\n", "\r\n", $email); + } + + $raw_header = substr($email, 0, strpos($email, "\r\n\r\n")); + + $header = new Header($raw_header, $this->config); + $subject = $header->get("subject"); + $returnPath = $header->get("Return-Path"); + /** @var Carbon $date */ + $date = $header->get("date")->first(); + /** @var Address $from */ + $from = $header->get("from")->first(); + /** @var Address $to */ + $to = $header->get("to")->first(); + + self::assertSame($raw_header, $header->raw); + self::assertInstanceOf(Attribute::class, $subject); + self::assertSame("Re: [Webklex/php-imap] Read all folders? (Issue #349)", $subject->toString()); + self::assertSame("Re: [Webklex/php-imap] Read all folders? (Issue #349)", (string)$header->subject); + self::assertSame("", $returnPath->toString()); + self::assertSame("return_path", $returnPath->getName()); + self::assertSame("-4.299", (string)$header->get("X-Spam-Score")); + self::assertSame("Webklex/php-imap/issues/349/1365266070@github.com", (string)$header->get("Message-ID")); + self::assertSame(6, $header->get("received")->count()); + self::assertSame(IMAP::MESSAGE_PRIORITY_UNKNOWN, (int)$header->get("priority")()); + + self::assertSame("Username", $from->personal); + self::assertSame("notifications", $from->mailbox); + self::assertSame("github.com", $from->host); + self::assertSame("notifications@github.com", $from->mail); + self::assertSame("Username ", $from->full); + + self::assertSame("Webklex/php-imap", $to->personal); + self::assertSame("php-imap", $to->mailbox); + self::assertSame("noreply.github.com", $to->host); + self::assertSame("php-imap@noreply.github.com", $to->mail); + self::assertSame("Webklex/php-imap ", $to->full); + + self::assertInstanceOf(Carbon::class, $date); + self::assertSame("2022-12-26 08:07:14 GMT-0800", $date->format("Y-m-d H:i:s T")); + + self::assertSame(48, count($header->getAttributes())); + } + + public function testRfc822ParseHeaders() { + $mock = $this->getMockBuilder(Header::class) + ->disableOriginalConstructor() + ->onlyMethods([]) + ->getMock(); + + $config = new \ReflectionProperty($mock, 'options'); + $config->setAccessible(true); + $config->setValue($mock, $this->config->get("options")); + + $mockHeader = "Content-Type: text/csv; charset=WINDOWS-1252; name*0=\"TH_Is_a_F ile name example 20221013.c\"; name*1=sv\r\nContent-Transfer-Encoding: quoted-printable\r\nContent-Disposition: attachment; filename*0=\"TH_Is_a_F ile name example 20221013.c\"; filename*1=\"sv\"\r\n"; + + $expected = new \stdClass(); + $expected->content_type = 'text/csv; charset=WINDOWS-1252; name*0="TH_Is_a_F ile name example 20221013.c"; name*1=sv'; + $expected->content_transfer_encoding = 'quoted-printable'; + $expected->content_disposition = 'attachment; filename*0="TH_Is_a_F ile name example 20221013.c"; filename*1="sv"'; + + $this->assertEquals($expected, $mock->rfc822_parse_headers($mockHeader)); + } + + public function testExtractHeaderExtensions() { + $mock = $this->getMockBuilder(Header::class) + ->disableOriginalConstructor() + ->onlyMethods([]) + ->getMock(); + + $method = new \ReflectionMethod($mock, 'extractHeaderExtensions'); + $method->setAccessible(true); + + $mockAttributes = [ + 'content_type' => new Attribute('content_type', 'text/csv; charset=WINDOWS-1252; name*0="TH_Is_a_F ile name example 20221013.c"; name*1=sv'), + 'content_transfer_encoding' => new Attribute('content_transfer_encoding', 'quoted-printable'), + 'content_disposition' => new Attribute('content_disposition', 'attachment; filename*0="TH_Is_a_F ile name example 20221013.c"; filename*1="sv"; attribute_test=attribute_test_value'), + ]; + + $attributes = new \ReflectionProperty($mock, 'attributes'); + $attributes->setAccessible(true); + $attributes->setValue($mock, $mockAttributes); + + $method->invoke($mock); + + $this->assertArrayHasKey('filename', $mock->getAttributes()); + $this->assertArrayNotHasKey('filename*0', $mock->getAttributes()); + $this->assertEquals('TH_Is_a_F ile name example 20221013.csv', $mock->get('filename')); + + $this->assertArrayHasKey('name', $mock->getAttributes()); + $this->assertArrayNotHasKey('name*0', $mock->getAttributes()); + $this->assertEquals('TH_Is_a_F ile name example 20221013.csv', $mock->get('name')); + + $this->assertArrayHasKey('content_type', $mock->getAttributes()); + $this->assertEquals('text/csv', $mock->get('content_type')->last()); + + $this->assertArrayHasKey('charset', $mock->getAttributes()); + $this->assertEquals('WINDOWS-1252', $mock->get('charset')->last()); + + $this->assertArrayHasKey('content_transfer_encoding', $mock->getAttributes()); + $this->assertEquals('quoted-printable', $mock->get('content_transfer_encoding')); + + $this->assertArrayHasKey('content_disposition', $mock->getAttributes()); + $this->assertEquals('attachment', $mock->get('content_disposition')->last()); + $this->assertEquals('quoted-printable', $mock->get('content_transfer_encoding')); + + $this->assertArrayHasKey('attribute_test', $mock->getAttributes()); + $this->assertEquals('attribute_test_value', $mock->get('attribute_test')); + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/ImapProtocolTest.php b/plugins/php-imap/tests/ImapProtocolTest.php new file mode 100644 index 00000000..78716548 --- /dev/null +++ b/plugins/php-imap/tests/ImapProtocolTest.php @@ -0,0 +1,52 @@ +config = Config::make(); + } + + + /** + * ImapProtocol test + * + * @return void + */ + public function testImapProtocol(): void { + + $protocol = new ImapProtocol($this->config, false); + self::assertSame(false, $protocol->getCertValidation()); + self::assertSame("", $protocol->getEncryption()); + + $protocol->setCertValidation(true); + $protocol->setEncryption("ssl"); + + self::assertSame(true, $protocol->getCertValidation()); + self::assertSame("ssl", $protocol->getEncryption()); + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/MessageTest.php b/plugins/php-imap/tests/MessageTest.php new file mode 100644 index 00000000..1bc9ce6f --- /dev/null +++ b/plugins/php-imap/tests/MessageTest.php @@ -0,0 +1,302 @@ + [ + "default" => [ + 'protocol' => 'imap', + 'encryption' => 'ssl', + 'username' => 'foo@domain.tld', + 'password' => 'bar', + 'proxy' => [ + 'socket' => null, + 'request_fulluri' => false, + 'username' => null, + 'password' => null, + ], + ]] + ]); + $this->client = new Client($config); + } + + /** + * Message test + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageNotFoundException + * @throws MessageSizeFetchingException + * @throws ReflectionException + * @throws ResponseException + * @throws RuntimeException + */ + public function testMessage(): void { + $this->createNewProtocolMockup(); + + $email = file_get_contents(implode(DIRECTORY_SEPARATOR, [__DIR__, "messages", "1366671050@github.com.eml"])); + if(!str_contains($email, "\r\n")){ + $email = str_replace("\n", "\r\n", $email); + } + + $raw_header = substr($email, 0, strpos($email, "\r\n\r\n")); + $raw_body = substr($email, strlen($raw_header)+8); + + $this->protocol->expects($this->any())->method('getUid')->willReturn(Response::empty()->setResult(22)); + $this->protocol->expects($this->any())->method('getMessageNumber')->willReturn(Response::empty()->setResult(21)); + $this->protocol->expects($this->any())->method('flags')->willReturn(Response::empty()->setResult([22 => [0 => "\\Seen"]])); + + self::assertNotEmpty($this->client->openFolder("INBOX")); + + $message = Message::make(22, null, $this->client, $raw_header, $raw_body, [0 => "\\Seen"], IMAP::ST_UID); + + self::assertInstanceOf(Client::class, $message->getClient()); + self::assertSame(22, $message->uid); + self::assertSame(21, $message->msgn); + self::assertContains("Seen", $message->flags()->toArray()); + + $subject = $message->get("subject"); + $returnPath = $message->get("Return-Path"); + + self::assertInstanceOf(Attribute::class, $subject); + self::assertSame("Re: [Webklex/php-imap] Read all folders? (Issue #349)", $subject->toString()); + self::assertSame("Re: [Webklex/php-imap] Read all folders? (Issue #349)", (string)$message->subject); + self::assertSame("", $returnPath->toString()); + self::assertSame("return_path", $returnPath->getName()); + self::assertSame("-4.299", (string)$message->get("X-Spam-Score")); + self::assertSame("Webklex/php-imap/issues/349/1365266070@github.com", (string)$message->get("Message-ID")); + self::assertSame(6, $message->get("received")->count()); + self::assertSame(IMAP::MESSAGE_PRIORITY_UNKNOWN, (int)$message->get("priority")()); + } + + /** + * Test getMessageNumber + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetMessageNumber(): void { + $this->createNewProtocolMockup(); + $this->protocol->expects($this->any())->method('getMessageNumber')->willReturn(Response::empty()->setResult("")); + + self::assertNotEmpty($this->client->openFolder("INBOX")); + + try { + $this->client->getConnection()->getMessageNumber(21)->validatedData(); + $this->fail("Message number should not exist"); + } catch (ResponseException $e) { + self::assertTrue(true); + } + + } + + /** + * Test loadMessageFromFile + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageNotFoundException + * @throws ReflectionException + * @throws ResponseException + * @throws RuntimeException + * @throws MessageSizeFetchingException + */ + public function testLoadMessageFromFile(): void { + $filename = implode(DIRECTORY_SEPARATOR, [__DIR__, "messages", "1366671050@github.com.eml"]); + $message = Message::fromFile($filename); + + $subject = $message->get("subject"); + $returnPath = $message->get("Return-Path"); + + self::assertInstanceOf(Attribute::class, $subject); + self::assertSame("Re: [Webklex/php-imap] Read all folders? (Issue #349)", $subject->toString()); + self::assertSame("Re: [Webklex/php-imap] Read all folders? (Issue #349)", (string)$message->subject); + self::assertSame("", $returnPath->toString()); + self::assertSame("return_path", $returnPath->getName()); + self::assertSame("-4.299", (string)$message->get("X-Spam-Score")); + self::assertSame("Webklex/php-imap/issues/349/1365266070@github.com", (string)$message->get("Message-ID")); + self::assertSame(6, $message->get("received")->count()); + self::assertSame(IMAP::MESSAGE_PRIORITY_UNKNOWN, (int)$message->get("priority")()); + + self::assertNull($message->getClient()); + self::assertSame(0, $message->uid); + + $filename = implode(DIRECTORY_SEPARATOR, [__DIR__, "messages", "example_attachment.eml"]); + $message = Message::fromFile($filename); + + $subject = $message->get("subject"); + $returnPath = $message->get("Return-Path"); + + self::assertInstanceOf(Attribute::class, $subject); + self::assertSame("ogqMVHhz7swLaq2PfSWsZj0k99w8wtMbrb4RuHdNg53i76B7icIIM0zIWpwGFtnk", $subject->toString()); + self::assertSame("ogqMVHhz7swLaq2PfSWsZj0k99w8wtMbrb4RuHdNg53i76B7icIIM0zIWpwGFtnk", (string)$message->subject); + self::assertSame("", $returnPath->toString()); + self::assertSame("return_path", $returnPath->getName()); + self::assertSame("1.103", (string)$message->get("X-Spam-Score")); + self::assertSame("d3a5e91963cb805cee975687d5acb1c6@swift.generated", (string)$message->get("Message-ID")); + self::assertSame(5, $message->get("received")->count()); + self::assertSame(IMAP::MESSAGE_PRIORITY_HIGHEST, (int)$message->get("priority")()); + + self::assertNull($message->getClient()); + self::assertSame(0, $message->uid); + self::assertSame(1, $message->getAttachments()->count()); + + /** @var Attachment $attachment */ + $attachment = $message->getAttachments()->first(); + self::assertSame("attachment", $attachment->disposition); + self::assertSame("znk551MP3TP3WPp9Kl1gnLErrWEgkJFAtvaKqkTgrk3dKI8dX38YT8BaVxRcOERN", $attachment->content); + self::assertSame("application/octet-stream", $attachment->content_type); + self::assertSame("6mfFxiU5Yhv9WYJx.txt", $attachment->name); + self::assertSame(2, $attachment->part_number); + self::assertSame("text", $attachment->type); + self::assertNotEmpty($attachment->id); + self::assertSame(90, $attachment->size); + self::assertSame("txt", $attachment->getExtension()); + self::assertInstanceOf(Message::class, $attachment->getMessage()); + self::assertSame("text/plain", $attachment->getMimeType()); + } + + /** + * Test issue #348 + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws ReflectionException + * @throws ResponseException + * @throws RuntimeException + */ + public function testIssue348() { + $filename = implode(DIRECTORY_SEPARATOR, [__DIR__, "messages", "issue-348.eml"]); + $message = Message::fromFile($filename); + + self::assertSame(1, $message->getAttachments()->count()); + + /** @var Attachment $attachment */ + $attachment = $message->getAttachments()->first(); + + self::assertSame("attachment", $attachment->disposition); + self::assertSame("application/pdf", $attachment->content_type); + self::assertSame("Kelvinsong—Font_test_page_bold.pdf", $attachment->name); + self::assertSame(1, $attachment->part_number); + self::assertSame("text", $attachment->type); + self::assertNotEmpty($attachment->id); + self::assertSame(92384, $attachment->size); + self::assertSame("pdf", $attachment->getExtension()); + self::assertInstanceOf(Message::class, $attachment->getMessage()); + self::assertSame("application/pdf", $attachment->getMimeType()); + } + + /** + * Create a new protocol mockup + * + * @return void + */ + protected function createNewProtocolMockup(): void { + $this->protocol = $this->createMock(ImapProtocol::class); + + $this->protocol->expects($this->any())->method('createStream')->willReturn(true); + $this->protocol->expects($this->any())->method('connected')->willReturn(true); + $this->protocol->expects($this->any())->method('getConnectionTimeout')->willReturn(30); + $this->protocol->expects($this->any())->method('logout')->willReturn(Response::empty()->setResponse([ + 0 => "BYE Logging out\r\n", + 1 => "OK Logout completed (0.001 + 0.000 secs).\r\n", + ])); + $this->protocol->expects($this->any())->method('selectFolder')->willReturn(Response::empty()->setResponse([ + "flags" => [ + 0 => [ + 0 => "\Answered", + 1 => "\Flagged", + 2 => "\Deleted", + 3 => "\Seen", + 4 => "\Draft", + 5 => "NonJunk", + 6 => "unknown-1", + ], + ], + "exists" => 139, + "recent" => 0, + "unseen" => 94, + "uidvalidity" => 1488899637, + "uidnext" => 278, + ])); + + $this->client->connection = $this->protocol; + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/PartTest.php b/plugins/php-imap/tests/PartTest.php new file mode 100644 index 00000000..8543c46b --- /dev/null +++ b/plugins/php-imap/tests/PartTest.php @@ -0,0 +1,107 @@ +config = Config::make(); + } + + /** + * Test parsing a text Part + * @throws InvalidMessageDateException + */ + public function testTextPart(): void { + $raw_headers = "Content-Type: text/plain;\r\n charset=UTF-8\r\nContent-Transfer-Encoding: 7bit\r\n"; + $raw_body = "\r\nAny updates?"; + + $headers = new Header($raw_headers, $this->config); + $part = new Part($raw_body, $this->config, $headers, 0); + + self::assertSame("UTF-8", $part->charset); + self::assertSame("text/plain", $part->content_type); + self::assertSame(12, $part->bytes); + self::assertSame(0, $part->part_number); + self::assertSame(false, $part->ifdisposition); + self::assertSame(false, $part->isAttachment()); + self::assertSame("Any updates?", $part->content); + self::assertSame(IMAP::MESSAGE_TYPE_TEXT, $part->type); + self::assertSame(IMAP::MESSAGE_ENC_7BIT, $part->encoding); + } + + /** + * Test parsing a html Part + * @throws InvalidMessageDateException + */ + public function testHTMLPart(): void { + $raw_headers = "Content-Type: text/html;\r\n charset=UTF-8\r\nContent-Transfer-Encoding: 7bit\r\n"; + $raw_body = "\r\n

\r\n

Any updates?

"; + + $headers = new Header($raw_headers, $this->config); + $part = new Part($raw_body, $this->config, $headers, 0); + + self::assertSame("UTF-8", $part->charset); + self::assertSame("text/html", $part->content_type); + self::assertSame(39, $part->bytes); + self::assertSame(0, $part->part_number); + self::assertSame(false, $part->ifdisposition); + self::assertSame(false, $part->isAttachment()); + self::assertSame("

\r\n

Any updates?

", $part->content); + self::assertSame(IMAP::MESSAGE_TYPE_TEXT, $part->type); + self::assertSame(IMAP::MESSAGE_ENC_7BIT, $part->encoding); + } + + /** + * Test parsing a html Part + * @throws InvalidMessageDateException + */ + public function testBase64Part(): void { + $raw_headers = "Content-Type: application/octet-stream; name=6mfFxiU5Yhv9WYJx.txt\r\nContent-Transfer-Encoding: base64\r\nContent-Disposition: attachment; filename=6mfFxiU5Yhv9WYJx.txt\r\n"; + $raw_body = "em5rNTUxTVAzVFAzV1BwOUtsMWduTEVycldFZ2tKRkF0dmFLcWtUZ3JrM2RLSThkWDM4WVQ4QmFW\r\neFJjT0VSTg=="; + + $headers = new Header($raw_headers, $this->config); + $part = new Part($raw_body, $this->config, $headers, 0); + + self::assertSame("", $part->charset); + self::assertSame("application/octet-stream", $part->content_type); + self::assertSame(90, $part->bytes); + self::assertSame(0, $part->part_number); + self::assertSame("znk551MP3TP3WPp9Kl1gnLErrWEgkJFAtvaKqkTgrk3dKI8dX38YT8BaVxRcOERN", base64_decode($part->content)); + self::assertSame(true, $part->ifdisposition); + self::assertSame("attachment", $part->disposition); + self::assertSame("6mfFxiU5Yhv9WYJx.txt", $part->name); + self::assertSame("6mfFxiU5Yhv9WYJx.txt", $part->filename); + self::assertSame(true, $part->isAttachment()); + self::assertSame(IMAP::MESSAGE_TYPE_TEXT, $part->type); + self::assertSame(IMAP::MESSAGE_ENC_BASE64, $part->encoding); + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/StructureTest.php b/plugins/php-imap/tests/StructureTest.php new file mode 100644 index 00000000..22d71bbf --- /dev/null +++ b/plugins/php-imap/tests/StructureTest.php @@ -0,0 +1,68 @@ +config = Config::make(); + } + + /** + * Test parsing email headers + * + * @throws InvalidMessageDateException + * @throws MessageContentFetchingException + */ + public function testStructureParsing(): void { + $email = file_get_contents(implode(DIRECTORY_SEPARATOR, [__DIR__, "messages", "1366671050@github.com.eml"])); + if(!str_contains($email, "\r\n")){ + $email = str_replace("\n", "\r\n", $email); + } + + $raw_header = substr($email, 0, strpos($email, "\r\n\r\n")); + $raw_body = substr($email, strlen($raw_header)+8); + + $header = new Header($raw_header, $this->config); + $structure = new Structure($raw_body, $header); + + self::assertSame(2, count($structure->parts)); + + $textPart = $structure->parts[0]; + + self::assertSame("UTF-8", $textPart->charset); + self::assertSame("text/plain", $textPart->content_type); + self::assertSame(278, $textPart->bytes); + + $htmlPart = $structure->parts[1]; + + self::assertSame("UTF-8", $htmlPart->charset); + self::assertSame("text/html", $htmlPart->content_type); + self::assertSame(1478, $htmlPart->bytes); + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/fixtures/AttachmentEncodedFilenameTest.php b/plugins/php-imap/tests/fixtures/AttachmentEncodedFilenameTest.php new file mode 100644 index 00000000..b5da3597 --- /dev/null +++ b/plugins/php-imap/tests/fixtures/AttachmentEncodedFilenameTest.php @@ -0,0 +1,52 @@ +getFixture("attachment_encoded_filename.eml"); + + self::assertEquals("", $message->subject); + self::assertEquals("multipart/mixed", $message->content_type->last()); + self::assertFalse($message->hasTextBody()); + self::assertFalse($message->hasHTMLBody()); + + self::assertCount(1, $message->attachments()); + + $attachment = $message->attachments()->first(); + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("Prostřeno_2014_poslední volné termíny.xls", $attachment->filename); + self::assertEquals("Prostřeno_2014_poslední volné termíny.xls", $attachment->name); + self::assertEquals('xls', $attachment->getExtension()); + self::assertEquals('text', $attachment->type); + self::assertEquals("application/vnd.ms-excel", $attachment->content_type); + self::assertEquals("a0ef7cfbc05b73dbcb298fe0bc224b41900cdaf60f9904e3fea5ba6c7670013c", hash("sha256", $attachment->content)); + self::assertEquals(146, $attachment->size); + self::assertEquals(0, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/fixtures/AttachmentLongFilenameTest.php b/plugins/php-imap/tests/fixtures/AttachmentLongFilenameTest.php new file mode 100644 index 00000000..650a2dcb --- /dev/null +++ b/plugins/php-imap/tests/fixtures/AttachmentLongFilenameTest.php @@ -0,0 +1,79 @@ +getFixture("attachment_long_filename.eml"); + + self::assertEquals("", $message->subject); + self::assertEquals("multipart/mixed", $message->content_type->last()); + self::assertFalse($message->hasTextBody()); + self::assertFalse($message->hasHTMLBody()); + + $attachments = $message->attachments(); + self::assertCount(3, $attachments); + + $attachment = $attachments[0]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("Buchungsbestätigung- Rechnung-Geschäftsbedingungen-Nr.B123-45 - XXXX xxxxxxxxxxxxxxxxx XxxX, Lüdxxxxxxxx - VM Klaus XXXXXX - xxxxxxxx.pdf", $attachment->name); + self::assertEquals("Buchungsbestätigung- Rechnung-Geschäftsbedingungen-Nr.B123-45 - XXXXX xxxxxxxxxxxxxxxxx XxxX, Lüxxxxxxxxxx - VM Klaus XXXXXX - xxxxxxxx.pdf", $attachment->filename); + self::assertEquals('text', $attachment->type); + self::assertEquals('pdf', $attachment->getExtension()); + self::assertEquals("text/plain", $attachment->content_type); + self::assertEquals("ca51ce1fb15acc6d69b8a5700256172fcc507e02073e6f19592e341bd6508ab8", hash("sha256", $attachment->content)); + self::assertEquals(4, $attachment->size); + self::assertEquals(0, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + + $attachment = $attachments[1]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals('01_A€àäąбيد@Z-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz.txt', $attachment->name); + self::assertEquals("f7b5181985862431bfc443d26e3af2371e20a0afd676eeb9b9595a26d42e0b73", hash("sha256", $attachment->filename)); + self::assertEquals('text', $attachment->type); + self::assertEquals('txt', $attachment->getExtension()); + self::assertEquals("text/plain", $attachment->content_type); + self::assertEquals("ca51ce1fb15acc6d69b8a5700256172fcc507e02073e6f19592e341bd6508ab8", hash("sha256", $attachment->content)); + self::assertEquals(4, $attachment->size); + self::assertEquals(1, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + + $attachment = $attachments[2]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals('02_A€àäąбيد@Z-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz.txt', $attachment->name); + self::assertEquals('02_A€àäąбيد@Z-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstuvz.txt', $attachment->filename); + self::assertEquals('text', $attachment->type); + self::assertEquals("text/plain", $attachment->content_type); + self::assertEquals('txt', $attachment->getExtension()); + self::assertEquals("ca51ce1fb15acc6d69b8a5700256172fcc507e02073e6f19592e341bd6508ab8", hash("sha256", $attachment->content)); + self::assertEquals(4, $attachment->size); + self::assertEquals(2, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/fixtures/AttachmentNoDispositionTest.php b/plugins/php-imap/tests/fixtures/AttachmentNoDispositionTest.php new file mode 100644 index 00000000..aa1ef946 --- /dev/null +++ b/plugins/php-imap/tests/fixtures/AttachmentNoDispositionTest.php @@ -0,0 +1,55 @@ +getFixture("attachment_no_disposition.eml"); + + self::assertEquals("", $message->subject); + self::assertEquals("multipart/mixed", $message->content_type->last()); + self::assertFalse($message->hasTextBody()); + self::assertFalse($message->hasHTMLBody()); + + self::assertCount(1, $message->attachments()); + + $attachment = $message->attachments()->first(); + + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals('26ed3dd2', $attachment->filename); + self::assertEquals('26ed3dd2', $attachment->id); + self::assertEquals("Prostřeno_2014_poslední volné termíny.xls", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals('xls', $attachment->getExtension()); + self::assertEquals("application/vnd.ms-excel", $attachment->content_type); + self::assertEquals("a0ef7cfbc05b73dbcb298fe0bc224b41900cdaf60f9904e3fea5ba6c7670013c", hash("sha256", $attachment->content)); + self::assertEquals(146, $attachment->size); + self::assertEquals(0, $attachment->part_number); + self::assertNull($attachment->disposition); + self::assertNotEmpty($attachment->id); + self::assertEmpty($attachment->content_id); + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/fixtures/BccTest.php b/plugins/php-imap/tests/fixtures/BccTest.php new file mode 100644 index 00000000..8de14f62 --- /dev/null +++ b/plugins/php-imap/tests/fixtures/BccTest.php @@ -0,0 +1,43 @@ +getFixture("bcc.eml"); + + self::assertEquals("test", $message->subject); + self::assertEquals("", $message->return_path); + self::assertEquals("1.0", $message->mime_version); + self::assertEquals("text/plain", $message->content_type); + self::assertEquals("Hi!", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + self::assertEquals("2017-09-27 10:48:51", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", $message->from); + self::assertEquals("to@here.com", $message->to); + self::assertEquals("A_€@{è_Z ", $message->bcc); + self::assertEquals("sender@here.com", $message->sender); + self::assertEquals("reply-to@here.com", $message->reply_to); + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/fixtures/BooleanDecodedContentTest.php b/plugins/php-imap/tests/fixtures/BooleanDecodedContentTest.php new file mode 100644 index 00000000..e49229a4 --- /dev/null +++ b/plugins/php-imap/tests/fixtures/BooleanDecodedContentTest.php @@ -0,0 +1,55 @@ +getFixture("boolean_decoded_content.eml"); + + self::assertEquals("Nuu", $message->subject); + self::assertEquals("Here is the problem mail\r\n \r\nBody text", $message->getTextBody()); + self::assertEquals("Here is the problem mail\r\n \r\nBody text", $message->getHTMLBody()); + + self::assertEquals("2017-09-13 11:05:45", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", $message->from); + self::assertEquals("to@here.com", $message->to); + + $attachments = $message->getAttachments(); + self::assertCount(1, $attachments); + + $attachment = $attachments[0]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("Example Domain.pdf", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals('pdf', $attachment->getExtension()); + self::assertEquals("application/pdf", $attachment->content_type); + self::assertEquals("1c449aaab4f509012fa5eaa180fd017eb7724ccacabdffc1c6066d3756dcde5c", hash("sha256", $attachment->content)); + self::assertEquals(53, $attachment->size); + self::assertEquals(3, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/fixtures/DateTemplateTest.php b/plugins/php-imap/tests/fixtures/DateTemplateTest.php new file mode 100644 index 00000000..9c43ae8b --- /dev/null +++ b/plugins/php-imap/tests/fixtures/DateTemplateTest.php @@ -0,0 +1,115 @@ + "2019-04-05 10:10:49", + "04 Jan 2018 10:12:47 UT" => "2018-01-04 10:12:47", + "22 Jun 18 03:56:36 PM -05:00 (GMT -05:00)" => "2018-06-22 20:56:36", + "Sat, 31 Aug 2013 20:08:23 +0580" => "2013-08-31 14:38:23", + "Fri, 1 Feb 2019 01:30:04 +0600 (+06)" => "2019-01-31 19:30:04", + "Mon, 4 Feb 2019 04:03:49 -0300 (-03)" => "2019-02-04 07:03:49", + "Sun, 6 Apr 2008 21:24:33 UT" => "2008-04-06 21:24:33", + "Wed, 11 Sep 2019 15:23:06 +0600 (+06)" => "2019-09-11 09:23:06", + "14 Sep 2019 00:10:08 UT +0200" => "2019-09-14 00:10:08", + "Tue, 08 Nov 2022 18:47:20 +0000 14:03:33 +0000" => "2022-11-08 18:47:20", + "Sat, 10, Dec 2022 09:35:19 +0100" => "2022-12-10 08:35:19", + "Thur, 16 Mar 2023 15:33:07 +0400" => "2023-03-16 11:33:07", + "fr., 25 nov. 2022 06:27:14 +0100/fr., 25 nov. 2022 06:27:14 +0100" => "2022-11-25 05:27:14", + "Di., 15 Feb. 2022 06:52:44 +0100 (MEZ)/Di., 15 Feb. 2022 06:52:44 +0100 (MEZ)" => "2022-02-15 05:52:44", + ]; + + /** + * Test the fixture date-template.eml + * + * @return void + * @throws InvalidMessageDateException + * @throws ReflectionException + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws ResponseException + * @throws RuntimeException + */ + public function testFixture() : void { + try { + $message = $this->getFixture("date-template.eml"); + $this->fail("Expected InvalidMessageDateException"); + } catch (InvalidMessageDateException $e) { + self::assertTrue(true); + } + + self::$manager->setConfig([ + "options" => [ + "fallback_date" => "2021-01-01 00:00:00", + ], + ]); + $message = $this->getFixture("date-template.eml", self::$manager->getConfig()); + + self::assertEquals("test", $message->subject); + self::assertEquals("1.0", $message->mime_version); + self::assertEquals("Hi!", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + self::assertEquals("2021-01-01 00:00:00", $message->date->first()->timezone("UTC")->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", (string)$message->from); + self::assertEquals("to@here.com", $message->to); + + self::$manager->setConfig([ + "options" => [ + "fallback_date" => null, + ], + ]); + + $filename = implode(DIRECTORY_SEPARATOR, [__DIR__, "..", "messages", "date-template.eml"]); + $blob = file_get_contents($filename); + self::assertNotFalse($blob); + + foreach ($this->dates as $date => $expected) { + $message = Message::fromString(str_replace("%date_raw_header%", $date, $blob)); + self::assertEquals("test", $message->subject); + self::assertEquals("1.0", $message->mime_version); + self::assertEquals("Hi!", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + self::assertEquals($expected, $message->date->first()->timezone("UTC")->format("Y-m-d H:i:s"), "Date \"$date\" should be \"$expected\""); + self::assertEquals("from@there.com", (string)$message->from); + self::assertEquals("to@here.com", $message->to); + } + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/fixtures/EmailAddressTest.php b/plugins/php-imap/tests/fixtures/EmailAddressTest.php new file mode 100644 index 00000000..d4e403d7 --- /dev/null +++ b/plugins/php-imap/tests/fixtures/EmailAddressTest.php @@ -0,0 +1,39 @@ +getFixture("email_address.eml"); + + self::assertEquals("", $message->subject); + self::assertEquals("123@example.com", $message->message_id); + self::assertEquals("Hi\r\nHow are you?", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + self::assertFalse($message->date->first()); + self::assertEquals("no_host@UNKNOWN", (string)$message->from); + self::assertEquals("", $message->to); + self::assertEquals("This one: is \"right\" , No-address@UNKNOWN", $message->cc); + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/fixtures/EmbeddedEmailTest.php b/plugins/php-imap/tests/fixtures/EmbeddedEmailTest.php new file mode 100644 index 00000000..fb6e2492 --- /dev/null +++ b/plugins/php-imap/tests/fixtures/EmbeddedEmailTest.php @@ -0,0 +1,64 @@ +getFixture("embedded_email.eml"); + + self::assertEquals("embedded message", $message->subject); + self::assertEquals([ + 'from webmail.my-office.cz (localhost [127.0.0.1]) by keira.cofis.cz ; Fri, 29 Jan 2016 14:25:40 +0100', + 'from webmail.my-office.cz (localhost [127.0.0.1]) by keira.cofis.cz' + ], $message->received->toArray()); + self::assertEquals("7e5798da5747415e5b82fdce042ab2a6@cerstor.cz", $message->message_id); + self::assertEquals("demo@cerstor.cz", $message->return_path); + self::assertEquals("1.0", $message->mime_version); + self::assertEquals("Roundcube Webmail/1.0.0", $message->user_agent); + self::assertEquals("email that contains embedded message", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + + self::assertEquals("2016-01-29 13:25:40", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("demo@cerstor.cz", $message->from); + self::assertEquals("demo@cerstor.cz", $message->x_sender); + self::assertEquals("demo@cerstor.cz", $message->to); + + $attachments = $message->getAttachments(); + self::assertCount(1, $attachments); + + $attachment = $attachments[0]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("demo.eml", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals('eml', $attachment->getExtension()); + self::assertEquals("message/rfc822", $attachment->content_type); + self::assertEquals("a1f965f10a9872e902a82dde039a237e863f522d238a1cb1968fe3396dbcac65", hash("sha256", $attachment->content)); + self::assertEquals(893, $attachment->size); + self::assertEquals(1, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/fixtures/EmbeddedEmailWithoutContentDispositionEmbeddedTest.php b/plugins/php-imap/tests/fixtures/EmbeddedEmailWithoutContentDispositionEmbeddedTest.php new file mode 100644 index 00000000..7d3fb2d0 --- /dev/null +++ b/plugins/php-imap/tests/fixtures/EmbeddedEmailWithoutContentDispositionEmbeddedTest.php @@ -0,0 +1,75 @@ +getFixture("embedded_email_without_content_disposition-embedded.eml"); + + self::assertEquals("embedded_message_subject", $message->subject); + self::assertEquals([ + 'from webmail.my-office.cz (localhost [127.0.0.1]) by keira.cofis.cz ; Fri, 29 Jan 2016 14:25:40 +0100', + 'from webmail.my-office.cz (localhost [127.0.0.1]) by keira.cofis.cz' + ], $message->received->toArray()); + self::assertEquals("AC39946EBF5C034B87BABD5343E96979012671D40E38@VM002.cerk.cc", $message->message_id); + self::assertEquals("pl-PL, nl-NL", $message->accept_language); + self::assertEquals("pl-PL", $message->content_language); + self::assertEquals("1.0", $message->mime_version); + self::assertEquals("some txt", $message->getTextBody()); + self::assertEquals("\r\n

some txt

\r\n", $message->getHTMLBody()); + + self::assertEquals("2019-04-05 10:10:49", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("demo@cerstor.cz", $message->from); + self::assertEquals("demo@cerstor.cz", $message->to); + + $attachments = $message->getAttachments(); + self::assertCount(2, $attachments); + + $attachment = $attachments[0]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("file1.xlsx", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals('xlsx', $attachment->getExtension()); + self::assertEquals("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", $attachment->content_type); + self::assertEquals("87737d24c106b96e177f9564af6712e2c6d3e932c0632bfbab69c88b0bb934dc", hash("sha256", $attachment->content)); + self::assertEquals(40, $attachment->size); + self::assertEquals(3, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + + $attachment = $attachments[1]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("file2.xlsx", $attachment->name); + self::assertEquals('xlsx', $attachment->getExtension()); + self::assertEquals('text', $attachment->type); + self::assertEquals("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", $attachment->content_type); + self::assertEquals("87737d24c106b96e177f9564af6712e2c6d3e932c0632bfbab69c88b0bb934dc", hash("sha256", $attachment->content)); + self::assertEquals(40, $attachment->size); + self::assertEquals(4, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php b/plugins/php-imap/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php new file mode 100644 index 00000000..603a956d --- /dev/null +++ b/plugins/php-imap/tests/fixtures/EmbeddedEmailWithoutContentDispositionTest.php @@ -0,0 +1,98 @@ +getFixture("embedded_email_without_content_disposition.eml"); + + self::assertEquals("Subject", $message->subject); + self::assertEquals([ + 'from webmail.my-office.cz (localhost [127.0.0.1]) by keira.cofis.cz ; Fri, 29 Jan 2016 14:25:40 +0100', + 'from webmail.my-office.cz (localhost [127.0.0.1]) by keira.cofis.cz' + ], $message->received->toArray()); + self::assertEquals("AC39946EBF5C034B87BABD5343E96979012671D9F7E4@VM002.cerk.cc", $message->message_id); + self::assertEquals("pl-PL, nl-NL", $message->accept_language); + self::assertEquals("1.0", $message->mime_version); + self::assertEquals("TexT\r\n\r\n[cid:file.jpg]", $message->getTextBody()); + self::assertEquals("

TexT

", $message->getHTMLBody()); + + self::assertEquals("2019-04-05 11:48:50", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("demo@cerstor.cz", $message->from); + self::assertEquals("demo@cerstor.cz", $message->to); + + $attachments = $message->getAttachments(); + self::assertCount(4, $attachments); + + $attachment = $attachments[0]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("file.jpg", $attachment->name); + self::assertEquals('jpg', $attachment->getExtension()); + self::assertEquals('text', $attachment->type); + self::assertEquals("image/jpeg", $attachment->content_type); + self::assertEquals("6b7fa434f92a8b80aab02d9bf1a12e49ffcae424e4013a1c4f68b67e3d2bbcd0", hash("sha256", $attachment->content)); + self::assertEquals(96, $attachment->size); + self::assertEquals(3, $attachment->part_number); + self::assertEquals("inline", $attachment->disposition); + self::assertNotEmpty($attachment->id); + + $attachment = $attachments[1]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals('a1abc19a', $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals('', $attachment->getExtension()); + self::assertEquals("message/rfc822", $attachment->content_type); + self::assertEquals("2476c8b91a93c6b2fe1bfff593cb55956c2fe8e7ca6de9ad2dc9d101efe7a867", hash("sha256", $attachment->content)); + self::assertEquals(2073, $attachment->size); + self::assertEquals(5, $attachment->part_number); + self::assertNull($attachment->disposition); + self::assertNotEmpty($attachment->id); + + $attachment = $attachments[2]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("file3.xlsx", $attachment->name); + self::assertEquals('xlsx', $attachment->getExtension()); + self::assertEquals('text', $attachment->type); + self::assertEquals("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", $attachment->content_type); + self::assertEquals("87737d24c106b96e177f9564af6712e2c6d3e932c0632bfbab69c88b0bb934dc", hash("sha256", $attachment->content)); + self::assertEquals(40, $attachment->size); + self::assertEquals(6, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + + $attachment = $attachments[3]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("file4.zip", $attachment->name); + self::assertEquals('zip', $attachment->getExtension()); + self::assertEquals('text', $attachment->type); + self::assertEquals("application/x-zip-compressed", $attachment->content_type); + self::assertEquals("87737d24c106b96e177f9564af6712e2c6d3e932c0632bfbab69c88b0bb934dc", hash("sha256", $attachment->content)); + self::assertEquals(40, $attachment->size); + self::assertEquals(7, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/fixtures/ExampleBounceTest.php b/plugins/php-imap/tests/fixtures/ExampleBounceTest.php new file mode 100644 index 00000000..d2f418a9 --- /dev/null +++ b/plugins/php-imap/tests/fixtures/ExampleBounceTest.php @@ -0,0 +1,99 @@ +getFixture("example_bounce.eml"); + + self::assertEquals("<>", $message->return_path); + self::assertEquals([ + 0 => 'from somewhere.your-server.de by somewhere.your-server.de with LMTP id 3TP8LrElAGSOaAAAmBr1xw (envelope-from <>); Thu, 02 Mar 2023 05:27:29 +0100', + 1 => 'from somewhere06.your-server.de ([1b21:2f8:e0a:50e4::2]) by somewhere.your-server.de with esmtps (TLS1.3) tls TLS_AES_256_GCM_SHA384 (Exim 4.94.2) id 1pXaXR-0006xQ-BN for demo@foo.de; Thu, 02 Mar 2023 05:27:29 +0100', + 2 => 'from [192.168.0.10] (helo=sslproxy01.your-server.de) by somewhere06.your-server.de with esmtps (TLSv1.3:TLS_AES_256_GCM_SHA384:256) (Exim 4.92) id 1pXaXO-000LYP-9R for demo@foo.de; Thu, 02 Mar 2023 05:27:26 +0100', + 3 => 'from localhost ([127.0.0.1] helo=sslproxy01.your-server.de) by sslproxy01.your-server.de with esmtps (TLSv1.3:TLS_AES_256_GCM_SHA384:256) (Exim 4.92) id 1pXaXO-0008gy-7x for demo@foo.de; Thu, 02 Mar 2023 05:27:26 +0100', + 4 => 'from Debian-exim by sslproxy01.your-server.de with local (Exim 4.92) id 1pXaXO-0008gb-6g for demo@foo.de; Thu, 02 Mar 2023 05:27:26 +0100', + 5 => 'from somewhere.your-server.de by somewhere.your-server.de with LMTP id 3TP8LrElAGSOaAAAmBr1xw (envelope-from <>)', + ], $message->received->all()); + self::assertEquals("demo@foo.de", $message->envelope_to); + self::assertEquals("Thu, 02 Mar 2023 05:27:29 +0100", $message->delivery_date); + self::assertEquals([ + 0 => 'somewhere.your-server.de; iprev=pass (somewhere06.your-server.de) smtp.remote-ip=1b21:2f8:e0a:50e4::2; spf=none smtp.mailfrom=<>; dmarc=skipped', + 1 => 'somewhere.your-server.de' + ], $message->authentication_results->all()); + self::assertEquals([ + 0 => 'from somewhere.your-server.de by somewhere.your-server.de with LMTP id 3TP8LrElAGSOaAAAmBr1xw (envelope-from <>); Thu, 02 Mar 2023 05:27:29 +0100', + 1 => 'from somewhere06.your-server.de ([1b21:2f8:e0a:50e4::2]) by somewhere.your-server.de with esmtps (TLS1.3) tls TLS_AES_256_GCM_SHA384 (Exim 4.94.2) id 1pXaXR-0006xQ-BN for demo@foo.de; Thu, 02 Mar 2023 05:27:29 +0100', + 2 => 'from [192.168.0.10] (helo=sslproxy01.your-server.de) by somewhere06.your-server.de with esmtps (TLSv1.3:TLS_AES_256_GCM_SHA384:256) (Exim 4.92) id 1pXaXO-000LYP-9R for demo@foo.de; Thu, 02 Mar 2023 05:27:26 +0100', + 3 => 'from localhost ([127.0.0.1] helo=sslproxy01.your-server.de) by sslproxy01.your-server.de with esmtps (TLSv1.3:TLS_AES_256_GCM_SHA384:256) (Exim 4.92) id 1pXaXO-0008gy-7x for demo@foo.de; Thu, 02 Mar 2023 05:27:26 +0100', + 4 => 'from Debian-exim by sslproxy01.your-server.de with local (Exim 4.92) id 1pXaXO-0008gb-6g for demo@foo.de; Thu, 02 Mar 2023 05:27:26 +0100', + 5 => 'from somewhere.your-server.de by somewhere.your-server.de with LMTP id 3TP8LrElAGSOaAAAmBr1xw (envelope-from <>)', + ], $message->received->all()); + self::assertEquals("ding@ding.de", $message->x_failed_recipients); + self::assertEquals("auto-replied", $message->auto_submitted); + self::assertEquals("Mail Delivery System ", $message->from); + self::assertEquals("demo@foo.de", $message->to); + self::assertEquals("1.0", $message->mime_version); + self::assertEquals("Mail delivery failed", $message->subject); + self::assertEquals("E1pXaXO-0008gb-6g@sslproxy01.your-server.de", $message->message_id); + self::assertEquals("2023-03-02 04:27:26", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("Clear (ClamAV 0.103.8/26827/Wed Mar 1 09:28:49 2023)", $message->x_virus_scanned); + self::assertEquals("0.0 (/)", $message->x_spam_score); + self::assertEquals("bar-demo@foo.de", $message->delivered_to); + self::assertEquals("multipart/report", $message->content_type->last()); + self::assertEquals("5d4847c21c8891e73d62c8246f260a46496958041a499f33ecd47444fdaa591b", hash("sha256", $message->getTextBody())); + self::assertFalse($message->hasHTMLBody()); + + $attachments = $message->attachments(); + self::assertCount(2, $attachments); + + $attachment = $attachments[0]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals('c541a506', $attachment->filename); + self::assertEquals("c541a506", $attachment->name); + self::assertEquals('', $attachment->getExtension()); + self::assertEquals('text', $attachment->type); + self::assertEquals("message/delivery-status", $attachment->content_type); + self::assertEquals("85ac09d1d74b2d85853084dc22abcad205a6bfde62d6056e3a933ffe7e82e45c", hash("sha256", $attachment->content)); + self::assertEquals(267, $attachment->size); + self::assertEquals(1, $attachment->part_number); + self::assertNull($attachment->disposition); + self::assertNotEmpty($attachment->id); + + $attachment = $attachments[1]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals('da786518', $attachment->filename); + self::assertEquals("da786518", $attachment->name); + self::assertEquals('', $attachment->getExtension()); + self::assertEquals('text', $attachment->type); + self::assertEquals("message/rfc822", $attachment->content_type); + self::assertEquals("7525331f5fab23ea77f595b995336aca7b8dad12db00ada14abebe7fe5b96e10", hash("sha256", $attachment->content)); + self::assertEquals(776, $attachment->size); + self::assertEquals(2, $attachment->part_number); + self::assertNull($attachment->disposition); + self::assertNotEmpty($attachment->id); + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/fixtures/FixtureTestCase.php b/plugins/php-imap/tests/fixtures/FixtureTestCase.php new file mode 100644 index 00000000..00f31157 --- /dev/null +++ b/plugins/php-imap/tests/fixtures/FixtureTestCase.php @@ -0,0 +1,94 @@ + [ + "debug" => $_ENV["LIVE_MAILBOX_DEBUG"] ?? false, + ], + 'accounts' => [ + 'default' => [ + 'host' => getenv("LIVE_MAILBOX_HOST"), + 'port' => getenv("LIVE_MAILBOX_PORT"), + 'encryption' => getenv("LIVE_MAILBOX_ENCRYPTION"), + 'validate_cert' => getenv("LIVE_MAILBOX_VALIDATE_CERT"), + 'username' => getenv("LIVE_MAILBOX_USERNAME"), + 'password' => getenv("LIVE_MAILBOX_PASSWORD"), + 'protocol' => 'imap', //might also use imap, [pop3 or nntp (untested)] + ], + ], + ]); + return self::$manager; + } + + /** + * Get a fixture message + * @param string $template + * + * @return Message + * @throws ReflectionException + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws ResponseException + * @throws RuntimeException + */ + final public function getFixture(string $template, Config $config = null) : Message { + $filename = implode(DIRECTORY_SEPARATOR, [__DIR__, "..", "messages", $template]); + $message = Message::fromFile($filename, $config); + self::assertInstanceOf(Message::class, $message); + + return $message; + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/fixtures/FourNestedEmailsTest.php b/plugins/php-imap/tests/fixtures/FourNestedEmailsTest.php new file mode 100644 index 00000000..e6c37ccd --- /dev/null +++ b/plugins/php-imap/tests/fixtures/FourNestedEmailsTest.php @@ -0,0 +1,55 @@ +getFixture("four_nested_emails.eml"); + + self::assertEquals("3-third-subject", $message->subject); + self::assertEquals("3-third-content", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + self::assertFalse($message->date->first()); + self::assertEquals("test@example.com", $message->from->first()->mail); + self::assertEquals("test@example.com", $message->to->first()->mail); + + $attachments = $message->getAttachments(); + self::assertCount(1, $attachments); + + $attachment = $attachments[0]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("2-second-email.eml", $attachment->name); + self::assertEquals('text', $attachment->type); + self::assertEquals('eml', $attachment->getExtension()); + self::assertEquals("message/rfc822", $attachment->content_type); + self::assertEquals("85012e6a26d064a0288ee62618b3192687385adb4a4e27e48a28f738a325ca46", hash("sha256", $attachment->content)); + self::assertEquals(1376, $attachment->size); + self::assertEquals(2, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/fixtures/GbkCharsetTest.php b/plugins/php-imap/tests/fixtures/GbkCharsetTest.php new file mode 100644 index 00000000..7492d908 --- /dev/null +++ b/plugins/php-imap/tests/fixtures/GbkCharsetTest.php @@ -0,0 +1,37 @@ +getFixture("gbk_charset.eml"); + + self::assertEquals("Nuu", $message->subject); + self::assertEquals("Hi", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + self::assertEquals("2017-09-13 11:05:45", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", $message->from->first()->mail); + self::assertEquals("to@here.com", $message->to->first()->mail); + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/fixtures/HtmlOnlyTest.php b/plugins/php-imap/tests/fixtures/HtmlOnlyTest.php new file mode 100644 index 00000000..90ef44e3 --- /dev/null +++ b/plugins/php-imap/tests/fixtures/HtmlOnlyTest.php @@ -0,0 +1,37 @@ +getFixture("html_only.eml"); + + self::assertEquals("Nuu", $message->subject); + self::assertEquals("Hi", $message->getHTMLBody()); + self::assertFalse($message->hasTextBody()); + self::assertEquals("2017-09-13 11:05:45", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", $message->from->first()->mail); + self::assertEquals("to@here.com", $message->to->first()->mail); + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/fixtures/ImapMimeHeaderDecodeReturnsFalseTest.php b/plugins/php-imap/tests/fixtures/ImapMimeHeaderDecodeReturnsFalseTest.php new file mode 100644 index 00000000..7f90a4ec --- /dev/null +++ b/plugins/php-imap/tests/fixtures/ImapMimeHeaderDecodeReturnsFalseTest.php @@ -0,0 +1,37 @@ +getFixture("imap_mime_header_decode_returns_false.eml"); + + self::assertEquals("=?UTF-8?B?nnDusSNdG92w6Fuw61fMjAxOF8wMy0xMzMyNTMzMTkzLnBkZg==?=", $message->subject->first()); + self::assertEquals("Hi", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + self::assertEquals("2017-09-13 11:05:45", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", $message->from->first()->mail); + self::assertEquals("to@here.com", $message->to->first()->mail); + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/fixtures/InlineAttachmentTest.php b/plugins/php-imap/tests/fixtures/InlineAttachmentTest.php new file mode 100644 index 00000000..edb380cd --- /dev/null +++ b/plugins/php-imap/tests/fixtures/InlineAttachmentTest.php @@ -0,0 +1,62 @@ +getFixture("inline_attachment.eml"); + + self::assertEquals("", $message->subject); + self::assertFalse($message->hasTextBody()); + self::assertEquals('', $message->getHTMLBody()); + + self::assertFalse($message->date->first()); + self::assertFalse($message->from->first()); + self::assertFalse($message->to->first()); + + + $attachments = $message->attachments(); + self::assertInstanceOf(AttachmentCollection::class, $attachments); + self::assertCount(1, $attachments); + + $attachment = $attachments[0]; + + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals('d2913999', $attachment->name); + self::assertEquals('d2913999', $attachment->filename); + self::assertEquals('ii_15f0aad691bb745f', $attachment->id); + self::assertEquals('text', $attachment->type); + self::assertEquals('', $attachment->getExtension()); + self::assertEquals("image/png", $attachment->content_type); + self::assertEquals("6568c9e9c35a7fa06f236e89f704d8c9b47183a24f2c978dba6c92e2747e3a13", hash("sha256", $attachment->content)); + self::assertEquals(1486, $attachment->size); + self::assertEquals(1, $attachment->part_number); + self::assertEquals("inline", $attachment->disposition); + self::assertEquals("", $attachment->content_id); + self::assertNotEmpty($attachment->id); + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/fixtures/KsC56011987HeadersTest.php b/plugins/php-imap/tests/fixtures/KsC56011987HeadersTest.php new file mode 100644 index 00000000..944e9bfb --- /dev/null +++ b/plugins/php-imap/tests/fixtures/KsC56011987HeadersTest.php @@ -0,0 +1,46 @@ +getFixture("ks_c_5601-1987_headers.eml"); + + self::assertEquals("RE: 회원님께 Ersi님이 메시지를 보냈습니다.", $message->subject); + self::assertEquals("=?ks_c_5601-1987?B?yLi/+LTUsrIgRXJzabTUwMwguN69w8H2uKYgurizwr3AtM+02S4=?=", $message->thread_topic); + self::assertEquals("1.0", $message->mime_version); + self::assertEquals("Content", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + self::assertEquals("2017-09-27 10:48:51", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("to@here.com", $message->to->first()->mail); + + + $from = $message->from->first(); + self::assertEquals("김 현진", $from->personal); + self::assertEquals("from", $from->mailbox); + self::assertEquals("there.com", $from->host); + self::assertEquals("from@there.com", $from->mail); + self::assertEquals("김 현진 ", $from->full); + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/fixtures/MailThatIsAttachmentTest.php b/plugins/php-imap/tests/fixtures/MailThatIsAttachmentTest.php new file mode 100644 index 00000000..0f4164c9 --- /dev/null +++ b/plugins/php-imap/tests/fixtures/MailThatIsAttachmentTest.php @@ -0,0 +1,63 @@ +getFixture("mail_that_is_attachment.eml"); + + self::assertEquals("Report domain: yyy.cz Submitter: google.com Report-ID: 2244696771454641389", $message->subject); + self::assertEquals("2244696771454641389@google.com", $message->message_id); + self::assertEquals("1.0", $message->mime_version); + self::assertFalse($message->hasTextBody()); + self::assertFalse($message->hasHTMLBody()); + + self::assertEquals("2015-02-15 10:21:51", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("xxx@yyy.cz", $message->to->first()->mail); + self::assertEquals("xxx@yyy.cz", $message->sender->first()->mail); + + $from = $message->from->first(); + self::assertEquals("noreply-dmarc-support via xxx", $from->personal); + self::assertEquals("xxx", $from->mailbox); + self::assertEquals("yyy.cz", $from->host); + self::assertEquals("xxx@yyy.cz", $from->mail); + self::assertEquals("noreply-dmarc-support via xxx ", $from->full); + + self::assertCount(1, $message->attachments()); + + $attachment = $message->attachments()->first(); + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("google.com!yyy.cz!1423872000!1423958399.zip", $attachment->name); + self::assertEquals('zip', $attachment->getExtension()); + self::assertEquals('text', $attachment->type); + self::assertEquals("application/zip", $attachment->content_type); + self::assertEquals("c0d4f47b6fde124cea7460c3e509440d1a062705f550b0502b8ba0cbf621c97a", hash("sha256", $attachment->content)); + self::assertEquals(1062, $attachment->size); + self::assertEquals(0, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/fixtures/MissingDateTest.php b/plugins/php-imap/tests/fixtures/MissingDateTest.php new file mode 100644 index 00000000..93595adc --- /dev/null +++ b/plugins/php-imap/tests/fixtures/MissingDateTest.php @@ -0,0 +1,37 @@ +getFixture("missing_date.eml"); + + self::assertEquals("Nuu", $message->getSubject()); + self::assertEquals("Hi", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + self::assertFalse($message->date->first()); + self::assertEquals("from@here.com", $message->from->first()->mail); + self::assertEquals("to@here.com", $message->to->first()->mail); + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/fixtures/MissingFromTest.php b/plugins/php-imap/tests/fixtures/MissingFromTest.php new file mode 100644 index 00000000..5d57340c --- /dev/null +++ b/plugins/php-imap/tests/fixtures/MissingFromTest.php @@ -0,0 +1,37 @@ +getFixture("missing_from.eml"); + + self::assertEquals("Nuu", $message->getSubject()); + self::assertEquals("Hi", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + self::assertEquals("2017-09-13 11:05:45", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertFalse($message->from->first()); + self::assertEquals("to@here.com", $message->to->first()->mail); + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/fixtures/MixedFilenameTest.php b/plugins/php-imap/tests/fixtures/MixedFilenameTest.php new file mode 100644 index 00000000..b2be0e32 --- /dev/null +++ b/plugins/php-imap/tests/fixtures/MixedFilenameTest.php @@ -0,0 +1,61 @@ +getFixture("mixed_filename.eml"); + + self::assertEquals("Свежий прайс-лист", $message->subject); + self::assertFalse($message->hasTextBody()); + self::assertFalse($message->hasHTMLBody()); + + self::assertEquals("2018-02-02 19:23:06", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + + $from = $message->from->first(); + self::assertEquals("Прайсы || ПартКом", $from->personal); + self::assertEquals("support", $from->mailbox); + self::assertEquals("part-kom.ru", $from->host); + self::assertEquals("support@part-kom.ru", $from->mail); + self::assertEquals("Прайсы || ПартКом ", $from->full); + + self::assertEquals("foo@bar.com", $message->to->first()); + + self::assertCount(1, $message->attachments()); + + $attachment = $message->attachments()->first(); + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("Price4VladDaKar.xlsx", $attachment->name); + self::assertEquals('xlsx', $attachment->getExtension()); + self::assertEquals('text', $attachment->type); + self::assertEquals("application/octet-stream", $attachment->content_type); + self::assertEquals("b832983842b0ad65db69e4c7096444c540a2393e2d43f70c2c9b8b9fceeedbb1", hash('sha256', $attachment->content)); + self::assertEquals(94, $attachment->size); + self::assertEquals(2, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/fixtures/MultipartWithoutBodyTest.php b/plugins/php-imap/tests/fixtures/MultipartWithoutBodyTest.php new file mode 100644 index 00000000..9989b3e7 --- /dev/null +++ b/plugins/php-imap/tests/fixtures/MultipartWithoutBodyTest.php @@ -0,0 +1,63 @@ +getFixture("multipart_without_body.eml"); + + self::assertEquals("This mail will not contain a body", $message->subject); + self::assertEquals("This mail will not contain a body", $message->getTextBody()); + self::assertEquals("d76dfb1ff3231e3efe1675c971ce73f722b906cc049d328db0d255f8d3f65568", hash("sha256", $message->getHTMLBody())); + self::assertEquals("2023-03-11 08:24:31", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("Foo Bülow Bar ", $message->from); + self::assertEquals("some one ", $message->to); + self::assertEquals([ + 0 => 'from AS8PR02MB6805.eurprd02.prod.outlook.com (2603:10a6:20b:252::8) by PA4PR02MB7071.eurprd02.prod.outlook.com with HTTPS; Sat, 11 Mar 2023 08:24:33 +0000', + 1 => 'from omef0ahNgeoJu.eurprd02.prod.outlook.com (2603:10a6:10:33c::12) by AS8PR02MB6805.eurprd02.prod.outlook.com (2603:10a6:20b:252::8) with Microsoft SMTP Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.20.6178.19; Sat, 11 Mar 2023 08:24:31 +0000', + 2 => 'from omef0ahNgeoJu.eurprd02.prod.outlook.com ([fe80::38c0:9c40:7fc6:93a7]) by omef0ahNgeoJu.eurprd02.prod.outlook.com ([fe80::38c0:9c40:7fc6:93a7%7]) with mapi id 15.20.6178.019; Sat, 11 Mar 2023 08:24:31 +0000', + 3 => 'from AS8PR02MB6805.eurprd02.prod.outlook.com (2603:10a6:20b:252::8) by PA4PR02MB7071.eurprd02.prod.outlook.com with HTTPS', + ], $message->received->all()); + self::assertEquals("This mail will not contain a body", $message->thread_topic); + self::assertEquals("AdlT8uVmpHPvImbCRM6E9LODIvAcQA==", $message->thread_index); + self::assertEquals("omef0ahNgeoJuEB51C568ED2227A2DAABB5BB9@omef0ahNgeoJu.eurprd02.prod.outlook.com", $message->message_id); + self::assertEquals("da-DK, en-US", $message->accept_language); + self::assertEquals("en-US", $message->content_language); + self::assertEquals("Internal", $message->x_ms_exchange_organization_authAs); + self::assertEquals("04", $message->x_ms_exchange_organization_authMechanism); + self::assertEquals("omef0ahNgeoJu.eurprd02.prod.outlook.com", $message->x_ms_exchange_organization_authSource); + self::assertEquals("", $message->x_ms_Has_Attach); + self::assertEquals("aa546a02-2b7a-4fb1-7fd4-08db220a09f1", $message->x_ms_exchange_organization_Network_Message_Id); + self::assertEquals("-1", $message->x_ms_exchange_organization_SCL); + self::assertEquals("", $message->x_ms_TNEF_Correlator); + self::assertEquals("0", $message->x_ms_exchange_organization_RecordReviewCfmType); + self::assertEquals("Email", $message->x_ms_publictraffictype); + self::assertEquals("ucf:0;jmr:0;auth:0;dest:I;ENG:(910001)(944506478)(944626604)(920097)(425001)(930097);", $message->X_Microsoft_Antispam_Mailbox_Delivery->first()); + self::assertEquals("0712b5fe22cf6e75fa220501c1a6715a61098983df9e69bad4000c07531c1295", hash("sha256", $message->X_Microsoft_Antispam_Message_Info)); + self::assertEquals("multipart/alternative", $message->Content_Type->last()); + self::assertEquals("1.0", $message->mime_version); + + self::assertCount(0, $message->getAttachments()); + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/fixtures/MultipleHtmlPartsAndAttachmentsTest.php b/plugins/php-imap/tests/fixtures/MultipleHtmlPartsAndAttachmentsTest.php new file mode 100644 index 00000000..c08c03e0 --- /dev/null +++ b/plugins/php-imap/tests/fixtures/MultipleHtmlPartsAndAttachmentsTest.php @@ -0,0 +1,76 @@ +getFixture("multiple_html_parts_and_attachments.eml"); + + self::assertEquals("multiple_html_parts_and_attachments", $message->subject); + self::assertEquals("This is the first html part\r\n\r\n\r\n\r\nThis is the second html part\r\n\r\n\r\n\r\nThis is the last html part\r\nhttps://www.there.com", $message->getTextBody()); + self::assertEquals("This is the first html part

\n

This is the second html part

\n

This is the last html part
https://www.there.com



\r\n
", $message->getHTMLBody()); + + self::assertEquals("2023-02-16 09:19:02", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + + $from = $message->from->first(); + self::assertEquals("FromName", $from->personal); + self::assertEquals("from", $from->mailbox); + self::assertEquals("there.com", $from->host); + self::assertEquals("from@there.com", $from->mail); + self::assertEquals("FromName ", $from->full); + + self::assertEquals("to@there.com", $message->to->first()); + + $attachments = $message->attachments(); + self::assertInstanceOf(AttachmentCollection::class, $attachments); + self::assertCount(2, $attachments); + + $attachment = $attachments[0]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("attachment1.pdf", $attachment->name); + self::assertEquals('pdf', $attachment->getExtension()); + self::assertEquals('text', $attachment->type); + self::assertEquals("application/pdf", $attachment->content_type); + self::assertEquals("c162adf19e0f67e26ef0b7f791b33a60b2c23b175560a505dc7f9ec490206e49", hash("sha256", $attachment->content)); + self::assertEquals(4814, $attachment->size); + self::assertEquals(4, $attachment->part_number); + self::assertEquals("inline", $attachment->disposition); + self::assertNotEmpty($attachment->id); + + $attachment = $attachments[1]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("attachment2.pdf", $attachment->name); + self::assertEquals('pdf', $attachment->getExtension()); + self::assertEquals('text', $attachment->type); + self::assertEquals("application/pdf", $attachment->content_type); + self::assertEquals("a337b37e9d3edb172a249639919f0eee3d344db352046d15f8f9887e55855a25", hash("sha256", $attachment->content)); + self::assertEquals(5090, $attachment->size); + self::assertEquals(6, $attachment->part_number); + self::assertEquals("inline", $attachment->disposition); + self::assertNotEmpty($attachment->id); + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/fixtures/MultipleNestedAttachmentsTest.php b/plugins/php-imap/tests/fixtures/MultipleNestedAttachmentsTest.php new file mode 100644 index 00000000..543c0e21 --- /dev/null +++ b/plugins/php-imap/tests/fixtures/MultipleNestedAttachmentsTest.php @@ -0,0 +1,69 @@ +getFixture("multiple_nested_attachments.eml"); + + self::assertEquals("", $message->subject); + self::assertEquals("------------------------------------------------------------------------", $message->getTextBody()); + self::assertEquals("\r\n \r\n\r\n \r\n \r\n \r\n


\r\n

\r\n
\r\n \r\n \r\n  \"\"\r\n
\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n

\r\n

\r\n
\r\n
\r\n \r\n", $message->getHTMLBody()); + + self::assertEquals("2018-01-15 09:54:09", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertFalse($message->from->first()); + self::assertFalse($message->to->first()); + + $attachments = $message->attachments(); + self::assertInstanceOf(AttachmentCollection::class, $attachments); + self::assertCount(2, $attachments); + + $attachment = $attachments[0]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("mleokdgdlgkkecep.png", $attachment->name); + self::assertEquals('png', $attachment->getExtension()); + self::assertEquals('text', $attachment->type); + self::assertEquals("image/png", $attachment->content_type); + self::assertEquals("e0e99b0bd6d5ea3ced99add53cc98b6f8eea6eae8ddd773fd06f3489289385fb", hash("sha256", $attachment->content)); + self::assertEquals(114, $attachment->size); + self::assertEquals(5, $attachment->part_number); + self::assertEquals("inline", $attachment->disposition); + self::assertNotEmpty($attachment->id); + + $attachment = $attachments[1]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("FF4D00-1.png", $attachment->name); + self::assertEquals('png', $attachment->getExtension()); + self::assertEquals('text', $attachment->type); + self::assertEquals("image/png", $attachment->content_type); + self::assertEquals("e0e99b0bd6d5ea3ced99add53cc98b6f8eea6eae8ddd773fd06f3489289385fb", hash("sha256", $attachment->content)); + self::assertEquals(114, $attachment->size); + self::assertEquals(8, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/fixtures/NestesEmbeddedWithAttachmentTest.php b/plugins/php-imap/tests/fixtures/NestesEmbeddedWithAttachmentTest.php new file mode 100644 index 00000000..c5c8c561 --- /dev/null +++ b/plugins/php-imap/tests/fixtures/NestesEmbeddedWithAttachmentTest.php @@ -0,0 +1,69 @@ +getFixture("nestes_embedded_with_attachment.eml"); + + self::assertEquals("Nuu", $message->subject); + self::assertEquals("Dear Sarah", $message->getTextBody()); + self::assertEquals("\r\n\r\n
Dear Sarah,
\r\n", $message->getHTMLBody()); + + self::assertEquals("2017-09-13 11:05:45", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", $message->from->first()->mail); + self::assertEquals("to@here.com", $message->to->first()->mail); + + $attachments = $message->attachments(); + self::assertInstanceOf(AttachmentCollection::class, $attachments); + self::assertCount(2, $attachments); + + $attachment = $attachments[0]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("first.eml", $attachment->name); + self::assertEquals('eml', $attachment->getExtension()); + self::assertEquals('text', $attachment->type); + self::assertEquals("message/rfc822", $attachment->content_type); + self::assertEquals("From: from@there.com\r\nTo: to@here.com\r\nSubject: FIRST\r\nDate: Sat, 28 Apr 2018 14:37:16 -0400\r\nMIME-Version: 1.0\r\nContent-Type: multipart/mixed;\r\n boundary=\"----=_NextPart_000_222_000\"\r\n\r\nThis is a multi-part message in MIME format.\r\n\r\n------=_NextPart_000_222_000\r\nContent-Type: multipart/alternative;\r\n boundary=\"----=_NextPart_000_222_111\"\r\n\r\n\r\n------=_NextPart_000_222_111\r\nContent-Type: text/plain;\r\n charset=\"UTF-8\"\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\nPlease respond directly to this email to update your RMA\r\n\r\n\r\n2018-04-17T11:04:03-04:00\r\n------=_NextPart_000_222_111\r\nContent-Type: text/html;\r\n charset=\"UTF-8\"\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\n\r\n\r\n
Please respond directly to this =\r\nemail to=20\r\nupdate your RMA
\r\n\r\n------=_NextPart_000_222_111--\r\n\r\n------=_NextPart_000_222_000\r\nContent-Type: image/png;\r\n name=\"chrome.png\"\r\nContent-Transfer-Encoding: base64\r\nContent-Disposition: attachment;\r\n filename=\"chrome.png\"\r\n\r\niVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAAB+FBMVEUAAAA/mUPidDHiLi5Cn0Xk\r\nNTPmeUrkdUg/m0Q0pEfcpSbwaVdKskg+lUP4zA/iLi3msSHkOjVAmETdJSjtYFE/lkPnRj3sWUs8\r\nkkLeqCVIq0fxvhXqUkbVmSjwa1n1yBLepyX1xxP0xRXqUkboST9KukpHpUbuvRrzrhF/ljbwalju\r\nZFM4jELaoSdLtElJrUj1xxP6zwzfqSU4i0HYnydMtUlIqUfywxb60AxZqEXaoifgMCXptR9MtklH\r\npEY2iUHWnSjvvRr70QujkC+pUC/90glMuEnlOjVMt0j70QriLS1LtEnnRj3qUUXfIidOjsxAhcZF\r\no0bjNDH0xxNLr0dIrUdmntVTkMoyfL8jcLBRuErhJyrgKyb4zA/5zg3tYFBBmUTmQTnhMinruBzv\r\nvhnxwxZ/st+Ktt5zp9hqota2vtK6y9FemNBblc9HiMiTtMbFtsM6gcPV2r6dwroseLrMrbQrdLGd\r\nyKoobKbo3Zh+ynrgVllZulTsXE3rV0pIqUf42UVUo0JyjEHoS0HmsiHRGR/lmRz/1hjqnxjvpRWf\r\nwtOhusaz0LRGf7FEfbDVmqHXlJeW0pbXq5bec3fX0nTnzmuJuWvhoFFhm0FtrziBsjaAaDCYWC+u\r\nSi6jQS3FsSfLJiTirCOkuCG1KiG+wSC+GBvgyhTszQ64Z77KAAAARXRSTlMAIQRDLyUgCwsE6ebm\r\n5ubg2dLR0byXl4FDQzU1NDEuLSUgC+vr6urq6ubb29vb2tra2tG8vLu7u7uXl5eXgYGBgYGBLiUA\r\nLabIAAABsElEQVQoz12S9VPjQBxHt8VaOA6HE+AOzv1wd7pJk5I2adpCC7RUcHd3d3fXf5PvLkxh\r\neD++z+yb7GSRlwD/+Hj/APQCZWxM5M+goF+RMbHK594v+tPoiN1uHxkt+xzt9+R9wnRTZZQpXQ0T\r\n5uP1IQxToyOAZiQu5HEpjeA4SWIoksRxNiGC1tRZJ4LNxgHgnU5nJZBDvuDdl8lzQRBsQ+s9PZt7\r\ns7Pz8wsL39/DkIfZ4xlB2Gqsq62ta9oxVlVrNZpihFRpGO9fzQw1ms0NDWZz07iGkJmIFH8xxkc3\r\na/WWlubmFkv9AB2SEpDvKxbjidN2faseaNV3zoHXvv7wMODJdkOHAegweAfFPx4G67KluxzottCU\r\n9n8CUqXzcIQdXOytAHqXxomvykhEKN9EFutG22p//0rbNvHVxiJywa8yS2KDfV1dfbu31H8jF1RH\r\niTKtWYeHxUvq3bn0pyjCRaiRU6aDO+gb3aEfEeVNsDgm8zzLy9egPa7Qt8TSJdwhjplk06HH43ZN\r\nJ3s91KKCHQ5x4sw1fRGYDZ0n1L4FKb9/BP5JLYxToheoFCVxz57PPS8UhhEpLBVeAAAAAElFTkSu\r\nQmCC\r\n\r\n------=_NextPart_000_222_000--", $attachment->content); + self::assertEquals(2535, $attachment->size); + self::assertEquals(5, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + + $attachment = $attachments[1]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("second.eml", $attachment->name); + self::assertEquals('eml', $attachment->getExtension()); + self::assertEquals('text', $attachment->type); + self::assertEquals("message/rfc822", $attachment->content_type); + self::assertEquals("From: from@there.com\r\nTo: to@here.com\r\nSubject: SECOND\r\nDate: Sat, 28 Apr 2018 13:37:30 -0400\r\nMIME-Version: 1.0\r\nContent-Type: multipart/alternative;\r\n boundary=\"----=_NextPart_000_333_000\"\r\n\r\nThis is a multi-part message in MIME format.\r\n\r\n------=_NextPart_000_333_000\r\nContent-Type: text/plain;\r\n charset=\"UTF-8\"\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\nT whom it may concern:\r\n------=_NextPart_000_333_000\r\nContent-Type: text/html;\r\n charset=\"UTF-8\"\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\n\r\n\r\n
T whom it may concern:
\r\n\r\n\r\n------=_NextPart_000_333_000--", $attachment->content); + self::assertEquals(631, $attachment->size); + self::assertEquals(6, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/fixtures/NullContentCharsetTest.php b/plugins/php-imap/tests/fixtures/NullContentCharsetTest.php new file mode 100644 index 00000000..f8e6c63f --- /dev/null +++ b/plugins/php-imap/tests/fixtures/NullContentCharsetTest.php @@ -0,0 +1,39 @@ +getFixture("null_content_charset.eml"); + + self::assertEquals("test", $message->getSubject()); + self::assertEquals("Hi!", $message->getTextBody()); + self::assertEquals("1.0", $message->mime_version); + self::assertFalse($message->hasHTMLBody()); + + self::assertEquals("2017-09-27 10:48:51", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", $message->from->first()->mail); + self::assertEquals("to@here.com", $message->to->first()->mail); + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/fixtures/PecTest.php b/plugins/php-imap/tests/fixtures/PecTest.php new file mode 100644 index 00000000..28ecb27b --- /dev/null +++ b/plugins/php-imap/tests/fixtures/PecTest.php @@ -0,0 +1,82 @@ +getFixture("pec.eml"); + + self::assertEquals("Certified", $message->subject); + self::assertEquals("Signed", $message->getTextBody()); + self::assertEquals("Signed", $message->getHTMLBody()); + + self::assertEquals("2017-10-02 10:13:43", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("test@example.com", $message->from->first()->mail); + self::assertEquals("test@example.com", $message->to->first()->mail); + + $attachments = $message->attachments(); + + self::assertInstanceOf(AttachmentCollection::class, $attachments); + self::assertCount(3, $attachments); + + $attachment = $attachments[0]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("data.xml", $attachment->name); + self::assertEquals('xml', $attachment->getExtension()); + self::assertEquals('text', $attachment->type); + self::assertEquals("application/xml", $attachment->content_type); + self::assertEquals("", $attachment->content); + self::assertEquals(8, $attachment->size); + self::assertEquals(4, $attachment->part_number); + self::assertEquals("inline", $attachment->disposition); + self::assertNotEmpty($attachment->id); + + $attachment = $attachments[1]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("postacert.eml", $attachment->name); + self::assertEquals('eml', $attachment->getExtension()); + self::assertEquals('text', $attachment->type); + self::assertEquals("message/rfc822", $attachment->content_type); + self::assertEquals("To: test@example.com\r\nFrom: test@example.com\r\nSubject: test-subject\r\nDate: Mon, 2 Oct 2017 12:13:50 +0200\r\nContent-Type: text/plain; charset=iso-8859-15; format=flowed\r\nContent-Transfer-Encoding: 7bit\r\n\r\ntest-content", $attachment->content); + self::assertEquals(216, $attachment->size); + self::assertEquals(5, $attachment->part_number); + self::assertEquals("inline", $attachment->disposition); + self::assertNotEmpty($attachment->id); + + $attachment = $attachments[2]; + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("smime.p7s", $attachment->name); + self::assertEquals('p7s', $attachment->getExtension()); + self::assertEquals('text', $attachment->type); + self::assertEquals("application/x-pkcs7-signature", $attachment->content_type); + self::assertEquals("1", $attachment->content); + self::assertEquals(4, $attachment->size); + self::assertEquals(7, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/fixtures/PlainOnlyTest.php b/plugins/php-imap/tests/fixtures/PlainOnlyTest.php new file mode 100644 index 00000000..b3a65bfe --- /dev/null +++ b/plugins/php-imap/tests/fixtures/PlainOnlyTest.php @@ -0,0 +1,37 @@ +getFixture("plain_only.eml"); + + self::assertEquals("Nuu", $message->getSubject()); + self::assertEquals("Hi", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + self::assertEquals("2017-09-13 11:05:45", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", $message->from->first()->mail); + self::assertEquals("to@here.com", $message->to->first()->mail); + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/fixtures/PlainTextAttachmentTest.php b/plugins/php-imap/tests/fixtures/PlainTextAttachmentTest.php new file mode 100644 index 00000000..2e9993cc --- /dev/null +++ b/plugins/php-imap/tests/fixtures/PlainTextAttachmentTest.php @@ -0,0 +1,54 @@ +getFixture("plain_text_attachment.eml"); + + self::assertEquals("Plain text attachment", $message->subject); + self::assertEquals("Test", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + + self::assertEquals("2018-08-21 07:05:14", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", $message->from->first()->mail); + self::assertEquals("to@here.com", $message->to->first()->mail); + + self::assertCount(1, $message->attachments()); + + $attachment = $message->attachments()->first(); + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("a.txt", $attachment->name); + self::assertEquals('txt', $attachment->getExtension()); + self::assertEquals('text', $attachment->type); + self::assertNull($attachment->content_type); + self::assertEquals("Hi!", $attachment->content); + self::assertEquals(4, $attachment->size); + self::assertEquals(2, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/fixtures/ReferencesTest.php b/plugins/php-imap/tests/fixtures/ReferencesTest.php new file mode 100644 index 00000000..18473d61 --- /dev/null +++ b/plugins/php-imap/tests/fixtures/ReferencesTest.php @@ -0,0 +1,54 @@ +getFixture("references.eml"); + + self::assertEquals("", $message->subject); + self::assertEquals("Hi\r\nHow are you?", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + self::assertFalse($message->date->first()); + + self::assertEquals("b9e87bd5e661a645ed6e3b832828fcc5@example.com", $message->in_reply_to); + self::assertEquals("", $message->from->first()->personal); + self::assertEquals("UNKNOWN", $message->from->first()->host); + self::assertEquals("no_host@UNKNOWN", $message->from->first()->mail); + self::assertFalse($message->to->first()); + + self::assertEquals([ + "231d9ac57aec7d8c1a0eacfeab8af6f3@example.com", + "08F04024-A5B3-4FDE-BF2C-6710DE97D8D9@example.com" + ], $message->getReferences()->all()); + + self::assertEquals([ + 'This one: is "right" ', + 'No-address@UNKNOWN' + ], $message->cc->map(function($address){ + /** @var \Webklex\PHPIMAP\Address $address */ + return $address->full; + })); + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/fixtures/SimpleMultipartTest.php b/plugins/php-imap/tests/fixtures/SimpleMultipartTest.php new file mode 100644 index 00000000..d2a2d885 --- /dev/null +++ b/plugins/php-imap/tests/fixtures/SimpleMultipartTest.php @@ -0,0 +1,37 @@ +getFixture("simple_multipart.eml"); + + self::assertEquals("test", $message->getSubject()); + self::assertEquals("MyPlain", $message->getTextBody()); + self::assertEquals("MyHtml", $message->getHTMLBody()); + self::assertEquals("2017-09-27 10:48:51", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", $message->from->first()->mail); + self::assertEquals("to@here.com", $message->to->first()->mail); + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/fixtures/StructuredWithAttachmentTest.php b/plugins/php-imap/tests/fixtures/StructuredWithAttachmentTest.php new file mode 100644 index 00000000..3fc591c3 --- /dev/null +++ b/plugins/php-imap/tests/fixtures/StructuredWithAttachmentTest.php @@ -0,0 +1,55 @@ +getFixture("structured_with_attachment.eml"); + + self::assertEquals("Test", $message->getSubject()); + self::assertEquals("Test", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + + self::assertEquals("2017-09-29 08:55:23", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", $message->from->first()->mail); + self::assertEquals("to@here.com", $message->to->first()->mail); + + self::assertCount(1, $message->attachments()); + + $attachment = $message->attachments()->first(); + self::assertInstanceOf(Attachment::class, $attachment); + self::assertEquals("MyFile.txt", $attachment->name); + self::assertEquals('txt', $attachment->getExtension()); + self::assertEquals('text', $attachment->type); + self::assertEquals("text/plain", $attachment->content_type); + self::assertEquals("MyFileContent", $attachment->content); + self::assertEquals(20, $attachment->size); + self::assertEquals(2, $attachment->part_number); + self::assertEquals("attachment", $attachment->disposition); + self::assertNotEmpty($attachment->id); + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/fixtures/UndefinedCharsetHeaderTest.php b/plugins/php-imap/tests/fixtures/UndefinedCharsetHeaderTest.php new file mode 100644 index 00000000..acb3029c --- /dev/null +++ b/plugins/php-imap/tests/fixtures/UndefinedCharsetHeaderTest.php @@ -0,0 +1,59 @@ +getFixture("undefined_charset_header.eml"); + + self::assertEquals("", $message->get("x-real-to")); + self::assertEquals("1.0", $message->get("mime-version")); + self::assertEquals("Mon, 27 Feb 2017 13:21:44 +0930", $message->get("Resent-Date")); + self::assertEquals("", $message->get("Resent-From")); + self::assertEquals("BlaBla", $message->get("X-Stored-In")); + self::assertEquals("", $message->get("Return-Path")); + self::assertEquals([ + 'from by bla.bla (CommuniGate Pro RULE 6.1.13) with RULE id 14057804; Mon, 27 Feb 2017 13:21:44 +0930', + 'from by bla.bla (CommuniGate Pro RULE 6.1.13) with RULE id 14057804' + ], $message->get("Received")->all()); + self::assertEquals(")", $message->getHTMLBody()); + self::assertFalse($message->hasTextBody()); + self::assertEquals("2017-02-27 03:51:29", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + + $from = $message->from->first(); + self::assertInstanceOf(Address::class, $from); + + self::assertEquals("myGov", $from->personal); + self::assertEquals("info", $from->mailbox); + self::assertEquals("bla.bla", $from->host); + self::assertEquals("info@bla.bla", $from->mail); + self::assertEquals("myGov ", $from->full); + + self::assertEquals("sales@bla.bla", $message->to->first()->mail); + self::assertEquals("Submit your tax refund | Australian Taxation Office.", $message->subject); + self::assertEquals("201702270351.BGF77614@bla.bla", $message->message_id); + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/fixtures/UndisclosedRecipientsMinusTest.php b/plugins/php-imap/tests/fixtures/UndisclosedRecipientsMinusTest.php new file mode 100644 index 00000000..f9ee7999 --- /dev/null +++ b/plugins/php-imap/tests/fixtures/UndisclosedRecipientsMinusTest.php @@ -0,0 +1,42 @@ +getFixture("undisclosed_recipients_minus.eml"); + + self::assertEquals("test", $message->subject); + self::assertEquals("Hi!", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + self::assertEquals("2017-09-27 10:48:51", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", $message->from); + self::assertEquals([ + "undisclosed-recipients", + "" + ], $message->to->map(function ($item) { + return $item->mailbox; + })); + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/fixtures/UndisclosedRecipientsSpaceTest.php b/plugins/php-imap/tests/fixtures/UndisclosedRecipientsSpaceTest.php new file mode 100644 index 00000000..b86321eb --- /dev/null +++ b/plugins/php-imap/tests/fixtures/UndisclosedRecipientsSpaceTest.php @@ -0,0 +1,42 @@ +getFixture("undisclosed_recipients_space.eml"); + + self::assertEquals("test", $message->subject); + self::assertEquals("Hi!", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + self::assertEquals("2017-09-27 10:48:51", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", $message->from); + self::assertEquals([ + "Undisclosed recipients", + "" + ], $message->to->map(function ($item) { + return $item->mailbox; + })); + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/fixtures/UndisclosedRecipientsTest.php b/plugins/php-imap/tests/fixtures/UndisclosedRecipientsTest.php new file mode 100644 index 00000000..f5f44164 --- /dev/null +++ b/plugins/php-imap/tests/fixtures/UndisclosedRecipientsTest.php @@ -0,0 +1,42 @@ +getFixture("undisclosed_recipients.eml"); + + self::assertEquals("test", $message->subject); + self::assertEquals("Hi!", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + self::assertEquals("2017-09-27 10:48:51", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", $message->from); + self::assertEquals([ + "Undisclosed Recipients", + "" + ], $message->to->map(function ($item) { + return $item->mailbox; + })); + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/fixtures/UnknownEncodingTest.php b/plugins/php-imap/tests/fixtures/UnknownEncodingTest.php new file mode 100644 index 00000000..3a570cbd --- /dev/null +++ b/plugins/php-imap/tests/fixtures/UnknownEncodingTest.php @@ -0,0 +1,37 @@ +getFixture("unknown_encoding.eml"); + + self::assertEquals("test", $message->getSubject()); + self::assertEquals("MyPlain", $message->getTextBody()); + self::assertEquals("MyHtml", $message->getHTMLBody()); + self::assertEquals("2017-09-27 10:48:51", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", $message->from->first()->mail); + self::assertEquals("to@here.com", $message->to->first()->mail); + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/fixtures/WithoutCharsetPlainOnlyTest.php b/plugins/php-imap/tests/fixtures/WithoutCharsetPlainOnlyTest.php new file mode 100644 index 00000000..497334d5 --- /dev/null +++ b/plugins/php-imap/tests/fixtures/WithoutCharsetPlainOnlyTest.php @@ -0,0 +1,37 @@ +getFixture("without_charset_plain_only.eml"); + + self::assertEquals("Nuu", $message->getSubject()); + self::assertEquals("Hi", $message->getTextBody()); + self::assertFalse($message->hasHTMLBody()); + self::assertEquals("2017-09-13 11:05:45", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", $message->from->first()->mail); + self::assertEquals("to@here.com", $message->to->first()->mail); + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/fixtures/WithoutCharsetSimpleMultipartTest.php b/plugins/php-imap/tests/fixtures/WithoutCharsetSimpleMultipartTest.php new file mode 100644 index 00000000..2a9ea2a0 --- /dev/null +++ b/plugins/php-imap/tests/fixtures/WithoutCharsetSimpleMultipartTest.php @@ -0,0 +1,37 @@ +getFixture("without_charset_simple_multipart.eml"); + + self::assertEquals("test", $message->getSubject()); + self::assertEquals("MyPlain", $message->getTextBody()); + self::assertEquals("MyHtml", $message->getHTMLBody()); + self::assertEquals("2017-09-27 10:48:51", $message->date->first()->setTimezone('UTC')->format("Y-m-d H:i:s")); + self::assertEquals("from@there.com", $message->from->first()->mail); + self::assertEquals("to@here.com", $message->to->first()->mail); + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/issues/Issue275Test.php b/plugins/php-imap/tests/issues/Issue275Test.php new file mode 100644 index 00000000..4049998d --- /dev/null +++ b/plugins/php-imap/tests/issues/Issue275Test.php @@ -0,0 +1,38 @@ +subject); + self::assertSame("Asdf testing123 this is a body", $message->getTextBody()); + } + + public function testIssueEmail2() { + $filename = implode(DIRECTORY_SEPARATOR, [__DIR__, "..", "messages", "issue-275-2.eml"]); + $message = Message::fromFile($filename); + + $body = "Test\r\n\r\nMed venlig hilsen\r\nMartin Larsen\r\nFeline Holidays A/S\r\nTlf 78 77 04 12"; + + self::assertSame("Test 1017", (string)$message->subject); + self::assertSame($body, $message->getTextBody()); + } + +} \ No newline at end of file diff --git a/plugins/php-imap/tests/issues/Issue355Test.php b/plugins/php-imap/tests/issues/Issue355Test.php new file mode 100644 index 00000000..0fa6d07e --- /dev/null +++ b/plugins/php-imap/tests/issues/Issue355Test.php @@ -0,0 +1,30 @@ +get("subject"); + + $this->assertEquals("Re: Uppdaterat ärende (447899), kostnader för hjälp med stadgeändring enligt ny lagstiftning", $subject->toString()); + } + +} \ No newline at end of file diff --git a/plugins/php-imap/tests/issues/Issue379Test.php b/plugins/php-imap/tests/issues/Issue379Test.php new file mode 100644 index 00000000..99b67921 --- /dev/null +++ b/plugins/php-imap/tests/issues/Issue379Test.php @@ -0,0 +1,61 @@ +getFolder('INBOX'); + + $message = $this->appendMessageTemplate($folder, "plain.eml"); + $this->assertEquals(214, $message->getSize()); + + // Clean up + $this->assertTrue($message->delete(true)); + } + +} \ No newline at end of file diff --git a/plugins/php-imap/tests/issues/Issue382Test.php b/plugins/php-imap/tests/issues/Issue382Test.php new file mode 100644 index 00000000..4e638827 --- /dev/null +++ b/plugins/php-imap/tests/issues/Issue382Test.php @@ -0,0 +1,33 @@ +from->first(); + + self::assertSame("Mail Delivery System", $from->personal); + self::assertSame("MAILER-DAEMON", $from->mailbox); + self::assertSame("mta-09.someserver.com", $from->host); + self::assertSame("MAILER-DAEMON@mta-09.someserver.com", $from->mail); + self::assertSame("Mail Delivery System ", $from->full); + } + +} \ No newline at end of file diff --git a/plugins/php-imap/tests/issues/Issue383Test.php b/plugins/php-imap/tests/issues/Issue383Test.php new file mode 100644 index 00000000..30cb3a9b --- /dev/null +++ b/plugins/php-imap/tests/issues/Issue383Test.php @@ -0,0 +1,69 @@ +getClient(); + $client->connect(); + + $delimiter = $this->getManager()->getConfig()->get("options.delimiter"); + $folder_path = implode($delimiter, ['INBOX', 'Entwürfe+']); + + $folder = $client->getFolder($folder_path); + $this->deleteFolder($folder); + + $folder = $client->createFolder($folder_path, false); + self::assertInstanceOf(Folder::class, $folder); + + $folder = $this->getFolder($folder_path); + self::assertInstanceOf(Folder::class, $folder); + + $this->assertEquals('Entwürfe+', $folder->name); + $this->assertEquals($folder_path, $folder->full_name); + + $folder_path = implode($delimiter, ['INBOX', 'Entw&APw-rfe+']); + $this->assertEquals($folder_path, $folder->path); + + // Clean up + if ($this->deleteFolder($folder) === false) { + $this->fail("Could not delete folder: " . $folder->path); + } + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/issues/Issue393Test.php b/plugins/php-imap/tests/issues/Issue393Test.php new file mode 100644 index 00000000..017ff535 --- /dev/null +++ b/plugins/php-imap/tests/issues/Issue393Test.php @@ -0,0 +1,62 @@ +getClient(); + $client->connect(); + + $delimiter = $this->getManager()->getConfig()->get("options.delimiter"); + $pattern = implode($delimiter, ['doesnt_exist', '%']); + + $folder = $client->getFolder('doesnt_exist'); + $this->deleteFolder($folder); + + $folders = $client->getFolders(true, $pattern, true); + self::assertCount(0, $folders); + + try { + $client->getFolders(true, $pattern, false); + $this->fail('Expected FolderFetchingException::class exception not thrown'); + } catch (FolderFetchingException $e) { + self::assertInstanceOf(FolderFetchingException::class, $e); + } + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/issues/Issue401Test.php b/plugins/php-imap/tests/issues/Issue401Test.php new file mode 100644 index 00000000..e250a550 --- /dev/null +++ b/plugins/php-imap/tests/issues/Issue401Test.php @@ -0,0 +1,27 @@ +subject); + } + +} \ No newline at end of file diff --git a/plugins/php-imap/tests/issues/Issue407Test.php b/plugins/php-imap/tests/issues/Issue407Test.php new file mode 100644 index 00000000..4adcbb39 --- /dev/null +++ b/plugins/php-imap/tests/issues/Issue407Test.php @@ -0,0 +1,57 @@ +getFolder('INBOX'); + self::assertInstanceOf(Folder::class, $folder); + + $message = $this->appendMessageTemplate($folder, "plain.eml"); + self::assertInstanceOf(Message::class, $message); + + $message->setFlag("Seen"); + + $flags = $this->getClient()->getConnection()->flags($message->uid, IMAP::ST_UID)->validatedData(); + + self::assertIsArray($flags); + self::assertSame(1, count($flags)); + self::assertSame("\\Seen", $flags[$message->uid][0]); + + $message->delete(); + } + +} \ No newline at end of file diff --git a/plugins/php-imap/tests/issues/Issue410Test.php b/plugins/php-imap/tests/issues/Issue410Test.php new file mode 100644 index 00000000..d02724ca --- /dev/null +++ b/plugins/php-imap/tests/issues/Issue410Test.php @@ -0,0 +1,52 @@ +subject); + + $attachments = $message->getAttachments(); + + self::assertSame(1, $attachments->count()); + + $attachment = $attachments->first(); + self::assertSame("☆第132号 「ガーデン&エクステリア」専門店のためのQ&Aサロン 【月刊エクステリア・ワーク】", $attachment->filename); + self::assertSame("☆第132号 「ガーデン&エクステリア」専門店のためのQ&Aサロン 【月刊エクステリア・ワーク】", $attachment->name); + } + + public function testIssueEmailB() { + $filename = implode(DIRECTORY_SEPARATOR, [__DIR__, "..", "messages", "issue-410b.eml"]); + $message = Message::fromFile($filename); + + self::assertSame("386 - 400021804 - 19., Heiligenstädter Straße 80 - 0819306 - Anfrage Vergabevorschlag", (string)$message->subject); + + $attachments = $message->getAttachments(); + + self::assertSame(1, $attachments->count()); + + $attachment = $attachments->first(); + self::assertSame("2021_Mängelliste_0819306.xlsx", $attachment->description); + self::assertSame("2021_Mängelliste_0819306.xlsx", $attachment->filename); + self::assertSame("2021_Mängelliste_0819306.xlsx", $attachment->name); + } + +} \ No newline at end of file diff --git a/plugins/php-imap/tests/issues/Issue412Test.php b/plugins/php-imap/tests/issues/Issue412Test.php new file mode 100644 index 00000000..bfaa883d --- /dev/null +++ b/plugins/php-imap/tests/issues/Issue412Test.php @@ -0,0 +1,31 @@ +subject); + self::assertSame("64254d63e92a36ee02c760676351e60a", md5($message->getTextBody())); + self::assertSame("2e4de288f6a1ed658548ed11fcdb1d79", md5($message->getHTMLBody())); + self::assertSame(0, $message->attachments()->count()); + } + +} \ No newline at end of file diff --git a/plugins/php-imap/tests/issues/Issue413Test.php b/plugins/php-imap/tests/issues/Issue413Test.php new file mode 100644 index 00000000..2162d6b5 --- /dev/null +++ b/plugins/php-imap/tests/issues/Issue413Test.php @@ -0,0 +1,82 @@ +getFolder('INBOX'); + self::assertInstanceOf(Folder::class, $folder); + + /** @var Message $message */ + $_message = $this->appendMessageTemplate($folder, 'issue-413.eml'); + + $message = $folder->messages()->getMessageByMsgn($_message->msgn); + self::assertEquals($message->uid, $_message->uid); + + self::assertSame("Test Message", (string)$message->subject); + self::assertSame("This is just a test, so ignore it (if you can!)\r\n\r\nTony Marston", $message->getTextBody()); + + $message->delete(); + } + + /** + * Static parsing test + * + * @return void + * @throws \ReflectionException + * @throws \Webklex\PHPIMAP\Exceptions\AuthFailedException + * @throws \Webklex\PHPIMAP\Exceptions\ConnectionFailedException + * @throws \Webklex\PHPIMAP\Exceptions\ImapBadRequestException + * @throws \Webklex\PHPIMAP\Exceptions\ImapServerErrorException + * @throws \Webklex\PHPIMAP\Exceptions\InvalidMessageDateException + * @throws \Webklex\PHPIMAP\Exceptions\MaskNotFoundException + * @throws \Webklex\PHPIMAP\Exceptions\MessageContentFetchingException + * @throws \Webklex\PHPIMAP\Exceptions\ResponseException + * @throws \Webklex\PHPIMAP\Exceptions\RuntimeException + */ + public function testIssueEmail() { + $filename = implode(DIRECTORY_SEPARATOR, [__DIR__, "..", "messages", "issue-413.eml"]); + $message = Message::fromFile($filename); + + self::assertSame("Test Message", (string)$message->subject); + self::assertSame("This is just a test, so ignore it (if you can!)\r\n\r\nTony Marston", $message->getTextBody()); + } + +} \ No newline at end of file diff --git a/plugins/php-imap/tests/issues/Issue414Test.php b/plugins/php-imap/tests/issues/Issue414Test.php new file mode 100644 index 00000000..36a1d300 --- /dev/null +++ b/plugins/php-imap/tests/issues/Issue414Test.php @@ -0,0 +1,43 @@ +subject); + + $attachments = $message->getAttachments(); + + self::assertSame(2, $attachments->count()); + + $attachment = $attachments->first(); + self::assertEmpty($attachment->description); + self::assertSame("exampleMyFile.txt", $attachment->filename); + self::assertSame("exampleMyFile.txt", $attachment->name); + self::assertSame("be62f7e6", $attachment->id); + + $attachment = $attachments->last(); + self::assertEmpty($attachment->description); + self::assertSame("phpfoo", $attachment->filename); + self::assertSame("phpfoo", $attachment->name); + self::assertSame("12e1d38b", $attachment->hash); + } + +} \ No newline at end of file diff --git a/plugins/php-imap/tests/issues/Issue420Test.php b/plugins/php-imap/tests/issues/Issue420Test.php new file mode 100644 index 00000000..0cac9b6e --- /dev/null +++ b/plugins/php-imap/tests/issues/Issue420Test.php @@ -0,0 +1,31 @@ +get("subject"); + + // Ticket No: [��17] Mailbox Inbox - (17) Incoming failed messages + $this->assertEquals('Ticket No: [??17] Mailbox Inbox - (17) Incoming failed messages', utf8_decode($subject->toString())); + } + +} \ No newline at end of file diff --git a/plugins/php-imap/tests/live/ClientTest.php b/plugins/php-imap/tests/live/ClientTest.php new file mode 100644 index 00000000..1d224d57 --- /dev/null +++ b/plugins/php-imap/tests/live/ClientTest.php @@ -0,0 +1,318 @@ +getClient()->connect()); + } + + /** + * Test if the connection is working + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws MaskNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testIsConnected(): void { + $client = $this->getClient()->connect(); + + self::assertTrue($client->isConnected()); + } + + /** + * Test if the connection state can be determined + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws MaskNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testDisconnect(): void { + $client = $this->getClient()->connect(); + + self::assertFalse($client->disconnect()->isConnected()); + } + + /** + * Test to get the default inbox folder + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws MaskNotFoundException + * @throws ResponseException + * @throws RuntimeException + * @throws FolderFetchingException + */ + public function testGetFolder(): void { + $client = $this->getClient()->connect(); + + $folder = $client->getFolder('INBOX'); + self::assertInstanceOf(Folder::class, $folder); + } + + /** + * Test to get the default inbox folder by name + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws MaskNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetFolderByName(): void { + $client = $this->getClient()->connect(); + + $folder = $client->getFolderByName('INBOX'); + self::assertInstanceOf(Folder::class, $folder); + } + + /** + * Test to get the default inbox folder by path + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws MaskNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetFolderByPath(): void { + $client = $this->getClient()->connect(); + + $folder = $client->getFolderByPath('INBOX'); + self::assertInstanceOf(Folder::class, $folder); + } + + /** + * Test to get all folders + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws MaskNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetFolders(): void { + $client = $this->getClient()->connect(); + + $folders = $client->getFolders(false); + self::assertTrue($folders->count() > 0); + } + + public function testGetFoldersWithStatus(): void { + $client = $this->getClient()->connect(); + + $folders = $client->getFoldersWithStatus(false); + self::assertTrue($folders->count() > 0); + } + + public function testOpenFolder(): void { + $client = $this->getClient()->connect(); + + $status = $client->openFolder("INBOX"); + self::assertTrue(isset($status["flags"]) && count($status["flags"]) > 0); + self::assertTrue(($status["uidnext"] ?? 0) > 0); + self::assertTrue(($status["uidvalidity"] ?? 0) > 0); + self::assertTrue(($status["recent"] ?? -1) >= 0); + self::assertTrue(($status["exists"] ?? -1) >= 0); + } + + public function testCreateFolder(): void { + $client = $this->getClient()->connect(); + + $delimiter = $this->getManager()->getConfig()->get("options.delimiter"); + $folder_path = implode($delimiter, ['INBOX', $this->getSpecialChars()]); + + $folder = $client->getFolder($folder_path); + + $this->deleteFolder($folder); + + $folder = $client->createFolder($folder_path, false); + self::assertInstanceOf(Folder::class, $folder); + + $folder = $this->getFolder($folder_path); + self::assertInstanceOf(Folder::class, $folder); + + $this->assertEquals($this->getSpecialChars(), $folder->name); + $this->assertEquals($folder_path, $folder->full_name); + + $folder_path = implode($delimiter, ['INBOX', EncodingAliases::convert($this->getSpecialChars(), "utf-8", "utf7-imap")]); + $this->assertEquals($folder_path, $folder->path); + + // Clean up + if ($this->deleteFolder($folder) === false) { + $this->fail("Could not delete folder: " . $folder->path); + } + } + + public function testCheckFolder(): void { + $client = $this->getClient()->connect(); + + $status = $client->checkFolder("INBOX"); + self::assertTrue(isset($status["flags"]) && count($status["flags"]) > 0); + self::assertTrue(($status["uidnext"] ?? 0) > 0); + self::assertTrue(($status["uidvalidity"] ?? 0) > 0); + self::assertTrue(($status["recent"] ?? -1) >= 0); + self::assertTrue(($status["exists"] ?? -1) >= 0); + } + + public function testGetFolderPath(): void { + $client = $this->getClient()->connect(); + + self::assertIsArray($client->openFolder("INBOX")); + self::assertEquals("INBOX", $client->getFolderPath()); + } + + public function testId(): void { + $client = $this->getClient()->connect(); + + $info = $client->Id(); + self::assertIsArray($info); + $valid = false; + foreach ($info as $value) { + if (str_starts_with($value, "OK")) { + $valid = true; + break; + } + } + self::assertTrue($valid); + } + + public function testGetQuotaRoot(): void { + if (!getenv("LIVE_MAILBOX_QUOTA_SUPPORT")) { + $this->markTestSkipped("Quota support is not enabled"); + } + + $client = $this->getClient()->connect(); + + $quota = $client->getQuotaRoot("INBOX"); + self::assertIsArray($quota); + self::assertTrue(count($quota) > 1); + self::assertIsArray($quota[0]); + self::assertEquals("INBOX", $quota[0][1]); + self::assertIsArray($quota[1]); + self::assertIsArray($quota[1][2]); + self::assertTrue($quota[1][2][2] > 0); + } + + public function testSetTimeout(): void { + $client = $this->getClient()->connect(); + + self::assertInstanceOf(ProtocolInterface::class, $client->setTimeout(57)); + self::assertEquals(57, $client->getTimeout()); + } + + public function testExpunge(): void { + $client = $this->getClient()->connect(); + + $client->openFolder("INBOX"); + $status = $client->expunge(); + + self::assertIsArray($status); + self::assertIsArray($status[0]); + self::assertEquals("OK", $status[0][0]); + } + + public function testGetDefaultMessageMask(): void { + $client = $this->getClient(); + + self::assertEquals(MessageMask::class, $client->getDefaultMessageMask()); + } + + public function testGetDefaultEvents(): void { + $client = $this->getClient(); + + self::assertIsArray($client->getDefaultEvents("message")); + } + + public function testSetDefaultMessageMask(): void { + $client = $this->getClient(); + + self::assertInstanceOf(Client::class, $client->setDefaultMessageMask(AttachmentMask::class)); + self::assertEquals(AttachmentMask::class, $client->getDefaultMessageMask()); + + $client->setDefaultMessageMask(MessageMask::class); + } + + public function testGetDefaultAttachmentMask(): void { + $client = $this->getClient(); + + self::assertEquals(AttachmentMask::class, $client->getDefaultAttachmentMask()); + } + + public function testSetDefaultAttachmentMask(): void { + $client = $this->getClient(); + + self::assertInstanceOf(Client::class, $client->setDefaultAttachmentMask(MessageMask::class)); + self::assertEquals(MessageMask::class, $client->getDefaultAttachmentMask()); + + $client->setDefaultAttachmentMask(AttachmentMask::class); + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/live/FolderTest.php b/plugins/php-imap/tests/live/FolderTest.php new file mode 100644 index 00000000..178329cf --- /dev/null +++ b/plugins/php-imap/tests/live/FolderTest.php @@ -0,0 +1,447 @@ +getFolder('INBOX'); + self::assertInstanceOf(Folder::class, $folder); + + self::assertInstanceOf(WhereQuery::class, $folder->query()); + self::assertInstanceOf(WhereQuery::class, $folder->search()); + self::assertInstanceOf(WhereQuery::class, $folder->messages()); + } + + /** + * Test Folder::hasChildren() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws MaskNotFoundException + * @throws ResponseException + * @throws RuntimeException + * @throws EventNotFoundException + */ + public function testHasChildren(): void { + $folder = $this->getFolder('INBOX'); + self::assertInstanceOf(Folder::class, $folder); + + $delimiter = $this->getManager()->getConfig()->get("options.delimiter"); + $child_path = implode($delimiter, ['INBOX', 'test']); + if ($folder->getClient()->getFolder($child_path) === null) { + $folder->getClient()->createFolder($child_path, false); + $folder = $this->getFolder('INBOX'); + } + + self::assertTrue($folder->hasChildren()); + } + + /** + * Test Folder::setChildren() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws MaskNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testSetChildren(): void { + $folder = $this->getFolder('INBOX'); + self::assertInstanceOf(Folder::class, $folder); + + $delimiter = $this->getManager()->getConfig()->get("options.delimiter"); + $child_path = implode($delimiter, ['INBOX', 'test']); + if ($folder->getClient()->getFolder($child_path) === null) { + $folder->getClient()->createFolder($child_path, false); + $folder = $this->getFolder('INBOX'); + } + self::assertTrue($folder->hasChildren()); + + $folder->setChildren(new FolderCollection()); + self::assertTrue($folder->getChildren()->isEmpty()); + } + + /** + * Test Folder::getChildren() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws MaskNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetChildren(): void { + $folder = $this->getFolder('INBOX'); + self::assertInstanceOf(Folder::class, $folder); + + $delimiter = $this->getManager()->getConfig()->get("options.delimiter"); + $child_path = implode($delimiter, ['INBOX', 'test']); + if ($folder->getClient()->getFolder($child_path) === null) { + $folder->getClient()->createFolder($child_path, false); + } + + $folder = $folder->getClient()->getFolders()->where('name', 'INBOX')->first(); + self::assertInstanceOf(Folder::class, $folder); + + self::assertTrue($folder->hasChildren()); + self::assertFalse($folder->getChildren()->isEmpty()); + } + + /** + * Test Folder::move() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws MaskNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testMove(): void { + $client = $this->getClient(); + + $delimiter = $this->getManager()->getConfig()->get("options.delimiter"); + $folder_path = implode($delimiter, ['INBOX', 'test']); + + $folder = $client->getFolder($folder_path); + if ($folder === null) { + $folder = $client->createFolder($folder_path, false); + } + $new_folder_path = implode($delimiter, ['INBOX', 'other']); + $new_folder = $client->getFolder($new_folder_path); + $new_folder?->delete(false); + + $status = $folder->move($new_folder_path, false); + self::assertIsArray($status); + self::assertTrue(str_starts_with($status[0], 'OK')); + + $new_folder = $client->getFolder($new_folder_path); + self::assertEquals($new_folder_path, $new_folder->path); + self::assertEquals('other', $new_folder->name); + + if ($this->deleteFolder($new_folder) === false) { + $this->fail("Could not delete folder: " . $new_folder->path); + } + } + + /** + * Test Folder::delete() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws MaskNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testDelete(): void { + $client = $this->getClient(); + + $delimiter = $this->getManager()->getConfig()->get("options.delimiter"); + $folder_path = implode($delimiter, ['INBOX', 'test']); + + $folder = $client->getFolder($folder_path); + if ($folder === null) { + $folder = $client->createFolder($folder_path, false); + } + self::assertInstanceOf(Folder::class, $folder); + + if ($this->deleteFolder($folder) === false) { + $this->fail("Could not delete folder: " . $folder->path); + } + } + + /** + * Test Folder::overview() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws MaskNotFoundException + * @throws ResponseException + * @throws RuntimeException + * @throws InvalidMessageDateException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + */ + public function testOverview(): void { + $folder = $this->getFolder('INBOX'); + self::assertInstanceOf(Folder::class, $folder); + + $folder->select(); + + // Test empty overview + $overview = $folder->overview(); + self::assertIsArray($overview); + self::assertCount(0, $overview); + + $message = $this->appendMessageTemplate($folder, "plain.eml"); + + $overview = $folder->overview(); + + self::assertIsArray($overview); + self::assertCount(1, $overview); + + self::assertEquals($message->from->first()->full, end($overview)["from"]->toString()); + + self::assertTrue($message->delete()); + } + + /** + * Test Folder::appendMessage() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testAppendMessage(): void { + $folder = $this->getFolder('INBOX'); + self::assertInstanceOf(Folder::class, $folder); + + $message = $this->appendMessageTemplate($folder, "plain.eml"); + self::assertInstanceOf(Message::class, $message); + + self::assertEquals("Example", $message->subject); + self::assertEquals("to@someone-else.com", $message->to); + self::assertEquals("from@someone.com", $message->from); + + // Clean up + $this->assertTrue($message->delete(true)); + } + + /** + * Test Folder::subscribe() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws MaskNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testSubscribe(): void { + $folder = $this->getFolder('INBOX'); + self::assertInstanceOf(Folder::class, $folder); + + $status = $folder->subscribe(); + self::assertIsArray($status); + self::assertTrue(str_starts_with($status[0], 'OK')); + + // Clean up + $folder->unsubscribe(); + } + + /** + * Test Folder::unsubscribe() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws MaskNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testUnsubscribe(): void { + $folder = $this->getFolder('INBOX'); + self::assertInstanceOf(Folder::class, $folder); + + $folder->subscribe(); + + $status = $folder->subscribe(); + self::assertIsArray($status); + self::assertTrue(str_starts_with($status[0], 'OK')); + } + + /** + * Test Folder::status() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws MaskNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testStatus(): void { + $folder = $this->getFolder('INBOX'); + self::assertInstanceOf(Folder::class, $folder); + + $status = $folder->status(); + self::assertEquals(0, $status['messages']); + self::assertEquals(0, $status['recent']); + self::assertEquals(0, $status['unseen']); + self::assertGreaterThan(0, $status['uidnext']); + self::assertGreaterThan(0, $status['uidvalidity']); + } + + /** + * Test Folder::examine() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws MaskNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testExamine(): void { + $folder = $this->getFolder('INBOX'); + self::assertInstanceOf(Folder::class, $folder); + + $status = $folder->examine(); + self::assertTrue(isset($status["flags"]) && count($status["flags"]) > 0); + self::assertTrue(($status["uidnext"] ?? 0) > 0); + self::assertTrue(($status["uidvalidity"] ?? 0) > 0); + self::assertTrue(($status["recent"] ?? -1) >= 0); + self::assertTrue(($status["exists"] ?? -1) >= 0); + } + + /** + * Test Folder::getClient() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws MaskNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetClient(): void { + $folder = $this->getFolder('INBOX'); + self::assertInstanceOf(Folder::class, $folder); + self::assertInstanceOf(Client::class, $folder->getClient()); + } + + /** + * Test Folder::setDelimiter() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws MaskNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testSetDelimiter(): void { + $folder = $this->getFolder('INBOX'); + self::assertInstanceOf(Folder::class, $folder); + + $folder->setDelimiter("/"); + self::assertEquals("/", $folder->delimiter); + + $folder->setDelimiter("."); + self::assertEquals(".", $folder->delimiter); + + $default_delimiter = $this->getManager()->getConfig()->get("options.delimiter", "/"); + $folder->setDelimiter(null); + self::assertEquals($default_delimiter, $folder->delimiter); + } + +} \ No newline at end of file diff --git a/plugins/php-imap/tests/live/LegacyTest.php b/plugins/php-imap/tests/live/LegacyTest.php new file mode 100644 index 00000000..61e2ee09 --- /dev/null +++ b/plugins/php-imap/tests/live/LegacyTest.php @@ -0,0 +1,475 @@ +markTestSkipped("This test requires a live mailbox. Please set the LIVE_MAILBOX environment variable to run this test."); + } + + parent::__construct($name, $data, $dataName); + $manager = new ClientManager([ + 'options' => [ + "debug" => $_ENV["LIVE_MAILBOX_DEBUG"] ?? false, + ], + 'accounts' => [ + 'legacy' => [ + 'host' => getenv("LIVE_MAILBOX_HOST"), + 'port' => getenv("LIVE_MAILBOX_PORT"), + 'encryption' => getenv("LIVE_MAILBOX_ENCRYPTION"), + 'validate_cert' => getenv("LIVE_MAILBOX_VALIDATE_CERT"), + 'username' => getenv("LIVE_MAILBOX_USERNAME"), + 'password' => getenv("LIVE_MAILBOX_PASSWORD"), + 'protocol' => 'legacy-imap', + ], + ], + ]); + self::$client = $manager->account('legacy'); + self::$client->connect(); + self::assertInstanceOf(Client::class, self::$client->connect()); + } + + /** + * @throws RuntimeException + * @throws MessageFlagException + * @throws MessageContentFetchingException + * @throws ResponseException + * @throws ImapServerErrorException + * @throws MaskNotFoundException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ConnectionFailedException + * @throws InvalidMessageDateException + * @throws AuthFailedException + * @throws MessageHeaderFetchingException + */ + public function testSizes(): void { + + $delimiter = self::$client->getConfig()->get("options.delimiter"); + $child_path = implode($delimiter, ['INBOX', 'test']); + if (self::$client->getFolder($child_path) === null) { + self::$client->createFolder($child_path, false); + } + $folder = $this->getFolder($child_path); + + self::assertInstanceOf(Folder::class, $folder); + + $message = $this->appendMessageTemplate($folder, "plain.eml"); + self::assertInstanceOf(Message::class, $message); + + self::assertEquals(214, $message->size); + self::assertEquals(214, self::$client->getConnection()->sizes($message->uid)->array()[$message->uid]); + } + + /** + * Try to create a new query instance + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws MaskNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testQuery(): void { + $folder = $this->getFolder('INBOX'); + self::assertInstanceOf(Folder::class, $folder); + + self::assertInstanceOf(WhereQuery::class, $folder->query()); + self::assertInstanceOf(WhereQuery::class, $folder->search()); + self::assertInstanceOf(WhereQuery::class, $folder->messages()); + } + + /** + * Get a folder + * @param string $folder_path + * + * @return Folder + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws MaskNotFoundException + * @throws ResponseException + * @throws RuntimeException + * @throws FolderFetchingException + */ + final protected function getFolder(string $folder_path = "INDEX"): Folder { + $folder = self::$client->getFolderByPath($folder_path); + self::assertInstanceOf(Folder::class, $folder); + + return $folder; + } + + + /** + * Append a message to a folder + * @param Folder $folder + * @param string $message + * + * @return Message + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws ResponseException + * @throws RuntimeException + */ + final protected function appendMessage(Folder $folder, string $message): Message { + $status = $folder->select(); + if (!isset($status['uidnext'])) { + $this->fail("No UIDNEXT returned"); + } + + $response = $folder->appendMessage($message); + $valid_response = false; + foreach ($response as $line) { + if (str_starts_with($line, 'OK')) { + $valid_response = true; + break; + } + } + if (!$valid_response) { + $this->fail("Failed to append message: ".implode("\n", $response)); + } + + $message = $folder->messages()->getMessageByUid($status['uidnext']); + self::assertInstanceOf(Message::class, $message); + + return $message; + } + + /** + * Append a message template to a folder + * @param Folder $folder + * @param string $template + * + * @return Message + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws ResponseException + * @throws RuntimeException + */ + final protected function appendMessageTemplate(Folder $folder, string $template): Message { + $content = file_get_contents(implode(DIRECTORY_SEPARATOR, [__DIR__, "..", "messages", $template])); + return $this->appendMessage($folder, $content); + } + + /** + * Delete a folder if it is given + * @param Folder|null $folder + * + * @return bool + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws ResponseException + * @throws RuntimeException + */ + final protected function deleteFolder(Folder $folder = null): bool { + $response = $folder?->delete(false); + if (is_array($response)) { + $valid_response = false; + foreach ($response as $line) { + if (str_starts_with($line, 'OK')) { + $valid_response = true; + break; + } + } + if (!$valid_response) { + $this->fail("Failed to delete mailbox: ".implode("\n", $response)); + } + return $valid_response; + } + return false; + } + + /** + * Try to create a new query instance with a where clause + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws ResponseException + * @throws RuntimeException + * @throws GetMessagesFailedException + * @throws InvalidWhereQueryCriteriaException + * @throws MessageSearchValidationException + */ + public function testQueryWhere(): void { + $delimiter = self::$client->getConfig()->get("options.delimiter"); + $folder_path = implode($delimiter, ['INBOX', 'search']); + + $folder = self::$client->getFolder($folder_path); + if ($folder !== null) { + self::assertTrue($this->deleteFolder($folder)); + } + $folder = self::$client->createFolder($folder_path, false); + + $messages = [ + $this->appendMessageTemplate($folder, '1366671050@github.com.eml'), + $this->appendMessageTemplate($folder, 'attachment_encoded_filename.eml'), + $this->appendMessageTemplate($folder, 'attachment_long_filename.eml'), + $this->appendMessageTemplate($folder, 'attachment_no_disposition.eml'), + $this->appendMessageTemplate($folder, 'bcc.eml'), + $this->appendMessageTemplate($folder, 'boolean_decoded_content.eml'), + $this->appendMessageTemplate($folder, 'email_address.eml'), + $this->appendMessageTemplate($folder, 'embedded_email.eml'), + $this->appendMessageTemplate($folder, 'embedded_email_without_content_disposition.eml'), + $this->appendMessageTemplate($folder, 'embedded_email_without_content_disposition-embedded.eml'), + $this->appendMessageTemplate($folder, 'example_attachment.eml'), + $this->appendMessageTemplate($folder, 'example_bounce.eml'), + $this->appendMessageTemplate($folder, 'four_nested_emails.eml'), + $this->appendMessageTemplate($folder, 'gbk_charset.eml'), + $this->appendMessageTemplate($folder, 'html_only.eml'), + $this->appendMessageTemplate($folder, 'imap_mime_header_decode_returns_false.eml'), + $this->appendMessageTemplate($folder, 'inline_attachment.eml'), + $this->appendMessageTemplate($folder, 'issue-275.eml'), + $this->appendMessageTemplate($folder, 'issue-275-2.eml'), + $this->appendMessageTemplate($folder, 'issue-348.eml'), + $this->appendMessageTemplate($folder, 'ks_c_5601-1987_headers.eml'), + $this->appendMessageTemplate($folder, 'mail_that_is_attachment.eml'), + $this->appendMessageTemplate($folder, 'missing_date.eml'), + $this->appendMessageTemplate($folder, 'missing_from.eml'), + $this->appendMessageTemplate($folder, 'mixed_filename.eml'), + $this->appendMessageTemplate($folder, 'multipart_without_body.eml'), + $this->appendMessageTemplate($folder, 'multiple_html_parts_and_attachments.eml'), + $this->appendMessageTemplate($folder, 'multiple_nested_attachments.eml'), + $this->appendMessageTemplate($folder, 'nestes_embedded_with_attachment.eml'), + $this->appendMessageTemplate($folder, 'null_content_charset.eml'), + $this->appendMessageTemplate($folder, 'pec.eml'), + $this->appendMessageTemplate($folder, 'plain.eml'), + $this->appendMessageTemplate($folder, 'plain_only.eml'), + $this->appendMessageTemplate($folder, 'plain_text_attachment.eml'), + $this->appendMessageTemplate($folder, 'references.eml'), + $this->appendMessageTemplate($folder, 'simple_multipart.eml'), + $this->appendMessageTemplate($folder, 'structured_with_attachment.eml'), + $this->appendMessageTemplate($folder, 'thread_my_topic.eml'), + $this->appendMessageTemplate($folder, 'thread_re_my_topic.eml'), + $this->appendMessageTemplate($folder, 'thread_unrelated.eml'), + $this->appendMessageTemplate($folder, 'undefined_charset_header.eml'), + $this->appendMessageTemplate($folder, 'undisclosed_recipients_minus.eml'), + $this->appendMessageTemplate($folder, 'undisclosed_recipients_space.eml'), + $this->appendMessageTemplate($folder, 'unknown_encoding.eml'), + $this->appendMessageTemplate($folder, 'without_charset_plain_only.eml'), + $this->appendMessageTemplate($folder, 'without_charset_simple_multipart.eml'), + ]; + + $folder->getClient()->expunge(); + + $query = $folder->query()->all(); + self::assertEquals(count($messages), $query->count()); + + $query = $folder->query()->whereSubject("test"); + self::assertEquals(11, $query->count()); + + $query = $folder->query()->whereOn(Carbon::now()); + self::assertEquals(count($messages), $query->count()); + + self::assertTrue($this->deleteFolder($folder)); + } + + /** + * Test query where criteria + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidWhereQueryCriteriaException + * @throws MaskNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testQueryWhereCriteria(): void { + self::$client->reconnect(); + + $folder = $this->getFolder('INBOX'); + self::assertInstanceOf(Folder::class, $folder); + + $this->assertWhereSearchCriteria($folder, 'SUBJECT', 'Test'); + $this->assertWhereSearchCriteria($folder, 'BODY', 'Test'); + $this->assertWhereSearchCriteria($folder, 'TEXT', 'Test'); + $this->assertWhereSearchCriteria($folder, 'KEYWORD', 'Test'); + $this->assertWhereSearchCriteria($folder, 'UNKEYWORD', 'Test'); + $this->assertWhereSearchCriteria($folder, 'FLAGGED', 'Seen'); + $this->assertWhereSearchCriteria($folder, 'UNFLAGGED', 'Seen'); + $this->assertHeaderSearchCriteria($folder, 'Message-ID', 'Seen'); + $this->assertHeaderSearchCriteria($folder, 'In-Reply-To', 'Seen'); + $this->assertWhereSearchCriteria($folder, 'BCC', 'test@example.com'); + $this->assertWhereSearchCriteria($folder, 'CC', 'test@example.com'); + $this->assertWhereSearchCriteria($folder, 'FROM', 'test@example.com'); + $this->assertWhereSearchCriteria($folder, 'TO', 'test@example.com'); + $this->assertWhereSearchCriteria($folder, 'UID', '1'); + $this->assertWhereSearchCriteria($folder, 'UID', '1,2'); + $this->assertWhereSearchCriteria($folder, 'ALL'); + $this->assertWhereSearchCriteria($folder, 'NEW'); + $this->assertWhereSearchCriteria($folder, 'OLD'); + $this->assertWhereSearchCriteria($folder, 'SEEN'); + $this->assertWhereSearchCriteria($folder, 'UNSEEN'); + $this->assertWhereSearchCriteria($folder, 'RECENT'); + $this->assertWhereSearchCriteria($folder, 'ANSWERED'); + $this->assertWhereSearchCriteria($folder, 'UNANSWERED'); + $this->assertWhereSearchCriteria($folder, 'DELETED'); + $this->assertWhereSearchCriteria($folder, 'UNDELETED'); + $this->assertHeaderSearchCriteria($folder, 'Content-Language','en_US'); + $this->assertWhereSearchCriteria($folder, 'CUSTOM X-Spam-Flag NO'); + $this->assertWhereSearchCriteria($folder, 'CUSTOM X-Spam-Flag YES'); + $this->assertWhereSearchCriteria($folder, 'NOT'); + $this->assertWhereSearchCriteria($folder, 'OR'); + $this->assertWhereSearchCriteria($folder, 'AND'); + $this->assertWhereSearchCriteria($folder, 'BEFORE', '01-Jan-2020', true); + $this->assertWhereSearchCriteria($folder, 'BEFORE', Carbon::now()->subDays(1), true); + $this->assertWhereSearchCriteria($folder, 'ON', '01-Jan-2020', true); + $this->assertWhereSearchCriteria($folder, 'ON', Carbon::now()->subDays(1), true); + $this->assertWhereSearchCriteria($folder, 'SINCE', '01-Jan-2020', true); + $this->assertWhereSearchCriteria($folder, 'SINCE', Carbon::now()->subDays(1), true); + } + + /** + * Assert where search criteria + * @param Folder $folder + * @param string $criteria + * @param string|Carbon|null $value + * @param bool $date + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidWhereQueryCriteriaException + * @throws ResponseException + * @throws RuntimeException + */ + protected function assertWhereSearchCriteria(Folder $folder, string $criteria, Carbon|string $value = null, bool $date = false): void { + $query = $folder->query()->where($criteria, $value); + self::assertInstanceOf(WhereQuery::class, $query); + + $item = $query->getQuery()->first(); + $criteria = str_replace("CUSTOM ", "", $criteria); + $expected = $value === null ? [$criteria] : [$criteria, $value]; + if ($date === true && $value instanceof Carbon) { + $date_format = $folder->getClient()->getConfig()->get('date_format', 'd M y'); + $expected[1] = $value->format($date_format); + } + + self::assertIsArray($item); + self::assertIsString($item[0]); + if($value !== null) { + self::assertCount(2, $item); + self::assertIsString($item[1]); + }else{ + self::assertCount(1, $item); + } + self::assertSame($expected, $item); + } + + /** + * Assert header search criteria + * @param Folder $folder + * @param string $criteria + * @param mixed|null $value + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidWhereQueryCriteriaException + * @throws ResponseException + * @throws RuntimeException + */ + protected function assertHeaderSearchCriteria(Folder $folder, string $criteria, mixed $value = null): void { + $query = $folder->query()->whereHeader($criteria, $value); + self::assertInstanceOf(WhereQuery::class, $query); + + $item = $query->getQuery()->first(); + + self::assertIsArray($item); + self::assertIsString($item[0]); + self::assertCount(1, $item); + self::assertSame(['HEADER '.$criteria.' '.$value], $item); + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/live/LiveMailboxTestCase.php b/plugins/php-imap/tests/live/LiveMailboxTestCase.php new file mode 100644 index 00000000..c59e6773 --- /dev/null +++ b/plugins/php-imap/tests/live/LiveMailboxTestCase.php @@ -0,0 +1,220 @@ +-@#[]_ß_б_π_€_✔_你_يد_Z_'; + + /** + * Client manager + * @var ClientManager $manager + */ + protected static ClientManager $manager; + + /** + * Get the client manager + * + * @return ClientManager + */ + final protected function getManager(): ClientManager { + if (!isset(self::$manager)) { + self::$manager = new ClientManager([ + 'options' => [ + "debug" => $_ENV["LIVE_MAILBOX_DEBUG"] ?? false, + ], + 'accounts' => [ + 'default' => [ + 'host' => getenv("LIVE_MAILBOX_HOST"), + 'port' => getenv("LIVE_MAILBOX_PORT"), + 'encryption' => getenv("LIVE_MAILBOX_ENCRYPTION"), + 'validate_cert' => getenv("LIVE_MAILBOX_VALIDATE_CERT"), + 'username' => getenv("LIVE_MAILBOX_USERNAME"), + 'password' => getenv("LIVE_MAILBOX_PASSWORD"), + 'protocol' => 'imap', //might also use imap, [pop3 or nntp (untested)] + ], + ], + ]); + } + return self::$manager; + } + + /** + * Get the client + * + * @return Client + * @throws MaskNotFoundException + */ + final protected function getClient(): Client { + if (!getenv("LIVE_MAILBOX") ?? false) { + $this->markTestSkipped("This test requires a live mailbox. Please set the LIVE_MAILBOX environment variable to run this test."); + } + return $this->getManager()->account('default'); + } + + /** + * Get special chars + * + * @return string + */ + final protected function getSpecialChars(): string { + return self::SPECIAL_CHARS; + } + + /** + * Get a folder + * @param string $folder_path + * + * @return Folder + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws MaskNotFoundException + * @throws ResponseException + * @throws RuntimeException + * @throws FolderFetchingException + */ + final protected function getFolder(string $folder_path = "INDEX"): Folder { + $client = $this->getClient(); + self::assertInstanceOf(Client::class, $client->connect()); + + $folder = $client->getFolderByPath($folder_path); + self::assertInstanceOf(Folder::class, $folder); + + return $folder; + } + + /** + * Append a message to a folder + * @param Folder $folder + * @param string $message + * + * @return Message + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws ResponseException + * @throws RuntimeException + */ + final protected function appendMessage(Folder $folder, string $message): Message { + $status = $folder->select(); + if (!isset($status['uidnext'])) { + $this->fail("No UIDNEXT returned"); + } + + $response = $folder->appendMessage($message); + $valid_response = false; + foreach ($response as $line) { + if (str_starts_with($line, 'OK')) { + $valid_response = true; + break; + } + } + if (!$valid_response) { + $this->fail("Failed to append message: ".implode("\n", $response)); + } + + $message = $folder->messages()->getMessageByUid($status['uidnext']); + self::assertInstanceOf(Message::class, $message); + + return $message; + } + + /** + * Append a message template to a folder + * @param Folder $folder + * @param string $template + * + * @return Message + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws ResponseException + * @throws RuntimeException + */ + final protected function appendMessageTemplate(Folder $folder, string $template): Message { + $content = file_get_contents(implode(DIRECTORY_SEPARATOR, [__DIR__, "..", "messages", $template])); + return $this->appendMessage($folder, $content); + } + + /** + * Delete a folder if it is given + * @param Folder|null $folder + * + * @return bool + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws ResponseException + * @throws RuntimeException + */ + final protected function deleteFolder(Folder $folder = null): bool { + $response = $folder?->delete(false); + if (is_array($response)) { + $valid_response = false; + foreach ($response as $line) { + if (str_starts_with($line, 'OK')) { + $valid_response = true; + break; + } + } + if (!$valid_response) { + $this->fail("Failed to delete mailbox: ".implode("\n", $response)); + } + return $valid_response; + } + return false; + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/live/MessageTest.php b/plugins/php-imap/tests/live/MessageTest.php new file mode 100644 index 00000000..30e953ec --- /dev/null +++ b/plugins/php-imap/tests/live/MessageTest.php @@ -0,0 +1,2410 @@ +getFolder('INBOX'); + + $message = $this->appendMessageTemplate($folder, "plain.eml"); + self::assertInstanceOf(Message::class, $message); + + return $message; + } + + /** + * Test Message::convertEncoding() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws ResponseException + * @throws RuntimeException + * @throws MessageNotFoundException + */ + public function testConvertEncoding(): void { + $message = $this->getDefaultMessage(); + self::assertEquals("Entwürfe+", $message->convertEncoding("Entw&APw-rfe+", "UTF7-IMAP", "UTF-8")); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::thread() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + * @throws GetMessagesFailedException + */ + public function testThread(): void { + $client = $this->getClient(); + + $delimiter = $this->getManager()->getConfig()->get("options.delimiter"); + $folder_path = implode($delimiter, ['INBOX', 'thread']); + + $folder = $client->getFolder($folder_path); + if ($folder !== null) { + self::assertTrue($this->deleteFolder($folder)); + } + $folder = $client->createFolder($folder_path, false); + + $message1 = $this->appendMessageTemplate($folder, "thread_my_topic.eml"); + $message2 = $this->appendMessageTemplate($folder, "thread_re_my_topic.eml"); + $message3 = $this->appendMessageTemplate($folder, "thread_unrelated.eml"); + + $thread = $message1->thread($folder); + self::assertCount(2, $thread); + + $thread = $message2->thread($folder); + self::assertCount(2, $thread); + + $thread = $message3->thread($folder); + self::assertCount(1, $thread); + + // Cleanup + self::assertTrue($message1->delete()); + self::assertTrue($message2->delete()); + self::assertTrue($message3->delete()); + $client->expunge(); + + self::assertTrue($this->deleteFolder($folder)); + } + + /** + * Test Message::hasAttachments() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testHasAttachments(): void { + $message = $this->getDefaultMessage(); + self::assertFalse($message->hasAttachments()); + + $folder = $message->getFolder(); + self::assertInstanceOf(Folder::class, $folder); + self::assertTrue($message->delete()); + + $message = $this->appendMessageTemplate($folder, "example_attachment.eml"); + self::assertInstanceOf(Message::class, $message); + self::assertTrue($message->hasAttachments()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getFetchOptions() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetFetchOptions(): void { + $message = $this->getDefaultMessage(); + self::assertEquals(IMAP::FT_PEEK, $message->getFetchOptions()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getMessageId() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetMessageId(): void { + $folder = $this->getFolder('INBOX'); + $message = $this->appendMessageTemplate($folder, "example_attachment.eml"); + self::assertInstanceOf(Message::class, $message); + + self::assertEquals("d3a5e91963cb805cee975687d5acb1c6@swift.generated", $message->getMessageId()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getReplyTo() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetReplyTo(): void { + $folder = $this->getFolder('INBOX'); + $message = $this->appendMessageTemplate($folder, "example_attachment.eml"); + self::assertInstanceOf(Message::class, $message); + + self::assertEquals("testreply_to ", $message->getReplyTo()); + self::assertEquals("someone@domain.tld", $message->getReplyTo()->first()->mail); + self::assertEquals("testreply_to", $message->getReplyTo()->first()->personal); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::setSequence() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testSetSequence(): void { + $message = $this->getDefaultMessage(); + self::assertEquals($message->uid, $message->getSequenceId()); + + $message->setSequence(IMAP::ST_MSGN); + self::assertEquals($message->msgn, $message->getSequenceId()); + + $message->setSequence(null); + self::assertEquals($message->uid, $message->getSequenceId()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getEvent() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetEvent(): void { + $message = $this->getDefaultMessage(); + + $message->setEvent("message", "test", "test"); + self::assertEquals("test", $message->getEvent("message", "test")); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::__construct() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function test__construct(): void { + $message = $this->getDefaultMessage(); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::setFlag() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testSetFlag(): void { + $message = $this->getDefaultMessage(); + + self::assertTrue($message->setFlag("seen")); + self::assertTrue($message->getFlags()->has("seen")); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getMsgn() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetMsgn(): void { + $client = $this->getClient(); + + $delimiter = $this->getManager()->getConfig()->get("options.delimiter"); + $folder_path = implode($delimiter, ['INBOX', 'test']); + + $folder = $client->getFolder($folder_path); + if ($folder !== null) { + self::assertTrue($this->deleteFolder($folder)); + } + $folder = $client->createFolder($folder_path, false); + + $message = $this->appendMessageTemplate($folder, "plain.eml"); + self::assertInstanceOf(Message::class, $message); + + self::assertEquals(1, $message->getMsgn()); + + // Cleanup + self::assertTrue($message->delete()); + self::assertTrue($this->deleteFolder($folder)); + } + + /** + * Test Message::peek() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testPeek(): void { + $message = $this->getDefaultMessage(); + self::assertFalse($message->getFlags()->has("seen")); + self::assertEquals(IMAP::FT_PEEK, $message->getFetchOptions()); + $message->peek(); + self::assertFalse($message->getFlags()->has("seen")); + + $message->setFetchOption(IMAP::FT_UID); + self::assertEquals(IMAP::FT_UID, $message->getFetchOptions()); + $message->peek(); + self::assertTrue($message->getFlags()->has("seen")); + + // Cleanup + self::assertTrue($message->delete()); + + } + + /** + * Test Message::unsetFlag() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testUnsetFlag(): void { + $message = $this->getDefaultMessage(); + + self::assertFalse($message->getFlags()->has("seen")); + + self::assertTrue($message->setFlag("seen")); + self::assertTrue($message->getFlags()->has("seen")); + + self::assertTrue($message->unsetFlag("seen")); + self::assertFalse($message->getFlags()->has("seen")); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::setSequenceId() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testSetSequenceId(): void { + $message = $this->getDefaultMessage(); + self::assertEquals($message->uid, $message->getSequenceId()); + + $original_sequence = $message->getSequenceId(); + + $message->setSequenceId(1, IMAP::ST_MSGN); + self::assertEquals(1, $message->getSequenceId()); + + $message->setSequenceId(1); + self::assertEquals(1, $message->getSequenceId()); + + $message->setSequenceId($original_sequence); + + // Cleanup + self::assertTrue($message->delete()); + + } + + /** + * Test Message::getTo() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetTo(): void { + $message = $this->getDefaultMessage(); + $folder = $message->getFolder(); + self::assertInstanceOf(Folder::class, $folder); + + self::assertEquals("to@someone-else.com", $message->getTo()); + self::assertTrue($message->delete()); + + $message = $this->appendMessageTemplate($folder, "example_attachment.eml"); + self::assertInstanceOf(Message::class, $message); + + self::assertEquals("testnameto ", $message->getTo()); + self::assertEquals("testnameto", $message->getTo()->first()->personal); + self::assertEquals("someone@domain.tld", $message->getTo()->first()->mail); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::setUid() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testSetUid(): void { + $message = $this->getDefaultMessage(); + self::assertEquals($message->uid, $message->getSequenceId()); + + $original_sequence = $message->getSequenceId(); + + $message->setUid(789); + self::assertEquals(789, $message->uid); + + $message->setUid($original_sequence); + + // Cleanup + self::assertTrue($message->delete()); + + } + + /** + * Test Message::getUid() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetUid(): void { + $message = $this->getDefaultMessage(); + self::assertEquals($message->uid, $message->getSequenceId()); + + $original_sequence = $message->uid; + + $message->setUid(789); + self::assertEquals(789, $message->uid); + self::assertEquals(789, $message->getUid()); + + $message->setUid($original_sequence); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::hasTextBody() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testHasTextBody(): void { + $message = $this->getDefaultMessage(); + self::assertTrue($message->hasTextBody()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::__get() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function test__get(): void { + $message = $this->getDefaultMessage(); + self::assertEquals($message->uid, $message->getSequenceId()); + self::assertEquals("Example", $message->subject); + self::assertEquals("to@someone-else.com", $message->to); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getDate() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetDate(): void { + $message = $this->getDefaultMessage(); + self::assertInstanceOf(Carbon::class, $message->getDate()->toDate()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::setMask() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testSetMask(): void { + $message = $this->getDefaultMessage(); + self::assertEquals(MessageMask::class, $message->getMask()); + + $message->setMask(AttachmentMask::class); + self::assertEquals(AttachmentMask::class, $message->getMask()); + + $message->setMask(MessageMask::class); + self::assertEquals(MessageMask::class, $message->getMask()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getSequenceId() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetSequenceId(): void { + $message = $this->getDefaultMessage(); + self::assertEquals($message->uid, $message->getSequenceId()); + + $original_sequence = $message->getSequenceId(); + + $message->setSequenceId(789, IMAP::ST_MSGN); + self::assertEquals(789, $message->getSequenceId()); + + $message->setSequenceId(789); + self::assertEquals(789, $message->getSequenceId()); + + $message->setSequenceId($original_sequence); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::setConfig() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testSetConfig(): void { + $message = $this->getDefaultMessage(); + + $options = $message->getOptions(); + self::assertIsArray($options); + + $message->setOptions(["foo" => "bar"]); + self::assertArrayHasKey("foo", $message->getOptions()); + + $message->setOptions($options); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getEvents() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetEvents(): void { + $message = $this->getDefaultMessage(); + + $events = $message->getEvents(); + self::assertIsArray($events); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::setFetchOption() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testSetFetchOption(): void { + $message = $this->getDefaultMessage(); + + $fetch_option = $message->fetch_options; + + $message->setFetchOption(IMAP::FT_UID); + self::assertEquals(IMAP::FT_UID, $message->fetch_options); + + $message->setFetchOption(IMAP::FT_PEEK); + self::assertEquals(IMAP::FT_PEEK, $message->fetch_options); + + $message->setFetchOption(IMAP::FT_UID | IMAP::FT_PEEK); + self::assertEquals(IMAP::FT_UID | IMAP::FT_PEEK, $message->fetch_options); + + $message->setFetchOption($fetch_option); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getMsglist() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetMsglist(): void { + $message = $this->getDefaultMessage(); + + self::assertEquals(0, (int)$message->getMsglist()->toString()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::decodeString() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testDecodeString(): void { + $message = $this->getDefaultMessage(); + + $string = '

Test

'; + self::assertEquals('

Test

', $message->decodeString($string, IMAP::MESSAGE_ENC_QUOTED_PRINTABLE)); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::attachments() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testAttachments(): void { + $folder = $this->getFolder('INBOX'); + + $message = $this->appendMessageTemplate($folder, "example_attachment.eml"); + self::assertTrue($message->hasAttachments()); + self::assertSameSize([1], $message->attachments()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getMask() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetMask(): void { + $message = $this->getDefaultMessage(); + self::assertEquals(MessageMask::class, $message->getMask()); + + $message->setMask(AttachmentMask::class); + self::assertEquals(AttachmentMask::class, $message->getMask()); + + $message->setMask(MessageMask::class); + self::assertEquals(MessageMask::class, $message->getMask()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::hasHTMLBody() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testHasHTMLBody(): void { + $folder = $this->getFolder('INBOX'); + + $message = $this->appendMessageTemplate($folder, "1366671050@github.com.eml"); + self::assertTrue($message->hasHTMLBody()); + + // Cleanup + self::assertTrue($message->delete()); + + $message = $this->getDefaultMessage(); + self::assertFalse($message->hasHTMLBody()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::setEvents() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testSetEvents(): void { + $message = $this->getDefaultMessage(); + + $events = $message->getEvents(); + self::assertIsArray($events); + + $message->setEvents(["foo" => "bar"]); + self::assertArrayHasKey("foo", $message->getEvents()); + + $message->setEvents($events); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::__set() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function test__set(): void { + $message = $this->getDefaultMessage(); + + $message->foo = "bar"; + self::assertEquals("bar", $message->getFoo()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getHTMLBody() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetHTMLBody(): void { + $folder = $this->getFolder('INBOX'); + + $message = $this->appendMessageTemplate($folder, "1366671050@github.com.eml"); + self::assertTrue($message->hasHTMLBody()); + self::assertIsString($message->getHTMLBody()); + + // Cleanup + self::assertTrue($message->delete()); + + $message = $this->getDefaultMessage(); + self::assertFalse($message->hasHTMLBody()); + self::assertEmpty($message->getHTMLBody()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getSequence() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetSequence(): void { + $message = $this->getDefaultMessage(); + self::assertEquals(IMAP::ST_UID, $message->getSequence()); + + $original_sequence = $message->getSequence(); + + $message->setSequence(IMAP::ST_MSGN); + self::assertEquals(IMAP::ST_MSGN, $message->getSequence()); + + $message->setSequence($original_sequence); + self::assertEquals(IMAP::ST_UID, $message->getSequence()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::restore() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testRestore(): void { + $message = $this->getDefaultMessage(); + + $message->setFlag("deleted"); + self::assertTrue($message->hasFlag("deleted")); + + $message->restore(); + self::assertFalse($message->hasFlag("deleted")); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getPriority() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetPriority(): void { + $folder = $this->getFolder('INBOX'); + + $message = $this->appendMessageTemplate($folder, "example_attachment.eml"); + self::assertEquals(1, $message->getPriority()->first()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::setAttachments() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testSetAttachments(): void { + $message = $this->getDefaultMessage(); + + $message->setAttachments(new AttachmentCollection(["foo" => "bar"])); + self::assertIsArray($message->attachments()->toArray()); + self::assertTrue($message->attachments()->has("foo")); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getFrom() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetFrom(): void { + $message = $this->getDefaultMessage(); + self::assertEquals("from@someone.com", $message->getFrom()->first()->mail); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::setEvent() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testSetEvent(): void { + $message = $this->getDefaultMessage(); + + $message->setEvent("message", "bar", "foo"); + self::assertArrayHasKey("bar", $message->getEvents()["message"]); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getInReplyTo() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetInReplyTo(): void { + $message = $this->getDefaultMessage(); + self::assertEquals("", $message->getInReplyTo()); + + // Cleanup + self::assertTrue($message->delete()); + + $folder = $this->getFolder('INBOX'); + + $message = $this->appendMessageTemplate($folder, "1366671050@github.com.eml"); + self::assertEquals("Webklex/php-imap/issues/349@github.com", $message->getInReplyTo()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::copy() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testCopy(): void { + $message = $this->getDefaultMessage(); + $client = $message->getClient(); + self::assertInstanceOf(Client::class, $client); + + $delimiter = $this->getManager()->getConfig()->get("options.delimiter"); + $folder_path = implode($delimiter, ['INBOX', 'test']); + + $folder = $client->getFolder($folder_path); + if ($folder !== null) { + self::assertTrue($this->deleteFolder($folder)); + } + $folder = $client->createFolder($folder_path, false); + self::assertInstanceOf(Folder::class, $folder); + + $new_message = $message->copy($folder->path, true); + self::assertInstanceOf(Message::class, $new_message); + self::assertEquals($folder->path, $new_message->getFolder()->path); + + // Cleanup + self::assertTrue($message->delete()); + self::assertTrue($new_message->delete()); + + } + + /** + * Test Message::getBodies() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetBodies(): void { + $message = $this->getDefaultMessage(); + self::assertIsArray($message->getBodies()); + self::assertCount(1, $message->getBodies()); + + // Cleanup + self::assertTrue($message->delete()); + + } + + /** + * Test Message::getFlags() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetFlags(): void { + $message = $this->getDefaultMessage(); + self::assertIsArray($message->getFlags()->all()); + + self::assertFalse($message->hasFlag("seen")); + + self::assertTrue($message->setFlag("seen")); + self::assertTrue($message->getFlags()->has("seen")); + self::assertTrue($message->hasFlag("seen")); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::addFlag() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testAddFlag(): void { + $message = $this->getDefaultMessage(); + self::assertFalse($message->hasFlag("seen")); + + self::assertTrue($message->addFlag("seen")); + self::assertTrue($message->hasFlag("seen")); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getSubject() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetSubject(): void { + $message = $this->getDefaultMessage(); + self::assertEquals("Example", $message->getSubject()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getClient() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetClient(): void { + $message = $this->getDefaultMessage(); + self::assertInstanceOf(Client::class, $message->getClient()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::setFetchFlagsOption() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testSetFetchFlagsOption(): void { + $message = $this->getDefaultMessage(); + + self::assertTrue($message->getFetchFlagsOption()); + $message->setFetchFlagsOption(false); + self::assertFalse($message->getFetchFlagsOption()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::mask() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testMask(): void { + $message = $this->getDefaultMessage(); + self::assertInstanceOf(MessageMask::class, $message->mask()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::setMsglist() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testSetMsglist(): void { + $message = $this->getDefaultMessage(); + $message->setMsglist("foo"); + self::assertEquals("foo", $message->getMsglist()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::flags() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testFlags(): void { + $message = $this->getDefaultMessage(); + self::assertInstanceOf(FlagCollection::class, $message->flags()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getAttributes() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetAttributes(): void { + $message = $this->getDefaultMessage(); + self::assertIsArray($message->getAttributes()); + self::assertArrayHasKey("subject", $message->getAttributes()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getAttachments() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetAttachments(): void { + $message = $this->getDefaultMessage(); + self::assertInstanceOf(AttachmentCollection::class, $message->getAttachments()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getRawBody() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetRawBody(): void { + $message = $this->getDefaultMessage(); + self::assertIsString($message->getRawBody()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::is() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testIs(): void { + $message = $this->getDefaultMessage(); + self::assertTrue($message->is($message)); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::setFlags() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testSetFlags(): void { + $message = $this->getDefaultMessage(); + $message->setFlags(new FlagCollection()); + self::assertFalse($message->hasFlag("recent")); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::make() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws ResponseException + * @throws RuntimeException + * @throws ReflectionException + */ + public function testMake(): void { + $folder = $this->getFolder('INBOX'); + $folder->getClient()->openFolder($folder->path); + + $email = file_get_contents(implode(DIRECTORY_SEPARATOR, [__DIR__, "..", "messages", "1366671050@github.com.eml"])); + if(!str_contains($email, "\r\n")){ + $email = str_replace("\n", "\r\n", $email); + } + + $raw_header = substr($email, 0, strpos($email, "\r\n\r\n")); + $raw_body = substr($email, strlen($raw_header)+8); + + $message = Message::make(0, null, $folder->getClient(), $raw_header, $raw_body, [0 => "\\Seen"], IMAP::ST_UID); + self::assertInstanceOf(Message::class, $message); + } + + /** + * Test Message::setAvailableFlags() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testSetAvailableFlags(): void { + $message = $this->getDefaultMessage(); + + $message->setAvailableFlags(["foo"]); + self::assertSameSize(["foo"], $message->getAvailableFlags()); + self::assertEquals("foo", $message->getAvailableFlags()[0]); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getSender() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetSender(): void { + $folder = $this->getFolder('INBOX'); + + $message = $this->appendMessageTemplate($folder, "example_attachment.eml"); + self::assertEquals("testsender ", $message->getSender()); + self::assertEquals("testsender", $message->getSender()->first()->personal); + self::assertEquals("someone@domain.tld", $message->getSender()->first()->mail); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::fromFile() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws ReflectionException + * @throws ResponseException + * @throws RuntimeException + */ + public function testFromFile(): void { + $this->getManager(); + $filename = implode(DIRECTORY_SEPARATOR, [__DIR__, "..", "messages", "1366671050@github.com.eml"]); + $message = Message::fromFile($filename); + self::assertInstanceOf(Message::class, $message); + } + + /** + * Test Message::getStructure() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetStructure(): void { + $message = $this->getDefaultMessage(); + self::assertInstanceOf(Structure::class, $message->getStructure()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::get() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + * @throws MessageSizeFetchingException + */ + public function testGet(): void { + $message = $this->getDefaultMessage(); + self::assertEquals("Example", $message->get("subject")); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getSize() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetSize(): void { + $message = $this->getDefaultMessage(); + self::assertEquals(214, $message->getSize()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getHeader() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetHeader(): void { + $message = $this->getDefaultMessage(); + self::assertInstanceOf(Header::class, $message->getHeader()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getReferences() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetReferences(): void { + $folder = $this->getFolder('INBOX'); + + $message = $this->appendMessageTemplate($folder, "1366671050@github.com.eml"); + self::assertIsArray($message->getReferences()->all()); + self::assertEquals("Webklex/php-imap/issues/349@github.com", $message->getReferences()->first()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::setFolderPath() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testSetFolderPath(): void { + $message = $this->getDefaultMessage(); + + $folder_path = $message->getFolderPath(); + + $message->setFolderPath("foo"); + self::assertEquals("foo", $message->getFolderPath()); + + $message->setFolderPath($folder_path); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getTextBody() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetTextBody(): void { + $message = $this->getDefaultMessage(); + self::assertIsString($message->getTextBody()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::move() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testMove(): void { + $message = $this->getDefaultMessage(); + $client = $message->getClient(); + self::assertInstanceOf(Client::class, $client); + + $delimiter = $this->getManager()->getConfig()->get("options.delimiter"); + $folder_path = implode($delimiter, ['INBOX', 'test']); + + $folder = $client->getFolder($folder_path); + if ($folder !== null) { + self::assertTrue($this->deleteFolder($folder)); + } + $folder = $client->createFolder($folder_path, false); + self::assertInstanceOf(Folder::class, $folder); + + $message = $message->move($folder->path, true); + self::assertInstanceOf(Message::class, $message); + self::assertEquals($folder->path, $message->getFolder()->path); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getFolderPath() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetFolderPath(): void { + $message = $this->getDefaultMessage(); + self::assertEquals("INBOX", $message->getFolderPath()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getFolder() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetFolder(): void { + $message = $this->getDefaultMessage(); + self::assertInstanceOf(Folder::class, $message->getFolder()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getFetchBodyOption() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetFetchBodyOption(): void { + $message = $this->getDefaultMessage(); + self::assertTrue($message->getFetchBodyOption()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::setFetchBodyOption() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testSetFetchBodyOption(): void { + $message = $this->getDefaultMessage(); + + $message->setFetchBodyOption(false); + self::assertFalse($message->getFetchBodyOption()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::getFetchFlagsOption() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testGetFetchFlagsOption(): void { + $message = $this->getDefaultMessage(); + self::assertTrue($message->getFetchFlagsOption()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::__call() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function test__call(): void { + $message = $this->getDefaultMessage(); + self::assertEquals("Example", $message->getSubject()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::setClient() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testSetClient(): void { + $message = $this->getDefaultMessage(); + $client = $message->getClient(); + self::assertInstanceOf(Client::class, $client); + + $message->setClient(null); + self::assertNull($message->getClient()); + + $message->setClient($client); + self::assertInstanceOf(Client::class, $message->getClient()); + + // Cleanup + self::assertTrue($message->delete()); + } + + /** + * Test Message::setMsgn() + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws MessageNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testSetMsgn(): void { + $message = $this->getDefaultMessage(); + + $uid = $message->getUid(); + $message->setMsgn(789); + self::assertEquals(789, $message->getMsgn()); + $message->setUid($uid); + + // Cleanup + self::assertTrue($message->delete()); + } +} diff --git a/plugins/php-imap/tests/live/QueryTest.php b/plugins/php-imap/tests/live/QueryTest.php new file mode 100644 index 00000000..34cd77a4 --- /dev/null +++ b/plugins/php-imap/tests/live/QueryTest.php @@ -0,0 +1,283 @@ +getFolder('INBOX'); + self::assertInstanceOf(Folder::class, $folder); + + self::assertInstanceOf(WhereQuery::class, $folder->query()); + self::assertInstanceOf(WhereQuery::class, $folder->search()); + self::assertInstanceOf(WhereQuery::class, $folder->messages()); + } + + /** + * Try to create a new query instance with a where clause + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws EventNotFoundException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidMessageDateException + * @throws MaskNotFoundException + * @throws MessageContentFetchingException + * @throws MessageFlagException + * @throws MessageHeaderFetchingException + * @throws ResponseException + * @throws RuntimeException + * @throws GetMessagesFailedException + * @throws InvalidWhereQueryCriteriaException + * @throws MessageSearchValidationException + */ + public function testQueryWhere(): void { + $client = $this->getClient(); + + $delimiter = $this->getManager()->getConfig()->get("options.delimiter"); + $folder_path = implode($delimiter, ['INBOX', 'search']); + + $folder = $client->getFolder($folder_path); + if ($folder !== null) { + self::assertTrue($this->deleteFolder($folder)); + } + $folder = $client->createFolder($folder_path, false); + + $messages = [ + $this->appendMessageTemplate($folder, '1366671050@github.com.eml'), + $this->appendMessageTemplate($folder, 'attachment_encoded_filename.eml'), + $this->appendMessageTemplate($folder, 'attachment_long_filename.eml'), + $this->appendMessageTemplate($folder, 'attachment_no_disposition.eml'), + $this->appendMessageTemplate($folder, 'bcc.eml'), + $this->appendMessageTemplate($folder, 'boolean_decoded_content.eml'), + $this->appendMessageTemplate($folder, 'email_address.eml'), + $this->appendMessageTemplate($folder, 'embedded_email.eml'), + $this->appendMessageTemplate($folder, 'embedded_email_without_content_disposition.eml'), + $this->appendMessageTemplate($folder, 'embedded_email_without_content_disposition-embedded.eml'), + $this->appendMessageTemplate($folder, 'example_attachment.eml'), + $this->appendMessageTemplate($folder, 'example_bounce.eml'), + $this->appendMessageTemplate($folder, 'four_nested_emails.eml'), + $this->appendMessageTemplate($folder, 'gbk_charset.eml'), + $this->appendMessageTemplate($folder, 'html_only.eml'), + $this->appendMessageTemplate($folder, 'imap_mime_header_decode_returns_false.eml'), + $this->appendMessageTemplate($folder, 'inline_attachment.eml'), + $this->appendMessageTemplate($folder, 'issue-275.eml'), + $this->appendMessageTemplate($folder, 'issue-275-2.eml'), + $this->appendMessageTemplate($folder, 'issue-348.eml'), + $this->appendMessageTemplate($folder, 'ks_c_5601-1987_headers.eml'), + $this->appendMessageTemplate($folder, 'mail_that_is_attachment.eml'), + $this->appendMessageTemplate($folder, 'missing_date.eml'), + $this->appendMessageTemplate($folder, 'missing_from.eml'), + $this->appendMessageTemplate($folder, 'mixed_filename.eml'), + $this->appendMessageTemplate($folder, 'multipart_without_body.eml'), + $this->appendMessageTemplate($folder, 'multiple_html_parts_and_attachments.eml'), + $this->appendMessageTemplate($folder, 'multiple_nested_attachments.eml'), + $this->appendMessageTemplate($folder, 'nestes_embedded_with_attachment.eml'), + $this->appendMessageTemplate($folder, 'null_content_charset.eml'), + $this->appendMessageTemplate($folder, 'pec.eml'), + $this->appendMessageTemplate($folder, 'plain.eml'), + $this->appendMessageTemplate($folder, 'plain_only.eml'), + $this->appendMessageTemplate($folder, 'plain_text_attachment.eml'), + $this->appendMessageTemplate($folder, 'references.eml'), + $this->appendMessageTemplate($folder, 'simple_multipart.eml'), + $this->appendMessageTemplate($folder, 'structured_with_attachment.eml'), + $this->appendMessageTemplate($folder, 'thread_my_topic.eml'), + $this->appendMessageTemplate($folder, 'thread_re_my_topic.eml'), + $this->appendMessageTemplate($folder, 'thread_unrelated.eml'), + $this->appendMessageTemplate($folder, 'undefined_charset_header.eml'), + $this->appendMessageTemplate($folder, 'undisclosed_recipients_minus.eml'), + $this->appendMessageTemplate($folder, 'undisclosed_recipients_space.eml'), + $this->appendMessageTemplate($folder, 'unknown_encoding.eml'), + $this->appendMessageTemplate($folder, 'without_charset_plain_only.eml'), + $this->appendMessageTemplate($folder, 'without_charset_simple_multipart.eml'), + ]; + + $folder->getClient()->expunge(); + + $query = $folder->query()->all(); + self::assertEquals(count($messages), $query->count()); + + $query = $folder->query()->whereSubject("test"); + self::assertEquals(11, $query->count()); + + $query = $folder->query()->whereOn(Carbon::now()); + self::assertEquals(count($messages), $query->count()); + + self::assertTrue($this->deleteFolder($folder)); + } + + /** + * Test query where criteria + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws FolderFetchingException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidWhereQueryCriteriaException + * @throws MaskNotFoundException + * @throws ResponseException + * @throws RuntimeException + */ + public function testQueryWhereCriteria(): void { + $folder = $this->getFolder('INBOX'); + self::assertInstanceOf(Folder::class, $folder); + + $this->assertWhereSearchCriteria($folder, 'SUBJECT', 'Test'); + $this->assertWhereSearchCriteria($folder, 'BODY', 'Test'); + $this->assertWhereSearchCriteria($folder, 'TEXT', 'Test'); + $this->assertWhereSearchCriteria($folder, 'KEYWORD', 'Test'); + $this->assertWhereSearchCriteria($folder, 'UNKEYWORD', 'Test'); + $this->assertWhereSearchCriteria($folder, 'FLAGGED', 'Seen'); + $this->assertWhereSearchCriteria($folder, 'UNFLAGGED', 'Seen'); + $this->assertHeaderSearchCriteria($folder, 'Message-ID', 'Seen'); + $this->assertHeaderSearchCriteria($folder, 'In-Reply-To', 'Seen'); + $this->assertWhereSearchCriteria($folder, 'BCC', 'test@example.com'); + $this->assertWhereSearchCriteria($folder, 'CC', 'test@example.com'); + $this->assertWhereSearchCriteria($folder, 'FROM', 'test@example.com'); + $this->assertWhereSearchCriteria($folder, 'TO', 'test@example.com'); + $this->assertWhereSearchCriteria($folder, 'UID', '1'); + $this->assertWhereSearchCriteria($folder, 'UID', '1,2'); + $this->assertWhereSearchCriteria($folder, 'ALL'); + $this->assertWhereSearchCriteria($folder, 'NEW'); + $this->assertWhereSearchCriteria($folder, 'OLD'); + $this->assertWhereSearchCriteria($folder, 'SEEN'); + $this->assertWhereSearchCriteria($folder, 'UNSEEN'); + $this->assertWhereSearchCriteria($folder, 'RECENT'); + $this->assertWhereSearchCriteria($folder, 'ANSWERED'); + $this->assertWhereSearchCriteria($folder, 'UNANSWERED'); + $this->assertWhereSearchCriteria($folder, 'DELETED'); + $this->assertWhereSearchCriteria($folder, 'UNDELETED'); + $this->assertHeaderSearchCriteria($folder, 'Content-Language','en_US'); + $this->assertWhereSearchCriteria($folder, 'CUSTOM X-Spam-Flag NO'); + $this->assertWhereSearchCriteria($folder, 'CUSTOM X-Spam-Flag YES'); + $this->assertWhereSearchCriteria($folder, 'NOT'); + $this->assertWhereSearchCriteria($folder, 'OR'); + $this->assertWhereSearchCriteria($folder, 'AND'); + $this->assertWhereSearchCriteria($folder, 'BEFORE', '01-Jan-2020', true); + $this->assertWhereSearchCriteria($folder, 'BEFORE', Carbon::now()->subDays(1), true); + $this->assertWhereSearchCriteria($folder, 'ON', '01-Jan-2020', true); + $this->assertWhereSearchCriteria($folder, 'ON', Carbon::now()->subDays(1), true); + $this->assertWhereSearchCriteria($folder, 'SINCE', '01-Jan-2020', true); + $this->assertWhereSearchCriteria($folder, 'SINCE', Carbon::now()->subDays(1), true); + } + + /** + * Assert where search criteria + * @param Folder $folder + * @param string $criteria + * @param string|Carbon|null $value + * @param bool $date + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidWhereQueryCriteriaException + * @throws ResponseException + * @throws RuntimeException + */ + protected function assertWhereSearchCriteria(Folder $folder, string $criteria, Carbon|string $value = null, bool $date = false): void { + $query = $folder->query()->where($criteria, $value); + self::assertInstanceOf(WhereQuery::class, $query); + + $item = $query->getQuery()->first(); + $criteria = str_replace("CUSTOM ", "", $criteria); + $expected = $value === null ? [$criteria] : [$criteria, $value]; + if ($date === true && $value instanceof Carbon) { + $date_format = $folder->getClient()->getConfig()->get('date_format', 'd M y'); + $expected[1] = $value->format($date_format); + } + + self::assertIsArray($item); + self::assertIsString($item[0]); + if($value !== null) { + self::assertCount(2, $item); + self::assertIsString($item[1]); + }else{ + self::assertCount(1, $item); + } + self::assertSame($expected, $item); + } + + /** + * Assert header search criteria + * @param Folder $folder + * @param string $criteria + * @param mixed|null $value + * + * @return void + * @throws AuthFailedException + * @throws ConnectionFailedException + * @throws ImapBadRequestException + * @throws ImapServerErrorException + * @throws InvalidWhereQueryCriteriaException + * @throws ResponseException + * @throws RuntimeException + */ + protected function assertHeaderSearchCriteria(Folder $folder, string $criteria, mixed $value = null): void { + $query = $folder->query()->whereHeader($criteria, $value); + self::assertInstanceOf(WhereQuery::class, $query); + + $item = $query->getQuery()->first(); + + self::assertIsArray($item); + self::assertIsString($item[0]); + self::assertCount(1, $item); + self::assertSame(['HEADER '.$criteria.' '.$value], $item); + } +} \ No newline at end of file diff --git a/plugins/php-imap/tests/messages/1366671050@github.com.eml b/plugins/php-imap/tests/messages/1366671050@github.com.eml new file mode 100644 index 00000000..91f51cf8 --- /dev/null +++ b/plugins/php-imap/tests/messages/1366671050@github.com.eml @@ -0,0 +1,108 @@ +Return-Path: +Delivered-To: someone@domain.tld +Received: from mx.domain.tld + by localhost with LMTP + id SABVMNfGqWP+PAAA0J78UA + (envelope-from ) + for ; Mon, 26 Dec 2022 17:07:51 +0100 +Received: from localhost (localhost [127.0.0.1]) + by mx.domain.tld (Postfix) with ESMTP id C3828140227 + for ; Mon, 26 Dec 2022 17:07:51 +0100 (CET) +X-Virus-Scanned: Debian amavisd-new at mx.domain.tld +X-Spam-Flag: NO +X-Spam-Score: -4.299 +X-Spam-Level: +X-Spam-Status: No, score=-4.299 required=6.31 tests=[BAYES_00=-1.9, + DKIMWL_WL_HIGH=-0.001, DKIM_SIGNED=0.1, DKIM_VALID=-0.1, + DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, HTML_IMAGE_ONLY_16=1.092, + HTML_MESSAGE=0.001, MAILING_LIST_MULTI=-1, RCVD_IN_DNSWL_MED=-2.3, + RCVD_IN_MSPIKE_H2=-0.001, T_KAM_HTML_FONT_INVALID=0.01] + autolearn=ham autolearn_force=no +Received: from mx.domain.tld ([127.0.0.1]) + by localhost (mx.domain.tld [127.0.0.1]) (amavisd-new, port 10024) + with ESMTP id JcIS9RuNBTNx for ; + Mon, 26 Dec 2022 17:07:21 +0100 (CET) +Received: from smtp.github.com (out-26.smtp.github.com [192.30.252.209]) + (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) + (No client certificate requested) + by mx.domain.tld (Postfix) with ESMTPS id 6410B13FEB2 + for ; Mon, 26 Dec 2022 17:07:21 +0100 (CET) +Received: from github-lowworker-891b8d2.va3-iad.github.net (github-lowworker-891b8d2.va3-iad.github.net [10.48.109.104]) + by smtp.github.com (Postfix) with ESMTP id 176985E0200 + for ; Mon, 26 Dec 2022 08:07:14 -0800 (PST) +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=github.com; + s=pf2014; t=1672070834; + bh=v91TPiLpM/cpUb4lgt2NMIUfM4HOIxCEWMR1+JTco+Q=; + h=Date:From:Reply-To:To:Cc:In-Reply-To:References:Subject:List-ID: + List-Archive:List-Post:List-Unsubscribe:From; + b=jW4Tac9IjWAPbEImyiYf1bzGLzY3ceohVbBg1V8BlpMTQ+o+yY3YB0eOe20hAsqZR + jrDjArx7rKQcslqBFL/b2B1C51rHuCBrz2cccLEERu9l/u0mTGCxTNtCRXHbCKbnR1 + VLWBeFLjATHth83kK6Kt7lkVuty+G3V1B6ZKPhCI= +Date: Mon, 26 Dec 2022 08:07:14 -0800 +From: Username +Reply-To: Webklex/php-imap +To: Webklex/php-imap +Cc: Subscribed +Message-ID: +In-Reply-To: +References: +Subject: Re: [Webklex/php-imap] Read all folders? (Issue #349) +Mime-Version: 1.0 +Content-Type: multipart/alternative; + boundary="--==_mimepart_63a9c6b293fe_65b5c71014155a"; + charset=UTF-8 +Content-Transfer-Encoding: 7bit +Precedence: list +X-GitHub-Sender: consigliere23 +X-GitHub-Recipient: Webklex +X-GitHub-Reason: subscribed +List-ID: Webklex/php-imap +List-Archive: https://github.com/Webklex/php-imap +List-Post: +List-Unsubscribe: , + +X-Auto-Response-Suppress: All +X-GitHub-Recipient-Address: someone@domain.tld + + +----==_mimepart_63a9c6b293fe_65b5c71014155a +Content-Type: text/plain; + charset=UTF-8 +Content-Transfer-Encoding: 7bit + +Any updates? + +-- +Reply to this email directly or view it on GitHub: +https://github.com/Webklex/php-imap/issues/349#issuecomment-1365266070 +You are receiving this because you are subscribed to this thread. + +Message ID: +----==_mimepart_63a9c6b293fe_65b5c71014155a +Content-Type: text/html; + charset=UTF-8 +Content-Transfer-Encoding: 7bit + +

+

Any updates?

+ +


Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.Message ID: <Webklex/php-imap/issues/349/1365266070@github.com>

+ +----==_mimepart_63a9c6b293fe_65b5c71014155a-- diff --git a/plugins/php-imap/tests/messages/attachment_encoded_filename.eml b/plugins/php-imap/tests/messages/attachment_encoded_filename.eml new file mode 100644 index 00000000..231d0452 --- /dev/null +++ b/plugins/php-imap/tests/messages/attachment_encoded_filename.eml @@ -0,0 +1,10 @@ +Content-Type: multipart/mixed; + boundary="BOUNDARY" + +--BOUNDARY +Content-Type: application/vnd.ms-excel; name="=?UTF-8?Q?Prost=C5=99eno=5F2014=5Fposledn=C3=AD_voln=C3=A9_term=C3=ADny.xls?="; charset="UTF-8" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename="=?UTF-8?Q?Prost=C5=99eno=5F2014=5Fposledn=C3=AD_voln=C3=A9_term=C3=ADny.xls?=" + +0M8R4KGxGuEAAAAAAAAAAAAAAAAAAAAAPgADAP7/CQAGAAAAAAAAAAAAAAACAAAAwgAAAAAA +AAAAEAAA/v///wAAAAD+////AAAAAMAAAADBAAAA//////////////////////////////// diff --git a/plugins/php-imap/tests/messages/attachment_long_filename.eml b/plugins/php-imap/tests/messages/attachment_long_filename.eml new file mode 100644 index 00000000..8630280f --- /dev/null +++ b/plugins/php-imap/tests/messages/attachment_long_filename.eml @@ -0,0 +1,55 @@ +Content-Type: multipart/mixed; + boundary="BOUNDARY" + +--BOUNDARY +Content-Type: text/plain; + name*0*=utf-8''Buchungsbest%C3%A4tigung-%20Rechnung-Gesch%C3%A4ftsbedingung; + name*1*=en-Nr.B123-45%20-%20XXXX%20xxxxxxxxxxxxxxxxx%20XxxX%2C%20L%C3%BCd; + name*2*=xxxxxxxx%20-%20VM%20Klaus%20XXXXXX%20-%20xxxxxxxx.pdf +Content-Disposition: attachment; + filename*0*=utf-8''Buchungsbest%C3%A4tigung-%20Rechnung-Gesch%C3%A4ftsbedin; + filename*1*=gungen-Nr.B123-45%20-%20XXXXX%20xxxxxxxxxxxxxxxxx%20XxxX%2C; + filename*2*=%20L%C3%BCxxxxxxxxxx%20-%20VM%20Klaus%20XXXXXX%20-%20xxxxxxxx.p; + filename*3*=df +Content-Transfer-Encoding: base64 + +SGkh +--BOUNDARY +Content-Type: text/plain; charset=UTF-8; + name="=?UTF-8?B?MDFfQeKCrMOgw6TEhdCx2YrYr0BaLTAxMjM0NTY3ODktcXdlcnR5dWlv?= + =?UTF-8?Q?pasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzx?= + =?UTF-8?Q?cvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstu?= + =?UTF-8?Q?vz.txt?=" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename*0*=iso-8859-15''%30%31%5F%41%A4%E0%E4%3F%3F%3F%3F%40%5A%2D%30%31; + filename*1*=%32%33%34%35%36%37%38%39%2D%71%77%65%72%74%79%75%69%6F%70%61; + filename*2*=%73%64%66%67%68%6A%6B%6C%7A%78%63%76%62%6E%6D%6F%70%71%72%73; + filename*3*=%74%75%76%7A%2D%30%31%32%33%34%35%36%37%38%39%2D%71%77%65%72; + filename*4*=%74%79%75%69%6F%70%61%73%64%66%67%68%6A%6B%6C%7A%78%63%76%62; + filename*5*=%6E%6D%6F%70%71%72%73%74%75%76%7A%2D%30%31%32%33%34%35%36%37; + filename*6*=%38%39%2D%71%77%65%72%74%79%75%69%6F%70%61%73%64%66%67%68%6A; + filename*7*=%6B%6C%7A%78%63%76%62%6E%6D%6F%70%71%72%73%74%75%76%7A%2E%74; + filename*8*=%78%74 + +SGkh +--BOUNDARY +Content-Type: text/plain; charset=UTF-8; + name="=?UTF-8?B?MDJfQeKCrMOgw6TEhdCx2YrYr0BaLTAxMjM0NTY3ODktcXdlcnR5dWlv?= + =?UTF-8?Q?pasdfghjklzxcvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzx?= + =?UTF-8?Q?cvbnmopqrstuvz-0123456789-qwertyuiopasdfghjklzxcvbnmopqrstu?= + =?UTF-8?Q?vz.txt?=" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename*0*=UTF-8''%30%32%5F%41%E2%82%AC%C3%A0%C3%A4%C4%85%D0%B1%D9%8A%D8; + filename*1*=%AF%40%5A%2D%30%31%32%33%34%35%36%37%38%39%2D%71%77%65%72%74; + filename*2*=%79%75%69%6F%70%61%73%64%66%67%68%6A%6B%6C%7A%78%63%76%62%6E; + filename*3*=%6D%6F%70%71%72%73%74%75%76%7A%2D%30%31%32%33%34%35%36%37%38; + filename*4*=%39%2D%71%77%65%72%74%79%75%69%6F%70%61%73%64%66%67%68%6A%6B; + filename*5*=%6C%7A%78%63%76%62%6E%6D%6F%70%71%72%73%74%75%76%7A%2D%30%31; + filename*6*=%32%33%34%35%36%37%38%39%2D%71%77%65%72%74%79%75%69%6F%70%61; + filename*7*=%73%64%66%67%68%6A%6B%6C%7A%78%63%76%62%6E%6D%6F%70%71%72%73; + filename*8*=%74%75%76%7A%2E%74%78%74 + +SGkh +--BOUNDARY-- diff --git a/plugins/php-imap/tests/messages/attachment_no_disposition.eml b/plugins/php-imap/tests/messages/attachment_no_disposition.eml new file mode 100644 index 00000000..ce0ea3e9 --- /dev/null +++ b/plugins/php-imap/tests/messages/attachment_no_disposition.eml @@ -0,0 +1,9 @@ +Content-Type: multipart/mixed; + boundary="BOUNDARY" + +--BOUNDARY +Content-Type: application/vnd.ms-excel; name="=?UTF-8?Q?Prost=C5=99eno=5F2014=5Fposledn=C3=AD_voln=C3=A9_term=C3=ADny.xls?="; charset="UTF-8" +Content-Transfer-Encoding: base64 + +0M8R4KGxGuEAAAAAAAAAAAAAAAAAAAAAPgADAP7/CQAGAAAAAAAAAAAAAAACAAAAwgAAAAAA +AAAAEAAA/v///wAAAAD+////AAAAAMAAAADBAAAA//////////////////////////////// diff --git a/plugins/php-imap/tests/messages/bcc.eml b/plugins/php-imap/tests/messages/bcc.eml new file mode 100644 index 00000000..1a6f16a8 --- /dev/null +++ b/plugins/php-imap/tests/messages/bcc.eml @@ -0,0 +1,12 @@ +Return-Path: +Subject: test +MIME-Version: 1.0 +Content-Type: text/plain +Date: Wed, 27 Sep 2017 12:48:51 +0200 +From: from@there.com +To: to@here.com +Bcc: =?UTF-8?B?QV/igqxAe8OoX1o=?= +Reply-To: reply-to@here.com +Sender: sender@here.com + +Hi! diff --git a/plugins/php-imap/tests/messages/boolean_decoded_content.eml b/plugins/php-imap/tests/messages/boolean_decoded_content.eml new file mode 100644 index 00000000..91818038 --- /dev/null +++ b/plugins/php-imap/tests/messages/boolean_decoded_content.eml @@ -0,0 +1,31 @@ +From: from@there.com +To: to@here.com +Subject: Nuu +Date: Wed, 13 Sep 2017 13:05:45 +0200 +Content-Type: multipart/mixed; boundary="=-vyqYb0SSRwuGFKv/Trdf" + +--=-vyqYb0SSRwuGFKv/Trdf +Content-Type: multipart/alternative; boundary="=-ewUvwipK68Y6itClYNpy" + +--=-ewUvwipK68Y6itClYNpy +Content-Type: text/plain; charset="us-ascii" + +Here is the problem mail + +Body text +--=-ewUvwipK68Y6itClYNpy +Content-Type: text/html; charset="us-ascii" + +Here is the problem mail + +Body text +--=-ewUvwipK68Y6itClYNpy-- + +--=-vyqYb0SSRwuGFKv/Trdf +Content-Type: application/pdf; name="Example Domain.pdf" +Content-Disposition: attachment; filename="Example Domain.pdf" +Content-Transfer-Encoding: base64 + +nnDusSNdG92w6Fuw61fMjAxOF8wMy0xMzMyNTMzMTkzLnBkZg==?= + +--=-vyqYb0SSRwuGFKv/Trdf-- diff --git a/plugins/php-imap/tests/messages/date-template.eml b/plugins/php-imap/tests/messages/date-template.eml new file mode 100644 index 00000000..9354fd10 --- /dev/null +++ b/plugins/php-imap/tests/messages/date-template.eml @@ -0,0 +1,8 @@ +Subject: test +MIME-Version: 1.0 +Content-Type: text/plain +Date: %date_raw_header% +From: from@there.com +To: to@here.com + +Hi! diff --git a/plugins/php-imap/tests/messages/email_address.eml b/plugins/php-imap/tests/messages/email_address.eml new file mode 100644 index 00000000..62b76d70 --- /dev/null +++ b/plugins/php-imap/tests/messages/email_address.eml @@ -0,0 +1,9 @@ +Message-ID: <123@example.com> +From: no_host +Cc: "This one: is \"right\"" , No-address +Content-Type: text/plain; + charset="us-ascii" +Content-Transfer-Encoding: quoted-printable + +Hi +How are you? diff --git a/plugins/php-imap/tests/messages/embedded_email.eml b/plugins/php-imap/tests/messages/embedded_email.eml new file mode 100644 index 00000000..293bc28e --- /dev/null +++ b/plugins/php-imap/tests/messages/embedded_email.eml @@ -0,0 +1,63 @@ +Return-Path: demo@cerstor.cz +Received: from webmail.my-office.cz (localhost [127.0.0.1]) + by keira.cofis.cz + ; Fri, 29 Jan 2016 14:25:40 +0100 +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="=_5d4b2de51e6aad0435b4bfbd7a23594a" +Date: Fri, 29 Jan 2016 14:25:40 +0100 +From: demo@cerstor.cz +To: demo@cerstor.cz +Subject: embedded message +Message-ID: <7e5798da5747415e5b82fdce042ab2a6@cerstor.cz> +X-Sender: demo@cerstor.cz +User-Agent: Roundcube Webmail/1.0.0 + +--=_5d4b2de51e6aad0435b4bfbd7a23594a +Content-Transfer-Encoding: 7bit +Content-Type: text/plain; charset=US-ASCII; + format=flowed + +email that contains embedded message +--=_5d4b2de51e6aad0435b4bfbd7a23594a +Content-Transfer-Encoding: 8bit +Content-Type: message/rfc822; + name=demo.eml +Content-Disposition: attachment; + filename=demo.eml; + size=889 + +Return-Path: demo@cerstor.cz +Received: from webmail.my-office.cz (localhost [127.0.0.1]) + by keira.cofis.cz + ; Fri, 29 Jan 2016 14:22:13 +0100 +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="=_995890bdbf8bd158f2cbae0e8d966000" +Date: Fri, 29 Jan 2016 14:22:13 +0100 +From: demo-from@cerstor.cz +To: demo-to@cerstor.cz +Subject: demo +Message-ID: <4cbaf57cb00891c53b32e1d63367740c@cerstor.cz> +X-Sender: demo@cerstor.cz +User-Agent: Roundcube Webmail/1.0.0 + +--=_995890bdbf8bd158f2cbae0e8d966000 +Content-Transfer-Encoding: 7bit +Content-Type: text/plain; charset=US-ASCII; + format=flowed + +demo text +--=_995890bdbf8bd158f2cbae0e8d966000 +Content-Transfer-Encoding: base64 +Content-Type: text/plain; + name=testfile.txt +Content-Disposition: attachment; + filename=testfile.txt; + size=29 + +IHRoaXMgaXMgY29udGVudCBvZiB0ZXN0IGZpbGU= +--=_995890bdbf8bd158f2cbae0e8d966000-- + + +--=_5d4b2de51e6aad0435b4bfbd7a23594a-- diff --git a/plugins/php-imap/tests/messages/embedded_email_without_content_disposition-embedded.eml b/plugins/php-imap/tests/messages/embedded_email_without_content_disposition-embedded.eml new file mode 100644 index 00000000..df5fa40b --- /dev/null +++ b/plugins/php-imap/tests/messages/embedded_email_without_content_disposition-embedded.eml @@ -0,0 +1,61 @@ +Received: from webmail.my-office.cz (localhost [127.0.0.1]) + by keira.cofis.cz + ; Fri, 29 Jan 2016 14:25:40 +0100 +From: demo@cerstor.cz +To: demo@cerstor.cz +Date: Fri, 5 Apr 2019 12:10:49 +0200 +Subject: embedded_message_subject +Message-ID: +Accept-Language: pl-PL, nl-NL +Content-Language: pl-PL +Content-Type: multipart/mixed; + boundary="_005_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_" +MIME-Version: 1.0 + +--_005_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_ +Content-Type: multipart/alternative; + boundary="_000_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_" + +--_000_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_ +Content-Type: text/plain; charset="iso-8859-2" +Content-Transfer-Encoding: quoted-printable + +some txt + + + + + +--_000_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_ +Content-Type: text/html; charset="iso-8859-2" +Content-Transfer-Encoding: quoted-printable + + +

some txt

+ + +--_000_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_-- + +--_005_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_ +Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; + name="file1.xlsx" +Content-Description: file1.xlsx +Content-Disposition: attachment; filename="file1.xlsx"; size=29; + creation-date="Fri, 05 Apr 2019 10:06:01 GMT"; + modification-date="Fri, 05 Apr 2019 10:10:49 GMT" +Content-Transfer-Encoding: base64 + +IHRoaXMgaXMgY29udGVudCBvZiB0ZXN0IGZpbGU= + +--_005_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_ +Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; + name="file2.xlsx" +Content-Description: file2 +Content-Disposition: attachment; filename="file2.xlsx"; size=29; + creation-date="Fri, 05 Apr 2019 10:10:19 GMT"; + modification-date="Wed, 03 Apr 2019 11:04:32 GMT" +Content-Transfer-Encoding: base64 + +IHRoaXMgaXMgY29udGVudCBvZiB0ZXN0IGZpbGU= + +--_005_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_-- diff --git a/plugins/php-imap/tests/messages/embedded_email_without_content_disposition.eml b/plugins/php-imap/tests/messages/embedded_email_without_content_disposition.eml new file mode 100644 index 00000000..c5c8cffe --- /dev/null +++ b/plugins/php-imap/tests/messages/embedded_email_without_content_disposition.eml @@ -0,0 +1,141 @@ +Return-Path: demo@cerstor.cz +Received: from webmail.my-office.cz (localhost [127.0.0.1]) + by keira.cofis.cz + ; Fri, 29 Jan 2016 14:25:40 +0100 +From: demo@cerstor.cz +To: demo@cerstor.cz +Date: Fri, 5 Apr 2019 13:48:50 +0200 +Subject: Subject +Message-ID: +Accept-Language: pl-PL, nl-NL +Content-Type: multipart/mixed; + boundary="_008_AC39946EBF5C034B87BABD5343E96979012671D9F7E4VM002emonsn_" +MIME-Version: 1.0 + +--_008_AC39946EBF5C034B87BABD5343E96979012671D9F7E4VM002emonsn_ +Content-Type: multipart/related; + boundary="_007_AC39946EBF5C034B87BABD5343E96979012671D9F7E4VM002emonsn_"; + type="multipart/alternative" + +--_007_AC39946EBF5C034B87BABD5343E96979012671D9F7E4VM002emonsn_ +Content-Type: multipart/alternative; + boundary="_000_AC39946EBF5C034B87BABD5343E96979012671D9F7E4VM002emonsn_" + +--_000_AC39946EBF5C034B87BABD5343E96979012671D9F7E4VM002emonsn_ +Content-Type: text/plain; charset="iso-8859-2" +Content-Transfer-Encoding: quoted-printable + +TexT + +[cid:file.jpg] + +--_000_AC39946EBF5C034B87BABD5343E96979012671D9F7E4VM002emonsn_ +Content-Type: text/html; charset="iso-8859-2" +Content-Transfer-Encoding: quoted-printable + +

TexT

= + +--_000_AC39946EBF5C034B87BABD5343E96979012671D9F7E4VM002emonsn_-- + +--_007_AC39946EBF5C034B87BABD5343E96979012671D9F7E4VM002emonsn_ +Content-Type: image/jpeg; name= + "file.jpg" +Content-Description: file.jpg +Content-Disposition: inline; filename= + "file.jpg"; + size=54558; creation-date="Fri, 05 Apr 2019 11:48:58 GMT"; + modification-date="Fri, 05 Apr 2019 11:48:58 GMT" +Content-ID: +Content-Transfer-Encoding: base64 + +iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg== + +--_007_AC39946EBF5C034B87BABD5343E96979012671D9F7E4VM002emonsn_-- + +--_008_AC39946EBF5C034B87BABD5343E96979012671D9F7E4VM002emonsn_ +Content-Type: message/rfc822 + +Received: from webmail.my-office.cz (localhost [127.0.0.1]) + by keira.cofis.cz + ; Fri, 29 Jan 2016 14:25:40 +0100 +From: demo@cerstor.cz +To: demo@cerstor.cz +Date: Fri, 5 Apr 2019 12:10:49 +0200 +Subject: embedded_message_subject +Message-ID: +Accept-Language: pl-PL, nl-NL +Content-Language: pl-PL +Content-Type: multipart/mixed; + boundary="_005_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_" +MIME-Version: 1.0 + +--_005_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_ +Content-Type: multipart/alternative; + boundary="_000_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_" + +--_000_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_ +Content-Type: text/plain; charset="iso-8859-2" +Content-Transfer-Encoding: quoted-printable + +some txt + + + + + +--_000_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_ +Content-Type: text/html; charset="iso-8859-2" +Content-Transfer-Encoding: quoted-printable + + +

some txt

+ + +--_000_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_-- + +--_005_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_ +Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; + name="file1.xlsx" +Content-Description: file1.xlsx +Content-Disposition: attachment; filename="file1.xlsx"; size=29; + creation-date="Fri, 05 Apr 2019 10:06:01 GMT"; + modification-date="Fri, 05 Apr 2019 10:10:49 GMT" +Content-Transfer-Encoding: base64 + +IHRoaXMgaXMgY29udGVudCBvZiB0ZXN0IGZpbGU= + +--_005_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_ +Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; + name="file2.xlsx" +Content-Description: file2 +Content-Disposition: attachment; filename="file2.xlsx"; size=29; + creation-date="Fri, 05 Apr 2019 10:10:19 GMT"; + modification-date="Wed, 03 Apr 2019 11:04:32 GMT" +Content-Transfer-Encoding: base64 + +IHRoaXMgaXMgY29udGVudCBvZiB0ZXN0IGZpbGU= + +--_005_AC39946EBF5C034B87BABD5343E96979012671D40E38VM002emonsn_-- + +--_008_AC39946EBF5C034B87BABD5343E96979012671D9F7E4VM002emonsn_ +Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; + name="file3.xlsx" +Content-Description: file3.xlsx +Content-Disposition: attachment; filename="file3.xlsx"; + size=90672; creation-date="Fri, 05 Apr 2019 11:46:30 GMT"; + modification-date="Thu, 28 Mar 2019 08:07:58 GMT" +Content-Transfer-Encoding: base64 + +IHRoaXMgaXMgY29udGVudCBvZiB0ZXN0IGZpbGU= + +--_008_AC39946EBF5C034B87BABD5343E96979012671D9F7E4VM002emonsn_ +Content-Type: application/x-zip-compressed; name="file4.zip" +Content-Description: file4.zip +Content-Disposition: attachment; filename="file4.zip"; size=29; + creation-date="Fri, 05 Apr 2019 11:48:45 GMT"; + modification-date="Fri, 05 Apr 2019 11:35:24 GMT" +Content-Transfer-Encoding: base64 + +IHRoaXMgaXMgY29udGVudCBvZiB0ZXN0IGZpbGU= + +--_008_AC39946EBF5C034B87BABD5343E96979012671D9F7E4VM002emonsn_-- diff --git a/plugins/php-imap/tests/messages/example_attachment.eml b/plugins/php-imap/tests/messages/example_attachment.eml new file mode 100644 index 00000000..a452ae79 --- /dev/null +++ b/plugins/php-imap/tests/messages/example_attachment.eml @@ -0,0 +1,56 @@ +Return-Path: +Delivered-To: +Received: from mx.domain.tld + by localhost (Dovecot) with LMTP id T7mwLn3ddlvKWwAA0J78UA + for ; Fri, 17 Aug 2018 16:36:45 +0200 +Received: from localhost (localhost [127.0.0.1]) + by mx.domain.tld (Postfix) with ESMTP id B642913BA0BE2 + for ; Fri, 17 Aug 2018 16:36:45 +0200 (CEST) +X-Virus-Scanned: Debian amavisd-new at mx.domain.tld +X-Spam-Flag: NO +X-Spam-Score: 1.103 +X-Spam-Level: * +X-Spam-Status: No, score=1.103 required=6.31 tests=[ALL_TRUSTED=-1, + OBFU_TEXT_ATTACH=1, TRACKER_ID=1.102, TVD_SPACE_RATIO=0.001] + autolearn=no autolearn_force=no +Received: from mx.domain.tld ([127.0.0.1]) + by localhost (mx.domain.tld [127.0.0.1]) (amavisd-new, port 10024) + with ESMTP id L8E9vyX80d44 for ; + Fri, 17 Aug 2018 16:36:39 +0200 (CEST) +Received: from [127.0.0.1] (ip.dynamic.domain.tld [192.168.0.24]) + (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) + (No client certificate requested) + by mx.domain.tld (Postfix) with ESMTPSA id EF01E13BA0BD7 + for ; Fri, 17 Aug 2018 16:36:38 +0200 (CEST) +Sender: testsender +Message-ID: +Date: Fri, 17 Aug 2018 14:36:24 +0000 +Subject: ogqMVHhz7swLaq2PfSWsZj0k99w8wtMbrb4RuHdNg53i76B7icIIM0zIWpwGFtnk +From: testfrom +Reply-To: testreply_to +To: testnameto +Cc: testnamecc +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="_=_swift_v4_1534516584_32c032a3715d2dfd5cd84c26f84dba8d_=_" +X-Priority: 1 (Highest) + + + + +--_=_swift_v4_1534516584_32c032a3715d2dfd5cd84c26f84dba8d_=_ +Content-Type: text/plain; charset=utf-8 +Content-Transfer-Encoding: quoted-printable + +n1IaXFkbeqKyg4lYToaJ3u1Ond2EDrN3UWuiLFNjOLJEAabSYagYQaOHtV5QDlZE + +--_=_swift_v4_1534516584_32c032a3715d2dfd5cd84c26f84dba8d_=_ +Content-Type: application/octet-stream; name=6mfFxiU5Yhv9WYJx.txt +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename=6mfFxiU5Yhv9WYJx.txt + +em5rNTUxTVAzVFAzV1BwOUtsMWduTEVycldFZ2tKRkF0dmFLcWtUZ3JrM2RLSThkWDM4WVQ4QmFW +eFJjT0VSTg== + +--_=_swift_v4_1534516584_32c032a3715d2dfd5cd84c26f84dba8d_=_-- + diff --git a/plugins/php-imap/tests/messages/example_bounce.eml b/plugins/php-imap/tests/messages/example_bounce.eml new file mode 100644 index 00000000..a1f5683f --- /dev/null +++ b/plugins/php-imap/tests/messages/example_bounce.eml @@ -0,0 +1,96 @@ +Return-Path: <> +Received: from somewhere.your-server.de + by somewhere.your-server.de with LMTP + id 3TP8LrElAGSOaAAAmBr1xw + (envelope-from <>); Thu, 02 Mar 2023 05:27:29 +0100 +Envelope-to: demo@foo.de +Delivery-date: Thu, 02 Mar 2023 05:27:29 +0100 +Authentication-Results: somewhere.your-server.de; + iprev=pass (somewhere06.your-server.de) smtp.remote-ip=1b21:2f8:e0a:50e4::2; + spf=none smtp.mailfrom=<>; + dmarc=skipped +Received: from somewhere06.your-server.de ([1b21:2f8:e0a:50e4::2]) + by somewhere.your-server.de with esmtps (TLS1.3) tls TLS_AES_256_GCM_SHA384 + (Exim 4.94.2) + id 1pXaXR-0006xQ-BN + for demo@foo.de; Thu, 02 Mar 2023 05:27:29 +0100 +Received: from [192.168.0.10] (helo=sslproxy01.your-server.de) + by somewhere06.your-server.de with esmtps (TLSv1.3:TLS_AES_256_GCM_SHA384:256) + (Exim 4.92) + id 1pXaXO-000LYP-9R + for demo@foo.de; Thu, 02 Mar 2023 05:27:26 +0100 +Received: from localhost ([127.0.0.1] helo=sslproxy01.your-server.de) + by sslproxy01.your-server.de with esmtps (TLSv1.3:TLS_AES_256_GCM_SHA384:256) + (Exim 4.92) + id 1pXaXO-0008gy-7x + for demo@foo.de; Thu, 02 Mar 2023 05:27:26 +0100 +Received: from Debian-exim by sslproxy01.your-server.de with local (Exim 4.92) + id 1pXaXO-0008gb-6g + for demo@foo.de; Thu, 02 Mar 2023 05:27:26 +0100 +X-Failed-Recipients: ding@ding.de +Auto-Submitted: auto-replied +From: Mail Delivery System +To: demo@foo.de +Content-Type: multipart/report; report-type=delivery-status; boundary=1677731246-eximdsn-678287796 +MIME-Version: 1.0 +Subject: Mail delivery failed +Message-Id: +Date: Thu, 02 Mar 2023 05:27:26 +0100 +X-Virus-Scanned: Clear (ClamAV 0.103.8/26827/Wed Mar 1 09:28:49 2023) +X-Spam-Score: 0.0 (/) +Delivered-To: bar-demo@foo.de + +--1677731246-eximdsn-678287796 +Content-type: text/plain; charset=us-ascii + +This message was created automatically by mail delivery software. + +A message sent by + + + +could not be delivered to one or more of its recipients. The following +address(es) failed: + + ding@ding.de + host 36.143.65.153 [36.143.65.153] + SMTP error from remote mail server after pipelined end of data: + 550-Verification failed for + 550-Unrouteable address + 550 Sender verify failed + +--1677731246-eximdsn-678287796 +Content-type: message/delivery-status + +Reporting-MTA: dns; sslproxy01.your-server.de + +Action: failed +Final-Recipient: rfc822;ding@ding.de +Status: 5.0.0 +Remote-MTA: dns; 36.143.65.153 +Diagnostic-Code: smtp; 550-Verification failed for + 550-Unrouteable address + 550 Sender verify failed + +--1677731246-eximdsn-678287796 +Content-type: message/rfc822 + +Return-path: +Received: from [31.18.107.47] (helo=127.0.0.1) + by sslproxy01.your-server.de with esmtpsa (TLSv1.3:TLS_AES_256_GCM_SHA384:256) + (Exim 4.92) + (envelope-from ) + id 1pXaXO-0008eK-11 + for ding@ding.de; Thu, 02 Mar 2023 05:27:26 +0100 +Date: Thu, 2 Mar 2023 05:27:25 +0100 +To: ding +From: =?iso-8859-1?Q?S=C3=BCderbar_Foo_=28SI=29_GmbH?= +Subject: Test +Message-ID: +X-Mailer: PHPMailer 6.7.1 (https://github.com/PHPMailer/PHPMailer) +Return-Path: bounce@foo.de +MIME-Version: 1.0 +Content-Type: text/html; charset=iso-8859-1 +X-Authenticated-Sender: demo@foo.de + +
Hallo, dies ist ein Beispiel-Text.
\ No newline at end of file diff --git a/plugins/php-imap/tests/messages/four_nested_emails.eml b/plugins/php-imap/tests/messages/four_nested_emails.eml new file mode 100644 index 00000000..0b34e986 --- /dev/null +++ b/plugins/php-imap/tests/messages/four_nested_emails.eml @@ -0,0 +1,69 @@ +To: test@example.com +From: test@example.com +Subject: 3-third-subject +Content-Type: multipart/mixed; + boundary="------------2E5D78A17C812FEFF825F7D5" + +This is a multi-part message in MIME format. +--------------2E5D78A17C812FEFF825F7D5 +Content-Type: text/plain; charset=iso-8859-15; format=flowed +Content-Transfer-Encoding: 7bit + +3-third-content +--------------2E5D78A17C812FEFF825F7D5 +Content-Type: message/rfc822; + name="2-second-email.eml" +Content-Transfer-Encoding: 7bit +Content-Disposition: attachment; + filename="2-second-email.eml" + +To: test@example.com +From: test@example.com +Subject: 2-second-subject +Content-Type: multipart/mixed; + boundary="------------9919377E37A03209B057D47F" + +This is a multi-part message in MIME format. +--------------9919377E37A03209B057D47F +Content-Type: text/plain; charset=iso-8859-15; format=flowed +Content-Transfer-Encoding: 7bit + +2-second-content +--------------9919377E37A03209B057D47F +Content-Type: message/rfc822; + name="1-first-email.eml" +Content-Transfer-Encoding: 7bit +Content-Disposition: attachment; + filename="1-first-email.eml" + +To: test@example.com +From: test@example.com +Subject: 1-first-subject +Content-Type: multipart/mixed; + boundary="------------0919377E37A03209B057D47A" + +This is a multi-part message in MIME format. +--------------0919377E37A03209B057D47A +Content-Type: text/plain; charset=iso-8859-15; format=flowed +Content-Transfer-Encoding: 7bit + +1-first-content +--------------0919377E37A03209B057D47A +Content-Type: message/rfc822; + name="0-zero-email.eml" +Content-Transfer-Encoding: 7bit +Content-Disposition: attachment; + filename="0-zero-email.eml" + +To: test@example.com +From: test@example.com +Subject: 0-zero-subject +Content-Type: text/plain; charset=iso-8859-15; format=flowed +Content-Transfer-Encoding: 7bit + +0-zero-content +--------------0919377E37A03209B057D47A-- + +--------------9919377E37A03209B057D47F-- + +--------------2E5D78A17C812FEFF825F7D5-- diff --git a/plugins/php-imap/tests/messages/gbk_charset.eml b/plugins/php-imap/tests/messages/gbk_charset.eml new file mode 100644 index 00000000..b40936bd --- /dev/null +++ b/plugins/php-imap/tests/messages/gbk_charset.eml @@ -0,0 +1,9 @@ +From: from@there.com +To: to@here.com +Subject: Nuu +Date: Wed, 13 Sep 2017 13:05:45 +0200 +Content-Type: text/plain; + charset="X-GBK" +Content-Transfer-Encoding: quoted-printable + +Hi diff --git a/plugins/php-imap/tests/messages/html_only.eml b/plugins/php-imap/tests/messages/html_only.eml new file mode 100644 index 00000000..ce3ee17c --- /dev/null +++ b/plugins/php-imap/tests/messages/html_only.eml @@ -0,0 +1,9 @@ +From: from@there.com +To: to@here.com +Subject: Nuu +Date: Wed, 13 Sep 2017 13:05:45 +0200 +Content-Type: text/html; + charset="us-ascii" +Content-Transfer-Encoding: quoted-printable + +Hi \ No newline at end of file diff --git a/plugins/php-imap/tests/messages/imap_mime_header_decode_returns_false.eml b/plugins/php-imap/tests/messages/imap_mime_header_decode_returns_false.eml new file mode 100644 index 00000000..af3a7f36 --- /dev/null +++ b/plugins/php-imap/tests/messages/imap_mime_header_decode_returns_false.eml @@ -0,0 +1,9 @@ +From: from@there.com +To: to@here.com +Subject: =?UTF-8?B?nnDusSNdG92w6Fuw61fMjAxOF8wMy0xMzMyNTMzMTkzLnBkZg==?= +Date: Wed, 13 Sep 2017 13:05:45 +0200 +Content-Type: text/plain; + charset="us-ascii" +Content-Transfer-Encoding: quoted-printable + +Hi diff --git a/plugins/php-imap/tests/messages/inline_attachment.eml b/plugins/php-imap/tests/messages/inline_attachment.eml new file mode 100644 index 00000000..be60f598 --- /dev/null +++ b/plugins/php-imap/tests/messages/inline_attachment.eml @@ -0,0 +1,41 @@ +Content-Type: multipart/mixed; + boundary="----=_Part_1114403_1160068121.1505882828080" + +------=_Part_1114403_1160068121.1505882828080 +Content-Type: multipart/related; + boundary="----=_Part_1114404_576719783.1505882828080" + +------=_Part_1114404_576719783.1505882828080 +Content-Type: text/html;charset=UTF-8 +Content-Transfer-Encoding: quoted-printable + + +------=_Part_1114404_576719783.1505882828080 +Content-Type: image/png +Content-Disposition: inline +Content-Transfer-Encoding: base64 +Content-ID: + +iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAAB+FBMVEUAAAA/mUPidDHiLi5Cn0Xk +NTPmeUrkdUg/m0Q0pEfcpSbwaVdKskg+lUP4zA/iLi3msSHkOjVAmETdJSjtYFE/lkPnRj3sWUs8 +kkLeqCVIq0fxvhXqUkbVmSjwa1n1yBLepyX1xxP0xRXqUkboST9KukpHpUbuvRrzrhF/ljbwalju +ZFM4jELaoSdLtElJrUj1xxP6zwzfqSU4i0HYnydMtUlIqUfywxb60AxZqEXaoifgMCXptR9MtklH +pEY2iUHWnSjvvRr70QujkC+pUC/90glMuEnlOjVMt0j70QriLS1LtEnnRj3qUUXfIidOjsxAhcZF +o0bjNDH0xxNLr0dIrUdmntVTkMoyfL8jcLBRuErhJyrgKyb4zA/5zg3tYFBBmUTmQTnhMinruBzv +vhnxwxZ/st+Ktt5zp9hqota2vtK6y9FemNBblc9HiMiTtMbFtsM6gcPV2r6dwroseLrMrbQrdLGd +yKoobKbo3Zh+ynrgVllZulTsXE3rV0pIqUf42UVUo0JyjEHoS0HmsiHRGR/lmRz/1hjqnxjvpRWf +wtOhusaz0LRGf7FEfbDVmqHXlJeW0pbXq5bec3fX0nTnzmuJuWvhoFFhm0FtrziBsjaAaDCYWC+u +Si6jQS3FsSfLJiTirCOkuCG1KiG+wSC+GBvgyhTszQ64Z77KAAAARXRSTlMAIQRDLyUgCwsE6ebm +5ubg2dLR0byXl4FDQzU1NDEuLSUgC+vr6urq6ubb29vb2tra2tG8vLu7u7uXl5eXgYGBgYGBLiUA +LabIAAABsElEQVQoz12S9VPjQBxHt8VaOA6HE+AOzv1wd7pJk5I2adpCC7RUcHd3d3fXf5PvLkxh +eD++z+yb7GSRlwD/+Hj/APQCZWxM5M+goF+RMbHK594v+tPoiN1uHxkt+xzt9+R9wnRTZZQpXQ0T +5uP1IQxToyOAZiQu5HEpjeA4SWIoksRxNiGC1tRZJ4LNxgHgnU5nJZBDvuDdl8lzQRBsQ+s9PZt7 +s7Pz8wsL39/DkIfZ4xlB2Gqsq62ta9oxVlVrNZpihFRpGO9fzQw1ms0NDWZz07iGkJmIFH8xxkc3 +a/WWlubmFkv9AB2SEpDvKxbjidN2faseaNV3zoHXvv7wMODJdkOHAegweAfFPx4G67KluxzottCU +9n8CUqXzcIQdXOytAHqXxomvykhEKN9EFutG22p//0rbNvHVxiJywa8yS2KDfV1dfbu31H8jF1RH +iTKtWYeHxUvq3bn0pyjCRaiRU6aDO+gb3aEfEeVNsDgm8zzLy9egPa7Qt8TSJdwhjplk06HH43ZN +J3s91KKCHQ5x4sw1fRGYDZ0n1L4FKb9/BP5JLYxToheoFCVxz57PPS8UhhEpLBVeAAAAAElFTkSu +QmCC +------=_Part_1114404_576719783.1505882828080-- + +------=_Part_1114403_1160068121.1505882828080-- diff --git a/plugins/php-imap/tests/messages/issue-275-2.eml b/plugins/php-imap/tests/messages/issue-275-2.eml new file mode 100644 index 00000000..d80e66ed --- /dev/null +++ b/plugins/php-imap/tests/messages/issue-275-2.eml @@ -0,0 +1,123 @@ +Received: from AS4PR02MB8234.eurprd02.prod.outlook.com (2603:10a6:20b:4f1::17) + by PA4PR02MB7071.eurprd02.prod.outlook.com with HTTPS; Wed, 18 Jan 2023 + 09:17:10 +0000 +Received: from AS1PR02MB7917.eurprd02.prod.outlook.com (2603:10a6:20b:4a5::5) + by AS4PR02MB8234.eurprd02.prod.outlook.com (2603:10a6:20b:4f1::17) with + Microsoft SMTP Server (version=TLS1_2, + cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.20.5986.19; Wed, 18 Jan + 2023 09:17:09 +0000 +Received: from AS1PR02MB7917.eurprd02.prod.outlook.com + ([fe80::4871:bdde:a499:c0d9]) by AS1PR02MB7917.eurprd02.prod.outlook.com + ([fe80::4871:bdde:a499:c0d9%9]) with mapi id 15.20.5986.023; Wed, 18 Jan 2023 + 09:17:09 +0000 +From: =?iso-8859-1?Q?Martin_B=FClow_Larsen?= +To: Cofman Drift +Subject: Test 1017 +Thread-Topic: Test 1017 +Thread-Index: AdkrHaKsy+bUiL/eTIS5W3AP+zzLjQ== +Date: Wed, 18 Jan 2023 09:17:09 +0000 +Message-ID: + +Accept-Language: da-DK, en-US +Content-Language: en-US +X-MS-Exchange-Organization-AuthAs: Internal +X-MS-Exchange-Organization-AuthMechanism: 04 +X-MS-Exchange-Organization-AuthSource: AS1PR02MB7917.eurprd02.prod.outlook.com +X-MS-Has-Attach: +X-MS-Exchange-Organization-Network-Message-Id: + 98cea1c9-a497-454a-6606-08daf934c6c4 +X-MS-Exchange-Organization-SCL: -1 +X-MS-TNEF-Correlator: +X-MS-Exchange-Organization-RecordReviewCfmType: 0 +x-ms-publictraffictype: Email +X-Microsoft-Antispam-Mailbox-Delivery: + ucf:0;jmr:0;auth:0;dest:I;ENG:(910001)(944506478)(944626604)(920097)(425001)(930097); +X-Microsoft-Antispam-Message-Info: + BzqL6hvPyQW0lSkWGop6vQlsIZK48umY74vuKlNgF0pb/H659W+0fuTB+6guqGM0oof00mlzu3gn1pu1R5pUOE2Fb58OqnBEFkB30vVrG6TNsG/6KBtecXkP3FptqO/WRmsxCQx7bK7J2VyS2SbOibqX8mDZhkTrwP1+IA0R9eD0/NvoMqX9GssewUDxSAbaaKdADCuU1dG7qpF8M9tfLDJz+dUL5qZoO+oGINGLLdo2y6Z/F+h3UWv7BXiS4BJKc+jwAng26BUMKmg2VVRdMvc+LbZTovUr9hyEq1orS9fOg1iIV6sPcyIVl3NIEy5bHMYh1d6sUCqvTO1UPSdf7lIvKxSszyKptIPNgioOvFpF9tTHDyKU5p1IiLm5FxW/+kGdPq6ZoTIZVriJoyjx6gWKpPY3vHN6bHUK9mA+LspIYAeuDFFvwkZx2b2Rtw3S99S7zz3eBqv3xlGqJixx/apl4Af7ZaxKFpMj9XZXAQVIv9BA0tIA+1nLByt4dPW4Xzoj3KcBbX5K/HkuR/30Lrq7gRQQDyNYgf5S/MO2MLJqcvnVFPXgVubK6XFu5quDibsZjPjxOUfBTJkJ/n4gB8Z8/TOM0oKD76hszXXoaWd9leUeQ1x88oy+QuXPRxzuLzVec3GiPNMYA42QvvTiWmrrhdceRzhV0J7pJBbi10ik+hXqSeNkldgktd5cWPss5F74yxAaEaPJO9I7MOUpE0XzlRfljPptykrIQt8SARMllykzJNrDt8VAl37ArEZbWxFLm3RuypOI0zgCZMRLf5JeElpCv1ay4wilz4vsYGr4fs3KUQzI1YY43uaDxNMz8k7dH/UkC9Dfg1oyHlNs+w== +Content-Type: multipart/alternative; + boundary="_000_AS1PR02MB791721260DE0273A15AFEC3EB5C79AS1PR02MB7917eurp_" +MIME-Version: 1.0 + +--_000_AS1PR02MB791721260DE0273A15AFEC3EB5C79AS1PR02MB7917eurp_ +Content-Type: text/plain; charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + +Test + +Med venlig hilsen +Martin Larsen +Feline Holidays A/S +Tlf 78 77 04 12 + + + +--_000_AS1PR02MB791721260DE0273A15AFEC3EB5C79AS1PR02MB7917eurp_ +Content-Type: text/html; charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + + + + + + + + +
+

Test

+

 

+

= +Med venlig hilsen

+

Martin Larsen

+

Feline Holidays A/S

+

Tlf 78 77 04 12

+

 

+

 

+
+ + + +--_000_AS1PR02MB791721260DE0273A15AFEC3EB5C79AS1PR02MB7917eurp_-- diff --git a/plugins/php-imap/tests/messages/issue-275.eml b/plugins/php-imap/tests/messages/issue-275.eml new file mode 100644 index 00000000..642d6a42 --- /dev/null +++ b/plugins/php-imap/tests/messages/issue-275.eml @@ -0,0 +1,208 @@ +Received: from FR0P281MB2649.DEUP281.PROD.OUTLOOK.COM (2603:10a6:d10:50::12) + by BEZP281MB2374.DEUP281.PROD.OUTLOOK.COM with HTTPS; Tue, 17 Jan 2023 + 09:25:19 +0000 +Received: from DB6P191CA0002.EURP191.PROD.OUTLOOK.COM (2603:10a6:6:28::12) by + FR0P281MB2649.DEUP281.PROD.OUTLOOK.COM (2603:10a6:d10:50::12) with Microsoft + SMTP Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id + 15.20.5986.23; Tue, 17 Jan 2023 09:25:18 +0000 +Received: from DB5EUR02FT011.eop-EUR02.prod.protection.outlook.com + (2603:10a6:6:28:cafe::3b) by DB6P191CA0002.outlook.office365.com + (2603:10a6:6:28::12) with Microsoft SMTP Server (version=TLS1_2, + cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.20.6002.19 via Frontend + Transport; Tue, 17 Jan 2023 09:25:18 +0000 +Authentication-Results: spf=pass (sender IP is 80.241.56.171) + smtp.mailfrom=*****; dkim=pass (signature was verified) + header.d=jankoppe.de;dmarc=pass action=none + header.from=jankoppe.de;compauth=pass reason=100 +Received-SPF: Pass (protection.outlook.com: domain of **** designates + 80.241.56.171 as permitted sender) receiver=protection.outlook.com; + client-ip=80.241.56.171; helo=mout-p-201.mailbox.org; pr=C +Received: from **** + DB5EUR02FT011.mail.protection.outlook.com (10.13.58.70) with Microsoft SMTP + Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384) id + 15.20.6002.13 via Frontend Transport; Tue, 17 Jan 2023 09:25:18 +0000 +Received: from **** + (***) with Microsoft SMTP Server (version=TLS1_2, + cipher=TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384_P384) id 15.1.2375.34; Tue, 17 + Jan 2023 10:25:18 +0100 +Received: from ***** + (***) with Microsoft SMTP Server (version=TLS1_2, + cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.1.2375.34; Tue, 17 Jan + 2023 10:25:16 +0100 +Received: from **** + (***) with Microsoft SMTP Server (version=TLS1_2, + cipher=TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384) id 15.1.2375.34 via Frontend + Transport; Tue, 17 Jan 2023 10:25:16 +0100 +Received: from ***** + (***) with Microsoft SMTP Server (version=TLS1_2, + cipher=TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384_P521) id 15.1.2375.34; Tue, 17 + Jan 2023 10:25:18 +0100 +IronPort-SDR: qF6UeVYj8pb73gXrskSNOtfmUEgr2JLTtqbIK+/6ymuIu+hw8DzzyKrOGqm7zNPwG/4zr+ma5y + XERFv6Zyf/cxrWoVjWXswWrkCVjqQuHglLaONZt1Mg4okFzEByeEZsyg3/3n6kI4O+kgViBgLW + nNMzGlNLSqYBX+EDhOfE1GEWB/4iRbwDY32SnTr5BYqR0HwgHf+0M0E3b23/NqV6iYrF3KETah + cLtLRt/b5JY6f5KvmKoz0Y395r7MwryWR1eLKAU2j3w+ioNYBUw36K/hsIRojcquvQk9w3Qtfu + Yz1BUNwOEOwfxr0VjQScHirO +X-IRON-External-Mail-Tag: Extern +X-IronPort-MID: 60840111 +X-IPAS-Result: =?us-ascii?q?A0G0CQCSaMZjmKs48VBaglqEBz5Fk0mfZoFqEw8BAQEBA?= + =?us-ascii?q?QEBAQEJRAQBAQQDihcCJToEDQECBAEBAQEDAgMBAQEBBQEBAQQBAQECAQEGA?= + =?us-ascii?q?wEBAQIQAQEBAQEBAQEVCRkFDhAFLw1XDV0LgUQLgXQLAzENgjgiggQsgXYnA?= + =?us-ascii?q?QE4hE6DIwetVIEBgggBAQaCY5xECYFBi12ESYEhHD8BgU2EP4VPhXKaXIE7f?= + =?us-ascii?q?IEnDoFIgSk3A0QdQAMLbQpANRZKKxobB4EJKigVAwQEAwIGEwMiAg0oMRQEK?= + =?us-ascii?q?RMNJyZpCQIDImYDAwQoLQkgHwcmJDwHVj0CDx83BgMJAwIhToEgJAUDCxUqR?= + =?us-ascii?q?wQINgUGUhICCA8SDyxDDkI3NBMGgQYLDhEDUIFOBIENfwpXxUKgLYIqgVChG?= + =?us-ascii?q?oNmAZMikiGXS6gMgXwKFoFcTSQUgyJPAQIBAQENAQIBAQMBAgEBAQkBAQEBj?= + =?us-ascii?q?jaEDIosQzE7AgcLAQEDCYwjAQE?= +IronPort-PHdr: A9a23:3aIm3RBfYL78XRTcz0LxUyQUT0MY04WdBeb1wqQuh78GSKm/5ZOqZ + BWZua8wygaRAM6CsKgMotGVmp6jcFRI2YyGvnEGfc4EfD4+ouJSoTYdBtWYA1bwNv/gYn9yN + s1DUFh44yPzahANS47xaFLIv3K98yMZFAnhOgppPOT1HZPZg9iq2+yo9JDffQVFiCCgbb9uL + Bi6ohjdu8cIjYB/Nqs/1xzFr2dHdOhR2W5mP0+YkQzm5se38p5j8iBQtOwk+sVdT6j0fLk2Q + KJBAjg+PG87+MPktR/YTQuS/XQcSXkZkgBJAwfe8h73WIr6vzbguep83CmaOtD2TawxVD+/4 + apnVAPkhSEaPDM/7WrZiNF/jLhDrRyiuhJxw5Dab52aOvRwcazQZs8aSGhbU8pNSyBNHp+wY + o0SBOQBJ+ZYqIz9qkMKoxSkAwmnGebhyjhQhn/uw6IxzuMsERnB3Aw7A9IDq3bUo8/zNKcRV + uC11LHIwivZY/xLxzjw8Y7FeQ0urv+QR7x/a9bRyVUxGAPfiFWdsZDpMjKV2OkTvWWV7+RtW + OOhhmMmqAx8oDuiy8Uih4fGh48Y1FPJ+Th4zYs6J9C0VFJ3bN64HZZftiyXKoR7Tt4kTmp1u + yg60qULtJ2ncCQQ1pgqyAPTZ+aHfoWJ+B7vSeScLSp+iXl4YrywnQyy/lKlyuDkVsm7zlJKr + i1dn9nJsXANygDT5tGfSvdk4EutxSuD2xrW6u5eIEA0kbHUK5kuw7IqkZoTq0vDEjf3mEXwk + qCWal0p9+u05+j9fLnrqYKQO5V0hwz/KKgih86yDfkgPggLRWeb+OC81LP5/U3+RbVHluU2k + q7CsJDGPskbpLS2AwlW0oYk8xa/Fymp3M4FknYZNF5FfgmIgJDzO17SOPD4Eeu/g1O0nTt23 + /zGJKHuAo3RLnjfl7fsZbJ960lTyAoyy9BT/olUCqwZIPLrXU/xrsDYAwQiPAyp2eboFc9y2 + poQWWKIGK+YPrndsUWV6e41PuaDetxdhDGoL/8q5virlmIhgVgHYYGjwIEbYTW2Ge55Kl+VJ + 3bh0fkbFmJfnAM4BM/tkEWPGWpLYG2ud6A14DI8EJqrS4vOENP+yIed1Tu2S8UFLltNDUqBR + C+ASg== +IronPort-Data: A9a23:VhC+4aKGZF33Q9F0FE+RvpMlxSXFcZb7ZxGr2PjKsXjdYENS1zBVm + zYfDDuGOvjcMGT0etAkYN6x8kwD7MLQm9Q3G1ForCE8RH9jl5HIVI+TRqvSF3rJd5WcFiqLz + Cm/hv3odp1coqr0/E/F3oAMLhCQ7InQLlbGILes1htZGEk1F0/NtTo5w7Ri2tcx3oDga++wk + YqaT/P3aQfNNwFcbzp8B5Kr8HuDa9yr5Vv0FnRnDRx6lAe2e0s9VfrzFonoR5fMebS4K8bhL + wr15ODjpz2Bp3/BPfv++lrzWhVirrc/pmFigFIOM0SpqkAqSiDfTs/XnRfTAKtao2zhojx/9 + DlCnZWXSl0jF72Qo9YUcCEfFTpSP4Rt/KCSdBBTseTLp6HHW37r3ukrFARsZdRe/+92BWtJ5 + bofMj9lghKr17rwmu7iDLQywJ18daEHP6tH0p1k5SneFuoOQ5nFQKLS/dIe0DpYasVmQ66OO + 5JAMGMHgBLoWwRwMVsFObICo/qFn1bzdyRihGDOnP9ii4TU5Fcojeaxb4O9lsaxbcFSkUee4 + 3nb53z+GA0yPsGFxTPA/HW2mebVkWX3Veov+KaQ8/l3nBiLgzZLUVsTXFq/q/6pzEmkVLqzN + nD45AIniqto/mW7EuLPVj6A53ifkhw1cN5PRrhSBB629oLY5AOQB24hRzFHacA7uMJeeQHGx + mNljPu0X2E06uT9pWa1r+zI9WroUcQABTVaDRLoWzfp9PHPjenfZDrlQ8xnEajdYjbdQGmpm + mHiQMQWo7wVgYYh2r+//Favvt5Bjp3OUxJw/kCNBjvj6wp4YISid8qv81ezARd8wGSxEQXpU + JsswZL2AAUy4XelyHzlrAIlQejB2hp9GGeA6WOD5rF4n9hXx1atfJpL/BZ1L1pzP8APdFfBO + RGM4lMAv88JYiL6Ncebhr5d7ex0ncAM8vy6DpjpgiZmO/CdiSfcrH02DaJu9zmyziDAbp3Ty + b/AKJvyUSlDYUiW5Dq7RuEZ3L4tgzsj3XvUX4yzwBGt0dKjiI29Ft843K+1Rrlhtsus+VyNm + /4Gbpfi40gBDIXWP3eGmaZNdgpiBSZgWvjLRzl/K7TrzvxOQj9xUpc8ANoJJuRYokiivryZo + S/lAxEGmAGXaL+uAVziV02PoYjHBf5XxU/X9wR1Vbpx83R8M4up8okFcJ47Iesu+OB5lKcmT + fADeMKYGvkJRjmeo2YRapz0rYpDchW3hFvQYHP+PWViJsBtF17T59vpXgrz7y1SXCC5gs1v8 + bSv2zTSTYcHWwk/Xt3db+iizg3tsHVEwLByUkLEL8N9YkLp9IQ2eSX9guVuepMOIBPAwSOC2 + kCaDE5A9+XKpoY09vjPhLyF9tn2SrAjQxcDQWSCtOS4LyjX+Gan0LRsaufQcGCPTn7w9YWje + f5Rk6P2PsoBzQRDvIdLGrp2yb4zuon0rLhAwwU6QHjGYgj5Cr5kJXXaj8BDurcXnO1cvhaqH + 1rKoIEDf7CAOcfvF05XIxAqN7zR2fYRkzjUzPI0PESjunAup+faDBwMMknekjFZIZt0LJghn + bUrtvkQul62hRcdO9qbijxZqjaXJXsaXqR56pwXXN3xhgwwxg0QaJDQEHWsspSIdskJKgxwe + mbSgaPDg75b1gzFaXVqTSrB2u9UhJIvvhFWzQZceA3Sx4eY36E6jE9L7DA6bgVJ1REbgeh9D + W46ZUR6KJKH8ypsmMUeDXunHBtMBUPF90H8o7fTeLY1k6V1uqfxwKHR9ApDEI31M46RQ9SDw + Iyl9Q== +IronPort-HdrOrdr: A9a23:8y8NqqHssZLZ5JoIpLqE88eALOsnbusQ8zAXPidKKSC9E/b4qy + nKpp4mPHDP+VUssR0b9uxoW5PwI080l6QZ3WB5B97LNzUO01HFEGgN1+Xf/wE= +X-IronPort-Anti-Spam-Filtered: true +X-IronPort-AV: E=Sophos;i="5.97,222,1669071600"; + d="scan'208";a="60840111" +X-Amp-Result: SKIPPED(no attachment in message) +Received: from mout-p-201.mailbox.org ([80.241.56.171]) + by maila.burda.com with ESMTP/TLS/ECDHE-RSA-AES256-GCM-SHA384; 17 Jan 2023 10:25:16 +0100 +Received: from smtp1.mailbox.org (smtp1.mailbox.org [10.196.197.1]) + (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) + key-exchange ECDHE (P-384) server-signature RSA-PSS (4096 bits) server-digest SHA256) + (No client certificate requested) + by mout-p-201.mailbox.org (Postfix) with ESMTPS id 4Nx3QJ6GdJz9sQp + for <***>; Tue, 17 Jan 2023 10:25:12 +0100 (CET) +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=****; s=MBO0001; + t=1673947512; + h=from:from:reply-to:subject:subject:date:date:message-id:message-id: + to:to:cc:mime-version:mime-version:content-type:content-type: + content-transfer-encoding:content-transfer-encoding; + bh=OMh+Pfp7SvIiDJjyXg53DevMPfaJRBhjQUkokwQIgDY=; + b=m3nM2KPbJdO7D+Vq/fMLLCXpkeDgvLs/JRzGTzWO4OoQhSaXLp76/vkBGPCU7BSGxir2Fu + g6e9Ggnrf0l8vL7rpvTgttta64wImaP9wOmJ0fOjzHcg/PX7sYjlUjyVyfThqZ7M5qg6/P + E9wItt4lQoUeQRVc6EoUlUaL7S+2R4P2WsQ6HCjulmpC3fmZdPOTXph/a1YfGvSfSj0pjp + LauH2n6EURKFmtMv8MbDcvTVKcq7o1bLGnK/RAjkLmRAORt+eC08IEAb5stVE6T6UPZ14Q + GUqrK2HEm5THS9vlH/11LwxwmnAdqUm8nl+Ymo3n1UNF9r8wkh8BUndGQFOQqQ== +Message-ID: <****> +Subject: Testing 123 +From: **** +To: *** +Content-Type: text/plain +Content-Transfer-Encoding: 7bit +Date: Tue, 17 Jan 2023 10:25:11 +0100 +Return-Path: *** +X-OrganizationHeadersPreserved: *** +X-MS-Exchange-Organization-ExpirationStartTime: 17 Jan 2023 09:25:18.2389 + (UTC) +X-MS-Exchange-Organization-ExpirationStartTimeReason: OriginalSubmit +X-MS-Exchange-Organization-ExpirationInterval: 1:00:00:00.0000000 +X-MS-Exchange-Organization-ExpirationIntervalReason: OriginalSubmit +X-MS-Exchange-Organization-Network-Message-Id: + ca3e116b-7c15-4efe-897e-08daf86cbfae +X-EOPAttributedMessage: 0 +X-MS-Exchange-Organization-MessageDirectionality: Originating +X-MS-Exchange-SkipListedInternetSender: + ip=[80.241.56.171];domain=mout-p-201.mailbox.org +X-MS-Exchange-ExternalOriginalInternetSender: + ip=[80.241.56.171];domain=mout-p-201.mailbox.org +X-CrossPremisesHeadersPromoted: + DB5EUR02FT011.eop-EUR02.prod.protection.outlook.com +X-CrossPremisesHeadersFiltered: + DB5EUR02FT011.eop-EUR02.prod.protection.outlook.com +X-MS-PublicTrafficType: Email +X-MS-TrafficTypeDiagnostic: DB5EUR02FT011:EE_|FR0P281MB2649:EE_ +X-MS-Exchange-Organization-AuthSource: **** +X-MS-Exchange-Organization-AuthAs: Anonymous +X-OriginatorOrg: *** +X-MS-Office365-Filtering-Correlation-Id: ca3e116b-7c15-4efe-897e-08daf86cbfae +X-MS-Exchange-Organization-SCL: 1 +X-Microsoft-Antispam: BCL:0; +X-Forefront-Antispam-Report: + CIP:193.26.100.144;CTRY:DE;LANG:en;SCL:1;SRV:;IPV:NLI;SFV:NSPM;H:mout-p-201.mailbox.org;PTR:mout-p-201.mailbox.org;CAT:NONE;SFS:(13230022)(4636009)(451199015)(6916009)(82310400005)(558084003)(2616005)(7116003)(6266002)(36005)(8676002)(86362001)(36756003)(5660300002)(1096003)(336012)(8936002)(156005)(7636003)(7596003);DIR:INB; +X-MS-Exchange-CrossTenant-OriginalArrivalTime: 17 Jan 2023 09:25:18.1452 + (UTC) +X-MS-Exchange-CrossTenant-Network-Message-Id: ca3e116b-7c15-4efe-897e-08daf86cbfae +X-MS-Exchange-CrossTenant-Id: 0a3106bb-aab1-42df-9639-39f349ecd2a0 +X-MS-Exchange-CrossTenant-OriginalAttributedTenantConnectingIp: TenantId=0a3106bb-aab1-42df-9639-39f349ecd2a0**** +X-MS-Exchange-CrossTenant-AuthSource: *** +X-MS-Exchange-CrossTenant-AuthAs: Anonymous +X-MS-Exchange-CrossTenant-FromEntityHeader: HybridOnPrem +X-MS-Exchange-Transport-CrossTenantHeadersStamped: FR0P281MB2649 +X-MS-Exchange-Transport-EndToEndLatency: 00:00:01.4839051 +X-MS-Exchange-Processed-By-BccFoldering: 15.20.6002.012 +X-Microsoft-Antispam-Mailbox-Delivery: + ucf:0;jmr:0;auth:0;dest:I;ENG:(910001)(944506478)(944626604)(920097)(930097); +X-Microsoft-Antispam-Message-Info: + =?iso-8859-1?Q?DN6oyY29jzFhRK1BGCOfg1yFnX4ACBaHBh9nbhnIoNZ4DGdi6zN+tIXnUR?= + =?iso-8859-1?Q?AY3F6SV1doOncexZBgqeE70YqvkB0xmo97NgnkQFb48xPvmZwoiAqkmIrS?= + =?iso-8859-1?Q?EbPesMMRL/pVX22Pde2C0KNThL0e02Li5ZCNvDxq+mEt0T9cww7HhoY6SS?= + =?iso-8859-1?Q?vMbDIBDA4c6Gyvb+34A6rYNFyHtZFVMas8S1KP4bV7QtI2WooRBzL1tOgM?= + =?iso-8859-1?Q?gyRVbzR69Mk++hTLhqTK7kBIkGMFhUC1McOMGMTOuUh9Ay7vMeY/oNGwOG?= + =?iso-8859-1?Q?WEFVsQjOz6lY4FIc3A+U5t3LMxkHtxRCql//ZhfKfjy78hPR7FCzYk70+T?= + =?iso-8859-1?Q?iCLlDZNF73KjvxH82FFKOersAl26L1kb2x5F/d1XJxAJe7BTG20/LXuMmx?= + =?iso-8859-1?Q?wGJs7s+C69LYa4cliwtGTMEuS6Rd7bOLihXnIy6hn8UhjVhNBv0+yGjint?= + =?iso-8859-1?Q?hbwBnQJ4Ny2jeS42bJYmKw+7Dnol+pphbvhjhtsDvKifo4Xx7xM45eV2s3?= + =?iso-8859-1?Q?wfKtSfeNGIR3Oi0VwreHTtsCOfvY8jbP+T2Z6KOFtQRvlUyutOFnOB2x1J?= + =?iso-8859-1?Q?BVgSblJzekltOB7gCAmZiCQnZuUMOtVVqRfURwUhOuQ47I/2LDJzi/NQnf?= + =?iso-8859-1?Q?XiqeHojof9SfngGas5TNx9tuQxmFqw4AWJE7iy4hxJo2ehSt4ReEvPzyt9?= + =?iso-8859-1?Q?1DTwLNYbyZ8Ub7Uq3EtUDE5vn3jY2thuVBo8eemdrjOvsx+hdH9RxcFwYz?= + =?iso-8859-1?Q?qU8NIflZ0TMRHTT2M2aBdzB9zsR3Kda+I8tNi7T6DCWve+MEEmLeRa4lU6?= + =?iso-8859-1?Q?XAFgPATiTR9BGrBBrwqKLye2Pnn6Bx8Hs+R7sFsv6HnQkS8B585+mjXV0A?= + =?iso-8859-1?Q?jXMm+5KDT/IUTiTAeJPwBTNEAG6Yo8gDg+6/9EMuZtQYAYZnM4tMefCwBm?= + =?iso-8859-1?Q?oV/M1vLbCGr7m1BDeRerH34C/2SWa0XnU9zOzqvm6/ery2Cv81aakjQr+S?= + =?iso-8859-1?Q?FdFwyYhZi96v0+HHAeT1Ncyrb84GZVp0kb1VYOBI0JH7TTZy88PMtFfbXQ?= + =?iso-8859-1?Q?nrSZ/VgrVa7hCvBOf2VQnj6iMnNqaJlmnI+E/yXvDNQJn2JDhor7QrClSA?= + =?iso-8859-1?Q?rrCXMDZyWWZhhDrf+VkN7ePKkju46/dqh0NRjqz6571xFdftnXx/b9dli1?= + =?iso-8859-1?Q?+XoZOiqbV8FyrJNCWDLuZRc1gIr0KVbbxN9nggMGa5c8tslnJvW/OTImm7?= + =?iso-8859-1?Q?SEKOX/bZx/aOe9bLlgzQhqdtwBkOJurGSWeDajB/ko2pFUcIYWyMaB6dFW?= + =?iso-8859-1?Q?RcQgdjWDgNNqiUnyvY585ZCFJCfiMaWj8hglJWADRQdLHNSqFAtuZdVCc1?= + =?iso-8859-1?Q?HPXeZDkLqPP+0VfD8EO3A1Fmp+E3kIrykjPJtNslbzwmtL3ATsC8e2mcob?= + =?iso-8859-1?Q?NGBKDmktRjDYG3mhya2XCCiFWaYiig6J/s1ePtuRp3TbBojEAUg2E5xItx?= + =?iso-8859-1?Q?gRGc6bNfg9Rihie4+BZSN+l+ynuYJQ/FGK3CvQYA9gXp00ZtfPwlHx7mXA?= + =?iso-8859-1?Q?gaDydNv1CyuYCOgUeTxo4Bi7N4Rrz72tn9pYxoZ7okujjzLWuiK7etRoRz?= + =?iso-8859-1?Q?JwOHGQklwT4VgMwwLQdXxNR+WMLbOyXjt6bfrKSUHq34oLx6ZVk2F6r9HI?= + =?iso-8859-1?Q?5uaCMLO8dLt3O9rb8FdV3EfLGg+zThKobHlrVJ2WFZLm0xtSsbzC04WLdR?= + =?iso-8859-1?Q?Oqf0F2GVBbFjulBFs9mvdhDXD33oUn5atsgLnkAitzWZH8qgeTOKgCbZD4?= + =?iso-8859-1?Q?WIXnz+DQx0g6sgmqGwa5fkPzRn+NfdGnwNwRyjXcAw7jW5DVkAnzd2OkEc?= + =?iso-8859-1?Q?WrVWN1eIbPedT77yCWDwLAjGBWSrpmI1bZkqI=3D?= +MIME-Version: 1.0 + +Asdf testing123 this is a body \ No newline at end of file diff --git a/plugins/php-imap/tests/messages/issue-348.eml b/plugins/php-imap/tests/messages/issue-348.eml new file mode 100644 index 00000000..6c9d385d --- /dev/null +++ b/plugins/php-imap/tests/messages/issue-348.eml @@ -0,0 +1,1261 @@ +MIME-Version: 1.0 +Date: Fri, 18 Nov 2022 16:52:06 +0100 +From: sender@testaddres.com +To: receiver@testaddres.com +Subject: 2 - test with a strange attachement +In-Reply-To: <30fdd5117dd9408487d74e9b50dedc27@testaddres.com> +References: + <30fdd5117dd9408487d74e9b50dedc27@testaddres.com> +User-Agent: REDACTED +Message-ID: <0a5495c56acb49d56545a2017dd1f931@testaddres.com> +X-Sender: sender@testaddres.com +Content-Type: multipart/mixed; + boundary="=_be6bc52b94449e229e651d02e56a25f1" + +--=_be6bc52b94449e229e651d02e56a25f1 +Content-Transfer-Encoding: 8bit +Content-Type: text/plain; charset=UTF-8; + format=flowed + + + +-------- Original Message -------- +Subject: 2 - test with a strange attachement +Date: 2022-11-09 08:36 + From: anotherSender@testaddres.com> +To: "receiver@testaddres.com" + +--- First Text as body test 80 column new line linux style 0x0A --- + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor +incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis +nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. +Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu +fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in +culpa qui officia deserunt mollit anim id est laborum. + +--------------------------------------------------------- + + +--- Second Text as body test no column limit (865 characters) one single line ending with 0x0A --- + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur + + +--------------------------------------------------------- + +Da: From Test sender in another language (IT) +Inviato: mercoledì 9 novembre 2022 03:17 +A: To in another language (IT) +Oggetto: Subject in another language + +--- Third Text as body test 80 column new line linux style 0x0A --- + +At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis +praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias +excepturi sint occaecati +cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia +animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et +expedita distinctio. Nam +libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo +minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, +omnis dolor repellendus. +Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus +saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. +Itaque earum rerum hic tenetur a +sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut +perferendis doloribus asperiores repellat. +--=_be6bc52b94449e229e651d02e56a25f1 +Content-Transfer-Encoding: base64 +Content-Type: application/pdf; + name="Kelvinsong—Font_test_page_bold.pdf" +Content-Disposition: attachment; + filename="Kelvinsong—Font_test_page_bold.pdf"; + size=35648 + +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 +YmoKMjY0NgplbmRvYmoKNCAwIG9iago8PC9UeXBlL1BhZ2UvTWVkaWFCb3ggWzAgMCA1OTUgODQy +XQovUm90YXRlIDkwL1BhcmVudCAzIDAgUgovUmVzb3VyY2VzPDwvUHJvY1NldFsvUERGIC9JbWFn +ZUMgL1RleHRdCi9FeHRHU3RhdGUgMTYgMCBSCi9YT2JqZWN0IDE3IDAgUgovRm9udCAxOCAwIFIK +Pj4KL0Fubm90c1sxMyAwIFJdL0NvbnRlbnRzIDUgMCBSCj4+CmVuZG9iagozIDAgb2JqCjw8IC9U +eXBlIC9QYWdlcyAvS2lkcyBbCjQgMCBSCl0gL0NvdW50IDEKPj4KZW5kb2JqCjEgMCBvYmoKPDwv +VHlwZSAvQ2F0YWxvZyAvUGFnZXMgMyAwIFIKL01ldGFkYXRhIDI1IDAgUgo+PgplbmRvYmoKNyAw +IG9iago8PC9UeXBlL0V4dEdTdGF0ZQovT1BNIDE+PmVuZG9iagoxMyAwIG9iago8PC9UeXBlL0Fu +bm90Ci9SZWN0IFswIDAgNTk0Ljk2IDE1XQovQm9yZGVyIFswIDAgMl0KL0MgWzEgMCAwXQovQTw8 +L1VSSShodHRwOi8vd3d3LmJ1bGx6aXAuY29tL2Rpc3BhdGNoLz9hY3Rpb249VHJpYWwmcHJvZHVj +dGlkPXBkZiZ2PTExLjExLjI4MDQmZj1Qcm9mZXNzaW9uYWwlMjBmZWF0dXJlczolMjBVc2VyJTIw +aXMlMjBub3QlMjBpbnRlcmFjdGl2ZSZyZD10cmlhbCZmbD0mcmZsPVBSTykKL1MvVVJJPj4KL1N1 +YnR5cGUvTGluaz4+ZW5kb2JqCjE2IDAgb2JqCjw8L1I3CjcgMCBSPj4KZW5kb2JqCjE3IDAgb2Jq +Cjw8L1IxMgoxMiAwIFI+PgplbmRvYmoKMTIgMCBvYmoKPDwvU3VidHlwZS9JbWFnZQovQ29sb3JT +cGFjZS9EZXZpY2VSR0IKL1dpZHRoIDQ4NQovSGVpZ2h0IDE3OAovQml0c1BlckNvbXBvbmVudCA4 +Ci9GaWx0ZXIvRmxhdGVEZWNvZGUKL0RlY29kZVBhcm1zPDwvUHJlZGljdG9yIDE1Ci9Db2x1bW5z +IDQ4NQovQ29sb3JzIDM+Pi9MZW5ndGggODU5Nj4+c3RyZWFtCnic7Z1PrF/Fdcfv29kKErY3cfAi +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 +mC/DQfYSeCdasGbx9iyEahLa0CpYc3zPF1qw5uBl7JiqhJuJkMGa43vWZrDm4GWwGaw5eHUWrFm8 +vXoGa475ahWsOXjZBgKTcJZX4d9FQlc8CywJq0TAVyyldGHOKC7hKnaFSCRsZhoYspRS02awFvE2 +toFeJBRyvSyfTwgfGb5Wn49TN38cR9vuetL0ZPGpum7t36u333ae6qDTH+WnOLQKZW5kc3RyZWFt +CmVuZG9iago4IDAgb2JqCjw8L0Jhc2VGb250L0dBVkNCTytBcmlhbC9Gb250RGVzY3JpcHRvciA5 +IDAgUi9Ub1VuaWNvZGUgMjIgMCBSL1R5cGUvRm9udAovRmlyc3RDaGFyIDEvTGFzdENoYXIgNzIv +V2lkdGhzWyA3MjIgNTU2IDIyMiAyNzggNTU2IDUwMCAyNzggNjY3IDU1NiA2NjcgMzMzIDUwMCA1 +NTYgNjY3IDMzMwo1NTYgMjc4IDIyMiA3MjIgNTU2IDI3OCA1NTYgNTU2IDI3OCA1NTYgNTU2IDU1 +NiA1NTYgNzc4IDc3OCAzMzMKNzIyIDMzMyAyNzggNTAwIDYxMSA2MTEgNzIyIDU1NiA1NTYgNTU2 +IDUwMCAxMDE1IDgzMyA3MjIgNTU2IDU1Ngo1NTYgNTU2IDY2NyA2NjcgNjExIDY2NyA1MDAgNTg0 +IDUwMCA4MzMgNjY3IDcyMiA1NTYgOTQ0IDcyMiAyNzgKNTU2IDE5MSAyNzggNDAwIDI3OCA1NTYg +NTU2IDU1NiAzNTVdCi9TdWJ0eXBlL1RydWVUeXBlPj4KZW5kb2JqCjIzIDAgb2JqCjw8L0ZpbHRl +ci9GbGF0ZURlY29kZS9MZW5ndGggNTAyPj5zdHJlYW0KeJxd1E1u2zAQBeC9T6EbmDP8kQME3KSb +LFoUbS8gU1SgRWRBcRa9fd+8qbvo4gV4EWnyo0yfX16/vG7rfTh/P27tZ78Py7rNR/+4fR6tD9f+ +tm4n0WFe2/1v49/2Pu2n88vXaf/1e+8DBvTF+7fpvZ9/aOR/xOe029w/9qn1Y9re+uk5hPq8LPXU +t/m/R/niM67LY6hUT8ihomr1hLJYjdUTRrWaqieMyWqunqCcW6onKOeO1RMy516qJ5Rs9al6whit +TtUTili9Vk9I/KhWPSFx3bl6Qnqy2qsnpG51qZ6QbCHBWVhQ7ZMFVqE32boCq9Cb+RRWoTdzLqxC +b+ZgWIXebNsQWIXeYusKrEKvssIq9EbjC6xCr85WYRV6IxeCVeiN3AasQm8sVmEVeiO3AavQW4wv +sAq9xQbjVTCotpDCqvQWm6uwKr3jZBVWpbeYSGFV945WYVV608UqrOreZhVWpTdxXVjVvRwMq9Ib +7buhsKp7uRCsSm/hQrCqv1/uGValV/kUVnWvnSS+XJ4w2Tbw8QwG20ni/BgAbV2cPYNqB4vzY1A5 +GNboXiPgVTB4agScPYM921lFWKO/X64La6Q38ymskd5ke8YMBrXxHj4unF1Ju9uPqzy0z+Po250/ +ALzgdrHXrf/7jdhvu80akNMfVk8LMQplbmRzdHJlYW0KZW5kb2JqCjEwIDAgb2JqCjw8L0Jhc2VG +b250L1FaUEVYSitBcmlhbCxCb2xkL0ZvbnREZXNjcmlwdG9yIDExIDAgUi9Ub1VuaWNvZGUgMjMg +MCBSL1R5cGUvRm9udAovRmlyc3RDaGFyIDEvTGFzdENoYXIgNTgvV2lkdGhzWyA2NjcgNjExIDM4 +OSAzMzMgMjc4IDI3OCA3MjIgNTU2IDU1NiA1NTYgNzc4IDcyMiAyNzggNzIyIDY2Nwo3MjIgNzIy +IDc3OCA3MjIgNjY3IDYxMSA2MTEgMjc4IDU1NiAzMzMgNTU2IDU1NiA1NTYgNTU2IDI3OCAzMzMK +ODg5IDU1NiA1MDAgNjExIDk0NCA3MjIgMjc4IDYxMSA1NTYgNTU2IDU1NiA2MTEgODMzIDIzOCA2 +MTEgNTU2Cjg4OSA2MTEgNjExIDYxMSA2NjcgNTU2IDMzMyAyNzggNjExIDc3OCA2MTFdCi9TdWJ0 +eXBlL1RydWVUeXBlPj4KZW5kb2JqCjI0IDAgb2JqCjw8L0ZpbHRlci9GbGF0ZURlY29kZS9MZW5n +dGggMzEyPj5zdHJlYW0KeJxdksFugzAMhu88Rd6AkBLTSpUv7aWHTdO2F4BgKg4NiNLD3n6/TbvD +Dr+lDxL5s5zydDlf8ri68mOZ0pesbhhzv8h9eixJXCfXMRdVcP2Y1idZTbd2LsrTWzt//8zicECG +jd/bm5SfobYv1XYnTb3c5zbJ0uarFEfv+TgMXEju//0KcbvRDc+jAUc13qMCe94SqgCsK7Z4jwrc +sQW4U6zZAqwVI1uAUfHAFuBBMbEFmBTRRAPsFQe2AOF9jIEt3kfViGgSrVHURgQjMitSK4IRmRWp +FUGBTINUg4gtQFLcswW4V4QgmSSpJHVsAXaK8CVzJnUm+JI5kzqTsAUoitAnG4F0hAb6jY3Q6AgN +9BsbAVUX89qA7kiX/dqtS49lkbzai7CN66bHLH+PZp5mveWQ4heSYaHqCmVuZHN0cmVhbQplbmRv +YmoKMTQgMCBvYmoKPDwvQmFzZUZvbnQvRk1ZRExOK0M6XFdpbmRvd3NcRm9udHNcYXJpYWwudHRm +L0ZvbnREZXNjcmlwdG9yIDE1IDAgUi9Ub1VuaWNvZGUgMjQgMCBSL1R5cGUvRm9udAovRmlyc3RD +aGFyIDMyL0xhc3RDaGFyIDExNi9XaWR0aHNbCjI3OCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAz +MzMgMCAwCjAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAKMCA2NjcgMCA3MjIgNzIyIDY2 +NyAwIDAgMCAyNzggMCAwIDU1NiA4MzMgMCA3NzgKMCAwIDcyMiAwIDYxMSAwIDAgMCAwIDAgMCAw +IDAgMCAwIDAKMCA1NTYgMCA1MDAgMCA1NTYgMjc4IDAgNTU2IDIyMiAwIDUwMCAyMjIgODMzIDU1 +NiA1NTYKMCAwIDMzMyAwIDI3OF0KL1N1YnR5cGUvVHJ1ZVR5cGU+PgplbmRvYmoKOSAwIG9iago8 +PC9UeXBlL0ZvbnREZXNjcmlwdG9yL0ZvbnROYW1lL0dBVkNCTytBcmlhbC9Gb250QkJveFswIC0y +MTAgOTc5IDcyOV0vRmxhZ3MgNAovQXNjZW50IDcyOQovQ2FwSGVpZ2h0IDcyOQovRGVzY2VudCAt +MjEwCi9JdGFsaWNBbmdsZSAwCi9TdGVtViAxNDYKL01pc3NpbmdXaWR0aCA3NTAKL0ZvbnRGaWxl +MiAxOSAwIFI+PgplbmRvYmoKMTkgMCBvYmoKPDwvRmlsdGVyL0ZsYXRlRGVjb2RlCi9MZW5ndGgx +IDYzMDI4L0xlbmd0aCAyODM2OT4+c3RyZWFtCnicpL0JfBTl/TD+PM+cuzu7O7vJHtnd7M5mkw3J +AoEkEAKRDLeISBAICRIJcgooBPA+CCqHEQVtS9VaxaviQd0cQDhaqPLTqqXaam21VanF89cotUg9 +yO77fZ7ZWTbV/t++n/9Onuf5zjPPzHN97+eZCcIIITtqRxxqmDGrohKx3+8uhKhx0RUL1xjnry5H +CL+86Or12p4j790FGX9BSLxy6ZplV4x63JtASILytuSyVdctNcpH4L5tLyxfsnDx/05r/A1CX56F +zJHLIcNdFViAkJueFy+/Yv21mfoeg9ORq1YvWmicPzMKodiwKxZeu8Z3p+VZhPI0yNSuXHjFkkz5 +aoh8a1avW2+cf/kXen3N2iVrHu76+FMoryOkVAgHUQGEgPAEKuDjyI9Q+iMIH9M0dXn6Y3qdpgTu +QL2ZgNButAdfjvagI+g5fAruehYdQD3o18iHJqIH0I3oh2gLEtE8yLkdXQyHAPk/xAXpHlSBHoZx +fBgdh7Jz0c3oIPJif/oTtAFt4l6HuzbBSBehcagBrUZ34gvTV6H56D3+VlSDLkRXojW4Pd2Uvit9 +T/ox9Dg6wP063Y9sKIAWwXE8/Znwp/Rf0BC440foPvQevseyF+lQSzuU/Clai+7nWnicXpb+BloQ +RddAG3g0HR3HR0kCnr4EfYT9+EZuAjzl0XQyfQxKhVALWo7uRwfxCDyFRIX56enp48gLdVwLT70P +daF9cPSiX6C3sSKcSj+WPoUK0GA0FfrTg36Lj3Kp/o2pehgxAUapDNXCldXol+hF9BqO4V+R1YIi +VAq6cH36DZSPhqM50Non4M4P8b/IzXBs4F7gJ6fHIweMy910tNH/oL/iAK7AM3AjKSOryYPcWiRD +jcPhWIwuh/G+F57+Lk7gfUQhr3KP8k/z34qFqRNpB8xIHP0E/RT9Ctuhpxpeh2/Bb+K/kQlkAfkJ +eZ/7If8k/3tpIfT6UnQFuhM9jf6F3XgUnokvwcvxjXgLvhvfh4/j1/DHZByZTVaSz7nlXBv3C348 +HLP4dfytwmbhDvHjVFPqWOp3qX+lK9Ob0UzAh43Q+h+hB6FnB9Cr6C043kPvYwHbsAMODUfxHHwD +HDfjO/EjeDd+EvdALa/h9/En+Av8Jf6WIDhEEiRRUgRHjKwl15AfkgfIq3C8Rv5OvuZ8XBGX4EZw +dVwztxpatYXbAcde7q98gH+VT8M4Vwo7hYeE3cLTwnPCKVGRbpGR/Juzj/aX97+bQqmtqZ2prlRP ++q/IA3MYgFGIoDpo/UI4VsB87wSMexa9jhUYuwAux2PxhTAyC/AK3IavhZG8Dd+PH2dt/zk+DKP0 +R/w5tNlOQqzNQ8kIMp7MgONSsoS0kR3kHtJD3iTfcBJn45ychyvnpnAt3BJuPXcdt5NLcr/h3uHe +585wZ+FI81Y+whfxcT7BT+EX8FfxD/If8R8J84VXhA9Eq3iFuFnsFf8hjZTGSg3STKlF2i7tk96Q +WwE7n0d70X6U88MnuI3cJG4vuotU8QXkt+S3gM8L0GJuOgFMJbvxVnIT7iHFwrXiGDIGX4RO8XEY +6xfIQ+QMGcNNx9PwLLSCDDeeJubzT0FSxz+P+vjD0LffwpOvFRV8M/lcVFAXRqQW6vwfbhif4F5B +b3PvYYl/GP2Zt2If7iNPcA2ABb/gxwpNKMo9gH7OteGb0F4yCSHrt/I2wOOL8FPAF2bjSvwVl0Yc +uQiwqIb7G7oVrSR/Qn1Ax1vRj/Fifhm6C1XhG9FH6GdAFWXClWK56MEvkcv5DpKHexDhn4Te1eJi +zAn56Dbcwt0vfk7eQlehV3krepd7Blr/Kvk5N50/JVyMlwMF3IQ2o7b0RnSd0MT/Hi9DHG5EJfwJ +4G43cpV8FNINwFXmA0/bB9R9EPjAOG465PgBcy4EvJgDHOJ+OO4FPsEDBl0OND4XuNhvUY84m/Si +ZYIDA9dBiH8ldTGal/4Zui+9DF2ZvgcNAX6wJX0jPHE3+gBtR7vxptQNaA0KA+W8iy8UJpNXhcnp +IaSDvEVmkZ0D5xdGuwT70adw/BxOxgqHUAf/RzQL1ae3pf8A2D0IOOx96DJ0AToJvfwMajifO4qq +UheRzvRkbg309z00M/1EOoKtaHl6FZqBDqPHJQEtlBIwx0n8e+jvDWgJuTi9nluSuhzGYTuMgg6j +dRXwn9v5Nv5W/mu0DWh+J/CbXUA3TwHlUNpH+iWb1q9b27Zm9ZVXrFq54vLly5YuuaylaW7jnNkz +Lhqn1489r27M6NpRNSOqqyqHD6sYOmRworxsUGm8pDhWFNUi4cJQMFDg93k9+Xlul+p02BWb1SJL +osBzBKPBk2KTW7VkvDXJx2Pnnz+EnscWQsbCnIzWpAZZkweWSWqtrJg2sKQOJZf+W0ndKKlnS2JV +q0N1QwZrk2Ja8vjEmNaL581sAvjOibFmLdnH4OkM3sFgO8DRKNygTfIvn6glcas2KTn56uUdk1on +wuM6bdYJsQlLrEMGo06rDUAbQElfbE0n9o3FDCC+SaM7CZLt0KhkIDZxUrIgNpG2IMmVTFq4ONkw +s2nSxGA02jxkcBJPWBS7LIli45POBCuCJrBqkuKEpMSq0S6nvUF3aJ2Dj3Zs61XRZa0JZXFs8cL5 +TUluYTOtw5WAeicmfdef9J87hYe7JzRtyb0a5Dom+S/X6GlHxxYtuWtmU+7VKI2bm+EZcC8pmdza +MRmq3gaDOG2WBrWRTc1NSbwJqtRoT2ivjP4tiU2iOa0rtKQlNj62vGNFK0xNoCOJLr4u2hUI6AfS +J1BgktYxuykWTdYHY80LJ4Y681HHxdd1F+hawcArQwZ3qi5jYDsdzgyg2HOBJdlrDGLFKTTt4uzI +Ytqi2FRAiKS2SIOWNMWgT6NotGQU6lg0CorBrxnDXcnFMCOXJy0TWjvU0TSf3p8UStSY1vElAgyI +9f19YM7CTI5Yon6JKEjxJItqcN2Ek4lEsrycoog0AeYU2jiWnY8YMvjqXhKLrVE1SGD4UAOM7cLm +0RUw/NEoneA7enV0GZwk22c2GecauizYhfSKRHOStNIrR80rnjn0Srt5JXt7awwwuQdRFdmTlOPZ +P6fqzZu0fHQSe/8/Li8xrk+bFZs2c16TNqmjNTO202YPODOuj8pey0DJvAlNXJBkIBLk2FVAyvnZ +wvSkSUnyJfAnMqRe3CvJgJUsB2uTk2rr+UbcbI1G/8ubetOn6F0sOXdbppnJ0YmB52MGnA9ontLB +QYNBvE6bPa+jwzrgGqCaUeHUTAIYj2Y3RbUJSTQHKLME/nrTR0fR0BxM6jBkE2gBwD8jK3M6oGAw +AzfDj2LnkMGTgdF1dEyOaZM7WjsW9qbbL4tpaqzjAHmOPNexZlKriTi96YN3BJOTtzXDWC3Ho4cM +jtErHR2LOxFXAtXowU7MgJoJdzQnZySaY8nLErForGkJ9KVzNFKis1snAETQ+M4Y3jqzU8dbZ81r +OqCCVbJ1dlMXwWRC6/jmzmK41nQAjBmd5RKaSzPpiUZP0DQMQ9NFZFY+eADsmHZ2lWcZ7HxRL0Ys +TzbzMFrUS4w81agozirSQbFc1MsbV3SzNA95spHXbpQelCktwxWVXjmIQOIgdtH4dcLJ7CbdWqOP +1sfoY0k9gRGhWV2QcxDKjsGoeyyux8FOeObFLLsXt3eO0YMH2JMuzpRsh5I0rz2bBy2nxXIeBPUZ +HZ9zrgdz5jV1j0XwfBZDifH0RzktNCKXhhhjong+N9GkkI5pswAD6UXrqKA157JGb0ziWHJB7Noo +7V2yMXZdFDJjSQ24NRTqRFNCzR0dGhwxGJVFjU1GTC/hwSF4UnOy/TKzbDAEOHHuVIFbGV51hygP +ydZ2g1nbWqiNAh1mdclF31sbtD6JL6Ex+2PN7xyJYkb9IKWNSjvmd8wDfIwmC2nFmXbAqSPUzJ4A +LbmXtQQz4bQIdIKllJY0yuSATcYu6CQXJViKWdpxQWzSYihBAwjdETBZUW1xMy0Vo0RDEf8/FsI5 +haggYQ/vUMeYZzhzZpBvR3LZwNPl2dPJNICOUjLUYBPQF0ay0eSKYHJVcyJbZCHtcwfQ9mhK4KPZ +zVNoaAWxMyXZvmghNBHkzdRFMci4ADK0psuMEaSCuoNqTosWwm10lDM1Ja9MDHgk8AQMLAoeRLuT +bG/QWpu1VuAheCYMdlBLCpBqS0F9ii2kfKPB6E8DMH9IFnbMgnsRnbZgUgJ+tnThkhhlrkmK78bo +0zby0Do0qymJgh0dMcAhaGLJZCgMj48nxfhUmsDfmkRs4RKq2S2lit0SQ+WA5rLRoU8LTopFm6EI +KWFjCQMHhHYZjRZ1UL2xpTUBI+HqcHdotR1A8C3Aq/j4osZW4Guaqk3W2FQvDMIZDMJUetYMDzIK +WkpoQbif/cWTVyQ6W6SScznsb3XCKCyzpzIlItlgFpHYHwBtiSTxjYKLtPP44nlMLsBE0cETSqbC +8OqAVUF6N1DR7IzYMO6fSm8NmhNm3AY5zaYAAHzvLMFbG3I54fyke9rFlwRhYIdQyS2NTV2EJqjo +m2e/uR7aigfaGug2muP6MdkL1vLLSIKnqMwWQIIvnUYCIp2zN42zcYPpQYpQIYqAmV4OhSNceZdY +GOnlBnXH/ZHXDnNl6AQEwpV1JQojB7hSrrBrTETv5WLdbk+lc9wQToOqKlisQbwawrMQjkDg0QIu +DPkqxBsgtEN4FsIRCK9BEBGCmF7VIKyG8BCEE/QKV8iFurSIOq6UK4B7C6ADTs6HPoeQhsBBO31Q +qw/NgLAAwnYID0EQWTmasxrCBghHIJxiV3TO13VPFbTd13UHS7pXrKpkpwuN0/kt7LR7brORTp9p +pBOnGsVGG8WGVxvZQ8cbaelgI3WXVLbT1GqvPDrOy3mhk15o+BqIMTmGnBiDDbqL86AkBMKJmRyd +c3cXxysfOsLxCHOEw2gxiqSPcrjL7qocZyVp8jlyowj5jPQZV0hft8NV+dC4C8j76FkIRyBw5H04 +/kr+ijaQE3TMIa6H8BCEIxBehfA5BJGcgOM9ON4l7yIneQdVQKiHsADCQxCOQPgcgkTegVglf6Go +xGIK10Mg5C8Qq+TP0K0/Q+wkbwP0NnkbmvZ6V01t5QEGJCoyQKQkA/iCGcDtrewlv+/6ugwwKg4z +DRh1iCtCY1EVV9RVMhzQz99Vd3mkl/ytW0tEdo0bRt5ASQgEWvIG1PwG0iA0QGiFsAaCCNCbAL2J +2iHsgLALQhICYBnEKgSNvAzhNxDeRMMg6BAaIMjktS6oppe82hUfHxnnJb8lLyIfjPhx8muW/oa8 +wNJXyP+w9CVIw5C+TF7oCkfQOBtcR3CPCqkKaQVcF8ivuovdkfQ4FzkCYxeBuAJCPYQZEBZA2A5B +JEdIUdfiiBsecgi9LCMo2YU+YenP0CMy0ldE9PgEQECNRvHR5wEE0UPaQ3Gix3feB6c0it91D0A0 +it+2DSAaxa/fCBCN4quuBohG8cUrAKJRfN4CgGgUnzEbIIh6yYP7i0sjNTNWYm2ck1wDo3QNjNI1 +MErXIJ5cQw/0NU/b9pOu8nIYsfv1RFl5pB30o8O4/WLc/ghuX4Lbb8btG3F7HW6/FLcncHsIt4dx +u47bD+FRMBTtWO8ZcFqr+3H7y7h9D25fh9vjuL0Etxfjdg3X6L0k2jW1iiWTWNI9jhIdpOeNBe7j +JFEY0SjgfBR4whGIX4WQZmc6FNKKjMIFYZoWdZfXG+dDR1euBvJ5Hm58HqbhefQeBB4m6HlAo+fh +Ic/DA5wQ10NYAOEohM8hpCGIULoIGr6dxU6IKyDUQ1gAYQOEzyGIrDmfQyBodaaJz7KG0UZXZBo+ +AwJPnoeDelCjJKoXqiE1oZ7PbQ9hZxjPCKfDpAZ5vcDT3S7Z1Yvt+/5l/+pfdmQZZyF3ke2UdZMd +mXR719fAuvG9XfFDkXEe/GMU5gHzcC2K4xJIR6F17HwECsk0rUYh8jSklV2hRrjN2RUfHDmIHfSu +fZGvQycjn4R6CYAfhw5F/qj18rgr8gfIeXpf5I3Q7ZGXKnplyDkc78WQHNRY0QOhUZE9L7OiG+HC +/V2Rm2myL3JTaEpkZYhdWGJcuHQdnOnOyMXxeZHz4XkTQ5dF9HXwzH2R+tClkTqj1Ah6z77IMGhC +wgDLobFlIVZpLAw5PZERc+bU9OLl+mBpp9QkzZBGSpXSYCkqRaRCKSjly25ZlR2yIltlWRZlXiYy +kvN70yf0BBWc+SKTnyJPY57BKqExQUyuEiwTdAFK5nHTyLRZ4/G05NFFaNplWvLMrFgvtoLxKMTG +Y5DOaNrs8clRiWm9UvriZE1iWlJquKSpE+O7miE3SbaC+TO7qRenadamIHXTHEAYuzbdGaTpoE13 +Njcjv/fqen+9e6yrdvLE74laM3Hi3M8/AC4cn9w5bVZT14innioc35ysZHA6DfC05A+oO+cA/gKf +mjTxAP4HTZqbDnBj8ReTLqb53NiJzc3TenEjK4c0/A8oB6jzD1ZOBilNyyFNDhvl7jfKlcD9UK6Y +JlDOYkElrFyJxcLK8ZiW61xXPGliZ3ExK+PT0DpWZp1Pyy3zcgmUKSlhZbzt6GVW5mVvOy2THMuK +hEJQJBxiRXAAhViREA6wIo3nilRkityeLXI7q4nD58qEjDL2E2YZ+wkok/hvf0vGJxK4e0zzovnU +FdYam7QEQmvyjquX+6lWr3Uuas74yOKtly1aTlPQa5tjSyYmF8Umap1j5n/P5fn08pjYxE40f9Ls +ps75+pKJXWP0MZNiCyc2d09pqK4ZUNft2bqqG77nYQ30YdW0rik133O5hl6eQuuqoXXV0Lqm6FNY +XYihekNTp4zGN0+Yb6TdxGYFtG0FW2C8V10zluHwmKj/5uBBUF12I1uiOanExiftEOilIeOGjKOX +gLToJQf1d2Yu+W8eEw0exLszl1TIdsXGo8T6q9ZdhfyTLp9o/K2DH2Stv4oOuBEn1v2nH1yblNQX +TqSrq9OS5bOmJevBgO6UJMhtpV1KjjbzbLZJvemjRuZQyBxNMzkuW5Dm1dE8iyVT8Lvzf1UmnUCp +oJ0c6sZ6GK9H65q5ZHjabAIcYXbGsXQQFCsqK9Y1QwfX4QReZz4j0+xEAhnniPbZDOuvykCZsVif +SY074ZZ15pBkf3SwEtkRW88ey4YzMb9pnIMbyVWgcaA7D4N0CKRDIK2EtJKr0N3xCEdqIha5JmKz +ToxI4sSI+dTmBGKOFTAcBNDZwZ4Y30PwSVHqJffpeUjgT3LIKvEnMSqQReEk4Q6T4ciC78NDkT+h +nqnrr7tIPV03vb8O1QOsnoVo+LCoK+oqgQiYLjqrcUfP6gL6Fmn8UagLXcp1k2uEg1CdDf0guSkB +I4zSX3UXlVQLvemv9KJ4WbVNtEoCAoEmCKLtM4sscxxBklxndVraLcQCs6Z77M5qy7uY4+sI1u2u +alygtD3hT0BjErQ1an+ipY41SoWjvw4i7HLX1tIwfBhOJIK6gnnJigQRRAaIA399vXrMVztseHMe +N6LKw1WxeEfl8SHvDD8+jOvGvlOnUp8YMZUd8yCrlPWijvZB9yCBw8JnBHEbNbwDE7xCpO1Rz7T0 +ofo+bNQb3At90lltgeO15yrbOpRV4f7yy9Rn8OwbUzNJq/A62HUXsWdbS50guNySrKq9uKobPeSQ +IdVd0kOOSxGnchrHcc+4frqNVdd/pk89A3XW1cNE4JZgN3JKmHYPasNx4qquGVlTJUpweFSM3/vR +b6fPO7zxutLzYgmcSM08jL/Cjs/e7v/2teaOnYd+kYqkNDSgRVezFimDyCCVWKwqRm4LbZP1IQ5D +2gOG2qWO3vSpHlUlcwD4qsfpZMDJHrudAX/XnVYrmeN0RBzE8Yw702qKit9pOXZazJbnxZCrujQO +R5XX5/WopH8jTGLReaXXbzw8b/qrqZn4BP7r4QM7O+b9/tv+tz9LfZGSabvXoj5+NL8PMK2ZtTuC +rrSQr2XuSkESLVdaeevXAr6ynswghBQoc+cx/GmZfrqur049WVeHKk4D6pyGedtPycMqcaSXq+pc +xSF/RaKqsrKqogKaVuKKjoi6qlxRT9RFcKoNb38Kb0+19eF7dtN0d+pK2pKnUu/iW9FxZEWLaUv2 +WoHUnhZ7cYMex1wdIdiK65AVLESuDomjpNEz0AK0Gm1Au6DmXbaH74VROt1y+qQKLQOsprHap/Yz +xKJ4JYmY+kopXlUch0ZVAVrli1LpyJE1+443zK2sHckdP952R3x6wcJLoDXjcC9ZQa4Aej+PjUvB +GrKGI9PxdGhIDJGAsAYKFfBr7qQjcrJF/RBVTO8bPgy1wbR0IR1mpSJAq8kbEfWMI2W4d+9euOEg +dHQL9JFDNeypfkK7VGd05FnE74Iyu3jWlzMtjC6g6d3ZhmeaffD48eNMO0t/RGoB5zhjxA4gLv1u +V34t6U2/q2v5tT/mMOEe4p4Fq/pqhPPhDmBjwKu4jxH5GHDxyb0I8d3XQ1116uk+1cCrLcLQRMtN +6jGKX8ADuoGTmRjmwVUYP7kj1VQg/P2bfGAJc9If8S7hKOB8IU7RFnQSw+EcCPNCfthu9wEj+pjh +NwX0AorgFhdSaA7yKgrECs1DFYDcxyE6Dj1mfe4Uv/uk0/AkkT7pQ6AUBnymF9hsIn2kSnOQqig0 +pnnZR557pn4RL24hW21bnS85BItk85NJeRd6LiiYEJydN98zv+Di4EpppW1R3irPyoLW4HXkGvFq +2/XOLeK90k71Jf/b5E3xTdufnYFsk8ap6dNIQQpMTyPypb8AGrJl4K+QHdmxrrsafessejRWPQzo +1KICZx5nhZvMgpb0x0bB/Y2WHRGXoii9YB42uhw2mwHIdjsA3Y2udYiydAWepCHqFjOLIjlTFBlF +9zWiHeEX76D4A11vSfRBTMGWNgZmhgK3tKGWJJmQ1BuaekStQA0BQ+oimu2X6RPIC8ENwQlhFP1h +CM3NzcFOe34vV9Gzym7nAwB0reIFwIxEfYKiueoeWVXp9bqB8YixotJ4nuqtqhzpUuOxIkmcs/L1 +XVd3rR+/4vWH37ju7gNP3njjk0/efOMFLeR1zOPznlnQnUq/nUqlnt9z737809SPPz+Fl+MVn12+ +GXD8PRCI3wKOWbGDYli3NdtzE7Cao4VMwGqMRXZQ9GgjRyXgSn4D2U7uk/lneGxBokA4i4AVgl+2 +stG10nlCmDrzwD5i/BmAT3UXQ9cQQ1cHQ1cYLb2AIqOJcQz7Aoqgg8wV6LMc9FkC1gRdIEKB7SCu +w5uQwSrajBlhPzgxNIN6Xy12UbnbglpAzLAfCGCCLaIuCBasWOhY17trgasAQ4MRj8ZcoiiNAPZV +Rb7tGff67B+/X7Gev2HsjZGfT3l5AfShDqhbgpELk1JGmwZFWVyq3Z+XJ86xU4JyuRjwmW5RVYDC ++UKYEqqPFgiH6dVwyAFXwgrtYbiXHII2WX0+LaK6CNEi0JSKN2iDKo6jCopgiXoaH6ukJEyyFSpu +N2EV6hani5j1nNBt7jwyJ5xP8+izu+DRlGHYbGSOj8pBNtrfVxulalofrY1Vpk8ZI4wRDwlHxEPS +i/JLIWmq0qzMdqxUFjuud1+fd7v7sPuDwAfBUwHliG1/HglbVVkUXw4F8kOhgBwKAKeUAyHOHlZ7 +yWPdM1zY1Yv9e2k7EW1YNyaKdQC5W3PI3Zold3ujdZ3vdWC0lOTxIbIRaUjFo3TFtbeeLCCryQbC +k4OkGEXw9k5GpC3AeM8kKP9l1AnCtL6vv+Wky03xAaItjqEJB7BjQ4plSFa3BNWQWqiGVfGX6VNI +AkKVIbVAMOl1VDNqwS1rgWrp1NqDkmQnYRDNPauIkm9n1JufoV5XrasKhpTKaU80XgMINXLkiGqg +WyYcgahBTIIuJEq8dLaG+Eoevf/z3ffdcMsD+EDeV797/cz5Tzz3yPzwnj3j6hYdvfnYB0tX/uCB +jrxX3/p0T9NThx/bunA4YGJj+kPeC5iYwGdypIStwK/T+fWHEKYkk1DgBJfFrHan4gxbrWWecIgP +l4WEMnvMrvgLQJHSVEqEmhSnWEKLxysojweBDgdy14KGCnIMOtP3gvqCu1Y9lqikgeLHMMHutU+y +b7bzk1xzXVcHuYu9q9QV+Yu9V9mvy99s78i/Pfi43WpT7A5ewlAfpohAF2kPYbpN0o5H9CiKh/cf +JI+hArJct0DrBGie3T0AL9w5eOHOEQPudQu01RrR/JSOtHZpwE1Szk1Szk3SujiTHXGM4mqcQK9P +76f3x3cM8ffiUV0Fr+ODeBSoAUd1W1Yy7Bjci+/JIFeij6FXhvmfTrRkZUD/SUpGoB9RXDNQLYte +XYLGAXUCGjVTdoTbKBIhjPmYYndaAXf2rnI6Q2U8QPtXldkL/P6Qh2FUiGFUZUUVRSqq/9VCUkWl +Q42XSgOGVVJNFjQRjGKYRGMUK4o39kR+tHLDs4/cVHVhvtu2rnfzisu35fdEP/35tS+vXLr4lh2p +j9/8VRrf6r9vS/KWGx/Of5Bce9OiW267Tdv74rKuxQseGBr+xV1HU19+SO2oAHBAFSwQK7KTEMW8 +w0hJf2MMe0+jXcwIEMGUJKIJWLKyxQQEU7aIJmDJShsTkORMYdkEJFM6y3K2TEY0ySYgmIBoAhYT +yMgxvabR3aQsV+5XnlReUoQLuQvtP+Q5N7AspIicJFhtnATS0G5/mePzOY7n7Igodl7iDpFDoDgS +vEu3Ip6HIuhlK99Llu4XBKteGKm2mmLOauhUDPiMKVfWXlyj2yW9KFYttUdHSDuchNKozZ5fjYhK +NAKKPtxM7wHg5D56D9nr6MXbGOr9neoeVMqdpjKhTv1QZUIO7OAzda7aWmbsbRma4IGzOZ1OEHvM +jWAH9dVdC3LiDd1WVcsVDanl+MLCOmaEAyJCGT1f0W21SntDraLHa5WiEKRDag0zHX+PlwolgvsU +3iJydrBLKvdT1QUpvClKE1VVlYYsBQMFV7mqPDEX58JkZ/9t5Kc/eOGFntQIvOBxbt/ZCx5PPQyc ++0f9K4EhUK03KvwM5KrENJI8E0fcJpCnZGbbbQJ5SmZK3QAcoIRuMMEDCMOo2ukw4pDDGvZ4Qm4q +ZG1Ong+H7A6MJD+oIEyFZgBjmFT8UYZHCRm60X8MmBzlcdVuJqadLJ4WuK6wo3Bn3hN5zytvKn8O +ypY8v6M8wOVZPe68vJcdznxHXr7DaQc+p+fRqnXHLrA3HU7dgzPN2O/k8euUB4Iw1F20Qa4F6mp1 +g7pd5dX/mof5GQ/zgxWh+onf5GH+HZr7MB6BnPhHUHJUl2Pv9/GyyEBeNoCbtVArD/gXG4MW4DQt +wPxPbpGHJgRAK5QrMHssw4RhtoMgJznG1yhna2uhrh1T0UIoZM9zgL7BewwO5/E4QzxTd0N2pxsk +Z9cqJ28KzAoaAF9chtzMZW/A0/LAzuWAryFPvgS6cHzOLzz3rbqlZ8+2udsGPXkXeat//4zb7j6K +5fV3nv51P25XO+449sj9XTPqveQfz6Sunp8687sX7+46Ad2fDpjmAblZiMrxJzmSM+LEEbwAczg4 +KKzbsd0O6lRQKArn261hjEpUqmgxW0sN+1SKOj4mN33M1vJlDKPjbxxX/8dEoZY+9VgLRaEhKwvw +REn3TCyYqM1zz9ZWcoulxfIK92JtvXxVaJO8OfSm/IbXJWl0DksNFiDOiVFlLkihKLtAm9VgJ9Cw +IH6d6qK9VGKajcRUdqG9JQPwpyQHf0py8KdkncrwR8VIBVYFfTu1n+rc6o7BwKNGdYdNogubbDgM +XPMQe04Y1+r2et8C32rfBh/vUzMFYDQYW3U0+rz0UT4vbbOvlxR3J7KmkyErc/GtzxCcTGDCgGWR +6wBVwHpKtZgW7TWxiz6Ays7m4F6MBat9EMMpuz2YX8RwKt8eFJjIDArncKrSwCYsxUuZ1SRKVDq6 +qfoVK0IutYbKSpyfg2vct93+wVNXNo6bcxkZd3hZT/81r93219TJn97+8Z53+mtm3HXR2sceueH6 +p/hZjhXDpg8b+9lfFrWm/vX7jr6b8TR8I37yV7ufO/tOy1PNvQ/e++yzMEsLQV56hSdg7O9g3gnH +MTvm4Y/IvAWECmVMwwjmLYp9HccROi0zmFbLkYBTXmf5XzQDsHIB4eohWY03gG1X4MgQMPUatdVN +P913kXqG2jzU20C1XdAQDNUW6DHYY1E4wBVKa5jRWlV9xoMiIk6UYiPd7pqF3N5tqb5pI50HuFv+ +eTv/zZ5tP0q5U9/2/nkP/hS/+ADi0CygmgKgGh+KoWHkhXN006OgYHgoFWNg35A5Q4e6o2FRGBR2 +28NU4DMnxel9zEeRcFIPHSUdp2mQUIBddPo5033HmaW4LMlxxR6FFvewJ3oYyXnO+SIGOjqoDOpj +bk/DOtvPGiKaDRGNhpxkfg+nKWYz9dM8AM7qRTSTVkvv9DDe72E9Pdc/szKoC1dkGmAGSvXTR3hx +mXeqd2r8Q+WTYYJlGL4J3YRv5NfLbba1ylX26313oA68jd8sb7Tdpmy23+n7jeuFPLeCwn6kQE27 +huKcwRxA1+Ecug6bdL2vMbzuiAVbxrnJMpTIKZ3IKZ3I4QKJdU5dAy7gxMipOomzF9/dU+k3Sd9v +kr7fdIL41yU5zPWSZd3FZqFis1Cx6VQpXucxTXXNo3uIZ8fwF01ZwwQMc56czsqbrPLsrm1hQ2m4 +rLNsoCh9oiukBYAJdGlaBU2GaKCzn+gs0xhXMOROy9o21AZ2WTeM3FDGFoJB0T2IsQW3XYwytiDm +sAXmBcfx+IjqjDFmqsoIcvLyc7hBLmvAK9as+vDI0U9XXrHlztSZt95Knbn7ss0rl2+6femyraOn +7pi1cfeeWzY8wQXL7l2x6+33di39cdngY1sPp0HNP7r9V3j28ttuXbBoy21n09N3zPhZ+y1P7UYZ +fx+lrDAqJ/PO+RT22yIg3UtcINvPMLSkQp7JBT91lAyieOl3McR0MX+Jy+8anLANClMP9wwH53Dk +owaMmRFoV13iHExVjSJqfNPRPpZoqWQct5INOOAsJSKVyq93/ifrZ8hpxDl1SS9n+pKL0eJ/qHVg +Xf9WVUVuRfqU0YELvXrsEu/c2FJulfeKwLLY9YGbwtsCd4Tv9z4ZOBz41PuhdkbLO8/7oHePlxtd +tlgkg8IzHAuoXhWileDXGwxp2EOrjYwrzcH9SA7uR0zcpzCuRbaccrb0mWw5W045Gx6luwYqWzsG +U1m7F2StSQUlJhWUmFRQss6VpQKX7iKuHYkBVAAiMEMBGfzPqlznROAhVAq6VSx9ojuqiZrpf2jD +Lc1MAPI2hyEAYcyzShWThLleiKwANNSpsWREdSmVfJAiQHy3i3kW45iht4fh/Zo93hsXzrqpYSQe +eeiKfWex9ML2vhuu/8cjz7xNXnl8/bVdT95408N4lnr9lRdu+NMaxd+4Est/eg+r96f+lvoi9VGq +++dHuOqf7Dv2wDYQf4DfBxDCm/k4W+Mz1pU0sBVEyULEOp6rwyJvJXWgdiNCfYQPy5k1hzYqy/pU +Ywkrs4ol8LK5CFBvLANUeehC1oHjx49zzcePn33i+HGoka15sBod6CFWY8U62622H9getZ2yCTCl +cWuNdbK10brEutf6vlWyWR0SbYlUJ4qCg7c9baXrIzGhjmeN24iQIEp1vHWUbbRQwdfzROMx/7DT +bGjd6ZNgiNGFEWqM9ff3qcYqCWs6Ul+iYhitbQvut1kHdKAitwvZRZPjmWUTsz/m4gnd85OaKf1B ++AOagubiEaxfc/moqnmj0ZIR9irHJMdU/8To5OLJU6c0znZcX+bwlpThuKW8MF42IjCydkJJo7+5 +8JJoY1nj1ObGJf4lJUvLrg5cX7i2eJP/tsC2wjuiW+IFDrXBgbhZVCmxOkuH2RpsxCZ5D5Hz0QQ0 +jRzqmTCas0aoETMaa4k1CZI4iKejUnJoX8X5xU4JS73kVt2pNoxFxe5dzuJh6hpQLg/iJ1GQPNhT +P6q8GMpbUIw8qFu0EXhEQdPcbZm1r75+aom09J3uh8Fs6UMVfX0twDtOwjDWt5wEyshoNNQBF9SD +5eUVo52lFU6Hc9Ysm807ehonI693ghwZTddVqurBjKhnhkSVu7ayvqoiY1OUUNSnTJ/51X01VZyB +8zUj3SOqSXGsiCeefDdfpRXXVIkiHysqLi6F0jVuFK3k6fofMz1K4zg/Q0tANQ7C3z7u4ZnNuy9/ +9Iu1cx+sLereES4rHNG4dtPTqT3HP03d9Ic/4B98iUV8WdPeqq9ST/3j3dTtqa8mzF58Pf4V1r/C +d6xd+Jt9f5o0J9+e8t4ye9SNbedvWai3rdAfnXbJ8j9tfAjX77qk5Sf9C7c5g6XnNWD79idw0c// +nFr26ZepB59M3nz52xvWfvCjX/z59DvYibVXXtrzSurdv75cXlqAL7z93gm3vbJ0685xO34L2JPu +BzRuFg4CFTpIB8WecYVgJ3+V43w9m4UtOflCDsybcI47RuSzfhlF+WXmlm8MZgrFRJvtl5l7T5uZ +RDEz8blM0Wp6c7zm0oNp6NtMp5LVanqOTMDiMJth5khGzv5G7HCqzJHyRU8G+IrJT0LVymamETLt +TmBxhTpMXSYvt7SqW7kd6kvCC+JR9ZRqk4Vm3Ega1OW2pPpP5Z/2fzosvMLbeQdns1oEnlfsDlmU +JAVgWVQkjBDdTOBkyxqapOTDJcJxNM9D8ziNV/LhLktYEOSwyIm9ZI1uQbLyiU4wIQexDXQFm+5W +NLRE4i5u4F/l3+O5HcBvejHWbQ3KUek9hduhYIWeq07pVYlskNolIv3A+eYfDX5UAAH+/EA6gQK1 +rw8Ioi4ApFRHF3H76FJkAkz5LUP9LDW2J9TWblGPHXMcO7ZFMFJgWtOStlnTkuGZ8wx5NK+ph3dy +snQwfYpunDD0rrVtLd/jIMr+gp2y2MsN15VVsowwMD9ZwYQRaD1bcACSjOEqHOOiXF6Ui5eKEkeq +fkea3nm6/ycPv4X/cd/kolCVcPCbyfhwaiKZh3ceuObOO4Cv7wSb6hPAZRez4t811mkBwfQyukrJ +85NjjbGlsXWW2yzi5YGrhDUW4P/CrTax1Gvh/KXlYW+hBfTpj3Pw/ePvLh/q/kaLJc8dLi8vK0Oh +wjBMUCQcdiHZD/emsvf6czQMP2gRCrvX2uiPiwo1Z8Te9Id6CVWeRDdVnESRIoIo05aKDPXEfIqW +4uySAc8daLubz1UbS+JKiD5XsdKnKRSZFfosJTAY2vgdu91qmuVhjS3CaZkVuDNMn2NAZvXtmx6G +tQYgGutxVrYG15IYM9+fXV9rqeun7seL2Pl0wwdu/M4tt0CA+a0DuUhVS7oa4q7FzB3O1uKCXRZ3 +OZike1e53RgZSyhIxoWGlUpy1HS6OO+K5nizHSSGo5XGako8FoVrNYwRA7yTxHe/sm7psk3b57b/ +alvqB/i8jaMumDb5lgdTf8ZXXBqfMG/07B9tS+0RDjYfWHLpz6pKD7cv62wdzl3s8i6dPnV12be7 +JGXUyskXX0dXV5amPxKuFl5HhbiC7ZtYRFYUEmyYrWxsPtYXUEhDlfZFaA1aX9iObivcge4XnuYe +tx/geuwv2l9DJwv/WehyuAtdhYVcuTjIVR7SIlPsjflzPY0Fy4WVhTe473Dfz93nuD+0Gz9Gdrv+ +4MhD+Sig5qsBnm446BpUyxR4bVCt6gQCCuaFFS4Y5i1q3HkBimugaQciPnPSfeak+zKTbm30xTUZ +A19mp/ZGmWGKXBBeNN/Ys5RoYRMIcwlAxhnj8hlblVroenYigdcGdSvwNd6pqgof7OUqe1bxFiUP +gK5VCmfMFZW0mWUJ7GOCE+bEXVwFUlOKU9WSylWqXPI9z52Xev6DvtQff/IsnvDcX/DgMUeqnvvB +k3+bf8WHmx99n5Dhn3/7K3zl7z/AczpPvDJk1z2PpD6/+1Dqk47DVAN6EGTYPKB7J8yLoUu6tQie +IBvU6VLDTiT7BlDRwB0EJhVF6MBYcIQtlVkYSVisbEeFn+UwomISIhApVM1hVa0Zf7NqKP5AVOp/ +TVT/MonqK5Oowt9DVJnTlgGUNHzYhOv0kVxQkkVZkHmZFwv8AT8RbVbgAVZQYbz53jwvJwY5XxS7 +HRD55VAUe62uKErQde9y+G0EvakTqd9Pahkq83l9XrcnnwCNlUQrM0uWpUBZD+Kvn553c/P6dRdd +f/fxTalOXHv348MnTf/xqov2pH4jHPQUXnhZ6tVjT6RSTy6s3DNy+KRPfvbhv8rDdDfNI4A49Osy +NtRn7FcThbAsSxLieDplVkvYhmSJ4ni+6q6WZnMXaFbNTqwBO28hWRlvrg5lmZnl/4GZWSz/gasp +Yy7JUEFmCqabjK1l+umT3+Fk1CoWZMa1BAEjizmU/He4ljGcnmgmPMIXn32QS5z9A3ebcHBPqv6Z +lH0PHRsw//lNMDYW9DobmyI2NttBjTaHB4bmAY1oNkICtv+f46HbjJ1BGfaV+s5oWMfM/4+jcdLw +K1L7dMBI7Gcj8W9D4P73EdjNvXP2A5Lsb6C9H72nfym09ArgrweAv5bgJ1jfA8H8oIe0luJL5Tzs +5oqLUdTtIyUoTBgD9NDWYiz6wg4uGhYtGMdLS4oHUHpxDqUXZynd3liscRyMYWkrW107yUaGKYWZ +Zba3GaYwpdBBayFr20txaaE52IXmYBdmmWphXLNia5apWpn7xVoQX3TJAKY6XW05kxlJlQ0lVXmy +HkoYTjg3lolrqWUDND6RjwVDgVBBiBOVuFriiUficgkfj5X47YVR5HXmRaFwfp4mwVmRUBLFIRsQ +e74LorAlGkXFHERs2y8QPbVCs4oYJX/UEtzP6cXFUQdzB+9dhbGDesYq968SLe68PIePsXQHN2C9 +2cV2x1C+PqLENYCze33SUAKsnW7ppEYTMAwXdyG5YnvqtV1/Sj3U040b/vwQxvfEn41etm/1pueu +iY7agsndN58aS+qfwf0n1q47gC/905t4Xc+y3h8OW9M+feZtM7Y+dCz1VfvCGuwCHHkMuH0R5R14 +sqHh2QETvHmeap4LW6y7rK9ZiVUgxCYDVxyACnIOKsgmKuxtlDVJEulaLFPGAAV0G1PI2DqOSFdb +PEwpw0wpa2m3YzuxmXhgM/HAZuDB/kabltnVdFS3QqP+C+KTM8SXIwu8GVak2bFmb7C32tfY+THN +/kRLW3Y7U1Y2GOiUqDOwiW0srG2pYAICg6rNWWFWdfsqjkMY1G1ZIIwg68/p2nRxjW4ZjUH82HPk +m+ee6xeFg/0/I/O+mUy6+6dDb44AY9oIY87hBrYOS8z+cyZApMxAcACMs2fMuK+zA45MGIoKiiEw +OQCyRb815oMVzcD7GimvI3STV/eo89hmr+6qaiMdMsxIB5UZaazESAvDRuoPGJvDyu1qtSbsEJ4V +gN5BX9mOdqEk4iuQjhrQe+gUEtwaZO5AnGAsutO58Wfm7O/mnH1mztkZXTWMODZnj/BvNucI6wnz +m7rawVJraW5bW9efNYHoajxTnbL2TzcwR5LZ6UlH/8hz1JqBcQYLRriY4jaZzfhfmCuqqZUto0ut +I8SR1inWudxm7o+cdLX1Le4tEPGUOzHVZJCwje8QnuI/lQUrj0fwb/J0X/oJ3eKOVnMajUBt7FZq +3TS3G87lTMrTtJClR7vdXpr/rj6hAOosKTlPthQUnMeX+/3jwTyRLFaLbBU4ntcEa74gwBlQjgim +rGi1IoHwGFAA8MvKERtGfC8ZrTuHCXiXkBSOCicEXrhApnm2YRLWwDRNSpzUSzZ3/0dKAhZq0/5f +TZMvzgnx3dTsTZzTm/pb2vqo/5AywDpKJnV1NAANUNuX7vuC1M+2SUiyWifXgaXrB0s3eM7Spcbk +n0Y1G554enKqW3HRoT2l+wAQVYerWlYdarWFQlYVUC/ztkVz4hwC0K0VLksRjPHgglqehqJgLSDf +u/u8AHprRToFNnetXJRfy+v5tXRK9pYA6KnNsaGb6ZNx29qWBKLGdhAeKYo8B1PBqJwfQOVVIFKq +MriGoxj+JNfO58ifsNR/H7kljfrPnAKCLyN/7P/52XvJh5+meAMX+XL2TsFjxi57TIDDCkimfs5e +8sReiWSpnzMnj8vqHtx/rYud+Y4OLH6fDvxhi6F6UTUDcaaaxXqZ5WIe6NXvgZb+yTSpexESndAD +lTO4llxuMxgPAWDAzhAQBwYzlh12FxP7QPEACHS/5iAKKW56WXAqnAVhIltsDiRbiNUm0v7ZVNon +G/RpHy1lUxHd5JPp+Vdmz8/2DNh/Tdcl648eVV977SjdEpbIoAgy92NHJMZkRBZzLOZZLLBYptge +oxBh2hsoB1RNcZzzYFlZLJkOLpkOcITtaROwolnd1U4WCWCtYQfo2zCYbE8SfRoD2EMOkUbkRipp +1O0ZNVE0p4s9FtFVzsTpitPMNAGSMjrTksPxDCII6hsQccr5JCjzVyublV/DUCpTlalOrowvsQ92 +NHGX8Ffbr3Vsscs2Isi19pGOGWQaN1HS5en28Q7rveQ+bqe0U97NPSGJbuJ0OIYJBBgRkRW7fZgg +AygrFzsvxjomRJYtVhswc4dDpfPU6m53E/dBshvEyvAuQZN78fC9isVqOhczHkTd0mjVdGWDDdsO +Qrcd2AZlSS8kTozGWXMWgxDTHxQq8JHmXKNitZc07teEVqFdAElCdne7qLwuoK9JtNT5+ykCM78b +nAVyTk+2UPytY+8FmUdA7WP+uC03MXccJMOHoazbrekXSAEBKaffRCT9JnO3TUsqcG1QLqOyp7/q +dFjpxcxurzf2RWsdg6Nsx9e+mlpHZQ0D9w6B3MyurkTz2rYW4CZ0KQnR+bJTfi7QF1qJM+Ogo4ch +tcCS9I2swVFQG3AMu+7FxfiSYd6CEXgBFg6lGp9NNQkHv/3i7vMbfsKd/WYy/8q3I/gT31KO8ADI +uAi1bwhhVMn5TQexbPreuhrdNlOBkv2Kly36f9yTAU7rMZdr/BxZYTEBuS7JIIZkInGcbOEJsUgy +z4Hq921W9eNyVD/OzN8LPEoUBVPMC1nVTzBoHTQyPcAIrkWzYc3WYGu1rbG12wSbnGtrZawvzdD5 +7NDk/87m4r+r9mVtrhytItGSqGP40tJ2+t/1PLb+UVu7hWfIYsolLn1iP4gjWYMIsY1Xw4dR3R8w +oUfWJ9fCEB7dN7lW1isNsLJWAmlEHUv7CgCsNECaGzPeb7HFaiVHPoQ8en56Xx6AhQZYCKCHgl91 +ZsUTzqF6A5EUTkZY+nexZHjxqjBVPbHrgRc5cvDFsynAmo38BsCY9m/b6TdJwCJ8R3gDOVAQGzrR +tIAT56v5+UFfMMjzKp9v89mC/JO+fY4XHJzP5w8SrVB3zcib4dMDTUKTZa46x7Ugb55vgb8xMDd4 +h+8+ohaEOc4dtlk8A+wDTw6SeEz7YF+jJ66B7f3LnFdJJMBFOr2SqRhKdJMInVSJrojTeZXMZXmJ +TjhjylKgvRAXOk2R6TRRyJm1IJ1xijnZt0wypmReIxJz+G5BaNE5i9z007VkkWX6v7960tLSFuy0 +uZmDzmbhCpg1x3E5b5PQVSzq32FWXI2KqiqRq5rEY0VoEd6KR76CJz/dk9p35NXUwd2/xoV//DMO +XvfJ3b9N/ZG8jK/AP30u9fhf3kvt2vtrPO+XqX+lXsXVONiNbT9IfWB45vh+oHU78uOhhka7xLUy +n0xTp+Vfol6Sz9uUMDBy5PMbfg33gAn53g3e3Y3uuHwIpsfwxjsaZebxltWMUDytu+k4yQEtgOEv +4LebI243R9yeVVLs/68Oku+6iwpydZVzXvA2Y0oy02H6i5hRRk3uTofCHCUOB3WU+L/fUVLpCxOY +l2jUBXDW6UbK7pm+6p7mz1IvpbbiGw4/2HLh8NtStwsHHe4l+644lOrvf4bD2zbMv9Vjp77Rh4Hb +7oEZ8KMisoHNQNRtc2D3yNC8yFL5ighvYS/UyCyWWFwMTIFhM3tthQKKCdhMwN2bfr/bHaiG9FR3 +UWm1i54XllarmdSZSeH6n7oL48Z1KK9mUnpdnwpAieOC0AXaLNv80BWhtZZrHdc5N1m3On9sf9LZ +6/zY8ZFTBdrRXM58l8vpcioWd5BEA16r6KZvugh+i8XrCxSEfb9MH83x5x41LHafD0WLGF75/U6n +Qw4PQK6Bm6Wy3oBw3PGAaL5JJ5qYwNwABcwhILJVmRateE1xezFXXOQn39kZlUUv/3+LXuJ/lAUx +ash81/+WofiCk/6MT5gqDhksA1sHTmor2HstxmstQvYtw5wfytisulXWnbVOdbTLPZqybNzGdAYH +cP5AQa0LZIMbgkMP1apgkqhFEQhZZt8c7LIUUBeRbltVUICwE7g8LmJMJoPPhp/h35ZsfF5fXowb +SgCjYwy72Vaq6MOk49hvrn/59emD5lyYPv3cnCvnDolO+yt+eNPOi378aGqYcHDGr6974M3CkuKL +rkq14eG3bRtlk/qv4qpqrpuynL7BNj/9Ef+/wutoGDeOrWW7UGnOrp14Dpzd9wrzpWZmsMAEAgCM +i7By9pw1QCUHtuXAoRw4aMJgC/kzCEFMABuAPqhxEbeIX8et5/mS0hFcbWgCN1W6sHBSZGLx5NJZ +XLM0v3DuoNvzHDHqkKTIU2wCJSYQN4FSE4gxvDIKG0CJCcRNoJR6MCZTaJA9XkyKudKSkc7q2MSS +SRXztMbYnJJVthX2lY6l+Uv819mut1/vvEm9qnhdyWauw3a7vcN5p7qp+NaSe+w7nTs94YyZMiQa +dwfjAUu8DMcRKgu4+crhcbQEWI99yHXB24MkWOK1DwmXluASwStkl1CE8BBLOOzlmKii7sQWw+9J +kxb2jkxFn3EE9SElxQ67TYiGCsNBWQIrl4i4pLgI8kQhHBwS0CkNbQde3+dFQ5hjmClwKtZwA27F +a/AOLOJenNSVIWEtL2/8HFqxQEnaTs9oU6AHF1gGbKa05PAHy7nNlJY4KsNlVMw7HGROGe0PI+Gy +QGXU3PgVNTlB1NwjCWOE426qadK73CYHcGd3BbhnU0ZRMDzjLG6ZfjJBd0RnVuFMuc6W4uiLeGp/ +S4LuiEmcpiMFRE7VKbqS2kz3wrSdo3Gce8IoPrgfB/GQoHeIwEzoITZvmEkfL2euVAC5GlvDwqSq +MrPYU1zKNkWyN4gya3eefJ+X9zF6FkFziM/fb1/w65tWPzWrYf6Y1KqZly+7+YsfPvr1ZuGgc8+T +yYdrR+G3mtqv3/ztT19M/fM+/Ef1yjvnjl83cdKymG9houbRJat/tfjy32x03HHXxktmVFWtHDRm +79VXvbpuPfu6wTDQIg7SPSrYxywG0WS5kgmIpvdR+r96H0XT+yj9X7yPwL8FEgZkQ+wT05Zesq5b +M7Zc7Bc1TCrorleM9+KMv/dj3cb4vJxh8l+Y/o33TW5/1uTuKcNypk+U992X6+qAqQcF/2TLhyp7 +s74+48bN/oI9SBbZi/+gxDH+WlmfeamGvfGflyrkO1JBwb5nzzf/pGP3MOjP1Fuej8cYX2+IO5v4 +Jvklmff2Zvzm1fwYeTJ/gXy182fCx05JQcRFXxAVLfkDBGZ+DkHkmwKzuzE/TkwLimQtKKJmVk9O +GBYUadG8WPM2eEmrd4233ct5/6NWtq/RzhZSTPvPqmXemjKkp9WkHWtWelr5jMfCkJ7WrPS0tnio +JXVOehqewOkq6MS52lmf8YmHBNXLRBeM7b5VogURm2GqUGUMV7ky+vEIsFaMvb8uvvW5xalv3/ht +6ps1z03Zc9Ob+4SDZzvfSZ199C5s/4SbcbbryN7LnmPfAkAW0MIm07fjwECnGCwMNl9xM0UDD0BG +5MgD1iy+ycJogAmSu5bxRVbkmINJHFk6sKQ/zUo92YR7Gq35dvsvM8/90MzExZm1bGIC1oBpndNi +mV1euNj0+gOQ40jT3XTdj2nyViRYZAEToeKd4+o7x11VVciw+ujm3uIKAZejQVyJtUIZprQqt8u3 +W3YoR5VTik1TGhTCE5tMMhueLVixMT9ffT3blAR3Wy0WTRbyZVlAQHxEyCdEsEBVn2hWJFuWyHgJ +JVBERi0xLjUKJbXtrvsKMyAwIG9iago8PCAvTGVuZ3RoIDQgMCBSCiAgIC9GaWx0ZXIgL0ZsYXRl +RGVjb2RlCj4+CnN0cmVhbQp4nNVaa5vjNhX+7l8h7jZLXEuybrA7sEsLpdBC6UApk6U4sTOT7kyy +TWZ3OuXy23mPZE+cSEm3tHxg9tE6kXXec9HR0TlSPs8qRv82l+ytpmKX2+zZeca578ND2IqZqi4r ++uPs/CZ7azGpJnjHzhfZRT4VQtfFROgKHzUvnp+/F0bwfsTjM7zkj4n67OHlA3mg1CAV+Kisxwud +A6oUZkDl+4Q8bwqdz4tDWFEq7YQRjJdc6krU7LzN8k8f/orzz7KJKJ0UrrJscmTUaLAsgYZ/NFZa +brnzY0XjkXZvRSlUrWqPdAHBVL4uJi7fFhPFHalmCtk/SWZRVkpoZ1lVKkK1HpUrQh3bkIwnzobO +XknQ8MrUwstUBYHMIaUuuZZWCnCAgNwKGkZw8hBumIppLiJr56siGo0Zk4HiUZgKNS08ofOe45jU +phQ064JpJfb95yRyQH0SHsGhID3n5Iylpj/jlbVEunPU2hIPbi1T0pTSWGOI20X+cWF43rEZzUR3 +uVwFcdcFr/JXG7YtXH6Ltt4UE1nl92y5Yh90d+yTQlPni5KdX3WbIqsBsV7N4W8du2u2rGGXy801 +ewHS1fpuxWb3rHvdEQjP79erjjWrlq3WoOlK9m63YVddg7dcZPktC0qFmbPOueAvkBAydSTY9Xp1 +2bXslj6v2XZ90xHk3dWazQvTy+7HtSt4k7h9UKO5L6cZiXzPrtevAdE1cxp4xTwRDYE6bFNAH//i +xXW33V7fl4HmZbMkrjSIvYTWMNlyTmNL9hG60nK3zYpcPchLlvUzt1hvLrtbdrW8KWGUHDZg7YaE +fw3oZksj8bajGbD5LXgBAeMf5F0ti4nNL68KrJzbwP6mmJi8aTu22HhTLbtVu/WG7lbdzbLbkqVh +8WlG6Nf3R+Tdkic0d545uQOEu1pug9QQETPc3Gy9QVY0faxt7jGWzZsbeBHEszl5xXKa3SyJeHXJ +luQPi1cea3UfDLhcXQbFn726HVyjCq5B/pMW7a7x6oJ+vodB2l/3sYSmkcwKj7Y5sLd4t1hj+CsY +AjptuyIj11h4y5DveIOu74JcxUTngYuXYyfZvtQXXD73MiIq+RVmscKYkXq0ki/yc0/6yR9gvCp/ +51e/L/D4wH85D8vsPLz5qP/qQx/ivMvfb8gHvZZFlt40KgpUNZo6snHw/CLab4hGHwa3/LmPFWGj +w2MXKwxXZSWV0+YwNI0DB7yJIgfzVu491i9NmE64EDTIaiFwmGoIHHD3bgNvFMpHD79ZGR9A8CCf +2XmBCA7QBxWT0+D9uOKdx8efhh6ILrS/BNghxtAybMK6vY3B9wMMmIQYY0OIwbPzvRRoiPs41rhR +qKHlVQ3RptdyCDeeJ+03+IcHXEbU1dHkoQm2pejRdovLq+VnL65vVuuXXs7PNz6ovXp998X9lw+6 +BE2ekqM9+yWsz/O3C+97v373N+/99nc0C+9/QO7tfRGOiY8fFhj2Rz/4o/M//dmjf/yXT/4aHG/P +wbkZO3iO3UvWShvrDnfWC3I00zuoRrNo7mwiqb9Bm6HN0VpyYLQu4cRT7MmU+vjNNF4EoFpAEpKG +00aKJtHq9HJQhvBqNIWmv/Pd731/CpIf/PBH03xa/PjRT8g2k/Ktn/6MPjx+cvbzX1xMp8//9unf +//HPf/07cpapqCFbDdlqiQbMGmlabaZCVWh4p/BO4Z0CT4V3Cu90RVkgGvo0aDSldug36DcYbzDe +mCnUQONoAk2i1WgKTaMZODDmaio5RnGM4hjFJYmNJ0ZyjOS6/w40URXcUqbIadLxFOSheEqEQgIS +dd+hivBdU1o5lRIMJBhIMJAQQwJcAlzq2B5SgqCuKH7iyfunoD0JT9l/r/un6vvBqDYFdcFsEmaT +MJtUEIyEh+mkAj+YT8J8SEPReFCMMmJHT9k/a++4+KD6DhDBthK2lfAjCftK2FfCByR8QBq8NyaO +riOt4GIczsnhwNxQRERzaHBgPgvOzOHEHE7MO7RF6KO8TsAlBT4IeZaFTqwE+EYFH6gw3xUqhwoJ +diUAJrAaBIAEgASABPxaUhoKEMxehYmoYNtKZgCAFSooVUkASABIAEgASADINkggASIXYfXVPPTV +AKoBVKOzVhn+A1ANoNr2AwBWA6wGWA2wuj07mGUaBOR6EdYsMmxk6GgyACggK7xQAFYAVpBQAVQB +VAFUzftxGfRUAFKLEBw0gDSANMTTANEA0Tq1jjHzAi6OsJMMCBpcNbhqcNVN4KbBWUMdDaYaTPUi +RCYDpkZETIgBVqIUqZBm5Jm09ISQRvWfoYEBoLEJcxkIYqC+gRAGQhgIYSCEWYSQaCGEheYWmluA +WmhuAWgBaG0yJkqT2tUtGFkwsmBk53H5Aq0Qa/qFe6iW7d3GdkNiYSGfq5LGqVKGcdDDQQ8HPVwd +0Bx0cdDFmbQeVVIWB7O4fqdwUMjRTkHIsJ5LeaSDOd2CdpUkG9TL0vjMCo3THo2wL5Ksm1QJDgoE +ZURIcr0qdjuii6pPIkOsFhWYVirNTCaZ8fQZAVmjgX80qWQPVBBRpvfIBjPQpGaAsfR42L+BMzWw +fQPbN7B7A69tYOZmETbuGWZ7hjmZYbZnFGPoidmegdeM1lZGAwA0A9AMQDNQzQA0A9AMQLNFYiIp +HZiDcC6CvnOgzoE+V/13oM+BPgfwHMBzAM8BPAfhHMBzAM+zPgZTXtECrIWYLYDa3ilbiNkCqAVQ +C6AWQG0fKVqAtQBrQdx2iJAt9O1itwoZG0dkRV4sZKmltbUN++D/tJweVUW0HfTZ3gsC/1pVNtWW +fd6+K6dRGIesPc3lSJU9znwP8l5s00Pii8XjS9fMxaW2olLb7SrtNPtU/X1QfvtCW+8KbZaosadZ +XGWnGYba21Ht/U1K72m2X3zHpffYxsnSm71J1W1C1X0DMuQbuS+600x8Ke7BR6U4leGeX/AfqtqG +UhzVmKiiojs7WnUbqrqP6BdqcXKch2L869fi2bgY9yXK7pTLCab1+Dg2v17e9BtWdDy6iI5G/RvK +kfP4lO+LxCnf/ple7fSpw7wv4o1zOPxtsOGh7H9M0fLsQCXDS4c/7RhC1gibBs/ijecpGext+u9Z ++rTZs5nzgTUwYis8pYPYUD8+iywxnOjWbHe2SyeOL9P6UToxm58dogyT8PkJKZvmCFmVbzzZhMuy +pjNlxSbhgPpp+sSU508Kjvoi/zRiB/Mii+V0HI46y/L+DHieSKLY/h9FtXgTJdoF0Q71OHW0/z1Y +W/3/gHXfAIzHR/jacM0Nk6VDTatUOP/3lwpYCLW2RvkIw8N1w6H7YZAYDSD5bMwDdRjK7hGOv19w +JWpHVETUjxzW8f7S4jV7kpZ+ER2qMfkTVrPEZUVXxWM9zyGMcOevBmrJlFElx+JX8uF0aJR2urO9 +I37hg58tDQis60NE7MbDjCQki/02mtAT1PEM2lJjaVrJaq9Q2AZCTn4QkvPZsZuON742i0LELBHe +h5s0t3eTFiMcvTlDLa6P3Jz192ZWazqwPnVvdurSLLJMc+xwVhtZchTtysRHh99+Ghqd+Oxd89g3 +TEAjlFMZafqCx371oWvE5fDGh/s01B1moScvfMJJ0Rj1ZZ/s+eudUQ6qTuegBxloljqL/tayzwj5 +MB2tKB3derqHzHPIOutx1hkhpdJQt0tDhyx0nG9GGMfugg4S0NO3PlkeZfKDGU/eA71Z6hnP+y4Z +JTGPXQyNfiOgSyqSEQoUclMKz8pF2eEiFVJR7sdJZ5ybXiC3TKWyEhkqNqy9m/XhWVfp7wf9bPCh +MT8kXXTipVnlA3wd8r9FMoS/eTZNMUrkj478mgGTk4rnxcPNmU4TItQfyfP1183zx8PoRxQSVhj/ +voBdhTv3EruwxiSWdGIcXn15CJpjXkTpUpPuoi174oibrmkj9QZ38U8oDtUZXfhF+/Kg/qNo+Ghj +jY9GB3M//Pyg54+Fs00j+Sz+xC9bRgqkJamOSXK4iW8RcU5AHBfhOOERK0zqkg45K5pgIWladhs3 +0lIkk7W0o3xSpvJSg1H1V+emtaCNfoelD7EqpBuK0kPKkQckN2RxI/RU3+zYuLEUUNLZGgFr4BAS +ZMrApa2scqfEm8Q6HFNVcVJjh+WvFN85zz7M/gOSUtWRCmVuZHN0cmVhbQplbmRvYmoKNCAwIG9i +agogICAzMDk0CmVuZG9iagoyIDAgb2JqCjw8CiAgIC9FeHRHU3RhdGUgPDwKICAgICAgL2EwIDw8 +IC9DQSAxIC9jYSAxID4+CiAgID4+CiAgIC9Gb250IDw8CiAgICAgIC9mLTAtMCA1IDAgUgogICAg +ICAvZi0wLTEgNiAwIFIKICAgICAgL2YtMS0wIDcgMCBSCiAgICAgIC9mLTEtMSA4IDAgUgogICA+ +Pgo+PgplbmRvYmoKOSAwIG9iago8PCAvVHlwZSAvUGFnZQogICAvUGFyZW50IDEgMCBSCiAgIC9N +ZWRpYUJveCBbIDAgMCA2MTIgNzkyIF0KICAgL0NvbnRlbnRzIDMgMCBSCiAgIC9Hcm91cCA8PAog +ICAgICAvVHlwZSAvR3JvdXAKICAgICAgL1MgL1RyYW5zcGFyZW5jeQogICAgICAvSSB0cnVlCiAg +ICAgIC9DUyAvRGV2aWNlUkdCCiAgID4+CiAgIC9SZXNvdXJjZXMgMiAwIFIKPj4KZW5kb2JqCjEw +IDAgb2JqCjw8IC9MZW5ndGggMTEgMCBSCiAgIC9GaWx0ZXIgL0ZsYXRlRGVjb2RlCiAgIC9TdWJ0 +eXBlIC9UeXBlMUMKPj4Kc3RyZWFtCnicnXoHQBTH9/9QllvuuLOuXQ5UxK7Yuyg2bBgRRYqgFKkK +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 +4jf4TfwGv4nf4DfxD/iH+Af8Q/wD/iH+Af8Q/4B/iH/AP8Q/4B/iH/AP8Q/4h/gH/EP8A/4h/gH/ +EP+Af4h/wD/EP+Af4h/w6+503Hv+zk/xwz0W5971d36eHO6x+jp47c7fPHsj9rfoPZb+3pmD9v93 +J1n44UpGkm+Uw86P5Z2L3l/HauwwE3tOJNZk+3vsk3G186vLnmuVvw72iViG7g8e++tmX4ll4uEe +l+9BdZ3UvffbPbV9vV7XK6pfjv1uqlvp6Wzf7s+X14uq/O9f/gio4gplbmRzdHJlYW0KZW5kb2Jq +CjEzIDAgb2JqCiAgIDExNjkKZW5kb2JqCjE0IDAgb2JqCjw8IC9UeXBlIC9Gb250RGVzY3JpcHRv +cgogICAvRm9udE5hbWUgL1JLSUFUTStTV0lGVERBWTNCb2xkCiAgIC9Gb250RmFtaWx5IChTV0lG +VERBWTMpCiAgIC9GbGFncyA0CiAgIC9Gb250QkJveCBbIC0zOTQgLTExODMgMTYzNSA5MzggXQog +ICAvSXRhbGljQW5nbGUgMAogICAvQXNjZW50IDc3MAogICAvRGVzY2VudCAtNDMwCiAgIC9DYXBI +ZWlnaHQgOTM4CiAgIC9TdGVtViA4MAogICAvU3RlbUggODAKICAgL0ZvbnRGaWxlMyAxMCAwIFIK +Pj4KZW5kb2JqCjUgMCBvYmoKPDwgL1R5cGUgL0ZvbnQKICAgL1N1YnR5cGUgL1R5cGUxCiAgIC9C +YXNlRm9udCAvUktJQVRNK1NXSUZUREFZM0JvbGQKICAgL0ZpcnN0Q2hhciAzMgogICAvTGFzdENo +YXIgMjU1CiAgIC9Gb250RGVzY3JpcHRvciAxNCAwIFIKICAgL0VuY29kaW5nIC9XaW5BbnNpRW5j +b2RpbmcKICAgL1dpZHRocyBbIDI2MCAyODYgMzI2IDQzNyA0NzUgOTE3IDcwMCAxNzIgMzEwIDMx +MCA0NDMgNDYwIDI1OSAzMDAgMjU5IDI4NiA1NzYgNDA2IDUwMCA0NDYgNTcwIDQzOSA1MTYgNDAw +IDUxOCA0OTYgMjU5IDI1OSAzMjAgNDYwIDMyMCA0MzAgNzQwIDY3MiA2NDEgNjcxIDc0MCA1ODkg +NTE0IDcwNSA3MzYgMzgwIDM2OCA2NjkgNTYyIDkxMCA3MDMgNzQzIDU1OSA3NDMgNjMwIDUxMCA1 +NTIgNjg4IDY0NiA5NTIgNjM5IDU5MyA1NzAgMzMwIDI4NiAzMzAgNDgwIDUwMCA0MDAgNDg0IDU1 +NiA0MzMgNTQwIDQ0OCAzNTMgNDkyIDYwNSAzMTQgMzE4IDU1NSAzMjAgODYzIDU5MSA1MTggNTY1 +IDU2NSA0MTkgNDMwIDM4NCA1NzAgNDg2IDc1MCA0OTAgNDYxIDQzNSAzMTAgMjA0IDMxMCA0NjAg +MCA2MjMgMCAxODAgMCA0MzAgMTAwMCA0NjAgNDYwIDQwMCAxMzAwIDUxMCAyOTIgODcwIDAgNTcw +IDAgMCAxODAgMTgwIDQzMCA0MzAgMCA1MDAgOTQwIDQwMCAwIDQzMCAyOTIgNzU0IDAgNDM1IDU5 +MyAwIDI4NiA0NTMgNDU0IDAgNTg4IDIwNCA0NzQgNDAwIDcyMSAzODQgNDgwIDQ5NCAwIDcyMSA0 +NDggMjgyIDQ2MCA0MTUgMzgzIDQwMCA1NzEgNTIzIDIzMCA0MDAgMzUyIDM5MyA0ODAgODQwIDg0 +MCA4NDAgNDMwIDY3MiA2NzIgNjcyIDY3MiA2NzIgNjcyIDg1NSA2NzEgNTg5IDU4OSA1ODkgNTg5 +IDM4MCAzODAgMzgwIDM4MCA3NDAgNzAzIDc0MyA3NDMgNzQzIDc0MyA3NDMgNDYwIDc0MyA2ODgg +Njg4IDY4OCA2ODggNTkzIDU2OCA1OTAgNDg0IDQ4NCA0ODQgNDg0IDQ4NCA0ODQgNjg0IDQzMyA0 +NDggNDQ4IDQ0OCA0NDggMzE4IDMxOCAzMTggMzE4IDU0MSA1OTEgNTE4IDUxOCA1MTggNTE4IDUx +OCA0NjAgNTE4IDU3MCA1NzAgNTcwIDU3MCA0NjEgNTY1IDQ2MSBdCiAgICAvVG9Vbmljb2RlIDEy +IDAgUgo+PgplbmRvYmoKMTUgMCBvYmoKPDwgL0xlbmd0aCAxNiAwIFIKICAgL0ZpbHRlciAvRmxh +dGVEZWNvZGUKICAgL1N1YnR5cGUgL0NJREZvbnRUeXBlMEMKPj4Kc3RyZWFtCnicnXoHXFRH1/dS +7u7lLrsKchWNsmpEjb1hizHYsCsBCyAgKigoXTqogKjoSRBRilJFLChYsGAXe+wt0WhMjKlvuqZ4 +7jqL+51715Z8yfM+38fvt7sz8z9n2qkzg5XK1lZlZWWl954+1mPKyGG+fYdHhQerrGxVKpUVfdyl +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 +oQxVKaEaZalOOWql8Bl8gk/PtOAz+ASfwSf4DD7BZ/AJPoNP8Bl8gs/gE3wGn+DTa9DiM/gsPoPP +4jP4LD6Dz+Iz+Cw+wWfxCT6LT/BZfILP4hN8Fp/gs/gEn8Un+Cw+wWfxCT6LT/A5fILP4RN8Dp/g +c/gEn8Mn+Bw+vcccPr03HT69Gx0+vf8cPr3/HD69/xw+vf8cPr3/HD69/xw+vfsdPovP47P4PD6L +z+Oz+Dw+i8/js/g8PofP43P4PD6Hz+Nz+Dw+h8/jc/g8PofP43P4PD6Hz+Nz+Dw+hy/gc/gCPocv +4HP4Aj6HL+BzPJn1Cfw/T+SA3CMPyD3ygNyjC8g9uoDcowvIPbqA3KMLyD26gNyrDrlXHXKPLiL3 +6CJyjy4i95y9iDxwhiLywBmK+AKGiC9giPgChogvYIj4AoaIL2CI+AKGiC9giPiCGvAFNeALGBK+ +gCHh0zUm4Yuch4Qv4kv4Ir6ET1ejhE9Xo4RPV6OET1ejhE/Xn4QvIkr4IqKELyJK+KIe9fCJ3rVp ++KSw3TJTbLcMn1S+uQjF3hdLsffFUTrK8EnjPCyB0u0ipdslirlehs9m5nPJFHO2DJ8tOubw2aqf +DZ9tzODCyq/r1sLKr+tWZuXXdSuz8us6kln5dXXIrPz67M6s/Pr0zJ7SUQLFFZLjKH265LtPzMKx +5IXS7TKl3yyjCjOR6yj9lZUbpd/sFFdIXikMZaY4f2X4TGOUIhR7L/xy018axVHYy/CJrj9l+ETn +rESKuS6J0m8ulO49U8xLKYypx1IpZrc0inNbOsX1UlaK66UOn+haUfllqs/uKhRjVnz6tK749Gld +8QWOpeLTp03Fp0+biq/3b5/I4r5/IFfgnemrwFcdHvjKpVMH3C5MWG2UcjrFJVBXiulrM8Ue2oD/ +e6M0odhDsxSnqzmKG6V5CmoLFNQWKS6Iligmsy0Uk9kyxWS2QnGCGr6Mr+HL+Bq+jK8NX+yM2Wdq ++Xai768n93epr+8+9X3b9tee8cI13nfubzqnc//6Tna9XO9bjb//AJ+bDlkKZW5kc3RyZWFtCmVu +ZG9iagoxOCAwIG9iagogICAxMTM1CmVuZG9iagoxOSAwIG9iago8PCAvVHlwZSAvRm9udERlc2Ny +aXB0b3IKICAgL0ZvbnROYW1lIC9QR0RSSVkrU1dJRlREQVkzQm9sZAogICAvRm9udEZhbWlseSAo +U1dJRlREQVkzKQogICAvRmxhZ3MgNAogICAvRm9udEJCb3ggWyAtMzk0IC0xMTgzIDE2MzUgOTM4 +IF0KICAgL0l0YWxpY0FuZ2xlIDAKICAgL0FzY2VudCA3NzAKICAgL0Rlc2NlbnQgLTQzMAogICAv +Q2FwSGVpZ2h0IDkzOAogICAvU3RlbVYgODAKICAgL1N0ZW1IIDgwCiAgIC9Gb250RmlsZTMgMTUg +MCBSCj4+CmVuZG9iagoyMCAwIG9iago8PCAvVHlwZSAvRm9udAogICAvU3VidHlwZSAvQ0lERm9u +dFR5cGUwCiAgIC9CYXNlRm9udCAvUEdEUklZK1NXSUZUREFZM0JvbGQKICAgL0NJRFN5c3RlbUlu +Zm8KICAgPDwgL1JlZ2lzdHJ5IChBZG9iZSkKICAgICAgL09yZGVyaW5nIChJZGVudGl0eSkKICAg +ICAgL1N1cHBsZW1lbnQgMAogICA+PgogICAvRm9udERlc2NyaXB0b3IgMTkgMCBSCiAgIC9XIFsw +IFsgNTAwIDU4MCA1NzYgNzUwIDM5NiA0OTYgNDQ1IDUwMCA0NDUgNTQyIDQ0MSA1MTYgMzgwIDUx +OCA0NDggMzkwIDM0MiAzODUgMzA1IDM5OSAzODUgNjcyIDQ4NCA2NzIgNDg0IDY3MSA0ODQgNjcx +IDQzMyA2NzEgNDMzIDY3MSA0MzMgNjcxIDQzMyA3NDAgNzAwIDc0MCA1NDAgNTg5IDQ0OCA1ODkg +NDQ4IDU4OSA0NDggNTg5IDQ0OCA1ODkgNDQ4IDcwNSA0OTIgNzA1IDQ5MiA3MDUgNDkyIDcwNSA0 +OTIgNzM2IDYwNSA3MzYgNjA1IDM4MCAzMTggMzgwIDMxOCAzODAgMzE4IDM3OSAzMTQgMzgwIDMx +OCA3MTEgNjMyIDM2OCAzMTggNjY5IDU1NSA1NDkgNTYyIDMyMCA1NjIgMzIwIDU2MiA0NjAgNTYy +IDQzNiA1NjIgMzU2IDcwMyA1OTEgNzAzIDU5MSA3MDMgNTkxIDYxOCA3MDMgNTg5IDc0MyA1MTgg +NzQzIDUxOCA3NDMgNTE4IDYzMCA0MTkgNjMwIDQxOSA2MzAgNDE5IDUxMCA0MzAgNTEwIDQzMCA1 +MTAgNDMwIDU1MiAzODQgNTUyIDM4NCA1NTIgMzg0IDY4OCA1NzAgNjg4IDU3MCA2ODggNTcwIDY4 +OCA1NzAgNjg4IDU3MCA2ODggNTcxIDk1MiA3NTAgNTkzIDQ2MSA1NzAgNDM1IDU3MCA0MzUgMzQz +IDMxOCA0MDAgNDAwIDQwMCA0MDAgNDAwIDQwMCA0MDAgNDAwIDQwMCA0MDAgNzE2IDU3OSA1NzYg +NTUwIDUzOCAzMDAgMzAwIDUwMCAxODAgNDMwIDIzMCAxNzAyIDIyMCA1MDAgMCA2MDAgNjAwIDc2 +MCA2MDAgNjAwIDYwMCA2MDAgNjAwIDYwMCA2MDAgNjAwIDY4MCA4MDAgMzYwIDQ2MCAyODYgNTgw +IDU4MCA3MTggNDgwIDQ2MCA0NjAgNDM2IDQzNiA0NjAgNDYwIDQ2MCA0NjAgNDYwIDQ2MCAzMzAg +MzMwIDEwMDAgMTAwMCAxMDAwIDEwMDAgMzkwIDM5MCAzOTAgMzkwIDM5MCAzOTAgMzEwIDMxMCAz +MTAgMzEwIDMxMCAzMTAgNDA0IDQwNCA0MDQgNDA0IDQwNCA0MDQgNDA0IDI4MCAyODAgXV0KPj4K +ZW5kb2JqCjYgMCBvYmoKPDwgL1R5cGUgL0ZvbnQKICAgL1N1YnR5cGUgL1R5cGUwCiAgIC9CYXNl +Rm9udCAvUEdEUklZK1NXSUZUREFZM0JvbGQKICAgL0VuY29kaW5nIC9JZGVudGl0eS1ICiAgIC9E +ZXNjZW5kYW50Rm9udHMgWyAyMCAwIFJdCiAgIC9Ub1VuaWNvZGUgMTcgMCBSCj4+CmVuZG9iagoy +MSAwIG9iago8PCAvTGVuZ3RoIDIyIDAgUgogICAvRmlsdGVyIC9GbGF0ZURlY29kZQogICAvU3Vi +dHlwZSAvVHlwZTFDCj4+CnN0cmVhbQp4nH1WCVhU1R4/d4bjud6xMYWrqeCgLW5UEpoKZbIKiKzK +GigOiyP7AOOwyKKC2onYF0GQfZMQEc20cd/KNCJ7fWal9arXy77K972+c+nM93pnBkNt8c439zv3 +v/7+5/yXwwELC8BxnFVQiJfHOjfnMAeXlMQYr4zoRI0acBYAAI79naTJQJrFSdYyyUYuWVkYeWsa +bT3B7deV0Iax351senc8zt6ccoppPYe9LEamFk6SATnHzXZ4yTUlNUurid+cYTtfvcD2hcX2S+1s +E2ITdZpkOzu7cd+2Jue2Y97Hiczc3Gh1ckis7aZ4TUqmNj0jy3drWMJz6zYn6mI8kyri9KlpztkA +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 +Ou0T+jL9C3JWmrN+hOZR2YGd5qyRs9Y49LXqLfQ26T3iXnij/ZfFKtWFt1YvQU+qJ5wV6VkRzpz0 +zC3YKlOLeJsu7nZDeoU6a/fZcJcYZSzSQKZ50EkYAt9ndhondaXnF1p7vskKZW5kc3RyZWFtCmVu +ZG9iagoyNCAwIG9iagogICAzODMKZW5kb2JqCjI1IDAgb2JqCjw8IC9UeXBlIC9Gb250RGVzY3Jp +cHRvcgogICAvRm9udE5hbWUgL0hRUUtGUCtTV0lGVERBWTNCb2xkSXRhbGljCiAgIC9Gb250RmFt +aWx5IChTV0lGVERBWTMpCiAgIC9GbGFncyA0CiAgIC9Gb250QkJveCBbIC0zNzIgLTExODMgMTYw +NCA5MzggXQogICAvSXRhbGljQW5nbGUgMAogICAvQXNjZW50IDc3MAogICAvRGVzY2VudCAtNDMw +CiAgIC9DYXBIZWlnaHQgOTM4CiAgIC9TdGVtViA4MAogICAvU3RlbUggODAKICAgL0ZvbnRGaWxl +MyAyMSAwIFIKPj4KZW5kb2JqCjcgMCBvYmoKPDwgL1R5cGUgL0ZvbnQKICAgL1N1YnR5cGUgL1R5 +cGUxCiAgIC9CYXNlRm9udCAvSFFRS0ZQK1NXSUZUREFZM0JvbGRJdGFsaWMKICAgL0ZpcnN0Q2hh +ciAzMgogICAvTGFzdENoYXIgMTQ2CiAgIC9Gb250RGVzY3JpcHRvciAyNSAwIFIKICAgL0VuY29k +aW5nIC9XaW5BbnNpRW5jb2RpbmcKICAgL1dpZHRocyBbIDIyMCAwIDAgMCAwIDAgMCAwIDAgMCAw +IDAgMCAwIDI3MCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCA2MzAgMCAwIDAg +MCAwIDAgNzE2IDAgMCAwIDAgMCA2NTYgMCAwIDAgMCAwIDU3NyAwIDAgODk5IDAgNTY4IDAgMCAw +IDAgMCAwIDAgNTIyIDUyMyA0MDkgNTMxIDQzNiAzNTAgNTExIDU0OSAzMDQgMCA1MjMgMjk1IDc4 +OCA1NDUgNDg3IDUyOSA1MTIgNDAyIDM2MiAzMjYgNTQ4IDQ3NSA2OTYgNDYyIDUxNSA0MDEgMCAw +IDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDE4OCBdCiAgICAvVG9V +bmljb2RlIDIzIDAgUgo+PgplbmRvYmoKMjYgMCBvYmoKPDwgL0xlbmd0aCAyNyAwIFIKICAgL0Zp +bHRlciAvRmxhdGVEZWNvZGUKICAgL1N1YnR5cGUgL0NJREZvbnRUeXBlMEMKPj4Kc3RyZWFtCnic +Y2RgYWFgZGQUCQ73dAtxcYw0dsrPSfEsSczJTGZgZGFgYGAEYqcf0j9kunnkfvAz/JBl/CHH9EOe ++YcIyx8Omd+JMmwuv+xY+3iUZIFqu3hUgBTDAR5VELWbH0SeEGzkYWJgZWRk49N28HRMyU9K9UxJ +zSvJLKl0zi+oLMpMzyhR0EjWVDAyMDTVUchOzSnLzNPR0YG7SAHkJAWIm+CCDAxMQKcxMoPdx8DM +wMzIqJS3p3vvD8O9jHv3fj+wl3mv2A+VH1P/qLDt/ZMs+sPw+4E/hux8u783f38u6l3aXf19Xvek +jaxm3yeJHp3j8Vvod0zAb2vDwIbz34W+xxz4bvVUftEO0d8Wv/l/x/4u+6263va7+HeL7/zfY7+X +fVdNv/lbXJ5v2fc7P5xFq/JY97eviOk25fitHGFo65Gy/aFc95tZt27s46hbwPpbPCpAqVuR4zfr +lahP3wW/G7z5HvKdwX6nhtys7/2iv03+yW1s/W7yS46VDxHaPKBQUwQF5wKhmlk/PLu/x07f0Mf2 +u6qbHSwj/KND5Eej6Bwerh4ePoZWRkYmZhZWNnYOTi5uHl4+fgFBIWERUTFxCUkpaRlZOXkFRSVl +FVU1dQ1NLW0dXT19A0MjYxNTM3MLSytrG1s7ewdHJ2cXVzd3D08vbx9fP/+AwKDgkNCw8IjIqOiY +2Lj4hMSk5JTUtPSMzKzsnNy8/ILCouKS0rLyisqq6prauvqGxqbmltY27sEAAJhry3MKZW5kc3Ry +ZWFtCmVuZG9iagoyNyAwIG9iagogICA1NjUKZW5kb2JqCjI4IDAgb2JqCjw8IC9MZW5ndGggMjkg +MCBSCiAgIC9GaWx0ZXIgL0ZsYXRlRGVjb2RlCj4+CnN0cmVhbQp4nF2QwWrDMAyG734KHbtDcZJS +2CEYRnfJYetotgdwbDkzLLZRnEPefo5SOpjAht/6P/Fb8tK9dsFnkB8UTY8ZnA+WcI4LGYQBRx9E +3YD1Jt8V32bSScgC9+ucceqCi6JtQd5Kc860wuHFxgGfBADIK1kkH0Y4fF36/alfUvrBCUOGSigF +Fl0Z96bTu54QJMPHzpa+z+uxYH+OzzUhNKzrPZKJFuekDZIOI4q2KqWgdaWUwGD/9ZudGpz51sTu +urir0/CsWDWs7JnZu2ubsn35EdEsRCUd74VjbYF8wMfqUkwbxecX6UZzeQplbmRzdHJlYW0KZW5k +b2JqCjI5IDAgb2JqCiAgIDIzMwplbmRvYmoKMzAgMCBvYmoKPDwgL1R5cGUgL0ZvbnREZXNjcmlw +dG9yCiAgIC9Gb250TmFtZSAvUFdDREVaK1NXSUZUREFZM0JvbGRJdGFsaWMKICAgL0ZvbnRGYW1p +bHkgKFNXSUZUREFZMykKICAgL0ZsYWdzIDQKICAgL0ZvbnRCQm94IFsgLTM3MiAtMTE4MyAxNjA0 +IDkzOCBdCiAgIC9JdGFsaWNBbmdsZSAwCiAgIC9Bc2NlbnQgNzcwCiAgIC9EZXNjZW50IC00MzAK +ICAgL0NhcEhlaWdodCA5MzgKICAgL1N0ZW1WIDgwCiAgIC9TdGVtSCA4MAogICAvRm9udEZpbGUz +IDI2IDAgUgo+PgplbmRvYmoKMzEgMCBvYmoKPDwgL1R5cGUgL0ZvbnQKICAgL1N1YnR5cGUgL0NJ +REZvbnRUeXBlMAogICAvQmFzZUZvbnQgL1BXQ0RFWitTV0lGVERBWTNCb2xkSXRhbGljCiAgIC9D +SURTeXN0ZW1JbmZvCiAgIDw8IC9SZWdpc3RyeSAoQWRvYmUpCiAgICAgIC9PcmRlcmluZyAoSWRl +bnRpdHkpCiAgICAgIC9TdXBwbGVtZW50IDAKICAgPj4KICAgL0ZvbnREZXNjcmlwdG9yIDMwIDAg +UgogICAvVyBbMCBbIDUwMCA1NDEgNTIwIF1dCj4+CmVuZG9iago4IDAgb2JqCjw8IC9UeXBlIC9G +b250CiAgIC9TdWJ0eXBlIC9UeXBlMAogICAvQmFzZUZvbnQgL1BXQ0RFWitTV0lGVERBWTNCb2xk +SXRhbGljCiAgIC9FbmNvZGluZyAvSWRlbnRpdHktSAogICAvRGVzY2VuZGFudEZvbnRzIFsgMzEg +MCBSXQogICAvVG9Vbmljb2RlIDI4IDAgUgo+PgplbmRvYmoKMSAwIG9iago8PCAvVHlwZSAvUGFn +ZXMKICAgL0tpZHMgWyA5IDAgUiBdCiAgIC9Db3VudCAxCj4+CmVuZG9iagozMiAwIG9iago8PCAv +Q3JlYXRvciAoY2Fpcm8gMS4xMy4xIChodHRwOi8vY2Fpcm9ncmFwaGljcy5vcmcpKQogICAvUHJv +ZHVjZXIgKGNhaXJvIDEuMTMuMSAoaHR0cDovL2NhaXJvZ3JhcGhpY3Mub3JnKSkKPj4KZW5kb2Jq +CjMzIDAgb2JqCjw8IC9UeXBlIC9DYXRhbG9nCiAgIC9QYWdlcyAxIDAgUgo+PgplbmRvYmoKeHJl +ZgowIDM0CjAwMDAwMDAwMDAgNjU1MzUgZiAKMDAwMDAzNDYzNSAwMDAwMCBuIAowMDAwMDAzMjA5 +IDAwMDAwIG4gCjAwMDAwMDAwMTUgMDAwMDAgbiAKMDAwMDAwMzE4NiAwMDAwMCBuIAowMDAwMDE2 +MDg0IDAwMDAwIG4gCjAwMDAwMjg3MzUgMDAwMDAgbiAKMDAwMDAzMjM5NSAwMDAwMCBuIAowMDAw +MDM0NDY2IDAwMDAwIG4gCjAwMDAwMDMzNzUgMDAwMDAgbiAKMDAwMDAwMzU4OSAwMDAwMCBuIAow +MDAwMDE0NTE3IDAwMDAwIG4gCjAwMDAwMTQ1NDIgMDAwMDAgbiAKMDAwMDAxNTc5MCAwMDAwMCBu +IAowMDAwMDE1ODE0IDAwMDAwIG4gCjAwMDAwMTcxNzIgMDAwMDAgbiAKMDAwMDAyNjA3MCAwMDAw +MCBuIAowMDAwMDI2MDk0IDAwMDAwIG4gCjAwMDAwMjczMDggMDAwMDAgbiAKMDAwMDAyNzMzMiAw +MDAwMCBuIAowMDAwMDI3NjAyIDAwMDAwIG4gCjAwMDAwMjg4OTggMDAwMDAgbiAKMDAwMDAzMTYx +MCAwMDAwMCBuIAowMDAwMDMxNjM0IDAwMDAwIG4gCjAwMDAwMzIwOTYgMDAwMDAgbiAKMDAwMDAz +MjExOSAwMDAwMCBuIAowMDAwMDMyOTEzIDAwMDAwIG4gCjAwMDAwMzM1ODQgMDAwMDAgbiAKMDAw +MDAzMzYwNyAwMDAwMCBuIAowMDAwMDMzOTE5IDAwMDAwIG4gCjAwMDAwMzM5NDIgMDAwMDAgbiAK +MDAwMDAzNDIxOCAwMDAwMCBuIAowMDAwMDM0NzAwIDAwMDAwIG4gCjAwMDAwMzQ4MjggMDAwMDAg +biAKdHJhaWxlcgo8PCAvU2l6ZSAzNAogICAvUm9vdCAzMyAwIFIKICAgL0luZm8gMzIgMCBSCj4+ +CnN0YXJ0eHJlZgozNDg4MQolJUVPRgo= +--=_be6bc52b94449e229e651d02e56a25f1-- diff --git a/plugins/php-imap/tests/messages/issue-382.eml b/plugins/php-imap/tests/messages/issue-382.eml new file mode 100644 index 00000000..f34b89fc --- /dev/null +++ b/plugins/php-imap/tests/messages/issue-382.eml @@ -0,0 +1,9 @@ +From: MAILER-DAEMON@mta-09.someserver.com (Mail Delivery System) +To: to@here.com +Subject: Test +Date: Wed, 13 Sep 2017 13:05:45 +0200 +Content-Type: text/plain; + charset="us-ascii" +Content-Transfer-Encoding: quoted-printable + +Hi \ No newline at end of file diff --git a/plugins/php-imap/tests/messages/issue-401.eml b/plugins/php-imap/tests/messages/issue-401.eml new file mode 100644 index 00000000..386fdad8 --- /dev/null +++ b/plugins/php-imap/tests/messages/issue-401.eml @@ -0,0 +1,9 @@ +From: from@there.com +To: to@here.com +Subject: 1;00pm Client running few minutes late +Date: Wed, 13 Sep 2017 13:05:45 +0200 +Content-Type: text/plain; + charset="us-ascii" +Content-Transfer-Encoding: quoted-printable + +Hi \ No newline at end of file diff --git a/plugins/php-imap/tests/messages/issue-410.eml b/plugins/php-imap/tests/messages/issue-410.eml new file mode 100644 index 00000000..e5fe0c62 --- /dev/null +++ b/plugins/php-imap/tests/messages/issue-410.eml @@ -0,0 +1,16 @@ +From: from@there.com +To: to@here.com +Subject: =?ISO-2022-JP?B?GyRCIXlCaBsoQjEzMhskQjlmISEhViUsITwlRyVzGyhCJhskQiUoJS8lOSVGJWolIiFXQGxMZ0U5JE4kPyRhJE4jURsoQiYbJEIjQSU1JW0lcyEhIVo3bjQpJSglLyU5JUYlaiUiISYlbyE8JS8hWxsoQg==?= +Date: Wed, 13 Sep 2017 13:05:45 +0200 +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="------------B832AF745285AEEC6D5AEE42" + +Hi +--------------B832AF745285AEEC6D5AEE42 +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename="=?ISO-2022-JP?B?GyRCIXlCaBsoQjEzMhskQjlmISEhViUsITwlRyVzGyhCJhskQiUoJS8lOSVGJWolIiFXQGxMZ0U5JE4kPyRhJE4jURsoQiYbJEIjQSU1JW0lcyEhIVo3bjQpJSglLyU5JUYlaiUiISYlbyE8JS8hWxsoQg==?=" + +SGkh +--------------B832AF745285AEEC6D5AEE42-- \ No newline at end of file diff --git a/plugins/php-imap/tests/messages/issue-410b.eml b/plugins/php-imap/tests/messages/issue-410b.eml new file mode 100644 index 00000000..b260a1e0 --- /dev/null +++ b/plugins/php-imap/tests/messages/issue-410b.eml @@ -0,0 +1,22 @@ +From: from@there.com +To: to@here.com +Subject: =?iso-8859-1?Q?386_-_400021804_-_19.,_Heiligenst=E4dter_Stra=DFe_80_-_081?= + =?iso-8859-1?Q?9306_-_Anfrage_Vergabevorschlag?= +Date: Wed, 13 Sep 2017 13:05:45 +0200 +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="------------B832AF745285AEEC6D5AEE42" + +Hi +--------------B832AF745285AEEC6D5AEE42 +Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; + name="=?iso-8859-1?Q?2021=5FM=E4ngelliste=5F0819306.xlsx?=" +Content-Description: =?iso-8859-1?Q?2021=5FM=E4ngelliste=5F0819306.xlsx?= +Content-Disposition: attachment; + filename="=?iso-8859-1?Q?2021=5FM=E4ngelliste=5F0819306.xlsx?="; size=11641; + creation-date="Mon, 10 Jan 2022 09:01:00 GMT"; + modification-date="Mon, 10 Jan 2022 09:01:00 GMT" +Content-Transfer-Encoding: base64 + +SGkh +--------------B832AF745285AEEC6D5AEE42-- \ No newline at end of file diff --git a/plugins/php-imap/tests/messages/issue-412.eml b/plugins/php-imap/tests/messages/issue-412.eml new file mode 100644 index 00000000..f28b81a7 --- /dev/null +++ b/plugins/php-imap/tests/messages/issue-412.eml @@ -0,0 +1,216 @@ +Return-Path: +Delivered-To: gmx@tonymarston.co.uk +Received: from ion.dnsprotect.com + by ion.dnsprotect.com with LMTP + id YFDRGeZ6d2SlCRUAzEkvSQ + (envelope-from ) + for ; Wed, 31 May 2023 12:50:46 -0400 +Return-path: +Envelope-to: gmx@tonymarston.co.uk +Delivery-date: Wed, 31 May 2023 12:50:46 -0400 +Received: from mail-vi1eur04olkn2050.outbound.protection.outlook.com ([40.92.75.50]:23637 helo=EUR04-VI1-obe.outbound.protection.outlook.com) + by ion.dnsprotect.com with esmtps (TLS1.2) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 + (Exim 4.96) + (envelope-from ) + id 1q4P28-005pTm-0E + for gmx@tonymarston.co.uk; + Wed, 31 May 2023 12:50:46 -0400 +ARC-Seal: i=1; a=rsa-sha256; s=arcselector9901; d=microsoft.com; cv=none; + b=SYBG6BEOiWsmVfcgXUQJ0moS1SuG/IdTK6DIT4H3g7CQ+hbWIbItTxOhFzJiHP+q0uz+XzR1FzX2Daso+4iKotX7x2ViHIA0Hs65xUZVFtvflMsUrB+5RLlf3Pr7DiNKguQQtC+R2IBLvedc+IqElnMrTHcxLVS2gWl89MZx5Q0bXGWW/9wBVq6yvc6C69ynppcEdD0QZsoUQlp2DgzDpg8iG3y6dYGxFiTvLzw08nTQiCuqi8qQ+nmHyeeItIiAmyKynvyiL+kh4frcSDS67r6PU/CBxvto/nP3RCAxHuzJEGOpS7LJPqoJAlRSrUp2zpeEMpmDFJUE/Jo0K+EgcQ== +ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=microsoft.com; + s=arcselector9901; + h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-AntiSpam-MessageData-ChunkCount:X-MS-Exchange-AntiSpam-MessageData-0:X-MS-Exchange-AntiSpam-MessageData-1; + bh=ISg5C/1AgVASEPkGp/cgowfc/y9qywGvXMTv5yjJffs=; + b=eVYGErUWMxOeFGLg2nPuB0E/ngKet0hEVcN8Ay4ujGFY4k7i+1hrmnOMD6XiaIk3gVrqPalsmDjmEHpS0zV3+fPPTSktlSvkLrUr5ViVI1kMVBbBQsowLD5x3FpX7fnP2q17WPQ2P6R8Ibudkdnei8uq7gZhc3CSDLv4PfNma45H0FmdaB40mF2dCYzj5hEzr6jmMliANHJjznuDEFEUH3CfS1/iIA9rzhBKPKtahipTNeYiLqvZpKo1fO/XkZ57T44fqHkocfCyEK3Y1rehWudmkU8a9eEZlU5nBC6xoGO3P5Q1XIUNEHFmx2HH7eO8IgGzq/vbLMhcvbc3Ysdb2A== +ARC-Authentication-Results: i=1; mx.microsoft.com 1; spf=none; dmarc=none; + dkim=none; arc=none +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=hotmail.com; + s=selector1; + h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-SenderADCheck; + bh=ISg5C/1AgVASEPkGp/cgowfc/y9qywGvXMTv5yjJffs=; + b=Qv71Bx+LsM9Lo0uen0vxQzWI3ju4Q095ZMkKPXDaKsd5Y8dA3QteMpAPhy3/mAdP1+EV8NviDBhamtTG4qMO+zEqu/pyRpBGZOtjyiJGKh7aFh2bbodOkJkiGqH3jPwYBnE7T1XAqDVFmvRpuqIkqBe9FXeCZKRrF/Na5Y+zuklH7ebuGQVzIK+xol6q7BDgb/oLul7Wa3r3Lw40cPW5leUgwxngRFMucUVVO5aJ4MWlk76CmcN8XqgwVcFaACY80HLWRqRZfM8n24/KzV9nKSZIQFCgJi2CiqnEWVRSZZtZ9SudJJv4S3C/gU4OYoiFKr7GkEQibyqE2QkGHCBA1g== +Received: from PAXP193MB2364.EURP193.PROD.OUTLOOK.COM (2603:10a6:102:22b::9) + by DB8P193MB0773.EURP193.PROD.OUTLOOK.COM (2603:10a6:10:15a::10) with + Microsoft SMTP Server (version=TLS1_2, + cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.20.6455.22; Wed, 31 May + 2023 16:50:03 +0000 +Received: from PAXP193MB2364.EURP193.PROD.OUTLOOK.COM + ([fe80::b962:59b:2d33:85c2]) by PAXP193MB2364.EURP193.PROD.OUTLOOK.COM + ([fe80::b962:59b:2d33:85c2%5]) with mapi id 15.20.6455.020; Wed, 31 May 2023 + 16:50:03 +0000 +From: Tony Marston +To: "gmx@tonymarston.co.uk" +Subject: RE: TEST MESSAGE +Thread-Topic: TEST MESSAGE +Thread-Index: AQHZjysmbQn8p2cYOEawNV6ywnFmN690oIyAgAAA5rw= +Date: Wed, 31 May 2023 16:50:03 +0000 +Message-ID: + +References: + + <944a7d16fcb607628f358cdd0f5ba8c0@tonymarston.co.uk> +In-Reply-To: <944a7d16fcb607628f358cdd0f5ba8c0@tonymarston.co.uk> +Accept-Language: en-GB, en-US +Content-Language: en-GB +X-MS-Has-Attach: +X-MS-TNEF-Correlator: +x-ms-exchange-messagesentrepresentingtype: 1 +x-tmn: [ABOzTq1ytyWYuvBD5A4I78Eqna1hBLeM] +x-ms-publictraffictype: Email +x-ms-traffictypediagnostic: PAXP193MB2364:EE_|DB8P193MB0773:EE_ +x-ms-office365-filtering-correlation-id: 7fbd6771-9a0d-495d-c4d3-08db61f714a6 +x-microsoft-antispam: BCL:0; +x-microsoft-antispam-message-info: + fwWKs8Qs/JyQ+pSnCNC804PQ86JYn/R63U7P6WCeXLC/fVSGqcAnslOc2vbxC2cUeTPPfow1WAz3f73/7Uk+byKxiE6L48WkgL6BFKVkYCwPKcQ3ps6KyEdGL6uUvPqYKwf5gFgUiTt+fKDiR/GJljj6qmN51jd5lUBvLf1g59q3LryUC+lEy6iLQ8cjCivzmBcR+0C2+uaa/xsjJxokbIEQoMicjjUiVWFkxRFBDr6FO8kEQyzzs70pNivK6mcXpVeJSOiLQfYa/2Q5QDYhE8kznG1EYUzHBQD/sLp/maUgmpKj1b8ObeI13QXed1qih+CtdLYAmPs5GPaoz8aY7pmaxroKLjBAqfOAC2IeQ3grxdQ8eRXlrnZ29cQnvD9tDryUvE9nyQLinaM2Dft4MueHvBTL5+WOTNnVQB98zVjnop8iVkwNrBKzPYiox9ufs9XFXNl0+2fFn66647ET7y/DkBKcszTKYF5RRp5o59QAh8rUTsaKeGJkPkyowZd+i6R12NIavL+eOOgnKvziTTbNl1lGgP+3zTKbgbb6K+DxfbKNl1zaTNvonOHwzI3jfdkDRtg5BvZVKstyl9AfZqo6OZ7ii3JKgVquRVWAtEQ6J6tx1Va/nTVrn1478+Dj +x-ms-exchange-antispam-messagedata-chunkcount: 1 +x-ms-exchange-antispam-messagedata-0: + =?us-ascii?Q?EMaSeqWhMBslXR7KptI34/opub+lBzTReVt3ACHDu/w7NvVUPpdp4YhwogpL?= + =?us-ascii?Q?8V9o/Zj+8vv6zZ7YL0n2NpODi8fzrPScgRiZGtJmfS58OqfgW2yygp1BmawZ?= + =?us-ascii?Q?y99Omv39THENDmCAba4d8zYLikOc+HJUfhpWeb/L9yqut7T2QkPFJYoeN5vU?= + =?us-ascii?Q?LeJ1hoPlguhNnDwMtE3HTgAl0Hbesms0Rb7wGaTDsgAc7XOO+8XpqLamnU0m?= + =?us-ascii?Q?zPpkQXV8+V5dFU2HxgIcYPYSaTX1CLPCdNcCAr1uHPnN81Ntm9Jb7fpYs1oA?= + =?us-ascii?Q?tgzPMwt37Ks9eQucJWZ5LQnNmZl/kYtVnvkylqYYMWiAg3y2XtjVkW/Ut/V2?= + =?us-ascii?Q?6vMfyCB5fEGJrn/uCp+KwL1s7s/4B332Bn4zvwpYd5TioMSGf9Rdp157eAfg?= + =?us-ascii?Q?LiNlLjDFdRc5SSEkUEl1TeX0FOQLsaCsQQqC/hzb10boa49GuxpoKwRmwhAO?= + =?us-ascii?Q?LZ2+veS5Jr1qWngBGo4MTkEq7nD6vBRIXQmKiLMpJc+Gk3/PCADL+H0IRH0+?= + =?us-ascii?Q?4uHnvcVr6CrDDZ2BEwcaWOa/ct8yUI5G9SC5gFP53TaS2llCnYwHAX1PkMAo?= + =?us-ascii?Q?w2LHFHE5I5dtQQJHaWNGMJGYdPmb9dDrksggLWXN+IxsxvFcFNSK2GPaqOMJ?= + =?us-ascii?Q?H4ht1rpqHTlU0uzgjb19gKCCcBdZfIzv118RTjFYG+EX/rsHlNRei/OWdTBF?= + =?us-ascii?Q?xf5cdnap836rQmre5ZoubNsSw2qKSRJhmZH1pCHoCFLtreM1fk7kkVJfkUz5?= + =?us-ascii?Q?ApMa03dEfFvzVv5wvPdWBLiCqzI6z6z8fUmwg2XfvK9Nyxb1AOZoT7JUXnp2?= + =?us-ascii?Q?Me6dTKqGKsBx87Dtny+fANHqgOm+Eo/pBZqyXwN93udbltxPmtNJ84MJJjyx?= + =?us-ascii?Q?vbEiDnMVb4knBO+sBqlKAVUv1F9ZJA2oUTrOD7t1xx6nBmQTgoYN6zsi+dgw?= + =?us-ascii?Q?nPebsl1/6fUy73FWLUKkeA64PeSa00Zi/q53ylXmUZV4Pc+11blKdL8o+p32?= + =?us-ascii?Q?dTD0ndul0WvvpQf8RdYNtGJ/BMurqNfvHq9wJo7Iu4fgTElR50ngwEsr28Bc?= + =?us-ascii?Q?G81pAb2fNhID6ewyOGfj87kqybxUhv1E+4pquh770UagjD1J3rKAUiw1sxWp?= + =?us-ascii?Q?u+FSmd7HKgd2cKJsmMErnQelF3DNozw5d0qdNELXZNO03xlMiADVvhqEJuqc?= + =?us-ascii?Q?uArdsSo2hyApUaiB+dM4Fp+oeiGienEQl64NJ7QFxRb/h96J0iTL3Vp8+8Y?= + =?us-ascii?Q?=3D?= +Content-Type: multipart/alternative; + boundary="_000_PAXP193MB2364F1160B9E7C559D7D897ABB489PAXP193MB2364EURP_" +MIME-Version: 1.0 +X-OriginatorOrg: sct-15-20-4755-11-msonline-outlook-80ceb.templateTenant +X-MS-Exchange-CrossTenant-AuthAs: Internal +X-MS-Exchange-CrossTenant-AuthSource: PAXP193MB2364.EURP193.PROD.OUTLOOK.COM +X-MS-Exchange-CrossTenant-RMS-PersistedConsumerOrg: 00000000-0000-0000-0000-000000000000 +X-MS-Exchange-CrossTenant-Network-Message-Id: 7fbd6771-9a0d-495d-c4d3-08db61f714a6 +X-MS-Exchange-CrossTenant-originalarrivaltime: 31 May 2023 16:50:03.3982 + (UTC) +X-MS-Exchange-CrossTenant-fromentityheader: Hosted +X-MS-Exchange-CrossTenant-id: 84df9e7f-e9f6-40af-b435-aaaaaaaaaaaa +X-MS-Exchange-CrossTenant-rms-persistedconsumerorg: 00000000-0000-0000-0000-000000000000 +X-MS-Exchange-Transport-CrossTenantHeadersStamped: DB8P193MB0773 +X-Spam-Status: No, score=-1.6 +X-Spam-Score: -15 +X-Spam-Bar: - +X-Ham-Report: Spam detection software, running on the system "ion.dnsprotect.com", + has NOT identified this incoming email as spam. The original + message has been attached to this so you can view it or label + similar future email. If you have any questions, see + root\@localhost for details. + Content preview: Here is my reply to your reply. Tony Marston From: gmx@tonymarston.co.uk + Sent: 31 May 2023 17:46 To: Tony Marston + Subject: Re: TEST MESSAGE + Content analysis details: (-1.6 points, 8.0 required) + pts rule name description + ---- ---------------------- -------------------------------------------------- + -1.9 BAYES_00 BODY: Bayes spam probability is 0 to 1% + [score: 0.0005] + 0.0 FREEMAIL_FROM Sender email is commonly abused enduser mail + provider + [tonymarston[at]hotmail.com] + -0.0 SPF_PASS SPF: sender matches SPF record + 0.5 SUBJ_ALL_CAPS Subject is all capitals + -0.0 SPF_HELO_PASS SPF: HELO matches SPF record + 0.0 HTML_MESSAGE BODY: HTML included in message + -0.1 DKIM_VALID_AU Message has a valid DKIM or DK signature from + author's domain + 0.1 DKIM_SIGNED Message has a DKIM or DK signature, not necessarily + valid + -0.1 DKIM_VALID_EF Message has a valid DKIM or DK signature from + envelope-from domain + -0.1 DKIM_VALID Message has at least one valid DKIM or DK signature + -0.0 T_SCC_BODY_TEXT_LINE No description available. +X-Spam-Flag: NO +X-From-Rewrite: unmodified, no actual sender determined from check mail permissions + +--_000_PAXP193MB2364F1160B9E7C559D7D897ABB489PAXP193MB2364EURP_ +Content-Type: text/plain; charset="us-ascii" +Content-Transfer-Encoding: quoted-printable + +Here is my reply to your reply. + +Tony Marston + +From: gmx@tonymarston.co.uk +Sent: 31 May 2023 17:46 +To: Tony Marston +Subject: Re: TEST MESSAGE + +On 2023-05-25 13:06, Tony Marston wrote: +Here is my reply to your message +> Tony Marston + + +--_000_PAXP193MB2364F1160B9E7C559D7D897ABB489PAXP193MB2364EURP_ +Content-Type: text/html; charset="us-ascii" +Content-Transfer-Encoding: quoted-printable + + + + + + + + +
+

Here is my reply to your reply.

+

 

+

Tony Marston

+

 

+
+

From: gmx@tonymarston.co.uk
+Sent: 31 May 2023 17:46
+To: Tony Marston
+Subject: Re: TEST MESSAGE

+
+

 

+

On 2023-05-25 13:06, Tony Marston wrote:
+Here is my reply to your message
+> Tony Marston

+

 

+
+ + + +--_000_PAXP193MB2364F1160B9E7C559D7D897ABB489PAXP193MB2364EURP_-- \ No newline at end of file diff --git a/plugins/php-imap/tests/messages/issue-413.eml b/plugins/php-imap/tests/messages/issue-413.eml new file mode 100644 index 00000000..9f0cbfa2 --- /dev/null +++ b/plugins/php-imap/tests/messages/issue-413.eml @@ -0,0 +1,32 @@ +Return-Path: +Delivered-To: gmx@tonymarston.co.uk +Received: from ion.dnsprotect.com + by ion.dnsprotect.com with LMTP + id oPy8IzIke2Rr4gIAzEkvSQ + (envelope-from ) + for ; Sat, 03 Jun 2023 07:29:54 -0400 +Return-path: +Envelope-to: gmx@tonymarston.net +Delivery-date: Sat, 03 Jun 2023 07:29:54 -0400 +Received: from [::1] (port=48740 helo=ion.dnsprotect.com) + by ion.dnsprotect.com with esmtpa (Exim 4.96) + (envelope-from ) + id 1q5PSF-000nPQ-1F + for gmx@tonymarston.net; + Sat, 03 Jun 2023 07:29:54 -0400 +MIME-Version: 1.0 +Date: Sat, 03 Jun 2023 07:29:54 -0400 +From: radicore +To: gmx@tonymarston.net +Subject: Test Message +User-Agent: Roundcube Webmail/1.6.0 +Message-ID: +X-Sender: radicore@radicore.org +Content-Type: text/plain; charset=US-ASCII; + format=flowed +Content-Transfer-Encoding: 7bit +X-From-Rewrite: unmodified, already matched + +This is just a test, so ignore it (if you can!) + +Tony Marston diff --git a/plugins/php-imap/tests/messages/issue-414.eml b/plugins/php-imap/tests/messages/issue-414.eml new file mode 100644 index 00000000..302e4492 --- /dev/null +++ b/plugins/php-imap/tests/messages/issue-414.eml @@ -0,0 +1,27 @@ +From: from@there.com +To: to@here.com +Subject: Test +Date: Fri, 29 Sep 2017 10:55:23 +0200 +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="------------5B1F217006A67C28E756A62E" + +This is a multi-part message in MIME format. + +--------------5B1F217006A67C28E756A62E +Content-Type: text/plain; charset=UTF-8; + name="../../example/MyFile.txt" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename="../../example/MyFile.txt" + +TXlGaWxlQ29udGVudA== +--------------5B1F217006A67C28E756A62E +Content-Type: text/plain; charset=UTF-8; + name="php://foo" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename="php://foo" + +TXlGaWxlQ29udGVudA== +--------------5B1F217006A67C28E756A62E-- diff --git a/plugins/php-imap/tests/messages/ks_c_5601-1987_headers.eml b/plugins/php-imap/tests/messages/ks_c_5601-1987_headers.eml new file mode 100644 index 00000000..a8de3d08 --- /dev/null +++ b/plugins/php-imap/tests/messages/ks_c_5601-1987_headers.eml @@ -0,0 +1,10 @@ +Subject: =?ks_c_5601-1987?B?UkU6IMi4v/i01LKyIEVyc2m01MDMILjevcPB9rimILq4s8K9wLTP?= + =?ks_c_5601-1987?B?tNku?= +MIME-Version: 1.0 +Content-Type: text/plain +Date: Wed, 27 Sep 2017 12:48:51 +0200 +Thread-Topic: =?ks_c_5601-1987?B?yLi/+LTUsrIgRXJzabTUwMwguN69w8H2uKYgurizwr3AtM+02S4=?= +From: =?ks_c_5601-1987?B?seggx/bB+A==?= +To: to@here.com + +Content diff --git a/plugins/php-imap/tests/messages/mail_that_is_attachment.eml b/plugins/php-imap/tests/messages/mail_that_is_attachment.eml new file mode 100644 index 00000000..89215cf8 --- /dev/null +++ b/plugins/php-imap/tests/messages/mail_that_is_attachment.eml @@ -0,0 +1,28 @@ +Sender: xxx@yyy.cz +MIME-Version: 1.0 +Message-ID: <2244696771454641389@google.com> +Date: Sun, 15 Feb 2015 10:21:51 +0000 +Subject: Report domain: yyy.cz Submitter: google.com Report-ID: 2244696771454641389 +From: noreply-dmarc-support via xxx +To: xxx@yyy.cz +Content-Type: application/zip; + name="google.com!yyy.cz!1423872000!1423958399.zip" +Content-Disposition: attachment; + filename="google.com!yyy.cz!1423872000!1423958399.zip" +Content-Transfer-Encoding: base64 +Reply-To: noreply-dmarc-support@google.com + +UEsDBAoAAAAIABRPT0bdJB+DSwIAALgKAAAuAAAAZ29vZ2xlLmNvbSFzdW5mb3guY3ohMTQyMzg3 +MjAwMCExNDIzOTU4Mzk5LnhtbO1WwY6bMBC971dEuQcDIQSQQ3rqF7RnZIwh7oJt2WY32a+viQ2h +u9moqnarqOop8Gbmjd/4OQbuj127eCJSUc52y8DzlwvCMK8oa3bL79++rpLlYp8/wJqQqkT4MX9Y +LKAkgktddESjCmk0YAblsikY6kjecN60xMO8g2ACbQ7pEG1zxg1De1pVHZJ4pXox0H2Zl9k8V3PU +EhWYM42wLiireX7QWmQAuErvUgkQKCkDiKlnIj1x2tunXRjF8SbxDfFbMtvFaaJVHoZRFKfxdhtE +myiOgnWSQnAJ23SjmxQSscYpM1BJGsryIArXyTb0fdPMImOcsOocTTfJOjWUw7slA7+yTd3mA4aC +txSfCtGXLVUHMi2Em1GxXPVGytHDL4bMIjaMqkfa5RIC++BAJeozNvxaSOSS/CBYQyAcoi6QGjGB +dR4MyoaH80qvrcrMEnM5LlDy52kEivcSk4KKPIz9bVYnpZ9Fvr/OsB9kWbgOTa8pZSzCvGemLQT2 +YYRdZ/KE2t6MrxoDw0yoElxRbTxtvMaImckMmeUNIxFIKZMwTceJr11gGtFM7aueZr9GjZBWhGla +U3OiprIDQRWRRS15N9+nOex43lRD1OtDIYnqW30hfLXY2xZw7h4YnCT3Mqma08GZ3g+gvhgMvFYy +JI82+R3HpL4XbDdesIm84SB/tE9Gr99wSm3+k646xQbu0Sl/uptW0Sfu5tXzH6b3dP7vd1f/+vl/ +KU83eRnpzbX6uY5JzMeJZ25PLwji920S/r8m/tVrAoLLR+hPUEsBAgoACgAAAAgAFE9PRt0kH4NL +AgAAuAoAAC4AAAAAAAAAAAAAAAAAAAAAAGdvb2dsZS5jb20hc3VuZm94LmN6ITE0MjM4NzIwMDAh +MTQyMzk1ODM5OS54bWxQSwUGAAAAAAEAAQBcAAAAlwIAAAAA \ No newline at end of file diff --git a/plugins/php-imap/tests/messages/missing_date.eml b/plugins/php-imap/tests/messages/missing_date.eml new file mode 100644 index 00000000..5f8a6032 --- /dev/null +++ b/plugins/php-imap/tests/messages/missing_date.eml @@ -0,0 +1,7 @@ +From: from@here.com +To: to@here.com +Subject: Nuu +Content-Type: text/plain; charset="us-ascii" +Content-Transfer-Encoding: quoted-printable + +Hi diff --git a/plugins/php-imap/tests/messages/missing_from.eml b/plugins/php-imap/tests/messages/missing_from.eml new file mode 100644 index 00000000..e09dd9d9 --- /dev/null +++ b/plugins/php-imap/tests/messages/missing_from.eml @@ -0,0 +1,7 @@ +To: to@here.com +Subject: Nuu +Date: Wed, 13 Sep 2017 13:05:45 +0200 +Content-Type: text/plain; charset="us-ascii" +Content-Transfer-Encoding: quoted-printable + +Hi diff --git a/plugins/php-imap/tests/messages/mixed_filename.eml b/plugins/php-imap/tests/messages/mixed_filename.eml new file mode 100644 index 00000000..4a353da3 --- /dev/null +++ b/plugins/php-imap/tests/messages/mixed_filename.eml @@ -0,0 +1,25 @@ +Date: Fri, 02 Feb 2018 22:23:06 +0300 +To: foo@bar.com +Subject: =?windows-1251?B?0eLl5ujpIO/w4OnxLevo8fI=?= +From: =?windows-1251?B?z/Dg6fH7IHx8IM/g8PLK7uw=?= +Content-Transfer-Encoding: quoted-printable +Content-Type: multipart/mixed; + boundary="=_743251f7a933f6b30c004fcb14eabb57" +Content-Disposition: inline + +This is a message in Mime Format. If you see this, your mail reader does not support this format. + +--=_743251f7a933f6b30c004fcb14eabb57 +Content-Type: text/html; charset=windows-1251 +Content-Transfer-Encoding: quoted-printable +Content-Disposition: inline + + +--=_743251f7a933f6b30c004fcb14eabb57 +Content-Type: application/octet-stream +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename="Price4VladDaKar.xlsx" + +UEsDBBQAAgAIAHOyQkwNlxQhWwEAAAYFAAATAAAAW0NvbnRlbnRfVHlwZXNdLnhtbK2UTU7D +CwALANECAAAT1E4AAAA= +--=_743251f7a933f6b30c004fcb14eabb57-- diff --git a/plugins/php-imap/tests/messages/multipart_without_body.eml b/plugins/php-imap/tests/messages/multipart_without_body.eml new file mode 100644 index 00000000..8f86799f --- /dev/null +++ b/plugins/php-imap/tests/messages/multipart_without_body.eml @@ -0,0 +1,110 @@ +Received: from AS8PR02MB6805.eurprd02.prod.outlook.com (2603:10a6:20b:252::8) + by PA4PR02MB7071.eurprd02.prod.outlook.com with HTTPS; Sat, 11 Mar 2023 + 08:24:33 +0000 +Received: from omef0ahNgeoJu.eurprd02.prod.outlook.com (2603:10a6:10:33c::12) + by AS8PR02MB6805.eurprd02.prod.outlook.com (2603:10a6:20b:252::8) with + Microsoft SMTP Server (version=TLS1_2, + cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.20.6178.19; Sat, 11 Mar + 2023 08:24:31 +0000 +Received: from omef0ahNgeoJu.eurprd02.prod.outlook.com + ([fe80::38c0:9c40:7fc6:93a7]) by omef0ahNgeoJu.eurprd02.prod.outlook.com + ([fe80::38c0:9c40:7fc6:93a7%7]) with mapi id 15.20.6178.019; Sat, 11 Mar 2023 + 08:24:31 +0000 +From: =?iso-8859-1?Q?Foo_B=FClow_Bar?= +To: some one +Subject: This mail will not contain a body +Thread-Topic: This mail will not contain a body +Thread-Index: AdlT8uVmpHPvImbCRM6E9LODIvAcQA== +Date: Sat, 11 Mar 2023 08:24:31 +0000 +Message-ID: + +Accept-Language: da-DK, en-US +Content-Language: en-US +X-MS-Exchange-Organization-AuthAs: Internal +X-MS-Exchange-Organization-AuthMechanism: 04 +X-MS-Exchange-Organization-AuthSource: omef0ahNgeoJu.eurprd02.prod.outlook.com +X-MS-Has-Attach: +X-MS-Exchange-Organization-Network-Message-Id: + aa546a02-2b7a-4fb1-7fd4-08db220a09f1 +X-MS-Exchange-Organization-SCL: -1 +X-MS-TNEF-Correlator: +X-MS-Exchange-Organization-RecordReviewCfmType: 0 +x-ms-publictraffictype: Email +X-Microsoft-Antispam-Mailbox-Delivery: + ucf:0;jmr:0;auth:0;dest:I;ENG:(910001)(944506478)(944626604)(920097)(425001)(930097); +X-Microsoft-Antispam-Message-Info: + PeifKDwBVlQGeDu/c7oB3MKTffBwvlRIg5GJo1AiA4LroGAjwgwlg+oPfLetX9CgtbtKZy4gZnbjLCn3jJnod5ehn3Sv9gQaoH9PWkH/PIj6izaJzwlcEk81/MdprZFrORMwR7TGNqP/7ELAHw8rOH2Dmz9vGCE4cv0EwyYS3ShUhXABj4eJ17GNu1B4o53T2m9CzTgm647FRR5jpvk5xNIjQOwrhonVXMkCf2XKwF21sd/k4XLS/jX08tFJXBNyBALhsB4cG9gx2rxZYDn11SBejGFDw4eaqIQw6fYsmsyZvEoCnBkO+vK9lGIrQhRzGj7nJ41+uHVBcUYThce7P/ORpxl3GgThHyQpXQDV00JbP1aCBzm+4w8TyFiL0aOHXDhU9UKRHg3A01F+oH8IKAIaYazLCoOzbVcijSw0icNjBQsNLWa0FvfRT8y/rIvGkOB3rZpKf7ZR0g7cSlDAB3Ml2AaTbIB4ZL6QMukP/waDIObMZFmlVaAvmJzdTEdhGSLtUBFk4CNJhd6szcwaaPZxROumOGtz0sDke2iap8wRZqpdMWHVYi/trA+IESlF2G8TPzZiXs1lRDvYjNlam5r+1Ay1zlSmKTMnGbfNvsJgHkTlcgswKTlip4jGFPh6INTSDZtx9dQuDi4vbyNQiN1qVxoOPScTXzxUKVZ2PJ+8ipL2dqudLb3R2GSDHL10uQTuoIftA/Wjf67QZ629qQ== +Content-Type: multipart/alternative; + boundary="_000_omef0ahNgeoJuEB51C568ED2227A2DAABB5BB9omef0ahNgeoJueurp_" +MIME-Version: 1.0 + +--_000_omef0ahNgeoJuEB51C568ED2227A2DAABB5BB9omef0ahNgeoJueurp_ +Content-Type: text/plain; charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + +This mail will not contain a body + + + +--_000_omef0ahNgeoJuEB51C568ED2227A2DAABB5BB9omef0ahNgeoJueurp_ +Content-Type: text/html; charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + + + + + + + + +
+

This mail will not contain a bo= +dy

+

 

+
+ + + +--_000_omef0ahNgeoJuEB51C568ED2227A2DAABB5BB9omef0ahNgeoJueurp_-- diff --git a/plugins/php-imap/tests/messages/multiple_html_parts_and_attachments.eml b/plugins/php-imap/tests/messages/multiple_html_parts_and_attachments.eml new file mode 100644 index 00000000..5ccbd2c3 --- /dev/null +++ b/plugins/php-imap/tests/messages/multiple_html_parts_and_attachments.eml @@ -0,0 +1,203 @@ +Return-Path: +Delivered-To: to@there.com +Return-path: +Envelope-to: to@there.com +Delivery-date: Thu, 16 Feb 2023 09:19:23 +0000 +From: FromName +Content-Type: multipart/alternative; + boundary="Apple-Mail=_5382A451-FE2F-4504-9E3F-8FEB0B716E43" +Mime-Version: 1.0 (Mac OS X Mail 16.0 \(3731.400.51.1.1\)) +Subject: multiple_html_parts_and_attachments +Date: Thu, 16 Feb 2023 10:19:02 +0100 +To: to@there.com + + + +--Apple-Mail=_5382A451-FE2F-4504-9E3F-8FEB0B716E43 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/plain; + charset=utf-8 + +This is the first html part + +=EF=BF=BC + +This is the second html part + +=EF=BF=BC + +This is the last html part +https://www.there.com + + +--Apple-Mail=_5382A451-FE2F-4504-9E3F-8FEB0B716E43 +Content-Type: multipart/mixed; + boundary="Apple-Mail=_7C42F551-D023-4101-858E-D004D27871BE" + + +--Apple-Mail=_7C42F551-D023-4101-858E-D004D27871BE +Content-Transfer-Encoding: 7bit +Content-Type: text/html; + charset=us-ascii + +This is the first html part

+--Apple-Mail=_7C42F551-D023-4101-858E-D004D27871BE +Content-Disposition: inline; + filename=attachment1.pdf +Content-Type: application/pdf; + x-unix-mode=0666; + name="attachment1.pdf" +Content-Transfer-Encoding: base64 + +JVBERi0xLjMKJcTl8uXrp/Og0MTGCjMgMCBvYmoKPDwgL0ZpbHRlciAvRmxhdGVEZWNvZGUgL0xl +bmd0aCA5MyA+PgpzdHJlYW0KeAEdjDsOgCAQBXtP8U7AzwWW3sZOKmtDKExEhej9JWbamamIqFAd +G6ww3jowacEcGC1jxQm55Jby/bzbgbZ3W2v6CzPCkBJu9AxSQRBRGFKBnIvGdPVz/ABGZhatCmVu +ZHN0cmVhbQplbmRvYmoKMSAwIG9iago8PCAvVHlwZSAvUGFnZSAvUGFyZW50IDIgMCBSIC9SZXNv +dXJjZXMgNCAwIFIgL0NvbnRlbnRzIDMgMCBSIC9NZWRpYUJveCBbMCAwIDU5NS4yNzU2IDg0MS44 +ODk4XQo+PgplbmRvYmoKNCAwIG9iago8PCAvUHJvY1NldCBbIC9QREYgL0ltYWdlQiAvSW1hZ2VD +IC9JbWFnZUkgXSAvWE9iamVjdCA8PCAvSW0xIDUgMCBSID4+ID4+CmVuZG9iago1IDAgb2JqCjw8 +IC9UeXBlIC9YT2JqZWN0IC9TdWJ0eXBlIC9JbWFnZSAvV2lkdGggMTE0IC9IZWlnaHQgMjMgL0lu +dGVycG9sYXRlIHRydWUKL0NvbG9yU3BhY2UgNiAwIFIgL0ludGVudCAvUGVyY2VwdHVhbCAvQml0 +c1BlckNvbXBvbmVudCA4IC9MZW5ndGggMTUzNCAvRmlsdGVyCi9GbGF0ZURlY29kZSA+PgpzdHJl +YW0KeAHtmAlTllUUxz9CZTVN+75rmkJmZqMpVgoCoogikArhgmyhKYoKCqK4gQQoCgIBKiiEGCIg +u4CoKU1Mi/uu7fUB7CdnunPnPmLvy4Bj08s888w592z3/J9zz7kvN286/voWgb9u9Hc8PUagu2/T +Y4cOQxBwoNoXZXBvonrj0uCfLg3ui3zvjk9bUD16/MP7xh3j2Vo0z9jVhTNvd3476rerA2XdYA1l +29nHvZoGzyq3Xb+PNG1M58p556CETV9W+6pt2ILqgpR4QXVsRL4yFCIwIRnRkWMf3ZY1lG1n7xFU +jey6239i9iJASMharBT+FVXq8Hmf2icmNT47pe7+8Uc7O0cqWwgjrsHqmnbR/wlUvz7psmJLzISF +O4DFXlSLK/0xmZe0Tip25bYlCp+AuNQXptUgHRW6i3WDFbWiioDgxA39Aw4MDS6dv27tnko/ZQ6x +r2bqB5H5T01u4MNNWrJN1bygWn7IZ+qyDEKMmFesV8KUmK3sp6Zpol9s2qt+B31XpFc1Tjrz4/CZ +8SkSKDot9o/rA1QgehSiIbP20VWCEze2HR2nRLgir6Y2N++lma9Mrxoxtzguc8mfXba3TUcZQmSV +zAZPgdReVAmHSd1hD0JDDJq5X3n2XJz1tHc9i06B+xanxhksahm7Q5DyAPvI+bsfnNAGXbh/pnjI +3DOXLfVzO/J+6K7nfGoR8T79w7tIQfUB13b0H3ZvHfBxhThZnxclhkgf6hI96nlYNsDK6/6VqKGM +QwjAEeWGFvcnJzewwtcZPmdvP9f2xyY2872UK2wf8WhByg4Jh+aa7M+QWtMRE+t7c2EYVvp3v3MH +OHPqHTapkITAvL7FXXk2jrzBukQUAE7rP7UBjJhTNphfPe9MS8E5JQdLn6HhI92Q9yksmYrmxbND +qZyc0k9gOWsSV6ToXz731vWLQ1hHyhdpaR+PQmX9ZFiOADS2lB9sckGEjNSSqukA6xRUprui2q9d +cGKFs4Oy28IckRrpyKL1bS+q63KjiLJqW7S4Wr5lGSwHWXk24hpsfvksVZmYkBHmHDpoRNAccOWq +45vRiJLzI1gBN8oY0ET6+7U3qKKXfKuFFSmTV9jVWbeGBadeWJBEgZYC29g6AdHAGV+JSN5jwgpZ +PH5iLCyanAi+nYjoG9StCmSkozvRaXtRdQ4qYwPTlmdEJcfzSDfg0P16ZZC4NeIaLDrNR1zpgaPD +drJVaUGCKucFz3qX1vdJshxJfQVzJqasIH1Rk6YUhOMqbVeo0n/Gu45Nwu4oDUYEwtSzevDMYkWd +Nwq4op0qQ4jX/A6qQNZ0dE1F24WqNFI2YH2YQeLTiKuz1AwAYksTYNzMWLU5NnMprKAauWk1tPUC +LG5J1rivGqjqUkFVdwWqTEBc0U+IAoz0IuM51OyJgjUQqNKHb5udLFrfdqEakpTElpJyFjBe1SM9 +QZDBvw6jwdIwMWeHp7oGEFKGLytiu/GLSOg5a9erTX7/3XvhGxNpoaxYk+0ZqsUHbl1gmDsqCgSB +aPW/dB03a6A+rdWfL78JIPQcfl/oWzrL/HJtZwTLuqCq5pHO5pYFkZHeORelrlSoMvKgGdnqd1n4 +hjWsgHYvosrtmt3SKhlqkgW/hflAdGlpy7agqrLTcdBp22s1ryyQHLlD6uZCj4/KRZS6Mwx2afoK +aK/o7Uwig2Uc0EjJiIvK9r2zgVf6KvO39rAHyh6LsrFl4G4pCvGP/Zz7AMdWCtuabM9qlSjysbg1 +cQqyS4K5XBGUGYGIxxpIr1UjOzGxvm1HVVJmUludpO+ez8ZcwgsR8R2ZsLByrg2WwmOsIOWhuZVW ++TL4oOXqQrXI+BMFrm1c5iUc84JLux76Zd9qaZUsGlJJKrN4rtLnlkVrFZb/0oSuXysheHPFjUlf +rqat4QoTfkeoQEY6yr9BUGB45nerWr/zfVWp3ZngoKkjhqbOcikCqxMdLsoDNNdgxZ4/Pay60etk +xxg01WKvE0TkLrq/dsq508Psda6nY6Ntr6BqY6z/j5oD1b741g5U7yaq3aHtWLcXgb8Bi7W2lwpl +bmRzdHJlYW0KZW5kb2JqCjcgMCBvYmoKPDwgL04gMyAvQWx0ZXJuYXRlIC9EZXZpY2VSR0IgL0xl +bmd0aCAzNDMgL0ZpbHRlciAvRmxhdGVEZWNvZGUgPj4Kc3RyZWFtCngBdZA7SwNBFIXPxsiCpkgh +Vim2sRBWiZsgdpKoiJBiiYqPbrKJiZDHsFkRG7HzD4iVYCdprWKhIP4CQVCxshC1FwI+wnomUTaF +3uHOfHPm3N3LBUIRIWU5DKBS9dzsfNpYXVs39BeEEIWOJEzh1GXKtjO04PdUHETrFpq63Yypb8VO +P7Kv+eP9+7Q5rYvGYuD7kwbyhbrDly+m5UjXA7Q42d72pOI98pDLpsgHiotdbijOdfm841nKztBz +TY46JZEnP5LNXI9e7OFKeUv9V4XqPlKoLqteh5kxzGIOGS4DNixOYQJT1PBPTbJTM4MaJHbgYhNF +lOCxOkVFoowCeQFVOBiHSbYQZybUrFnLCGYYaLUIMPnMx6tAE0/A2e7PSAxVCYzQEz0CLt6lcEVX +4661wvWNhNW5a4NNoP/Q999WAH0UaN/5/mfT99snQN8DcNn6BsE0ZMMKZW5kc3RyZWFtCmVuZG9i +ago2IDAgb2JqClsgL0lDQ0Jhc2VkIDcgMCBSIF0KZW5kb2JqCjIgMCBvYmoKPDwgL1R5cGUgL1Bh +Z2VzIC9NZWRpYUJveCBbMCAwIDU5NS4yNzU2IDg0MS44ODk4XSAvQ291bnQgMSAvS2lkcyBbIDEg +MCBSIF0KPj4KZW5kb2JqCjggMCBvYmoKPDwgL1R5cGUgL0NhdGFsb2cgL1BhZ2VzIDIgMCBSID4+ +CmVuZG9iago5IDAgb2JqCjw8IC9UaXRsZSAo/v9cMDAwU1wwMDBjXDAwMGhcMDAwZVwwMDByXDAw +MG1cMDAwrVwwMDBhXDAwMGZcMDAwYlwwMDBlXDAwMGVcMDAwbFwwMDBkXDAwMGlcMDAwblwwMDBn +XDAwMCBcMDAwMlwwMDAwXDAwMDJcMDAwM1wwMDAtXDAwMDBcMDAwMlwwMDAtXDAwMDFcMDAwNlww +MDAgXDAwMG9cMDAwbVwwMDAgXDAwMDFcMDAwMFwwMDAuXDAwMDFcMDAwMVwwMDAuXDAwMDBcMDAw +M1wwMDAuXDAwMHBcMDAwblwwMDBnKQovUHJvZHVjZXIgKG1hY09TIFZlcnNpZSAxMy4yIFwoYnVp +bGQgMjJENDlcKSBRdWFydHogUERGQ29udGV4dCkgL0NyZWF0b3IgKFZvb3J2ZXJ0b25pbmcpCi9D +cmVhdGlvbkRhdGUgKEQ6MjAyMzAyMTYwOTExNDdaMDAnMDAnKSAvTW9kRGF0ZSAoRDoyMDIzMDIx +NjA5MTE0N1owMCcwMCcpCj4+CmVuZG9iagp4cmVmCjAgMTAKMDAwMDAwMDAwMCA2NTUzNSBmIAow +MDAwMDAwMTg2IDAwMDAwIG4gCjAwMDAwMDI2MDIgMDAwMDAgbiAKMDAwMDAwMDAyMiAwMDAwMCBu +IAowMDAwMDAwMzAwIDAwMDAwIG4gCjAwMDAwMDAzODkgMDAwMDAgbiAKMDAwMDAwMjU2NyAwMDAw +MCBuIAowMDAwMDAyMTI1IDAwMDAwIG4gCjAwMDAwMDI2OTUgMDAwMDAgbiAKMDAwMDAwMjc0NCAw +MDAwMCBuIAp0cmFpbGVyCjw8IC9TaXplIDEwIC9Sb290IDggMCBSIC9JbmZvIDkgMCBSIC9JRCBb +IDxlNzk0OWE2YzQ2MWM1MDYxNTk0ODU5YjkxZjczYzAzMz4KPGU3OTQ5YTZjNDYxYzUwNjE1OTQ4 +NTliOTFmNzNjMDMzPiBdID4+CnN0YXJ0eHJlZgozMTYxCiUlRU9GCg== +--Apple-Mail=_7C42F551-D023-4101-858E-D004D27871BE +Content-Transfer-Encoding: 7bit +Content-Type: text/html; + charset=us-ascii + +

This is the second html part

+--Apple-Mail=_7C42F551-D023-4101-858E-D004D27871BE +Content-Disposition: inline; + filename=attachment2.pdf +Content-Type: application/pdf; + x-unix-mode=0666; + name="attachment2.pdf" +Content-Transfer-Encoding: base64 + +JVBERi0xLjMKJcTl8uXrp/Og0MTGCjMgMCBvYmoKPDwgL0ZpbHRlciAvRmxhdGVEZWNvZGUgL0xl +bmd0aCA5MyA+PgpzdHJlYW0KeAEdjDsOgCAQBXtP8U4ALB9Zehs7qawNoTARFaL3l5hpZ6YiokJ1 +XHBCezeCLQnmwGgZK07IJbeU7+fdDrS926TpL4yCNl6Q8QyrnAjWhiEVyLkQpquf4wdGBBarCmVu +ZHN0cmVhbQplbmRvYmoKMSAwIG9iago8PCAvVHlwZSAvUGFnZSAvUGFyZW50IDIgMCBSIC9SZXNv +dXJjZXMgNCAwIFIgL0NvbnRlbnRzIDMgMCBSIC9NZWRpYUJveCBbMCAwIDU5NS4yNzU2IDg0MS44 +ODk4XQo+PgplbmRvYmoKNCAwIG9iago8PCAvUHJvY1NldCBbIC9QREYgL0ltYWdlQiAvSW1hZ2VD +IC9JbWFnZUkgXSAvWE9iamVjdCA8PCAvSW0xIDUgMCBSID4+ID4+CmVuZG9iago1IDAgb2JqCjw8 +IC9UeXBlIC9YT2JqZWN0IC9TdWJ0eXBlIC9JbWFnZSAvV2lkdGggMTIxIC9IZWlnaHQgMzAgL0lu +dGVycG9sYXRlIHRydWUKL0NvbG9yU3BhY2UgNiAwIFIgL0ludGVudCAvUGVyY2VwdHVhbCAvQml0 +c1BlckNvbXBvbmVudCA4IC9MZW5ndGggMTczNSAvRmlsdGVyCi9GbGF0ZURlY29kZSA+PgpzdHJl +YW0KeAHtl4lT1kUYx/+Eymq677s0TTHzajSPPBFE8UA0T1RuxRMVUfEAEVECFQVREQ9UxiNFVFAU +FJAxGrDM+9bu+gPsI8+0s+27v3qFmnLm987OO8+9u9999nn2d++e+3MRcBFwEXARcBFwEXBE4Le7 +Td3RYAQcYbUpGjyL6wgCNkQdZS5ijUHAEVabojETNd737vWW311v2fg4/1UEG6KOMn2RVdU9Hul1 +mrFme6guh7568aO62s4/3WoucoM1jL1nnws43nL0Xu/t/yXLv93O+XMd0rZEjluUMjRu1ayMuZXV +PWQljrDaFPrip6xIEKi7R+fqcugxC1NRVZzuKXKDNYy9Z/8nUP/1dsjAt4IOsf0n+51kQDzue2rd +zvFs04aoo0zBQsa+Nrj4+QGlrwwqebR3VV1dJ6WCMBZjsLrlA9EPBdQ9J28C3qUbYoDo1lWflXmR +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 +99snQN8DcNn6BsE0ZMMKZW5kc3RyZWFtCmVuZG9iago2IDAgb2JqClsgL0lDQ0Jhc2VkIDcgMCBS +IF0KZW5kb2JqCjIgMCBvYmoKPDwgL1R5cGUgL1BhZ2VzIC9NZWRpYUJveCBbMCAwIDU5NS4yNzU2 +IDg0MS44ODk4XSAvQ291bnQgMSAvS2lkcyBbIDEgMCBSIF0KPj4KZW5kb2JqCjggMCBvYmoKPDwg +L1R5cGUgL0NhdGFsb2cgL1BhZ2VzIDIgMCBSID4+CmVuZG9iago5IDAgb2JqCjw8IC9UaXRsZSAo +/v9cMDAwU1wwMDBjXDAwMGhcMDAwZVwwMDByXDAwMG1cMDAwrVwwMDBhXDAwMGZcMDAwYlwwMDBl +XDAwMGVcMDAwbFwwMDBkXDAwMGlcMDAwblwwMDBnXDAwMCBcMDAwMlwwMDAwXDAwMDJcMDAwM1ww +MDAtXDAwMDBcMDAwMlwwMDAtXDAwMDFcMDAwNlwwMDAgXDAwMG9cMDAwbVwwMDAgXDAwMDFcMDAw +MFwwMDAuXDAwMDFcMDAwMVwwMDAuXDAwMDFcMDAwMFwwMDAuXDAwMHBcMDAwblwwMDBnKQovUHJv +ZHVjZXIgKG1hY09TIFZlcnNpZSAxMy4yIFwoYnVpbGQgMjJENDlcKSBRdWFydHogUERGQ29udGV4 +dCkgL0NyZWF0b3IgKFZvb3J2ZXJ0b25pbmcpCi9DcmVhdGlvbkRhdGUgKEQ6MjAyMzAyMTYwOTEx +MzhaMDAnMDAnKSAvTW9kRGF0ZSAoRDoyMDIzMDIxNjA5MTEzOFowMCcwMCcpCj4+CmVuZG9iagp4 +cmVmCjAgMTAKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMTg2IDAwMDAwIG4gCjAwMDAwMDI4 +MDMgMDAwMDAgbiAKMDAwMDAwMDAyMiAwMDAwMCBuIAowMDAwMDAwMzAwIDAwMDAwIG4gCjAwMDAw +MDAzODkgMDAwMDAgbiAKMDAwMDAwMjc2OCAwMDAwMCBuIAowMDAwMDAyMzI2IDAwMDAwIG4gCjAw +MDAwMDI4OTYgMDAwMDAgbiAKMDAwMDAwMjk0NSAwMDAwMCBuIAp0cmFpbGVyCjw8IC9TaXplIDEw +IC9Sb290IDggMCBSIC9JbmZvIDkgMCBSIC9JRCBbIDxjOWI4YzBlZGQ5Mjk1Y2U3ZmQ2YzFkOWJj +ZTJhZTRiOD4KPGM5YjhjMGVkZDkyOTVjZTdmZDZjMWQ5YmNlMmFlNGI4PiBdID4+CnN0YXJ0eHJl +ZgozMzYyCiUlRU9GCg== +--Apple-Mail=_7C42F551-D023-4101-858E-D004D27871BE +Content-Transfer-Encoding: 7bit +Content-Type: text/html; + charset=us-ascii + +

This is the last html part
https://www.there.com



+
+--Apple-Mail=_7C42F551-D023-4101-858E-D004D27871BE-- + +--Apple-Mail=_5382A451-FE2F-4504-9E3F-8FEB0B716E43-- diff --git a/plugins/php-imap/tests/messages/multiple_nested_attachments.eml b/plugins/php-imap/tests/messages/multiple_nested_attachments.eml new file mode 100644 index 00000000..ad48d47b --- /dev/null +++ b/plugins/php-imap/tests/messages/multiple_nested_attachments.eml @@ -0,0 +1,84 @@ +Date: Mon, 15 Jan 2018 10:54:09 +0100 +User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101 + Thunderbird/52.5.0 +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="------------85793CE1578A77318D5A90A6" +Content-Language: en-US + +This is a multi-part message in MIME format. +--------------85793CE1578A77318D5A90A6 +Content-Type: multipart/alternative; + boundary="------------32D598A7FC3F8A8E228998B0" + + +--------------32D598A7FC3F8A8E228998B0 +Content-Type: text/plain; charset=utf-8; format=flowed +Content-Transfer-Encoding: 7bit + + +------------------------------------------------------------------------ + + + + +--------------32D598A7FC3F8A8E228998B0 +Content-Type: multipart/related; + boundary="------------261DC39B47CFCDB73BCE3C18" + + +--------------261DC39B47CFCDB73BCE3C18 +Content-Type: text/html; charset=utf-8 +Content-Transfer-Encoding: 8bit + + + + + + + +


+

+
+ + + Â +
+ + + + + + + +

+

+
+
+ + + +--------------261DC39B47CFCDB73BCE3C18 +Content-Type: image/png; + name="mleokdgdlgkkecep.png" +Content-Transfer-Encoding: base64 +Content-ID: +Content-Disposition: inline; + filename="mleokdgdlgkkecep.png" + +iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX/TQBcNTh/AAAACklE +QVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg== +--------------261DC39B47CFCDB73BCE3C18-- + +--------------32D598A7FC3F8A8E228998B0-- + +--------------85793CE1578A77318D5A90A6 +Content-Type: image/png; + name="FF4D00-1.png" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename="FF4D00-1.png" + +iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX/TQBcNTh/AAAACklE +QVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg== +--------------85793CE1578A77318D5A90A6-- \ No newline at end of file diff --git a/plugins/php-imap/tests/messages/nestes_embedded_with_attachment.eml b/plugins/php-imap/tests/messages/nestes_embedded_with_attachment.eml new file mode 100644 index 00000000..44498103 --- /dev/null +++ b/plugins/php-imap/tests/messages/nestes_embedded_with_attachment.eml @@ -0,0 +1,145 @@ +From: from@there.com +To: to@here.com +Subject: Nuu +Date: Wed, 13 Sep 2017 13:05:45 +0200 +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="----=_NextPart_000_000_000" + +This is a multi-part message in MIME format. + +------=_NextPart_000_000_000 +Content-Type: multipart/alternative; + boundary="----=_NextPart_000_111_000" + + +------=_NextPart_000_111_000 +Content-Type: text/plain; + charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + +Dear Sarah + +------=_NextPart_000_111_000 +Content-Type: text/html; + charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + + + +
Dear Sarah,
+ + +------=_NextPart_000_111_000-- + +------=_NextPart_000_000_000 +Content-Type: message/rfc822; + name="first.eml" +Content-Transfer-Encoding: 7bit +Content-Disposition: attachment; + filename="first.eml" + +From: from@there.com +To: to@here.com +Subject: FIRST +Date: Sat, 28 Apr 2018 14:37:16 -0400 +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="----=_NextPart_000_222_000" + +This is a multi-part message in MIME format. + +------=_NextPart_000_222_000 +Content-Type: multipart/alternative; + boundary="----=_NextPart_000_222_111" + + +------=_NextPart_000_222_111 +Content-Type: text/plain; + charset="UTF-8" +Content-Transfer-Encoding: quoted-printable + +Please respond directly to this email to update your RMA + + +2018-04-17T11:04:03-04:00 +------=_NextPart_000_222_111 +Content-Type: text/html; + charset="UTF-8" +Content-Transfer-Encoding: quoted-printable + + + +
Please respond directly to this = +email to=20 +update your RMA
+ +------=_NextPart_000_222_111-- + +------=_NextPart_000_222_000 +Content-Type: image/png; + name="chrome.png" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename="chrome.png" + +iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAAB+FBMVEUAAAA/mUPidDHiLi5Cn0Xk +NTPmeUrkdUg/m0Q0pEfcpSbwaVdKskg+lUP4zA/iLi3msSHkOjVAmETdJSjtYFE/lkPnRj3sWUs8 +kkLeqCVIq0fxvhXqUkbVmSjwa1n1yBLepyX1xxP0xRXqUkboST9KukpHpUbuvRrzrhF/ljbwalju +ZFM4jELaoSdLtElJrUj1xxP6zwzfqSU4i0HYnydMtUlIqUfywxb60AxZqEXaoifgMCXptR9MtklH +pEY2iUHWnSjvvRr70QujkC+pUC/90glMuEnlOjVMt0j70QriLS1LtEnnRj3qUUXfIidOjsxAhcZF +o0bjNDH0xxNLr0dIrUdmntVTkMoyfL8jcLBRuErhJyrgKyb4zA/5zg3tYFBBmUTmQTnhMinruBzv +vhnxwxZ/st+Ktt5zp9hqota2vtK6y9FemNBblc9HiMiTtMbFtsM6gcPV2r6dwroseLrMrbQrdLGd +yKoobKbo3Zh+ynrgVllZulTsXE3rV0pIqUf42UVUo0JyjEHoS0HmsiHRGR/lmRz/1hjqnxjvpRWf +wtOhusaz0LRGf7FEfbDVmqHXlJeW0pbXq5bec3fX0nTnzmuJuWvhoFFhm0FtrziBsjaAaDCYWC+u +Si6jQS3FsSfLJiTirCOkuCG1KiG+wSC+GBvgyhTszQ64Z77KAAAARXRSTlMAIQRDLyUgCwsE6ebm +5ubg2dLR0byXl4FDQzU1NDEuLSUgC+vr6urq6ubb29vb2tra2tG8vLu7u7uXl5eXgYGBgYGBLiUA +LabIAAABsElEQVQoz12S9VPjQBxHt8VaOA6HE+AOzv1wd7pJk5I2adpCC7RUcHd3d3fXf5PvLkxh +eD++z+yb7GSRlwD/+Hj/APQCZWxM5M+goF+RMbHK594v+tPoiN1uHxkt+xzt9+R9wnRTZZQpXQ0T +5uP1IQxToyOAZiQu5HEpjeA4SWIoksRxNiGC1tRZJ4LNxgHgnU5nJZBDvuDdl8lzQRBsQ+s9PZt7 +s7Pz8wsL39/DkIfZ4xlB2Gqsq62ta9oxVlVrNZpihFRpGO9fzQw1ms0NDWZz07iGkJmIFH8xxkc3 +a/WWlubmFkv9AB2SEpDvKxbjidN2faseaNV3zoHXvv7wMODJdkOHAegweAfFPx4G67KluxzottCU +9n8CUqXzcIQdXOytAHqXxomvykhEKN9EFutG22p//0rbNvHVxiJywa8yS2KDfV1dfbu31H8jF1RH +iTKtWYeHxUvq3bn0pyjCRaiRU6aDO+gb3aEfEeVNsDgm8zzLy9egPa7Qt8TSJdwhjplk06HH43ZN +J3s91KKCHQ5x4sw1fRGYDZ0n1L4FKb9/BP5JLYxToheoFCVxz57PPS8UhhEpLBVeAAAAAElFTkSu +QmCC + +------=_NextPart_000_222_000-- + +------=_NextPart_000_000_000 +Content-Type: message/rfc822; + name="second.eml" +Content-Transfer-Encoding: 7bit +Content-Disposition: attachment; + filename="second.eml" + +From: from@there.com +To: to@here.com +Subject: SECOND +Date: Sat, 28 Apr 2018 13:37:30 -0400 +MIME-Version: 1.0 +Content-Type: multipart/alternative; + boundary="----=_NextPart_000_333_000" + +This is a multi-part message in MIME format. + +------=_NextPart_000_333_000 +Content-Type: text/plain; + charset="UTF-8" +Content-Transfer-Encoding: quoted-printable + +T whom it may concern: +------=_NextPart_000_333_000 +Content-Type: text/html; + charset="UTF-8" +Content-Transfer-Encoding: quoted-printable + + + +
T whom it may concern:
+ + +------=_NextPart_000_333_000-- + +------=_NextPart_000_000_000-- + diff --git a/plugins/php-imap/tests/messages/null_content_charset.eml b/plugins/php-imap/tests/messages/null_content_charset.eml new file mode 100644 index 00000000..0f19eb44 --- /dev/null +++ b/plugins/php-imap/tests/messages/null_content_charset.eml @@ -0,0 +1,8 @@ +Subject: test +MIME-Version: 1.0 +Content-Type: text/plain +Date: Wed, 27 Sep 2017 12:48:51 +0200 +From: from@there.com +To: to@here.com + +Hi! \ No newline at end of file diff --git a/plugins/php-imap/tests/messages/pec.eml b/plugins/php-imap/tests/messages/pec.eml new file mode 100644 index 00000000..15dfacca --- /dev/null +++ b/plugins/php-imap/tests/messages/pec.eml @@ -0,0 +1,65 @@ +To: test@example.com +From: test@example.com +Subject: Certified +Date: Mon, 2 Oct 2017 12:13:43 +0200 +Content-Type: multipart/signed; protocol="application/x-pkcs7-signature"; micalg="sha1"; boundary="----258A05BDE519DE69AAE8D59024C32F5E" + +This is an S/MIME signed message + +------258A05BDE519DE69AAE8D59024C32F5E +Content-Type: multipart/mixed; boundary="----------=_1506939223-24530-42" +Content-Transfer-Encoding: binary + +------------=_1506939223-24530-42 +Content-Type: multipart/alternative; + boundary="----------=_1506939223-24530-43" +Content-Transfer-Encoding: binary + +------------=_1506939223-24530-43 +Content-Type: text/plain; charset="iso-8859-1" +Content-Disposition: inline +Content-Transfer-Encoding: quoted-printable + +Signed + +------------=_1506939223-24530-43 +Content-Type: text/html; charset="iso-8859-1" +Content-Disposition: inline +Content-Transfer-Encoding: quoted-printable + +Signed + +------------=_1506939223-24530-43-- + +------------=_1506939223-24530-42 +Content-Type: application/xml; name="data.xml" +Content-Disposition: inline; filename="data.xml" +Content-Transfer-Encoding: base64 + +PHhtbC8+ + +------------=_1506939223-24530-42 +Content-Type: message/rfc822; name="postacert.eml" +Content-Disposition: inline; filename="postacert.eml" +Content-Transfer-Encoding: 7bit + +To: test@example.com +From: test@example.com +Subject: test-subject +Date: Mon, 2 Oct 2017 12:13:50 +0200 +Content-Type: text/plain; charset=iso-8859-15; format=flowed +Content-Transfer-Encoding: 7bit + +test-content + +------------=_1506939223-24530-42-- + +------258A05BDE519DE69AAE8D59024C32F5E +Content-Type: application/x-pkcs7-signature; name="smime.p7s" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename="smime.p7s" + +MQ== + +------258A05BDE519DE69AAE8D59024C32F5E-- + diff --git a/plugins/php-imap/tests/messages/plain.eml b/plugins/php-imap/tests/messages/plain.eml new file mode 100644 index 00000000..4d3d22b1 --- /dev/null +++ b/plugins/php-imap/tests/messages/plain.eml @@ -0,0 +1,9 @@ +From: from@someone.com +To: to@someone-else.com +Subject: Example +Date: Mon, 21 Jan 2023 19:36:45 +0200 +Content-Type: text/plain; + charset=\"us-ascii\" +Content-Transfer-Encoding: quoted-printable + +Hi there! \ No newline at end of file diff --git a/plugins/php-imap/tests/messages/plain_only.eml b/plugins/php-imap/tests/messages/plain_only.eml new file mode 100644 index 00000000..bbf9f3b3 --- /dev/null +++ b/plugins/php-imap/tests/messages/plain_only.eml @@ -0,0 +1,9 @@ +From: from@there.com +To: to@here.com +Subject: Nuu +Date: Wed, 13 Sep 2017 13:05:45 +0200 +Content-Type: text/plain; + charset="us-ascii" +Content-Transfer-Encoding: quoted-printable + +Hi \ No newline at end of file diff --git a/plugins/php-imap/tests/messages/plain_text_attachment.eml b/plugins/php-imap/tests/messages/plain_text_attachment.eml new file mode 100644 index 00000000..6ec6a6c5 --- /dev/null +++ b/plugins/php-imap/tests/messages/plain_text_attachment.eml @@ -0,0 +1,21 @@ +To: to@here.com +From: from@there.com +Subject: Plain text attachment +Date: Tue, 21 Aug 2018 09:05:14 +0200 +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="------------B832AF745285AEEC6D5AEE42" + +This is a multi-part message in MIME format. +--------------B832AF745285AEEC6D5AEE42 +Content-Type: text/plain; charset=iso-8859-15; format=flowed +Content-Transfer-Encoding: 7bit + +Test +--------------B832AF745285AEEC6D5AEE42 +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename="a.txt" + +SGkh +--------------B832AF745285AEEC6D5AEE42-- diff --git a/plugins/php-imap/tests/messages/references.eml b/plugins/php-imap/tests/messages/references.eml new file mode 100644 index 00000000..478383cf --- /dev/null +++ b/plugins/php-imap/tests/messages/references.eml @@ -0,0 +1,11 @@ +Message-ID: <123@example.com> +From: no_host +Cc: "This one: is \"right\"" , No-address +In-Reply-To: +References: <231d9ac57aec7d8c1a0eacfeab8af6f3@example.com> <08F04024-A5B3-4FDE-BF2C-6710DE97D8D9@example.com> +Content-Type: text/plain; + charset="us-ascii" +Content-Transfer-Encoding: quoted-printable + +Hi +How are you? diff --git a/plugins/php-imap/tests/messages/simple_multipart.eml b/plugins/php-imap/tests/messages/simple_multipart.eml new file mode 100644 index 00000000..60a6521c --- /dev/null +++ b/plugins/php-imap/tests/messages/simple_multipart.eml @@ -0,0 +1,23 @@ +From: from@there.com +To: to@here.com +Date: Wed, 27 Sep 2017 12:48:51 +0200 +Subject: test +Content-Type: multipart/alternative; + boundary="----=_NextPart_000_0081_01D32C91.033E7020" + +This is a multipart message in MIME format. + +------=_NextPart_000_0081_01D32C91.033E7020 +Content-Type: text/plain; + charset="us-ascii" +Content-Transfer-Encoding: 7bit + +MyPlain +------=_NextPart_000_0081_01D32C91.033E7020 +Content-Type: text/html; + charset="us-ascii" +Content-Transfer-Encoding: quoted-printable + +MyHtml +------=_NextPart_000_0081_01D32C91.033E7020-- + diff --git a/plugins/php-imap/tests/messages/structured_with_attachment.eml b/plugins/php-imap/tests/messages/structured_with_attachment.eml new file mode 100644 index 00000000..f795ec3a --- /dev/null +++ b/plugins/php-imap/tests/messages/structured_with_attachment.eml @@ -0,0 +1,24 @@ +From: from@there.com +To: to@here.com +Subject: Test +Date: Fri, 29 Sep 2017 10:55:23 +0200 +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="------------5B1F217006A67C28E756A62E" + +This is a multi-part message in MIME format. +--------------5B1F217006A67C28E756A62E +Content-Type: text/plain; charset=iso-8859-15; format=flowed +Content-Transfer-Encoding: 7bit + +Test + +--------------5B1F217006A67C28E756A62E +Content-Type: text/plain; charset=UTF-8; + name="MyFile.txt" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename="MyFile.txt" + +TXlGaWxlQ29udGVudA== +--------------5B1F217006A67C28E756A62E-- diff --git a/plugins/php-imap/tests/messages/thread_my_topic.eml b/plugins/php-imap/tests/messages/thread_my_topic.eml new file mode 100644 index 00000000..9a196351 --- /dev/null +++ b/plugins/php-imap/tests/messages/thread_my_topic.eml @@ -0,0 +1,10 @@ +Message-ID: <123@example.com> +From: from@there.com +To: to@here.com +Subject: Nuu +Date: Wed, 13 Sep 2017 13:05:45 +0200 +Content-Type: text/plain; + charset="us-ascii" +Content-Transfer-Encoding: quoted-printable + +Hi diff --git a/plugins/php-imap/tests/messages/thread_re_my_topic.eml b/plugins/php-imap/tests/messages/thread_re_my_topic.eml new file mode 100644 index 00000000..db4fdf41 --- /dev/null +++ b/plugins/php-imap/tests/messages/thread_re_my_topic.eml @@ -0,0 +1,11 @@ +Message-ID: <456@example.com> +In-Reply-To: <123@example.com> +From: from@there.com +To: to@here.com +Subject: Re: Nuu +Date: Wed, 13 Sep 2017 13:05:45 +0200 +Content-Type: text/plain; + charset="us-ascii" +Content-Transfer-Encoding: quoted-printable + +Hi diff --git a/plugins/php-imap/tests/messages/thread_unrelated.eml b/plugins/php-imap/tests/messages/thread_unrelated.eml new file mode 100644 index 00000000..e47faa81 --- /dev/null +++ b/plugins/php-imap/tests/messages/thread_unrelated.eml @@ -0,0 +1,10 @@ +Message-ID: <999@example.com> +From: from@there.com +To: to@here.com +Subject: Wut +Date: Wed, 13 Sep 2017 13:05:45 +0200 +Content-Type: text/plain; + charset="us-ascii" +Content-Transfer-Encoding: quoted-printable + +Hi diff --git a/plugins/php-imap/tests/messages/undefined_charset_header.eml b/plugins/php-imap/tests/messages/undefined_charset_header.eml new file mode 100644 index 00000000..117a33e4 --- /dev/null +++ b/plugins/php-imap/tests/messages/undefined_charset_header.eml @@ -0,0 +1,20 @@ +X-Real-To: +X-Stored-In: BlaBla +Return-Path: +Received: from + by bla.bla (CommuniGate Pro RULE 6.1.13) + with RULE id 14057804; Mon, 27 Feb 2017 13:21:44 +0930 +X-Autogenerated: Mirror +Resent-From: +Resent-Date: Mon, 27 Feb 2017 13:21:44 +0930 +Message-Id: <201702270351.BGF77614@bla.bla> +From: =?X-IAS-German?B?bXlHb3Y=?= +To: sales@bla.bla +Subject: =?X-IAS-German?B?U3VibWl0IHlvdXIgdGF4IHJlZnVuZCB8IEF1c3RyYWxpYW4gVGF4YXRpb24gT2ZmaWNlLg==?= +Date: 27 Feb 2017 04:51:29 +0100 +MIME-Version: 1.0 +Content-Type: text/html; + charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + +) \ No newline at end of file diff --git a/plugins/php-imap/tests/messages/undisclosed_recipients.eml b/plugins/php-imap/tests/messages/undisclosed_recipients.eml new file mode 100644 index 00000000..5d33ecff --- /dev/null +++ b/plugins/php-imap/tests/messages/undisclosed_recipients.eml @@ -0,0 +1,8 @@ +Subject: test +MIME-Version: 1.0 +Content-Type: text/plain +Date: Wed, 27 Sep 2017 12:48:51 +0200 +From: from@there.com +To: "Undisclosed Recipients" <> + +Hi! diff --git a/plugins/php-imap/tests/messages/undisclosed_recipients_minus.eml b/plugins/php-imap/tests/messages/undisclosed_recipients_minus.eml new file mode 100644 index 00000000..73f7b83f --- /dev/null +++ b/plugins/php-imap/tests/messages/undisclosed_recipients_minus.eml @@ -0,0 +1,8 @@ +Subject: test +MIME-Version: 1.0 +Content-Type: text/plain +Date: Wed, 27 Sep 2017 12:48:51 +0200 +From: from@there.com +To: undisclosed-recipients:; + +Hi! diff --git a/plugins/php-imap/tests/messages/undisclosed_recipients_space.eml b/plugins/php-imap/tests/messages/undisclosed_recipients_space.eml new file mode 100644 index 00000000..6c06cbd4 --- /dev/null +++ b/plugins/php-imap/tests/messages/undisclosed_recipients_space.eml @@ -0,0 +1,8 @@ +Subject: test +MIME-Version: 1.0 +Content-Type: text/plain +Date: Wed, 27 Sep 2017 12:48:51 +0200 +From: from@there.com +To: Undisclosed recipients:; + +Hi! diff --git a/plugins/php-imap/tests/messages/unknown_encoding.eml b/plugins/php-imap/tests/messages/unknown_encoding.eml new file mode 100644 index 00000000..271bc4e2 --- /dev/null +++ b/plugins/php-imap/tests/messages/unknown_encoding.eml @@ -0,0 +1,24 @@ +From: from@there.com +To: to@here.com +Date: Wed, 27 Sep 2017 12:48:51 +0200 +Subject: test +Content-Type: multipart/alternative; + boundary="----=_NextPart_000_0081_01D32C91.033E7020" + +This is a multipart message in MIME format. + +------=_NextPart_000_0081_01D32C91.033E7020 +Content-Type: text/plain; + charset= +Content-Transfer-Encoding: foobar + +MyPlain + +------=_NextPart_000_0081_01D32C91.033E7020 +Content-Type: text/html; + charset="" +Content-Transfer-Encoding: quoted-printable + +MyHtml +------=_NextPart_000_0081_01D32C91.033E7020-- + diff --git a/plugins/php-imap/tests/messages/without_charset_plain_only.eml b/plugins/php-imap/tests/messages/without_charset_plain_only.eml new file mode 100644 index 00000000..bd4fe5de --- /dev/null +++ b/plugins/php-imap/tests/messages/without_charset_plain_only.eml @@ -0,0 +1,8 @@ +From: from@there.com +To: to@here.com +Subject: Nuu +Date: Wed, 13 Sep 2017 13:05:45 +0200 +Content-Type: text/plain; charset= +Content-Transfer-Encoding: quoted-printable + +Hi \ No newline at end of file diff --git a/plugins/php-imap/tests/messages/without_charset_simple_multipart.eml b/plugins/php-imap/tests/messages/without_charset_simple_multipart.eml new file mode 100644 index 00000000..d1a092d2 --- /dev/null +++ b/plugins/php-imap/tests/messages/without_charset_simple_multipart.eml @@ -0,0 +1,23 @@ +From: from@there.com +To: to@here.com +Date: Wed, 27 Sep 2017 12:48:51 +0200 +Subject: test +Content-Type: multipart/alternative; + boundary="----=_NextPart_000_0081_01D32C91.033E7020" + +This is a multipart message in MIME format. + +------=_NextPart_000_0081_01D32C91.033E7020 +Content-Type: text/plain; + charset= +Content-Transfer-Encoding: 7bit + +MyPlain +------=_NextPart_000_0081_01D32C91.033E7020 +Content-Type: text/html; + charset="" +Content-Transfer-Encoding: quoted-printable + +MyHtml +------=_NextPart_000_0081_01D32C91.033E7020-- + diff --git a/plugins/php-mime-mail-parser/src/Attachment.php b/plugins/php-mime-mail-parser/src/Attachment.php deleted file mode 100644 index 1a731635..00000000 --- a/plugins/php-mime-mail-parser/src/Attachment.php +++ /dev/null @@ -1,276 +0,0 @@ -filename = $filename; - $this->contentType = $contentType; - $this->stream = $stream; - $this->content = null; - $this->contentDisposition = $contentDisposition; - $this->contentId = $contentId; - $this->headers = $headers; - $this->mimePartStr = $mimePartStr; - } - - /** - * retrieve the attachment filename - * - * @return string - */ - public function getFilename() - { - return $this->filename; - } - - /** - * Retrieve the Attachment Content-Type - * - * @return string - */ - public function getContentType() - { - return $this->contentType; - } - - /** - * Retrieve the Attachment Content-Disposition - * - * @return string - */ - public function getContentDisposition() - { - return $this->contentDisposition; - } - - /** - * Retrieve the Attachment Content-ID - * - * @return string - */ - public function getContentID() - { - return $this->contentId; - } - - /** - * Retrieve the Attachment Headers - * - * @return array - */ - public function getHeaders() - { - return $this->headers; - } - - /** - * Get a handle to the stream - * - * @return resource - */ - public function getStream() - { - return $this->stream; - } - - /** - * Rename a file if it already exists at its destination. - * Renaming is done by adding a duplicate number to the file name. E.g. existingFileName_1.ext. - * After a max duplicate number, renaming the file will switch over to generating a random suffix. - * - * @param string $fileName Complete path to the file. - * @return string The suffixed file name. - */ - protected function suffixFileName(string $fileName): string - { - $pathInfo = pathinfo($fileName); - $dirname = $pathInfo['dirname'].DIRECTORY_SEPARATOR; - $filename = $pathInfo['filename']; - $extension = empty($pathInfo['extension']) ? '' : '.'.$pathInfo['extension']; - - $i = 0; - do { - $i++; - - if ($i > $this->maxDuplicateNumber) { - $duplicateExtension = uniqid(); - } else { - $duplicateExtension = $i; - } - - $resultName = $dirname.$filename."_$duplicateExtension".$extension; - } while (file_exists($resultName)); - - return $resultName; - } - - /** - * Read the contents a few bytes at a time until completed - * Once read to completion, it always returns false - * - * @param int $bytes (default: 2082) - * - * @return string|bool - */ - public function read($bytes = 2082) - { - return feof($this->stream) ? false : fread($this->stream, $bytes); - } - - /** - * Retrieve the file content in one go - * Once you retrieve the content you cannot use MimeMailParser_attachment::read() - * - * @return string - */ - public function getContent() - { - if ($this->content === null) { - fseek($this->stream, 0); - while (($buf = $this->read()) !== false) { - $this->content .= $buf; - } - } - - return $this->content; - } - - /** - * Get mime part string for this attachment - * - * @return string - */ - public function getMimePartStr() - { - return $this->mimePartStr; - } - - /** - * Save the attachment individually - * - * @param string $attach_dir - * @param string $filenameStrategy - * - * @return string - */ - public function save( - $attach_dir, - $filenameStrategy = Parser::ATTACHMENT_DUPLICATE_SUFFIX - ) { - $attach_dir = rtrim($attach_dir, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR; - if (!is_dir($attach_dir)) { - mkdir($attach_dir); - } - - // Determine filename - switch ($filenameStrategy) { - case Parser::ATTACHMENT_RANDOM_FILENAME: - $fileInfo = pathinfo($this->getFilename()); - $extension = empty($fileInfo['extension']) ? '' : '.'.$fileInfo['extension']; - $attachment_path = $attach_dir.uniqid().$extension; - break; - case Parser::ATTACHMENT_DUPLICATE_THROW: - case Parser::ATTACHMENT_DUPLICATE_SUFFIX: - $attachment_path = $attach_dir.$this->getFilename(); - break; - default: - throw new Exception('Invalid filename strategy argument provided.'); - } - - // Handle duplicate filename - if (file_exists($attachment_path)) { - switch ($filenameStrategy) { - case Parser::ATTACHMENT_DUPLICATE_THROW: - throw new Exception('Could not create file for attachment: duplicate filename.'); - case Parser::ATTACHMENT_DUPLICATE_SUFFIX: - $attachment_path = $this->suffixFileName($attachment_path); - break; - } - } - - /** @var resource $fp */ - if ($fp = fopen($attachment_path, 'w')) { - while ($bytes = $this->read()) { - fwrite($fp, $bytes); - } - fclose($fp); - return realpath($attachment_path); - } else { - throw new Exception('Could not write attachments. Your directory may be unwritable by PHP.'); - } - } -} diff --git a/plugins/php-mime-mail-parser/src/Charset.php b/plugins/php-mime-mail-parser/src/Charset.php deleted file mode 100644 index cd219f22..00000000 --- a/plugins/php-mime-mail-parser/src/Charset.php +++ /dev/null @@ -1,370 +0,0 @@ - 'us-ascii', - 'us-ascii' => 'us-ascii', - 'ansi_x3.4-1968' => 'us-ascii', - '646' => 'us-ascii', - 'iso-8859-1' => 'iso-8859-1', - 'iso-8859-2' => 'iso-8859-2', - 'iso-8859-3' => 'iso-8859-3', - 'iso-8859-4' => 'iso-8859-4', - 'iso-8859-5' => 'iso-8859-5', - 'iso-8859-6' => 'iso-8859-6', - 'iso-8859-6-i' => 'iso-8859-6-i', - 'iso-8859-6-e' => 'iso-8859-6-e', - 'iso-8859-7' => 'iso-8859-7', - 'iso-8859-8' => 'iso-8859-8', - 'iso-8859-8-i' => 'iso-8859-8', - 'iso-8859-8-e' => 'iso-8859-8-e', - 'iso-8859-9' => 'iso-8859-9', - 'iso-8859-10' => 'iso-8859-10', - 'iso-8859-11' => 'iso-8859-11', - 'iso-8859-13' => 'iso-8859-13', - 'iso-8859-14' => 'iso-8859-14', - 'iso-8859-15' => 'iso-8859-15', - 'iso-8859-16' => 'iso-8859-16', - 'iso-ir-111' => 'iso-ir-111', - 'iso-2022-cn' => 'iso-2022-cn', - 'iso-2022-cn-ext' => 'iso-2022-cn', - 'iso-2022-kr' => 'iso-2022-kr', - 'iso-2022-jp' => 'iso-2022-jp', - 'utf-16be' => 'utf-16be', - 'utf-16le' => 'utf-16le', - 'utf-16' => 'utf-16', - 'windows-1250' => 'windows-1250', - 'windows-1251' => 'windows-1251', - 'windows-1252' => 'windows-1252', - 'windows-1253' => 'windows-1253', - 'windows-1254' => 'windows-1254', - 'windows-1255' => 'windows-1255', - 'windows-1256' => 'windows-1256', - 'windows-1257' => 'windows-1257', - 'windows-1258' => 'windows-1258', - 'ibm866' => 'ibm866', - 'ibm850' => 'ibm850', - 'ibm852' => 'ibm852', - 'ibm855' => 'ibm855', - 'ibm857' => 'ibm857', - 'ibm862' => 'ibm862', - 'ibm864' => 'ibm864', - 'utf-8' => 'utf-8', - 'utf-7' => 'utf-7', - 'shift_jis' => 'shift_jis', - 'big5' => 'big5', - 'euc-jp' => 'euc-jp', - 'euc-kr' => 'euc-kr', - 'gb2312' => 'gb2312', - 'gb18030' => 'gb18030', - 'viscii' => 'viscii', - 'koi8-r' => 'koi8-r', - 'koi8_r' => 'koi8-r', - 'cskoi8r' => 'koi8-r', - 'koi' => 'koi8-r', - 'koi8' => 'koi8-r', - 'koi8-u' => 'koi8-u', - 'tis-620' => 'tis-620', - 't.61-8bit' => 't.61-8bit', - 'hz-gb-2312' => 'hz-gb-2312', - 'big5-hkscs' => 'big5-hkscs', - 'gbk' => 'gbk', - 'cns11643' => 'x-euc-tw', - 'x-imap4-modified-utf7' => 'x-imap4-modified-utf7', - 'x-euc-tw' => 'x-euc-tw', - 'x-mac-ce' => 'macce', - 'x-mac-turkish' => 'macturkish', - 'x-mac-greek' => 'macgreek', - 'x-mac-icelandic' => 'macicelandic', - 'x-mac-croatian' => 'maccroatian', - 'x-mac-romanian' => 'macromanian', - 'x-mac-cyrillic' => 'maccyrillic', - 'x-mac-ukrainian' => 'macukrainian', - 'x-mac-hebrew' => 'machebrew', - 'x-mac-arabic' => 'macarabic', - 'x-mac-farsi' => 'macfarsi', - 'x-mac-devanagari' => 'macdevanagari', - 'x-mac-gujarati' => 'macgujarati', - 'x-mac-gurmukhi' => 'macgurmukhi', - 'armscii-8' => 'armscii-8', - 'x-viet-tcvn5712' => 'x-viet-tcvn5712', - 'x-viet-vps' => 'x-viet-vps', - 'iso-10646-ucs-2' => 'utf-16be', - 'x-iso-10646-ucs-2-be' => 'utf-16be', - 'x-iso-10646-ucs-2-le' => 'utf-16le', - 'x-user-defined' => 'x-user-defined', - 'x-johab' => 'x-johab', - 'latin1' => 'iso-8859-1', - 'iso_8859-1' => 'iso-8859-1', - 'iso8859-1' => 'iso-8859-1', - 'iso8859-2' => 'iso-8859-2', - 'iso8859-3' => 'iso-8859-3', - 'iso8859-4' => 'iso-8859-4', - 'iso8859-5' => 'iso-8859-5', - 'iso8859-6' => 'iso-8859-6', - 'iso8859-7' => 'iso-8859-7', - 'iso8859-8' => 'iso-8859-8', - 'iso8859-9' => 'iso-8859-9', - 'iso8859-10' => 'iso-8859-10', - 'iso8859-11' => 'iso-8859-11', - 'iso8859-13' => 'iso-8859-13', - 'iso8859-14' => 'iso-8859-14', - 'iso8859-15' => 'iso-8859-15', - 'iso_8859-1:1987' => 'iso-8859-1', - 'iso-ir-100' => 'iso-8859-1', - 'l1' => 'iso-8859-1', - 'ibm819' => 'iso-8859-1', - 'cp819' => 'iso-8859-1', - 'csisolatin1' => 'iso-8859-1', - 'latin2' => 'iso-8859-2', - 'iso_8859-2' => 'iso-8859-2', - 'iso_8859-2:1987' => 'iso-8859-2', - 'iso-ir-101' => 'iso-8859-2', - 'l2' => 'iso-8859-2', - 'csisolatin2' => 'iso-8859-2', - 'latin3' => 'iso-8859-3', - 'iso_8859-3' => 'iso-8859-3', - 'iso_8859-3:1988' => 'iso-8859-3', - 'iso-ir-109' => 'iso-8859-3', - 'l3' => 'iso-8859-3', - 'csisolatin3' => 'iso-8859-3', - 'latin4' => 'iso-8859-4', - 'iso_8859-4' => 'iso-8859-4', - 'iso_8859-4:1988' => 'iso-8859-4', - 'iso-ir-110' => 'iso-8859-4', - 'l4' => 'iso-8859-4', - 'csisolatin4' => 'iso-8859-4', - 'cyrillic' => 'iso-8859-5', - 'iso_8859-5' => 'iso-8859-5', - 'iso_8859-5:1988' => 'iso-8859-5', - 'iso-ir-144' => 'iso-8859-5', - 'csisolatincyrillic' => 'iso-8859-5', - 'arabic' => 'iso-8859-6', - 'iso_8859-6' => 'iso-8859-6', - 'iso_8859-6:1987' => 'iso-8859-6', - 'iso-ir-127' => 'iso-8859-6', - 'ecma-114' => 'iso-8859-6', - 'asmo-708' => 'iso-8859-6', - 'csisolatinarabic' => 'iso-8859-6', - 'csiso88596i' => 'iso-8859-6-i', - 'csiso88596e' => 'iso-8859-6-e', - 'greek' => 'iso-8859-7', - 'greek8' => 'iso-8859-7', - 'sun_eu_greek' => 'iso-8859-7', - 'iso_8859-7' => 'iso-8859-7', - 'iso_8859-7:1987' => 'iso-8859-7', - 'iso-ir-126' => 'iso-8859-7', - 'elot_928' => 'iso-8859-7', - 'ecma-118' => 'iso-8859-7', - 'csisolatingreek' => 'iso-8859-7', - 'hebrew' => 'iso-8859-8', - 'iso_8859-8' => 'iso-8859-8', - 'visual' => 'iso-8859-8', - 'iso_8859-8:1988' => 'iso-8859-8', - 'iso-ir-138' => 'iso-8859-8', - 'csisolatinhebrew' => 'iso-8859-8', - 'csiso88598i' => 'iso-8859-8', - 'iso-8859-8i' => 'iso-8859-8', - 'logical' => 'iso-8859-8', - 'csiso88598e' => 'iso-8859-8-e', - 'latin5' => 'iso-8859-9', - 'iso_8859-9' => 'iso-8859-9', - 'iso_8859-9:1989' => 'iso-8859-9', - 'iso-ir-148' => 'iso-8859-9', - 'l5' => 'iso-8859-9', - 'csisolatin5' => 'iso-8859-9', - 'unicode-1-1-utf-8' => 'utf-8', - 'utf8' => 'utf-8', - 'x-sjis' => 'shift_jis', - 'shift-jis' => 'shift_jis', - 'ms_kanji' => 'shift_jis', - 'csshiftjis' => 'shift_jis', - 'windows-31j' => 'shift_jis', - 'cp932' => 'shift_jis', - 'sjis' => 'shift_jis', - 'cseucpkdfmtjapanese' => 'euc-jp', - 'x-euc-jp' => 'euc-jp', - 'csiso2022jp' => 'iso-2022-jp', - 'iso-2022-jp-2' => 'iso-2022-jp', - 'csiso2022jp2' => 'iso-2022-jp', - 'csbig5' => 'big5', - 'cn-big5' => 'big5', - 'x-x-big5' => 'big5', - 'zh_tw-big5' => 'big5', - 'cseuckr' => 'euc-kr', - 'ks_c_5601-1987' => 'euc-kr', - 'iso-ir-149' => 'euc-kr', - 'ks_c_5601-1989' => 'euc-kr', - 'ksc_5601' => 'euc-kr', - 'ksc5601' => 'euc-kr', - 'korean' => 'euc-kr', - 'csksc56011987' => 'euc-kr', - '5601' => 'euc-kr', - 'windows-949' => 'euc-kr', - 'gb_2312-80' => 'gb2312', - 'iso-ir-58' => 'gb2312', - 'chinese' => 'gb2312', - 'csiso58gb231280' => 'gb2312', - 'csgb2312' => 'gb2312', - 'zh_cn.euc' => 'gb2312', - 'gb_2312' => 'gb2312', - 'x-cp1250' => 'windows-1250', - 'x-cp1251' => 'windows-1251', - 'x-cp1252' => 'windows-1252', - 'x-cp1253' => 'windows-1253', - 'x-cp1254' => 'windows-1254', - 'x-cp1255' => 'windows-1255', - 'x-cp1256' => 'windows-1256', - 'x-cp1257' => 'windows-1257', - 'x-cp1258' => 'windows-1258', - 'windows-874' => 'windows-874', - 'ibm874' => 'windows-874', - 'dos-874' => 'windows-874', - 'macintosh' => 'macintosh', - 'x-mac-roman' => 'macintosh', - 'mac' => 'macintosh', - 'csmacintosh' => 'macintosh', - 'cp866' => 'ibm866', - 'cp-866' => 'ibm866', - '866' => 'ibm866', - 'csibm866' => 'ibm866', - 'cp850' => 'ibm850', - '850' => 'ibm850', - 'csibm850' => 'ibm850', - 'cp852' => 'ibm852', - '852' => 'ibm852', - 'csibm852' => 'ibm852', - 'cp855' => 'ibm855', - '855' => 'ibm855', - 'csibm855' => 'ibm855', - 'cp857' => 'ibm857', - '857' => 'ibm857', - 'csibm857' => 'ibm857', - 'cp862' => 'ibm862', - '862' => 'ibm862', - 'csibm862' => 'ibm862', - 'cp864' => 'ibm864', - '864' => 'ibm864', - 'csibm864' => 'ibm864', - 'ibm-864' => 'ibm864', - 't.61' => 't.61-8bit', - 'iso-ir-103' => 't.61-8bit', - 'csiso103t618bit' => 't.61-8bit', - 'x-unicode-2-0-utf-7' => 'utf-7', - 'unicode-2-0-utf-7' => 'utf-7', - 'unicode-1-1-utf-7' => 'utf-7', - 'csunicode11utf7' => 'utf-7', - 'csunicode' => 'utf-16be', - 'csunicode11' => 'utf-16be', - 'iso-10646-ucs-basic' => 'utf-16be', - 'csunicodeascii' => 'utf-16be', - 'iso-10646-unicode-latin1' => 'utf-16be', - 'csunicodelatin1' => 'utf-16be', - 'iso-10646' => 'utf-16be', - 'iso-10646-j-1' => 'utf-16be', - 'latin6' => 'iso-8859-10', - 'iso-ir-157' => 'iso-8859-10', - 'l6' => 'iso-8859-10', - 'csisolatin6' => 'iso-8859-10', - 'iso_8859-15' => 'iso-8859-15', - 'csisolatin9' => 'iso-8859-15', - 'l9' => 'iso-8859-15', - 'ecma-cyrillic' => 'iso-ir-111', - 'csiso111ecmacyrillic' => 'iso-ir-111', - 'csiso2022kr' => 'iso-2022-kr', - 'csviscii' => 'viscii', - 'zh_tw-euc' => 'x-euc-tw', - 'iso88591' => 'iso-8859-1', - 'iso88592' => 'iso-8859-2', - 'iso88593' => 'iso-8859-3', - 'iso88594' => 'iso-8859-4', - 'iso88595' => 'iso-8859-5', - 'iso88596' => 'iso-8859-6', - 'iso88597' => 'iso-8859-7', - 'iso88598' => 'iso-8859-8', - 'iso88599' => 'iso-8859-9', - 'iso885910' => 'iso-8859-10', - 'iso885911' => 'iso-8859-11', - 'iso885912' => 'iso-8859-12', - 'iso885913' => 'iso-8859-13', - 'iso885914' => 'iso-8859-14', - 'iso885915' => 'iso-8859-15', - 'tis620' => 'tis-620', - 'cp1250' => 'windows-1250', - 'cp1251' => 'windows-1251', - 'cp1252' => 'windows-1252', - 'cp1253' => 'windows-1253', - 'cp1254' => 'windows-1254', - 'cp1255' => 'windows-1255', - 'cp1256' => 'windows-1256', - 'cp1257' => 'windows-1257', - 'cp1258' => 'windows-1258', - 'x-gbk' => 'gbk', - 'windows-936' => 'gbk', - 'ansi-1251' => 'windows-1251', - ]; - - /** - * {@inheritdoc} - */ - public function decodeCharset($encodedString, $charset) - { - $charset = $this->getCharsetAlias($charset); - - if ($charset == 'utf-8' || $charset == 'us-ascii') { - return $encodedString; - } - - if (function_exists('mb_convert_encoding')) { - if ($charset == 'iso-2022-jp') { - return mb_convert_encoding($encodedString, 'utf-8', 'iso-2022-jp-ms'); - } - - if (array_search($charset, $this->getSupportedEncodings())) { - return mb_convert_encoding($encodedString, 'utf-8', $charset); - } - } - - return iconv($charset, 'utf-8//translit//ignore', $encodedString); - } - - /** - * {@inheritdoc} - */ - public function getCharsetAlias($charset) - { - $charset = strtolower($charset); - - if (array_key_exists($charset, $this->charsetAlias)) { - return $this->charsetAlias[$charset]; - } - - return 'us-ascii'; - } - - private function getSupportedEncodings() - { - return - array_map( - 'strtolower', - array_unique( - array_merge( - $enc = mb_list_encodings(), - call_user_func_array( - 'array_merge', - array_map( - "mb_encoding_aliases", - $enc - ) - ) - ) - ) - ); - } -} diff --git a/plugins/php-mime-mail-parser/src/Contracts/CharsetManager.php b/plugins/php-mime-mail-parser/src/Contracts/CharsetManager.php deleted file mode 100644 index 660ec00c..00000000 --- a/plugins/php-mime-mail-parser/src/Contracts/CharsetManager.php +++ /dev/null @@ -1,24 +0,0 @@ -parser = $fn; - } - - /** - * Process a mime part, optionally delegating parsing to the $next MiddlewareStack - */ - public function parse(MimePart $part, MiddlewareStack $next) - { - return call_user_func($this->parser, $part, $next); - } -} diff --git a/plugins/php-mime-mail-parser/src/MiddlewareStack.php b/plugins/php-mime-mail-parser/src/MiddlewareStack.php deleted file mode 100644 index 3ef6da93..00000000 --- a/plugins/php-mime-mail-parser/src/MiddlewareStack.php +++ /dev/null @@ -1,89 +0,0 @@ -add($Middleware) - * - * @param Middleware $middleware - */ - public function __construct(MiddleWareContracts $middleware = null) - { - $this->middleware = $middleware; - } - - /** - * Creates a chained middleware in MiddlewareStack - * - * @param Middleware $middleware - * @return MiddlewareStack Immutable MiddlewareStack - */ - public function add(MiddleWareContracts $middleware) - { - $stack = new static($middleware); - $stack->next = $this; - return $stack; - } - - /** - * Parses the MimePart by passing it through the Middleware - * @param MimePart $part - * @return MimePart - */ - public function parse(MimePart $part) - { - if (!$this->middleware) { - return $part; - } - $part = call_user_func(array($this->middleware, 'parse'), $part, $this->next); - return $part; - } - - /** - * Creates a MiddlewareStack based on an array of middleware - * - * @param Middleware[] $middlewares - * @return MiddlewareStack - */ - public static function factory(array $middlewares = array()) - { - $stack = new static; - foreach ($middlewares as $middleware) { - $stack = $stack->add($middleware); - } - return $stack; - } - - /** - * Allow calling MiddlewareStack instance directly to invoke parse() - * - * @param MimePart $part - * @return MimePart - */ - public function __invoke(MimePart $part) - { - return $this->parse($part); - } -} diff --git a/plugins/php-mime-mail-parser/src/MimePart.php b/plugins/php-mime-mail-parser/src/MimePart.php deleted file mode 100644 index d2211b7c..00000000 --- a/plugins/php-mime-mail-parser/src/MimePart.php +++ /dev/null @@ -1,119 +0,0 @@ -getPart(); - * $part['headers']['from'] = 'modified@example.com'; - * $MimePart->setPart($part); - */ -class MimePart implements \ArrayAccess -{ - /** - * Internal mime part - * - * @var array - */ - protected $part = array(); - - /** - * Immutable Part Id - * - * @var string - */ - private $id; - - /** - * Create a mime part - * - * @param array $part - * @param string $id - */ - public function __construct($id, array $part) - { - $this->part = $part; - $this->id = $id; - } - - /** - * Retrieve the part Id - * - * @return string - */ - public function getId() - { - return $this->id; - } - - /** - * Retrieve the part data - * - * @return array - */ - public function getPart() - { - return $this->part; - } - - /** - * Set the mime part data - * - * @param array $part - * @return void - */ - public function setPart(array $part) - { - $this->part = $part; - } - - /** - * ArrayAccess - */ - #[\ReturnTypeWillChange] - public function offsetSet($offset, $value) - { - if (is_null($offset)) { - $this->part[] = $value; - return; - } - $this->part[$offset] = $value; - } - - /** - * ArrayAccess - */ - #[\ReturnTypeWillChange] - public function offsetExists($offset) - { - return isset($this->part[$offset]); - } - - /** - * ArrayAccess - */ - #[\ReturnTypeWillChange] - public function offsetUnset($offset) - { - unset($this->part[$offset]); - } - - /** - * ArrayAccess - */ - #[\ReturnTypeWillChange] - public function offsetGet($offset) - { - return isset($this->part[$offset]) ? $this->part[$offset] : null; - } -} diff --git a/plugins/php-mime-mail-parser/src/Parser.php b/plugins/php-mime-mail-parser/src/Parser.php deleted file mode 100644 index 6502b57e..00000000 --- a/plugins/php-mime-mail-parser/src/Parser.php +++ /dev/null @@ -1,923 +0,0 @@ -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); - } -}