Migrated away from PHP Mail Parser to the new WebKlex PHP IMAP Mail Parser this will open the way to support OAUTH2 for Mail servers such as Microsoft 365 and Google Workspaces

This commit is contained in:
johnnyq 2024-06-12 15:39:52 -04:00
parent d64a7ce31e
commit 779527cf6a
218 changed files with 14781 additions and 2722 deletions

View File

@ -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, "<i>Email from: $contact_email at $date:-</i> <br><br>$message");
$message = "<i>Email from: $contact_email at $date:-</i> <br><br>$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.<br>";
mysqli_query($mysqli, "INSERT INTO logs SET log_type = 'Ticket', log_action = 'Create', log_description = 'Email parser: Client contact $contact_email created ticket $config_ticket_prefix$ticket_number ($subject) ($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 = "<i style=\'color: #808080\'>##- Please type your reply above this line -##</i><br><br>Hello $contact_name,<br><br>Thank you for your email. A ticket regarding \"$subject\" has been automatically created for you.<br><br>Ticket: $config_ticket_prefix$ticket_number<br>Subject: $subject<br>Status: New<br>https://$config_base_url/portal/ticket.php?id=$id<br><br>--<br>$company_name - Support<br>$config_ticket_from_email<br>$company_phone";
$body = "<i style='color: #808080'>##- Please type your reply above this line -##</i><br><br>Hello $contact_name,<br><br>Thank you for your email. A ticket regarding \"$subject\" has been automatically created for you.<br><br>Ticket: $config_ticket_prefix$ticket_number<br>Subject: $subject<br>Status: New<br>https://$config_base_url/portal/ticket.php?id=$id<br><br>--<br>$company_name - Support<br>$config_ticket_from_email<br>$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, "<i>Email from: $from_email at $date:-</i> <br><br>$message");
$message = "<i>Email from: $from_email at $date:-</i> <br><br>$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, <br><br>You\'ve tried to reply to a ticket that is closed - we won\'t see your response. <br><br>Please raise a new ticket by sending a fresh e-mail to our support address below. <br><br>--<br>$company_name - Support<br>$config_ticket_from_email<br>$company_phone";
$email_body = "Hi there, <br><br>You've tried to reply to a ticket that is closed - we won't see your response. <br><br>Please raise a new ticket by sending a fresh e-mail to our support address below. <br><br>--<br>$company_name - Support<br>$config_ticket_from_email<br>$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 = "<b>WARNING: Contact email mismatch</b><br>$message"; // Add a warning at the start of the message - for the techs benefit (think phishing/scams)
$message = "<b>WARNING: Contact email mismatch</b><br>$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.<br>";
mysqli_query($mysqli, "INSERT INTO logs SET log_type = 'Ticket', log_action = 'Update', log_description = 'Email parser: Client contact $from_email updated ticket $config_ticket_prefix$ticket_number ($subject)', log_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/<blank>)
$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.<br>";
mysqli_query($mysqli, "INSERT INTO logs SET log_type = 'Contact', log_action = 'Create', log_description = 'Email parser: created contact $contact_name', log_client_id = $client_id");
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);
?>

2
plugins/php-imap/.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,2 @@
ko_fi: webklex
custom: ['https://www.buymeacoffee.com/webklex']

View File

@ -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.

View File

@ -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.

View File

@ -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 :)

View File

@ -0,0 +1,23 @@
FROM ubuntu:latest
LABEL maintainer="Webklex <github@webklex.com>"
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"]

View File

@ -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"

View File

@ -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

7
plugins/php-imap/.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
vendor
composer.lock
.idea
/build/
test.php
.phpunit.result.cache
phpunit.xml

911
plugins/php-imap/CHANGELOG.md Executable file
View File

@ -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))

View File

@ -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/

View File

@ -1,293 +0,0 @@
<?php
/*
* File: ClientManager.php
* Category: -
* Author: M. Goldenbaum
* Created: 19.01.17 22:21
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP;
/**
* Class ClientManager
*
* @package Webklex\IMAP
*
* @mixin Client
*/
class ClientManager {
/**
* All library config
*
* @var array $config
*/
public static array $config = [];
/**
* @var array $accounts
*/
protected array $accounts = [];
/**
* ClientManager constructor.
* @param array|string $config
*/
public function __construct(array|string $config = []) {
$this->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 <mark.roduner@gmail.com>
*/
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;
}
}

21
plugins/php-imap/LICENSE Normal file
View File

@ -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.

View File

@ -0,0 +1,21 @@
# The MIT License (MIT)
Copyright (c) 2016 Malte Goldenbaum <info@webklex.com>
> 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.

235
plugins/php-imap/README.md Executable file
View File

@ -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().'<br />';
echo 'Attachments: '.$message->getAttachments()->count().'<br />';
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
<php>
<env name="LIVE_MAILBOX" value="false"/>
</php>
```
### 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
<php>
<env name="LIVE_MAILBOX" value="true"/>
<env name="LIVE_MAILBOX_DEBUG" value="true"/>
<env name="LIVE_MAILBOX_HOST" value="mail.example.local"/>
<env name="LIVE_MAILBOX_PORT" value="993"/>
<env name="LIVE_MAILBOX_VALIDATE_CERT" value="false"/>
<env name="LIVE_MAILBOX_QUOTA_SUPPORT" value="true"/>
<env name="LIVE_MAILBOX_ENCRYPTION" value="ssl"/>
<env name="LIVE_MAILBOX_USERNAME" value="root@example.local"/>
<env name="LIVE_MAILBOX_PASSWORD" value="foobar"/>
</php>
```
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 :)
&#96;&#96;&#96;php
echo 'your php code...';
&#96;&#96;&#96;
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

View File

@ -1 +0,0 @@
5.5.0

View File

@ -0,0 +1 @@
theme: jekyll-theme-cayman

View File

@ -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
}

View File

@ -0,0 +1,57 @@
<?php
/*
* File: custom_message_mask.php
* Category: Example
* Author: M.Goldenbaum
* Created: 14.03.19 18:47
* Updated: -
*
* Description:
* -
*/
class CustomAttachmentMask extends \Webklex\PHPIMAP\Support\Masks\AttachmentMask {
/**
* New custom method which can be called through a mask
* @return string
*/
public function token(): string {
return implode('-', [$this->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();

View File

@ -0,0 +1,51 @@
<?php
/*
* File: custom_message_mask.php
* Category: Example
* Author: M.Goldenbaum
* Created: 14.03.19 18:47
* Updated: -
*
* Description:
* -
*/
class CustomMessageMask extends \Webklex\PHPIMAP\Support\Masks\MessageMask {
/**
* New custom method which can be called through a mask
* @return string
*/
public function token(): string {
return implode('-', [$this->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');

View File

@ -0,0 +1,42 @@
<?php
/*
* File: folder_structure.blade.php
* Category: View
* Author: M.Goldenbaum
* Created: 15.09.18 19:53
* Updated: -
*
* Description:
* -
*/
/**
* @var \Webklex\PHPIMAP\Support\FolderCollection $paginator
* @var \Webklex\PHPIMAP\Folder $folder
*/
?>
<table>
<thead>
<tr>
<th>Folder</th>
<th>Unread messages</th>
</tr>
</thead>
<tbody>
<?php if($paginator->count() > 0): ?>
<?php foreach($paginator as $folder): ?>
<tr>
<td><?php echo $folder->name; ?></td>
<td><?php echo $folder->search()->unseen()->setFetchBody(false)->count(); ?></td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="4">No folders found</td>
</tr>
<?php endif; ?>
</tbody>
</table>
<?php echo $paginator->links(); ?>

View File

@ -0,0 +1,46 @@
<?php
/*
* File: message_table.blade.php
* Category: View
* Author: M.Goldenbaum
* Created: 15.09.18 19:53
* Updated: -
*
* Description:
* -
*/
/**
* @var \Webklex\PHPIMAP\Support\FolderCollection $paginator
* @var \Webklex\PHPIMAP\Message $message
*/
?>
<table>
<thead>
<tr>
<th>UID</th>
<th>Subject</th>
<th>From</th>
<th>Attachments</th>
</tr>
</thead>
<tbody>
<?php if($paginator->count() > 0): ?>
<?php foreach($paginator as $message): ?>
<tr>
<td><?php echo $message->getUid(); ?></td>
<td><?php echo $message->getSubject(); ?></td>
<td><?php echo $message->getFrom()[0]->mail; ?></td>
<td><?php echo $message->getAttachments()->count() > 0 ? 'yes' : 'no'; ?></td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="4">No messages found</td>
</tr>
<?php endif; ?>
</tbody>
</table>
<?php echo $paginator->links(); ?>

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" bootstrap="vendor/autoload.php" backupGlobals="false" backupStaticAttributes="false" colors="true" verbose="true" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd">
<coverage>
<include>
<directory suffix=".php">src/</directory>
</include>
<report>
<clover outputFile="build/logs/clover.xml"/>
<html outputDirectory="build/coverage"/>
<text outputFile="build/coverage.txt"/>
</report>
</coverage>
<testsuites>
<testsuite name="PHP-IMAP Test Suite">
<directory>tests</directory>
<directory>tests/fixtures</directory>
<directory>tests/issues</directory>
<directory>tests/live</directory>
</testsuite>
</testsuites>
<logging>
<junit outputFile="build/report.junit.xml"/>
</logging>
<php>
<env name="LIVE_MAILBOX" value="false"/>
<env name="LIVE_MAILBOX_DEBUG" value="false"/>
<env name="LIVE_MAILBOX_HOST" value="mail.example.local"/>
<env name="LIVE_MAILBOX_PORT" value="993"/>
<env name="LIVE_MAILBOX_VALIDATE_CERT" value="false"/>
<env name="LIVE_MAILBOX_QUOTA_SUPPORT" value="false"/>
<env name="LIVE_MAILBOX_ENCRYPTION" value=""/>
<env name="LIVE_MAILBOX_USERNAME" value="root@example.local"/>
<env name="LIVE_MAILBOX_PASSWORD" value="foobar"/>
</php>
</phpunit>

View File

@ -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

View File

@ -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);

View File

@ -0,0 +1,131 @@
<?php
/*
* File: ClientManager.php
* Category: -
* Author: M. Goldenbaum
* Created: 19.01.17 22:21
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP;
/**
* Class ClientManager
*
* @package Webklex\IMAP
*/
class ClientManager {
/**
* All library config
*
* @var Config $config
*/
public Config $config;
/**
* @var array $accounts
*/
protected array $accounts = [];
/**
* ClientManager constructor.
* @param array|string|Config $config
*/
public function __construct(array|string|Config $config = []) {
$this->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;
}
}

View File

@ -0,0 +1,266 @@
<?php
/*
* File: Config.php
* Category: -
* Author: M.Goldenbaum
* Created: 10.04.24 15:42
* Updated: -
*
* Description:
* -
*/
namespace Webklex\PHPIMAP;
/**
* Class Config
*
* @package Webklex\PHPIMAP
*/
class Config {
/**
* Configuration array
* @var array $config
*/
protected array $config = [];
/**
* Config constructor.
* @param array $config
*/
public function __construct(array $config = []) {
$this->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 <mark.roduner@gmail.com>
*/
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;
}
}

View File

@ -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);

View File

@ -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

View File

@ -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;
}
}

View File

@ -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

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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;
}
/**

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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<int, int> $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);
}

View File

@ -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)];
}
}

View File

@ -18,6 +18,7 @@ use Webklex\PHPIMAP\Attachment;
* Class AttachmentMask
*
* @package Webklex\PHPIMAP\Support\Masks
* @mixin Attachment
*/
class AttachmentMask extends Mask {

View File

@ -19,6 +19,7 @@ use Webklex\PHPIMAP\Message;
* Class MessageMask
*
* @package Webklex\PHPIMAP\Support\Masks
* @mixin Message
*/
class MessageMask extends Mask {

View File

@ -0,0 +1,72 @@
<?php
/*
* File: AddressTest.php
* Category: -
* Author: M.Goldenbaum
* Created: 28.12.22 18:11
* Updated: -
*
* Description:
* -
*/
namespace Tests;
use PHPUnit\Framework\TestCase;
use Webklex\PHPIMAP\Address;
class AddressTest extends TestCase {
/**
* Test data
*
* @var array|string[] $data
*/
protected array $data = [
"personal" => "Username",
"mailbox" => "info",
"host" => "domain.tld",
"mail" => "info@domain.tld",
"full" => "Username <info@domain.tld>",
];
/**
* 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 <info@domain.tld>", $address->full);
}
/**
* Test Address to string conversion
*
* @return void
*/
public function testAddressToStringConversion(): void {
$address = new Address((object)$this->data);
self::assertSame("Username <info@domain.tld>", (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);
}
}
}

View File

@ -0,0 +1,75 @@
<?php
/*
* File: AttributeTest.php
* Category: -
* Author: M.Goldenbaum
* Created: 28.12.22 18:11
* Updated: -
*
* Description:
* -
*/
namespace Tests;
use Carbon\Carbon;
use PHPUnit\Framework\TestCase;
use Webklex\PHPIMAP\Attribute;
class AttributeTest extends TestCase {
/**
* String Attribute test
*
* @return void
*/
public function testStringAttribute(): void {
$attribute = new Attribute("foo", "bar");
self::assertSame("bar", $attribute->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()));
}
}

View File

@ -0,0 +1,94 @@
<?php
/*
* File: ClientManagerTest.php
* Category: -
* Author: M.Goldenbaum
* Created: 28.12.22 18:11
* Updated: -
*
* Description:
* -
*/
namespace Tests;
use PHPUnit\Framework\TestCase;
use Webklex\PHPIMAP\Client;
use Webklex\PHPIMAP\ClientManager;
use Webklex\PHPIMAP\Config;
use Webklex\PHPIMAP\Exceptions\MaskNotFoundException;
use Webklex\PHPIMAP\IMAP;
class ClientManagerTest extends TestCase {
/** @var ClientManager $cm */
protected ClientManager $cm;
/**
* Setup the test environment.
*
* @return void
*/
public function setUp(): void {
$this->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")));
}
}

View File

@ -0,0 +1,314 @@
<?php
/*
* File: ClientTest.php
* Category: -
* Author: M.Goldenbaum
* Created: 28.12.22 18:11
* Updated: -
*
* Description:
* -
*/
namespace Tests;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Webklex\PHPIMAP\Client;
use Webklex\PHPIMAP\Config;
use Webklex\PHPIMAP\Connection\Protocols\ImapProtocol;
use Webklex\PHPIMAP\Connection\Protocols\Response;
use Webklex\PHPIMAP\Exceptions\AuthFailedException;
use Webklex\PHPIMAP\Exceptions\ConnectionFailedException;
use Webklex\PHPIMAP\Exceptions\ImapBadRequestException;
use Webklex\PHPIMAP\Exceptions\ImapServerErrorException;
use Webklex\PHPIMAP\Exceptions\MaskNotFoundException;
use Webklex\PHPIMAP\Exceptions\RuntimeException;
use Webklex\PHPIMAP\Folder;
use Webklex\PHPIMAP\Support\Masks\AttachmentMask;
use Webklex\PHPIMAP\Support\Masks\MessageMask;
class ClientTest extends TestCase {
/** @var Client $client */
protected Client $client;
/** @var MockObject ImapProtocol mockup */
protected MockObject $protocol;
/**
* Setup the test environment.
*
* @return void
* @throws MaskNotFoundException
*/
public function setUp(): void {
$config = Config::make([
"accounts" => [
"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;
}
}

View File

@ -0,0 +1,155 @@
<?php
/*
* File: HeaderTest.php
* Category: -
* Author: M.Goldenbaum
* Created: 28.12.22 18:11
* Updated: -
*
* Description:
* -
*/
namespace Tests;
use Carbon\Carbon;
use PHPUnit\Framework\TestCase;
use Webklex\PHPIMAP\Address;
use Webklex\PHPIMAP\Attribute;
use Webklex\PHPIMAP\Config;
use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException;
use Webklex\PHPIMAP\Header;
use Webklex\PHPIMAP\IMAP;
class HeaderTest extends TestCase {
/** @var Config $config */
protected Config $config;
/**
* Setup the test environment.
*
* @return void
*/
public function setUp(): void {
$this->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("<noreply@github.com>", $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 <notifications@github.com>", $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 <php-imap@noreply.github.com>", $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'));
}
}

View File

@ -0,0 +1,52 @@
<?php
/*
* File: ImapProtocolTest.php
* Category: -
* Author: M.Goldenbaum
* Created: 28.12.22 18:11
* Updated: -
*
* Description:
* -
*/
namespace Tests;
use PHPUnit\Framework\TestCase;
use Webklex\PHPIMAP\Config;
use Webklex\PHPIMAP\Connection\Protocols\ImapProtocol;
use Webklex\PHPIMAP\Exceptions\ConnectionFailedException;
class ImapProtocolTest extends TestCase {
/** @var Config $config */
protected Config $config;
/**
* Setup the test environment.
*
* @return void
*/
public function setUp(): void {
$this->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());
}
}

View File

@ -0,0 +1,302 @@
<?php
/*
* File: MessageTest.php
* Category: -
* Author: M.Goldenbaum
* Created: 28.12.22 18:11
* Updated: -
*
* Description:
* -
*/
namespace Tests;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use ReflectionException;
use Webklex\PHPIMAP\Attachment;
use Webklex\PHPIMAP\Attribute;
use Webklex\PHPIMAP\Client;
use Webklex\PHPIMAP\Config;
use Webklex\PHPIMAP\Connection\Protocols\Response;
use Webklex\PHPIMAP\Exceptions\EventNotFoundException;
use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException;
use Webklex\PHPIMAP\Exceptions\MessageContentFetchingException;
use Webklex\PHPIMAP\Exceptions\MessageFlagException;
use Webklex\PHPIMAP\Exceptions\MessageNotFoundException;
use Webklex\PHPIMAP\Exceptions\MessageSizeFetchingException;
use Webklex\PHPIMAP\Exceptions\ResponseException;
use Webklex\PHPIMAP\IMAP;
use Webklex\PHPIMAP\Message;
use Webklex\PHPIMAP\Connection\Protocols\ImapProtocol;
use Webklex\PHPIMAP\Exceptions\AuthFailedException;
use Webklex\PHPIMAP\Exceptions\ConnectionFailedException;
use Webklex\PHPIMAP\Exceptions\ImapBadRequestException;
use Webklex\PHPIMAP\Exceptions\ImapServerErrorException;
use Webklex\PHPIMAP\Exceptions\MaskNotFoundException;
use Webklex\PHPIMAP\Exceptions\RuntimeException;
class MessageTest extends TestCase {
/** @var Message $message */
protected Message $message;
/** @var Client $client */
protected Client $client;
/** @var MockObject ImapProtocol mockup */
protected MockObject $protocol;
/**
* Setup the test environment.
*
* @return void
*/
public function setUp(): void {
$config = Config::make([
"accounts" => [
"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("<noreply@github.com>", $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("<noreply@github.com>", $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("<someone@domain.tld>", $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;
}
}

View File

@ -0,0 +1,107 @@
<?php
/*
* File: StructureTest.php
* Category: -
* Author: M.Goldenbaum
* Created: 28.12.22 18:11
* Updated: -
*
* Description:
* -
*/
namespace Tests;
use Carbon\Carbon;
use PHPUnit\Framework\TestCase;
use Webklex\PHPIMAP\Config;
use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException;
use Webklex\PHPIMAP\Exceptions\MessageContentFetchingException;
use Webklex\PHPIMAP\Header;
use Webklex\PHPIMAP\Part;
use Webklex\PHPIMAP\Structure;
use Webklex\PHPIMAP\IMAP;
class PartTest extends TestCase {
/** @var Config $config */
protected Config $config;
/**
* Setup the test environment.
*
* @return void
*/
public function setUp(): void {
$this->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<p></p>\r\n<p dir=\"auto\">Any updates?</p>";
$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("<p></p>\r\n<p dir=\"auto\">Any updates?</p>", $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);
}
}

View File

@ -0,0 +1,68 @@
<?php
/*
* File: StructureTest.php
* Category: -
* Author: M.Goldenbaum
* Created: 28.12.22 18:11
* Updated: -
*
* Description:
* -
*/
namespace Tests;
use PHPUnit\Framework\TestCase;
use Webklex\PHPIMAP\Config;
use Webklex\PHPIMAP\Exceptions\InvalidMessageDateException;
use Webklex\PHPIMAP\Exceptions\MessageContentFetchingException;
use Webklex\PHPIMAP\Header;
use Webklex\PHPIMAP\Structure;
class StructureTest extends TestCase {
/** @var Config $config */
protected Config $config;
/**
* Setup the test environment.
*
* @return void
*/
public function setUp(): void {
$this->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);
}
}

View File

@ -0,0 +1,52 @@
<?php
/*
* File: AttachmentEncodedFilenameTest.php
* Category: -
* Author: M.Goldenbaum
* Created: 09.03.23 02:24
* Updated: -
*
* Description:
* -
*/
namespace Tests\fixtures;
use Webklex\PHPIMAP\Attachment;
/**
* Class AttachmentEncodedFilenameTest
*
* @package Tests\fixtures
*/
class AttachmentEncodedFilenameTest extends FixtureTestCase {
/**
* Test the fixture attachment_encoded_filename.eml
*
* @return void
*/
public function testFixture() : void {
$message = $this->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);
}
}

View File

@ -0,0 +1,79 @@
<?php
/*
* File: AttachmentLongFilenameTest.php
* Category: -
* Author: M.Goldenbaum
* Created: 09.03.23 02:24
* Updated: -
*
* Description:
* -
*/
namespace Tests\fixtures;
use Webklex\PHPIMAP\Attachment;
/**
* Class AttachmentLongFilenameTest
*
* @package Tests\fixtures
*/
class AttachmentLongFilenameTest extends FixtureTestCase {
/**
* Test the fixture attachment_long_filename.eml
*
* @return void
*/
public function testFixture() : void {
$message = $this->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);
}
}

View File

@ -0,0 +1,55 @@
<?php
/*
* File: AttachmentNoDispositionTest.php
* Category: -
* Author: M.Goldenbaum
* Created: 09.03.23 02:24
* Updated: -
*
* Description:
* -
*/
namespace Tests\fixtures;
use Webklex\PHPIMAP\Attachment;
/**
* Class AttachmentNoDispositionTest
*
* @package Tests\fixtures
*/
class AttachmentNoDispositionTest extends FixtureTestCase {
/**
* Test the fixture attachment_no_disposition.eml
*
* @return void
*/
public function testFixture() : void {
$message = $this->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);
}
}

View File

@ -0,0 +1,43 @@
<?php
/*
* File: BccTest.php
* Category: -
* Author: M.Goldenbaum
* Created: 09.03.23 02:24
* Updated: -
*
* Description:
* -
*/
namespace Tests\fixtures;
/**
* Class BccTest
*
* @package Tests\fixtures
*/
class BccTest extends FixtureTestCase {
/**
* Test the fixture bcc.eml
*
* @return void
*/
public function testFixture() : void {
$message = $this->getFixture("bcc.eml");
self::assertEquals("test", $message->subject);
self::assertEquals("<return-path@here.com>", $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 <bcc@here.com>", $message->bcc);
self::assertEquals("sender@here.com", $message->sender);
self::assertEquals("reply-to@here.com", $message->reply_to);
}
}

View File

@ -0,0 +1,55 @@
<?php
/*
* File: BooleanDecodedContentTest.php
* Category: -
* Author: M.Goldenbaum
* Created: 09.03.23 02:24
* Updated: -
*
* Description:
* -
*/
namespace Tests\fixtures;
use Webklex\PHPIMAP\Attachment;
/**
* Class BooleanDecodedContentTest
*
* @package Tests\fixtures
*/
class BooleanDecodedContentTest extends FixtureTestCase {
/**
* Test the fixture boolean_decoded_content.eml
*
* @return void
*/
public function testFixture() : void {
$message = $this->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);
}
}

Some files were not shown because too many files have changed in this diff Show More