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 the mail function. A properly configured server is required for this to work
  • SmtpAdapter - Uses the PHPMailer library to provide SMTP 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 either MessageType::PlainText or MessageType::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, optional string
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, either RecipientType::To, RecipientType::CC or RecipientType::BCC, defaults to RecipientType::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 like Foo Bar <[email protected]>
  • MailBox - A MailBox instance, you can create one by calling new 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 attachment
  • AttachmentType::Inline - Inline attachment
  • AttachmentType::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;
    }
}