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.1
ext-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 themail
function. A properly configured server is required for this to workSmtpAdapter
- Uses the PHPMailer library to provideSMTP
capabilities
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::PlainText
orMessageType::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::CC
orRecipientType::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
- AMailBox
instance, 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;
}
}