mirror of
https://github.com/itflow-org/itflow
synced 2026-03-11 08:14:52 +00:00
Revert "Add new optional beta email parser thats based on ImapEngine instead of Webklex"
This reverts commit 9cb1ff7330.
This commit is contained in:
@@ -1,994 +0,0 @@
|
||||
<?php
|
||||
/*
|
||||
* CRON - Email Parser (ImapEngine v1.22.4)
|
||||
* Process emails and create/update tickets using DirectoryTree/ImapEngine (no PHP IMAP extension).
|
||||
*
|
||||
* This is a full rewrite of your Webklex version to match behavior:
|
||||
* - Supports Standard IMAP, Google OAuth IMAP, Microsoft OAuth IMAP
|
||||
* - Pulls UNSEEN messages from INBOX
|
||||
* - Saves original email to .eml in uploads/tmp and attaches it to new tickets
|
||||
* - Parses From/Subject/CC/Date
|
||||
* - Prefers HTML body, falls back to text
|
||||
* - Inlines CID images into HTML (data: URIs) using body structure
|
||||
* - Stores non-inline attachments (extension allowlist)
|
||||
* - Handles replies by [PREFIX123] in subject, fuzzy duplicate subject match, known contact/domain, unknown sender option
|
||||
* - Moves processed mail to "ITFlow" folder, flags unprocessed
|
||||
*/
|
||||
|
||||
// Start the timer
|
||||
$script_start_time = microtime(true);
|
||||
|
||||
// Set working directory to the directory this cron script lives at.
|
||||
chdir(dirname(__FILE__));
|
||||
|
||||
// Ensure we're running from command line
|
||||
if (php_sapi_name() !== 'cli') {
|
||||
die("This script must be run from the command line.\n");
|
||||
}
|
||||
|
||||
// Autoload (ImapEngine & any composer deps)
|
||||
require_once "../plugins/vendor/autoload.php";
|
||||
|
||||
// Get ITFlow config & helper functions
|
||||
require_once "../config.php";
|
||||
|
||||
// Set Timezone
|
||||
require_once "../includes/inc_set_timezone.php";
|
||||
require_once "../functions.php";
|
||||
|
||||
// Get settings for the "default" company
|
||||
require_once "../includes/load_global_settings.php";
|
||||
|
||||
$config_ticket_prefix = sanitizeInput($config_ticket_prefix);
|
||||
$config_ticket_from_name = sanitizeInput($config_ticket_from_name);
|
||||
$config_ticket_email_parse_unknown_senders = intval($row['config_ticket_email_parse_unknown_senders']);
|
||||
|
||||
// Get company name & phone & timezone
|
||||
$sql = mysqli_query($mysqli, "SELECT * FROM companies, settings WHERE companies.company_id = settings.company_id AND companies.company_id = 1");
|
||||
$row = mysqli_fetch_assoc($sql);
|
||||
$company_name = sanitizeInput($row['company_name']);
|
||||
$company_phone = sanitizeInput(formatPhoneNumber($row['company_phone'], $row['company_phone_country_code']));
|
||||
|
||||
// Check setting enabled
|
||||
if ($config_ticket_email_parse == 0) {
|
||||
logApp("Cron-Email-Parser", "error", "Cron Email Parser unable to run - not enabled in admin settings.");
|
||||
exit("Email Parser: Feature is not enabled - check Settings > Ticketing > Email-to-ticket parsing. See https://docs.itflow.org/ticket_email_parse -- Quitting..");
|
||||
}
|
||||
|
||||
// System temp directory & lock
|
||||
$temp_dir = sys_get_temp_dir();
|
||||
$lock_file_path = "{$temp_dir}/itflow_email_parser_{$installation_id}.lock";
|
||||
|
||||
if (file_exists($lock_file_path)) {
|
||||
$file_age = time() - filemtime($lock_file_path);
|
||||
if ($file_age > 300) {
|
||||
unlink($lock_file_path);
|
||||
logApp("Cron-Email-Parser", "warning", "Cron Email Parser detected a lock file was present but was over 5 minutes old so it removed it.");
|
||||
} else {
|
||||
logApp("Cron-Email-Parser", "warning", "Lock file present. Cron Email Parser attempted to execute but was already executing, so instead it terminated.");
|
||||
exit("Script is already running. Exiting.");
|
||||
}
|
||||
}
|
||||
file_put_contents($lock_file_path, "Locked");
|
||||
|
||||
// Ensure lock gets removed even on fatal error
|
||||
register_shutdown_function(function() use ($lock_file_path) {
|
||||
if (file_exists($lock_file_path)) {
|
||||
@unlink($lock_file_path);
|
||||
}
|
||||
});
|
||||
|
||||
// Allowed attachment extensions
|
||||
$allowed_extensions = array('jpg', 'jpeg', 'gif', 'png', 'webp', 'svg', 'pdf', 'txt', 'md', 'doc', 'docx', 'csv', 'xls', 'xlsx', 'xlsm', 'zip', 'tar', 'gz');
|
||||
|
||||
/** ------------------------------------------------------------------
|
||||
* Ticket / Reply helpers (UNCHANGED from your Webklex version)
|
||||
* ------------------------------------------------------------------ */
|
||||
function addTicket($contact_id, $contact_name, $contact_email, $client_id, $date, $subject, $message, $attachments, $original_message_file, $ccs) {
|
||||
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_ticket_default_billable, $allowed_extensions;
|
||||
$bad_pattern = "/do[\W_]*not[\W_]*reply|no[\W_]*reply/i"; // Email addresses to ignore
|
||||
|
||||
// Atomically increment and get the new ticket number
|
||||
mysqli_query($mysqli, "
|
||||
UPDATE settings
|
||||
SET
|
||||
config_ticket_next_number = LAST_INSERT_ID(config_ticket_next_number),
|
||||
config_ticket_next_number = config_ticket_next_number + 1
|
||||
WHERE company_id = 1
|
||||
");
|
||||
|
||||
$ticket_number = mysqli_insert_id($mysqli);
|
||||
|
||||
// Clean up the message
|
||||
$message = trim($message);
|
||||
// Remove DOCTYPE and meta tags
|
||||
$message = preg_replace('/<!DOCTYPE[^>]*>/i', '', $message);
|
||||
$message = preg_replace('/<meta[^>]*>/i', '', $message);
|
||||
// Remove <html>, <head>, <body> and their closing tags
|
||||
$message = preg_replace('/<\/?(html|head|body)[^>]*>/i', '', $message);
|
||||
// Collapse excess whitespace
|
||||
$message = preg_replace('/\s+/', ' ', $message);
|
||||
// Convert newlines to <br>
|
||||
$message = nl2br($message);
|
||||
// Wrap final formatted message
|
||||
$message = "<i>Email from: <b>$contact_name</b> <$contact_email> at $date:-</i> <br><br><div style='line-height:1.5;'>$message</div>";
|
||||
|
||||
$ticket_prefix_esc = mysqli_real_escape_string($mysqli, $config_ticket_prefix);
|
||||
$message_esc = mysqli_real_escape_string($mysqli, $message);
|
||||
$contact_email_esc = mysqli_real_escape_string($mysqli, $contact_email);
|
||||
$client_id = intval($client_id);
|
||||
|
||||
$url_key = randomString(32);
|
||||
|
||||
mysqli_query($mysqli, "INSERT INTO tickets SET ticket_prefix = '$ticket_prefix_esc', ticket_number = $ticket_number, ticket_source = 'Email', ticket_subject = '$subject', ticket_details = '$message_esc', ticket_priority = 'Low', ticket_status = 1, ticket_billable = $config_ticket_default_billable, ticket_created_by = 0, ticket_contact_id = $contact_id, ticket_url_key = '$url_key', ticket_client_id = $client_id");
|
||||
$id = mysqli_insert_id($mysqli);
|
||||
|
||||
// Logging
|
||||
logAction("Ticket", "Create", "Email parser: Client contact $contact_email_esc created ticket $ticket_prefix_esc$ticket_number ($subject) ($id)", $client_id, $id);
|
||||
|
||||
mkdirMissing('../uploads/tickets/');
|
||||
$att_dir = "../uploads/tickets/" . $id . "/";
|
||||
mkdirMissing($att_dir);
|
||||
|
||||
// Move original .eml into the ticket folder
|
||||
rename("../uploads/tmp/{$original_message_file}", "{$att_dir}/{$original_message_file}");
|
||||
$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");
|
||||
|
||||
// Save non-inline attachments
|
||||
foreach ($attachments as $attachment) {
|
||||
$att_name = $attachment['name'];
|
||||
$att_extension = strtolower(pathinfo($att_name, PATHINFO_EXTENSION));
|
||||
|
||||
if (in_array($att_extension, $allowed_extensions)) {
|
||||
$att_saved_filename = md5(uniqid(rand(), true)) . '.' . $att_extension;
|
||||
$att_saved_path = $att_dir . $att_saved_filename;
|
||||
file_put_contents($att_saved_path, $attachment['content']);
|
||||
|
||||
$ticket_attachment_name = sanitizeInput($att_name);
|
||||
$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_esc = mysqli_real_escape_string($mysqli, $att_name);
|
||||
logAction("Ticket", "Edit", "Email parser: Blocked attachment $ticket_attachment_name_esc from Client contact $contact_email_esc for ticket $ticket_prefix_esc$ticket_number", $client_id, $id);
|
||||
}
|
||||
}
|
||||
|
||||
// Add unknown guests as ticket watcher
|
||||
if ($client_id == 0 && !preg_match($bad_pattern, $contact_email_esc)) {
|
||||
mysqli_query($mysqli, "INSERT INTO ticket_watchers SET watcher_email = '$contact_email_esc', watcher_ticket_id = $id");
|
||||
}
|
||||
|
||||
// Add CCs as ticket watchers
|
||||
foreach ($ccs as $cc) {
|
||||
if (filter_var($cc, FILTER_VALIDATE_EMAIL) && !preg_match($bad_pattern, $cc)) {
|
||||
$cc_esc = mysqli_real_escape_string($mysqli, $cc);
|
||||
mysqli_query($mysqli, "INSERT INTO ticket_watchers SET watcher_email = '$cc_esc', watcher_ticket_id = $id");
|
||||
}
|
||||
}
|
||||
|
||||
// External email
|
||||
$data = [];
|
||||
if ($config_ticket_client_general_notifications == 1 && !preg_match($bad_pattern, $contact_email)) {
|
||||
$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>Portal: <a href='https://$config_base_url/guest/guest_view_ticket.php?ticket_id=$id&url_key=$url_key'>View ticket</a><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' => mysqli_real_escape_string($mysqli, $body)
|
||||
];
|
||||
}
|
||||
|
||||
// Internal email
|
||||
if ($config_ticket_new_ticket_notification_email) {
|
||||
if ($client_id == 0) {
|
||||
$client_name = "Guest";
|
||||
$client_uri = '';
|
||||
} else {
|
||||
$client_sql = mysqli_query($mysqli, "SELECT client_name FROM clients WHERE client_id = $client_id");
|
||||
$client_row = mysqli_fetch_assoc($client_sql);
|
||||
$client_name = sanitizeInput($client_row['client_name']);
|
||||
$client_uri = "&client_id=$client_id";
|
||||
}
|
||||
$email_subject = "$config_app_name - New Ticket - $client_name: $subject";
|
||||
$email_body = "Hello, <br><br>This is a notification that a new ticket has been raised in ITFlow. <br>Client: $client_name<br>Priority: Low (email parsed)<br>Link: https://$config_base_url/agent/ticket.php?ticket_id=$id$client_uri <br><br>--------------------------------<br><br><b>$subject</b><br>$message";
|
||||
|
||||
$data[] = [
|
||||
'from' => $config_ticket_from_email,
|
||||
'from_name' => $config_ticket_from_name,
|
||||
'recipient' => $config_ticket_new_ticket_notification_email,
|
||||
'recipient_name' => $config_ticket_from_name,
|
||||
'subject' => $email_subject,
|
||||
'body' => mysqli_real_escape_string($mysqli, $email_body)
|
||||
];
|
||||
}
|
||||
|
||||
addToMailQueue($data);
|
||||
customAction('ticket_create', $id);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function addReply($from_email, $date, $subject, $ticket_number, $message, $attachments) {
|
||||
global $mysqli, $config_app_name, $company_name, $company_phone, $config_ticket_prefix, $config_base_url, $config_ticket_from_name, $config_ticket_from_email, $allowed_extensions;
|
||||
|
||||
$ticket_reply_type = 'Client';
|
||||
|
||||
// 1) Remove the reply separator and everything below it (HTML-aware)
|
||||
$message = preg_replace(
|
||||
'/<i[^>]*>##-\s*Please\s+type\s+your\s+reply\s+above\s+this\s+line\s*-##<\/i>.*$/is',
|
||||
'',
|
||||
$message
|
||||
);
|
||||
|
||||
// 2) Clean up the remaining message
|
||||
$message = preg_replace('/<!DOCTYPE[^>]*>/i', '', $message);
|
||||
$message = preg_replace('/<meta[^>]*>/i', '', $message);
|
||||
$message = preg_replace('/<\/?(html|head|body)[^>]*>/i', '', $message);
|
||||
$message = trim($message);
|
||||
$message = preg_replace('/\r\n|\r|\n/', ' ', $message);
|
||||
$message = nl2br($message);
|
||||
|
||||
// 3) Final wrapper
|
||||
$message = "<i>Email from: $from_email at $date:-</i><br><br><div style='line-height:1.5;'>$message</div>";
|
||||
|
||||
$ticket_number_esc = intval($ticket_number);
|
||||
$message_esc = mysqli_real_escape_string($mysqli, $message);
|
||||
$from_email_esc = mysqli_real_escape_string($mysqli, $from_email);
|
||||
|
||||
$row = mysqli_fetch_assoc(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_esc LIMIT 1"));
|
||||
|
||||
if ($row) {
|
||||
$ticket_id = intval($row['ticket_id']);
|
||||
$ticket_subject = sanitizeInput($row['ticket_subject']);
|
||||
$ticket_status = sanitizeInput($row['ticket_status']);
|
||||
$ticket_reply_contact = intval($row['ticket_contact_id']);
|
||||
$ticket_contact_email = sanitizeInput($row['contact_email']);
|
||||
$client_id = intval($row['ticket_client_id']);
|
||||
$client_uri = $client_id ? "&client_id=$client_id" : '';
|
||||
$client_name = sanitizeInput($row['client_name']);
|
||||
|
||||
if ($ticket_status == 5) {
|
||||
$config_ticket_prefix_esc = mysqli_real_escape_string($mysqli, $config_ticket_prefix);
|
||||
$ticket_number_esc2 = mysqli_real_escape_string($mysqli, $ticket_number);
|
||||
|
||||
appNotify("Ticket", "Email parser: $from_email attempted to re-open ticket $config_ticket_prefix_esc$ticket_number_esc2 (ID $ticket_id) - check inbox manually to see email", "/agent/ticket.php?ticket_id=$ticket_id$client_uri", $client_id);
|
||||
|
||||
$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 new e-mail to our support address below. <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' => $from_email,
|
||||
'recipient_name' => $from_email,
|
||||
'subject' => $email_subject,
|
||||
'body' => mysqli_real_escape_string($mysqli, $email_body)
|
||||
]
|
||||
];
|
||||
|
||||
addToMailQueue($data);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (empty($ticket_contact_email) || $ticket_contact_email !== $from_email) {
|
||||
$from_email_esc2 = mysqli_real_escape_string($mysqli, $from_email);
|
||||
$row2 = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT contact_id FROM contacts WHERE contact_email = '$from_email_esc2' AND contact_client_id = $client_id LIMIT 1"));
|
||||
if ($row2) {
|
||||
$ticket_reply_contact = intval($row2['contact_id']);
|
||||
} else {
|
||||
$ticket_reply_type = 'Internal';
|
||||
$ticket_reply_contact = '0';
|
||||
$message = "<b>WARNING: Contact email mismatch</b><br>$message";
|
||||
$message_esc = mysqli_real_escape_string($mysqli, $message);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
$ticket_dir = "../uploads/tickets/" . $ticket_id . "/";
|
||||
mkdirMissing($ticket_dir);
|
||||
|
||||
foreach ($attachments as $attachment) {
|
||||
$att_name = $attachment['name'];
|
||||
$att_extension = strtolower(pathinfo($att_name, PATHINFO_EXTENSION));
|
||||
|
||||
if (in_array($att_extension, $allowed_extensions)) {
|
||||
$att_saved_filename = md5(uniqid(rand(), true)) . '.' . $att_extension;
|
||||
$att_saved_path = $ticket_dir . $att_saved_filename;
|
||||
file_put_contents($att_saved_path, $attachment['content']);
|
||||
|
||||
$ticket_attachment_name = sanitizeInput($att_name);
|
||||
$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_esc = mysqli_real_escape_string($mysqli, $att_name);
|
||||
logAction("Ticket", "Edit", "Email parser: Blocked attachment $ticket_attachment_name_esc from Client contact $from_email_esc for ticket $config_ticket_prefix$ticket_number_esc", $client_id, $ticket_id);
|
||||
}
|
||||
}
|
||||
|
||||
$ticket_assigned_to_sql = mysqli_query($mysqli, "SELECT ticket_assigned_to FROM tickets WHERE ticket_id = $ticket_id LIMIT 1");
|
||||
if ($ticket_assigned_to_sql) {
|
||||
$row3 = mysqli_fetch_assoc($ticket_assigned_to_sql);
|
||||
$ticket_assigned_to = intval($row3['ticket_assigned_to']);
|
||||
|
||||
if ($ticket_assigned_to) {
|
||||
$tech_sql = mysqli_query($mysqli, "SELECT user_email, user_name FROM users WHERE user_id = $ticket_assigned_to LIMIT 1");
|
||||
$tech_row = mysqli_fetch_assoc($tech_sql);
|
||||
$tech_email = sanitizeInput($tech_row['user_email']);
|
||||
$tech_name = sanitizeInput($tech_row['user_name']);
|
||||
|
||||
$email_subject = "$config_app_name - Ticket updated - [$config_ticket_prefix$ticket_number] $ticket_subject";
|
||||
$email_body = "Hello $tech_name,<br><br>A new reply has been added to the below ticket.<br><br>Client: $client_name<br>Ticket: $config_ticket_prefix$ticket_number<br>Subject: $ticket_subject<br>Link: https://$config_base_url/agent/ticket.php?ticket_id=$ticket_id$client_uri<br><br>--------------------------------<br>$message_esc";
|
||||
|
||||
$data = [
|
||||
[
|
||||
'from' => $config_ticket_from_email,
|
||||
'from_name' => $config_ticket_from_name,
|
||||
'recipient' => $tech_email,
|
||||
'recipient_name' => $tech_name,
|
||||
'subject' => mysqli_real_escape_string($mysqli, $email_subject),
|
||||
'body' => mysqli_real_escape_string($mysqli, $email_body)
|
||||
]
|
||||
];
|
||||
addToMailQueue($data);
|
||||
}
|
||||
}
|
||||
|
||||
mysqli_query($mysqli, "UPDATE tickets SET ticket_status = 2, ticket_resolved_at = NULL WHERE ticket_id = $ticket_id AND ticket_client_id = $client_id LIMIT 1");
|
||||
|
||||
logAction("Ticket", "Edit", "Email parser: Client contact $from_email_esc updated ticket $config_ticket_prefix$ticket_number_esc ($subject)", $client_id, $ticket_id);
|
||||
customAction('ticket_reply_client', $ticket_id);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** ------------------------------------------------------------------
|
||||
* OAuth helpers + provider guard (UNCHANGED)
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
// returns true if expires_at ('Y-m-d H:i:s') is in the past (or missing)
|
||||
function tokenExpired(?string $expires_at): bool {
|
||||
if (empty($expires_at)) return true;
|
||||
$ts = strtotime($expires_at);
|
||||
if ($ts === false) return true;
|
||||
// refresh a little early (60s) to avoid race
|
||||
return ($ts - 60) <= time();
|
||||
}
|
||||
|
||||
// very small form-encoded POST helper using curl
|
||||
function httpFormPost(string $url, array $fields): array {
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($fields, '', '&'));
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 20);
|
||||
$raw = curl_exec($ch);
|
||||
$err = curl_error($ch);
|
||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
return ['ok' => ($raw !== false && $code >= 200 && $code < 300), 'body' => $raw, 'code' => $code, 'err' => $err];
|
||||
}
|
||||
|
||||
function getGoogleAccessToken(string $username): ?string {
|
||||
global $mysqli,
|
||||
$config_mail_oauth_client_id,
|
||||
$config_mail_oauth_client_secret,
|
||||
$config_mail_oauth_refresh_token,
|
||||
$config_mail_oauth_access_token,
|
||||
$config_mail_oauth_access_token_expires_at;
|
||||
|
||||
if (!empty($config_mail_oauth_access_token) && !tokenExpired($config_mail_oauth_access_token_expires_at)) {
|
||||
return $config_mail_oauth_access_token;
|
||||
}
|
||||
|
||||
if (empty($config_mail_oauth_client_id) || empty($config_mail_oauth_client_secret) || empty($config_mail_oauth_refresh_token)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$resp = httpFormPost(
|
||||
'https://oauth2.googleapis.com/token',
|
||||
[
|
||||
'client_id' => $config_mail_oauth_client_id,
|
||||
'client_secret' => $config_mail_oauth_client_secret,
|
||||
'refresh_token' => $config_mail_oauth_refresh_token,
|
||||
'grant_type' => 'refresh_token',
|
||||
]
|
||||
);
|
||||
|
||||
if (!$resp['ok']) return null;
|
||||
|
||||
$json = json_decode($resp['body'], true);
|
||||
if (!is_array($json) || empty($json['access_token'])) return null;
|
||||
|
||||
$expires_at = date('Y-m-d H:i:s', time() + (int)($json['expires_in'] ?? 3600));
|
||||
|
||||
$config_mail_oauth_access_token = $json['access_token'];
|
||||
$config_mail_oauth_access_token_expires_at = $expires_at;
|
||||
|
||||
$at_esc = mysqli_real_escape_string($mysqli, $config_mail_oauth_access_token);
|
||||
$exp_esc = mysqli_real_escape_string($mysqli, $config_mail_oauth_access_token_expires_at);
|
||||
mysqli_query($mysqli, "UPDATE settings SET
|
||||
config_mail_oauth_access_token = '{$at_esc}',
|
||||
config_mail_oauth_access_token_expires_at = '{$exp_esc}'
|
||||
WHERE company_id = 1
|
||||
");
|
||||
|
||||
return $config_mail_oauth_access_token;
|
||||
}
|
||||
|
||||
function getMicrosoftAccessToken(string $username): ?string {
|
||||
global $mysqli,
|
||||
$config_mail_oauth_client_id,
|
||||
$config_mail_oauth_client_secret,
|
||||
$config_mail_oauth_tenant_id,
|
||||
$config_mail_oauth_refresh_token,
|
||||
$config_mail_oauth_access_token,
|
||||
$config_mail_oauth_access_token_expires_at;
|
||||
|
||||
if (!empty($config_mail_oauth_access_token) && !tokenExpired($config_mail_oauth_access_token_expires_at)) {
|
||||
return $config_mail_oauth_access_token;
|
||||
}
|
||||
|
||||
if (empty($config_mail_oauth_client_id) || empty($config_mail_oauth_client_secret) || empty($config_mail_oauth_refresh_token) || empty($config_mail_oauth_tenant_id)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$url = "https://login.microsoftonline.com/".rawurlencode($config_mail_oauth_tenant_id)."/oauth2/v2.0/token";
|
||||
|
||||
$resp = httpFormPost($url, [
|
||||
'client_id' => $config_mail_oauth_client_id,
|
||||
'client_secret' => $config_mail_oauth_client_secret,
|
||||
'refresh_token' => $config_mail_oauth_refresh_token,
|
||||
'grant_type' => 'refresh_token',
|
||||
]);
|
||||
|
||||
if (!$resp['ok']) return null;
|
||||
|
||||
$json = json_decode($resp['body'], true);
|
||||
if (!is_array($json) || empty($json['access_token'])) return null;
|
||||
|
||||
$expires_at = date('Y-m-d H:i:s', time() + (int)($json['expires_in'] ?? 3600));
|
||||
|
||||
$config_mail_oauth_access_token = $json['access_token'];
|
||||
$config_mail_oauth_access_token_expires_at = $expires_at;
|
||||
|
||||
$at_esc = mysqli_real_escape_string($mysqli, $config_mail_oauth_access_token);
|
||||
$exp_esc = mysqli_real_escape_string($mysqli, $config_mail_oauth_access_token_expires_at);
|
||||
mysqli_query($mysqli, "UPDATE settings SET
|
||||
config_mail_oauth_access_token = '{$at_esc}',
|
||||
config_mail_oauth_access_token_expires_at = '{$exp_esc}'
|
||||
WHERE company_id = 1
|
||||
");
|
||||
|
||||
return $config_mail_oauth_access_token;
|
||||
}
|
||||
|
||||
// Provider from settings (may be NULL/empty to disable IMAP polling)
|
||||
$imap_provider = $config_imap_provider ?? '';
|
||||
if ($imap_provider === null) $imap_provider = '';
|
||||
|
||||
if ($imap_provider === '') {
|
||||
logApp("Cron-Email-Parser", "info", "IMAP polling skipped: provider not configured.");
|
||||
@unlink($lock_file_path);
|
||||
exit(0);
|
||||
}
|
||||
|
||||
/** ------------------------------------------------------------------
|
||||
* ImapEngine setup (Standard / Google OAuth / Microsoft OAuth)
|
||||
* ------------------------------------------------------------------ */
|
||||
use DirectoryTree\ImapEngine\Mailbox;
|
||||
|
||||
// Defaults from settings (standard IMAP)
|
||||
$validate_cert = true;
|
||||
|
||||
$host = $config_imap_host;
|
||||
$port = (int)$config_imap_port;
|
||||
$encr = !empty($config_imap_encryption) ? $config_imap_encryption : null; // 'ssl'|'starttls'|null
|
||||
$user = $config_imap_username;
|
||||
$pass = $config_imap_password;
|
||||
$auth = 'plain'; // 'oauth' for OAuth providers
|
||||
|
||||
if ($imap_provider === 'google_oauth') {
|
||||
$host = 'imap.gmail.com';
|
||||
$port = 993;
|
||||
$encr = 'ssl';
|
||||
$auth = 'oauth';
|
||||
$pass = getGoogleAccessToken($user);
|
||||
if (empty($pass)) {
|
||||
logApp("Cron-Email-Parser", "error", "Google OAuth: no usable access token (check refresh token/client credentials).");
|
||||
@unlink($lock_file_path);
|
||||
exit(1);
|
||||
}
|
||||
} elseif ($imap_provider === 'microsoft_oauth') {
|
||||
$host = 'outlook.office365.com';
|
||||
$port = 993;
|
||||
$encr = 'ssl';
|
||||
$auth = 'oauth';
|
||||
$pass = getMicrosoftAccessToken($user);
|
||||
if (empty($pass)) {
|
||||
logApp("Cron-Email-Parser", "error", "Microsoft OAuth: no usable access token (check refresh token/client credentials/tenant).");
|
||||
@unlink($lock_file_path);
|
||||
exit(1);
|
||||
}
|
||||
} else {
|
||||
// standard_imap (username/password)
|
||||
if (empty($host) || empty($port) || empty($user)) {
|
||||
logApp("Cron-Email-Parser", "error", "Standard IMAP: missing host/port/username.");
|
||||
@unlink($lock_file_path);
|
||||
exit(1);
|
||||
}
|
||||
// Map "tls" / "notls" from your old settings into ImapEngine's expected values
|
||||
// ImapEngine uses: 'ssl', 'starttls', or null.
|
||||
$e = strtolower((string)$encr);
|
||||
if ($e === 'tls') $encr = 'starttls';
|
||||
if ($e === 'notls' || $e === '') $encr = null;
|
||||
}
|
||||
|
||||
$mailbox = new Mailbox([
|
||||
'host' => $host,
|
||||
'port' => $port,
|
||||
'username' => $user,
|
||||
'password' => $pass, // OAuth token when authentication = oauth
|
||||
'encryption' => $encr, // 'ssl'|'starttls'|null
|
||||
'validate_cert' => (bool)$validate_cert,
|
||||
'authentication'=> $auth, // 'plain'|'oauth'
|
||||
'timeout' => 30,
|
||||
'debug' => false,
|
||||
]);
|
||||
|
||||
try {
|
||||
$mailbox->connect();
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error connecting to IMAP server: " . $e->getMessage() . "\n";
|
||||
@unlink($lock_file_path);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// INBOX + ensure target folder exists
|
||||
$inbox = $mailbox->inbox();
|
||||
|
||||
$targetFolderPath = 'ITFlow';
|
||||
$targetFolder = $mailbox->folders()->find($targetFolderPath);
|
||||
if (!$targetFolder) {
|
||||
try {
|
||||
$targetFolder = $mailbox->folders()->create($targetFolderPath);
|
||||
} catch (\Throwable $e) {
|
||||
// Race: if another process created it, find again
|
||||
$targetFolder = $mailbox->folders()->find($targetFolderPath);
|
||||
if (!$targetFolder) {
|
||||
logApp("Cron-Email-Parser", "error", "Failed to create/find target folder [$targetFolderPath]: ".$e->getMessage());
|
||||
$mailbox->disconnect();
|
||||
@unlink($lock_file_path);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch unseen messages (match Webklex behavior: iterate messages, then explicitly mark/move)
|
||||
$messages = $inbox->messages()
|
||||
->unseen()
|
||||
->withHeaders()
|
||||
->withFlags()
|
||||
->withBody()
|
||||
->withBodyStructure()
|
||||
->get();
|
||||
|
||||
// Counters
|
||||
$processed_count = 0;
|
||||
$unprocessed_count = 0;
|
||||
|
||||
// Process messages
|
||||
foreach ($messages as $message) {
|
||||
$email_processed = false;
|
||||
$original_message_file = null;
|
||||
|
||||
// Save original message as .eml
|
||||
try {
|
||||
mkdirMissing('../uploads/tmp/');
|
||||
$original_message_file = "processed-eml-" . randomString(200) . ".eml";
|
||||
|
||||
$rawHead = $message->head() ?? '';
|
||||
$rawBody = $message->body() ?? '';
|
||||
|
||||
// Ensure header/body separator
|
||||
$raw_message = (string)$rawHead;
|
||||
if ($raw_message !== '' && !preg_match("/\r?\n\r?\n$/", $raw_message)) {
|
||||
$raw_message .= "\r\n\r\n";
|
||||
}
|
||||
$raw_message .= (string)$rawBody;
|
||||
|
||||
// Fallback if body() wasn't fetched / empty
|
||||
if (trim($raw_message) === '') {
|
||||
$raw_message = "Subject: ".($message->subject() ?? '')."\r\n\r\n".($message->html() ?: $message->text() ?: '');
|
||||
}
|
||||
|
||||
file_put_contents("../uploads/tmp/{$original_message_file}", $raw_message);
|
||||
} catch (\Throwable $e) {
|
||||
logApp("Cron-Email-Parser", "warning", "Failed saving .eml (uid=".$message->uid()."): ".$e->getMessage());
|
||||
// Keep processing anyway
|
||||
$original_message_file = "processed-eml-" . randomString(200) . ".eml";
|
||||
@file_put_contents("../uploads/tmp/{$original_message_file}", "");
|
||||
}
|
||||
|
||||
// From
|
||||
$fromAddr = $message->from(); // Address|null
|
||||
$from_email = sanitizeInput($fromAddr ? ($fromAddr->email() ?? 'itflow-guest@example.com') : 'itflow-guest@example.com');
|
||||
$from_name = sanitizeInput($fromAddr ? ($fromAddr->name() ?? 'Unknown') : 'Unknown');
|
||||
|
||||
$from_domain = explode("@", $from_email);
|
||||
$from_domain = sanitizeInput(end($from_domain));
|
||||
|
||||
// Subject
|
||||
$subject = sanitizeInput((string)($message->subject() ?: 'No Subject'));
|
||||
|
||||
// CC list
|
||||
$ccs = [];
|
||||
try {
|
||||
foreach (($message->cc() ?? []) as $ccAddr) {
|
||||
if ($ccAddr && $ccAddr->email()) {
|
||||
$ccs[] = $ccAddr->email();
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
// Date
|
||||
try {
|
||||
$dt = $message->date();
|
||||
$date = sanitizeInput($dt ? $dt->format('Y-m-d H:i:s') : date('Y-m-d H:i:s'));
|
||||
} catch (\Throwable $e) {
|
||||
$date = sanitizeInput(date('Y-m-d H:i:s'));
|
||||
}
|
||||
|
||||
// Body (prefer HTML)
|
||||
$message_body_html = '';
|
||||
$message_body_text = '';
|
||||
try { $message_body_html = (string)($message->html() ?? ''); } catch (\Throwable $e) {}
|
||||
try { $message_body_text = (string)($message->text() ?? ''); } catch (\Throwable $e) {}
|
||||
|
||||
if (!empty($message_body_html)) {
|
||||
$message_body = $message_body_html;
|
||||
} elseif (!empty($message_body_text)) {
|
||||
$message_body = nl2br(htmlspecialchars($message_body_text));
|
||||
} else {
|
||||
// Final fallback to raw body (may be MIME-y, but keeps parity with your old fallback)
|
||||
$message_body = nl2br(htmlspecialchars((string)($message->body() ?? '')));
|
||||
}
|
||||
|
||||
// Inline CID handling + attachments collection
|
||||
$attachments = [];
|
||||
|
||||
// Track inline CIDs we embedded so we don't save them as attachments too
|
||||
$inline_cids = [];
|
||||
|
||||
// Use body structure to find inline attachment parts and replace cid: refs
|
||||
try {
|
||||
if ($message->hasBodyStructure()) {
|
||||
$structure = $message->bodyStructure();
|
||||
$parts = $structure ? $structure->flatten() : [];
|
||||
|
||||
foreach ($parts as $part) {
|
||||
// We want inline parts with a Content-ID
|
||||
if (!$part) continue;
|
||||
|
||||
$isInline = false;
|
||||
try { $isInline = (bool)$part->isInline(); } catch (\Throwable $e) { $isInline = false; }
|
||||
|
||||
$cid = null;
|
||||
try { $cid = $part->id(); } catch (\Throwable $e) { $cid = null; }
|
||||
if (!$isInline || empty($cid)) continue;
|
||||
|
||||
// Fetch this inline part and embed into HTML
|
||||
$cid_trim = trim((string)$cid, "<> \t\r\n");
|
||||
if ($cid_trim === '') continue;
|
||||
|
||||
$mime = 'application/octet-stream';
|
||||
try { $mime = (string)($part->contentType() ?: $mime); } catch (\Throwable $e) {}
|
||||
|
||||
$content = null;
|
||||
try {
|
||||
$content = $message->bodyPart($part->partNumber()); // default peek (does not mark seen)
|
||||
} catch (\Throwable $e) {
|
||||
$content = null;
|
||||
}
|
||||
|
||||
if ($content === null || $content === '') continue;
|
||||
|
||||
$dataUri = "data:$mime;base64," . base64_encode($content);
|
||||
$message_body = str_replace(["cid:$cid_trim", "cid:<$cid_trim>", "cid:$cid"], $dataUri, $message_body);
|
||||
$inline_cids[$cid_trim] = true;
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// ignore inline replacement errors
|
||||
}
|
||||
|
||||
// Collect non-inline attachments from the high-level Attachment API
|
||||
try {
|
||||
foreach ($message->attachments() as $att) {
|
||||
if (!$att) continue;
|
||||
|
||||
$name = $att->filename() ?: 'attachment';
|
||||
$content = $att->contents();
|
||||
$cid = null;
|
||||
try { $cid = $att->contentId(); } catch (\Throwable $e) { $cid = null; }
|
||||
$cid_trim = $cid ? trim((string)$cid, "<> \t\r\n") : null;
|
||||
|
||||
// Skip inline ones we already embedded
|
||||
if ($cid_trim && isset($inline_cids[$cid_trim])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($content !== null && $content !== '') {
|
||||
$attachments[] = [
|
||||
'name' => $name,
|
||||
'content' => $content,
|
||||
];
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
// 1. Reply to existing ticket with the number in subject
|
||||
if (preg_match("/\[$config_ticket_prefix(\d+)\]/", $subject, $ticket_number_matches)) {
|
||||
$ticket_number = intval($ticket_number_matches[1]);
|
||||
$email_processed = addReply($from_email, $date, $subject, $ticket_number, $message_body, $attachments);
|
||||
}
|
||||
|
||||
// 2. Fuzzy duplicate check using a known contact/domain and similar_text subject
|
||||
if (!$email_processed && strlen(trim($subject)) > 10) {
|
||||
$contact_id = 0;
|
||||
$client_id = 0;
|
||||
|
||||
$from_email_esc = mysqli_real_escape_string($mysqli, $from_email);
|
||||
$contact_sql = mysqli_query($mysqli, "SELECT * FROM contacts WHERE contact_email = '$from_email_esc' AND contact_archived_at IS NULL LIMIT 1");
|
||||
$contact_row = mysqli_fetch_assoc($contact_sql);
|
||||
|
||||
if ($contact_row) {
|
||||
$contact_id = intval($contact_row['contact_id']);
|
||||
$client_id = intval($contact_row['contact_client_id']);
|
||||
} else {
|
||||
$from_domain_esc = mysqli_real_escape_string($mysqli, $from_domain);
|
||||
$domain_sql = mysqli_query($mysqli, "SELECT * FROM domains WHERE domain_name = '$from_domain_esc' AND domain_archived_at IS NULL LIMIT 1");
|
||||
$domain_row = mysqli_fetch_assoc($domain_sql);
|
||||
|
||||
if ($domain_row && $from_domain == $domain_row['domain_name']) {
|
||||
$client_id = intval($domain_row['domain_client_id']);
|
||||
}
|
||||
}
|
||||
|
||||
if ($client_id) {
|
||||
$recent_tickets_sql = mysqli_query($mysqli,
|
||||
"SELECT ticket_id, ticket_number, ticket_subject
|
||||
FROM tickets
|
||||
WHERE ticket_client_id = $client_id AND ticket_resolved_at IS NULL
|
||||
AND ticket_created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)"
|
||||
);
|
||||
|
||||
while ($rowt = mysqli_fetch_assoc($recent_tickets_sql)) {
|
||||
$ticket_number = intval($rowt['ticket_number']);
|
||||
$existing_subject = $rowt['ticket_subject'];
|
||||
|
||||
similar_text(strtolower($subject), strtolower($existing_subject), $percent);
|
||||
|
||||
if ($percent >= 95) {
|
||||
$email_processed = addReply($from_email, $date, $subject, $ticket_number, $message_body, $attachments);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. A known, registered contact?
|
||||
if (!$email_processed) {
|
||||
$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' AND contact_archived_at IS NULL LIMIT 1");
|
||||
$rowc = mysqli_fetch_assoc($any_contact_sql);
|
||||
|
||||
if ($rowc) {
|
||||
$contact_name = sanitizeInput($rowc['contact_name']);
|
||||
$contact_id = intval($rowc['contact_id']);
|
||||
$contact_email = sanitizeInput($rowc['contact_email']);
|
||||
$client_id = intval($rowc['contact_client_id']);
|
||||
|
||||
$email_processed = addTicket($contact_id, $contact_name, $contact_email, $client_id, $date, $subject, $message_body, $attachments, $original_message_file, $ccs);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. A known domain?
|
||||
if (!$email_processed) {
|
||||
$from_domain_esc = mysqli_real_escape_string($mysqli, $from_domain);
|
||||
$domain_sql = mysqli_query($mysqli, "SELECT * FROM domains WHERE domain_name = '$from_domain_esc' AND domain_archived_at IS NULL LIMIT 1");
|
||||
$rowd = mysqli_fetch_assoc($domain_sql);
|
||||
|
||||
if ($rowd && $from_domain == $rowd['domain_name']) {
|
||||
$client_id = intval($rowd['domain_client_id']);
|
||||
|
||||
// Create a new contact
|
||||
$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_client_id = $client_id");
|
||||
$contact_id = mysqli_insert_id($mysqli);
|
||||
|
||||
logAction("Contact", "Create", "Email parser: created contact " . mysqli_real_escape_string($mysqli, $contact_name), $client_id, $contact_id);
|
||||
customAction('contact_create', $contact_id);
|
||||
|
||||
$email_processed = addTicket($contact_id, $contact_name, $contact_email, $client_id, $date, $subject, $message_body, $attachments, $original_message_file, $ccs);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Unknown sender allowed?
|
||||
if (!$email_processed && $config_ticket_email_parse_unknown_senders) {
|
||||
$bad_from_pattern = "/daemon|postmaster|bounce|mta/i"; // Stop NDRs with bad subjects raising new tickets
|
||||
|
||||
if (!preg_match($bad_from_pattern, $from_email)) {
|
||||
$email_processed = addTicket(0, $from_name, $from_email, 0, $date, $subject, $message_body, $attachments, $original_message_file, $ccs);
|
||||
} else {
|
||||
// Probably an NDR message without a ticket ref in the subject
|
||||
$failed_recipient = null;
|
||||
$diagnostic_code = null;
|
||||
$status_code = null;
|
||||
$original_subject = null;
|
||||
$original_to = null;
|
||||
|
||||
// ImapEngine: check attachments for DSN blocks (similar to your Webklex approach)
|
||||
try {
|
||||
foreach ($message->attachments() as $attachment) {
|
||||
$ctype = strtolower((string)$attachment->contentType());
|
||||
$body = (string)$attachment->contents();
|
||||
|
||||
// 1. Delivery status block
|
||||
if (strpos($ctype, 'delivery-status') !== false) {
|
||||
if (preg_match('/Final-Recipient:\s*rfc822;\s*(.+)/i', $body, $m)) {
|
||||
$failed_recipient = sanitizeInput(trim($m[1]));
|
||||
}
|
||||
if (preg_match('/Diagnostic-Code:\s*(.+)/i', $body, $m)) {
|
||||
$diagnostic_code = sanitizeInput(trim($m[1]));
|
||||
}
|
||||
if (preg_match('/Status:\s*([0-9\.]+)/i', $body, $m)) {
|
||||
$status_code = sanitizeInput(trim($m[1]));
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Original message headers
|
||||
if (strpos($ctype, 'message/rfc822') !== false) {
|
||||
if (preg_match('/^To:\s*(.+)$/mi', $body, $m)) {
|
||||
$original_to = sanitizeInput(trim($m[1]));
|
||||
}
|
||||
if (preg_match('/^Subject:\s*(.+)$/mi', $body, $m)) {
|
||||
$original_subject = sanitizeInput(trim($m[1]));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
// 3. Fallback: extract diagnostic from human-readable text
|
||||
if (!$diagnostic_code) {
|
||||
$text = $message_body_text ?: strip_tags($message_body);
|
||||
if (preg_match('/\n\s{2,}(.+)/', $text, $m)) {
|
||||
$diagnostic_code = sanitizeInput(trim($m[1]));
|
||||
}
|
||||
}
|
||||
|
||||
$failed_recipient = $failed_recipient ?: 'unknown recipient';
|
||||
$diagnostic_code = $diagnostic_code ?: 'unknown diagnostic code';
|
||||
$status_code = $status_code ?: 'unknown status code';
|
||||
$original_subject = $original_subject ?: $subject;
|
||||
|
||||
appNotify(
|
||||
"Ticket",
|
||||
"Email parser NDR: Message to $failed_recipient bounced. Subject: $original_subject Diagnostics: $status_code / $diagnostic_code - check ITFlow folder manually to see email",
|
||||
"",
|
||||
0
|
||||
);
|
||||
|
||||
// If the original subject has a ticket, add the NDR there too
|
||||
if (preg_match("/\[$config_ticket_prefix(\d+)\]/", $original_subject, $ticket_number_matches)) {
|
||||
$ticket_number = intval($ticket_number_matches[1]);
|
||||
|
||||
$reply_body = "Email delivery failed.\n".
|
||||
"Recipient: $failed_recipient\n".
|
||||
"Status: $status_code\n".
|
||||
"Diagnostic: $diagnostic_code\n";
|
||||
|
||||
addReply(
|
||||
$from_email,
|
||||
$date,
|
||||
$original_subject,
|
||||
$ticket_number,
|
||||
$reply_body,
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
$email_processed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Flag/move based on processing result
|
||||
if ($email_processed) {
|
||||
$processed_count++;
|
||||
try {
|
||||
$message->markSeen();
|
||||
$message->move($targetFolderPath); // expunge later
|
||||
} catch (\Throwable $e) {
|
||||
$subj = (string)($message->subject() ?? '');
|
||||
$uid = (int)$message->uid();
|
||||
$path = $targetFolder ? $targetFolder->path() : $targetFolderPath;
|
||||
logApp(
|
||||
"Cron-Email-Parser",
|
||||
"warning",
|
||||
"Move failed (subject=\"$subj\", uid=$uid) to [$path]: ".$e->getMessage()
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$unprocessed_count++;
|
||||
try {
|
||||
$message->markFlagged();
|
||||
$message->unmarkSeen();
|
||||
} catch (\Throwable $e) {
|
||||
logApp("Cron-Email-Parser", "warning", "Flag update failed: ".$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup temp .eml if still present (e.g., reply path or addTicket moved it)
|
||||
if (!empty($original_message_file)) {
|
||||
$tmp_path = "../uploads/tmp/{$original_message_file}";
|
||||
if (file_exists($tmp_path)) { @unlink($tmp_path); }
|
||||
}
|
||||
}
|
||||
|
||||
// Expunge inbox after moves (removes any \Deleted left by server-side MOVE implementation)
|
||||
try {
|
||||
$inbox->expunge();
|
||||
} catch (\Throwable $e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
// Disconnect
|
||||
try {
|
||||
$mailbox->disconnect();
|
||||
} catch (\Throwable $e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
// Execution timing (optional)
|
||||
$script_end_time = microtime(true);
|
||||
$execution_time = $script_end_time - $script_start_time;
|
||||
$execution_time_formatted = number_format($execution_time, 2);
|
||||
|
||||
$processed_info = "Processed: $processed_count email(s), Unprocessed: $unprocessed_count email(s)";
|
||||
// logAction("Cron-Email-Parser", "Execution", "Cron Email Parser executed in $execution_time_formatted seconds. $processed_info");
|
||||
|
||||
// Remove the lock file
|
||||
unlink($lock_file_path);
|
||||
|
||||
// DEBUG
|
||||
echo "\nLock File Path: $lock_file_path\n";
|
||||
if (file_exists($lock_file_path)) {
|
||||
echo "\nLock is present\n\n";
|
||||
}
|
||||
echo "Processed Emails: $processed_count\n";
|
||||
echo "Unprocessed Emails: $unprocessed_count\n";
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"require": {
|
||||
"webklex/php-imap": "^6.2",
|
||||
"directorytree/imapengine": "^1.22"
|
||||
"webklex/php-imap": "^6.2"
|
||||
}
|
||||
}
|
||||
|
||||
1265
plugins/composer.lock
generated
1265
plugins/composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -18,7 +18,6 @@ return array(
|
||||
'DateRangeError' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateRangeError.php',
|
||||
'Deprecated' => $vendorDir . '/symfony/polyfill-php84/Resources/stubs/Deprecated.php',
|
||||
'NoDiscard' => $vendorDir . '/symfony/polyfill-php85/Resources/stubs/NoDiscard.php',
|
||||
'Normalizer' => $vendorDir . '/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php',
|
||||
'Override' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/Override.php',
|
||||
'ReflectionConstant' => $vendorDir . '/symfony/polyfill-php84/Resources/stubs/ReflectionConstant.php',
|
||||
'SQLite3Exception' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/SQLite3Exception.php',
|
||||
|
||||
7
plugins/vendor/composer/autoload_files.php
vendored
7
plugins/vendor/composer/autoload_files.php
vendored
@@ -11,15 +11,10 @@ return array(
|
||||
'6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
|
||||
'606a39d89246991a373564698c2d8383' => $vendorDir . '/symfony/polyfill-php85/bootstrap.php',
|
||||
'2203a247e6fda86070a5e4e07aed533a' => $vendorDir . '/symfony/clock/Resources/now.php',
|
||||
'a1105708a18b76903365ca1c4aa61b02' => $vendorDir . '/symfony/translation/Resources/functions.php',
|
||||
'9d2b9fc6db0f153a0a149fefb182415e' => $vendorDir . '/symfony/polyfill-php84/bootstrap.php',
|
||||
'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php',
|
||||
'a1105708a18b76903365ca1c4aa61b02' => $vendorDir . '/symfony/translation/Resources/functions.php',
|
||||
'23f09fe3194f8c2f70923f90d6702129' => $vendorDir . '/illuminate/collections/functions.php',
|
||||
'60799491728b879e74601d83e38b2cad' => $vendorDir . '/illuminate/collections/helpers.php',
|
||||
'f598d06aa772fa33d905e87be6398fb1' => $vendorDir . '/symfony/polyfill-intl-idn/bootstrap.php',
|
||||
'f625ee536139dfb962a398b200bdb2bd' => $vendorDir . '/illuminate/support/functions.php',
|
||||
'72579e7bd17821bb1321b87411366eae' => $vendorDir . '/illuminate/support/helpers.php',
|
||||
'7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php',
|
||||
'def43f6c87e4f8dfd0c9e1b1bab14fe8' => $vendorDir . '/symfony/polyfill-iconv/bootstrap.php',
|
||||
'b33e3d135e5d9e47d845c576147bda89' => $vendorDir . '/php-di/php-di/src/functions.php',
|
||||
);
|
||||
|
||||
16
plugins/vendor/composer/autoload_psr4.php
vendored
16
plugins/vendor/composer/autoload_psr4.php
vendored
@@ -7,38 +7,22 @@ $baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
'voku\\' => array($vendorDir . '/voku/portable-ascii/src/voku'),
|
||||
'ZBateson\\StreamDecorators\\' => array($vendorDir . '/zbateson/stream-decorators/src'),
|
||||
'ZBateson\\MbWrapper\\' => array($vendorDir . '/zbateson/mb-wrapper/src'),
|
||||
'ZBateson\\MailMimeParser\\' => array($vendorDir . '/zbateson/mail-mime-parser/src'),
|
||||
'Webklex\\PHPIMAP\\' => array($vendorDir . '/webklex/php-imap/src'),
|
||||
'Symfony\\Polyfill\\Php85\\' => array($vendorDir . '/symfony/polyfill-php85'),
|
||||
'Symfony\\Polyfill\\Php84\\' => array($vendorDir . '/symfony/polyfill-php84'),
|
||||
'Symfony\\Polyfill\\Php83\\' => array($vendorDir . '/symfony/polyfill-php83'),
|
||||
'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'),
|
||||
'Symfony\\Polyfill\\Intl\\Normalizer\\' => array($vendorDir . '/symfony/polyfill-intl-normalizer'),
|
||||
'Symfony\\Polyfill\\Intl\\Idn\\' => array($vendorDir . '/symfony/polyfill-intl-idn'),
|
||||
'Symfony\\Polyfill\\Iconv\\' => array($vendorDir . '/symfony/polyfill-iconv'),
|
||||
'Symfony\\Contracts\\Translation\\' => array($vendorDir . '/symfony/translation-contracts'),
|
||||
'Symfony\\Component\\Translation\\' => array($vendorDir . '/symfony/translation'),
|
||||
'Symfony\\Component\\Mime\\' => array($vendorDir . '/symfony/mime'),
|
||||
'Symfony\\Component\\HttpFoundation\\' => array($vendorDir . '/symfony/http-foundation'),
|
||||
'Symfony\\Component\\Clock\\' => array($vendorDir . '/symfony/clock'),
|
||||
'Psr\\SimpleCache\\' => array($vendorDir . '/psr/simple-cache/src'),
|
||||
'Psr\\Log\\' => array($vendorDir . '/psr/log/src'),
|
||||
'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-factory/src', $vendorDir . '/psr/http-message/src'),
|
||||
'Psr\\Container\\' => array($vendorDir . '/psr/container/src'),
|
||||
'Psr\\Clock\\' => array($vendorDir . '/psr/clock/src'),
|
||||
'Laravel\\SerializableClosure\\' => array($vendorDir . '/laravel/serializable-closure/src'),
|
||||
'Invoker\\' => array($vendorDir . '/php-di/invoker/src'),
|
||||
'Illuminate\\Support\\' => array($vendorDir . '/illuminate/collections', $vendorDir . '/illuminate/conditionable', $vendorDir . '/illuminate/macroable', $vendorDir . '/illuminate/support'),
|
||||
'Illuminate\\Pagination\\' => array($vendorDir . '/illuminate/pagination'),
|
||||
'Illuminate\\Contracts\\' => array($vendorDir . '/illuminate/contracts'),
|
||||
'GuzzleHttp\\Psr7\\' => array($vendorDir . '/guzzlehttp/psr7/src'),
|
||||
'Egulias\\EmailValidator\\' => array($vendorDir . '/egulias/email-validator/src'),
|
||||
'Doctrine\\Inflector\\' => array($vendorDir . '/doctrine/inflector/src'),
|
||||
'Doctrine\\Common\\Lexer\\' => array($vendorDir . '/doctrine/lexer/src'),
|
||||
'DirectoryTree\\ImapEngine\\' => array($vendorDir . '/directorytree/imapengine/src'),
|
||||
'DI\\' => array($vendorDir . '/php-di/php-di/src'),
|
||||
'Carbon\\Doctrine\\' => array($vendorDir . '/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine'),
|
||||
'Carbon\\' => array($vendorDir . '/nesbot/carbon/src/Carbon'),
|
||||
);
|
||||
|
||||
101
plugins/vendor/composer/autoload_static.php
vendored
101
plugins/vendor/composer/autoload_static.php
vendored
@@ -12,17 +12,12 @@ class ComposerStaticInit9b9826e5b5cc7806cd328c4112cca75e
|
||||
'6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php',
|
||||
'606a39d89246991a373564698c2d8383' => __DIR__ . '/..' . '/symfony/polyfill-php85/bootstrap.php',
|
||||
'2203a247e6fda86070a5e4e07aed533a' => __DIR__ . '/..' . '/symfony/clock/Resources/now.php',
|
||||
'a1105708a18b76903365ca1c4aa61b02' => __DIR__ . '/..' . '/symfony/translation/Resources/functions.php',
|
||||
'9d2b9fc6db0f153a0a149fefb182415e' => __DIR__ . '/..' . '/symfony/polyfill-php84/bootstrap.php',
|
||||
'e69f7f6ee287b969198c3c9d6777bd38' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/bootstrap.php',
|
||||
'a1105708a18b76903365ca1c4aa61b02' => __DIR__ . '/..' . '/symfony/translation/Resources/functions.php',
|
||||
'23f09fe3194f8c2f70923f90d6702129' => __DIR__ . '/..' . '/illuminate/collections/functions.php',
|
||||
'60799491728b879e74601d83e38b2cad' => __DIR__ . '/..' . '/illuminate/collections/helpers.php',
|
||||
'f598d06aa772fa33d905e87be6398fb1' => __DIR__ . '/..' . '/symfony/polyfill-intl-idn/bootstrap.php',
|
||||
'f625ee536139dfb962a398b200bdb2bd' => __DIR__ . '/..' . '/illuminate/support/functions.php',
|
||||
'72579e7bd17821bb1321b87411366eae' => __DIR__ . '/..' . '/illuminate/support/helpers.php',
|
||||
'7b11c4dc42b3b3023073cb14e519683c' => __DIR__ . '/..' . '/ralouphie/getallheaders/src/getallheaders.php',
|
||||
'def43f6c87e4f8dfd0c9e1b1bab14fe8' => __DIR__ . '/..' . '/symfony/polyfill-iconv/bootstrap.php',
|
||||
'b33e3d135e5d9e47d845c576147bda89' => __DIR__ . '/..' . '/php-di/php-di/src/functions.php',
|
||||
);
|
||||
|
||||
public static $prefixLengthsPsr4 = array (
|
||||
@@ -30,12 +25,6 @@ class ComposerStaticInit9b9826e5b5cc7806cd328c4112cca75e
|
||||
array (
|
||||
'voku\\' => 5,
|
||||
),
|
||||
'Z' =>
|
||||
array (
|
||||
'ZBateson\\StreamDecorators\\' => 26,
|
||||
'ZBateson\\MbWrapper\\' => 19,
|
||||
'ZBateson\\MailMimeParser\\' => 24,
|
||||
),
|
||||
'W' =>
|
||||
array (
|
||||
'Webklex\\PHPIMAP\\' => 16,
|
||||
@@ -46,48 +35,26 @@ class ComposerStaticInit9b9826e5b5cc7806cd328c4112cca75e
|
||||
'Symfony\\Polyfill\\Php84\\' => 23,
|
||||
'Symfony\\Polyfill\\Php83\\' => 23,
|
||||
'Symfony\\Polyfill\\Mbstring\\' => 26,
|
||||
'Symfony\\Polyfill\\Intl\\Normalizer\\' => 33,
|
||||
'Symfony\\Polyfill\\Intl\\Idn\\' => 26,
|
||||
'Symfony\\Polyfill\\Iconv\\' => 23,
|
||||
'Symfony\\Contracts\\Translation\\' => 30,
|
||||
'Symfony\\Component\\Translation\\' => 30,
|
||||
'Symfony\\Component\\Mime\\' => 23,
|
||||
'Symfony\\Component\\HttpFoundation\\' => 33,
|
||||
'Symfony\\Component\\Clock\\' => 24,
|
||||
),
|
||||
'P' =>
|
||||
array (
|
||||
'Psr\\SimpleCache\\' => 16,
|
||||
'Psr\\Log\\' => 8,
|
||||
'Psr\\Http\\Message\\' => 17,
|
||||
'Psr\\Container\\' => 14,
|
||||
'Psr\\Clock\\' => 10,
|
||||
),
|
||||
'L' =>
|
||||
array (
|
||||
'Laravel\\SerializableClosure\\' => 28,
|
||||
),
|
||||
'I' =>
|
||||
array (
|
||||
'Invoker\\' => 8,
|
||||
'Illuminate\\Support\\' => 19,
|
||||
'Illuminate\\Pagination\\' => 22,
|
||||
'Illuminate\\Contracts\\' => 21,
|
||||
),
|
||||
'G' =>
|
||||
array (
|
||||
'GuzzleHttp\\Psr7\\' => 16,
|
||||
),
|
||||
'E' =>
|
||||
array (
|
||||
'Egulias\\EmailValidator\\' => 23,
|
||||
),
|
||||
'D' =>
|
||||
array (
|
||||
'Doctrine\\Inflector\\' => 19,
|
||||
'Doctrine\\Common\\Lexer\\' => 22,
|
||||
'DirectoryTree\\ImapEngine\\' => 25,
|
||||
'DI\\' => 3,
|
||||
),
|
||||
'C' =>
|
||||
array (
|
||||
@@ -101,18 +68,6 @@ class ComposerStaticInit9b9826e5b5cc7806cd328c4112cca75e
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/voku/portable-ascii/src/voku',
|
||||
),
|
||||
'ZBateson\\StreamDecorators\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/zbateson/stream-decorators/src',
|
||||
),
|
||||
'ZBateson\\MbWrapper\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/zbateson/mb-wrapper/src',
|
||||
),
|
||||
'ZBateson\\MailMimeParser\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/zbateson/mail-mime-parser/src',
|
||||
),
|
||||
'Webklex\\PHPIMAP\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/webklex/php-imap/src',
|
||||
@@ -133,18 +88,6 @@ class ComposerStaticInit9b9826e5b5cc7806cd328c4112cca75e
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring',
|
||||
),
|
||||
'Symfony\\Polyfill\\Intl\\Normalizer\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer',
|
||||
),
|
||||
'Symfony\\Polyfill\\Intl\\Idn\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/symfony/polyfill-intl-idn',
|
||||
),
|
||||
'Symfony\\Polyfill\\Iconv\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/symfony/polyfill-iconv',
|
||||
),
|
||||
'Symfony\\Contracts\\Translation\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/symfony/translation-contracts',
|
||||
@@ -153,10 +96,6 @@ class ComposerStaticInit9b9826e5b5cc7806cd328c4112cca75e
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/symfony/translation',
|
||||
),
|
||||
'Symfony\\Component\\Mime\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/symfony/mime',
|
||||
),
|
||||
'Symfony\\Component\\HttpFoundation\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/symfony/http-foundation',
|
||||
@@ -169,15 +108,6 @@ class ComposerStaticInit9b9826e5b5cc7806cd328c4112cca75e
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/psr/simple-cache/src',
|
||||
),
|
||||
'Psr\\Log\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/psr/log/src',
|
||||
),
|
||||
'Psr\\Http\\Message\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/psr/http-factory/src',
|
||||
1 => __DIR__ . '/..' . '/psr/http-message/src',
|
||||
),
|
||||
'Psr\\Container\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/psr/container/src',
|
||||
@@ -186,14 +116,6 @@ class ComposerStaticInit9b9826e5b5cc7806cd328c4112cca75e
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/psr/clock/src',
|
||||
),
|
||||
'Laravel\\SerializableClosure\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/laravel/serializable-closure/src',
|
||||
),
|
||||
'Invoker\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/php-di/invoker/src',
|
||||
),
|
||||
'Illuminate\\Support\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/illuminate/collections',
|
||||
@@ -209,30 +131,10 @@ class ComposerStaticInit9b9826e5b5cc7806cd328c4112cca75e
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/illuminate/contracts',
|
||||
),
|
||||
'GuzzleHttp\\Psr7\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/guzzlehttp/psr7/src',
|
||||
),
|
||||
'Egulias\\EmailValidator\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/egulias/email-validator/src',
|
||||
),
|
||||
'Doctrine\\Inflector\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/doctrine/inflector/src',
|
||||
),
|
||||
'Doctrine\\Common\\Lexer\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/doctrine/lexer/src',
|
||||
),
|
||||
'DirectoryTree\\ImapEngine\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/directorytree/imapengine/src',
|
||||
),
|
||||
'DI\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/php-di/php-di/src',
|
||||
),
|
||||
'Carbon\\Doctrine\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine',
|
||||
@@ -256,7 +158,6 @@ class ComposerStaticInit9b9826e5b5cc7806cd328c4112cca75e
|
||||
'DateRangeError' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateRangeError.php',
|
||||
'Deprecated' => __DIR__ . '/..' . '/symfony/polyfill-php84/Resources/stubs/Deprecated.php',
|
||||
'NoDiscard' => __DIR__ . '/..' . '/symfony/polyfill-php85/Resources/stubs/NoDiscard.php',
|
||||
'Normalizer' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php',
|
||||
'Override' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/Override.php',
|
||||
'ReflectionConstant' => __DIR__ . '/..' . '/symfony/polyfill-php84/Resources/stubs/ReflectionConstant.php',
|
||||
'SQLite3Exception' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/SQLite3Exception.php',
|
||||
|
||||
1317
plugins/vendor/composer/installed.json
vendored
1317
plugins/vendor/composer/installed.json
vendored
File diff suppressed because it is too large
Load Diff
184
plugins/vendor/composer/installed.php
vendored
184
plugins/vendor/composer/installed.php
vendored
@@ -5,7 +5,7 @@
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
'reference' => '1ba19cc2492aa1397d8556f7442ad0c66513c2bf',
|
||||
'reference' => '612041635d962d37f2f400ba1974bec5456ccd1e',
|
||||
'name' => '__root__',
|
||||
'dev' => true,
|
||||
),
|
||||
@@ -16,7 +16,7 @@
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
'reference' => '1ba19cc2492aa1397d8556f7442ad0c66513c2bf',
|
||||
'reference' => '612041635d962d37f2f400ba1974bec5456ccd1e',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'carbonphp/carbon-doctrine-types' => array(
|
||||
@@ -28,15 +28,6 @@
|
||||
'reference' => '18ba5ddfec8976260ead6e866180bd5d2f71aa1d',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'directorytree/imapengine' => array(
|
||||
'pretty_version' => 'v1.22.4',
|
||||
'version' => '1.22.4.0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../directorytree/imapengine',
|
||||
'aliases' => array(),
|
||||
'reference' => 'e41dd11f94bc9077a905de1e0c17bea87632ee64',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'doctrine/inflector' => array(
|
||||
'pretty_version' => '2.1.0',
|
||||
'version' => '2.1.0.0',
|
||||
@@ -46,33 +37,6 @@
|
||||
'reference' => '6d6c96277ea252fc1304627204c3d5e6e15faa3b',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'doctrine/lexer' => array(
|
||||
'pretty_version' => '3.0.1',
|
||||
'version' => '3.0.1.0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../doctrine/lexer',
|
||||
'aliases' => array(),
|
||||
'reference' => '31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'egulias/email-validator' => array(
|
||||
'pretty_version' => '4.0.4',
|
||||
'version' => '4.0.4.0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../egulias/email-validator',
|
||||
'aliases' => array(),
|
||||
'reference' => 'd42c8731f0624ad6bdc8d3e5e9a4524f68801cfa',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'guzzlehttp/psr7' => array(
|
||||
'pretty_version' => '2.8.0',
|
||||
'version' => '2.8.0.0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../guzzlehttp/psr7',
|
||||
'aliases' => array(),
|
||||
'reference' => '21dc724a0583619cd1652f673303492272778051',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'illuminate/collections' => array(
|
||||
'pretty_version' => 'v12.28.1',
|
||||
'version' => '12.28.1.0',
|
||||
@@ -127,15 +91,6 @@
|
||||
'reference' => '487bbe527806615b818e87c364d93ba91f27db9b',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'laravel/serializable-closure' => array(
|
||||
'pretty_version' => 'v2.0.10',
|
||||
'version' => '2.0.10.0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../laravel/serializable-closure',
|
||||
'aliases' => array(),
|
||||
'reference' => '870fc81d2f879903dfc5b60bf8a0f94a1609e669',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'nesbot/carbon' => array(
|
||||
'pretty_version' => '3.10.3',
|
||||
'version' => '3.10.3.0',
|
||||
@@ -145,24 +100,6 @@
|
||||
'reference' => '8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'php-di/invoker' => array(
|
||||
'pretty_version' => '2.3.7',
|
||||
'version' => '2.3.7.0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../php-di/invoker',
|
||||
'aliases' => array(),
|
||||
'reference' => '3c1ddfdef181431fbc4be83378f6d036d59e81e1',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'php-di/php-di' => array(
|
||||
'pretty_version' => '7.1.1',
|
||||
'version' => '7.1.1.0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../php-di/php-di',
|
||||
'aliases' => array(),
|
||||
'reference' => 'f88054cc052e40dbe7b383c8817c19442d480352',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'psr/clock' => array(
|
||||
'pretty_version' => '1.0.0',
|
||||
'version' => '1.0.0.0',
|
||||
@@ -187,51 +124,6 @@
|
||||
'reference' => 'c71ecc56dfe541dbd90c5360474fbc405f8d5963',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'psr/container-implementation' => array(
|
||||
'dev_requirement' => false,
|
||||
'provided' => array(
|
||||
0 => '^1.0',
|
||||
),
|
||||
),
|
||||
'psr/http-factory' => array(
|
||||
'pretty_version' => '1.1.0',
|
||||
'version' => '1.1.0.0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../psr/http-factory',
|
||||
'aliases' => array(),
|
||||
'reference' => '2b4765fddfe3b508ac62f829e852b1501d3f6e8a',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'psr/http-factory-implementation' => array(
|
||||
'dev_requirement' => false,
|
||||
'provided' => array(
|
||||
0 => '1.0',
|
||||
),
|
||||
),
|
||||
'psr/http-message' => array(
|
||||
'pretty_version' => '2.0',
|
||||
'version' => '2.0.0.0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../psr/http-message',
|
||||
'aliases' => array(),
|
||||
'reference' => '402d35bcb92c70c026d1a6a9883f06b2ead23d71',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'psr/http-message-implementation' => array(
|
||||
'dev_requirement' => false,
|
||||
'provided' => array(
|
||||
0 => '1.0',
|
||||
),
|
||||
),
|
||||
'psr/log' => array(
|
||||
'pretty_version' => '3.0.2',
|
||||
'version' => '3.0.2.0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../psr/log',
|
||||
'aliases' => array(),
|
||||
'reference' => 'f16e1d5863e37f8d8c2a01719f5b34baa2b714d3',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'psr/simple-cache' => array(
|
||||
'pretty_version' => '3.0.0',
|
||||
'version' => '3.0.0.0',
|
||||
@@ -241,15 +133,6 @@
|
||||
'reference' => '764e0b3939f5ca87cb904f570ef9be2d78a07865',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'ralouphie/getallheaders' => array(
|
||||
'pretty_version' => '3.0.3',
|
||||
'version' => '3.0.3.0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../ralouphie/getallheaders',
|
||||
'aliases' => array(),
|
||||
'reference' => '120b605dfeb996808c31b6477290a714d356e822',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'spatie/once' => array(
|
||||
'dev_requirement' => false,
|
||||
'replaced' => array(
|
||||
@@ -283,42 +166,6 @@
|
||||
'reference' => 'db488a62f98f7a81d5746f05eea63a74e55bb7c4',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'symfony/mime' => array(
|
||||
'pretty_version' => 'v8.0.6',
|
||||
'version' => '8.0.6.0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../symfony/mime',
|
||||
'aliases' => array(),
|
||||
'reference' => '632aef4f15ead4d48c16395e447f2da12543d201',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'symfony/polyfill-iconv' => array(
|
||||
'pretty_version' => 'v1.33.0',
|
||||
'version' => '1.33.0.0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../symfony/polyfill-iconv',
|
||||
'aliases' => array(),
|
||||
'reference' => '5f3b930437ae03ae5dff61269024d8ea1b3774aa',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'symfony/polyfill-intl-idn' => array(
|
||||
'pretty_version' => 'v1.33.0',
|
||||
'version' => '1.33.0.0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../symfony/polyfill-intl-idn',
|
||||
'aliases' => array(),
|
||||
'reference' => '9614ac4d8061dc257ecc64cba1b140873dce8ad3',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'symfony/polyfill-intl-normalizer' => array(
|
||||
'pretty_version' => 'v1.33.0',
|
||||
'version' => '1.33.0.0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../symfony/polyfill-intl-normalizer',
|
||||
'aliases' => array(),
|
||||
'reference' => '3833d7255cc303546435cb650316bff708a1c75c',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'symfony/polyfill-mbstring' => array(
|
||||
'pretty_version' => 'v1.33.0',
|
||||
'version' => '1.33.0.0',
|
||||
@@ -397,32 +244,5 @@
|
||||
'reference' => '6b8ef85d621bbbaf52741b00cca8e9237e2b2e05',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'zbateson/mail-mime-parser' => array(
|
||||
'pretty_version' => '3.0.5',
|
||||
'version' => '3.0.5.0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../zbateson/mail-mime-parser',
|
||||
'aliases' => array(),
|
||||
'reference' => 'ff054c8e05310c445c2028c6128a4319cc9f6aa8',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'zbateson/mb-wrapper' => array(
|
||||
'pretty_version' => '2.0.1',
|
||||
'version' => '2.0.1.0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../zbateson/mb-wrapper',
|
||||
'aliases' => array(),
|
||||
'reference' => '50a14c0c9537f978a61cde9fdc192a0267cc9cff',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'zbateson/stream-decorators' => array(
|
||||
'pretty_version' => '2.1.1',
|
||||
'version' => '2.1.1.0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../zbateson/stream-decorators',
|
||||
'aliases' => array(),
|
||||
'reference' => '32a2a62fb0f26313395c996ebd658d33c3f9c4e5',
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
4
plugins/vendor/composer/platform_check.php
vendored
4
plugins/vendor/composer/platform_check.php
vendored
@@ -4,8 +4,8 @@
|
||||
|
||||
$issues = array();
|
||||
|
||||
if (!(PHP_VERSION_ID >= 80400)) {
|
||||
$issues[] = 'Your Composer dependencies require a PHP version ">= 8.4.0". You are running ' . PHP_VERSION . '.';
|
||||
if (!(PHP_VERSION_ID >= 80200)) {
|
||||
$issues[] = 'Your Composer dependencies require a PHP version ">= 8.2.0". You are running ' . PHP_VERSION . '.';
|
||||
}
|
||||
|
||||
if ($issues) {
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
{
|
||||
"name": "directorytree/imapengine",
|
||||
"type": "library",
|
||||
"description": "A fully-featured IMAP library -- without the PHP extension",
|
||||
"keywords": [
|
||||
"imap",
|
||||
"mail",
|
||||
"engine"
|
||||
],
|
||||
"homepage": "https://github.com/directorytree/imapengine",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Steve Bauman",
|
||||
"email": "steven_bauman@outlook.com",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^8.1",
|
||||
"symfony/mime": ">=6.0",
|
||||
"nesbot/carbon": ">=2.0",
|
||||
"illuminate/collections": ">=9.0",
|
||||
"zbateson/mail-mime-parser": "^3.0",
|
||||
"egulias/email-validator": "^4.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"spatie/ray": "^1.0",
|
||||
"pestphp/pest": "^2.0|^3.0|^4.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"DirectoryTree\\ImapEngine\\": "src"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\": "tests"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"pestphp/pest-plugin": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine;
|
||||
|
||||
use DirectoryTree\ImapEngine\Support\Str;
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
use JsonSerializable;
|
||||
|
||||
class Address implements Arrayable, JsonSerializable
|
||||
{
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
protected string $email,
|
||||
protected string $name,
|
||||
) {
|
||||
$this->name = Str::decodeMimeHeader($this->name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the address's email.
|
||||
*/
|
||||
public function email(): string
|
||||
{
|
||||
return $this->email;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the address's name.
|
||||
*/
|
||||
public function name(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array representation of the address.
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'email' => $this->email,
|
||||
'name' => $this->name,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the JSON representation of the address.
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine;
|
||||
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
use JsonSerializable;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
use Symfony\Component\Mime\MimeTypes;
|
||||
|
||||
class Attachment implements Arrayable, JsonSerializable
|
||||
{
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
protected ?string $filename,
|
||||
protected ?string $contentId,
|
||||
protected string $contentType,
|
||||
protected ?string $contentDisposition,
|
||||
protected StreamInterface $contentStream,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get the attachment's filename.
|
||||
*/
|
||||
public function filename(): ?string
|
||||
{
|
||||
return $this->filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attachment's content ID.
|
||||
*/
|
||||
public function contentId(): ?string
|
||||
{
|
||||
return $this->contentId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attachment's content type.
|
||||
*/
|
||||
public function contentType(): string
|
||||
{
|
||||
return $this->contentType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attachment's content disposition.
|
||||
*/
|
||||
public function contentDisposition(): string
|
||||
{
|
||||
return $this->contentDisposition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attachment's contents.
|
||||
*/
|
||||
public function contents(): string
|
||||
{
|
||||
return $this->contentStream->getContents();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attachment's content stream.
|
||||
*/
|
||||
public function contentStream(): StreamInterface
|
||||
{
|
||||
return $this->contentStream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the attachment to a file.
|
||||
*/
|
||||
public function save(string $path): false|int
|
||||
{
|
||||
return file_put_contents($path, $this->contents());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attachment's extension.
|
||||
*/
|
||||
public function extension(): ?string
|
||||
{
|
||||
if ($ext = pathinfo($this->filename ?? '', PATHINFO_EXTENSION)) {
|
||||
return $ext;
|
||||
}
|
||||
|
||||
if ($ext = (MimeTypes::getDefault()->getExtensions($this->contentType)[0] ?? null)) {
|
||||
return $ext;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array representation of the attachment.
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'filename' => $this->filename,
|
||||
'content_type' => $this->contentType,
|
||||
'contents' => $this->contents(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the JSON representation of the attachment.
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
}
|
||||
@@ -1,275 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine;
|
||||
|
||||
use Countable;
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\Data\ListData;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\Nil;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\Token;
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
use IteratorAggregate;
|
||||
use JsonSerializable;
|
||||
use Traversable;
|
||||
|
||||
/**
|
||||
* @implements IteratorAggregate<int, BodyStructurePart|BodyStructureCollection>
|
||||
*/
|
||||
class BodyStructureCollection implements Arrayable, Countable, IteratorAggregate, JsonSerializable
|
||||
{
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param array<BodyStructurePart|BodyStructureCollection> $parts
|
||||
*/
|
||||
public function __construct(
|
||||
protected string $subtype = 'mixed',
|
||||
protected array $parameters = [],
|
||||
protected array $parts = [],
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Parse a multipart BODYSTRUCTURE ListData into a BodyStructureCollection.
|
||||
*/
|
||||
public static function fromListData(ListData $data, ?string $partNumber = null): static
|
||||
{
|
||||
$tokens = $data->tokens();
|
||||
|
||||
$parts = [];
|
||||
$childIndex = 1;
|
||||
$subtypeIndex = null;
|
||||
|
||||
foreach ($tokens as $index => $token) {
|
||||
if ($token instanceof Token && ! $token instanceof Nil) {
|
||||
$subtypeIndex = $index;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (! $token instanceof ListData) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$childPartNumber = $partNumber ? "{$partNumber}.{$childIndex}" : (string) $childIndex;
|
||||
|
||||
$parts[] = static::isMultipart($token)
|
||||
? static::fromListData($token, $childPartNumber)
|
||||
: BodyStructurePart::fromListData($token, $childPartNumber);
|
||||
|
||||
$childIndex++;
|
||||
}
|
||||
|
||||
$parameters = [];
|
||||
|
||||
if ($subtypeIndex) {
|
||||
foreach (array_slice($tokens, $subtypeIndex + 1) as $token) {
|
||||
if ($token instanceof ListData && ! static::isDispositionList($token)) {
|
||||
$parameters = $token->toKeyValuePairs();
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new static(
|
||||
$subtypeIndex ? strtolower($tokens[$subtypeIndex]->value) : 'mixed',
|
||||
$parameters,
|
||||
$parts
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a ListData represents a multipart structure.
|
||||
*/
|
||||
protected static function isMultipart(ListData $data): bool
|
||||
{
|
||||
return head($data->tokens()) instanceof ListData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a ListData represents a disposition (INLINE or ATTACHMENT).
|
||||
*/
|
||||
protected static function isDispositionList(ListData $data): bool
|
||||
{
|
||||
$tokens = $data->tokens();
|
||||
|
||||
if (count($tokens) < 2 || ! isset($tokens[0]) || ! $tokens[0] instanceof Token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return in_array(strtoupper($tokens[0]->value), ['INLINE', 'ATTACHMENT']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the multipart subtype (mixed, alternative, related, etc.).
|
||||
*/
|
||||
public function subtype(): string
|
||||
{
|
||||
return $this->subtype;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content type.
|
||||
*/
|
||||
public function contentType(): string
|
||||
{
|
||||
return "multipart/{$this->subtype}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parameters (e.g., boundary).
|
||||
*/
|
||||
public function parameters(): array
|
||||
{
|
||||
return $this->parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the boundary parameter.
|
||||
*/
|
||||
public function boundary(): ?string
|
||||
{
|
||||
return $this->parameters['boundary'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the direct child parts.
|
||||
*
|
||||
* @return array<BodyStructurePart|BodyStructureCollection>
|
||||
*/
|
||||
public function parts(): array
|
||||
{
|
||||
return $this->parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all parts flattened (including nested parts).
|
||||
*
|
||||
* @return BodyStructurePart[]
|
||||
*/
|
||||
public function flatten(): array
|
||||
{
|
||||
$flattened = [];
|
||||
|
||||
foreach ($this->parts as $part) {
|
||||
if ($part instanceof self) {
|
||||
$flattened = array_merge($flattened, $part->flatten());
|
||||
} else {
|
||||
$flattened[] = $part;
|
||||
}
|
||||
}
|
||||
|
||||
return $flattened;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a part by its part number.
|
||||
*/
|
||||
public function find(string $partNumber): BodyStructurePart|BodyStructureCollection|null
|
||||
{
|
||||
foreach ($this->parts as $part) {
|
||||
if ($part instanceof self) {
|
||||
if ($found = $part->find($partNumber)) {
|
||||
return $found;
|
||||
}
|
||||
} elseif ($part->partNumber() === $partNumber) {
|
||||
return $part;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the text/plain part if available.
|
||||
*/
|
||||
public function text(): ?BodyStructurePart
|
||||
{
|
||||
foreach ($this->flatten() as $part) {
|
||||
if ($part->isText()) {
|
||||
return $part;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the text/html part if available.
|
||||
*/
|
||||
public function html(): ?BodyStructurePart
|
||||
{
|
||||
foreach ($this->flatten() as $part) {
|
||||
if ($part->isHtml()) {
|
||||
return $part;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all attachment parts.
|
||||
*
|
||||
* @return BodyStructurePart[]
|
||||
*/
|
||||
public function attachments(): array
|
||||
{
|
||||
return array_values(array_filter(
|
||||
$this->flatten(),
|
||||
fn (BodyStructurePart $part) => $part->isAttachment()
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the collection has attachments.
|
||||
*/
|
||||
public function hasAttachments(): bool
|
||||
{
|
||||
return count($this->attachments()) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the count of attachments.
|
||||
*/
|
||||
public function attachmentCount(): int
|
||||
{
|
||||
return count($this->attachments());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the count of parts.
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
return count($this->parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an iterator for the parts.
|
||||
*/
|
||||
public function getIterator(): Traversable
|
||||
{
|
||||
yield from $this->parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array representation.
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'subtype' => $this->subtype,
|
||||
'parameters' => $this->parameters,
|
||||
'content_type' => $this->contentType(),
|
||||
'parts' => array_map(fn (Arrayable $part) => $part->toArray(), $this->parts),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the JSON representation.
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
}
|
||||
@@ -1,243 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine;
|
||||
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\Data\ListData;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\Nil;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\Token;
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
use JsonSerializable;
|
||||
|
||||
class BodyStructurePart implements Arrayable, JsonSerializable
|
||||
{
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
protected string $partNumber,
|
||||
protected string $type,
|
||||
protected string $subtype,
|
||||
protected array $parameters = [],
|
||||
protected ?string $id = null,
|
||||
protected ?string $description = null,
|
||||
protected ?string $encoding = null,
|
||||
protected ?int $size = null,
|
||||
protected ?int $lines = null,
|
||||
protected ?ContentDisposition $disposition = null,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Parse a single part BODYSTRUCTURE ListData into a BodyStructurePart.
|
||||
*/
|
||||
public static function fromListData(ListData $data, string $partNumber = '1'): static
|
||||
{
|
||||
return static::parse($data->tokens(), $partNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a single (non-multipart) part.
|
||||
*
|
||||
* @param array<Token|ListData> $tokens
|
||||
*/
|
||||
protected static function parse(array $tokens, string $partNumber): static
|
||||
{
|
||||
return new static(
|
||||
partNumber: $partNumber,
|
||||
type: isset($tokens[0]) ? strtolower($tokens[0]->value) : 'text',
|
||||
subtype: isset($tokens[1]) ? strtolower($tokens[1]->value) : 'plain',
|
||||
parameters: isset($tokens[2]) && $tokens[2] instanceof ListData ? $tokens[2]->toKeyValuePairs() : [],
|
||||
id: isset($tokens[3]) && ! $tokens[3] instanceof Nil ? $tokens[3]->value : null,
|
||||
description: isset($tokens[4]) && ! $tokens[4] instanceof Nil ? $tokens[4]->value : null,
|
||||
encoding: isset($tokens[5]) && ! $tokens[5] instanceof Nil ? $tokens[5]->value : null,
|
||||
size: isset($tokens[6]) && ! $tokens[6] instanceof Nil ? (int) $tokens[6]->value : null,
|
||||
lines: isset($tokens[7]) && ! $tokens[7] instanceof Nil ? (int) $tokens[7]->value : null,
|
||||
disposition: ContentDisposition::parse($tokens),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the part number (e.g., "1", "1.2", "2.1.3").
|
||||
*/
|
||||
public function partNumber(): string
|
||||
{
|
||||
return $this->partNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the MIME type (e.g., "text", "image", "multipart").
|
||||
*/
|
||||
public function type(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the MIME subtype (e.g., "plain", "html", "jpeg", "mixed").
|
||||
*/
|
||||
public function subtype(): string
|
||||
{
|
||||
return $this->subtype;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full content type (e.g., "text/plain", "multipart/alternative").
|
||||
*/
|
||||
public function contentType(): string
|
||||
{
|
||||
return "{$this->type}/{$this->subtype}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parameters (e.g., charset, boundary).
|
||||
*/
|
||||
public function parameters(): array
|
||||
{
|
||||
return $this->parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific parameter value.
|
||||
*/
|
||||
public function parameter(string $name): ?string
|
||||
{
|
||||
return $this->parameters[strtolower($name)] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content ID.
|
||||
*/
|
||||
public function id(): ?string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content description.
|
||||
*/
|
||||
public function description(): ?string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content transfer encoding.
|
||||
*/
|
||||
public function encoding(): ?string
|
||||
{
|
||||
return $this->encoding;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size in bytes.
|
||||
*/
|
||||
public function size(): ?int
|
||||
{
|
||||
return $this->size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of lines (for text parts).
|
||||
*/
|
||||
public function lines(): ?int
|
||||
{
|
||||
return $this->lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content disposition.
|
||||
*/
|
||||
public function disposition(): ?ContentDisposition
|
||||
{
|
||||
return $this->disposition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the filename from disposition parameters.
|
||||
*/
|
||||
public function filename(): ?string
|
||||
{
|
||||
return $this->disposition?->filename() ?? $this->parameters['name'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the charset from parameters.
|
||||
*/
|
||||
public function charset(): ?string
|
||||
{
|
||||
return $this->parameters['charset'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if this is a text part.
|
||||
*/
|
||||
public function isText(): bool
|
||||
{
|
||||
return $this->type === 'text' && $this->subtype === 'plain';
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if this is an HTML part.
|
||||
*/
|
||||
public function isHtml(): bool
|
||||
{
|
||||
return $this->type === 'text' && $this->subtype === 'html';
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if this is an attachment.
|
||||
*/
|
||||
public function isAttachment(): bool
|
||||
{
|
||||
if ($this->disposition?->isAttachment()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Inline parts are not attachments.
|
||||
if ($this->disposition?->isInline()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Consider non-text/html parts with filenames as attachments.
|
||||
if ($this->filename() && ! $this->isText() && ! $this->isHtml()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if this is an inline part.
|
||||
*/
|
||||
public function isInline(): bool
|
||||
{
|
||||
return $this->disposition?->isInline() ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array representation.
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'type' => $this->type,
|
||||
'size' => $this->size,
|
||||
'lines' => $this->lines,
|
||||
'subtype' => $this->subtype,
|
||||
'encoding' => $this->encoding,
|
||||
'parameters' => $this->parameters,
|
||||
'part_number' => $this->partNumber,
|
||||
'description' => $this->description,
|
||||
'content_type' => $this->contentType(),
|
||||
'disposition' => $this->disposition?->toArray(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the JSON representation.
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Collections;
|
||||
|
||||
use DirectoryTree\ImapEngine\FolderInterface;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* @template-extends Collection<array-key, FolderInterface>
|
||||
*/
|
||||
class FolderCollection extends Collection {}
|
||||
@@ -1,32 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Collections;
|
||||
|
||||
use DirectoryTree\ImapEngine\Message;
|
||||
use DirectoryTree\ImapEngine\MessageInterface;
|
||||
|
||||
/**
|
||||
* @template-extends PaginatedCollection<array-key, MessageInterface|Message>
|
||||
*/
|
||||
class MessageCollection extends PaginatedCollection
|
||||
{
|
||||
/**
|
||||
* Find a message by its UID.
|
||||
*/
|
||||
public function find(int $uid): ?MessageInterface
|
||||
{
|
||||
return $this->first(
|
||||
fn (MessageInterface $message) => $message->uid() === $uid
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a message by its UID or throw an exception.
|
||||
*/
|
||||
public function findOrFail(int $uid): MessageInterface
|
||||
{
|
||||
return $this->firstOrFail(
|
||||
fn (MessageInterface $message) => $message->uid() === $uid
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Collections;
|
||||
|
||||
use DirectoryTree\ImapEngine\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* @template TKey of array-key
|
||||
* @template TValue
|
||||
*
|
||||
* @template-extends Collection<TKey, TValue>
|
||||
*/
|
||||
class PaginatedCollection extends Collection
|
||||
{
|
||||
/**
|
||||
* The total number of items.
|
||||
*/
|
||||
protected int $total = 0;
|
||||
|
||||
/**
|
||||
* Paginate the current collection.
|
||||
*
|
||||
* @return LengthAwarePaginator<TKey, TValue>
|
||||
*/
|
||||
public function paginate(int $perPage = 15, ?int $page = null, string $pageName = 'page', bool $prepaginated = false): LengthAwarePaginator
|
||||
{
|
||||
$total = $this->total ?: $this->count();
|
||||
|
||||
$results = ! $prepaginated && $total ? $this->forPage($page, $perPage) : $this;
|
||||
|
||||
return $this->paginator($results, $total, $perPage, $page, $pageName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new length-aware paginator instance.
|
||||
*
|
||||
* @return LengthAwarePaginator<TKey, TValue>
|
||||
*/
|
||||
protected function paginator(Collection $items, int $total, int $perPage, ?int $currentPage, string $pageName): LengthAwarePaginator
|
||||
{
|
||||
return new LengthAwarePaginator($items, $total, $perPage, $currentPage, $pageName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or set the total amount.
|
||||
*/
|
||||
public function total(?int $total = null): ?int
|
||||
{
|
||||
if (is_null($total)) {
|
||||
return $this->total;
|
||||
}
|
||||
|
||||
return $this->total = $total;
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Collections;
|
||||
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\ContinuationResponse;
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\TaggedResponse;
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\UntaggedResponse;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* @template TKey of array-key
|
||||
*
|
||||
* @template-covariant TValue
|
||||
*
|
||||
* @extends Collection<array-key, TValue>
|
||||
*/
|
||||
class ResponseCollection extends Collection
|
||||
{
|
||||
/**
|
||||
* Filter the collection to only tagged responses.
|
||||
*
|
||||
* @return self<array-key, TaggedResponse>
|
||||
*/
|
||||
public function tagged(): self
|
||||
{
|
||||
return $this->whereInstanceOf(TaggedResponse::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the collection to only untagged responses.
|
||||
*
|
||||
* @return self<array-key, UntaggedResponse>
|
||||
*/
|
||||
public function untagged(): self
|
||||
{
|
||||
return $this->whereInstanceOf(UntaggedResponse::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the collection to only continuation responses.
|
||||
*
|
||||
* @return self<array-key, ContinuationResponse>
|
||||
*/
|
||||
public function continuation(): self
|
||||
{
|
||||
return $this->whereInstanceOf(ContinuationResponse::class);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine;
|
||||
|
||||
trait ComparesFolders
|
||||
{
|
||||
/**
|
||||
* Determine if two folders are the same.
|
||||
*/
|
||||
protected function isSameFolder(FolderInterface $a, FolderInterface $b): bool
|
||||
{
|
||||
return $a->path() === $b->path()
|
||||
&& $a->mailbox()->config('host') === $b->mailbox()->config('host')
|
||||
&& $a->mailbox()->config('username') === $b->mailbox()->config('username');
|
||||
}
|
||||
}
|
||||
@@ -1,344 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Connection;
|
||||
|
||||
use DirectoryTree\ImapEngine\Collections\ResponseCollection;
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\TaggedResponse;
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\UntaggedResponse;
|
||||
use DirectoryTree\ImapEngine\Enums\ImapFetchIdentifier;
|
||||
use DirectoryTree\ImapEngine\Enums\ImapSortKey;
|
||||
use Generator;
|
||||
|
||||
interface ConnectionInterface
|
||||
{
|
||||
/**
|
||||
* Open a new connection.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-state-and-flow-diagram
|
||||
*/
|
||||
public function connect(string $host, ?int $port = null, array $options = []): void;
|
||||
|
||||
/**
|
||||
* Close the current connection.
|
||||
*/
|
||||
public function disconnect(): void;
|
||||
|
||||
/**
|
||||
* Determine if the current session is connected.
|
||||
*/
|
||||
public function connected(): bool;
|
||||
|
||||
/**
|
||||
* Send a "LOGIN" command.
|
||||
*
|
||||
* Login to a new session.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-login-command
|
||||
*/
|
||||
public function login(string $user, string $password): TaggedResponse;
|
||||
|
||||
/**
|
||||
* Send a "LOGOUT" command.
|
||||
*
|
||||
* Logout of the current server session.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-logout-command
|
||||
*/
|
||||
public function logout(): void;
|
||||
|
||||
/**
|
||||
* Send an "AUTHENTICATE" command.
|
||||
*
|
||||
* Authenticate the current session.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-authenticate-command
|
||||
*/
|
||||
public function authenticate(string $user, string $token): TaggedResponse;
|
||||
|
||||
/**
|
||||
* Send a "STARTTLS" command.
|
||||
*
|
||||
* Upgrade the current plaintext connection to a secure TLS-encrypted connection.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-starttls-command
|
||||
*/
|
||||
public function startTls(): void;
|
||||
|
||||
/**
|
||||
* Send an "IDLE" command.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-idle-command
|
||||
*/
|
||||
public function idle(int $timeout): Generator;
|
||||
|
||||
/**
|
||||
* Send a "DONE" command.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#section-6.3.13
|
||||
*/
|
||||
public function done(): void;
|
||||
|
||||
/**
|
||||
* Send a "NOOP" command.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-noop-command
|
||||
*/
|
||||
public function noop(): TaggedResponse;
|
||||
|
||||
/**
|
||||
* Send a "EXPUNGE" command.
|
||||
*
|
||||
* Apply session saved changes to the server.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-expunge-command
|
||||
*/
|
||||
public function expunge(): ResponseCollection;
|
||||
|
||||
/**
|
||||
* Send a "CAPABILITY" command.
|
||||
*
|
||||
* Get the mailbox's available capabilities.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-capability-command
|
||||
*/
|
||||
public function capability(): UntaggedResponse;
|
||||
|
||||
/**
|
||||
* Send a "SEARCH" command.
|
||||
*
|
||||
* Execute a search request.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-search-command
|
||||
*/
|
||||
public function search(array $params): UntaggedResponse;
|
||||
|
||||
/**
|
||||
* Send a "SORT" command.
|
||||
*
|
||||
* Execute a sort request using RFC 5256.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc5256
|
||||
*/
|
||||
public function sort(ImapSortKey $key, string $direction, array $params): UntaggedResponse;
|
||||
|
||||
/**
|
||||
* Send a "FETCH" command.
|
||||
*
|
||||
* Exchange identification information.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc2971.
|
||||
*/
|
||||
public function id(?array $ids = null): UntaggedResponse;
|
||||
|
||||
/**
|
||||
* Send a "FETCH UID" command.
|
||||
*
|
||||
* Fetch message UIDs using the given message numbers.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-uid-command
|
||||
*/
|
||||
public function uid(int|array $ids, ImapFetchIdentifier $identifier): ResponseCollection;
|
||||
|
||||
/**
|
||||
* Send a "FETCH BODY[TEXT]" command.
|
||||
*
|
||||
* Fetch message text contents.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#section-6.4.5-9.9
|
||||
*/
|
||||
public function bodyText(int|array $ids, bool $peek = true): ResponseCollection;
|
||||
|
||||
/**
|
||||
* Send a "FETCH BODY[HEADER]" command.
|
||||
*
|
||||
* Fetch message headers.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#section-6.4.5-9.9
|
||||
*/
|
||||
public function bodyHeader(int|array $ids, bool $peek = true): ResponseCollection;
|
||||
|
||||
/**
|
||||
* Send a "FETCH BODYSTRUCTURE" command.
|
||||
*
|
||||
* Fetch message body structure.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#section-6.4.5-9.9
|
||||
*/
|
||||
public function bodyStructure(int|array $ids): ResponseCollection;
|
||||
|
||||
/**
|
||||
* Send a "FETCH BODY[i]" command.
|
||||
*
|
||||
* Fetch a specific part of the message BODY, such as BODY[1], BODY[1.2], etc.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#section-6.4.5-9.9
|
||||
*/
|
||||
public function bodyPart(string $partIndex, int|array $ids, bool $peek = false): ResponseCollection;
|
||||
|
||||
/**
|
||||
* Send a "FETCH FLAGS" command.
|
||||
*
|
||||
* Fetch a message flags.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#section-6.4.5-9.17
|
||||
*/
|
||||
public function flags(int|array $ids): ResponseCollection;
|
||||
|
||||
/**
|
||||
* Send a "FETCH" command.
|
||||
*
|
||||
* Fetch one or more items for one or more messages.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-fetch-command
|
||||
*/
|
||||
public function fetch(array|string $items, array|int $from, mixed $to = null, ImapFetchIdentifier $identifier = ImapFetchIdentifier::Uid): ResponseCollection;
|
||||
|
||||
/**
|
||||
* Send a "RFC822.SIZE" command.
|
||||
*
|
||||
* Fetch message sizes for one or more messages.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#section-6.4.5-9.21
|
||||
*/
|
||||
public function size(int|array $ids): ResponseCollection;
|
||||
|
||||
/**
|
||||
* Send an IMAP command.
|
||||
*/
|
||||
public function send(string $name, array $tokens = [], ?string &$tag = null): void;
|
||||
|
||||
/**
|
||||
* Send a "SELECT" command.
|
||||
*
|
||||
* Select the specified folder.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-select-command
|
||||
*/
|
||||
public function select(string $folder): ResponseCollection;
|
||||
|
||||
/**
|
||||
* Send a "EXAMINE" command.
|
||||
*
|
||||
* Examine a given folder.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-examine-command
|
||||
*/
|
||||
public function examine(string $folder): ResponseCollection;
|
||||
|
||||
/**
|
||||
* Send a "LIST" command.
|
||||
*
|
||||
* Get a list of available folders.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-list-command
|
||||
*/
|
||||
public function list(string $reference = '', string $folder = '*'): ResponseCollection;
|
||||
|
||||
/**
|
||||
* Send a "STATUS" command.
|
||||
*
|
||||
* Get the status of a given folder.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-status-command
|
||||
*/
|
||||
public function status(string $folder, array $arguments = ['MESSAGES', 'UNSEEN', 'RECENT', 'UIDNEXT', 'UIDVALIDITY']): UntaggedResponse;
|
||||
|
||||
/**
|
||||
* Send a "STORE" command.
|
||||
*
|
||||
* Set message flags.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-store-command
|
||||
*/
|
||||
public function store(array|string $flags, array|int $from, ?int $to = null, ?string $mode = null, bool $silent = true, ?string $item = null): ResponseCollection;
|
||||
|
||||
/**
|
||||
* Send a "APPEND" command.
|
||||
*
|
||||
* Append a new message to given folder.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-append-command
|
||||
*/
|
||||
public function append(string $folder, string $message, ?array $flags = null): TaggedResponse;
|
||||
|
||||
/**
|
||||
* Send a "UID COPY" command.
|
||||
*
|
||||
* Copy message set from current folder to other folder.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-copy-command
|
||||
*/
|
||||
public function copy(string $folder, array|int $from, ?int $to = null): TaggedResponse;
|
||||
|
||||
/**
|
||||
* Send a "UID MOVE" command.
|
||||
*
|
||||
* Move a message set from current folder to another folder.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-move-command
|
||||
*/
|
||||
public function move(string $folder, array|int $from, ?int $to = null): TaggedResponse;
|
||||
|
||||
/**
|
||||
* Send a "CREATE" command.
|
||||
*
|
||||
* Create a new folder.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-create-command
|
||||
*/
|
||||
public function create(string $folder): ResponseCollection;
|
||||
|
||||
/**
|
||||
* Send a "DELETE" command.
|
||||
*
|
||||
* Delete a folder.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-delete-command
|
||||
*/
|
||||
public function delete(string $folder): TaggedResponse;
|
||||
|
||||
/**
|
||||
* Send a "RENAME" command.
|
||||
*
|
||||
* Rename an existing folder.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-rename-command
|
||||
*/
|
||||
public function rename(string $oldPath, string $newPath): TaggedResponse;
|
||||
|
||||
/**
|
||||
* Send a "SUBSCRIBE" command.
|
||||
*
|
||||
* Subscribe to a folder.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-subscribe-command
|
||||
*/
|
||||
public function subscribe(string $folder): TaggedResponse;
|
||||
|
||||
/**
|
||||
* Send a "UNSUBSCRIBE" command.
|
||||
*
|
||||
* Unsubscribe from a folder.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-unsubscribe-command
|
||||
*/
|
||||
public function unsubscribe(string $folder): TaggedResponse;
|
||||
|
||||
/**
|
||||
* Send a "GETQUOTA" command.
|
||||
*
|
||||
* Retrieve quota information about a specific quota root.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9208#name-getquota
|
||||
*/
|
||||
public function quota(string $root): UntaggedResponse;
|
||||
|
||||
/**
|
||||
* Send a "GETQUOTAROOT" command.
|
||||
*
|
||||
* Retrieve quota root information about a mailbox.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9208#name-getquotaroot
|
||||
*/
|
||||
public function quotaRoot(string $mailbox): ResponseCollection;
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Connection;
|
||||
|
||||
use Stringable;
|
||||
|
||||
class ImapCommand implements Stringable
|
||||
{
|
||||
/**
|
||||
* The compiled command lines.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected ?array $compiled = null;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
protected string $tag,
|
||||
protected string $command,
|
||||
protected array $tokens = [],
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get the IMAP tag.
|
||||
*/
|
||||
public function tag(): string
|
||||
{
|
||||
return $this->tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the IMAP command.
|
||||
*/
|
||||
public function command(): string
|
||||
{
|
||||
return $this->command;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the IMAP tokens.
|
||||
*/
|
||||
public function tokens(): array
|
||||
{
|
||||
return $this->tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile the command into lines for transmission.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function compile(): array
|
||||
{
|
||||
if (is_array($this->compiled)) {
|
||||
return $this->compiled;
|
||||
}
|
||||
|
||||
$lines = [];
|
||||
|
||||
$line = trim("{$this->tag} {$this->command}");
|
||||
|
||||
foreach ($this->tokens as $token) {
|
||||
if (is_array($token)) {
|
||||
// For tokens provided as arrays, the first element is a placeholder
|
||||
// (for example, "{20}") that signals a literal value will follow.
|
||||
// The second element holds the actual literal content.
|
||||
[$placeholder, $literal] = $token;
|
||||
|
||||
$lines[] = "{$line} {$placeholder}";
|
||||
|
||||
$line = $literal;
|
||||
} else {
|
||||
$line .= " {$token}";
|
||||
}
|
||||
}
|
||||
|
||||
$lines[] = $line;
|
||||
|
||||
return $this->compiled = $lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a redacted version of the command for safe exposure.
|
||||
*/
|
||||
public function redacted(): ImapCommand
|
||||
{
|
||||
return new static($this->tag, $this->command, array_map(
|
||||
function (mixed $token) {
|
||||
return is_array($token)
|
||||
? array_map(fn () => '[redacted]', $token)
|
||||
: '[redacted]';
|
||||
}, $this->tokens)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the command as a string.
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return implode("\r\n", $this->compile());
|
||||
}
|
||||
}
|
||||
@@ -1,815 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Connection;
|
||||
|
||||
use DirectoryTree\ImapEngine\Collections\ResponseCollection;
|
||||
use DirectoryTree\ImapEngine\Connection\Loggers\LoggerInterface;
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\ContinuationResponse;
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\Data\Data;
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\Data\ListData;
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\Response;
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\TaggedResponse;
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\UntaggedResponse;
|
||||
use DirectoryTree\ImapEngine\Connection\Streams\FakeStream;
|
||||
use DirectoryTree\ImapEngine\Connection\Streams\StreamInterface;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\Token;
|
||||
use DirectoryTree\ImapEngine\Enums\ImapFetchIdentifier;
|
||||
use DirectoryTree\ImapEngine\Enums\ImapSortKey;
|
||||
use DirectoryTree\ImapEngine\Exceptions\ImapCommandException;
|
||||
use DirectoryTree\ImapEngine\Exceptions\ImapConnectionClosedException;
|
||||
use DirectoryTree\ImapEngine\Exceptions\ImapConnectionFailedException;
|
||||
use DirectoryTree\ImapEngine\Exceptions\ImapConnectionTimedOutException;
|
||||
use DirectoryTree\ImapEngine\Exceptions\ImapResponseException;
|
||||
use DirectoryTree\ImapEngine\Exceptions\ImapStreamException;
|
||||
use DirectoryTree\ImapEngine\Support\Str;
|
||||
use Exception;
|
||||
use Generator;
|
||||
use LogicException;
|
||||
use Throwable;
|
||||
|
||||
class ImapConnection implements ConnectionInterface
|
||||
{
|
||||
/**
|
||||
* Sequence number used to generate unique command tags.
|
||||
*/
|
||||
protected int $sequence = 0;
|
||||
|
||||
/**
|
||||
* The result instance.
|
||||
*/
|
||||
protected ?Result $result = null;
|
||||
|
||||
/**
|
||||
* The parser instance.
|
||||
*/
|
||||
protected ?ImapParser $parser = null;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
protected StreamInterface $stream,
|
||||
protected ?LoggerInterface $logger = null,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Create a new connection with a fake stream.
|
||||
*/
|
||||
public static function fake(array $responses = []): static
|
||||
{
|
||||
$stream = new FakeStream;
|
||||
|
||||
$stream->open();
|
||||
|
||||
$stream->feed($responses);
|
||||
|
||||
return new static($stream);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tear down the connection.
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
if (! $this->connected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
@$this->logout();
|
||||
} catch (Exception $e) {
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function connect(string $host, ?int $port = null, array $options = []): void
|
||||
{
|
||||
$transport = strtolower($options['encryption'] ?? '') ?: 'tcp';
|
||||
|
||||
if (in_array($transport, ['ssl', 'tls'])) {
|
||||
$port ??= 993;
|
||||
} else {
|
||||
$port ??= 143;
|
||||
}
|
||||
|
||||
$this->setParser(
|
||||
$this->newParser($this->stream)
|
||||
);
|
||||
|
||||
$this->stream->open(
|
||||
$transport === 'starttls' ? 'tcp' : $transport,
|
||||
$host,
|
||||
$port,
|
||||
$options['timeout'] ?? 30,
|
||||
$this->getDefaultSocketOptions(
|
||||
$transport,
|
||||
$options['proxy'] ?? [],
|
||||
$options['validate_cert'] ?? true
|
||||
)
|
||||
);
|
||||
|
||||
$this->assertNextResponse(
|
||||
fn (Response $response) => $response instanceof UntaggedResponse,
|
||||
fn (UntaggedResponse $response) => $response->type()->is('OK'),
|
||||
fn () => new ImapConnectionFailedException("Connection to $host:$port failed")
|
||||
);
|
||||
|
||||
if ($transport === 'starttls') {
|
||||
$this->startTls();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default socket options for the given transport.
|
||||
*
|
||||
* @param 'ssl'|'tls'|'starttls'|'tcp' $transport
|
||||
*/
|
||||
protected function getDefaultSocketOptions(string $transport, array $proxy = [], bool $validateCert = true): array
|
||||
{
|
||||
$options = [];
|
||||
|
||||
$key = match ($transport) {
|
||||
'ssl', 'tls' => 'ssl',
|
||||
'starttls', 'tcp' => 'tcp',
|
||||
};
|
||||
|
||||
if (in_array($transport, ['ssl', 'tls'])) {
|
||||
$options[$key] = [
|
||||
'verify_peer' => $validateCert,
|
||||
'verify_peer_name' => $validateCert,
|
||||
];
|
||||
}
|
||||
|
||||
if (! isset($proxy['socket'])) {
|
||||
return $options;
|
||||
}
|
||||
|
||||
$options[$key]['proxy'] = $proxy['socket'];
|
||||
$options[$key]['request_fulluri'] = $proxy['request_fulluri'] ?? false;
|
||||
|
||||
if (isset($proxy['username'])) {
|
||||
$auth = base64_encode($proxy['username'].':'.$proxy['password']);
|
||||
|
||||
$options[$key]['header'] = ["Proxy-Authorization: Basic $auth"];
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function disconnect(): void
|
||||
{
|
||||
$this->stream->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function connected(): bool
|
||||
{
|
||||
return $this->stream->opened();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function login(string $user, string $password): TaggedResponse
|
||||
{
|
||||
$this->send('LOGIN', Str::literal([$user, $password]), $tag);
|
||||
|
||||
return $this->assertTaggedResponse($tag, fn (TaggedResponse $response) => (
|
||||
ImapCommandException::make($this->result->command()->redacted(), $response)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function logout(): void
|
||||
{
|
||||
$this->send('LOGOUT', tag: $tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function authenticate(string $user, string $token): TaggedResponse
|
||||
{
|
||||
$this->send('AUTHENTICATE', ['XOAUTH2', Str::credentials($user, $token)], $tag);
|
||||
|
||||
return $this->assertTaggedResponse($tag, fn (TaggedResponse $response) => (
|
||||
ImapCommandException::make($this->result->command()->redacted(), $response)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function startTls(): void
|
||||
{
|
||||
$this->send('STARTTLS', tag: $tag);
|
||||
|
||||
$this->assertTaggedResponse($tag);
|
||||
|
||||
$this->stream->setSocketSetCrypto(true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function select(string $folder = 'INBOX'): ResponseCollection
|
||||
{
|
||||
return $this->examineOrSelect('SELECT', $folder);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function examine(string $folder = 'INBOX'): ResponseCollection
|
||||
{
|
||||
return $this->examineOrSelect('EXAMINE', $folder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Examine and select have the same response.
|
||||
*/
|
||||
protected function examineOrSelect(string $command = 'EXAMINE', string $folder = 'INBOX'): ResponseCollection
|
||||
{
|
||||
$this->send($command, [Str::literal($folder)], $tag);
|
||||
|
||||
$this->assertTaggedResponse($tag);
|
||||
|
||||
return $this->result->responses()->untagged();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function status(string $folder = 'INBOX', array $arguments = ['MESSAGES', 'UNSEEN', 'RECENT', 'UIDNEXT', 'UIDVALIDITY']): UntaggedResponse
|
||||
{
|
||||
$this->send('STATUS', [
|
||||
Str::literal($folder),
|
||||
Str::list($arguments),
|
||||
], $tag);
|
||||
|
||||
$this->assertTaggedResponse($tag);
|
||||
|
||||
return $this->result->responses()->untagged()->firstWhere(
|
||||
fn (UntaggedResponse $response) => $response->type()->is('STATUS')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function create(string $folder): ResponseCollection
|
||||
{
|
||||
$this->send('CREATE', [Str::literal($folder)], $tag);
|
||||
|
||||
$this->assertTaggedResponse($tag);
|
||||
|
||||
return $this->result->responses()->untagged()->filter(
|
||||
fn (UntaggedResponse $response) => $response->type()->is('LIST')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function delete(string $folder): TaggedResponse
|
||||
{
|
||||
$this->send('DELETE', [Str::literal($folder)], tag: $tag);
|
||||
|
||||
return $this->assertTaggedResponse($tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function rename(string $oldPath, string $newPath): TaggedResponse
|
||||
{
|
||||
$this->send('RENAME', Str::literal([$oldPath, $newPath]), tag: $tag);
|
||||
|
||||
return $this->assertTaggedResponse($tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function subscribe(string $folder): TaggedResponse
|
||||
{
|
||||
$this->send('SUBSCRIBE', [Str::literal($folder)], tag: $tag);
|
||||
|
||||
return $this->assertTaggedResponse($tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function unsubscribe(string $folder): TaggedResponse
|
||||
{
|
||||
$this->send('UNSUBSCRIBE', [Str::literal($folder)], tag: $tag);
|
||||
|
||||
return $this->assertTaggedResponse($tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function quota(string $root): UntaggedResponse
|
||||
{
|
||||
$this->send('GETQUOTA', [Str::literal($root)], tag: $tag);
|
||||
|
||||
$this->assertTaggedResponse($tag);
|
||||
|
||||
return $this->result->responses()->untagged()->firstOrFail(
|
||||
fn (UntaggedResponse $response) => $response->type()->is('QUOTA')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function quotaRoot(string $mailbox): ResponseCollection
|
||||
{
|
||||
$this->send('GETQUOTAROOT', [Str::literal($mailbox)], tag: $tag);
|
||||
|
||||
$this->assertTaggedResponse($tag);
|
||||
|
||||
return $this->result->responses()->untagged()->filter(
|
||||
fn (UntaggedResponse $response) => $response->type()->is('QUOTA')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function list(string $reference = '', string $folder = '*'): ResponseCollection
|
||||
{
|
||||
$this->send('LIST', Str::literal([$reference, $folder]), $tag);
|
||||
|
||||
$this->assertTaggedResponse($tag);
|
||||
|
||||
return $this->result->responses()->untagged()->filter(
|
||||
fn (UntaggedResponse $response) => $response->type()->is('LIST')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function append(string $folder, string $message, ?array $flags = null): TaggedResponse
|
||||
{
|
||||
$tokens = [];
|
||||
|
||||
$tokens[] = Str::literal($folder);
|
||||
|
||||
if ($flags) {
|
||||
$tokens[] = Str::list($flags);
|
||||
}
|
||||
|
||||
$tokens[] = Str::literal($message);
|
||||
|
||||
$this->send('APPEND', $tokens, tag: $tag);
|
||||
|
||||
return $this->assertTaggedResponse($tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function copy(string $folder, array|int $from, ?int $to = null): TaggedResponse
|
||||
{
|
||||
$this->send('UID COPY', [
|
||||
Str::set($from, $to),
|
||||
Str::literal($folder),
|
||||
], $tag);
|
||||
|
||||
return $this->assertTaggedResponse($tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function move(string $folder, array|int $from, ?int $to = null): TaggedResponse
|
||||
{
|
||||
$this->send('UID MOVE', [
|
||||
Str::set($from, $to),
|
||||
Str::literal($folder),
|
||||
], $tag);
|
||||
|
||||
return $this->assertTaggedResponse($tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function store(array|string $flags, array|int $from, ?int $to = null, ?string $mode = null, bool $silent = true, ?string $item = null): ResponseCollection
|
||||
{
|
||||
$set = Str::set($from, $to);
|
||||
|
||||
$flags = Str::list((array) $flags);
|
||||
|
||||
$item = ($mode == '-' ? '-' : '+').(is_null($item) ? 'FLAGS' : $item).($silent ? '.SILENT' : '');
|
||||
|
||||
$this->send('UID STORE', [$set, $item, $flags], tag: $tag);
|
||||
|
||||
$this->assertTaggedResponse($tag);
|
||||
|
||||
return $silent ? new ResponseCollection : $this->result->responses()->untagged()->filter(
|
||||
fn (UntaggedResponse $response) => $response->type()->is('FETCH')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function uid(int|array $ids, ImapFetchIdentifier $identifier): ResponseCollection
|
||||
{
|
||||
return $this->fetch(['UID'], (array) $ids, null, $identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function bodyText(int|array $ids, bool $peek = true): ResponseCollection
|
||||
{
|
||||
return $this->fetch([$peek ? 'BODY.PEEK[TEXT]' : 'BODY[TEXT]'], (array) $ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function bodyHeader(int|array $ids, bool $peek = true): ResponseCollection
|
||||
{
|
||||
return $this->fetch([$peek ? 'BODY.PEEK[HEADER]' : 'BODY[HEADER]'], (array) $ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the BODYSTRUCTURE for the given message(s).
|
||||
*/
|
||||
public function bodyStructure(int|array $ids): ResponseCollection
|
||||
{
|
||||
return $this->fetch(['BODYSTRUCTURE'], (array) $ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a specific part of the message BODY, such as BODY[1], BODY[1.2], etc.
|
||||
*/
|
||||
public function bodyPart(string $partIndex, int|array $ids, bool $peek = false): ResponseCollection
|
||||
{
|
||||
$part = $peek ? "BODY.PEEK[$partIndex]" : "BODY[$partIndex]";
|
||||
|
||||
return $this->fetch([$part], (array) $ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function flags(int|array $ids): ResponseCollection
|
||||
{
|
||||
return $this->fetch(['FLAGS'], (array) $ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function size(int|array $ids): ResponseCollection
|
||||
{
|
||||
return $this->fetch(['RFC822.SIZE'], (array) $ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function search(array $params): UntaggedResponse
|
||||
{
|
||||
$this->send('UID SEARCH', $params, tag: $tag);
|
||||
|
||||
$this->assertTaggedResponse($tag);
|
||||
|
||||
return $this->result->responses()->untagged()->firstOrFail(
|
||||
fn (UntaggedResponse $response) => $response->type()->is('SEARCH')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function sort(ImapSortKey $key, string $direction, array $params): UntaggedResponse
|
||||
{
|
||||
$sortCriteria = $direction === 'desc' ? "REVERSE {$key->value}" : $key->value;
|
||||
|
||||
$this->send('UID SORT', ["({$sortCriteria})", 'UTF-8', ...$params], tag: $tag);
|
||||
|
||||
$this->assertTaggedResponse($tag);
|
||||
|
||||
return $this->result->responses()->untagged()->firstOrFail(
|
||||
fn (UntaggedResponse $response) => $response->type()->is('SORT')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function capability(): UntaggedResponse
|
||||
{
|
||||
$this->send('CAPABILITY', tag: $tag);
|
||||
|
||||
$this->assertTaggedResponse($tag);
|
||||
|
||||
return $this->result->responses()->untagged()->firstOrFail(
|
||||
fn (UntaggedResponse $response) => $response->type()->is('CAPABILITY')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function id(?array $ids = null): UntaggedResponse
|
||||
{
|
||||
$token = 'NIL';
|
||||
|
||||
if (is_array($ids) && ! empty($ids)) {
|
||||
$token = '(';
|
||||
|
||||
foreach ($ids as $id) {
|
||||
$token .= '"'.Str::escape($id).'" ';
|
||||
}
|
||||
|
||||
$token = rtrim($token).')';
|
||||
}
|
||||
|
||||
$this->send('ID', [$token], tag: $tag);
|
||||
|
||||
$this->assertTaggedResponse($tag);
|
||||
|
||||
return $this->result->responses()->untagged()->firstOrFail(
|
||||
fn (UntaggedResponse $response) => $response->type()->is('ID')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function expunge(): ResponseCollection
|
||||
{
|
||||
$this->send('EXPUNGE', tag: $tag);
|
||||
|
||||
$this->assertTaggedResponse($tag);
|
||||
|
||||
return $this->result->responses()->untagged();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function noop(): TaggedResponse
|
||||
{
|
||||
$this->send('NOOP', tag: $tag);
|
||||
|
||||
return $this->assertTaggedResponse($tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function idle(int $timeout): Generator
|
||||
{
|
||||
$this->stream->setTimeout($timeout);
|
||||
|
||||
$this->send('IDLE', tag: $tag);
|
||||
|
||||
$this->assertNextResponse(
|
||||
fn (Response $response) => $response instanceof ContinuationResponse,
|
||||
fn (ContinuationResponse $response) => true,
|
||||
fn (ContinuationResponse $response) => ImapCommandException::make(new ImapCommand('', 'IDLE'), $response),
|
||||
);
|
||||
|
||||
while ($response = $this->nextReply()) {
|
||||
yield $response;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function done(): void
|
||||
{
|
||||
$this->write('DONE');
|
||||
|
||||
// After issuing a "DONE" command, the server must eventually respond with a
|
||||
// tagged response to indicate that the IDLE command has been successfully
|
||||
// terminated and the server is ready to accept further commands.
|
||||
$this->assertNextResponse(
|
||||
fn (Response $response) => $response instanceof TaggedResponse,
|
||||
fn (TaggedResponse $response) => $response->successful(),
|
||||
fn (TaggedResponse $response) => ImapCommandException::make(new ImapCommand('', 'DONE'), $response),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an IMAP command.
|
||||
*
|
||||
* @param-out string $tag
|
||||
*/
|
||||
public function send(string $name, array $tokens = [], ?string &$tag = null): void
|
||||
{
|
||||
if (! $tag) {
|
||||
$this->sequence++;
|
||||
$tag = 'TAG'.$this->sequence;
|
||||
}
|
||||
|
||||
$command = new ImapCommand($tag, $name, $tokens);
|
||||
|
||||
// After every command, we'll overwrite any previous result
|
||||
// with the new command and its responses, so that we can
|
||||
// easily access the commands responses for assertion.
|
||||
$this->setResult(new Result($command));
|
||||
|
||||
foreach ($command->compile() as $line) {
|
||||
$this->write($line);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write data to the connected stream.
|
||||
*/
|
||||
protected function write(string $data): void
|
||||
{
|
||||
if ($this->stream->fwrite($data."\r\n") === false) {
|
||||
throw new ImapStreamException('Failed to write data to stream');
|
||||
}
|
||||
|
||||
$this->logger?->sent($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch one or more items for one or more messages.
|
||||
*/
|
||||
public function fetch(array|string $items, array|int $from, mixed $to = null, ImapFetchIdentifier $identifier = ImapFetchIdentifier::Uid): ResponseCollection
|
||||
{
|
||||
$prefix = ($identifier === ImapFetchIdentifier::Uid) ? 'UID' : '';
|
||||
|
||||
$this->send(trim($prefix.' FETCH'), [
|
||||
Str::set($from, $to),
|
||||
Str::list((array) $items),
|
||||
], $tag);
|
||||
|
||||
$this->assertTaggedResponse($tag);
|
||||
|
||||
// Some IMAP servers can send unsolicited untagged responses along with fetch
|
||||
// requests. We'll need to filter these out so that we can return only the
|
||||
// responses that are relevant to the fetch command. For example:
|
||||
// >> TAG123 FETCH (UID 456 BODY[TEXT])
|
||||
// << * 123 FETCH (UID 456 BODY[TEXT] {14}\nHello, World!)
|
||||
// << * 123 FETCH (FLAGS (\Seen)) <-- Unsolicited response
|
||||
return $this->result->responses()->untagged()->filter(function (UntaggedResponse $response) use ($items, $identifier) {
|
||||
// Skip over any untagged responses that are not FETCH responses.
|
||||
// The third token should always be the list of data items.
|
||||
if (! ($data = $response->tokenAt(3)) instanceof ListData) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return match ($identifier) {
|
||||
// If we're fetching UIDs, we can check if a UID token is contained in the list.
|
||||
ImapFetchIdentifier::Uid => $data->contains('UID'),
|
||||
|
||||
// If we're fetching message numbers, we can check if the requested items are all contained in the list.
|
||||
ImapFetchIdentifier::MessageNumber => $data->contains($items),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current result instance.
|
||||
*/
|
||||
protected function setResult(Result $result): void
|
||||
{
|
||||
$this->result = $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current parser instance.
|
||||
*/
|
||||
protected function setParser(ImapParser $parser): void
|
||||
{
|
||||
$this->parser = $parser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new parser instance.
|
||||
*/
|
||||
protected function newParser(StreamInterface $stream): ImapParser
|
||||
{
|
||||
return new ImapParser($this->newTokenizer($stream));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new tokenizer instance.
|
||||
*/
|
||||
protected function newTokenizer(StreamInterface $stream): ImapTokenizer
|
||||
{
|
||||
return new ImapTokenizer($stream);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the next response is a successful tagged response.
|
||||
*/
|
||||
protected function assertTaggedResponse(string $tag, ?callable $exception = null): TaggedResponse
|
||||
{
|
||||
/** @var TaggedResponse $response */
|
||||
$response = $this->assertNextResponse(
|
||||
fn (Response $response) => (
|
||||
$response instanceof TaggedResponse && $response->tag()->is($tag)
|
||||
),
|
||||
fn (TaggedResponse $response) => (
|
||||
$response->successful()
|
||||
),
|
||||
$exception ?? fn (TaggedResponse $response) => (
|
||||
ImapCommandException::make($this->result->command(), $response)
|
||||
),
|
||||
);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the next response matches the given filter and assertion.
|
||||
*
|
||||
* @template T of Response
|
||||
*
|
||||
* @param callable(Response): bool $filter
|
||||
* @param callable(T): bool $assertion
|
||||
* @param callable(T): Throwable $exception
|
||||
* @return T
|
||||
*
|
||||
* @throws ImapResponseException
|
||||
*/
|
||||
protected function assertNextResponse(callable $filter, callable $assertion, callable $exception): Response
|
||||
{
|
||||
while ($response = $this->nextResponse($filter)) {
|
||||
if ($assertion($response)) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
throw $exception($response);
|
||||
}
|
||||
|
||||
throw new ImapResponseException('No matching response found');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next response matching the given filter.
|
||||
*
|
||||
* @template T of Response
|
||||
*
|
||||
* @param callable(T): bool $filter
|
||||
* @return T|null
|
||||
*/
|
||||
protected function nextResponse(callable $filter): ?Response
|
||||
{
|
||||
if (! $this->parser) {
|
||||
throw new LogicException('No parser instance set');
|
||||
}
|
||||
|
||||
while ($response = $this->nextReply()) {
|
||||
if (! $response instanceof Response) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->result?->addResponse($response);
|
||||
|
||||
if ($filter($response)) {
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the next reply from the stream.
|
||||
*/
|
||||
protected function nextReply(): Data|Token|Response|null
|
||||
{
|
||||
if (! $reply = $this->parser->next()) {
|
||||
$meta = $this->stream->meta();
|
||||
|
||||
throw match (true) {
|
||||
$meta['timed_out'] ?? false => new ImapConnectionTimedOutException('Stream timed out, no response'),
|
||||
$meta['eof'] ?? false => new ImapConnectionClosedException('Server closed the connection (EOF)'),
|
||||
default => new ImapConnectionFailedException('Unknown stream error. Metadata: '.json_encode($meta)),
|
||||
};
|
||||
}
|
||||
|
||||
$this->logger?->received($reply);
|
||||
|
||||
return $reply;
|
||||
}
|
||||
}
|
||||
@@ -1,270 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Connection;
|
||||
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\ContinuationResponse;
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\Data\Data;
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\Data\ListData;
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\Data\ResponseCodeData;
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\Response;
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\TaggedResponse;
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\UntaggedResponse;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\Atom;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\Crlf;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\ListClose;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\ListOpen;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\Number;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\ResponseCodeClose;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\ResponseCodeOpen;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\Token;
|
||||
use DirectoryTree\ImapEngine\Exceptions\ImapParserException;
|
||||
|
||||
class ImapParser
|
||||
{
|
||||
/**
|
||||
* The current token being parsed.
|
||||
*
|
||||
* Expected to be an associative array with keys like "type" and "value".
|
||||
*/
|
||||
protected ?Token $currentToken = null;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
protected ImapTokenizer $tokenizer
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get the next response from the tokenizer.
|
||||
*/
|
||||
public function next(): Data|Token|Response|null
|
||||
{
|
||||
// Attempt to load the first token.
|
||||
if (! $this->currentToken) {
|
||||
$this->advance();
|
||||
}
|
||||
|
||||
// No token was found, return null.
|
||||
if (! $this->currentToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If the token indicates the beginning of a list, parse it.
|
||||
if ($this->currentToken instanceof ListOpen) {
|
||||
return $this->parseList();
|
||||
}
|
||||
|
||||
// If the token is an Atom or Number, check its value for special markers.
|
||||
if ($this->currentToken instanceof Atom || $this->currentToken instanceof Number) {
|
||||
// '*' marks an untagged response.
|
||||
if ($this->currentToken->value === '*') {
|
||||
return $this->parseUntaggedResponse();
|
||||
}
|
||||
|
||||
// '+' marks a continuation response.
|
||||
if ($this->currentToken->value === '+') {
|
||||
return $this->parseContinuationResponse();
|
||||
}
|
||||
|
||||
// If it's an ATOM and not '*' or '+', it's likely a tagged response.
|
||||
return $this->parseTaggedResponse();
|
||||
}
|
||||
|
||||
return $this->parseElement();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an untagged response.
|
||||
*
|
||||
* An untagged response begins with the '*' token. It may contain
|
||||
* multiple elements, including lists and response codes.
|
||||
*/
|
||||
protected function parseUntaggedResponse(): UntaggedResponse
|
||||
{
|
||||
// Capture the initial '*' token.
|
||||
$elements[] = clone $this->currentToken;
|
||||
|
||||
$this->advance();
|
||||
|
||||
// Collect all tokens until the end-of-response marker.
|
||||
while ($this->currentToken && ! $this->currentToken instanceof Crlf) {
|
||||
$elements[] = $this->parseElement();
|
||||
}
|
||||
|
||||
// If the end-of-response marker (CRLF) is present, consume it.
|
||||
if ($this->currentToken && $this->currentToken instanceof Crlf) {
|
||||
$this->currentToken = null;
|
||||
} else {
|
||||
throw new ImapParserException('Unterminated untagged response');
|
||||
}
|
||||
|
||||
return new UntaggedResponse($elements);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a continuation response.
|
||||
*
|
||||
* A continuation response starts with a '+' token, indicating
|
||||
* that the server expects additional data from the client.
|
||||
*/
|
||||
protected function parseContinuationResponse(): ContinuationResponse
|
||||
{
|
||||
// Capture the initial '+' token.
|
||||
$elements[] = clone $this->currentToken;
|
||||
|
||||
$this->advance();
|
||||
|
||||
// Collect all tokens until the CRLF marker.
|
||||
while ($this->currentToken && ! $this->currentToken instanceof Crlf) {
|
||||
$elements[] = $this->parseElement();
|
||||
}
|
||||
|
||||
// Consume the CRLF marker if present.
|
||||
if ($this->currentToken && $this->currentToken instanceof Crlf) {
|
||||
$this->currentToken = null;
|
||||
} else {
|
||||
throw new ImapParserException('Unterminated continuation response');
|
||||
}
|
||||
|
||||
return new ContinuationResponse($elements);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a tagged response.
|
||||
*
|
||||
* A tagged response begins with a tag (which is not '*' or '+')
|
||||
* and is followed by a status and optional data.
|
||||
*/
|
||||
protected function parseTaggedResponse(): TaggedResponse
|
||||
{
|
||||
// Capture the initial TAG token.
|
||||
$tokens[] = clone $this->currentToken;
|
||||
|
||||
$this->advance();
|
||||
|
||||
// Collect tokens until the end-of-response marker is reached.
|
||||
while ($this->currentToken && ! $this->currentToken instanceof Crlf) {
|
||||
$tokens[] = $this->parseElement();
|
||||
}
|
||||
|
||||
// Consume the CRLF marker if present.
|
||||
if ($this->currentToken && $this->currentToken instanceof Crlf) {
|
||||
$this->currentToken = null;
|
||||
} else {
|
||||
throw new ImapParserException('Unterminated tagged response');
|
||||
}
|
||||
|
||||
return new TaggedResponse($tokens);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a bracket group of elements delimited by '[' and ']'.
|
||||
*
|
||||
* Bracket groups are used to represent response codes.
|
||||
*/
|
||||
protected function parseBracketGroup(): ResponseCodeData
|
||||
{
|
||||
// Consume the opening '[' token.
|
||||
$this->advance();
|
||||
|
||||
$elements = [];
|
||||
|
||||
while (
|
||||
$this->currentToken
|
||||
&& ! $this->currentToken instanceof ResponseCodeClose
|
||||
) {
|
||||
// Skip CRLF tokens that may appear inside bracket groups.
|
||||
if ($this->currentToken instanceof Crlf) {
|
||||
$this->advance();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$elements[] = $this->parseElement();
|
||||
}
|
||||
|
||||
if ($this->currentToken === null) {
|
||||
throw new ImapParserException('Unterminated bracket group in response');
|
||||
}
|
||||
|
||||
// Consume the closing ']' token.
|
||||
$this->advance();
|
||||
|
||||
return new ResponseCodeData($elements);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a list of elements delimited by '(' and ')'.
|
||||
*
|
||||
* Lists are handled recursively, as a list may contain nested lists.
|
||||
*/
|
||||
protected function parseList(): ListData
|
||||
{
|
||||
// Consume the opening '(' token.
|
||||
$this->advance();
|
||||
|
||||
$elements = [];
|
||||
|
||||
// Continue to parse elements until we find the corresponding ')'.
|
||||
while (
|
||||
$this->currentToken
|
||||
&& ! $this->currentToken instanceof ListClose
|
||||
) {
|
||||
// Skip CRLF tokens that appear inside lists (after literals).
|
||||
if ($this->currentToken instanceof Crlf) {
|
||||
$this->advance();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$elements[] = $this->parseElement();
|
||||
}
|
||||
|
||||
// If we reached the end without finding a closing ')', throw an exception.
|
||||
if ($this->currentToken === null) {
|
||||
throw new ImapParserException('Unterminated list in response');
|
||||
}
|
||||
|
||||
// Consume the closing ')' token.
|
||||
$this->advance();
|
||||
|
||||
return new ListData($elements);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a single element, which might be a list or a simple token.
|
||||
*/
|
||||
protected function parseElement(): Data|Token|null
|
||||
{
|
||||
// If there is no current token, return null.
|
||||
if ($this->currentToken === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If the token indicates the start of a list, parse it as a list.
|
||||
if ($this->currentToken instanceof ListOpen) {
|
||||
return $this->parseList();
|
||||
}
|
||||
|
||||
// If the token indicates the start of a group, parse it as a group.
|
||||
if ($this->currentToken instanceof ResponseCodeOpen) {
|
||||
return $this->parseBracketGroup();
|
||||
}
|
||||
|
||||
// Otherwise, capture the current token.
|
||||
$token = clone $this->currentToken;
|
||||
|
||||
$this->advance();
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Advance to the next token from the tokenizer.
|
||||
*/
|
||||
protected function advance(): void
|
||||
{
|
||||
$this->currentToken = $this->tokenizer->nextToken();
|
||||
}
|
||||
}
|
||||
@@ -1,510 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Connection;
|
||||
|
||||
use BackedEnum;
|
||||
use Carbon\Carbon;
|
||||
use Carbon\CarbonInterface;
|
||||
use DateTimeInterface;
|
||||
use DirectoryTree\ImapEngine\Enums\ImapSearchKey;
|
||||
use DirectoryTree\ImapEngine\Support\Str;
|
||||
|
||||
class ImapQueryBuilder
|
||||
{
|
||||
/**
|
||||
* The where conditions for the query.
|
||||
*/
|
||||
protected array $wheres = [];
|
||||
|
||||
/**
|
||||
* The date format to use for date based queries.
|
||||
*/
|
||||
protected string $dateFormat = 'd-M-Y';
|
||||
|
||||
/**
|
||||
* Add a where "ALL" clause to the query.
|
||||
*/
|
||||
public function all(): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::All);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "NEW" clause to the query.
|
||||
*/
|
||||
public function new(): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::New);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "OLD" clause to the query.
|
||||
*/
|
||||
public function old(): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::Old);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "SEEN" clause to the query.
|
||||
*/
|
||||
public function seen(): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::Seen);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "DRAFT" clause to the query.
|
||||
*/
|
||||
public function draft(): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::Draft);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "RECENT" clause to the query.
|
||||
*/
|
||||
public function recent(): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::Recent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "UNSEEN" clause to the query.
|
||||
*/
|
||||
public function unseen(): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::Unseen);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "FLAGGED" clause to the query.
|
||||
*/
|
||||
public function flagged(): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::Flagged);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "DELETED" clause to the query.
|
||||
*/
|
||||
public function deleted(): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::Deleted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "ANSWERED" clause to the query.
|
||||
*/
|
||||
public function answered(): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::Answered);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "UNDELETED" clause to the query.
|
||||
*/
|
||||
public function undeleted(): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::Undeleted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "UNFLAGGED" clause to the query.
|
||||
*/
|
||||
public function unflagged(): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::Unflagged);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "UNANSWERED" clause to the query.
|
||||
*/
|
||||
public function unanswered(): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::Unanswered);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "FROM" clause to the query.
|
||||
*/
|
||||
public function from(string $email): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::From, $email);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "TO" clause to the query.
|
||||
*/
|
||||
public function to(string $value): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::To, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "CC" clause to the query.
|
||||
*/
|
||||
public function cc(string $value): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::Cc, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "BCC" clause to the query.
|
||||
*/
|
||||
public function bcc(string $value): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::Bcc, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "BODY" clause to the query.
|
||||
*/
|
||||
public function body(string $value): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::Body, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "KEYWORD" clause to the query.
|
||||
*/
|
||||
public function keyword(string $value): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::Keyword, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "UNKEYWORD" clause to the query.
|
||||
*/
|
||||
public function unkeyword(string $value): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::Unkeyword, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "ON" clause to the query.
|
||||
*/
|
||||
public function on(mixed $date): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::On, new RawQueryValue(
|
||||
$this->parseDate($date)->format($this->dateFormat)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "SINCE" clause to the query.
|
||||
*/
|
||||
public function since(mixed $date): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::Since, new RawQueryValue(
|
||||
$this->parseDate($date)->format($this->dateFormat)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "BEFORE" clause to the query.
|
||||
*/
|
||||
public function before(mixed $value): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::Before, new RawQueryValue(
|
||||
$this->parseDate($value)->format($this->dateFormat)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "SENTON" clause to the query.
|
||||
*/
|
||||
public function sentOn(mixed $date): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::SentOn, new RawQueryValue(
|
||||
$this->parseDate($date)->format($this->dateFormat)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "SENTSINCE" clause to the query.
|
||||
*/
|
||||
public function sentSince(mixed $date): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::SentSince, new RawQueryValue(
|
||||
$this->parseDate($date)->format($this->dateFormat)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "SENTBEFORE" clause to the query.
|
||||
*/
|
||||
public function sentBefore(mixed $date): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::SentBefore, new RawQueryValue(
|
||||
$this->parseDate($date)->format($this->dateFormat)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "SUBJECT" clause to the query.
|
||||
*/
|
||||
public function subject(string $value): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::Subject, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "TEXT" clause to the query.
|
||||
*/
|
||||
public function text(string $value): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::Text, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "HEADER" clause to the query.
|
||||
*/
|
||||
public function header(string $header, string $value): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::Header->value." $header", $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "UID" clause to the query.
|
||||
*/
|
||||
public function uid(int|string|array $from, int|float|null $to = null): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::Uid, new RawQueryValue(Str::set($from, $to)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "LARGER" clause to the query.
|
||||
*/
|
||||
public function larger(int $bytes): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::Larger, new RawQueryValue($bytes));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a where "SMALLER" clause to the query.
|
||||
*/
|
||||
public function smaller(int $bytes): static
|
||||
{
|
||||
return $this->where(ImapSearchKey::Smaller, new RawQueryValue($bytes));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a "where" condition.
|
||||
*/
|
||||
public function where(mixed $column, mixed $value = null): static
|
||||
{
|
||||
if (is_callable($column)) {
|
||||
$this->addNestedCondition('AND', $column);
|
||||
} else {
|
||||
$this->addBasicCondition('AND', $column, $value);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an "or where" condition.
|
||||
*/
|
||||
public function orWhere(mixed $column, mixed $value = null): static
|
||||
{
|
||||
if (is_callable($column)) {
|
||||
$this->addNestedCondition('OR', $column);
|
||||
} else {
|
||||
$this->addBasicCondition('OR', $column, $value);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a "where not" condition.
|
||||
*/
|
||||
public function whereNot(mixed $column, mixed $value = null): static
|
||||
{
|
||||
$this->addBasicCondition('AND', $column, $value, true);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the query has any where conditions.
|
||||
*/
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return empty($this->wheres);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the instance into an IMAP-compatible query string.
|
||||
*/
|
||||
public function toImap(): string
|
||||
{
|
||||
return $this->compileWheres($this->wheres);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new query instance (like Eloquent's newQuery).
|
||||
*/
|
||||
protected function newQuery(): static
|
||||
{
|
||||
return new static;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a basic condition to the query.
|
||||
*/
|
||||
protected function addBasicCondition(string $boolean, mixed $column, mixed $value, bool $not = false): void
|
||||
{
|
||||
$value = $this->prepareWhereValue($value);
|
||||
|
||||
$column = Str::enum($column);
|
||||
|
||||
$this->wheres[] = [
|
||||
'type' => 'basic',
|
||||
'not' => $not,
|
||||
'key' => $column,
|
||||
'value' => $value,
|
||||
'boolean' => $boolean,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the where value, escaping it as needed.
|
||||
*/
|
||||
protected function prepareWhereValue(mixed $value): RawQueryValue|string|null
|
||||
{
|
||||
if (is_null($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($value instanceof RawQueryValue) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if ($value instanceof BackedEnum) {
|
||||
$value = $value->value;
|
||||
}
|
||||
|
||||
if ($value instanceof DateTimeInterface) {
|
||||
$value = Carbon::instance($value);
|
||||
}
|
||||
|
||||
if ($value instanceof CarbonInterface) {
|
||||
$value = $value->format($this->dateFormat);
|
||||
}
|
||||
|
||||
return Str::escape($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a nested condition group to the query.
|
||||
*/
|
||||
protected function addNestedCondition(string $boolean, callable $callback): void
|
||||
{
|
||||
$nested = $this->newQuery();
|
||||
|
||||
$callback($nested);
|
||||
|
||||
$this->wheres[] = [
|
||||
'type' => 'nested',
|
||||
'query' => $nested,
|
||||
'boolean' => $boolean,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to parse a date string into a Carbon instance.
|
||||
*/
|
||||
protected function parseDate(mixed $date): CarbonInterface
|
||||
{
|
||||
if ($date instanceof CarbonInterface) {
|
||||
return $date;
|
||||
}
|
||||
|
||||
return Carbon::parse($date);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a single expression node from a basic or nested where.
|
||||
*
|
||||
* @param array{type: 'basic'|'nested', boolean: 'AND'|'OR', query: ImapQueryBuilder} $where
|
||||
*/
|
||||
protected function makeExpressionNode(array $where): array
|
||||
{
|
||||
return match ($where['type']) {
|
||||
'basic' => [
|
||||
'expr' => $this->compileBasic($where),
|
||||
'boolean' => $where['boolean'],
|
||||
],
|
||||
|
||||
'nested' => [
|
||||
'expr' => $where['query']->toImap(),
|
||||
'boolean' => $where['boolean'],
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge the existing expression with the next expression, respecting the boolean operator.
|
||||
*
|
||||
* @param 'AND'|'OR' $boolean
|
||||
*/
|
||||
protected function mergeExpressions(string $existing, string $next, string $boolean): string
|
||||
{
|
||||
return match ($boolean) {
|
||||
// AND is implicit – just append.
|
||||
'AND' => $existing.' '.$next,
|
||||
|
||||
// IMAP's OR is binary; nest accordingly.
|
||||
'OR' => 'OR ('.$existing.') ('.$next.')',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively compile the wheres array into an IMAP-compatible string.
|
||||
*/
|
||||
protected function compileWheres(array $wheres): string
|
||||
{
|
||||
if (empty($wheres)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Convert each "where" into a node for later merging.
|
||||
$exprNodes = array_map(fn (array $where) => (
|
||||
$this->makeExpressionNode($where)
|
||||
), $wheres);
|
||||
|
||||
// Start with the first expression.
|
||||
$combined = array_shift($exprNodes)['expr'];
|
||||
|
||||
// Merge the rest of the expressions.
|
||||
foreach ($exprNodes as $node) {
|
||||
$combined = $this->mergeExpressions(
|
||||
$combined, $node['expr'], $node['boolean']
|
||||
);
|
||||
}
|
||||
|
||||
return trim($combined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile a basic where condition into an IMAP-compatible string.
|
||||
*/
|
||||
protected function compileBasic(array $where): string
|
||||
{
|
||||
$part = strtoupper($where['key']);
|
||||
|
||||
if ($where['value'] instanceof RawQueryValue) {
|
||||
$part .= ' '.$where['value']->value;
|
||||
} elseif ($where['value']) {
|
||||
$part .= ' "'.Str::toImapUtf7($where['value']).'"';
|
||||
}
|
||||
|
||||
if ($where['not']) {
|
||||
$part = 'NOT '.$part;
|
||||
}
|
||||
|
||||
return $part;
|
||||
}
|
||||
}
|
||||
@@ -1,511 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Connection;
|
||||
|
||||
use DirectoryTree\ImapEngine\Connection\Streams\StreamInterface;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\Atom;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\Crlf;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\EmailAddress;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\ListClose;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\ListOpen;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\Literal;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\Nil;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\Number;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\QuotedString;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\ResponseCodeClose;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\ResponseCodeOpen;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\Token;
|
||||
use DirectoryTree\ImapEngine\Exceptions\ImapParserException;
|
||||
use DirectoryTree\ImapEngine\Exceptions\ImapStreamException;
|
||||
|
||||
class ImapTokenizer
|
||||
{
|
||||
/**
|
||||
* The current position in the buffer.
|
||||
*/
|
||||
protected int $position = 0;
|
||||
|
||||
/**
|
||||
* The buffer of characters read from the stream.
|
||||
*/
|
||||
protected string $buffer = '';
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
protected StreamInterface $stream
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Returns the next token from the stream.
|
||||
*/
|
||||
public function nextToken(): ?Token
|
||||
{
|
||||
$this->skipWhitespace();
|
||||
|
||||
$this->ensureBuffer(1);
|
||||
|
||||
$char = $this->currentChar();
|
||||
|
||||
if ($char === null || $char === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check for line feed.
|
||||
if ($char === "\n") {
|
||||
// With a valid IMAP response, we should never reach this point,
|
||||
// but in case we receive a malformed response, we will flush
|
||||
// the buffer and return null to prevent an infinite loop.
|
||||
$this->flushBuffer();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check for carriage return. (\r\n)
|
||||
if ($char === "\r") {
|
||||
$this->advance(); // Consume CR
|
||||
|
||||
$this->ensureBuffer(1);
|
||||
|
||||
if ($this->currentChar() !== "\n") {
|
||||
throw new ImapParserException('Expected LF after CR');
|
||||
}
|
||||
|
||||
$this->advance(); // Consume LF (\n)
|
||||
|
||||
return new Crlf("\r\n");
|
||||
}
|
||||
|
||||
// Check for parameter list opening.
|
||||
if ($char === '(') {
|
||||
$this->advance();
|
||||
|
||||
return new ListOpen('(');
|
||||
}
|
||||
|
||||
// Check for a parameter list closing.
|
||||
if ($char === ')') {
|
||||
$this->advance();
|
||||
|
||||
return new ListClose(')');
|
||||
}
|
||||
|
||||
// Check for a response group open.
|
||||
if ($char === '[') {
|
||||
$this->advance();
|
||||
|
||||
return new ResponseCodeOpen('[');
|
||||
}
|
||||
|
||||
// Check for response group close.
|
||||
if ($char === ']') {
|
||||
$this->advance();
|
||||
|
||||
return new ResponseCodeClose(']');
|
||||
}
|
||||
|
||||
// Check for angle bracket open (email addresses).
|
||||
if ($char === '<') {
|
||||
$this->advance();
|
||||
|
||||
return $this->readEmailAddress();
|
||||
}
|
||||
|
||||
// Check for quoted string.
|
||||
if ($char === '"') {
|
||||
return $this->readQuotedString();
|
||||
}
|
||||
|
||||
// Check for literal block open.
|
||||
if ($char === '{') {
|
||||
return $this->readLiteral();
|
||||
}
|
||||
|
||||
// Otherwise, parse a number or atom.
|
||||
return $this->readNumberOrAtom();
|
||||
}
|
||||
|
||||
/**
|
||||
* Skips whitespace characters (spaces and tabs only, preserving CRLF).
|
||||
*/
|
||||
protected function skipWhitespace(): void
|
||||
{
|
||||
while (true) {
|
||||
$this->ensureBuffer(1);
|
||||
|
||||
$char = $this->currentChar();
|
||||
|
||||
// Break on EOF.
|
||||
if ($char === null || $char === '') {
|
||||
break;
|
||||
}
|
||||
|
||||
// Break on CRLF.
|
||||
if ($char === "\r" || $char === "\n") {
|
||||
break;
|
||||
}
|
||||
|
||||
// Break on non-whitespace.
|
||||
if ($char !== ' ' && $char !== "\t") {
|
||||
break;
|
||||
}
|
||||
|
||||
$this->advance();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a quoted string token.
|
||||
*
|
||||
* Quoted strings are enclosed in double quotes and may contain escaped characters.
|
||||
*/
|
||||
protected function readQuotedString(): QuotedString
|
||||
{
|
||||
// Skip the opening quote.
|
||||
$this->advance();
|
||||
|
||||
$value = '';
|
||||
|
||||
while (true) {
|
||||
$this->ensureBuffer(1);
|
||||
|
||||
$char = $this->currentChar();
|
||||
|
||||
if ($char === null) {
|
||||
throw new ImapParserException(sprintf(
|
||||
'Unterminated quoted string at buffer offset %d. Buffer: "%s"',
|
||||
$this->position,
|
||||
substr($this->buffer, max(0, $this->position - 10), 20)
|
||||
));
|
||||
}
|
||||
|
||||
if ($char === '\\') {
|
||||
$this->advance(); // Skip the backslash.
|
||||
|
||||
$this->ensureBuffer(1);
|
||||
|
||||
$escapedChar = $this->currentChar();
|
||||
|
||||
if ($escapedChar === null) {
|
||||
throw new ImapParserException('Unterminated escape sequence in quoted string');
|
||||
}
|
||||
|
||||
$value .= $escapedChar;
|
||||
|
||||
$this->advance();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($char === '"') {
|
||||
$this->advance(); // Skip the closing quote.
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
$value .= $char;
|
||||
|
||||
$this->advance();
|
||||
}
|
||||
|
||||
return new QuotedString($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a literal token.
|
||||
*
|
||||
* Literal blocks in IMAP have the form {<length>}\r\n<data>.
|
||||
*/
|
||||
protected function readLiteral(): Literal
|
||||
{
|
||||
// Skip the opening '{'.
|
||||
$this->advance();
|
||||
|
||||
// This will contain the size of the literal block in a sequence of digits.
|
||||
// {<size>}\r\n<data>
|
||||
$numStr = '';
|
||||
|
||||
while (true) {
|
||||
$this->ensureBuffer(1);
|
||||
|
||||
$char = $this->currentChar();
|
||||
|
||||
if ($char === null) {
|
||||
throw new ImapParserException('Unterminated literal specifier');
|
||||
}
|
||||
|
||||
if ($char === '}') {
|
||||
$this->advance(); // Skip the '}'.
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
$numStr .= $char;
|
||||
|
||||
$this->advance();
|
||||
}
|
||||
|
||||
// Expect carriage return after the literal specifier.
|
||||
$this->ensureBuffer(2);
|
||||
|
||||
// Get the carriage return.
|
||||
$crlf = substr($this->buffer, $this->position, 2);
|
||||
|
||||
if ($crlf !== "\r\n") {
|
||||
throw new ImapParserException('Expected CRLF after literal specifier');
|
||||
}
|
||||
|
||||
// Skip the CRLF.
|
||||
$this->advance(2);
|
||||
|
||||
$length = (int) $numStr;
|
||||
|
||||
// Use any data that is already in our buffer.
|
||||
$available = strlen($this->buffer) - $this->position;
|
||||
|
||||
if ($available >= $length) {
|
||||
$literal = substr($this->buffer, $this->position, $length);
|
||||
|
||||
$this->advance($length);
|
||||
} else {
|
||||
// Consume whatever is available without flushing the whole buffer.
|
||||
$literal = substr($this->buffer, $this->position);
|
||||
|
||||
$consumed = strlen($literal);
|
||||
|
||||
// Advance the pointer by the number of bytes we took.
|
||||
$this->advance($consumed);
|
||||
|
||||
// Calculate how many bytes are still needed.
|
||||
$remaining = $length - $consumed;
|
||||
|
||||
// Read the missing bytes from the stream.
|
||||
$data = $this->stream->read($remaining);
|
||||
|
||||
if ($data === false || strlen($data) !== $remaining) {
|
||||
throw new ImapStreamException('Unexpected end of stream while trying to fill the buffer');
|
||||
}
|
||||
|
||||
$literal .= $data;
|
||||
}
|
||||
|
||||
// Verify that the literal length matches the expected length.
|
||||
if (strlen($literal) !== $length) {
|
||||
throw new ImapParserException(sprintf(
|
||||
'Literal length mismatch: expected %d, got %d',
|
||||
$length,
|
||||
strlen($literal)
|
||||
));
|
||||
}
|
||||
|
||||
return new Literal($literal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a number or atom token.
|
||||
*/
|
||||
protected function readNumberOrAtom(): Token
|
||||
{
|
||||
$position = $this->position;
|
||||
|
||||
// First char must be a digit to even consider a number.
|
||||
if (! ctype_digit($this->buffer[$position] ?? '')) {
|
||||
return $this->readAtom();
|
||||
}
|
||||
|
||||
// Walk forward to find the end of the digit run.
|
||||
while (ctype_digit($this->buffer[$position] ?? '')) {
|
||||
$position++;
|
||||
|
||||
$this->ensureBuffer($position - $this->position + 1);
|
||||
}
|
||||
|
||||
$next = $this->buffer[$position] ?? null;
|
||||
|
||||
// If next is EOF or a delimiter, it's a Number.
|
||||
if ($next === null || $this->isDelimiter($next)) {
|
||||
return $this->readNumber();
|
||||
}
|
||||
|
||||
// Otherwise it's an Atom.
|
||||
return $this->readAtom();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a number token.
|
||||
*
|
||||
* A number consists of one or more digit characters and represents a numeric value.
|
||||
*/
|
||||
protected function readNumber(): Number
|
||||
{
|
||||
$start = $this->position;
|
||||
|
||||
while (true) {
|
||||
$this->ensureBuffer(1);
|
||||
|
||||
$char = $this->currentChar();
|
||||
|
||||
if ($char === null) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (! ctype_digit($char)) {
|
||||
break;
|
||||
}
|
||||
|
||||
$this->advance();
|
||||
}
|
||||
|
||||
return new Number(substr($this->buffer, $start, $this->position - $start));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads an atom token.
|
||||
*
|
||||
* ATOMs are sequences of printable ASCII characters that do not contain delimiters.
|
||||
*/
|
||||
protected function readAtom(): Atom
|
||||
{
|
||||
$value = '';
|
||||
|
||||
while (true) {
|
||||
$this->ensureBuffer(1);
|
||||
|
||||
$char = $this->currentChar();
|
||||
|
||||
if ($char === null) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (! $this->isValidAtomCharacter($char)) {
|
||||
break;
|
||||
}
|
||||
|
||||
$value .= $char;
|
||||
|
||||
$this->advance();
|
||||
}
|
||||
|
||||
if (strcasecmp($value, 'NIL') === 0) {
|
||||
return new Nil($value);
|
||||
}
|
||||
|
||||
return new Atom($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads an email address token enclosed in angle brackets.
|
||||
*
|
||||
* Email addresses are enclosed in angle brackets ("<" and ">").
|
||||
*
|
||||
* For example "<johndoe@email.com>"
|
||||
*/
|
||||
protected function readEmailAddress(): ?EmailAddress
|
||||
{
|
||||
$value = '';
|
||||
|
||||
while (true) {
|
||||
$this->ensureBuffer(1);
|
||||
|
||||
$char = $this->currentChar();
|
||||
|
||||
if ($char === null) {
|
||||
throw new ImapParserException('Unterminated email address, expected ">"');
|
||||
}
|
||||
|
||||
if ($char === '>') {
|
||||
$this->advance(); // Skip the closing '>'.
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
$value .= $char;
|
||||
|
||||
$this->advance();
|
||||
}
|
||||
|
||||
return new EmailAddress($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that at least the given length in characters are available in the buffer.
|
||||
*/
|
||||
protected function ensureBuffer(int $length): void
|
||||
{
|
||||
// If we have enough data in the buffer, return early.
|
||||
while ((strlen($this->buffer) - $this->position) < $length) {
|
||||
$data = $this->stream->fgets();
|
||||
|
||||
if ($data === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->buffer .= $data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current character in the buffer.
|
||||
*/
|
||||
protected function currentChar(): ?string
|
||||
{
|
||||
return $this->buffer[$this->position] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Advances the internal pointer by $n characters.
|
||||
*/
|
||||
protected function advance(int $n = 1): void
|
||||
{
|
||||
$this->position += $n;
|
||||
|
||||
// If we have consumed the entire buffer, reset it.
|
||||
if ($this->position >= strlen($this->buffer)) {
|
||||
$this->flushBuffer();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush the buffer and reset the position.
|
||||
*/
|
||||
protected function flushBuffer(): void
|
||||
{
|
||||
$this->buffer = '';
|
||||
$this->position = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the given character is a valid atom character.
|
||||
*/
|
||||
protected function isValidAtomCharacter(string $char): bool
|
||||
{
|
||||
// Get the ASCII code.
|
||||
$code = ord($char);
|
||||
|
||||
// Allow only printable ASCII (32-126).
|
||||
if ($code < 32 || $code > 126) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Delimiters are not allowed inside ATOMs.
|
||||
if ($this->isDelimiter($char)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the given character is a delimiter for tokenizing responses.
|
||||
*/
|
||||
protected function isDelimiter(string $char): bool
|
||||
{
|
||||
// This delimiter list includes additional characters (such as square
|
||||
// brackets, curly braces, and angle brackets) to ensure that tokens
|
||||
// like the response code group brackets are split out. This is fine
|
||||
// for tokenizing responses, even though it’s more restrictive
|
||||
// than the IMAP atom definition in RFC 3501 (section 9).
|
||||
return in_array($char, [' ', '(', ')', '[', ']', '{', '}', '<', '>'], true);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Connection\Loggers;
|
||||
|
||||
class EchoLogger extends Logger
|
||||
{
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function write(string $message): void
|
||||
{
|
||||
echo $message;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Connection\Loggers;
|
||||
|
||||
class FileLogger extends Logger
|
||||
{
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
protected string $path
|
||||
) {}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function write(string $message): void
|
||||
{
|
||||
file_put_contents($this->path, $message, FILE_APPEND);
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Connection\Loggers;
|
||||
|
||||
abstract class Logger implements LoggerInterface
|
||||
{
|
||||
/**
|
||||
* Write a message to the log.
|
||||
*/
|
||||
abstract protected function write(string $message): void;
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function sent(string $message): void
|
||||
{
|
||||
$this->write(sprintf('%s: >> %s', $this->date(), $message).PHP_EOL);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function received(string $message): void
|
||||
{
|
||||
$this->write(sprintf('%s: << %s', $this->date(), $message).PHP_EOL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current date and time.
|
||||
*/
|
||||
protected function date(): string
|
||||
{
|
||||
return date('Y-m-d H:i:s');
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Connection\Loggers;
|
||||
|
||||
interface LoggerInterface
|
||||
{
|
||||
/**
|
||||
* Log when a message is sent.
|
||||
*/
|
||||
public function sent(string $message): void;
|
||||
|
||||
/**
|
||||
* Log when a message is received.
|
||||
*/
|
||||
public function received(string $message): void;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Connection\Loggers;
|
||||
|
||||
class RayLogger extends Logger
|
||||
{
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function write(string $message): void
|
||||
{
|
||||
ray($message);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Connection;
|
||||
|
||||
use Stringable;
|
||||
|
||||
class RawQueryValue
|
||||
{
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly Stringable|string $value
|
||||
) {}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Connection\Responses;
|
||||
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\Token;
|
||||
|
||||
class ContinuationResponse extends Response
|
||||
{
|
||||
/**
|
||||
* Get the data tokens.
|
||||
*
|
||||
* @return Token[]
|
||||
*/
|
||||
public function data(): array
|
||||
{
|
||||
return array_slice($this->tokens, 1);
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Connection\Responses\Data;
|
||||
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\HasTokens;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\Token;
|
||||
use Stringable;
|
||||
|
||||
abstract class Data implements Stringable
|
||||
{
|
||||
use HasTokens;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
protected array $tokens
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get the tokens.
|
||||
*
|
||||
* @return Token[]|Data[]
|
||||
*/
|
||||
public function tokens(): array
|
||||
{
|
||||
return $this->tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first token.
|
||||
*/
|
||||
public function first(): Token|Data|null
|
||||
{
|
||||
return $this->tokens[0] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last token.
|
||||
*/
|
||||
public function last(): Token|Data|null
|
||||
{
|
||||
return $this->tokens[count($this->tokens) - 1] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the data contains a specific value.
|
||||
*/
|
||||
public function contains(array|string $needles): bool
|
||||
{
|
||||
$haystack = $this->values();
|
||||
|
||||
foreach ((array) $needles as $needle) {
|
||||
if (! in_array($needle, $haystack)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the token's values.
|
||||
*/
|
||||
public function values(): array
|
||||
{
|
||||
return array_map(function (Token|Data $token) {
|
||||
return $token instanceof Data
|
||||
? $token->values()
|
||||
: $token->value;
|
||||
}, $this->tokens);
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Connection\Responses\Data;
|
||||
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\Token;
|
||||
|
||||
class ListData extends Data
|
||||
{
|
||||
/**
|
||||
* Find the immediate successor token of the given field in the list.
|
||||
*/
|
||||
public function lookup(string $field): Data|Token|null
|
||||
{
|
||||
foreach ($this->tokens as $index => $token) {
|
||||
if ((string) $token === $field) {
|
||||
return $this->tokenAt(++$index);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert alternating key/value tokens to an associative array.
|
||||
*/
|
||||
public function toKeyValuePairs(): array
|
||||
{
|
||||
$pairs = [];
|
||||
|
||||
for ($i = 0; $i < count($this->tokens) - 1; $i += 2) {
|
||||
$key = strtolower($this->tokens[$i]->value);
|
||||
|
||||
$pairs[$key] = $this->tokens[$i + 1]->value;
|
||||
}
|
||||
|
||||
return $pairs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list as a string.
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return sprintf('(%s)', implode(
|
||||
' ', array_map('strval', $this->tokens)
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Connection\Responses\Data;
|
||||
|
||||
class ResponseCodeData extends Data
|
||||
{
|
||||
/**
|
||||
* Get the group as a string.
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return sprintf('[%s]', implode(
|
||||
' ', array_map('strval', $this->tokens)
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Connection\Responses;
|
||||
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\Data\Data;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\Token;
|
||||
|
||||
trait HasTokens
|
||||
{
|
||||
/**
|
||||
* Get the response tokens.
|
||||
*
|
||||
* @return Token[]|Data[]
|
||||
*/
|
||||
abstract public function tokens(): array;
|
||||
|
||||
/**
|
||||
* Get the response token at the given index.
|
||||
*/
|
||||
public function tokenAt(int $index): Token|Data|null
|
||||
{
|
||||
return $this->tokens()[$index] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the response tokens after the given index.
|
||||
*/
|
||||
public function tokensAfter(int $index): array
|
||||
{
|
||||
return array_slice($this->tokens(), $index);
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Connection\Responses;
|
||||
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\Data\ResponseCodeData;
|
||||
|
||||
class MessageResponseParser
|
||||
{
|
||||
/**
|
||||
* Get the UID from a tagged move or copy response.
|
||||
*/
|
||||
public static function getUidFromCopy(TaggedResponse $response): ?int
|
||||
{
|
||||
if (! $data = $response->tokenAt(2)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (! $data instanceof ResponseCodeData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (! $value = $data->tokenAt(3)?->value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (int) $value;
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Connection\Responses;
|
||||
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\Data\Data;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\Token;
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
use Stringable;
|
||||
|
||||
class Response implements Arrayable, Stringable
|
||||
{
|
||||
use HasTokens;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
protected array $tokens
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get the response tokens.
|
||||
*
|
||||
* @return Token[]|Data[]
|
||||
*/
|
||||
public function tokens(): array
|
||||
{
|
||||
return $this->tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the instance as an array.
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return array_map(function (Token|Data $token) {
|
||||
return $token instanceof Data
|
||||
? $token->values()
|
||||
: $token->value;
|
||||
}, $this->tokens);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a JSON representation of the response tokens.
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return implode(' ', $this->tokens);
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Connection\Responses;
|
||||
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\Atom;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\Number;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\Token;
|
||||
|
||||
class TaggedResponse extends Response
|
||||
{
|
||||
/**
|
||||
* Get the response tag.
|
||||
*/
|
||||
public function tag(): Atom|Number
|
||||
{
|
||||
return $this->tokens[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the response status token.
|
||||
*/
|
||||
public function status(): Atom
|
||||
{
|
||||
return $this->tokens[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the response data tokens.
|
||||
*
|
||||
* @return Token[]
|
||||
*/
|
||||
public function data(): array
|
||||
{
|
||||
return array_slice($this->tokens, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the response was successful.
|
||||
*/
|
||||
public function successful(): bool
|
||||
{
|
||||
return strtoupper($this->status()->value) === 'OK';
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the response failed.
|
||||
*/
|
||||
public function failed(): bool
|
||||
{
|
||||
return in_array(strtoupper($this->status()->value), ['NO', 'BAD']);
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Connection\Responses;
|
||||
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\Atom;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\Number;
|
||||
|
||||
class UntaggedResponse extends Response
|
||||
{
|
||||
/**
|
||||
* Get the response type token.
|
||||
*/
|
||||
public function type(): Atom|Number
|
||||
{
|
||||
return $this->tokens[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data tokens.
|
||||
*
|
||||
* @return Atom[]
|
||||
*/
|
||||
public function data(): array
|
||||
{
|
||||
return array_slice($this->tokens, 2);
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Connection;
|
||||
|
||||
use DirectoryTree\ImapEngine\Collections\ResponseCollection;
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\Response;
|
||||
|
||||
class Result
|
||||
{
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
protected ImapCommand $command,
|
||||
protected array $responses = [],
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get the executed command.
|
||||
*/
|
||||
public function command(): ImapCommand
|
||||
{
|
||||
return $this->command;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a response to the result.
|
||||
*/
|
||||
public function addResponse(Response $response): void
|
||||
{
|
||||
$this->responses[] = $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the recently received responses.
|
||||
*/
|
||||
public function responses(): ResponseCollection
|
||||
{
|
||||
return new ResponseCollection($this->responses);
|
||||
}
|
||||
}
|
||||
@@ -1,232 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Connection\Streams;
|
||||
|
||||
use PHPUnit\Framework\Assert;
|
||||
use RuntimeException;
|
||||
|
||||
class FakeStream implements StreamInterface
|
||||
{
|
||||
/**
|
||||
* Lines queued for testing; each call to fgets() pops the next line.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected array $buffer = [];
|
||||
|
||||
/**
|
||||
* Data that has been "written" to this fake stream (for assertion).
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected array $written = [];
|
||||
|
||||
/**
|
||||
* The connection info.
|
||||
*/
|
||||
protected ?array $connection = null;
|
||||
|
||||
/**
|
||||
* The mock meta info.
|
||||
*/
|
||||
protected array $meta = [
|
||||
'crypto' => [
|
||||
'protocol' => '',
|
||||
'cipher_name' => '',
|
||||
'cipher_bits' => 0,
|
||||
'cipher_version' => '',
|
||||
],
|
||||
'mode' => 'c',
|
||||
'eof' => false,
|
||||
'blocked' => false,
|
||||
'timed_out' => false,
|
||||
'seekable' => false,
|
||||
'unread_bytes' => 0,
|
||||
'stream_type' => 'tcp_socket/unknown',
|
||||
];
|
||||
|
||||
/**
|
||||
* Feed a line to the stream buffer with a newline character.
|
||||
*/
|
||||
public function feed(array|string $lines): self
|
||||
{
|
||||
// We'll ensure that each line ends with a CRLF,
|
||||
// as this is the expected behavior of every
|
||||
// reply that comes from an IMAP server.
|
||||
$lines = array_map(fn (string $line) => (
|
||||
rtrim($line, "\r\n")."\r\n"
|
||||
), (array) $lines);
|
||||
|
||||
array_push($this->buffer, ...$lines);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Feed a raw line to the stream buffer.
|
||||
*/
|
||||
public function feedRaw(array|string $lines): self
|
||||
{
|
||||
array_push($this->buffer, ...(array) $lines);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the timed out status.
|
||||
*/
|
||||
public function setMeta(string $attribute, mixed $value): self
|
||||
{
|
||||
if (! isset($this->meta[$attribute])) {
|
||||
throw new RuntimeException(
|
||||
"Unknown metadata attribute: {$attribute}"
|
||||
);
|
||||
}
|
||||
|
||||
if (gettype($this->meta[$attribute]) !== gettype($value)) {
|
||||
throw new RuntimeException(
|
||||
"Metadata attribute {$attribute} must be of type ".gettype($this->meta[$attribute])
|
||||
);
|
||||
}
|
||||
|
||||
$this->meta[$attribute] = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function open(?string $transport = null, ?string $host = null, ?int $port = null, ?int $timeout = null, array $options = []): bool
|
||||
{
|
||||
$this->connection = compact('transport', 'host', 'port', 'timeout', 'options');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function close(): void
|
||||
{
|
||||
$this->buffer = [];
|
||||
$this->connection = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function read(int $length): string|false
|
||||
{
|
||||
if (! $this->opened()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->meta['eof'] && empty($this->buffer)) {
|
||||
return false; // EOF and no data left. Indicate end of stream.
|
||||
}
|
||||
|
||||
$data = implode('', $this->buffer);
|
||||
|
||||
$availableLength = strlen($data);
|
||||
|
||||
if ($availableLength === 0) {
|
||||
// No data available right now (but not EOF).
|
||||
// Simulate non-blocking behavior.
|
||||
return '';
|
||||
}
|
||||
|
||||
$bytesToRead = min($length, $availableLength);
|
||||
|
||||
$result = substr($data, 0, $bytesToRead);
|
||||
|
||||
$remainingData = substr($data, $bytesToRead);
|
||||
|
||||
$this->buffer = $remainingData !== '' ? [$remainingData] : [];
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function fgets(): string|false
|
||||
{
|
||||
if (! $this->opened()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Simulate timeout/eof checks.
|
||||
if ($this->meta['timed_out'] || $this->meta['eof']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return array_shift($this->buffer) ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function fwrite(string $data): int|false
|
||||
{
|
||||
if (! $this->opened()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->written[] = $data;
|
||||
|
||||
return strlen($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function meta(): array
|
||||
{
|
||||
return $this->meta;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function opened(): bool
|
||||
{
|
||||
return (bool) $this->connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function setTimeout(int $seconds): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function setSocketSetCrypto(bool $enabled, ?int $method): bool|int
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the given data was written to the stream.
|
||||
*/
|
||||
public function assertWritten(string $string): void
|
||||
{
|
||||
$found = false;
|
||||
|
||||
foreach ($this->written as $index => $written) {
|
||||
if (str_contains($written, $string)) {
|
||||
unset($this->written[$index]);
|
||||
|
||||
$found = true;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Assert::assertTrue($found, "Failed asserting that the string '{$string}' was written to the stream.");
|
||||
}
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Connection\Streams;
|
||||
|
||||
use DirectoryTree\ImapEngine\Exceptions\ImapConnectionFailedException;
|
||||
|
||||
class ImapStream implements StreamInterface
|
||||
{
|
||||
/**
|
||||
* The underlying PHP stream resource.
|
||||
*
|
||||
* @var resource|null
|
||||
*/
|
||||
protected mixed $stream = null;
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function open(string $transport, string $host, int $port, int $timeout, array $options = []): bool
|
||||
{
|
||||
$this->stream = @stream_socket_client(
|
||||
$address = "{$transport}://{$host}:{$port}",
|
||||
$errno,
|
||||
$errstr,
|
||||
$timeout,
|
||||
STREAM_CLIENT_CONNECT,
|
||||
stream_context_create($options)
|
||||
);
|
||||
|
||||
if (! $this->stream) {
|
||||
throw new ImapConnectionFailedException("Unable to connect to {$address} ({$errstr})", $errno);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function close(): void
|
||||
{
|
||||
if ($this->opened()) {
|
||||
fclose($this->stream);
|
||||
}
|
||||
|
||||
$this->stream = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function read(int $length): string|false
|
||||
{
|
||||
if (! $this->opened()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$data = '';
|
||||
|
||||
while (strlen($data) < $length && ! feof($this->stream)) {
|
||||
$chunk = fread($this->stream, $length - strlen($data));
|
||||
|
||||
if ($chunk === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$data .= $chunk;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function fgets(): string|false
|
||||
{
|
||||
return $this->opened() ? fgets($this->stream) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function fwrite(string $data): int|false
|
||||
{
|
||||
return $this->opened() ? fwrite($this->stream, $data) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function meta(): array
|
||||
{
|
||||
return $this->opened() ? stream_get_meta_data($this->stream) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function opened(): bool
|
||||
{
|
||||
return is_resource($this->stream);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function setTimeout(int $seconds): bool
|
||||
{
|
||||
return stream_set_timeout($this->stream, $seconds);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function setSocketSetCrypto(bool $enabled, ?int $method): bool|int
|
||||
{
|
||||
return stream_socket_enable_crypto($this->stream, $enabled, $method);
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Connection\Streams;
|
||||
|
||||
interface StreamInterface
|
||||
{
|
||||
/**
|
||||
* Open the underlying stream.
|
||||
*/
|
||||
public function open(string $transport, string $host, int $port, int $timeout, array $options = []): bool;
|
||||
|
||||
/**
|
||||
* Close the underlying stream.
|
||||
*/
|
||||
public function close(): void;
|
||||
|
||||
/**
|
||||
* Read data from the stream.
|
||||
*/
|
||||
public function read(int $length): string|false;
|
||||
|
||||
/**
|
||||
* Read a single line from the stream.
|
||||
*/
|
||||
public function fgets(): string|false;
|
||||
|
||||
/**
|
||||
* Write data to the stream.
|
||||
*/
|
||||
public function fwrite(string $data): int|false;
|
||||
|
||||
/**
|
||||
* Return meta info (like stream_get_meta_data).
|
||||
*/
|
||||
public function meta(): array;
|
||||
|
||||
/**
|
||||
* Determine if the stream is open.
|
||||
*/
|
||||
public function opened(): bool;
|
||||
|
||||
/**
|
||||
* Set the timeout on the stream.
|
||||
*/
|
||||
public function setTimeout(int $seconds): bool;
|
||||
|
||||
/**
|
||||
* Set encryption state on an already connected socked.
|
||||
*/
|
||||
public function setSocketSetCrypto(bool $enabled, ?int $method): bool|int;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Connection\Tokens;
|
||||
|
||||
/**
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-atom
|
||||
*/
|
||||
class Atom extends Token {}
|
||||
@@ -1,5 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Connection\Tokens;
|
||||
|
||||
class Crlf extends Token {}
|
||||
@@ -1,14 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Connection\Tokens;
|
||||
|
||||
class EmailAddress extends Token
|
||||
{
|
||||
/**
|
||||
* Get the token's value.
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return '<'.$this->value.'>';
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Connection\Tokens;
|
||||
|
||||
class ListClose extends Token {}
|
||||
@@ -1,5 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Connection\Tokens;
|
||||
|
||||
class ListOpen extends Token {}
|
||||
@@ -1,14 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Connection\Tokens;
|
||||
|
||||
class Literal extends Token
|
||||
{
|
||||
/**
|
||||
* Get the token's value.
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return sprintf("{%d}\r\n%s", strlen($this->value), $this->value);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Connection\Tokens;
|
||||
|
||||
/**
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#name-nil
|
||||
*/
|
||||
class Nil extends Atom {}
|
||||
@@ -1,5 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Connection\Tokens;
|
||||
|
||||
class Number extends Token {}
|
||||
@@ -1,14 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Connection\Tokens;
|
||||
|
||||
class QuotedString extends Token
|
||||
{
|
||||
/**
|
||||
* Get the token's value.
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return '"'.$this->value.'"';
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Connection\Tokens;
|
||||
|
||||
class ResponseCodeClose extends Token {}
|
||||
@@ -1,5 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Connection\Tokens;
|
||||
|
||||
class ResponseCodeOpen extends Token {}
|
||||
@@ -1,39 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Connection\Tokens;
|
||||
|
||||
use Stringable;
|
||||
|
||||
abstract class Token implements Stringable
|
||||
{
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
public string $value,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Determine if the token is the given value.
|
||||
*/
|
||||
public function is(string $value): bool
|
||||
{
|
||||
return $this->value === $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the token is not the given value.
|
||||
*/
|
||||
public function isNot(string $value): bool
|
||||
{
|
||||
return ! $this->is($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the token's value.
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine;
|
||||
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\Data\ListData;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\Token;
|
||||
use DirectoryTree\ImapEngine\Enums\ContentDispositionType;
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
use JsonSerializable;
|
||||
|
||||
/**
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc2183
|
||||
*/
|
||||
class ContentDisposition implements Arrayable, JsonSerializable
|
||||
{
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
protected ContentDispositionType $type,
|
||||
protected array $parameters = [],
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Parse the disposition from tokens.
|
||||
*
|
||||
* @param array<Token|ListData> $tokens
|
||||
*/
|
||||
public static function parse(array $tokens): ?static
|
||||
{
|
||||
for ($i = 8; $i < count($tokens); $i++) {
|
||||
if (! $tokens[$i] instanceof ListData) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$innerTokens = $tokens[$i]->tokens();
|
||||
|
||||
if (! isset($innerTokens[0]) || ! $innerTokens[0] instanceof Token) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! $type = ContentDispositionType::tryFrom(strtolower($innerTokens[0]->value))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$parameters = isset($innerTokens[1]) && $innerTokens[1] instanceof ListData
|
||||
? $innerTokens[1]->toKeyValuePairs()
|
||||
: [];
|
||||
|
||||
return new self($type, $parameters);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the disposition type.
|
||||
*/
|
||||
public function type(): ContentDispositionType
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the disposition parameters.
|
||||
*/
|
||||
public function parameters(): array
|
||||
{
|
||||
return $this->parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific parameter value.
|
||||
*/
|
||||
public function parameter(string $name): ?string
|
||||
{
|
||||
return $this->parameters[strtolower($name)] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the filename parameter.
|
||||
*/
|
||||
public function filename(): ?string
|
||||
{
|
||||
return $this->parameters['filename'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if this is an attachment disposition.
|
||||
*/
|
||||
public function isAttachment(): bool
|
||||
{
|
||||
return $this->type === ContentDispositionType::Attachment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if this is an inline disposition.
|
||||
*/
|
||||
public function isInline(): bool
|
||||
{
|
||||
return $this->type === ContentDispositionType::Inline;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array representation.
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'type' => $this->type->value,
|
||||
'parameters' => $this->parameters,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the JSON representation.
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine;
|
||||
|
||||
use DateTimeInterface;
|
||||
use Stringable;
|
||||
use Symfony\Component\Mime\Email;
|
||||
|
||||
class DraftMessage implements Stringable
|
||||
{
|
||||
/**
|
||||
* The underlying Symfony Email instance.
|
||||
*/
|
||||
protected Email $message;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
protected ?string $from = null,
|
||||
protected array|string $to = [],
|
||||
protected array|string $cc = [],
|
||||
protected array|string $bcc = [],
|
||||
protected ?string $subject = null,
|
||||
protected ?string $text = null,
|
||||
protected ?string $html = null,
|
||||
protected array $headers = [],
|
||||
protected array $attachments = [],
|
||||
protected ?DateTimeInterface $date = null,
|
||||
) {
|
||||
$this->message = new Email;
|
||||
|
||||
if ($this->from) {
|
||||
$this->message->from($this->from);
|
||||
}
|
||||
|
||||
if ($this->subject) {
|
||||
$this->message->subject($this->subject);
|
||||
}
|
||||
|
||||
if ($this->text) {
|
||||
$this->message->text($this->text);
|
||||
}
|
||||
|
||||
if ($this->html) {
|
||||
$this->message->html($this->html);
|
||||
}
|
||||
|
||||
if ($this->date) {
|
||||
$this->message->date($this->date);
|
||||
}
|
||||
|
||||
if (! empty($this->to)) {
|
||||
$this->message->to(...(array) $this->to);
|
||||
}
|
||||
|
||||
if (! empty($this->cc)) {
|
||||
$this->message->cc(...(array) $this->cc);
|
||||
}
|
||||
|
||||
if (! empty($this->bcc)) {
|
||||
$this->message->bcc(...(array) $this->bcc);
|
||||
}
|
||||
|
||||
foreach ($this->attachments as $attachment) {
|
||||
match (true) {
|
||||
$attachment instanceof Attachment => $this->message->attach(
|
||||
$attachment->contents(),
|
||||
$attachment->filename(),
|
||||
$attachment->contentType()
|
||||
),
|
||||
|
||||
is_resource($attachment) => $this->message->attach($attachment),
|
||||
|
||||
default => $this->message->attachFromPath($attachment),
|
||||
};
|
||||
}
|
||||
|
||||
foreach ($this->headers as $name => $value) {
|
||||
$this->message->getHeaders()->addTextHeader($name, $value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the underlying Symfony Email instance.
|
||||
*/
|
||||
public function email(): Email
|
||||
{
|
||||
return $this->message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the email as a string.
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->message->toString();
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Enums;
|
||||
|
||||
/**
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc2183
|
||||
*/
|
||||
enum ContentDispositionType: string
|
||||
{
|
||||
case Inline = 'inline';
|
||||
case Attachment = 'attachment';
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Enums;
|
||||
|
||||
enum ImapFetchIdentifier
|
||||
{
|
||||
case Uid;
|
||||
case MessageNumber;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Enums;
|
||||
|
||||
enum ImapFlag: string
|
||||
{
|
||||
case Seen = '\Seen';
|
||||
case Draft = '\Draft';
|
||||
case Recent = '\Recent';
|
||||
case Flagged = '\Flagged';
|
||||
case Deleted = '\Deleted';
|
||||
case Answered = '\Answered';
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Enums;
|
||||
|
||||
enum ImapSearchKey: string
|
||||
{
|
||||
case Cc = 'CC';
|
||||
case On = 'ON';
|
||||
case To = 'TO';
|
||||
case All = 'ALL';
|
||||
case New = 'NEW';
|
||||
case Old = 'OLD';
|
||||
case Bcc = 'BCC';
|
||||
case Uid = 'UID';
|
||||
case Seen = 'SEEN';
|
||||
case Body = 'BODY';
|
||||
case From = 'FROM';
|
||||
case Text = 'TEXT';
|
||||
case Draft = 'DRAFT';
|
||||
case Since = 'SINCE';
|
||||
case SentOn = 'SENTON';
|
||||
case SentSince = 'SENTSINCE';
|
||||
case SentBefore = 'SENTBEFORE';
|
||||
case Recent = 'RECENT';
|
||||
case Unseen = 'UNSEEN';
|
||||
case Before = 'BEFORE';
|
||||
case Header = 'HEADER';
|
||||
case Larger = 'LARGER';
|
||||
case Deleted = 'DELETED';
|
||||
case Flagged = 'FLAGGED';
|
||||
case Keyword = 'KEYWORD';
|
||||
case Unkeyword = 'UNKEYWORD';
|
||||
case Subject = 'SUBJECT';
|
||||
case Smaller = 'SMALLER';
|
||||
case Answered = 'ANSWERED';
|
||||
case Undeleted = 'UNDELETED';
|
||||
case Unflagged = 'UNFLAGGED';
|
||||
case Unanswered = 'UNANSWERED';
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Enums;
|
||||
|
||||
enum ImapSortKey: string
|
||||
{
|
||||
case Cc = 'CC';
|
||||
case To = 'TO';
|
||||
case Date = 'DATE';
|
||||
case From = 'FROM';
|
||||
case Size = 'SIZE';
|
||||
case Arrival = 'ARRIVAL';
|
||||
case Subject = 'SUBJECT';
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Exceptions;
|
||||
|
||||
class Exception extends \Exception {}
|
||||
@@ -1,5 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Exceptions;
|
||||
|
||||
class ImapCapabilityException extends Exception {}
|
||||
@@ -1,48 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Exceptions;
|
||||
|
||||
use DirectoryTree\ImapEngine\Connection\ImapCommand;
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\Response;
|
||||
|
||||
class ImapCommandException extends Exception
|
||||
{
|
||||
/**
|
||||
* The IMAP response.
|
||||
*/
|
||||
protected Response $response;
|
||||
|
||||
/**
|
||||
* The failed IMAP command.
|
||||
*/
|
||||
protected ImapCommand $command;
|
||||
|
||||
/**
|
||||
* Make a new instance from a failed command and response.
|
||||
*/
|
||||
public static function make(ImapCommand $command, Response $response): static
|
||||
{
|
||||
$exception = new static(sprintf('IMAP command "%s" failed. Response: "%s"', $command, $response));
|
||||
|
||||
$exception->command = $command;
|
||||
$exception->response = $response;
|
||||
|
||||
return $exception;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the failed IMAP command.
|
||||
*/
|
||||
public function command(): ImapCommand
|
||||
{
|
||||
return $this->command;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the IMAP response.
|
||||
*/
|
||||
public function response(): Response
|
||||
{
|
||||
return $this->response;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Exceptions;
|
||||
|
||||
class ImapConnectionClosedException extends ImapConnectionException {}
|
||||
@@ -1,5 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Exceptions;
|
||||
|
||||
abstract class ImapConnectionException extends Exception {}
|
||||
@@ -1,5 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Exceptions;
|
||||
|
||||
class ImapConnectionFailedException extends ImapConnectionException {}
|
||||
@@ -1,5 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Exceptions;
|
||||
|
||||
class ImapConnectionTimedOutException extends ImapConnectionException {}
|
||||
@@ -1,5 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Exceptions;
|
||||
|
||||
class ImapParserException extends Exception {}
|
||||
@@ -1,5 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Exceptions;
|
||||
|
||||
class ImapResponseException extends Exception {}
|
||||
@@ -1,5 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Exceptions;
|
||||
|
||||
class ImapStreamException extends Exception {}
|
||||
@@ -1,5 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Exceptions;
|
||||
|
||||
class RuntimeException extends \RuntimeException {}
|
||||
@@ -1,99 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine;
|
||||
|
||||
use BackedEnum;
|
||||
use BadMethodCallException;
|
||||
|
||||
class FileMessage implements MessageInterface
|
||||
{
|
||||
use HasFlags, HasParsedMessage;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
protected string $contents
|
||||
) {}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function uid(): int
|
||||
{
|
||||
throw new BadMethodCallException('FileMessage does not support a UID');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function size(): ?int
|
||||
{
|
||||
return strlen($this->contents);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function flag(BackedEnum|string $flag, string $operation, bool $expunge = false): void
|
||||
{
|
||||
throw new BadMethodCallException('FileMessage does not support flagging');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the string representation of the message.
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if this message is equal to another.
|
||||
*/
|
||||
public function is(MessageInterface $message): bool
|
||||
{
|
||||
return $message instanceof self
|
||||
&& $this->contents === $message->contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message flags.
|
||||
*/
|
||||
public function flags(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function bodyStructure(): ?BodyStructureCollection
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function hasBodyStructure(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function bodyPart(string $partNumber, bool $peek = true): ?string
|
||||
{
|
||||
throw new BadMethodCallException('FileMessage does not support fetching body parts');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the message is empty.
|
||||
*/
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return empty($this->contents);
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine;
|
||||
|
||||
use BackedEnum;
|
||||
|
||||
interface FlaggableInterface
|
||||
{
|
||||
/**
|
||||
* Mark the message as read. Alias for markSeen.
|
||||
*/
|
||||
public function markRead(): void;
|
||||
|
||||
/**
|
||||
* Mark the message as unread. Alias for unmarkSeen.
|
||||
*/
|
||||
public function markUnread(): void;
|
||||
|
||||
/**
|
||||
* Mark the message as seen.
|
||||
*/
|
||||
public function markSeen(): void;
|
||||
|
||||
/**
|
||||
* Unmark the seen flag.
|
||||
*/
|
||||
public function unmarkSeen(): void;
|
||||
|
||||
/**
|
||||
* Mark the message as answered.
|
||||
*/
|
||||
public function markAnswered(): void;
|
||||
|
||||
/**
|
||||
* Unmark the answered flag.
|
||||
*/
|
||||
public function unmarkAnswered(): void;
|
||||
|
||||
/**
|
||||
* Mark the message as flagged.
|
||||
*/
|
||||
public function markFlagged(): void;
|
||||
|
||||
/**
|
||||
* Unmark the flagged flag.
|
||||
*/
|
||||
public function unmarkFlagged(): void;
|
||||
|
||||
/**
|
||||
* Mark the message as deleted.
|
||||
*/
|
||||
public function markDeleted(bool $expunge = false): void;
|
||||
|
||||
/**
|
||||
* Unmark the deleted flag.
|
||||
*/
|
||||
public function unmarkDeleted(): void;
|
||||
|
||||
/**
|
||||
* Mark the message as a draft.
|
||||
*/
|
||||
public function markDraft(): void;
|
||||
|
||||
/**
|
||||
* Unmark the draft flag.
|
||||
*/
|
||||
public function unmarkDraft(): void;
|
||||
|
||||
/**
|
||||
* Mark the message as recent.
|
||||
*/
|
||||
public function markRecent(): void;
|
||||
|
||||
/**
|
||||
* Unmark the recent flag.
|
||||
*/
|
||||
public function unmarkRecent(): void;
|
||||
|
||||
/**
|
||||
* Determine if the message is marked as seen.
|
||||
*/
|
||||
public function isSeen(): bool;
|
||||
|
||||
/**
|
||||
* Determine if the message is marked as answered.
|
||||
*/
|
||||
public function isAnswered(): bool;
|
||||
|
||||
/**
|
||||
* Determine if the message is flagged.
|
||||
*/
|
||||
public function isFlagged(): bool;
|
||||
|
||||
/**
|
||||
* Determine if the message is marked as deleted.
|
||||
*/
|
||||
public function isDeleted(): bool;
|
||||
|
||||
/**
|
||||
* Determine if the message is marked as a draft.
|
||||
*/
|
||||
public function isDraft(): bool;
|
||||
|
||||
/**
|
||||
* Determine if the message is marked as recent.
|
||||
*/
|
||||
public function isRecent(): bool;
|
||||
|
||||
/**
|
||||
* Get the message's flags.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function flags(): array;
|
||||
|
||||
/**
|
||||
* Determine if the message has the given flag.
|
||||
*/
|
||||
public function hasFlag(BackedEnum|string $flag): bool;
|
||||
|
||||
/**
|
||||
* Add or remove a flag from the message.
|
||||
*
|
||||
* @param '+'|'-' $operation
|
||||
*/
|
||||
public function flag(BackedEnum|string $flag, string $operation, bool $expunge = false): void;
|
||||
}
|
||||
@@ -1,278 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine;
|
||||
|
||||
use Closure;
|
||||
use DirectoryTree\ImapEngine\Connection\ImapQueryBuilder;
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\UntaggedResponse;
|
||||
use DirectoryTree\ImapEngine\Enums\ImapFetchIdentifier;
|
||||
use DirectoryTree\ImapEngine\Exceptions\Exception;
|
||||
use DirectoryTree\ImapEngine\Exceptions\ImapCapabilityException;
|
||||
use DirectoryTree\ImapEngine\Support\Str;
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
use Illuminate\Support\ItemNotFoundException;
|
||||
use JsonSerializable;
|
||||
|
||||
class Folder implements Arrayable, FolderInterface, JsonSerializable
|
||||
{
|
||||
use ComparesFolders;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
protected Mailbox $mailbox,
|
||||
protected string $path,
|
||||
protected array $flags = [],
|
||||
protected string $delimiter = '/',
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get the folder's mailbox.
|
||||
*/
|
||||
public function mailbox(): Mailbox
|
||||
{
|
||||
return $this->mailbox;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the folder path.
|
||||
*/
|
||||
public function path(): string
|
||||
{
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the folder flags.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function flags(): array
|
||||
{
|
||||
return $this->flags;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function delimiter(): string
|
||||
{
|
||||
return $this->delimiter;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function name(): string
|
||||
{
|
||||
return Str::fromImapUtf7(
|
||||
last(explode($this->delimiter, $this->path))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function is(FolderInterface $folder): bool
|
||||
{
|
||||
return $this->isSameFolder($this, $folder);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function messages(): MessageQuery
|
||||
{
|
||||
// Ensure the folder is selected.
|
||||
$this->select(true);
|
||||
|
||||
return new MessageQuery($this, new ImapQueryBuilder);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function idle(callable $callback, ?callable $query = null, callable|int $timeout = 300): void
|
||||
{
|
||||
if (! in_array('IDLE', $this->mailbox->capabilities())) {
|
||||
throw new ImapCapabilityException('Unable to IDLE. IMAP server does not support IDLE capability.');
|
||||
}
|
||||
|
||||
// Normalize timeout into a closure.
|
||||
if (is_callable($timeout) && ! $timeout instanceof Closure) {
|
||||
$timeout = $timeout(...);
|
||||
}
|
||||
|
||||
// The message query to use when fetching messages.
|
||||
$query ??= fn (MessageQuery $query) => $query;
|
||||
|
||||
// Fetch the message by message number.
|
||||
$fetch = fn (int $msgn) => (
|
||||
$query($this->messages())->findOrFail($msgn, ImapFetchIdentifier::MessageNumber)
|
||||
);
|
||||
|
||||
(new Idle(clone $this->mailbox, $this->path, $timeout))->await(
|
||||
function (int $msgn) use ($callback, $fetch) {
|
||||
if (! $this->mailbox->connected()) {
|
||||
$this->mailbox->connect();
|
||||
}
|
||||
|
||||
try {
|
||||
$message = $fetch($msgn);
|
||||
} catch (ItemNotFoundException) {
|
||||
// The message wasn't found. We will skip
|
||||
// it and continue awaiting new messages.
|
||||
return;
|
||||
} catch (Exception) {
|
||||
// Something else happened. We will attempt
|
||||
// reconnecting and re-fetching the message.
|
||||
$this->mailbox->reconnect();
|
||||
|
||||
$message = $fetch($msgn);
|
||||
}
|
||||
|
||||
$callback($message);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function poll(callable $callback, ?callable $query = null, callable|int $frequency = 60): void
|
||||
{
|
||||
(new Poll(clone $this->mailbox, $this->path, $frequency))->start(
|
||||
function (MessageInterface $message) use ($callback) {
|
||||
if (! $this->mailbox->connected()) {
|
||||
$this->mailbox->connect();
|
||||
}
|
||||
|
||||
try {
|
||||
$callback($message);
|
||||
} catch (Exception) {
|
||||
// Something unexpected happened. We will attempt
|
||||
// reconnecting and continue polling for messages.
|
||||
$this->mailbox->reconnect();
|
||||
}
|
||||
},
|
||||
$query ?? fn (MessageQuery $query) => $query
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function move(string $newPath): void
|
||||
{
|
||||
$this->mailbox->connection()->rename($this->path, $newPath);
|
||||
|
||||
$this->path = $newPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function select(bool $force = false): void
|
||||
{
|
||||
$this->mailbox->select($this, $force);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function quota(): array
|
||||
{
|
||||
if (! in_array('QUOTA', $this->mailbox->capabilities())) {
|
||||
throw new ImapCapabilityException(
|
||||
'Unable to fetch mailbox quotas. IMAP server does not support QUOTA capability.'
|
||||
);
|
||||
}
|
||||
|
||||
$responses = $this->mailbox->connection()->quotaRoot($this->path);
|
||||
|
||||
$values = [];
|
||||
|
||||
foreach ($responses as $response) {
|
||||
$resource = $response->tokenAt(2);
|
||||
|
||||
$tokens = $response->tokenAt(3)->tokens();
|
||||
|
||||
for ($i = 0; $i + 2 < count($tokens); $i += 3) {
|
||||
$values[$resource->value][$tokens[$i]->value] = [
|
||||
'usage' => (int) $tokens[$i + 1]->value,
|
||||
'limit' => (int) $tokens[$i + 2]->value,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function status(): array
|
||||
{
|
||||
$response = $this->mailbox->connection()->status($this->path);
|
||||
|
||||
$tokens = $response->tokenAt(3)->tokens();
|
||||
|
||||
$values = [];
|
||||
|
||||
// Tokens are expected to alternate between keys and values.
|
||||
for ($i = 0; $i < count($tokens); $i += 2) {
|
||||
$values[$tokens[$i]->value] = $tokens[$i + 1]->value;
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function examine(): array
|
||||
{
|
||||
return $this->mailbox->connection()->examine($this->path)->map(
|
||||
fn (UntaggedResponse $response) => $response->toArray()
|
||||
)->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function expunge(): array
|
||||
{
|
||||
return $this->mailbox->connection()->expunge()->map(
|
||||
fn (UntaggedResponse $response) => $response->tokenAt(1)->value
|
||||
)->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function delete(): void
|
||||
{
|
||||
$this->mailbox->connection()->delete($this->path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array representation of the folder.
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'path' => $this->path,
|
||||
'flags' => $this->flags,
|
||||
'delimiter' => $this->delimiter,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the JSON representation of the folder.
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine;
|
||||
|
||||
interface FolderInterface
|
||||
{
|
||||
/**
|
||||
* Get the folder's mailbox.
|
||||
*/
|
||||
public function mailbox(): MailboxInterface;
|
||||
|
||||
/**
|
||||
* Get the folder path.
|
||||
*/
|
||||
public function path(): string;
|
||||
|
||||
/**
|
||||
* Get the folder flags.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function flags(): array;
|
||||
|
||||
/**
|
||||
* Get the folder delimiter.
|
||||
*/
|
||||
public function delimiter(): string;
|
||||
|
||||
/**
|
||||
* Get the folder name.
|
||||
*/
|
||||
public function name(): string;
|
||||
|
||||
/**
|
||||
* Determine if the current folder is the same as the given.
|
||||
*/
|
||||
public function is(FolderInterface $folder): bool;
|
||||
|
||||
/**
|
||||
* Begin querying for messages.
|
||||
*/
|
||||
public function messages(): MessageQueryInterface;
|
||||
|
||||
/**
|
||||
* Begin idling on the current folder for the given timeout in seconds.
|
||||
*/
|
||||
public function idle(callable $callback, ?callable $query = null, callable|int $timeout = 300): void;
|
||||
|
||||
/**
|
||||
* Begin polling for new messages at the given frequency in seconds.
|
||||
*/
|
||||
public function poll(callable $callback, ?callable $query = null, callable|int $frequency = 60): void;
|
||||
|
||||
/**
|
||||
* Move or rename the current folder.
|
||||
*/
|
||||
public function move(string $newPath): void;
|
||||
|
||||
/**
|
||||
* Select the current folder.
|
||||
*/
|
||||
public function select(bool $force = false): void;
|
||||
|
||||
/**
|
||||
* Get the folder's quotas.
|
||||
*/
|
||||
public function quota(): array;
|
||||
|
||||
/**
|
||||
* Get the folder's status.
|
||||
*/
|
||||
public function status(): array;
|
||||
|
||||
/**
|
||||
* Examine the current folder and get detailed status information.
|
||||
*/
|
||||
public function examine(): array;
|
||||
|
||||
/**
|
||||
* Expunge the mailbox and return the expunged message sequence numbers.
|
||||
*/
|
||||
public function expunge(): array;
|
||||
|
||||
/**
|
||||
* Delete the current folder.
|
||||
*/
|
||||
public function delete(): void;
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine;
|
||||
|
||||
use DirectoryTree\ImapEngine\Collections\FolderCollection;
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\UntaggedResponse;
|
||||
use DirectoryTree\ImapEngine\Support\Str;
|
||||
|
||||
class FolderRepository implements FolderRepositoryInterface
|
||||
{
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
protected Mailbox $mailbox
|
||||
) {}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function find(string $path): ?FolderInterface
|
||||
{
|
||||
return $this->get($path)->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function findOrFail(string $path): FolderInterface
|
||||
{
|
||||
return $this->get($path)->firstOrFail();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function create(string $path): FolderInterface
|
||||
{
|
||||
$this->mailbox->connection()->create(
|
||||
Str::toImapUtf7($path)
|
||||
);
|
||||
|
||||
return $this->find($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function firstOrCreate(string $path): FolderInterface
|
||||
{
|
||||
return $this->find($path) ?? $this->create($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function get(?string $match = '*', ?string $reference = ''): FolderCollection
|
||||
{
|
||||
return $this->mailbox->connection()->list($reference, Str::toImapUtf7($match))->map(
|
||||
fn (UntaggedResponse $response) => new Folder(
|
||||
mailbox: $this->mailbox,
|
||||
path: $response->tokenAt(4)->value,
|
||||
flags: $response->tokenAt(2)->values(),
|
||||
delimiter: $response->tokenAt(3)->value,
|
||||
)
|
||||
)->pipeInto(FolderCollection::class);
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine;
|
||||
|
||||
use DirectoryTree\ImapEngine\Collections\FolderCollection;
|
||||
|
||||
interface FolderRepositoryInterface
|
||||
{
|
||||
/**
|
||||
* Find a folder.
|
||||
*/
|
||||
public function find(string $path): ?FolderInterface;
|
||||
|
||||
/**
|
||||
* Find a folder or throw an exception.
|
||||
*/
|
||||
public function findOrFail(string $path): FolderInterface;
|
||||
|
||||
/**
|
||||
* Create a new folder.
|
||||
*/
|
||||
public function create(string $path): FolderInterface;
|
||||
|
||||
/**
|
||||
* Find or create a folder.
|
||||
*/
|
||||
public function firstOrCreate(string $path): FolderInterface;
|
||||
|
||||
/**
|
||||
* Get the mailboxes folders.
|
||||
*/
|
||||
public function get(?string $match = '*', ?string $reference = ''): FolderCollection;
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine;
|
||||
|
||||
use BackedEnum;
|
||||
use DirectoryTree\ImapEngine\Enums\ImapFlag;
|
||||
use DirectoryTree\ImapEngine\Support\Str;
|
||||
|
||||
trait HasFlags
|
||||
{
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function markRead(): void
|
||||
{
|
||||
$this->markSeen();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function markUnread(): void
|
||||
{
|
||||
$this->unmarkSeen();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function markSeen(): void
|
||||
{
|
||||
$this->flag(ImapFlag::Seen, '+');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function unmarkSeen(): void
|
||||
{
|
||||
$this->flag(ImapFlag::Seen, '-');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function markAnswered(): void
|
||||
{
|
||||
$this->flag(ImapFlag::Answered, '+');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function unmarkAnswered(): void
|
||||
{
|
||||
$this->flag(ImapFlag::Answered, '-');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function markFlagged(): void
|
||||
{
|
||||
$this->flag(ImapFlag::Flagged, '+');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function unmarkFlagged(): void
|
||||
{
|
||||
$this->flag(ImapFlag::Flagged, '-');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function markDeleted(bool $expunge = false): void
|
||||
{
|
||||
$this->flag(ImapFlag::Deleted, '+', $expunge);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function unmarkDeleted(): void
|
||||
{
|
||||
$this->flag(ImapFlag::Deleted, '-');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function markDraft(): void
|
||||
{
|
||||
$this->flag(ImapFlag::Draft, '+');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function unmarkDraft(): void
|
||||
{
|
||||
$this->flag(ImapFlag::Draft, '-');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function markRecent(): void
|
||||
{
|
||||
$this->flag(ImapFlag::Recent, '+');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function unmarkRecent(): void
|
||||
{
|
||||
$this->flag(ImapFlag::Recent, '-');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function isSeen(): bool
|
||||
{
|
||||
return $this->hasFlag(ImapFlag::Seen);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function isAnswered(): bool
|
||||
{
|
||||
return $this->hasFlag(ImapFlag::Answered);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function isFlagged(): bool
|
||||
{
|
||||
return $this->hasFlag(ImapFlag::Flagged);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function isDeleted(): bool
|
||||
{
|
||||
return $this->hasFlag(ImapFlag::Deleted);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function isDraft(): bool
|
||||
{
|
||||
return $this->hasFlag(ImapFlag::Draft);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function isRecent(): bool
|
||||
{
|
||||
return $this->hasFlag(ImapFlag::Recent);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function hasFlag(BackedEnum|string $flag): bool
|
||||
{
|
||||
return in_array(Str::enum($flag), $this->flags());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
abstract public function flags(): array;
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
abstract public function flag(BackedEnum|string $flag, string $operation, bool $expunge = false): void;
|
||||
}
|
||||
@@ -1,258 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Carbon\CarbonInterface;
|
||||
use DirectoryTree\ImapEngine\Exceptions\RuntimeException;
|
||||
use GuzzleHttp\Psr7\Utils;
|
||||
use ZBateson\MailMimeParser\Header\DateHeader;
|
||||
use ZBateson\MailMimeParser\Header\HeaderConsts;
|
||||
use ZBateson\MailMimeParser\Header\IHeader;
|
||||
use ZBateson\MailMimeParser\Header\IHeaderPart;
|
||||
use ZBateson\MailMimeParser\Header\Part\AddressPart;
|
||||
use ZBateson\MailMimeParser\Header\Part\ContainerPart;
|
||||
use ZBateson\MailMimeParser\Header\Part\NameValuePart;
|
||||
use ZBateson\MailMimeParser\IMessage;
|
||||
use ZBateson\MailMimeParser\Message\IMessagePart;
|
||||
|
||||
trait HasParsedMessage
|
||||
{
|
||||
/**
|
||||
* The parsed message.
|
||||
*/
|
||||
protected ?IMessage $parsed = null;
|
||||
|
||||
/**
|
||||
* Get the message date and time.
|
||||
*/
|
||||
public function date(): ?CarbonInterface
|
||||
{
|
||||
if (! $header = $this->header(HeaderConsts::DATE)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (! $header instanceof DateHeader) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (! $date = $header->getDateTime()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Carbon::instance($date);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message's message-id.
|
||||
*/
|
||||
public function messageId(): ?string
|
||||
{
|
||||
return $this->header(HeaderConsts::MESSAGE_ID)?->getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message's subject.
|
||||
*/
|
||||
public function subject(): ?string
|
||||
{
|
||||
return $this->header(HeaderConsts::SUBJECT)?->getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the FROM address.
|
||||
*/
|
||||
public function from(): ?Address
|
||||
{
|
||||
return head($this->addresses(HeaderConsts::FROM)) ?: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the SENDER address.
|
||||
*/
|
||||
public function sender(): ?Address
|
||||
{
|
||||
return head($this->addresses(HeaderConsts::SENDER)) ?: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the REPLY-TO address.
|
||||
*/
|
||||
public function replyTo(): ?Address
|
||||
{
|
||||
return head($this->addresses(HeaderConsts::REPLY_TO)) ?: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the IN-REPLY-TO message identifier(s).
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function inReplyTo(): array
|
||||
{
|
||||
$parts = $this->header(HeaderConsts::IN_REPLY_TO)?->getParts() ?? [];
|
||||
|
||||
$values = array_map(function (IHeaderPart $part) {
|
||||
return $part->getValue();
|
||||
}, $parts);
|
||||
|
||||
return array_values(array_filter($values));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the TO addresses.
|
||||
*
|
||||
* @return Address[]
|
||||
*/
|
||||
public function to(): array
|
||||
{
|
||||
return $this->addresses(HeaderConsts::TO);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the CC addresses.
|
||||
*
|
||||
* @return Address[]
|
||||
*/
|
||||
public function cc(): array
|
||||
{
|
||||
return $this->addresses(HeaderConsts::CC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the BCC addresses.
|
||||
*
|
||||
* @return Address[]
|
||||
*/
|
||||
public function bcc(): array
|
||||
{
|
||||
return $this->addresses(HeaderConsts::BCC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message's attachments.
|
||||
*
|
||||
* @return Attachment[]
|
||||
*/
|
||||
public function attachments(): array
|
||||
{
|
||||
$attachments = [];
|
||||
|
||||
foreach ($this->parse()->getAllAttachmentParts() as $part) {
|
||||
if ($this->isForwardedMessage($part)) {
|
||||
$message = new FileMessage($part->getContent());
|
||||
|
||||
$attachments = array_merge($attachments, $message->attachments());
|
||||
} else {
|
||||
$attachments[] = new Attachment(
|
||||
$part->getFilename(),
|
||||
$part->getContentId(),
|
||||
$part->getContentType(),
|
||||
$part->getContentDisposition(),
|
||||
$part->getBinaryContentStream() ?? Utils::streamFor(''),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $attachments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the message has attachments.
|
||||
*/
|
||||
public function hasAttachments(): bool
|
||||
{
|
||||
return $this->attachmentCount() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the count of attachments.
|
||||
*/
|
||||
public function attachmentCount(): int
|
||||
{
|
||||
return $this->parse()->getAttachmentCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the attachment should be treated as an embedded forwarded message.
|
||||
*/
|
||||
protected function isForwardedMessage(IMessagePart $part): bool
|
||||
{
|
||||
return empty($part->getFilename())
|
||||
&& strtolower((string) $part->getContentType()) === 'message/rfc822'
|
||||
&& strtolower((string) $part->getContentDisposition()) !== 'attachment';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get addresses from the given header.
|
||||
*
|
||||
* @return Address[]
|
||||
*/
|
||||
public function addresses(string $header): array
|
||||
{
|
||||
$parts = $this->header($header)?->getParts() ?? [];
|
||||
|
||||
$addresses = array_map(fn (IHeaderPart $part) => match (true) {
|
||||
$part instanceof AddressPart => new Address($part->getEmail(), $part->getName()),
|
||||
$part instanceof NameValuePart => new Address($part->getName(), $part->getValue()),
|
||||
$part instanceof ContainerPart => new Address($part->getValue(), ''),
|
||||
default => null,
|
||||
}, $parts);
|
||||
|
||||
return array_filter($addresses);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message's HTML content.
|
||||
*/
|
||||
public function html(): ?string
|
||||
{
|
||||
return $this->parse()->getHtmlContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message's text content.
|
||||
*/
|
||||
public function text(): ?string
|
||||
{
|
||||
return $this->parse()->getTextContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all headers from the message.
|
||||
*/
|
||||
public function headers(): array
|
||||
{
|
||||
return $this->parse()->getAllHeaders();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a header from the message.
|
||||
*/
|
||||
public function header(string $name, int $offset = 0): ?IHeader
|
||||
{
|
||||
return $this->parse()->getHeader($name, $offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the message into a MailMimeMessage instance.
|
||||
*/
|
||||
public function parse(): IMessage
|
||||
{
|
||||
if ($this->isEmpty()) {
|
||||
throw new RuntimeException('Cannot parse an empty message');
|
||||
}
|
||||
|
||||
return $this->parsed ??= MessageParser::parse((string) $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the message is empty.
|
||||
*/
|
||||
abstract public function isEmpty(): bool;
|
||||
|
||||
/**
|
||||
* Get the string representation of the message.
|
||||
*/
|
||||
abstract public function __toString(): string;
|
||||
}
|
||||
173
plugins/vendor/directorytree/imapengine/src/Idle.php
vendored
173
plugins/vendor/directorytree/imapengine/src/Idle.php
vendored
@@ -1,173 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Carbon\CarbonInterface;
|
||||
use Closure;
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\UntaggedResponse;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\Atom;
|
||||
use DirectoryTree\ImapEngine\Exceptions\Exception;
|
||||
use DirectoryTree\ImapEngine\Exceptions\ImapConnectionClosedException;
|
||||
use DirectoryTree\ImapEngine\Exceptions\ImapConnectionTimedOutException;
|
||||
use Generator;
|
||||
|
||||
class Idle
|
||||
{
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
protected Mailbox $mailbox,
|
||||
protected string $folder,
|
||||
protected Closure|int $timeout,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Destructor.
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
$this->disconnect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Await new messages on the connection.
|
||||
*/
|
||||
public function await(callable $callback): void
|
||||
{
|
||||
$this->connect();
|
||||
|
||||
while ($ttl = $this->getNextTimeout()) {
|
||||
try {
|
||||
$this->listen($callback, $ttl);
|
||||
} catch (ImapConnectionTimedOutException) {
|
||||
$this->restart();
|
||||
} catch (ImapConnectionClosedException) {
|
||||
$this->reconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start listening for new messages using the idle() generator.
|
||||
*/
|
||||
protected function listen(callable $callback, CarbonInterface $ttl): void
|
||||
{
|
||||
// Iterate over responses yielded by the idle generator.
|
||||
foreach ($this->idle($ttl) as $response) {
|
||||
if (! $response instanceof UntaggedResponse) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! $token = $response->tokenAt(2)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($token instanceof Atom && $token->is('EXISTS')) {
|
||||
$msgn = (int) $response->tokenAt(1)->value;
|
||||
|
||||
$callback($msgn);
|
||||
|
||||
$ttl = $this->getNextTimeout();
|
||||
}
|
||||
|
||||
if ($ttl === false) {
|
||||
break;
|
||||
}
|
||||
|
||||
// If we've been idle too long, break out to restart the session.
|
||||
if (Carbon::now()->greaterThanOrEqualTo($ttl)) {
|
||||
$this->restart();
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the folder to idle.
|
||||
*/
|
||||
protected function folder(): FolderInterface
|
||||
{
|
||||
return $this->mailbox->folders()->findOrFail($this->folder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Issue a done command and restart the idle session.
|
||||
*/
|
||||
protected function restart(): void
|
||||
{
|
||||
try {
|
||||
// Send DONE to terminate the current IDLE session gracefully.
|
||||
$this->done();
|
||||
} catch (Exception) {
|
||||
$this->reconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconnect the client and restart the idle session.
|
||||
*/
|
||||
protected function reconnect(): void
|
||||
{
|
||||
$this->mailbox->disconnect();
|
||||
|
||||
$this->connect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect the client and select the folder to idle.
|
||||
*/
|
||||
protected function connect(): void
|
||||
{
|
||||
$this->mailbox->connect();
|
||||
|
||||
$this->mailbox->select($this->folder(), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect the client.
|
||||
*/
|
||||
protected function disconnect(): void
|
||||
{
|
||||
try {
|
||||
// Attempt to terminate IDLE gracefully.
|
||||
$this->done();
|
||||
} catch (Exception) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
$this->mailbox->disconnect();
|
||||
}
|
||||
|
||||
/**
|
||||
* End the current IDLE session.
|
||||
*/
|
||||
protected function done(): void
|
||||
{
|
||||
$this->mailbox->connection()->done();
|
||||
}
|
||||
|
||||
/**
|
||||
* Begin a new IDLE session as a generator.
|
||||
*/
|
||||
protected function idle(CarbonInterface $ttl): Generator
|
||||
{
|
||||
yield from $this->mailbox->connection()->idle(
|
||||
(int) Carbon::now()->diffInSeconds($ttl, true)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next timeout as a Carbon instance.
|
||||
*/
|
||||
protected function getNextTimeout(): CarbonInterface|false
|
||||
{
|
||||
if (is_numeric($seconds = value($this->timeout))) {
|
||||
return Carbon::now()->addSeconds(abs($seconds));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,232 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine;
|
||||
|
||||
use DirectoryTree\ImapEngine\Connection\ConnectionInterface;
|
||||
use DirectoryTree\ImapEngine\Connection\ImapConnection;
|
||||
use DirectoryTree\ImapEngine\Connection\Loggers\EchoLogger;
|
||||
use DirectoryTree\ImapEngine\Connection\Loggers\FileLogger;
|
||||
use DirectoryTree\ImapEngine\Connection\Streams\ImapStream;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\Token;
|
||||
use Exception;
|
||||
|
||||
class Mailbox implements MailboxInterface
|
||||
{
|
||||
/**
|
||||
* The mailbox configuration.
|
||||
*/
|
||||
protected array $config = [
|
||||
'port' => 993,
|
||||
'host' => '',
|
||||
'timeout' => 30,
|
||||
'debug' => false,
|
||||
'username' => '',
|
||||
'password' => '',
|
||||
'encryption' => 'ssl',
|
||||
'validate_cert' => true,
|
||||
'authentication' => 'plain',
|
||||
'proxy' => [
|
||||
'socket' => null,
|
||||
'username' => null,
|
||||
'password' => null,
|
||||
'request_fulluri' => false,
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* The cached mailbox capabilities.
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc9051#section-6.1.1
|
||||
*/
|
||||
protected ?array $capabilities = null;
|
||||
|
||||
/**
|
||||
* The currently selected folder.
|
||||
*/
|
||||
protected ?FolderInterface $selected = null;
|
||||
|
||||
/**
|
||||
* The mailbox connection.
|
||||
*/
|
||||
protected ?ConnectionInterface $connection = null;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(array $config = [])
|
||||
{
|
||||
$this->config = array_merge($this->config, $config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the cloned instance.
|
||||
*/
|
||||
public function __clone(): void
|
||||
{
|
||||
$this->connection = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a new mailbox instance.
|
||||
*/
|
||||
public static function make(array $config = []): static
|
||||
{
|
||||
return new static($config);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function config(?string $key = null, mixed $default = null): mixed
|
||||
{
|
||||
if (is_null($key)) {
|
||||
return $this->config;
|
||||
}
|
||||
|
||||
return data_get($this->config, $key, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function connection(): ConnectionInterface
|
||||
{
|
||||
if (! $this->connection) {
|
||||
$this->connect();
|
||||
}
|
||||
|
||||
return $this->connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function connected(): bool
|
||||
{
|
||||
return (bool) $this->connection?->connected();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function reconnect(): void
|
||||
{
|
||||
$this->disconnect();
|
||||
|
||||
$this->connect();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function connect(?ConnectionInterface $connection = null): void
|
||||
{
|
||||
if ($this->connected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$debug = $this->config('debug');
|
||||
|
||||
$this->connection = $connection ?? new ImapConnection(new ImapStream, match (true) {
|
||||
class_exists($debug) => new $debug,
|
||||
is_string($debug) => new FileLogger($debug),
|
||||
is_bool($debug) && $debug => new EchoLogger,
|
||||
default => null,
|
||||
});
|
||||
|
||||
$this->connection->connect($this->config('host'), $this->config('port'), [
|
||||
'proxy' => $this->config('proxy'),
|
||||
'debug' => $this->config('debug'),
|
||||
'timeout' => $this->config('timeout'),
|
||||
'encryption' => $this->config('encryption'),
|
||||
'validate_cert' => $this->config('validate_cert'),
|
||||
]);
|
||||
|
||||
$this->authenticate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate the current session.
|
||||
*/
|
||||
protected function authenticate(): void
|
||||
{
|
||||
if ($this->config('authentication') === 'oauth') {
|
||||
$this->connection->authenticate(
|
||||
$this->config('username'),
|
||||
$this->config('password')
|
||||
);
|
||||
} else {
|
||||
$this->connection->login(
|
||||
$this->config('username'),
|
||||
$this->config('password'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function disconnect(): void
|
||||
{
|
||||
try {
|
||||
$this->connection?->logout();
|
||||
$this->connection?->disconnect();
|
||||
} catch (Exception) {
|
||||
// Do nothing.
|
||||
} finally {
|
||||
$this->connection = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function inbox(): FolderInterface
|
||||
{
|
||||
// "INBOX" is a special name reserved for the user's primary mailbox.
|
||||
// See: https://datatracker.ietf.org/doc/html/rfc9051#section-5.1
|
||||
return $this->folders()->find('INBOX');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function folders(): FolderRepositoryInterface
|
||||
{
|
||||
// Ensure the connection is established.
|
||||
$this->connection();
|
||||
|
||||
return new FolderRepository($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function capabilities(): array
|
||||
{
|
||||
return $this->capabilities ??= array_map(
|
||||
fn (Token $token) => $token->value,
|
||||
$this->connection()->capability()->tokensAfter(2)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function select(FolderInterface $folder, bool $force = false): void
|
||||
{
|
||||
if (! $this->selected($folder) || $force) {
|
||||
$this->connection()->select($folder->path());
|
||||
}
|
||||
|
||||
$this->selected = $folder;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function selected(FolderInterface $folder): bool
|
||||
{
|
||||
return $this->selected?->is($folder) ?? false;
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine;
|
||||
|
||||
use DirectoryTree\ImapEngine\Connection\ConnectionInterface;
|
||||
|
||||
interface MailboxInterface
|
||||
{
|
||||
/**
|
||||
* Get mailbox configuration values.
|
||||
*/
|
||||
public function config(?string $key = null, mixed $default = null): mixed;
|
||||
|
||||
/**
|
||||
* Get the mailbox connection.
|
||||
*/
|
||||
public function connection(): ConnectionInterface;
|
||||
|
||||
/**
|
||||
* Determine if connection was established.
|
||||
*/
|
||||
public function connected(): bool;
|
||||
|
||||
/**
|
||||
* Force a reconnection to the server.
|
||||
*/
|
||||
public function reconnect(): void;
|
||||
|
||||
/**
|
||||
* Connect to the server.
|
||||
*/
|
||||
public function connect(?ConnectionInterface $connection = null): void;
|
||||
|
||||
/**
|
||||
* Disconnect from server.
|
||||
*/
|
||||
public function disconnect(): void;
|
||||
|
||||
/**
|
||||
* Get the mailbox's inbox folder.
|
||||
*/
|
||||
public function inbox(): FolderInterface;
|
||||
|
||||
/**
|
||||
* Begin querying for mailbox folders.
|
||||
*/
|
||||
public function folders(): FolderRepositoryInterface;
|
||||
|
||||
/**
|
||||
* Get the mailbox's capabilities.
|
||||
*/
|
||||
public function capabilities(): array;
|
||||
|
||||
/**
|
||||
* Select the given folder.
|
||||
*/
|
||||
public function select(FolderInterface $folder, bool $force = false): void;
|
||||
|
||||
/**
|
||||
* Determine if the given folder is selected.
|
||||
*/
|
||||
public function selected(FolderInterface $folder): bool;
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine;
|
||||
|
||||
use DirectoryTree\ImapEngine\Exceptions\RuntimeException;
|
||||
use Generator;
|
||||
|
||||
class Mbox
|
||||
{
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
protected string $filepath
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get the messages from the mbox file.
|
||||
*/
|
||||
public function messages(
|
||||
string $delimiter = '/^From\s+\S+\s+' // From
|
||||
.'(Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s+' // Day
|
||||
.'(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+' // Month
|
||||
.'\d{1,2}\s+\d{2}:\d{2}:\d{2}' // Time (HH:MM:SS)
|
||||
.'(?:\s+[+-]\d{4})?' // Optional Timezone ("+0000")
|
||||
.'\s+\d{4}/' // Year
|
||||
): Generator {
|
||||
if (! $handle = fopen($this->filepath, 'r')) {
|
||||
throw new RuntimeException('Failed to open mbox file: '.$this->filepath);
|
||||
}
|
||||
|
||||
$buffer = '';
|
||||
|
||||
while (($line = fgets($handle)) !== false) {
|
||||
if (preg_match($delimiter, $line) && $buffer !== '') {
|
||||
yield new FileMessage($buffer);
|
||||
|
||||
$buffer = '';
|
||||
}
|
||||
|
||||
$buffer .= $line;
|
||||
}
|
||||
|
||||
if ($buffer !== '') {
|
||||
yield new FileMessage($buffer);
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
}
|
||||
}
|
||||
@@ -1,299 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine;
|
||||
|
||||
use BackedEnum;
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\Data\ListData;
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\MessageResponseParser;
|
||||
use DirectoryTree\ImapEngine\Exceptions\ImapCapabilityException;
|
||||
use DirectoryTree\ImapEngine\Support\Str;
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
use JsonSerializable;
|
||||
|
||||
class Message implements Arrayable, JsonSerializable, MessageInterface
|
||||
{
|
||||
use HasFlags, HasParsedMessage;
|
||||
|
||||
/**
|
||||
* The parsed body structure.
|
||||
*/
|
||||
protected ?BodyStructureCollection $bodyStructure = null;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
protected FolderInterface $folder,
|
||||
protected int $uid,
|
||||
protected array $flags,
|
||||
protected string $head,
|
||||
protected string $body,
|
||||
protected ?int $size = null,
|
||||
protected ?ListData $bodyStructureData = null,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get the names of properties that should be serialized.
|
||||
*/
|
||||
public function __sleep(): array
|
||||
{
|
||||
// We don't want to serialize the parsed message.
|
||||
return ['folder', 'uid', 'flags', 'head', 'body', 'size'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message's folder.
|
||||
*/
|
||||
public function folder(): FolderInterface
|
||||
{
|
||||
return $this->folder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message's identifier.
|
||||
*/
|
||||
public function uid(): int
|
||||
{
|
||||
return $this->uid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message's size in bytes (RFC822.SIZE).
|
||||
*/
|
||||
public function size(): ?int
|
||||
{
|
||||
return $this->size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message's flags.
|
||||
*/
|
||||
public function flags(): array
|
||||
{
|
||||
return $this->flags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message's raw headers.
|
||||
*/
|
||||
public function head(): string
|
||||
{
|
||||
return $this->head;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the message has headers.
|
||||
*/
|
||||
public function hasHead(): bool
|
||||
{
|
||||
return ! empty($this->head);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message's raw body.
|
||||
*/
|
||||
public function body(): string
|
||||
{
|
||||
return $this->body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the message has contents.
|
||||
*/
|
||||
public function hasBody(): bool
|
||||
{
|
||||
return ! empty($this->body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message's body structure.
|
||||
*/
|
||||
public function bodyStructure(): ?BodyStructureCollection
|
||||
{
|
||||
if ($this->bodyStructure) {
|
||||
return $this->bodyStructure;
|
||||
}
|
||||
|
||||
if (! $tokens = $this->bodyStructureData?->tokens()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If the first token is a list, it's a multipart message.
|
||||
return $this->bodyStructure = head($tokens) instanceof ListData
|
||||
? BodyStructureCollection::fromListData($this->bodyStructureData)
|
||||
: new BodyStructureCollection(parts: [BodyStructurePart::fromListData($this->bodyStructureData)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the message has body structure data.
|
||||
*/
|
||||
public function hasBodyStructure(): bool
|
||||
{
|
||||
return (bool) $this->bodyStructureData;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function is(MessageInterface $message): bool
|
||||
{
|
||||
return $message instanceof self
|
||||
&& $this->uid === $message->uid
|
||||
&& $this->folder->is($message->folder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add or remove a flag from the message.
|
||||
*/
|
||||
public function flag(BackedEnum|string $flag, string $operation, bool $expunge = false): void
|
||||
{
|
||||
$flag = Str::enum($flag);
|
||||
|
||||
$this->folder->mailbox()
|
||||
->connection()
|
||||
->store($flag, $this->uid, mode: $operation);
|
||||
|
||||
if ($expunge) {
|
||||
$this->folder->expunge();
|
||||
}
|
||||
|
||||
$this->flags = match ($operation) {
|
||||
'+' => array_unique(array_merge($this->flags, [$flag])),
|
||||
'-' => array_diff($this->flags, [$flag]),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the message to the given folder.
|
||||
*/
|
||||
public function copy(string $folder): ?int
|
||||
{
|
||||
$mailbox = $this->folder->mailbox();
|
||||
|
||||
$capabilities = $mailbox->capabilities();
|
||||
|
||||
if (! in_array('UIDPLUS', $capabilities)) {
|
||||
throw new ImapCapabilityException(
|
||||
'Unable to copy message. IMAP server does not support UIDPLUS capability'
|
||||
);
|
||||
}
|
||||
|
||||
$response = $mailbox->connection()->copy($folder, $this->uid);
|
||||
|
||||
return MessageResponseParser::getUidFromCopy($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the message to the given folder.
|
||||
*
|
||||
* @throws ImapCapabilityException
|
||||
*/
|
||||
public function move(string $folder, bool $expunge = false): ?int
|
||||
{
|
||||
$mailbox = $this->folder->mailbox();
|
||||
|
||||
$capabilities = $mailbox->capabilities();
|
||||
|
||||
switch (true) {
|
||||
case in_array('MOVE', $capabilities):
|
||||
$response = $mailbox->connection()->move($folder, $this->uid);
|
||||
|
||||
if ($expunge) {
|
||||
$this->folder->expunge();
|
||||
}
|
||||
|
||||
return MessageResponseParser::getUidFromCopy($response);
|
||||
|
||||
case in_array('UIDPLUS', $capabilities):
|
||||
$uid = $this->copy($folder);
|
||||
|
||||
$this->delete($expunge);
|
||||
|
||||
return $uid;
|
||||
|
||||
default:
|
||||
throw new ImapCapabilityException(
|
||||
'Unable to move message. IMAP server does not support MOVE or UIDPLUS capabilities'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a specific body part by part number.
|
||||
*/
|
||||
public function bodyPart(string $partNumber, bool $peek = true): ?string
|
||||
{
|
||||
$response = $this->folder->mailbox()
|
||||
->connection()
|
||||
->bodyPart($partNumber, $this->uid, $peek);
|
||||
|
||||
if ($response->isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = $response->first()->tokenAt(3);
|
||||
|
||||
if (! $data instanceof ListData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $data->lookup("[$partNumber]")?->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the message.
|
||||
*/
|
||||
public function delete(bool $expunge = false): void
|
||||
{
|
||||
$this->markDeleted($expunge);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the message.
|
||||
*/
|
||||
public function restore(): void
|
||||
{
|
||||
$this->unmarkDeleted();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array representation of the message.
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'uid' => $this->uid,
|
||||
'flags' => $this->flags,
|
||||
'head' => $this->head,
|
||||
'body' => $this->body,
|
||||
'size' => $this->size,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the string representation of the message.
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return implode("\r\n\r\n", array_filter([
|
||||
rtrim($this->head),
|
||||
ltrim($this->body),
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the JSON representation of the message.
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the message is empty.
|
||||
*/
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return ! $this->hasHead() && ! $this->hasBody();
|
||||
}
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine;
|
||||
|
||||
use Carbon\CarbonInterface;
|
||||
use Stringable;
|
||||
use ZBateson\MailMimeParser\Header\IHeader;
|
||||
use ZBateson\MailMimeParser\IMessage;
|
||||
use ZBateson\MailMimeParser\Message as MailMimeMessage;
|
||||
|
||||
interface MessageInterface extends FlaggableInterface, Stringable
|
||||
{
|
||||
/**
|
||||
* Get the message's identifier.
|
||||
*/
|
||||
public function uid(): int;
|
||||
|
||||
/**
|
||||
* Get the message's size in bytes (RFC822.SIZE).
|
||||
*/
|
||||
public function size(): ?int;
|
||||
|
||||
/**
|
||||
* Get the message date and time.
|
||||
*/
|
||||
public function date(): ?CarbonInterface;
|
||||
|
||||
/**
|
||||
* Get the message's subject.
|
||||
*/
|
||||
public function subject(): ?string;
|
||||
|
||||
/**
|
||||
* Get the 'From' address.
|
||||
*/
|
||||
public function from(): ?Address;
|
||||
|
||||
/**
|
||||
* Get the 'Sender' address.
|
||||
*/
|
||||
public function sender(): ?Address;
|
||||
|
||||
/**
|
||||
* Get the message's 'Message-ID'.
|
||||
*/
|
||||
public function messageId(): ?string;
|
||||
|
||||
/**
|
||||
* Get the 'Reply-To' address.
|
||||
*/
|
||||
public function replyTo(): ?Address;
|
||||
|
||||
/**
|
||||
* Get the 'In-Reply-To' message identifier(s).
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function inReplyTo(): array;
|
||||
|
||||
/**
|
||||
* Get the 'To' addresses.
|
||||
*
|
||||
* @return Address[]
|
||||
*/
|
||||
public function to(): array;
|
||||
|
||||
/**
|
||||
* Get the 'CC' addresses.
|
||||
*
|
||||
* @return Address[]
|
||||
*/
|
||||
public function cc(): array;
|
||||
|
||||
/**
|
||||
* Get the 'BCC' addresses.
|
||||
*
|
||||
* @return Address[]
|
||||
*/
|
||||
public function bcc(): array;
|
||||
|
||||
/**
|
||||
* Get the message's attachments.
|
||||
*
|
||||
* @return Attachment[]
|
||||
*/
|
||||
public function attachments(): array;
|
||||
|
||||
/**
|
||||
* Determine if the message has attachments.
|
||||
*/
|
||||
public function hasAttachments(): bool;
|
||||
|
||||
/**
|
||||
* Get the count of attachments.
|
||||
*/
|
||||
public function attachmentCount(): int;
|
||||
|
||||
/**
|
||||
* Get addresses from the given header.
|
||||
*
|
||||
* @return Address[]
|
||||
*/
|
||||
public function addresses(string $header): array;
|
||||
|
||||
/**
|
||||
* Get the message's HTML content.
|
||||
*/
|
||||
public function html(): ?string;
|
||||
|
||||
/**
|
||||
* Get the message's text content.
|
||||
*/
|
||||
public function text(): ?string;
|
||||
|
||||
/**
|
||||
* Get all headers from the message.
|
||||
*/
|
||||
public function headers(): array;
|
||||
|
||||
/**
|
||||
* Get a header from the message.
|
||||
*/
|
||||
public function header(string $name, int $offset = 0): ?IHeader;
|
||||
|
||||
/**
|
||||
* Parse the message into a MailMimeMessage instance.
|
||||
*/
|
||||
public function parse(): IMessage;
|
||||
|
||||
/**
|
||||
* Get the message's body structure.
|
||||
*/
|
||||
public function bodyStructure(): ?BodyStructureCollection;
|
||||
|
||||
/**
|
||||
* Determine if the message has body structure data.
|
||||
*/
|
||||
public function hasBodyStructure(): bool;
|
||||
|
||||
/**
|
||||
* Fetch a specific body part by part number.
|
||||
*/
|
||||
public function bodyPart(string $partNumber, bool $peek = true): ?string;
|
||||
|
||||
/**
|
||||
* Determine if the message is the same as another message.
|
||||
*/
|
||||
public function is(MessageInterface $message): bool;
|
||||
|
||||
/**
|
||||
* Get the string representation of the message.
|
||||
*/
|
||||
public function __toString(): string;
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine;
|
||||
|
||||
use ZBateson\MailMimeParser\IMessage;
|
||||
use ZBateson\MailMimeParser\MailMimeParser;
|
||||
|
||||
class MessageParser
|
||||
{
|
||||
/**
|
||||
* The mail mime parser instance.
|
||||
*/
|
||||
protected static ?MailMimeParser $parser = null;
|
||||
|
||||
/**
|
||||
* Parse the given message contents.
|
||||
*/
|
||||
public static function parse(string $contents): IMessage
|
||||
{
|
||||
return static::parser()->parse($contents, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mail mime parser instance.
|
||||
*/
|
||||
protected static function parser(): MailMimeParser
|
||||
{
|
||||
return static::$parser ??= new MailMimeParser;
|
||||
}
|
||||
}
|
||||
@@ -1,521 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine;
|
||||
|
||||
use BackedEnum;
|
||||
use DirectoryTree\ImapEngine\Collections\MessageCollection;
|
||||
use DirectoryTree\ImapEngine\Collections\ResponseCollection;
|
||||
use DirectoryTree\ImapEngine\Connection\ConnectionInterface;
|
||||
use DirectoryTree\ImapEngine\Connection\ImapQueryBuilder;
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\Data\ListData;
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\UntaggedResponse;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\Token;
|
||||
use DirectoryTree\ImapEngine\Enums\ImapFetchIdentifier;
|
||||
use DirectoryTree\ImapEngine\Enums\ImapFlag;
|
||||
use DirectoryTree\ImapEngine\Exceptions\ImapCapabilityException;
|
||||
use DirectoryTree\ImapEngine\Exceptions\ImapCommandException;
|
||||
use DirectoryTree\ImapEngine\Exceptions\RuntimeException;
|
||||
use DirectoryTree\ImapEngine\Pagination\LengthAwarePaginator;
|
||||
use DirectoryTree\ImapEngine\Support\Str;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\ItemNotFoundException;
|
||||
|
||||
/**
|
||||
* @mixin \DirectoryTree\ImapEngine\Connection\ImapQueryBuilder
|
||||
*/
|
||||
class MessageQuery implements MessageQueryInterface
|
||||
{
|
||||
use QueriesMessages;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
protected FolderInterface $folder,
|
||||
protected ImapQueryBuilder $query,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Count all available messages matching the current search criteria.
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
return $this->search()->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first message in the resulting collection.
|
||||
*/
|
||||
public function first(): ?MessageInterface
|
||||
{
|
||||
try {
|
||||
return $this->firstOrFail();
|
||||
} catch (ItemNotFoundException) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first message in the resulting collection or throw an exception.
|
||||
*/
|
||||
public function firstOrFail(): MessageInterface
|
||||
{
|
||||
return $this->limit(1)->get()->firstOrFail();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the messages matching the current query.
|
||||
*/
|
||||
public function get(): MessageCollection
|
||||
{
|
||||
return $this->process($this->sortKey ? $this->sort() : $this->search());
|
||||
}
|
||||
|
||||
/**
|
||||
* Append a new message to the folder.
|
||||
*/
|
||||
public function append(string $message, mixed $flags = null): int
|
||||
{
|
||||
$response = $this->connection()->append(
|
||||
$this->folder->path(), $message, (array) Str::enums($flags),
|
||||
);
|
||||
|
||||
return (int) $response // TAG4 OK [APPENDUID <uidvalidity> <uid>] APPEND completed.
|
||||
->tokenAt(2) // [APPENDUID <uidvalidity> <uid>]
|
||||
->tokenAt(2) // <uid>
|
||||
->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a callback over each message via a chunked query.
|
||||
*/
|
||||
public function each(callable $callback, int $chunkSize = 10, int $startChunk = 1): void
|
||||
{
|
||||
$this->chunk(function (MessageCollection $messages) use ($callback) {
|
||||
foreach ($messages as $key => $message) {
|
||||
if ($callback($message, $key) === false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}, $chunkSize, $startChunk);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a callback over each chunk of messages.
|
||||
*/
|
||||
public function chunk(callable $callback, int $chunkSize = 10, int $startChunk = 1): void
|
||||
{
|
||||
$startChunk = max($startChunk, 1);
|
||||
$chunkSize = max($chunkSize, 1);
|
||||
|
||||
// Get all search result tokens once.
|
||||
$messages = $this->search();
|
||||
|
||||
// Calculate how many chunks there are
|
||||
$totalChunks = (int) ceil($messages->count() / $chunkSize);
|
||||
|
||||
// If startChunk is beyond our total chunks, return early.
|
||||
if ($startChunk > $totalChunks) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Save previous state to restore later.
|
||||
$previousLimit = $this->limit;
|
||||
$previousPage = $this->page;
|
||||
|
||||
$this->limit = $chunkSize;
|
||||
|
||||
// Iterate from the starting chunk to the last chunk.
|
||||
for ($page = $startChunk; $page <= $totalChunks; $page++) {
|
||||
$this->page = $page;
|
||||
|
||||
// populate() will use $this->page to slice the results.
|
||||
$hydrated = $this->populate($messages);
|
||||
|
||||
// If no messages are returned, break out to prevent infinite loop.
|
||||
if ($hydrated->isEmpty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
// If the callback returns false, break out.
|
||||
if ($callback($hydrated, $page) === false) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Restore the original state.
|
||||
$this->limit = $previousLimit;
|
||||
$this->page = $previousPage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Paginate the current query.
|
||||
*/
|
||||
public function paginate(int $perPage = 5, $page = null, string $pageName = 'page'): LengthAwarePaginator
|
||||
{
|
||||
if (is_null($page) && isset($_GET[$pageName]) && $_GET[$pageName] > 0) {
|
||||
$this->page = intval($_GET[$pageName]);
|
||||
} elseif ($page > 0) {
|
||||
$this->page = (int) $page;
|
||||
}
|
||||
|
||||
$this->limit = $perPage;
|
||||
|
||||
return $this->get()->paginate($perPage, $this->page, $pageName, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a message by the given identifier type or throw an exception.
|
||||
*/
|
||||
public function findOrFail(int $id, ImapFetchIdentifier $identifier = ImapFetchIdentifier::Uid): MessageInterface
|
||||
{
|
||||
/** @var UntaggedResponse $response */
|
||||
$response = $this->id($id, $identifier)->firstOrFail();
|
||||
|
||||
$uid = $response->tokenAt(3) // ListData
|
||||
->tokenAt(1) // Atom
|
||||
->value; // UID
|
||||
|
||||
return $this->process(new MessageCollection([$uid]))->firstOrFail();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a message by the given identifier type.
|
||||
*/
|
||||
public function find(int $id, ImapFetchIdentifier $identifier = ImapFetchIdentifier::Uid): ?MessageInterface
|
||||
{
|
||||
$response = $this->id($id, $identifier)->first();
|
||||
|
||||
if (! $response instanceof UntaggedResponse) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$uid = $response->tokenAt(3) // ListData
|
||||
->tokenAt(1) // Atom
|
||||
->value; // UID
|
||||
|
||||
return $this->process(new MessageCollection([$uid]))->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the given messages.
|
||||
*/
|
||||
public function destroy(array|int $uids, bool $expunge = false): void
|
||||
{
|
||||
$uids = (array) $uids;
|
||||
|
||||
$this->folder->mailbox()
|
||||
->connection()
|
||||
->store([ImapFlag::Deleted->value], $uids, mode: '+');
|
||||
|
||||
if ($expunge) {
|
||||
$this->folder->expunge();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function flag(BackedEnum|string $flag, string $operation, bool $expunge = false): int
|
||||
{
|
||||
$uids = $this->search()->all();
|
||||
|
||||
if (empty($uids)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$this->connection()->store(
|
||||
(array) Str::enums($flag),
|
||||
$uids,
|
||||
mode: $operation
|
||||
);
|
||||
|
||||
if ($expunge) {
|
||||
$this->folder->expunge();
|
||||
}
|
||||
|
||||
return count($uids);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function markRead(): int
|
||||
{
|
||||
return $this->flag(ImapFlag::Seen, '+');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function markUnread(): int
|
||||
{
|
||||
return $this->flag(ImapFlag::Seen, '-');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function markFlagged(): int
|
||||
{
|
||||
return $this->flag(ImapFlag::Flagged, '+');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function unmarkFlagged(): int
|
||||
{
|
||||
return $this->flag(ImapFlag::Flagged, '-');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function delete(bool $expunge = false): int
|
||||
{
|
||||
return $this->flag(ImapFlag::Deleted, '+', $expunge);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function move(string $folder, bool $expunge = false): int
|
||||
{
|
||||
$uids = $this->search()->all();
|
||||
|
||||
if (empty($uids)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$this->connection()->move($folder, $uids);
|
||||
|
||||
if ($expunge) {
|
||||
$this->folder->expunge();
|
||||
}
|
||||
|
||||
return count($uids);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function copy(string $folder): int
|
||||
{
|
||||
$uids = $this->search()->all();
|
||||
|
||||
if (empty($uids)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$this->connection()->copy($folder, $uids);
|
||||
|
||||
return count($uids);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the collection of messages.
|
||||
*/
|
||||
protected function process(Collection $messages): MessageCollection
|
||||
{
|
||||
if ($messages->isNotEmpty()) {
|
||||
return $this->populate($messages);
|
||||
}
|
||||
|
||||
return MessageCollection::make();
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate a given id collection and receive a fully fetched message collection.
|
||||
*/
|
||||
protected function populate(Collection $uids): MessageCollection
|
||||
{
|
||||
$messages = MessageCollection::make();
|
||||
|
||||
$messages->total($uids->count());
|
||||
|
||||
foreach ($this->fetch($uids) as $uid => $response) {
|
||||
$messages->push(
|
||||
$this->newMessage(
|
||||
$uid,
|
||||
$response['flags'] ?? [],
|
||||
$response['head'] ?? '',
|
||||
$response['body'] ?? '',
|
||||
$response['size'] ?? null,
|
||||
$response['bodystructure'] ?? null,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return $messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a given id collection.
|
||||
*/
|
||||
protected function fetch(Collection $messages): array
|
||||
{
|
||||
// Only apply client-side sorting when not using server-side sorting.
|
||||
// When sortKey is set, the IMAP SORT command already returns UIDs
|
||||
// in the correct order, so we should preserve that order.
|
||||
if (! $this->sortKey) {
|
||||
$messages = match ($this->fetchOrder) {
|
||||
'asc' => $messages->sort(SORT_NUMERIC),
|
||||
'desc' => $messages->sortDesc(SORT_NUMERIC),
|
||||
};
|
||||
}
|
||||
|
||||
$uids = $messages->forPage($this->page, $this->limit)->values();
|
||||
|
||||
$fetch = [];
|
||||
|
||||
if ($this->fetchFlags) {
|
||||
$fetch[] = 'FLAGS';
|
||||
}
|
||||
|
||||
if ($this->fetchSize) {
|
||||
$fetch[] = 'RFC822.SIZE';
|
||||
}
|
||||
|
||||
if ($this->fetchHeaders) {
|
||||
$fetch[] = $this->fetchAsUnread
|
||||
? 'BODY.PEEK[HEADER]'
|
||||
: 'BODY[HEADER]';
|
||||
}
|
||||
|
||||
if ($this->fetchBody) {
|
||||
$fetch[] = $this->fetchAsUnread
|
||||
? 'BODY.PEEK[TEXT]'
|
||||
: 'BODY[TEXT]';
|
||||
}
|
||||
|
||||
if ($this->fetchBodyStructure) {
|
||||
$fetch[] = 'BODYSTRUCTURE';
|
||||
}
|
||||
|
||||
if (empty($fetch)) {
|
||||
return $uids->mapWithKeys(fn (string|int $uid) => [
|
||||
$uid => [
|
||||
'size' => null,
|
||||
'flags' => [],
|
||||
'head' => '',
|
||||
'body' => '',
|
||||
'bodystructure' => null,
|
||||
],
|
||||
])->all();
|
||||
}
|
||||
|
||||
return $this->connection()->fetch($fetch, $uids->all())->mapWithKeys(function (UntaggedResponse $response) {
|
||||
$data = $response->tokenAt(3);
|
||||
|
||||
if (! $data instanceof ListData) {
|
||||
throw new RuntimeException(sprintf(
|
||||
'Expected instance of %s at index 3 in FETCH response, got %s',
|
||||
ListData::class,
|
||||
get_debug_type($data)
|
||||
));
|
||||
}
|
||||
|
||||
$uid = $data->lookup('UID')->value;
|
||||
|
||||
$size = $data->lookup('RFC822.SIZE')?->value;
|
||||
|
||||
return [
|
||||
$uid => [
|
||||
'size' => $size ? (int) $size : null,
|
||||
'flags' => $data->lookup('FLAGS')?->values() ?? [],
|
||||
'head' => $data->lookup('[HEADER]')->value ?? '',
|
||||
'body' => $data->lookup('[TEXT]')->value ?? '',
|
||||
'bodystructure' => $data->lookup('BODYSTRUCTURE'),
|
||||
],
|
||||
];
|
||||
})->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an IMAP search request.
|
||||
*/
|
||||
protected function search(): Collection
|
||||
{
|
||||
// If the query is empty, default to fetching all.
|
||||
if ($this->query->isEmpty()) {
|
||||
$this->query->all();
|
||||
}
|
||||
|
||||
$response = $this->connection()->search([
|
||||
$this->query->toImap(),
|
||||
]);
|
||||
|
||||
return new Collection(array_map(
|
||||
fn (Token $token) => $token->value,
|
||||
$response->tokensAfter(2)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an IMAP UID SORT request using RFC 5256.
|
||||
*/
|
||||
protected function sort(): Collection
|
||||
{
|
||||
if (! in_array('SORT', $this->folder->mailbox()->capabilities())) {
|
||||
throw new ImapCapabilityException(
|
||||
'Unable to sort messages. IMAP server does not support SORT capability.'
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->query->isEmpty()) {
|
||||
$this->query->all();
|
||||
}
|
||||
|
||||
$response = $this->connection()->sort(
|
||||
$this->sortKey,
|
||||
$this->sortDirection,
|
||||
[$this->query->toImap()]
|
||||
);
|
||||
|
||||
return new Collection(array_map(
|
||||
fn (Token $token) => $token->value,
|
||||
$response->tokensAfter(2)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the UID for the given identifier.
|
||||
*/
|
||||
protected function id(int $id, ImapFetchIdentifier $identifier = ImapFetchIdentifier::Uid): ResponseCollection
|
||||
{
|
||||
try {
|
||||
return $this->connection()->uid([$id], $identifier);
|
||||
} catch (ImapCommandException $e) {
|
||||
// IMAP servers may return an error if the message number is not found.
|
||||
// If the identifier being used is a message number, and the message
|
||||
// number is in the command tokens, we can assume this has occurred
|
||||
// and safely ignore the error and return an empty collection.
|
||||
if (
|
||||
$identifier === ImapFetchIdentifier::MessageNumber
|
||||
&& in_array($id, $e->command()->tokens())
|
||||
) {
|
||||
return ResponseCollection::make();
|
||||
}
|
||||
|
||||
// Otherwise, re-throw the exception.
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a new message from given raw components.
|
||||
*/
|
||||
protected function newMessage(int $uid, array $flags, string $head, string $body, ?int $size = null, ?ListData $bodystructure = null): Message
|
||||
{
|
||||
return new Message($this->folder, $uid, $flags, $head, $body, $size, $bodystructure);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the connection instance.
|
||||
*/
|
||||
protected function connection(): ConnectionInterface
|
||||
{
|
||||
return $this->folder->mailbox()->connection();
|
||||
}
|
||||
}
|
||||
@@ -1,297 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine;
|
||||
|
||||
use BackedEnum;
|
||||
use DirectoryTree\ImapEngine\Collections\MessageCollection;
|
||||
use DirectoryTree\ImapEngine\Enums\ImapFetchIdentifier;
|
||||
use DirectoryTree\ImapEngine\Enums\ImapSortKey;
|
||||
use DirectoryTree\ImapEngine\Pagination\LengthAwarePaginator;
|
||||
|
||||
/**
|
||||
* @mixin \DirectoryTree\ImapEngine\Connection\ImapQueryBuilder
|
||||
*/
|
||||
interface MessageQueryInterface
|
||||
{
|
||||
/**
|
||||
* Don't mark messages as read when fetching.
|
||||
*/
|
||||
public function leaveUnread(): MessageQueryInterface;
|
||||
|
||||
/**
|
||||
* Mark all messages as read when fetching.
|
||||
*/
|
||||
public function markAsRead(): MessageQueryInterface;
|
||||
|
||||
/**
|
||||
* Set the limit and page for the current query.
|
||||
*/
|
||||
public function limit(int $limit, int $page = 1): MessageQueryInterface;
|
||||
|
||||
/**
|
||||
* Get the set fetch limit.
|
||||
*/
|
||||
public function getLimit(): ?int;
|
||||
|
||||
/**
|
||||
* Set the fetch limit.
|
||||
*/
|
||||
public function setLimit(int $limit): MessageQueryInterface;
|
||||
|
||||
/**
|
||||
* Get the set page.
|
||||
*/
|
||||
public function getPage(): int;
|
||||
|
||||
/**
|
||||
* Set the page.
|
||||
*/
|
||||
public function setPage(int $page): MessageQueryInterface;
|
||||
|
||||
/**
|
||||
* Determine if the body of messages is being fetched.
|
||||
*/
|
||||
public function isFetchingBody(): bool;
|
||||
|
||||
/**
|
||||
* Determine if the flags of messages is being fetched.
|
||||
*/
|
||||
public function isFetchingFlags(): bool;
|
||||
|
||||
/**
|
||||
* Determine if the headers of messages is being fetched.
|
||||
*/
|
||||
public function isFetchingHeaders(): bool;
|
||||
|
||||
/**
|
||||
* Determine if the size of messages is being fetched.
|
||||
*/
|
||||
public function isFetchingSize(): bool;
|
||||
|
||||
/**
|
||||
* Determine if the body structure of messages is being fetched.
|
||||
*/
|
||||
public function isFetchingBodyStructure(): bool;
|
||||
|
||||
/**
|
||||
* Fetch the flags of messages.
|
||||
*/
|
||||
public function withFlags(): MessageQueryInterface;
|
||||
|
||||
/**
|
||||
* Fetch the body of messages.
|
||||
*/
|
||||
public function withBody(): MessageQueryInterface;
|
||||
|
||||
/**
|
||||
* Fetch the headers of messages.
|
||||
*/
|
||||
public function withHeaders(): MessageQueryInterface;
|
||||
|
||||
/**
|
||||
* Fetch the size of messages.
|
||||
*/
|
||||
public function withSize(): MessageQueryInterface;
|
||||
|
||||
/**
|
||||
* Fetch the body structure of messages.
|
||||
*/
|
||||
public function withBodyStructure(): MessageQueryInterface;
|
||||
|
||||
/**
|
||||
* Don't fetch the body of messages.
|
||||
*/
|
||||
public function withoutBody(): MessageQueryInterface;
|
||||
|
||||
/**
|
||||
* Don't fetch the headers of messages.
|
||||
*/
|
||||
public function withoutHeaders(): MessageQueryInterface;
|
||||
|
||||
/**
|
||||
* Don't fetch the flags of messages.
|
||||
*/
|
||||
public function withoutFlags(): MessageQueryInterface;
|
||||
|
||||
/**
|
||||
* Don't fetch the size of messages.
|
||||
*/
|
||||
public function withoutSize(): MessageQueryInterface;
|
||||
|
||||
/**
|
||||
* Don't fetch the body structure of messages.
|
||||
*/
|
||||
public function withoutBodyStructure(): MessageQueryInterface;
|
||||
|
||||
/**
|
||||
* Set the fetch order.
|
||||
*/
|
||||
public function setFetchOrder(string $fetchOrder): MessageQueryInterface;
|
||||
|
||||
/**
|
||||
* Get the fetch order.
|
||||
*/
|
||||
public function getFetchOrder(): string;
|
||||
|
||||
/**
|
||||
* Set the fetch order to 'ascending'.
|
||||
*/
|
||||
public function setFetchOrderAsc(): MessageQueryInterface;
|
||||
|
||||
/**
|
||||
* Set the fetch order to 'descending'.
|
||||
*/
|
||||
public function setFetchOrderDesc(): MessageQueryInterface;
|
||||
|
||||
/**
|
||||
* Set the fetch order to show oldest messages first (ascending).
|
||||
*/
|
||||
public function oldest(): MessageQueryInterface;
|
||||
|
||||
/**
|
||||
* Set the fetch order to show newest messages first (descending).
|
||||
*/
|
||||
public function newest(): MessageQueryInterface;
|
||||
|
||||
/**
|
||||
* Set the sort key for server-side sorting (RFC 5256).
|
||||
*/
|
||||
public function setSortKey(ImapSortKey|string|null $key): MessageQueryInterface;
|
||||
|
||||
/**
|
||||
* Get the sort key for server-side sorting.
|
||||
*/
|
||||
public function getSortKey(): ?ImapSortKey;
|
||||
|
||||
/**
|
||||
* Set the sort direction for server-side sorting.
|
||||
*/
|
||||
public function setSortDirection(string $direction): MessageQueryInterface;
|
||||
|
||||
/**
|
||||
* Get the sort direction for server-side sorting.
|
||||
*/
|
||||
public function getSortDirection(): string;
|
||||
|
||||
/**
|
||||
* Sort messages by a field using server-side sorting (RFC 5256).
|
||||
*/
|
||||
public function sortBy(ImapSortKey|string $key, string $direction = 'asc'): MessageQueryInterface;
|
||||
|
||||
/**
|
||||
* Sort messages by a field in descending order using server-side sorting.
|
||||
*/
|
||||
public function sortByDesc(ImapSortKey|string $key): MessageQueryInterface;
|
||||
|
||||
/**
|
||||
* Count all available messages matching the current search criteria.
|
||||
*/
|
||||
public function count(): int;
|
||||
|
||||
/**
|
||||
* Get the first message in the resulting collection.
|
||||
*/
|
||||
public function first(): ?MessageInterface;
|
||||
|
||||
/**
|
||||
* Get the first message in the resulting collection or throw an exception.
|
||||
*/
|
||||
public function firstOrFail(): MessageInterface;
|
||||
|
||||
/**
|
||||
* Get the messages matching the current query.
|
||||
*/
|
||||
public function get(): MessageCollection;
|
||||
|
||||
/**
|
||||
* Append a new message to the folder.
|
||||
*/
|
||||
public function append(string $message, mixed $flags = null): int;
|
||||
|
||||
/**
|
||||
* Execute a callback over each message via a chunked query.
|
||||
*/
|
||||
public function each(callable $callback, int $chunkSize = 10, int $startChunk = 1): void;
|
||||
|
||||
/**
|
||||
* Execute a callback over each chunk of messages.
|
||||
*/
|
||||
public function chunk(callable $callback, int $chunkSize = 10, int $startChunk = 1): void;
|
||||
|
||||
/**
|
||||
* Paginate the current query.
|
||||
*/
|
||||
public function paginate(int $perPage = 5, $page = null, string $pageName = 'page'): LengthAwarePaginator;
|
||||
|
||||
/**
|
||||
* Find a message by the given identifier type or throw an exception.
|
||||
*/
|
||||
public function findOrFail(int $id, ImapFetchIdentifier $identifier = ImapFetchIdentifier::Uid): MessageInterface;
|
||||
|
||||
/**
|
||||
* Find a message by the given identifier type.
|
||||
*/
|
||||
public function find(int $id, ImapFetchIdentifier $identifier = ImapFetchIdentifier::Uid): ?MessageInterface;
|
||||
|
||||
/**
|
||||
* Destroy the given messages.
|
||||
*/
|
||||
public function destroy(array|int $uids, bool $expunge = false): void;
|
||||
|
||||
/**
|
||||
* Add or remove a flag from all messages matching the current query.
|
||||
*
|
||||
* @param string $operation '+'|'-'
|
||||
* @return int The number of messages affected.
|
||||
*/
|
||||
public function flag(BackedEnum|string $flag, string $operation, bool $expunge = false): int;
|
||||
|
||||
/**
|
||||
* Mark all messages matching the current query as read.
|
||||
*
|
||||
* @return int The number of messages affected.
|
||||
*/
|
||||
public function markRead(): int;
|
||||
|
||||
/**
|
||||
* Mark all messages matching the current query as unread.
|
||||
*
|
||||
* @return int The number of messages affected.
|
||||
*/
|
||||
public function markUnread(): int;
|
||||
|
||||
/**
|
||||
* Mark all messages matching the current query as flagged.
|
||||
*
|
||||
* @return int The number of messages affected.
|
||||
*/
|
||||
public function markFlagged(): int;
|
||||
|
||||
/**
|
||||
* Unmark all messages matching the current query as flagged.
|
||||
*
|
||||
* @return int The number of messages affected.
|
||||
*/
|
||||
public function unmarkFlagged(): int;
|
||||
|
||||
/**
|
||||
* Delete all messages matching the current query.
|
||||
*
|
||||
* @return int The number of messages affected.
|
||||
*/
|
||||
public function delete(bool $expunge = false): int;
|
||||
|
||||
/**
|
||||
* Move all messages matching the current query to the given folder.
|
||||
*
|
||||
* @return int The number of messages affected.
|
||||
*/
|
||||
public function move(string $folder, bool $expunge = false): int;
|
||||
|
||||
/**
|
||||
* Copy all messages matching the current query to the given folder.
|
||||
*
|
||||
* @return int The number of messages affected.
|
||||
*/
|
||||
public function copy(string $folder): int;
|
||||
}
|
||||
@@ -1,181 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Pagination;
|
||||
|
||||
use DirectoryTree\ImapEngine\Support\ForwardsCalls;
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
use Illuminate\Support\Collection;
|
||||
use JsonSerializable;
|
||||
|
||||
/**
|
||||
* @template TKey of array-key
|
||||
* @template TValue
|
||||
*
|
||||
* @template-implements Arrayable<TKey, TValue>
|
||||
*/
|
||||
class LengthAwarePaginator implements Arrayable, JsonSerializable
|
||||
{
|
||||
use ForwardsCalls;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
protected Collection $items,
|
||||
protected int $total,
|
||||
protected int $perPage,
|
||||
protected int $currentPage = 1,
|
||||
protected string $path = '',
|
||||
protected array $query = [],
|
||||
protected string $pageName = 'page',
|
||||
) {
|
||||
$this->currentPage = max($currentPage, 1);
|
||||
|
||||
$this->path = rtrim($path, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle dynamic method calls on the paginator.
|
||||
*/
|
||||
public function __call(string $method, array $parameters): mixed
|
||||
{
|
||||
return $this->forwardCallTo($this->items, $method, $parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the items being paginated.
|
||||
*
|
||||
* @return Collection<TKey, TValue>
|
||||
*/
|
||||
public function items(): Collection
|
||||
{
|
||||
return $this->items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total number of items.
|
||||
*/
|
||||
public function total(): int
|
||||
{
|
||||
return $this->total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of items per page.
|
||||
*/
|
||||
public function perPage(): int
|
||||
{
|
||||
return $this->perPage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current page number.
|
||||
*/
|
||||
public function currentPage(): int
|
||||
{
|
||||
return $this->currentPage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last page (total pages).
|
||||
*/
|
||||
public function lastPage(): int
|
||||
{
|
||||
return (int) ceil($this->total / $this->perPage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if there are enough items to split into multiple pages.
|
||||
*/
|
||||
public function hasPages(): bool
|
||||
{
|
||||
return $this->total() > $this->perPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if there is a next page.
|
||||
*/
|
||||
public function hasMorePages(): bool
|
||||
{
|
||||
return $this->currentPage() < $this->lastPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the URL for a given page.
|
||||
*/
|
||||
public function url(int $page): string
|
||||
{
|
||||
$params = array_merge($this->query, [$this->pageName => $page]);
|
||||
|
||||
$queryString = http_build_query($params);
|
||||
|
||||
return $this->path.($queryString ? '?'.$queryString : '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL for the next page, or null if none.
|
||||
*/
|
||||
public function nextPageUrl(): ?string
|
||||
{
|
||||
if ($this->hasMorePages()) {
|
||||
return $this->url($this->currentPage() + 1);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL for the previous page, or null if none.
|
||||
*/
|
||||
public function previousPageUrl(): ?string
|
||||
{
|
||||
if ($this->currentPage() > 1) {
|
||||
return $this->url($this->currentPage() - 1);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array representation of the paginator.
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'path' => $this->path,
|
||||
'total' => $this->total(),
|
||||
'to' => $this->calculateTo(),
|
||||
'per_page' => $this->perPage(),
|
||||
'last_page' => $this->lastPage(),
|
||||
'first_page_url' => $this->url(1),
|
||||
'data' => $this->items()->toArray(),
|
||||
'current_page' => $this->currentPage(),
|
||||
'next_page_url' => $this->nextPageUrl(),
|
||||
'prev_page_url' => $this->previousPageUrl(),
|
||||
'last_page_url' => $this->url($this->lastPage()),
|
||||
'from' => $this->total() ? ($this->currentPage() - 1) * $this->perPage() + 1 : null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the "to" index for the current page.
|
||||
*/
|
||||
protected function calculateTo(): ?int
|
||||
{
|
||||
if (! $this->total()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$to = $this->currentPage() * $this->perPage();
|
||||
|
||||
return min($to, $this->total());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the JSON representation of the paginator.
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
}
|
||||
135
plugins/vendor/directorytree/imapengine/src/Poll.php
vendored
135
plugins/vendor/directorytree/imapengine/src/Poll.php
vendored
@@ -1,135 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine;
|
||||
|
||||
use Closure;
|
||||
use DirectoryTree\ImapEngine\Exceptions\Exception;
|
||||
use DirectoryTree\ImapEngine\Exceptions\ImapConnectionClosedException;
|
||||
|
||||
class Poll
|
||||
{
|
||||
/**
|
||||
* The last seen message UID.
|
||||
*/
|
||||
protected ?int $lastSeenUid = null;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
protected Mailbox $mailbox,
|
||||
protected string $folder,
|
||||
protected Closure|int $frequency,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Destructor.
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
$this->disconnect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll for new messages at a given frequency.
|
||||
*/
|
||||
public function start(callable $callback, callable $query): void
|
||||
{
|
||||
$this->connect();
|
||||
|
||||
while ($frequency = $this->getNextFrequency()) {
|
||||
try {
|
||||
$this->check($callback, $query);
|
||||
} catch (ImapConnectionClosedException) {
|
||||
$this->reconnect();
|
||||
}
|
||||
|
||||
sleep($frequency);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for new messages since the last seen UID.
|
||||
*/
|
||||
protected function check(callable $callback, callable $query): void
|
||||
{
|
||||
$folder = $this->folder();
|
||||
|
||||
// If we don't have a last seen UID, we will fetch
|
||||
// the last one in the folder as a starting point.
|
||||
if (! $this->lastSeenUid) {
|
||||
$this->lastSeenUid = $folder->messages()
|
||||
->first()
|
||||
?->uid() ?? 0;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$query($folder->messages())
|
||||
->uid($this->lastSeenUid + 1, INF)
|
||||
->each(function (MessageInterface $message) use ($callback) {
|
||||
// Avoid processing the same message twice on subsequent polls.
|
||||
// Some IMAP servers will always return the last seen UID in
|
||||
// the search results regardless of given UID search range.
|
||||
if ($this->lastSeenUid === $message->uid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$callback($message);
|
||||
|
||||
$this->lastSeenUid = $message->uid();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the folder to poll.
|
||||
*/
|
||||
protected function folder(): FolderInterface
|
||||
{
|
||||
return $this->mailbox->folders()->findOrFail($this->folder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconnect the client and restart the poll session.
|
||||
*/
|
||||
protected function reconnect(): void
|
||||
{
|
||||
$this->mailbox->disconnect();
|
||||
|
||||
$this->connect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect the client and select the folder to poll.
|
||||
*/
|
||||
protected function connect(): void
|
||||
{
|
||||
$this->mailbox->connect();
|
||||
|
||||
$this->mailbox->select($this->folder(), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect the client.
|
||||
*/
|
||||
protected function disconnect(): void
|
||||
{
|
||||
try {
|
||||
$this->mailbox->disconnect();
|
||||
} catch (Exception) {
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next frequency in seconds.
|
||||
*/
|
||||
protected function getNextFrequency(): int|false
|
||||
{
|
||||
if (is_numeric($seconds = value($this->frequency))) {
|
||||
return abs((int) $seconds);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,448 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine;
|
||||
|
||||
use DirectoryTree\ImapEngine\Connection\ImapQueryBuilder;
|
||||
use DirectoryTree\ImapEngine\Enums\ImapSortKey;
|
||||
use DirectoryTree\ImapEngine\Support\ForwardsCalls;
|
||||
use Illuminate\Support\Traits\Conditionable;
|
||||
|
||||
trait QueriesMessages
|
||||
{
|
||||
use Conditionable, ForwardsCalls;
|
||||
|
||||
/**
|
||||
* The query builder instance.
|
||||
*/
|
||||
protected ImapQueryBuilder $query;
|
||||
|
||||
/**
|
||||
* The current page.
|
||||
*/
|
||||
protected int $page = 1;
|
||||
|
||||
/**
|
||||
* The fetch limit.
|
||||
*/
|
||||
protected ?int $limit = null;
|
||||
|
||||
/**
|
||||
* Whether to fetch the message body.
|
||||
*/
|
||||
protected bool $fetchBody = false;
|
||||
|
||||
/**
|
||||
* Whether to fetch the message flags.
|
||||
*/
|
||||
protected bool $fetchFlags = false;
|
||||
|
||||
/**
|
||||
* Whether to fetch the message headers.
|
||||
*/
|
||||
protected bool $fetchHeaders = false;
|
||||
|
||||
/**
|
||||
* Whether to fetch the message size.
|
||||
*/
|
||||
protected bool $fetchSize = false;
|
||||
|
||||
/**
|
||||
* Whether to fetch the message body structure.
|
||||
*/
|
||||
protected bool $fetchBodyStructure = false;
|
||||
|
||||
/**
|
||||
* The fetch order.
|
||||
*
|
||||
* @var 'asc'|'desc'
|
||||
*/
|
||||
protected string $fetchOrder = 'desc';
|
||||
|
||||
/**
|
||||
* Whether to leave messages fetched as unread by default.
|
||||
*/
|
||||
protected bool $fetchAsUnread = true;
|
||||
|
||||
/**
|
||||
* The methods that should be returned from query builder.
|
||||
*/
|
||||
protected array $passthru = ['toimap', 'isempty'];
|
||||
|
||||
/**
|
||||
* The sort key for server-side sorting (RFC 5256).
|
||||
*/
|
||||
protected ?ImapSortKey $sortKey = null;
|
||||
|
||||
/**
|
||||
* The sort direction for server-side sorting.
|
||||
*
|
||||
* @var 'asc'|'desc'
|
||||
*/
|
||||
protected string $sortDirection = 'asc';
|
||||
|
||||
/**
|
||||
* Handle dynamic method calls into the query builder.
|
||||
*/
|
||||
public function __call(string $method, array $parameters): mixed
|
||||
{
|
||||
if (in_array(strtolower($method), $this->passthru)) {
|
||||
return $this->query->{$method}(...$parameters);
|
||||
}
|
||||
|
||||
$this->forwardCallTo($this->query, $method, $parameters);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function leaveUnread(): MessageQueryInterface
|
||||
{
|
||||
$this->fetchAsUnread = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function markAsRead(): MessageQueryInterface
|
||||
{
|
||||
$this->fetchAsUnread = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function limit(int $limit, int $page = 1): MessageQueryInterface
|
||||
{
|
||||
if ($page >= 1) {
|
||||
$this->page = $page;
|
||||
}
|
||||
|
||||
$this->limit = $limit;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getLimit(): ?int
|
||||
{
|
||||
return $this->limit;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function setLimit(int $limit): MessageQueryInterface
|
||||
{
|
||||
$this->limit = max($limit, 1);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getPage(): int
|
||||
{
|
||||
return $this->page;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function setPage(int $page): MessageQueryInterface
|
||||
{
|
||||
$this->page = $page;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function isFetchingBody(): bool
|
||||
{
|
||||
return $this->fetchBody;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function isFetchingFlags(): bool
|
||||
{
|
||||
return $this->fetchFlags;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function isFetchingHeaders(): bool
|
||||
{
|
||||
return $this->fetchHeaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function isFetchingSize(): bool
|
||||
{
|
||||
return $this->fetchSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function isFetchingBodyStructure(): bool
|
||||
{
|
||||
return $this->fetchBodyStructure;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function withFlags(): MessageQueryInterface
|
||||
{
|
||||
return $this->setFetchFlags(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function withBody(): MessageQueryInterface
|
||||
{
|
||||
return $this->setFetchBody(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function withHeaders(): MessageQueryInterface
|
||||
{
|
||||
return $this->setFetchHeaders(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function withSize(): MessageQueryInterface
|
||||
{
|
||||
return $this->setFetchSize(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function withBodyStructure(): MessageQueryInterface
|
||||
{
|
||||
return $this->setFetchBodyStructure(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function withoutBody(): MessageQueryInterface
|
||||
{
|
||||
return $this->setFetchBody(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function withoutHeaders(): MessageQueryInterface
|
||||
{
|
||||
return $this->setFetchHeaders(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function withoutFlags(): MessageQueryInterface
|
||||
{
|
||||
return $this->setFetchFlags(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function withoutSize(): MessageQueryInterface
|
||||
{
|
||||
return $this->setFetchSize(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function withoutBodyStructure(): MessageQueryInterface
|
||||
{
|
||||
return $this->setFetchBodyStructure(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether to fetch the flags.
|
||||
*/
|
||||
protected function setFetchFlags(bool $fetchFlags): MessageQueryInterface
|
||||
{
|
||||
$this->fetchFlags = $fetchFlags;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the fetch body flag.
|
||||
*/
|
||||
protected function setFetchBody(bool $fetchBody): MessageQueryInterface
|
||||
{
|
||||
$this->fetchBody = $fetchBody;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether to fetch the headers.
|
||||
*/
|
||||
protected function setFetchHeaders(bool $fetchHeaders): MessageQueryInterface
|
||||
{
|
||||
$this->fetchHeaders = $fetchHeaders;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether to fetch the size.
|
||||
*/
|
||||
protected function setFetchSize(bool $fetchSize): MessageQueryInterface
|
||||
{
|
||||
$this->fetchSize = $fetchSize;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether to fetch the body structure.
|
||||
*/
|
||||
protected function setFetchBodyStructure(bool $fetchBodyStructure): MessageQueryInterface
|
||||
{
|
||||
$this->fetchBodyStructure = $fetchBodyStructure;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
public function setFetchOrder(string $fetchOrder): MessageQueryInterface
|
||||
{
|
||||
$fetchOrder = strtolower($fetchOrder);
|
||||
|
||||
if (in_array($fetchOrder, ['asc', 'desc'])) {
|
||||
$this->fetchOrder = $fetchOrder;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getFetchOrder(): string
|
||||
{
|
||||
return $this->fetchOrder;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function setFetchOrderAsc(): MessageQueryInterface
|
||||
{
|
||||
return $this->setFetchOrder('asc');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function setFetchOrderDesc(): MessageQueryInterface
|
||||
{
|
||||
return $this->setFetchOrder('desc');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function oldest(): MessageQueryInterface
|
||||
{
|
||||
return $this->setFetchOrder('asc');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function newest(): MessageQueryInterface
|
||||
{
|
||||
return $this->setFetchOrder('desc');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function setSortKey(ImapSortKey|string|null $key): MessageQueryInterface
|
||||
{
|
||||
if (is_string($key)) {
|
||||
$key = ImapSortKey::from(strtoupper($key));
|
||||
}
|
||||
|
||||
$this->sortKey = $key;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getSortKey(): ?ImapSortKey
|
||||
{
|
||||
return $this->sortKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function setSortDirection(string $direction): MessageQueryInterface
|
||||
{
|
||||
$direction = strtolower($direction);
|
||||
|
||||
if (in_array($direction, ['asc', 'desc'])) {
|
||||
$this->sortDirection = $direction;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getSortDirection(): string
|
||||
{
|
||||
return $this->sortDirection;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function sortBy(ImapSortKey|string $key, string $direction = 'asc'): MessageQueryInterface
|
||||
{
|
||||
return $this->setSortKey($key)->setSortDirection($direction);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function sortByDesc(ImapSortKey|string $key): MessageQueryInterface
|
||||
{
|
||||
return $this->sortBy($key, 'desc');
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Support;
|
||||
|
||||
use BadMethodCallException;
|
||||
use Error;
|
||||
|
||||
trait ForwardsCalls
|
||||
{
|
||||
/**
|
||||
* Forward a method call to the given object.
|
||||
*/
|
||||
protected function forwardCallTo(object $object, string $method, array $parameters): mixed
|
||||
{
|
||||
try {
|
||||
return $object->{$method}(...$parameters);
|
||||
} catch (Error|BadMethodCallException $e) {
|
||||
$pattern = '~^Call to undefined method (?P<class>[^:]+)::(?P<method>[^\(]+)\(\)$~';
|
||||
|
||||
if (! preg_match($pattern, $e->getMessage(), $matches)) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
if ($matches['class'] != get_class($object) ||
|
||||
$matches['method'] != $method) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
static::throwBadMethodCallException($method);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Throw a bad method call exception for the given method.
|
||||
*/
|
||||
protected static function throwBadMethodCallException(string $method): never
|
||||
{
|
||||
throw new BadMethodCallException(sprintf(
|
||||
'Call to undefined method %s::%s()', static::class, $method
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1,313 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Support;
|
||||
|
||||
use BackedEnum;
|
||||
|
||||
class Str
|
||||
{
|
||||
/**
|
||||
* Make a list with literals or nested lists.
|
||||
*/
|
||||
public static function list(array $list): string
|
||||
{
|
||||
$values = [];
|
||||
|
||||
foreach ($list as $value) {
|
||||
if (is_array($value)) {
|
||||
$values[] = static::list($value);
|
||||
} else {
|
||||
$values[] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return sprintf('(%s)', implode(' ', $values));
|
||||
}
|
||||
|
||||
/**
|
||||
* Make one or more literals.
|
||||
*/
|
||||
public static function literal(array|string $string): array|string
|
||||
{
|
||||
if (is_array($string)) {
|
||||
$result = [];
|
||||
|
||||
foreach ($string as $value) {
|
||||
$result[] = static::literal($value);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
if (str_contains($string, "\n")) {
|
||||
return ['{'.strlen($string).'}', $string];
|
||||
}
|
||||
|
||||
return '"'.static::escape($string).'"';
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the value of the given enums.
|
||||
*/
|
||||
public static function enums(BackedEnum|array|string|null $enums = null): array|string|null
|
||||
{
|
||||
if (is_null($enums)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_array($enums)) {
|
||||
return array_map([static::class, 'enums'], $enums);
|
||||
}
|
||||
|
||||
return Str::enum($enums);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the value of the given enum.
|
||||
*/
|
||||
public static function enum(BackedEnum|string $enum): string
|
||||
{
|
||||
if ($enum instanceof BackedEnum) {
|
||||
return $enum->value;
|
||||
}
|
||||
|
||||
return (string) $enum;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a range set for use in a search command.
|
||||
*/
|
||||
public static function set(int|string|array $from, int|float|string|null $to = null): string
|
||||
{
|
||||
// If $from is an array with multiple elements, return them as a comma-separated list.
|
||||
if (is_array($from) && count($from) > 1) {
|
||||
return implode(',', $from);
|
||||
}
|
||||
|
||||
// If $from is an array with a single element, return that element.
|
||||
if (is_array($from) && count($from) === 1) {
|
||||
return (string) reset($from);
|
||||
}
|
||||
|
||||
// At this point, $from is an integer. No upper bound provided, return $from as a string.
|
||||
if (is_null($to)) {
|
||||
return (string) $from;
|
||||
}
|
||||
|
||||
// If the upper bound is infinite, use the '*' notation.
|
||||
if ($to == INF) {
|
||||
return $from.':*';
|
||||
}
|
||||
|
||||
// Otherwise, return a typical range string.
|
||||
return $from.':'.$to;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a credentials string for use in the AUTHENTICATE command.
|
||||
*/
|
||||
public static function credentials(string $user, string $token): string
|
||||
{
|
||||
return base64_encode("user=$user\1auth=Bearer $token\1\1");
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefix a string with the given prefix if it does not already start with it.
|
||||
*/
|
||||
public static function prefix(string $value, string $prefix): string
|
||||
{
|
||||
return str_starts_with($value, $prefix) ? $value : $prefix.$value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape a string for use in a list.
|
||||
*/
|
||||
public static function escape(string $string): string
|
||||
{
|
||||
// Remove newlines and control characters (ASCII 0-31 and 127).
|
||||
$string = preg_replace('/[\r\n\x00-\x1F\x7F]/', '', $string);
|
||||
|
||||
// Escape backslashes first to avoid double-escaping and then escape double quotes.
|
||||
return str_replace(['\\', '"'], ['\\\\', '\\"'], $string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a modified UTF-7 string (IMAP specific) to UTF-8.
|
||||
*/
|
||||
public static function fromImapUtf7(string $string): string
|
||||
{
|
||||
// If the string doesn't contain any '&' character, it's not UTF-7 encoded.
|
||||
if (! str_contains($string, '&')) {
|
||||
return $string;
|
||||
}
|
||||
|
||||
// Handle the special case of '&-' which represents '&' in UTF-7.
|
||||
if ($string === '&-') {
|
||||
return '&';
|
||||
}
|
||||
|
||||
// Direct implementation of IMAP's modified UTF-7 decoding.
|
||||
return preg_replace_callback('/&([^-]*)-?/', function ($matches) {
|
||||
/** @var array{0: string, 1: string, 2?: string} $matches */
|
||||
// If it's just an ampersand.
|
||||
if ($matches[1] === '') {
|
||||
return '&';
|
||||
}
|
||||
|
||||
// If it's the special case for ampersand.
|
||||
if ($matches[1] === '-') {
|
||||
return '&';
|
||||
}
|
||||
|
||||
// Convert modified base64 to standard base64.
|
||||
$base64 = strtr($matches[1], ',', '/');
|
||||
|
||||
// Add padding if necessary.
|
||||
switch (strlen($base64) % 4) {
|
||||
case 1: $base64 .= '===';
|
||||
break;
|
||||
case 2: $base64 .= '==';
|
||||
break;
|
||||
case 3: $base64 .= '=';
|
||||
break;
|
||||
}
|
||||
|
||||
// Decode base64 to binary.
|
||||
$binary = base64_decode($base64, true);
|
||||
|
||||
if ($binary === false) {
|
||||
// If decoding fails, return the original string.
|
||||
return '&'.$matches[1].($matches[2] ?? '');
|
||||
}
|
||||
|
||||
$result = '';
|
||||
|
||||
// Convert binary UTF-16BE to UTF-8.
|
||||
for ($i = 0; $i < strlen($binary); $i += 2) {
|
||||
if (isset($binary[$i + 1])) {
|
||||
$char = (ord($binary[$i]) << 8) | ord($binary[$i + 1]);
|
||||
|
||||
if ($char < 0x80) {
|
||||
$result .= chr($char);
|
||||
} elseif ($char < 0x800) {
|
||||
$result .= chr(0xC0 | ($char >> 6)).chr(0x80 | ($char & 0x3F));
|
||||
} else {
|
||||
$result .= chr(0xE0 | ($char >> 12)).chr(0x80 | (($char >> 6) & 0x3F)).chr(0x80 | ($char & 0x3F));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}, $string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a UTF-8 string to modified UTF-7 (IMAP specific).
|
||||
*/
|
||||
public static function toImapUtf7(string $string): string
|
||||
{
|
||||
$result = '';
|
||||
$buffer = '';
|
||||
|
||||
// Iterate over each character in the UTF-8 string.
|
||||
for ($i = 0; $i < mb_strlen($string, 'UTF-8'); $i++) {
|
||||
$char = mb_substr($string, $i, 1, 'UTF-8');
|
||||
|
||||
// Convert character to its UTF-16BE code unit (for deciding if ASCII).
|
||||
$ord = unpack('n', mb_convert_encoding($char, 'UTF-16BE', 'UTF-8'))[1];
|
||||
|
||||
// Handle printable ASCII characters (0x20 - 0x7E) except '&'
|
||||
if ($ord >= 0x20 && $ord <= 0x7E && $char !== '&') {
|
||||
// If there is any buffered non-ASCII content, flush it as a base64 section.
|
||||
if ($buffer !== '') {
|
||||
// Encode the buffer to UTF-16BE, then to base64, swap '/' for ',', trim '=' padding, and wrap with '&' and '-'.
|
||||
$result .= '&'.rtrim(strtr(base64_encode(mb_convert_encoding($buffer, 'UTF-16BE', 'UTF-8')), '/', ','), '=').'-';
|
||||
$buffer = '';
|
||||
}
|
||||
|
||||
// Append the ASCII character as-is.
|
||||
$result .= $char;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Special handling for literal '&' which becomes '&-'
|
||||
if ($char === '&') {
|
||||
// Flush any buffered non-ASCII content first.
|
||||
if ($buffer !== '') {
|
||||
$result .= '&'.rtrim(strtr(base64_encode(mb_convert_encoding($buffer, 'UTF-16BE', 'UTF-8')), '/', ','), '=').'-';
|
||||
$buffer = '';
|
||||
}
|
||||
|
||||
// '&' is encoded as '&-'
|
||||
$result .= '&-';
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Buffer non-ASCII characters for later base64 encoding.
|
||||
$buffer .= $char;
|
||||
}
|
||||
|
||||
// After the loop, flush any remaining buffered non-ASCII content.
|
||||
if ($buffer !== '') {
|
||||
$result .= '&'.rtrim(strtr(base64_encode(mb_convert_encoding($buffer, 'UTF-16BE', 'UTF-8')), '/', ','), '=').'-';
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a given string matches a given pattern.
|
||||
*/
|
||||
public static function is(array|string $pattern, string $value, bool $ignoreCase = false): bool
|
||||
{
|
||||
if (! is_iterable($pattern)) {
|
||||
$pattern = [$pattern];
|
||||
}
|
||||
|
||||
foreach ($pattern as $pattern) {
|
||||
$pattern = (string) $pattern;
|
||||
|
||||
// If the given value is an exact match we can of course return true right
|
||||
// from the beginning. Otherwise, we will translate asterisks and do an
|
||||
// actual pattern match against the two strings to see if they match.
|
||||
if ($pattern === '*' || $pattern === $value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($ignoreCase && mb_strtolower($pattern) === mb_strtolower($value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$pattern = preg_quote($pattern, '#');
|
||||
|
||||
// Asterisks are translated into zero-or-more regular expression wildcards
|
||||
// to make it convenient to check if the strings starts with the given
|
||||
// pattern such as "library/*", making any string check convenient.
|
||||
$pattern = str_replace('\*', '.*', $pattern);
|
||||
|
||||
if (preg_match('#^'.$pattern.'\z#'.($ignoreCase ? 'isu' : 'su'), $value) === 1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode MIME-encoded header values.
|
||||
*/
|
||||
public static function decodeMimeHeader(string $value): string
|
||||
{
|
||||
if (! str_contains($value, '=?')) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if ($decoded = iconv_mime_decode($value, ICONV_MIME_DECODE_CONTINUE_ON_ERROR, 'UTF-8')) {
|
||||
return $decoded;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
@@ -1,245 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Testing;
|
||||
|
||||
use DirectoryTree\ImapEngine\ComparesFolders;
|
||||
use DirectoryTree\ImapEngine\Exceptions\Exception;
|
||||
use DirectoryTree\ImapEngine\FolderInterface;
|
||||
use DirectoryTree\ImapEngine\MailboxInterface;
|
||||
use DirectoryTree\ImapEngine\MessageQueryInterface;
|
||||
use DirectoryTree\ImapEngine\Support\Str;
|
||||
|
||||
class FakeFolder implements FolderInterface
|
||||
{
|
||||
use ComparesFolders;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
protected string $path = '',
|
||||
protected array $flags = [],
|
||||
/** @var FakeMessage[] */
|
||||
protected array $messages = [],
|
||||
protected string $delimiter = '/',
|
||||
protected ?MailboxInterface $mailbox = null,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function mailbox(): MailboxInterface
|
||||
{
|
||||
return $this->mailbox ?? throw new Exception('Folder has no mailbox.');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function path(): string
|
||||
{
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function flags(): array
|
||||
{
|
||||
return $this->flags;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function delimiter(): string
|
||||
{
|
||||
return $this->delimiter;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function name(): string
|
||||
{
|
||||
return Str::fromImapUtf7(
|
||||
last(explode($this->delimiter, $this->path))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function is(FolderInterface $folder): bool
|
||||
{
|
||||
return $this->isSameFolder($this, $folder);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function messages(): MessageQueryInterface
|
||||
{
|
||||
// Ensure the folder is selected.
|
||||
$this->select(true);
|
||||
|
||||
return new FakeMessageQuery($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function idle(callable $callback, ?callable $query = null, callable|int $timeout = 300): void
|
||||
{
|
||||
foreach ($this->messages as $message) {
|
||||
$callback($message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function poll(callable $callback, ?callable $query = null, callable|int $frequency = 60): void
|
||||
{
|
||||
foreach ($this->messages as $message) {
|
||||
$callback($message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function move(string $newPath): void
|
||||
{
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function select(bool $force = false): void
|
||||
{
|
||||
$this->mailbox?->select($this, $force);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function status(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function examine(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function expunge(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function quota(): array
|
||||
{
|
||||
return [
|
||||
$this->path => [
|
||||
'STORAGE' => [
|
||||
'usage' => 0,
|
||||
'limit' => 0,
|
||||
],
|
||||
'MESSAGE' => [
|
||||
'usage' => 0,
|
||||
'limit' => 0,
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function delete(): void
|
||||
{
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the folder's path.
|
||||
*/
|
||||
public function setPath(string $path): FakeFolder
|
||||
{
|
||||
$this->path = $path;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the folder's flags.
|
||||
*/
|
||||
public function setFlags(array $flags): FakeFolder
|
||||
{
|
||||
$this->flags = $flags;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the folder's mailbox.
|
||||
*/
|
||||
public function setMailbox(MailboxInterface $mailbox): FakeFolder
|
||||
{
|
||||
$this->mailbox = $mailbox;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the folder's messages.
|
||||
*
|
||||
* @param FakeMessage[] $messages
|
||||
*/
|
||||
public function setMessages(array $messages): FakeFolder
|
||||
{
|
||||
$this->messages = $messages;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the folder's messages.
|
||||
*
|
||||
* @return FakeMessage[]
|
||||
*/
|
||||
public function getMessages(): array
|
||||
{
|
||||
return $this->messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a message to the folder.
|
||||
*/
|
||||
public function addMessage(FakeMessage $message): void
|
||||
{
|
||||
$this->messages[] = $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the folder's delimiter.
|
||||
*/
|
||||
public function setDelimiter(string $delimiter = '/'): FakeFolder
|
||||
{
|
||||
$this->delimiter = $delimiter;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Testing;
|
||||
|
||||
use DirectoryTree\ImapEngine\Collections\FolderCollection;
|
||||
use DirectoryTree\ImapEngine\FolderInterface;
|
||||
use DirectoryTree\ImapEngine\FolderRepositoryInterface;
|
||||
use DirectoryTree\ImapEngine\MailboxInterface;
|
||||
use DirectoryTree\ImapEngine\Support\Str;
|
||||
use Illuminate\Support\ItemNotFoundException;
|
||||
|
||||
class FakeFolderRepository implements FolderRepositoryInterface
|
||||
{
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
protected MailboxInterface $mailbox,
|
||||
/** @var FolderInterface[] */
|
||||
protected array $folders = []
|
||||
) {}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function find(string $path): ?FolderInterface
|
||||
{
|
||||
try {
|
||||
return $this->findOrFail($path);
|
||||
} catch (ItemNotFoundException) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function findOrFail(string $path): FolderInterface
|
||||
{
|
||||
return $this->get()->firstOrFail(
|
||||
fn (FolderInterface $folder) => strtolower($folder->path()) === strtolower($path)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function create(string $path): FolderInterface
|
||||
{
|
||||
return $this->folders[] = new FakeFolder($path, mailbox: $this->mailbox);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function firstOrCreate(string $path): FolderInterface
|
||||
{
|
||||
return $this->find($path) ?? $this->create($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function get(?string $match = '*', ?string $reference = ''): FolderCollection
|
||||
{
|
||||
$folders = FolderCollection::make($this->folders);
|
||||
|
||||
// If we're not matching all, filter the folders by the match pattern.
|
||||
if (! in_array($match, ['*', null])) {
|
||||
return $folders->filter(
|
||||
fn (FolderInterface $folder) => Str::is($match, $folder->path())
|
||||
);
|
||||
}
|
||||
|
||||
return $folders;
|
||||
}
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine\Testing;
|
||||
|
||||
use DirectoryTree\ImapEngine\Connection\ConnectionInterface;
|
||||
use DirectoryTree\ImapEngine\Exceptions\Exception;
|
||||
use DirectoryTree\ImapEngine\FolderInterface;
|
||||
use DirectoryTree\ImapEngine\FolderRepositoryInterface;
|
||||
use DirectoryTree\ImapEngine\MailboxInterface;
|
||||
|
||||
class FakeMailbox implements MailboxInterface
|
||||
{
|
||||
/**
|
||||
* The currently selected folder.
|
||||
*/
|
||||
protected ?FolderInterface $selected = null;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
protected array $config = [],
|
||||
/** @var FakeFolder[] */
|
||||
protected array $folders = [],
|
||||
protected array $capabilities = [],
|
||||
) {
|
||||
foreach ($folders as $folder) {
|
||||
$folder->setMailbox($this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function config(?string $key = null, mixed $default = null): mixed
|
||||
{
|
||||
if (is_null($key)) {
|
||||
return $this->config;
|
||||
}
|
||||
|
||||
return data_get($this->config, $key, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function connection(): ConnectionInterface
|
||||
{
|
||||
throw new Exception('Unsupported.');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function connected(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function reconnect(): void
|
||||
{
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function connect(?ConnectionInterface $connection = null): void
|
||||
{
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function disconnect(): void
|
||||
{
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function inbox(): FolderInterface
|
||||
{
|
||||
return $this->folders()->findOrFail('inbox');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function folders(): FolderRepositoryInterface
|
||||
{
|
||||
return new FakeFolderRepository($this, $this->folders);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function capabilities(): array
|
||||
{
|
||||
return $this->capabilities;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function select(FolderInterface $folder, bool $force = false): void
|
||||
{
|
||||
$this->selected = $folder;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function selected(FolderInterface $folder): bool
|
||||
{
|
||||
return $this->selected?->is($folder) ?? false;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user