sfaut / zimbra
Search, read and send simply Zimbra messages with PHP. Attachments are managed.
Requires
- php: >=7
This package is auto-updated.
Last update: 2024-05-20 15:51:09 UTC
README
Read and send simply Zimbra messages. Attachments are managed.
Based on Zimbra 8 SOAP API.
Introduction
- E-mail messages are versatile.
sfaut\Zimbra
is a relatively low-level class whose aim is to provide a simple anonymous object representing a message and its main components.sfaut\Zimbra
also provides some helper methods to send and get messages, upload and download attachments, explore directories structure, and make a search. - KISS – Fire & Forget
Composer installation
You can add sfaut\Zimbra
package to your project with Composer :
> composer require sfaut/zimbra
And include it as usual with autoloader :
<?php require_once '/path/to/vendor/autoload.php'; $zimbra = sfaut\Zimbra::authenticate(...); // ...
Raw installation
sfaut\Zimbra
is an unique class with no dependancies,
so you can download /src/Zimbra.php
and include it in your script like any others scripts.
<?php require_once '/path/to/sfaut/src/Zimbra.php'; $zimbra = sfaut\Zimbra::authenticate(...); // ...
Object structure
Here is a search response structure within an array of messages. A message is an anonymous object.
[
{
id: <Message ID, useful for internal usage like attachment download>
mid: <Another message ID, useful for querying a specific message>
folder: <Folder ID>
conversation: <Conversation ID>
timestamp: <Message creation, format "Y-m-d H:i:s">
subject: <Message subject>
addresses: {
to: [...]
from: [...]
cc: [...]
}
fragment: <Fragment of the message>
flags: <Message flags>
size: <Message size, in bytes>
body: <Message body>
attachments: [
{
part: <Attachment's part message>
disposition: <MIME disposition, "inline" or "attachment">
type: <MIME type, eg. "text/csv">
size: <Attachment size, in bytes>
basename: <Attachment basename (with extension), eg. "Report.csv">
filename: <Attachment filename (without extension), eg. "Report">
extension: <Attachment extension without dot, eg. "csv">
}
...
]
}
...
]
Connection
Static method Zimbra::authenticate()
creates a new sfaut\Zimbra
instance and immediately connects to Zimbra server.
An exception is raised on failure.
<?php use sfaut\Zimbra; require_once '/path/to/sfaut/src/Zimbra.php'; $host = 'https://zimbra.example.net'; $user = 'root@example.net'; $password = 'M;P455w0r|)'; $zimbra = Zimbra::authenticate($host, $user, $password);
To shorten following examples,
use
,require
and others out of scope parts will be snipped. Assumesfaut\Zimbra
is instanciate in$zimbra
.
Error management
sfaut\Zimbra
is exceptions-oriented.
So, you should encapsulate statements within try / catch / finally
blocks.
try { $zimbra = Zimbra::authenticate($host, $user, $password); // ... } catch (Exception $e) { echo $e->getMessage(); exit(1); }
To shorten following examples, exceptions management will be snipped.
Get mailbox messages
All messages listing are, in reality, search result.
Search is performed with Zimbra::search()
method and Zimbra client search capacities.
Zimbra search tips have been archived on this gist.
$folder = '/Inbox'; $messages = $zimbra->search(['in' => $folder]); foreach ($messages as $i => $message) { printf( "%6d %s %40s %s\r\n", $message->id, $message->timestamp, $message->from[0], $message->subject, ); }
Send a message
$addresses = ['to' => 'user@example.net']; $subject = 'Hello!'; $body = <<<BUFFER Dear Sir,\n URGENT BUSINESS PROPOSAL\n ... BUFFER; $zimbra->send($addresses, $subject, $body);
Send a message to multiple recipients
You can use arrays to specify multiple e-mail addresses :
// 1 mail to 3 direct recipients, 1 carbon copy, 1 blind carbon copy // Response to trash $addresses = [ 'to' => ['user1@example.net', 'user2@example.net', 'user3@example.net'], 'cc' => 'ml@example.net', 'bcc' => 'archive@example.net', 'r' => 'trash@example.net', ]; $zimbra->send($addresses, $subject, $body);
Send a message with attachment
4th Zimbra::send()
parameter is an array of attachments.
Basically, an attachment is an anonymous object (or an array) containing 2 properties :
basename
: the name and extension of the attached filetype
: containing the attachment natur
Possible type
values are :
file
: the value represents the file full path to attachbuffer
: the value represents the raw data to attachstream
: the value is the stream resource to attach- Attachment ID : a string given when uploading a file previously
Attach a locale file to a message :
$attachments = [ ['basename' => 'data.csv', 'file' => '/path/to/data.csv'], ]; $zimbra->send($addresses, $subject, $body, $attachments);
You can also attach multiple files in a row :
$attachments = [ ['basename' => 'data-1.csv', 'file' => '/path/to/data-1.csv'], ['basename' => 'data-2.csv', 'file' => '/path/to/data-2.csv'], ['basename' => 'data-3.csv', 'file' => '/path/to/data-3.csv'], ]; $zimbra->send($addresses, $subject, $body, $attachments);
And you can mix different types of data sources :
$buffer = 'Contents that will be attached to a file'; $stream = fopen('/path/to/file.csv', 'r'); $attachments = [ ['basename' => 'data-1.csv', 'file' => '/path/to/data.csv'], ['basename' => 'data-2.txt', 'buffer' => $buffer], ['basename' => 'data-3.csv', 'stream' => $stream], ]; $zimbra->send($addresses, $subject, $body, $attachments);
Eeach attachment is uploaded while sending message.
That can be unnecessarily resource-consuming if you send multiple messages with the same attachments.
To save resources, you can first upload files with Zimbra::upload()
, then attach them to messages.
// ⚠️ YOU SHOULD NOT DO THIS // The same file uploaded 3 times for 3 messages $attachments = [ ['basename' => 'decennial-data.csv', 'file' => '/path/to/decennial-data.csv'], ['basename' => 'another-data.csv', 'file' => '/path/to/another-data.csv'], ]; $zimbra->send($addresses_1, $subject, $body, $attachments); // 🙅🏻♂️ $zimbra->send($addresses_2, $subject, $body, $attachments); // 🙅🏻♂️ $zimbra->send($addresses_3, $subject, $body, $attachments); // 🙅🏻♂️ // 💡 YOU SHOULD DO THAT // 1 upload for 3 messages $attachments = [ ['basename' => 'decennial-data.csv', 'file' => '/path/to/decennial-data.csv'], ['basename' => 'another-data.csv', 'file' => '/path/to/another-data.csv'], ]; // 🥂 That's the trick // An attachment ID is retrieved and reused as necessary $attachments = $zimbra->upload($attachments); $zimbra->send($addresses_1, $subject, $body, $attachments); $zimbra->send($addresses_2, $subject, $body, $attachments); $zimbra->send($addresses_3, $subject, $body, $attachments);
Attachments
Message attachments are specified in array $message->attachments
.
Attachments can be uploaded with Zimbra::upload()
and downloaded with Zimbra::download()
.
Each attachment is an anonymous object having the following structure :
{ part: <MIME part of the attachment in the message, eg. "2" or "2.1.1"> disposition: <Attachment method to the message : "attachment" or "inline"> type: <MIME type of the attachment file, eg. "text/plain", "text/csv"> size: <Attachement file size in bytes> basename: <Attachement file name with extension, eg. "my-data.csv"> filename: <Attachment file name without extension, eg. "my-data"> extension: <Attachment extension without dot, eg. "csv"> stream: <Stream from temporary, only after Zimbra::download() call> }
Attachments retrieving
All attachments of a message are retrieved in an array returned by Zimbra::download()
.
Attachments are downloaded in temporary files.
These temporary files are automatically deleted at script end.
Each attachment is an anonymous object containing, among other things,
a stream
property pointing to the temporary file, and ready to read and write.
$message = $zimbra->search(['subject' => 'Annual result 2022'])[0]; // Get 1 message $attachment = $zimbra->download($message)[0]; // Get 1 attachment // Where you want to save your file $destination_file = '/path/to/' . $attachment->basename; // 1st method, with stream // Memory efficient $destination_stream = fopen($destination_file, 'w'); stream_copy_to_stream($attachment->stream, $destination_stream); // 2nd method, with buffer // Memory inefficient on huge files, but you can process $buffer $buffer = stream_get_contents($attachment->stream); file_put_contents($destination_file, $buffer);
You can filter attachments to retrieve with a closure
accepting an attachment object as parameter (returning true
by default).
$message = $zimbra->search(['subject' => 'Summer 2022 holidays photos'])[0]; // Your filter closure // You need to keep only images attachments $filter = fn ($attachment) => strpos($attachment->type, 'image/') === 0; $attachments = $zimbra->download($message, $filter); foreach ($attachments as $attachment) { // Save the downloaded attachments / photos to a safe place $stream_destination = fopen('/home/me/holidays/' . $attachment->basename, 'w'); stream_copy_to_stream($attachment->stream, $stream_destination); }
Real-life use case
Mass download attachments
- You need to download tons of messages CSV attachments, deadline : yesterday
- Messages are stored on mailbox in folder
/Inbox/Reports
- Each message has 0 to n attachments
- Attachments can be of any types like
.csv
,.xlsx
,.pdf
, etc., and you need to retrieve only.csv
- CSV files are named in the following format :
Report Y-m-d.csv
, eg.Report 2022-03-06.csv
- Filename, and its extension, can be in lower or upper case, or mix, you need to manage that
- You must download all CSV attachments starting
2020-01-01
, eg.Report 2019-12-31.csv
is not downloaded whereasReport 2020-01-01.csv
is downloaded - There are a lot of files, so you must save them as Gzip files
- Each message subject is unique, but each attachment name is not, so attachments downloaded must have a name in format
Message subject -- Attachment basename.gz
, eg.Report 2020-01-01.csv.gz
- Target directory is the locale subdirectory
/mailbox/reports
PHP and sfaut\Zimbra
allows you to do that easily :)
<?php use sfaut\Zimbra; require_once '/path/to/vendor/autoload.php'; require_once '/path/to/settings.php'; // Starting attachment, you choose an all upper case reference $starting_file = 'REPORT 2020-01-01.CSV'; // Mailbox source folder $source_folder = '/Inbox/Reports'; // Locale target subdirectory, where all CSV attachments will be downloaded $target_directory = '/path/to/mailbox/reports'; $zimbra = Zimbra::authenticate($host, $user, $password); // Search messages in source folder that have at least one CSV attachment begining with "Report" // You reduce unuseful messages retrieving $messages = $zimbra->search(['in' => $source_folder, 'filename' => 'Report*', 'type' => 'text/csv']); foreach ($messages as $message) { // Download attachments, only what you need $attachments = $zimbra->download($message, function ($attachment) { if (strtoupper($attachment->extension) !== 'CSV') { return false; } if (strtoupper($attachment->basename) < $starting_file) { return false; } return true; }); // Save CSV attachments in compressed files in safe place foreach ($attachments as $attachment) { $target_file = "{$target_directory}/{$message->subject} -- {$attachment->basename}.gz"; $target_stream = gzopen($target_file, 'w'); stream_copy_to_stream($attachment->stream, $target_stream); } } exit(0);
That's all Folks! 🐰