thingmabobby / imap-email-checker
A PHP class to fetch and process emails from an IMAP mailbox.
Requires
- php: ^8.0
- ext-imap: *
- ext-mbstring: *
README
A PHP class to fetch and process emails from an IMAP mailbox. This class provides functionalities to retrieve emails based on different criteria, check mailbox status, manage read/unread flags, perform custom searches, decode email bodies (including handling inline images), extract attachments, delete and archive emails.
Purpose
This class is designed to simplify the process of accessing and managing emails via IMAP in PHP. It provides robust error handling using exceptions and can be used for various applications such as:
- Email Archiving: Storing emails in a database or other storage for record-keeping.
- Automated Email Processing: Building scripts to analyze incoming emails, trigger actions based on email content, or integrate email data into other systems.
- Linking Emails to System Records: Extracting specific identifiers (like ticket numbers, order IDs, or custom tags) directly from email subjects using a configurable regular expression to associate emails with records in your application (e.g., CRM, helpdesk, order management).
- Email Backup Solutions: Downloading and backing up emails from a mailbox.
- Mailbox Monitoring: Checking for new or unread emails and mailbox status using efficient methods.
- Targeted Retrieval: Searching for specific emails based on various criteria and fetching only those.
The class handles complexities like:
- Robust Decoding: Correctly decodes email bodies and headers in various encodings (e.g., Base64, Quoted-Printable) and normalizes text content to UTF-8.
- Inline Image Embedding: Automatically embeds inline images (referenced via
cid:
) within HTML email bodies as Base64 data URIs. - Attachment Extraction: Provides access to non-inline email attachments.
- UID Management: Focuses on using Unique Identifiers (UIDs) for reliable message identification and efficient incremental retrieval.
- Status Checks & Flag Management: Provides methods to check mailbox status efficiently and manage the
\Seen
(read/unread) flag. - Custom Search: Allows flexible searching using standard IMAP criteria strings.
- Exception-Based Errors: Uses standard PHP exceptions (
RuntimeException
,InvalidArgumentException
) to signal errors clearly.
Installation
Composer:
composer require thingmabobby/imap-email-checker
Standalone:
require '..path/to/src/IMAPEmailChecker.php'; // Adjust path as needed
Usage
To use the IMAPEmailChecker class, you need to have the PHP IMAP extension enabled. Here's an extended example covering many common operations, demonstrating the use of try...catch for error handling:
<?php declare(strict_types=1); // Use Composer's autoloader // Make sure to run 'composer install' in your project directory require 'vendor/autoload.php'; // Or if not using Composer: // require '..path/to/src/IMAPEmailChecker.php'; // Adjust path use IMAPEmailChecker\IMAPEmailChecker; use DateTime; // --- Configuration --- // IMPORTANT: Store credentials securely (e.g., .env file, environment variables), not directly in code! $hostname = getenv('IMAP_HOSTNAME') ?: '{your_imap_server:993/imap/ssl}INBOX'; $username = getenv('IMAP_USERNAME') ?: 'your_username@example.com'; $password = getenv('IMAP_PASSWORD') ?: 'your_password'; $archiveFolder = getenv('IMAP_ARCHIVE_FOLDER') ?: 'Archive'; try { // Debug mode enabled (for more verbose logging of non-critical issues) $debugMode = true; // Set to true to enable debug logging (defaults to false) echo "Connecting to " . htmlspecialchars($hostname) . "...<br>"; $emailChecker = IMAPEmailChecker::connect($hostname, $username, $password, $debugMode); // --- Helper Function for Display --- function displayEmailDetails(array $email): void { $uid = $email['uid'] ?? 'N/A'; echo "<h4>--- Details for Email UID: {$uid} ---</h4>"; echo "<ul>"; echo "<li><b>Subject:</b> " . htmlspecialchars($email['subject'] ?? 'N/A') . "</li>"; echo "<li><b>From:</b> " . htmlspecialchars($email['from'] ?? 'N/A') . "</li>"; echo "<li><b>Date:</b> " . htmlspecialchars($email['date'] ?? 'N/A') . "</li>"; if (!empty($email['to'])) echo "<li><b>To:</b> " . htmlspecialchars(implode(', ', $email['to'])) . "</li>"; if (!empty($email['cc'])) echo "<li><b>Cc:</b> " . htmlspecialchars(implode(', ', $email['cc'])) . "</li>"; $body = $email['message_body'] ?? ''; echo "<li><b>Body Length:</b> " . strlen($body) . " characters</li>"; $snippet = mb_substr(strip_tags($body), 0, 100); echo "<li><b>Body Snippet:</b> " . htmlspecialchars($snippet) . "...</li>"; $attachments = $email['attachments'] ?? []; if (!empty($attachments)) { echo "<li><b>Attachments (" . count($attachments) . "):</b><ul>"; foreach ($attachments as $attachment) { $filename = htmlspecialchars($attachment['filename'] ?? 'unknown'); $filesize = isset($attachment['content']) ? round(strlen($attachment['content']) / 1024) : 'N/A'; $filetype = htmlspecialchars($attachment['type'] ?? 'unknown'); echo "<li>{$filename} ({$filesize} KB, Type: {$filetype})</li>"; } echo "</ul></li>"; } else { echo "<li><b>Attachments:</b> None</li>"; } echo "</ul>"; } // ======================================================== // --- Example Operations --- // ======================================================== // 1. Check Mailbox Status echo "<h3>1. Mailbox Status:</h3>"; $mailboxStatus = $emailChecker->checkMailboxStatus(); // Throws on failure echo "<ul>"; echo "<li>Total Messages: " . $mailboxStatus['total'] . "</li>"; echo "<li>Unseen Count: " . count($mailboxStatus['unseen_uids']) . "</li>"; echo "<li>Highest UID: " . $mailboxStatus['highest_uid'] . "</li>"; echo "</ul>"; echo "<hr>"; // 2. Check Unread Emails & Display Details echo "<h3>2. Processing Unread Emails:</h3>"; $unreadEmails = $emailChecker->checkUnreadEmails(); // Throws on search failure $uidsToMarkRead = []; if (empty($unreadEmails)) { echo "<p>No unread emails found.</p>"; } else { echo "<p>Found " . count($unreadEmails) . " unread emails. Processing details...</p>"; // Note: processMessage failures inside checkUnreadEmails are logged & skipped foreach ($unreadEmails as $uid => $email) { displayEmailDetails($email); $uidsToMarkRead[] = $uid; } echo "<p>Last UID updated by checkUnreadEmails (if any processed): " . $emailChecker->lastuid . "</p>"; // 3. Mark Processed Emails as Read if (!empty($uidsToMarkRead)) { echo "<h4>Marking " . count($uidsToMarkRead) . " emails as Read:</h4>"; $emailChecker->setMessageReadStatus($uidsToMarkRead, true); // Throws on failure echo "<p>Successfully marked emails as read.</p>"; } } echo "<hr>"; // 4. Check emails since the last known UID (Incremental Check) echo "<h3>4. Checking New Emails Since Last Run:</h3>"; $lastKnownUID = 0; // <<< Replace with loading logic from persistent storage echo "<p>Checking for emails newer than UID: " . $lastKnownUID . "</p>"; $emailsSinceLastUID = $emailChecker->checkSinceLastUID($lastKnownUID); // Throws on overview failure $uidsProcessedInThisRun = []; if (empty($emailsSinceLastUID)) { echo "<p>No new emails found since UID " . $lastKnownUID . ".</p>"; } else { echo "<p>Found " . count($emailsSinceLastUID) . " new emails.</p>"; // Note: processMessage failures inside checkSinceLastUID are logged & skipped foreach ($emailsSinceLastUID as $uid => $email) { displayEmailDetails($email); $uidsProcessedInThisRun[] = $uid; } $newLastUID = $emailChecker->lastuid; echo "<p>Last UID updated to: " . $newLastUID . "</p>"; // IMPORTANT: Store $newLastUID persistently for the next run // save_last_uid_to_storage($newLastUID); } echo "<hr>"; // 5. Custom Search & Fetch Specific Messages echo "<h3>5. Custom Search & Fetch Specific Details:</h3>"; $currentMonth = date('M-Y'); $searchCriteria = 'TEXT ".pdf" SINCE "1-' . $currentMonth . '"'; echo "<h4>Searching for: \"{$searchCriteria}\" (UIDs)</h4>"; $pdfEmailUids = $emailChecker->search($searchCriteria); // Throws on failure or bad criteria if (empty($pdfEmailUids)) { echo "<p>No emails found matching criteria.</p>"; } else { echo "<p>Found " . count($pdfEmailUids) . " matching UIDs: " . implode(', ', $pdfEmailUids) . ". Fetching details...</p>"; // 6. Fetch the details ONLY for the messages found by search // Note: fetchMessagesByIds logs & skips individual message processing failures $pdfEmails = $emailChecker->fetchMessagesByIds($pdfEmailUids); if (empty($pdfEmails)) { echo "<p style='color: orange;'>Could not fetch details for any of the found UIDs (check logs).</p>"; } else { echo "<p>Successfully fetched details for " . count($pdfEmails) . " emails:</p>"; foreach ($pdfEmails as $uid => $email) { displayEmailDetails($email); } } } echo "<hr>"; // --- Destructive Operations (Use with extreme caution!) --- $uids_to_process = $pdfEmailUids ?? $uidsToMarkRead ?? []; // 7. Delete an email by UID (Example: Last UID found) if (!empty($uids_to_process)) { $uid_to_delete = end($uids_to_process); // Get the last element echo "<h3>7. Attempting to Delete Email (UID {$uid_to_delete}):</h3>"; echo "<p><strong>Warning: This action is permanent!</strong></p>"; // Uncomment the following line ONLY if you are sure! // $emailChecker->deleteEmail($uid_to_delete); // Throws on failure // echo "<p>Email UID {$uid_to_delete} deleted successfully.</p>"; echo "<p>(Deletion code is commented out for safety)</p>"; } else { echo "<h3>7. Deleting Email:</h3><p>No UIDs available to demonstrate deletion.</p>"; } echo "<hr>"; // 8. Archive an email by UID (Example: First UID found) if (!empty($uids_to_process)) { $uid_to_archive = reset($uids_to_process); // Get the first element echo "<h3>8. Attempting to Archive Email (UID {$uid_to_archive}) to '{$archiveFolder}':</h3>"; echo "<p><strong>Warning: This moves the email and expunges! Ensure folder '{$archiveFolder}' exists.</strong></p>"; // Uncomment the following line ONLY if you are sure! // $emailChecker->archiveEmail($uid_to_archive, $archiveFolder); // Throws on failure // echo "<p>Email UID {$uid_to_archive} archived successfully.</p>"; echo "<p>(Archival code is commented out for safety)</p>"; } else { echo "<h3>8. Archiving Email:</h3><p>No UIDs available to demonstrate archival.</p>"; } echo "<hr>"; } catch (\InvalidArgumentException $e) { // Handle errors related to invalid arguments passed to methods echo "<p style='color: purple; font-weight: bold;'>Argument Error: " . htmlspecialchars($e->getMessage()) . "</p>"; // Log detailed error: error_log($e->getMessage() . "\n" . $e->getTraceAsString()); exit(1); } catch (\RuntimeException $e) { // Handle errors related to IMAP operations or other runtime issues echo "<p style='color: red; font-weight: bold;'>Runtime Error: " . htmlspecialchars($e->getMessage()) . "</p>"; // Log detailed error: error_log($e->getMessage() . "\n" . $e->getTraceAsString()); exit(1); } catch (\Throwable $e) { // Catch any other unexpected errors echo "<p style='color: darkred; font-weight: bold;'>Unexpected Error: " . htmlspecialchars($e->getMessage()) . "</p>"; // Log detailed error: error_log($e->getMessage() . "\n" . $e->getTraceAsString()); exit(1); } ?>
Available Methods
Methods that perform IMAP operations may throw exceptions on failure. Batch processing methods (check*
, fetchMessagesByIds
) generally handle failures for individual messages internally (logging the error and skipping the message) but will throw exceptions for failures affecting the entire batch operation (e.g., the initial search/overview). Action methods (setMessageReadStatus
, deleteEmail
, archiveEmail
) return void
and throw exceptions on any failure.
-
static connect(string $hostname, string $username, string $password, string $mailbox = 'INBOX', int $port = 993, string $flags = '/ssl', int $retries = 0, bool $debug = false, string $bidRegex = '/#\s*(\d+)/')
:- A static factory method that establishes an IMAP connection and returns an IMAPEmailChecker instance. This is the recommended way to create an object of this class. The constructor remains available for injecting a pre-existing IMAP connection, ensuring backward compatibility.
- Parameters:
$hostname
: The IMAP server hostname (e.g.,imap.example.com
).$username
: The username for the IMAP account.$password
: The password for the IMAP account.$mailbox
(optionalstring
, defaultINBOX
): The mailbox to connect to.$port
(optionalint
, default993
): The IMAP server port (e.g., 993 for IMAPS/SSL, 143 for IMAP/STARTTLS).$flags
(optionalstring
, default/ssl
): Connection flags (e.g.,/ssl
,/tls
,/novalidate-cert
).$retries
(optionalint
, default0
): Number of connection retries forimap_open()
.$debug
(optionalbool
, defaultfalse
): Enables verbose logging viaerror_log()
for non-critical issues within the created instance.$bidRegex
(optionalstring
, default/#\s*(\d+)/
): A PCRE regex string used to extract an identifier from the email subject. See constructor documentation for details.
- Returns:
IMAPEmailChecker
- An instance of the class. - Throws:
InvalidArgumentException
If$hostname
,$username
, or$bidRegex
is invalid.RuntimeException
If the IMAP connection fails after all retries.
-
__construct($connection, bool $debug = false, string $bidRegex = '/#\s*(\d+)/')
:- Instantiates the email checker.
- Parameters:
$connection
: The established IMAP connection resource orIMAP\Connection
object.$debug
(optionalbool
, defaultfalse
): Enables verbose logging viaerror_log()
for non-critical issues.$bidRegex
(optionalstring
, default/\\#\\s*(\\d+)/
): A PCRE regex string used to extract an identifier string from the email subject. The regex must include a capturing group (typically group 1) that captures the desired ID string and cannot be an empty string.
- Throws:
InvalidArgumentException
If$bidRegex
is an empty string.RuntimeException
If the provided$connection
is invalid or closed.
-
checkMailboxStatus()
:- Checks the status of the current mailbox efficiently.
- Returns:
array
- An associative array with keys:total
,unseen_uids
,highest_uid
. - Throws:
RuntimeException
If any underlying IMAP operation (check
,uid
,search
) fails.
-
search(string $criteria, bool $returnUids = true)
:- Performs a search using custom IMAP criteria.
- Parameters:
$criteria
,$returnUids
(optional). - Returns:
array
- Sorted array of integer UIDs or sequence numbers. Empty array[]
if no match. - Throws:
InvalidArgumentException
If$criteria
is empty.RuntimeException
If theimap_search
operation fails.
-
fetchMessagesByIds(array $identifiers, bool $isUid = true)
:- Fetches full details for specific messages. Does not update class properties
$messages
or$lastuid
. Handles individual message processing errors internally by logging and skipping. - Parameters:
$identifiers
,$isUid
(optional). - Returns:
array
- Associative array of successfully processed message data (see "Message Array Structure"), keyed by identifier. Omits messages that failed processing.
- Fetches full details for specific messages. Does not update class properties
-
checkAllEmail()
:- Retrieves all emails. Use with caution on large mailboxes. Stores results in
$messages
. Handles individual message processing errors internally by logging and skipping. - Returns:
array
- Associative array (keyed by UID) of successfully processed email details. Empty array[]
if mailbox is empty. Updates$lastuid
. - Throws:
RuntimeException
If the initialimap_num_msg
fails.
- Retrieves all emails. Use with caution on large mailboxes. Stores results in
-
checkSinceDate(DateTime $date)
:- Retrieves emails on or after
$date
. Stores results in$messages
. Handles individual message processing errors internally by logging and skipping. - Parameters:
$date
. - Returns:
array
- Associative array of successfully processed emails (keyed by UID). Empty array[]
if no messages found. Updates$lastuid
. - Throws:
RuntimeException
If the initialimap_search
fails.
- Retrieves emails on or after
-
checkSinceLastUID(int $uid)
:- Retrieves emails with UID >
$uid
. Recommended for incremental fetching. Stores results in$messages
. Handles individual message processing errors internally by logging and skipping. - Parameters:
$uid
. - Returns:
array
- Associative array of successfully processed emails (keyed by UID). Empty array[]
if no messages found. Updates$lastuid
. - Throws:
RuntimeException
If the initialimap_fetch_overview
fails.
- Retrieves emails with UID >
-
checkUnreadEmails()
:- Retrieves emails marked
\Unseen
. Stores results in$messages
. Handles individual message processing errors internally by logging and skipping. - Returns:
array
- Associative array of successfully processed unread emails (keyed by UID). Empty array[]
if no unread emails found. Updates$lastuid
. - Throws:
RuntimeException
If the initialimap_search
fails.
- Retrieves emails marked
-
setMessageReadStatus(array $uids, bool $markAsRead)
:- Sets or clears the
\Seen
(read/unread) flag. - Parameters:
$uids
,$markAsRead
. - Returns:
void
- Throws:
InvalidArgumentException
If$uids
contains invalid values.RuntimeException
If the IMAP flag operation fails.
- Sets or clears the
-
deleteEmail(int $uid)
:- Deletes an email by UID (marks and expunges). Permanent! Use with caution.
- Parameters:
$uid
. - Returns:
void
- Throws:
InvalidArgumentException
If$uid
is invalid.RuntimeException
Ifimap_delete
orimap_expunge
fails with an error.
-
archiveEmail(int $uid, string $archiveFolder = 'Archive')
:- Moves an email by UID and expunges. Use with caution. Ensure folder exists.
- Parameters:
$uid
,$archiveFolder
(optional). - Returns:
void
- Throws:
InvalidArgumentException
If$uid
or$archiveFolder
is invalid.RuntimeException
Ifimap_mail_move
orimap_expunge
fails with an error.
Public Properties
-
$lastuid
:- Type:
int
- Description: After calling
checkAllEmail()
,checkSinceDate()
,checkSinceLastUID()
, orcheckUnreadEmails()
, this property holds the UID of the last (highest UID) email processed during that specific call. Store this between runs for efficient use withcheckSinceLastUID()
. Initialized to0
. Note:fetchMessagesByIds
does not update this property.
- Type:
-
$messages
:- Type:
array
- Description: An associative array containing the email messages fetched by the last successful call to a
check*
method that retrieves full message details (e.g.,checkAllEmail
,checkUnreadEmails
). The array is keyed by the message UID. Note:fetchMessagesByIds
populates its own return value but does not update this property.
- Type:
Message Array Structure
Each email message returned by check*
methods or fetchMessagesByIds
is an associative array with the following keys:
uid
: (int
) - The unique identifier (UID) of the message in the mailbox (persistent).message_number
: (int
) - The sequence number of the message (may not be persistent).message_id
: (string|null
) - The unique Message-ID header, angle brackets trimmed.null
if not present.subject
: (string
) - The decoded, UTF-8 subject. Empty string if not present.message_body
: (string
) - The decoded, UTF-8 body (HTML preferred), with inline images embedded. Empty string if empty or decoding failed.date
: (string|null
) - The raw date string from the header.null
if not present.datetime
: (DateTime|null
) - ADateTime
object for the email's date.null
if parsing failed.fromaddress
: (string
) - The decoded sender email address. Empty string if parsing failed.from
: (string
) - The decoded sender friendly name and address.to
: (array
) - Array of decoded "To" addresses.tocount
: (int
) - Count of "To" addresses.cc
: (array
) - Array of decoded "CC" addresses.cccount
: (int
) - Count of "CC" addresses.bcc
: (array
) - Array of decoded "BCC" addresses (often empty).bcccount
: (int
) - Count of "BCC" addresses.attachments
: (array
) - Array of non-inline attachments. Each is an array:filename
: (string
) - Decoded filename.content
: (string
) - Raw (decoded) attachment content.type
: (string
) - Decoded MIME subtype (e.g.,jpeg
,pdf
).
bid
: (string|null
) - A string value extracted from the subject using the regex provided to the constructor (or the default/#(\d+)/
). The value is taken directly from the first capturing group of the regex match.null
if the regex didn't match or the first capturing group wasn't found.unseen
: (bool
) -true
if\Unseen
flag was set at the time the message details were fetched. CheckcheckMailboxStatus
for current status.
Requirements
- PHP version 8.0 or higher.
- PHP IMAP extension enabled (
ext-imap
). - PHP Multibyte String extension enabled (
ext-mbstring
).
Notes
- Error Handling: The class uses exceptions for critical errors. Catch
RuntimeException
for IMAP/runtime errors andInvalidArgumentException
for bad input passed to methods. Use exception messages ($e->getMessage()
) for details, which includeimap_last_error()
where relevant. - Debug Mode: You can enable a debug mode by passing
true
as an argument to the connect() method or to the constructor (new IMAPEmailChecker($conn, true)
). When enabled, non-critical processing issues (e.g., skipping an invalid identifier in a list, failing to decode a specific attachment part, fallback during character encoding) will be logged viaerror_log()
for diagnostic purposes. These events are handled gracefully by the class and do not throw exceptions, but logging them can help identify problematic emails or configurations. Exceptions for critical errors are always thrown, regardless of debug mode. - Performance:
checkMailboxStatus
andsearch
are efficient for checking status or finding specific message IDs.checkSinceLastUID
is best for polling. Fetching full details (check*
,fetchMessagesByIds
) is slower. AvoidcheckAllEmail
on large mailboxes. - Security: Never hardcode credentials. Use environment variables or secure configuration methods. Ensure you connect via SSL/TLS.
- UID Focus: The class prioritizes UIDs for reliability.
- State Management: Be aware that only the main
check*
methods update the$messages
and$lastuid
properties.fetchMessagesByIds
returns its results directly without altering the main class state. - UTF-8: Textual content is normalized to UTF-8.
- Resource Management: The destructor closes the connection. Explicit
imap_close($connection)
after use is still good practice. - Flags: The
unseen
value in the message array is a snapshot. UsecheckMailboxStatus
orsetMessageReadStatus
for current flag states.\Recent
flag behavior varies by server. - Custom BID Extraction: You can provide a custom regular expression to the constructor (
$bidRegex
parameter) to extract an identifier string (called a "bid") from email subjects. Ensure your regex includes a capturing group for the ID string you want to extract. The default extracts numbers following a#
. This can be used for any sort of identifier that you wish to use such as a Ticket # found in the subject for a ticketing system.