Overview
Description
Caldera Mailer is a mailing abstraction layer.
As with the other Caldera components it has been built to be swappable and modular.
Installation
The easisest way to install it is to use Composer:
composer require vecode/caldera-mailer
Requires
php: >=8.1ext-mbstring: *
Basic usage
Getting started
Sending mail is usually a chore. There are lots of ways of sending mail messages, from the dreaded mail function to the SMTP protocol to transactional services via API.
What happens when you implement a site and then change the delivery method? A refactor is required! And it's not only about sending the message, but adding recipients, attachments, setting the format, etc.
The aim of this component is to ease that, make changes painless and create an standard way to prepare then send the messages.
With just a few lines required to send a mailing:
use Caldera\Mailer\Mailer;
use Caldera\Mailer\Adapter\MailAdapter;
use Caldera\Mailer\Message;
use Caldera\Mailer\MessageType;
$options = [];
$adapter = new MailAdapter($options);
$mailer = new Mailer($adapter);
$message = new Message('Test', '<h1>This is a test</h1>', MessageType::Html);
$message->setSender('[email protected]', 'Test')
->addRecipient('[email protected]')
$sent = $mailer->send($message);
Creating and adapter
First you create an instance of an AdapterInterface implementation. Several implementations can exist, each of them abstracting a protocol, service, etc.; for example there are two included:
MailAdapter- Uses themailfunction. A properly configured server is required for this to workSmtpAdapter- Uses the PHPMailer library to provideSMTPcapabilities
Each adapter may require different options, for example, with SmtpAdapter you may do:
use Caldera\Mailer\Mailer;
use Caldera\Mailer\Adapter\SmtpAdapter;
use Caldera\Mailer\Message;
use Caldera\Mailer\MessageType;
$options = [
'host' => 'smtp.example.com',
'port' => '445',
'user' => 'test',
'password' => 'pAsSwOrD',
];
$adapter = new SmtpAdapter($options);
$mailer = new Mailer($adapter);
$message = new Message('Test', '<h1>This is a test</h1>', MessageType::Html);
$message->setSender('[email protected]', 'Test')
->addRecipient('[email protected]')
$sent = $mailer->send($message);
This way if you switch to a new delivery protocol/service you just need to implement your own AdapterInterface and your sending logic will not be modified: using a container implementation you can set your mailing adapter (or even better, the Mailer instance) as a service and then use dependency injection to get a ready-to-use instance each time.
Creating the Mailer instance
Once you've created the adapter, you will need to create a Mailer instance, passing the AdapterInterface implementation instance:
$mailer = new Mailer($adapter);
The Message object
Then you'll need to create a Message instance:
$message = new Message('Test', '<h1>This is a test</h1>', MessageType::Html);
The constructor takes three parameters:
$subject- The message subject,string$body- The message contents,string$type- The message type, can be eitherMessageType::PlainTextorMessageType::Html
The Message object contains all the properties of the mailing, such as recipients, the sender, attachments, etc.
Setting the sender
For example, to set the sender use the setSender method:
$message->setSender('[email protected]', 'Test')
There are two parameters for this method:
$sender- The sender,mixed$name- Sender name, optionalstring
Adding recipients
And to add recipients use the addRecipient method:
$message->addRecipient('[email protected]')
This method takes two parameters:
$recipient- The recipient,mixed$type- Recipient type, eitherRecipientType::To,RecipientType::CCorRecipientType::BCC, defaults toRecipientType::To
Both setSender and addRecipient methods accept an string or MailBox variable for its first parameter:
string- Just the email, like[email protected]or an RFC 5322 mailbox likeFoo Bar <[email protected]>MailBox- AMailBoxinstance, you can create one by callingnew MailBox($address, $name)
In the case of addRecipient you may also pass an array<string>, each with a recipient in either plain address or RFC 5322 format:
$message->addRecipient(['Copy <[email protected]>', '[email protected]'], RecipientType::CC);
Adding attachments
The easiest way is to just pass the content of the attachment and its name:
$message->addAttachment('Lorem ipsum', 'lorem.txt')
This may work for smaller files (like single-line keys or codes for example) but for larger files you may pass an stream handle:
$handle = fopen('lorem.txt', 'r');
$message->addAttachment($handle, 'lorem.txt');
fclose($handle);
You may also pass image handles directly, for on-the-fly generated images:
$image = imagecreatetruecolor(10, 10);
$message->addAttachment($image, 'lorem.png');
imagedestroy($image);
On each call you may pass a third parameter, $type which can be:
AttachmentType::Regular- Regular attachmentAttachmentType::Inline- Inline attachmentAttachmentType::EmbeddedImage- Embedded image attachment
These types may affect the way the attachment is processed depending on the selected $adapter; for example MailAdapter does not support attachments and EmbeddedImage supports both AttachmentType::Regular and AttachmentType::EmbeddedImage but not AttachmentType::Inline.
Sending the message
Once you've set the sender and content, added recipients and maybe attachments, just call the send method to send your message:
$mailer->send($message);
It will return a bool with the result of the operation, which again, depends on the selected $adapter.
Advanced topics
Getting the driver
Sometimes you will need to change something directly on the driver used by the adapter. To get it just call the getDriver method:
$driver = $adapter->getDriver();
Once you've got a hold of the driver you can interact with it directly, for example, this changes some settings on the SmtpAdapter driver, a PHPMailer instance:
$driver->SMTPDebug = 2
$driver->Debugoutput = 'error_log';
Creating adapters
To create a custom adapter you'll just need to implement your own version of AdapterInterface.
Also, for convenience you can just extend the AbstractAdapter class, just remember to override the getDriver method if required.
For example, to interact with an API you will require either an HTTP client (like Guzzle) or the provider's SDK; either way you must at least implement the send method, which receives the Message instance and must return a bool upon completion.
The main thing to do is take the received Message instance, extract its properties, create the payload and call the provider's API using the required protocol. Depending on the response you return either true or false.
Using dependency injection you can provide a logger instance for example to log the results, or a container to retrieve additional services, settings, etc.
This is a rough sample of a MailJet adapter, using the Client class from Caldera HTTP for communication and Settings from Caldera Settings to get the configuration values:
use Caldera\Http\Client;
use Caldera\Mailer\Adapter\AbstractAdapter;
use Caldera\Mailer\AttachmentType;
use Caldera\Mailer\Message;
use Caldera\Mailer\RecipientType;
use Caldera\Settings\Settings;
class MailjetAdapter extends AbstractAdapter {
/**
* API endpoint URL
* @var string
*/
protected $endpoint;
/**
* Sandbox flag
* @var bool
*/
protected $sandbox;
/**
* API key
* @var string
*/
protected $key;
/**
* API secret
* @var string
*/
protected $secret;
/**
* HTTP Client
* @var Client
*/
protected $client;
/**
* Constructor
* @param Client $client Client instance
* @param Settings $settings Settings instance
*/
public function __construct(Client $client, Settings $settings) {
$this->client = $client;
$this->endpoint = $settings->get('mail.mailjet.endpoint', true);
$this->sandbox = $settings->get('mail.mailjet.sandbox', true);
$this->key = $settings->get('mail.mailjet.key');
$this->secret = $settings->get('mail.mailjet.secret');
}
/**
* Send a message
* @param Message $message Message instance
* @return bool
*/
public function send(Message $message): bool {
# Add headers
$headers = [];
$headers['Authorization'] = sprintf("Basic %s", base64_encode("{$this->key}:{$this->secret}"));
$headers['Content-Type'] = 'application/json';
# Get recipients
$to = $message->getRecipients(RecipientType::To);
$cc = $message->getRecipients(RecipientType::CC);
$bcc = $message->getRecipients(RecipientType::BCC);
# Set from field
$from = [
'Email' => $message->getSender()->getAddress(),
'Name' => $message->getSender()->getName(),
];
# Set base recipients
$recipients = [];
foreach ($to as $recipient) {
$recipients[] = [
'Email' => $recipient->getAddress(),
'Name' => $recipient->getName(),
];
}
# Set CC and BCC addresses
$cc_recipients = [];
$bcc_recipients = [];
if ($cc) {
foreach ($cc as $recipient) {
$cc_recipients[] = [
'Email' => $recipient->getAddress(),
'Name' => $recipient->getName(),
];
}
}
if ($bcc) {
foreach ($bcc as $recipient) {
$bcc_recipients[] = [
'Email' => $recipient->getAddress(),
'Name' => $recipient->getName(),
];
}
}
# Now add attachments
$attachments = [];
$inlined_attachments = [];
if ( $message->hasAttachments() ) {
foreach ($message->getAttachments() as $attachment) {
# Detect MIME type
$finfo =finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_buffer($finfo, $attachment->getContents(), FILEINFO_MIME_TYPE);
finfo_close($finfo);
# And add attachment
if ( $attachment->getType() == AttachmentType::EmbeddedImage ) {
$cid = pathinfo($attachment->getName(), PATHINFO_FILENAME);
$inlined_attachments[] = [
'ContentType' => $mime,
'Filename' => $attachment->getName(),
'ContentID' => $cid,
'Base64Content' => base64_encode( $attachment->getContents() )
];
} else {
$attachments[] = [
'ContentType' => $mime,
'Filename' => $attachment->getName(),
'Base64Content' => base64_encode( $attachment->getContents() )
];
}
}
}
# Create payload object
$payload = [
'From' => $from,
'To' => $recipients,
'Cc' => $cc_recipients,
'Bcc' => $bcc_recipients,
'Subject' => $message->getSubject(),
'TextPart' => strip_tags( $message->getBody() ),
'HTMLPart' => $message->getBody(),
'Attachments' => $attachments,
'InlinedAttachments' => $inlined_attachments,
];
# And prepare request
$fields = [];
$fields['SandboxMode'] = $this->sandbox;
$fields['Messages'] = [$payload];
# Set request body
$body = json_encode($fields);
# And execute it
$response = $this->client->post($this->endpoint, [
'headers' => $headers,
'body' => $body,
]);
return $response->getStatusCode() === 200;
}
}