Skip to content

Instantly share code, notes, and snippets.

@bitbay
Last active July 15, 2025 09:51
Show Gist options
  • Save bitbay/0e713dc9225283fb9807290c8bddb83e to your computer and use it in GitHub Desktop.
Save bitbay/0e713dc9225283fb9807290c8bddb83e to your computer and use it in GitHub Desktop.
Laravel - Postal mail transport
...
MAIL_MAILER=postal
...
# URL of postal service, without routes ('/api/...')
POSTAL_MAIL_API_URL=
# CLIENT API KEY configured in postal
POSTAL_MAIL_API_KEY=
...
<?php
/**
* app/Providers/AppServiceProvider.php
*/
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Mail;
use App\Mail\Transport\PostalTransport;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Mail::extend('postal', function (array $config = []) {
return new PostalTransport($config);
});
}
}
<?php
use App\Mail\WelcomeEmail;
use Illuminate\Support\Facades\Mail;
$user = ['name' => 'John Doe', 'email' => '[email protected]'];
// WelcomeEmail is created with `php artisan make:mail WelcomeEmail`
// and a corresponding blade template in resources/views/emails/welcome.blade.php
Mail::to($user['email'])->send(new WelcomeEmail());
<?php
/**
* config/mail.php
*/
return [
/*
|--------------------------------------------------------------------------
| Default Mailer
|--------------------------------------------------------------------------
|
| This option controls the default mailer that is used to send all email
| messages unless another mailer is explicitly specified when sending
| the message. All additional mailers can be configured within the
| "mailers" array. Examples of each type of mailer are provided.
|
*/
'default' => env('MAIL_MAILER', 'postal'),
/*
|--------------------------------------------------------------------------
| Mailer Configurations
|--------------------------------------------------------------------------
|
| Here you may configure all of the mailers used by your application plus
| their respective settings. Several examples have been configured for
| you and you are free to add your own as your application requires.
|
| Laravel supports a variety of mail "transport" drivers that can be used
| when delivering an email. You may specify which one you're using for
| your mailers below. You may also add additional mailers if needed.
|
| Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2",
| "postmark", "resend", "log", "array",
| "failover", "roundrobin"
|
*/
'mailers' => [
'postal' => [
'transport' => 'postal',
'api_url' => env('POSTAL_MAIL_API_URL'),
'api_key' => env('POSTAL_MAIL_API_KEY')
],
// ...
]
// ...
];
<?php
/**
* app/Mail/Transport/PostalTransport.php
*/
namespace App\Mail\Transport;
use Illuminate\Support\Facades\Http;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Log;
use Exception;
use Symfony\Component\Mailer\Envelope;
use Symfony\Component\Mailer\Exception\TransportException;
use Symfony\Component\Mailer\SentMessage;
use Symfony\Component\Mailer\Transport\AbstractTransport;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Email;
use Symfony\Component\Mime\MessageConverter;
class PostalTransport extends AbstractTransport
{
/**
* Create a new Postal transport instance.
*/
public function __construct(protected $config)
{
parent::__construct();
}
/**
* {@inheritDoc}
*/
protected function doSend(SentMessage $message): void
{
$email = MessageConverter::toEmail($message->getOriginalMessage());
$envelope = $message->getEnvelope();
try {
$response = Http::withHeaders([
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'X-Server-API-Key' => $this->config['api_key']
])->post($this->config['api_url'] . '/api/v1/send/message', $this->getPayload($email, $envelope));
$error = $this->getError($response);
if (!is_null($error)) {
Log::error('throwing error: ' . $error);
throw new Exception($error);
}
// Log::debug($response->json('status'));
// Log::debug($response->json('data')['message_id']);
} catch (Exception $exception) {
throw new TransportException(
sprintf('Request to the API failed. Reason: %s', $exception->getMessage()),
is_int($exception->getCode()) ? $exception->getCode() : 0,
$exception
);
}
$messageId = $response->json('data')['message_id'];
$email->getHeaders()->addHeader('X-Message-ID', $messageId);
}
private function getPayload(Email $email, Envelope $envelope): array
{
$payload = [
'from' => $envelope->getSender()->getAddress(),
'to' => array_map(fn(Address $address) => $address->getAddress(), $this->getRecipients($email, $envelope)),
'subject' => $email->getSubject(),
];
if ($emails = $email->getCc()) {
$payload['cc'] = array_map(fn(Address $address) => $address->getAddress(), $emails);
}
if ($emails = $email->getBcc()) {
$payload['bcc'] = array_map(fn(Address $address) => $address->getAddress(), $emails);
}
if ($email->getTextBody()) {
$payload['plain_body'] = $email->getTextBody();
}
if ($email->getHtmlBody()) {
$payload['html_body'] = $email->getHtmlBody();
}
if ($attachments = $this->prepareAttachments($email)) {
$payload['attachments'] = $attachments;
}
if ($headers = $this->getCustomHeaders($email)) {
$payload['headers'] = $headers;
}
if ($emails = $email->getReplyTo()) {
$payload['reply_to'] = $emails[0]->getAddress();
}
return $payload;
}
private function prepareAttachments(Email $email): array
{
$attachments = [];
foreach ($email->getAttachments() as $attachment) {
$attachments[] = [
'name' => $attachment->getFilename(),
'content_type' => $attachment->getContentType(),
'data' => base64_encode($attachment->getBody()),
];
}
return $attachments;
}
private function getCustomHeaders(Email $email): array
{
$headers = [];
$headersToBypass = ['from', 'to', 'cc', 'bcc', 'subject', 'content-type', 'sender', 'reply-to'];
foreach ($email->getHeaders()->all() as $name => $header) {
if (in_array($name, $headersToBypass, true)) {
continue;
}
$headers[] = [
'key' => $header->getName(),
'value' => $header->getBodyAsString(),
];
}
return $headers;
}
/**
* @return Address[]
*/
private function getRecipients(Email $email, Envelope $envelope): array
{
return array_filter($envelope->getRecipients(), fn(Address $address) => false === in_array($address, array_merge($email->getCc(), $email->getBcc()), true));
}
private function getError(Response $response): ?string
{
if (!$response->successful()) {
return 'HTTP Request not successful';
}
if (200 !== $response->status()) {
return sprintf('HTTP STATUS: %d', $response->status());
}
if ('success' !== $response->json('status')) {
if (is_null($response->json('status'))) {
return 'Postal empty response';
} else {
return sprintf('Postal status: %s', $response->json('status'));
}
} else if (is_null($response->json('data'))) {
if (is_null($response->json('message'))) {
return 'Postal empty data';
} else {
return sprintf('Postal message: %s', $response->json('message'));
}
}
return null;
}
/**
* Get the string representation of the transport.
*/
public function __toString(): string
{
return 'postal';
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment