-
Notifications
You must be signed in to change notification settings - Fork 122
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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
1 parent
b0f1836
commit 19e381a
Showing
14 changed files
with
288 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
26 changes: 26 additions & 0 deletions
26
database/migrations/2021_10_22_202820_add_postal_email_service_type.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(), | ||
] | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" /> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
27 changes: 27 additions & 0 deletions
27
src/Http/Controllers/Api/Webhooks/PostalWebhooksController.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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')); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters