Skip to content

Commit

Permalink
Add Postal Mail Service (#139)
Browse files Browse the repository at this point in the history
* add postal emailservice

* Postal

* Postal

* Postal Webhooks

* Postal Webhooks

* Postal Webhooks

* Postal Webhooks

* Delete .DS_Store

* Create 2021_10_22_202820_add_postal_email_service_type.php

* Update 2021_10_22_202820_add_postal_email_service_type.php

* Update PostalAdapter.php

Co-authored-by: Ruben Muehlhans <ruben@iMac.local>
  • Loading branch information
rubenmuehlhans and Ruben Muehlhans authored Mar 17, 2022
1 parent b0f1836 commit 19e381a
Show file tree
Hide file tree
Showing 14 changed files with 288 additions and 1 deletion.
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"nyholm/psr7": "^1.3",
"rap2hpoutre/fast-excel": "^2.3",
"sendgrid/sendgrid": "^7.9",
"wildbit/postmark-php": "^4.0"
"wildbit/postmark-php": "^4.0",
"postal/postal": "^1.0"
},
"require-dev": {
"orchestra/testbench": "^6.0",
Expand Down
Empty file modified config/config.php
100755 → 100644
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
use Sendportal\Base\Models\EmailServiceType;

class AddPostalEmailServiceType extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
DB::table('sendportal_email_service_types')
->insert(
[
'id' => EmailServiceType::POSTAL,
'name' => 'Postal',
'created_at' => now(),
'updated_at' => now(),
]
);
}
}
2 changes: 2 additions & 0 deletions resources/views/email_services/options/postal.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<x-sendportal.text-field name="settings[postal_host]" :label="__('Postal Host')" :value="Arr::get($settings ?? [], 'postal_host')" />
<x-sendportal.text-field name="settings[key]" :label="__('API Key')" :value="Arr::get($settings ?? [], 'key')" autocomplete="off" />
46 changes: 46 additions & 0 deletions src/Adapters/PostalAdapter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

declare(strict_types=1);

namespace Sendportal\Base\Adapters;

use DomainException;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Postal\Client;
use Postal\SendMessage;
use Sendportal\Base\Services\Messages\MessageTrackingOptions;
use Symfony\Component\HttpFoundation\Response;

class PostalAdapter extends BaseMailAdapter
{

/**
* @throws TypeException
* @throws \Throwable
*/
public function send(string $fromEmail, string $fromName, string $toEmail, string $subject, MessageTrackingOptions $trackingOptions, string $content): string
{
$client = new Client('https://' . Arr::get($this->config, 'postal_host'), Arr::get($this->config, 'key'));

$message = new SendMessage($client);
$message->to($toEmail);
$message->from($fromName.' <'.$fromEmail.'>');
$message->subject($subject);
$message->htmlBody($content);
$response = $message->send();

return $this->resolveMessageId($response);
}



protected function resolveMessageId($response): string
{
foreach ($response->recipients() as $email => $message) {
return (string) $message->id();
}

throw new DomainException('Unable to resolve message ID');
}
}
14 changes: 14 additions & 0 deletions src/Events/Webhooks/PostalWebhookReceived.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace Sendportal\Base\Events\Webhooks;

class PostalWebhookReceived
{
/** @var array */
public $payload;

public function __construct(array $payload)
{
$this->payload = $payload;
}
}
2 changes: 2 additions & 0 deletions src/Factories/MailAdapterFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Sendportal\Base\Adapters\SendgridMailAdapter;
use Sendportal\Base\Adapters\SesMailAdapter;
use Sendportal\Base\Adapters\SmtpAdapter;
use Sendportal\Base\Adapters\PostalAdapter;
use Sendportal\Base\Interfaces\MailAdapterInterface;
use Sendportal\Base\Models\EmailService;
use Sendportal\Base\Models\EmailServiceType;
Expand All @@ -25,6 +26,7 @@ class MailAdapterFactory
EmailServiceType::POSTMARK => PostmarkMailAdapter::class,
EmailServiceType::MAILJET => MailjetAdapter::class,
EmailServiceType::SMTP => SmtpAdapter::class,
EmailServiceType::POSTAL => PostalAdapter::class,
];

/**
Expand Down
27 changes: 27 additions & 0 deletions src/Http/Controllers/Api/Webhooks/PostalWebhooksController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace Sendportal\Base\Http\Controllers\Api\Webhooks;

use Illuminate\Support\Arr;

use Illuminate\Http\Response;
use Illuminate\Support\Facades\Log;
use Sendportal\Base\Events\Webhooks\PostalWebhookReceived;
use Sendportal\Base\Http\Controllers\Controller;

class PostalWebhooksController extends Controller
{
public function handle(): Response
{
$payload = json_decode(request()->getContent(), true);

Log::info('Postal webhook received');

event(new PostalWebhookReceived($payload));


return response('OK');
}
}
160 changes: 160 additions & 0 deletions src/Listeners/Webhooks/HandlePostalWebhook.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
<?php

declare(strict_types=1);

namespace Sendportal\Base\Listeners\Webhooks;

use Carbon\Carbon;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use RuntimeException;
use Sendportal\Base\Events\Webhooks\PostalWebhookReceived;
use Sendportal\Base\Services\Webhooks\EmailWebhookService;

class HandlePostalWebhook implements ShouldQueue
{
/** @var string */
public $queue = 'sendportal-webhook-process';

/** @var EmailWebhookService */
private $emailWebhookService;

public function __construct(EmailWebhookService $emailWebhookService)
{
$this->emailWebhookService = $emailWebhookService;
}

public function handle(PostalWebhookReceived $event): void
{

$messageId = $this->extractMessageId($event->payload);
$eventName = $this->extractEventName($event->payload);

Log::info('Processing Postal webhook.', ['type' => $eventName, 'message_id' => $messageId]);

switch ($eventName) {
case 'MessageSent':
$this->handleDelivered($messageId, $event->payload);
break;

case 'MessageLoaded':
$this->handleOpen($messageId, $event->payload);
break;

case 'MessageLinkClicked':
$this->handleClick($messageId, $event->payload);
break;

case 'MessageBounced':
$messageId = $this->extractMessageIdBounced($event->payload);
$this->handleBounce($messageId, $event->payload);
break;

case 'MessageDeliveryFailed':
$this->handleFailed($messageId, $event->payload);
break;

case 'MessageHeld':
$this->handleHeld($messageId, $event->payload);
break;

default:
throw new RuntimeException("Unknown Postal webhook event type '{$eventName}'.");
}
}

private function handleDelivered(string $messageId, array $content): void
{
$timestamp = $this->extractTimestamp($content);

$this->emailWebhookService->handleDelivery($messageId, $timestamp);
}

private function handleOpen(string $messageId, array $content): void
{
$ipAddress = Arr::get($content, 'ip');
$timestamp = $this->extractTimestamp($content);

$this->emailWebhookService->handleOpen($messageId, $timestamp, $ipAddress);
}

private function handleClick(string $messageId, array $content): void
{
$url = Arr::get($content, 'payload.url');
$timestamp = $this->extractTimestamp($content);

$this->emailWebhookService->handleClick($messageId, $timestamp, $url);
}

private function handleBounce(string $messageId, array $content): void
{
$timestamp = $this->extractTimestampBounced($content);
$description = Arr::get($content, 'payload.bounce.subject');

$this->emailWebhookService->handleFailure($messageId, 'Permanent', $description, $timestamp);

$this->emailWebhookService->handlePermanentBounce($messageId, $timestamp);
}

private function handleFailed(string $messageId, array $content): void
{
$severity = Arr::get($content, 'payload.status');
$description = Arr::get($content, 'payload.output');
$timestamp = $this->extractTimestampFailed($content);

$this->emailWebhookService->handleFailure($messageId, $severity, $description, $timestamp);

if ($severity === 'HardFail') {
$this->emailWebhookService->handlePermanentBounce($messageId, $timestamp);
}
}

private function handleHeld(string $messageId, array $content): void
{
$severity = Arr::get($content, 'payload.status');
$description = Arr::get($content, 'payload.details');
$timestamp = $this->extractTimestampFailed($content);

$this->emailWebhookService->handleFailure($messageId, $severity, $description, $timestamp);

if ($severity === 'Held') {
$this->emailWebhookService->handlePermanentBounce($messageId, $timestamp);
}
}

private function extractEventName(array $payload): string
{
return Arr::get($payload, 'event');
}

private function extractMessageIdBounced(array $payload): string
{
$messageId = Arr::get($payload, 'payload.original_message.id');

return trim((string) $messageId);
}

private function extractMessageId(array $payload): string
{
$messageId = Arr::get($payload, 'payload.message.id');

return trim((string) $messageId);
}

private function extractTimestampBounced($payload): Carbon
{
return Carbon::createFromTimestamp(Arr::get($payload, 'payload.bounce.timestamp'));
}

private function extractTimestampFailed($payload): Carbon
{
return Carbon::createFromTimestamp(Arr::get($payload, 'payload.timestamp'));
}

private function extractTimestamp($payload): Carbon
{
return Carbon::createFromTimestamp(Arr::get($payload, 'timestamp'));
}
}
2 changes: 2 additions & 0 deletions src/Models/EmailServiceType.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class EmailServiceType extends BaseModel
public const POSTMARK = 4;
public const MAILJET = 5;
public const SMTP = 6;
public const POSTAL = 7;

/** @var array */
protected static $types = [
Expand All @@ -23,6 +24,7 @@ class EmailServiceType extends BaseModel
self::POSTMARK => 'Postmark',
self::MAILJET => 'Mailjet',
self::SMTP => 'SMTP',
self::POSTAL => 'Postal',
];

/**
Expand Down
5 changes: 5 additions & 0 deletions src/Providers/EventServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@
use Sendportal\Base\Events\Webhooks\PostmarkWebhookReceived;
use Sendportal\Base\Events\Webhooks\SendgridWebhookReceived;
use Sendportal\Base\Events\Webhooks\SesWebhookReceived;
use Sendportal\Base\Events\Webhooks\PostalWebhookReceived;
use Sendportal\Base\Listeners\MessageDispatchHandler;
use Sendportal\Base\Listeners\Webhooks\HandleMailgunWebhook;
use Sendportal\Base\Listeners\Webhooks\HandleMailjetWebhook;
use Sendportal\Base\Listeners\Webhooks\HandlePostmarkWebhook;
use Sendportal\Base\Listeners\Webhooks\HandleSendgridWebhook;
use Sendportal\Base\Listeners\Webhooks\HandleSesWebhook;
use Sendportal\Base\Listeners\Webhooks\HandlePostalWebhook;

class EventServiceProvider extends ServiceProvider
{
Expand Down Expand Up @@ -43,6 +45,9 @@ class EventServiceProvider extends ServiceProvider
MailjetWebhookReceived::class => [
HandleMailjetWebhook::class
],
PostalWebhookReceived::class => [
HandlePostalWebhook::class
],
SubscriberAddedEvent::class => [
// ...
],
Expand Down
1 change: 1 addition & 0 deletions src/Routes/ApiRoutes.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public function sendportalPublicApiRoutes(): callable
$webhookRouter->post('postmark', 'PostmarkWebhooksController@handle')->name('postmark');
$webhookRouter->post('sendgrid', 'SendgridWebhooksController@handle')->name('sendgrid');
$webhookRouter->post('mailjet', 'MailjetWebhooksController@handle')->name('mailjet');
$webhookRouter->post('postal', 'PostalWebhooksController@handle')->name('postal');
});

$this->get('v1/ping', '\Sendportal\Base\Http\Controllers\Api\PingController@index');
Expand Down
Empty file modified src/SendportalBaseServiceProvider.php
100755 → 100644
Empty file.
1 change: 1 addition & 0 deletions src/Services/QuotaService.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public function exceedsQuota(EmailService $emailService, int $messageCount): boo
case EmailServiceType::POSTMARK:
case EmailServiceType::MAILJET:
case EmailServiceType::SMTP:
case EmailServiceType::POSTAL:
return false;
}

Expand Down

0 comments on commit 19e381a

Please # to comment.