Skip to content

Commit

Permalink
Merge pull request #2 from chijioke-ibekwe/add-ses-email-provider
Browse files Browse the repository at this point in the history
feat: implement amazon ses email provider
  • Loading branch information
chijioke-ibekwe authored Mar 12, 2024
2 parents c5885d4 + d151a0d commit d923bfa
Show file tree
Hide file tree
Showing 16 changed files with 374 additions and 160 deletions.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ build
composer.lock
coverage
docs
phpunit.xml
phpstan.neon
testbench.yaml
vendor
Expand Down
14 changes: 12 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,27 @@
"description": "Multi-channel Laravel notification sender",
"type": "library",
"license": "MIT",
"keywords": [
"laravel",
"sendgrid",
"notifications",
"sendgrid-api",
"laravel-package"
],
"authors": [
{
"name": "Chijioke Ibekwe",
"email": "ibekwe.chijioke18@gmail.com"
}
],
"homepage": "https://github.com/chijioke-ibekwe/raven",
"require": {
"sendgrid/sendgrid": "~7"
"sendgrid/sendgrid": "~7",
"aws/aws-sdk-php": "^3.300",
"phpmailer/phpmailer": "^6.9"
},
"require-dev": {
"orchestra/testbench": "7.0",
"orchestra/testbench": "^6.0",
"phpunit/phpunit": "^9.6"
},
"autoload": {
Expand Down
27 changes: 19 additions & 8 deletions config/raven.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,31 @@

return [

'notification-service' => [
'default' => [
'email' => env('EMAIL_NOTIFICATION_PROVIDER', 'sendgrid'),
'sms' => env('SMS_NOTIFICATION_PROVIDER', 'nexmo')
],

'api-key' => [
'sendgrid' => env('SENDGRID_API_KEY')
'providers' => [
'sendgrid' => [
'key' => env('SENDGRID_API_KEY')
],
'ses' => [
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'template_source' => env('AWS_SES_TEMPLATE_SOURCE', 'sendgrid'),
'template_directory' => env('AWS_SES_TEMPLATE_DIRECTORY', 'resources/views/emails')
]
],

'mail' => [
'from' => [
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
'name' => env('MAIL_FROM_NAME', 'Example'),
]
'customizations' => [
'mail' => [
'from' => [
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
'name' => env('MAIL_FROM_NAME', 'Example'),
]
],
],

'api' => [
Expand Down
32 changes: 32 additions & 0 deletions phpunit.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="vendor/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false">
<testsuites>
<testsuite name="Unit">
<directory suffix="Test.php">./tests/Unit</directory>
</testsuite>

<testsuite name="Feature">
<directory suffix="Test.php">./tests/Feature</directory>
</testsuite>
</testsuites>

<php>
<env name="APP_ENV" value="testing"/>
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
</php>

<source>
<include>
<directory suffix=".php">src/</directory>
</include>
</source>
</phpunit>
111 changes: 111 additions & 0 deletions src/Channels/AmazonSesChannel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<?php

namespace ChijiokeIbekwe\Raven\Channels;

use Aws\Ses\Exception\SesException;
use Aws\Ses\SesClient;
use ChijiokeIbekwe\Raven\Notifications\EmailNotificationSender;
use Exception;
use Illuminate\Support\Facades\Log;
use SendGrid;

class AmazonSesChannel
{
private SesClient $sesClient;
private SendGrid $sendGrid;

public function __construct()
{
$this->sesClient = app(SesClient::class);
$this->sendGrid = app(SendGrid::class);
}

/**
* Send the given notification.
*
* @param mixed $notifiable
* @param EmailNotificationSender $emailNotification
* @return void
* @throws Exception
*/
public function send(mixed $notifiable, EmailNotificationSender $emailNotification): void
{
$email = $emailNotification->toAmazonSes($notifiable);

$sender = config('raven.customizations.mail.from');
$email->setFrom($sender['address'], $sender['name']);

$template_source = config('raven.providers.ses.template_source');
if($template_source !== 'sendgrid') {
Log::error("Template source $template_source not currently supported");
throw new Exception("Template source $template_source not currently supported");
}

$template_response = $this->getSendGridTemplateContent($emailNotification);

$params = $emailNotification->notificationData->getParams();
$clean_html = $this->cleanTemplate($template_response['html_content'], $params);
$clean_plain = $this->cleanTemplate($template_response['plain_content'], $params);

$email->Subject = $template_response['subject'];
$email->Body = $clean_html;
$email->AltBody = $clean_plain;

if (!$email->preSend()) {
Log::error("Failed sending mail: " . $email->ErrorInfo);
throw new Exception($email->ErrorInfo);
} else {
$message = $email->getSentMIMEMessage();
}

try {
$result = $this->sesClient->sendRawEmail([
'RawMessage' => [
'Data' => $message
]
]);
Log::info($result);
} catch (SesException $error) {
Log::error("Failed sending mail: " . $error->getAwsErrorMessage());
}
}

/**
* @throws Exception
*/
private function getSendGridTemplateContent(EmailNotificationSender $emailNotification): array
{
try {
$template_id = $emailNotification->notificationContext->email_template_id;
$response = $this->sendGrid->client->templates()->_($template_id)->get();

if(!($response->statusCode() >= '200' && $response->statusCode() < 300)) {
throw new Exception("SendGrid server returned error response");
}

$body_json = $response->body();
$body_arr = json_decode($body_json, true);
$subject = $body_arr['versions'][0]['subject'];
$html_content = $body_arr['versions'][0]['html_content'];
$plain_content = $body_arr['versions'][0]['plain_content'];

return [
'subject' => $subject,
'html_content' => $html_content,
'plain_content' => $plain_content
];

} catch (Exception $e) {
Log::error("Failed sending mail: " . $e->getMessage());
throw new Exception($e);
}
}

private function cleanTemplate($template, $data)
{
foreach ($data as $key => $value) {
$template = str_replace('{{' . $key . '}}', $value, $template);
}
return $template;
}
}
24 changes: 15 additions & 9 deletions src/Channels/SendGridChannel.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,31 +9,37 @@

class SendGridChannel
{
private SendGrid $sendGrid;

public function __construct()
{
$this->sendGrid = app(SendGrid::class);
}

/**
* Send the given notification.
*
* @param mixed $notifiable
* @param EmailNotificationSender $sender
* @param SendGrid $sendGrid
* @param EmailNotificationSender $emailNotification
* @return void
*/
public function send(mixed $notifiable, EmailNotificationSender $sender, SendGrid $sendGrid): void
public function send(mixed $notifiable, EmailNotificationSender $emailNotification): void
{

try {
$email = $sender->toSendgrid($notifiable);
$email = $emailNotification->toSendgrid($notifiable);
$email->setClickTracking(true, true);
$email->setOpenTracking(true, "--sub--");
$email->setFrom(config('raven.mail.from.address'), config('raven.mail.from.name'));
$sender = config('raven.customizations.mail.from');
$email->setFrom($sender['address'], $sender['name']);

$response = $sendGrid->send($email);
$response = $this->sendGrid->send($email);

if ($response->statusCode() != '202') {
if($response->statusCode() >= '200' && $response->statusCode() < 300) {
Log::info("Mail success response: " . $response->body());
}

} catch (Exception $e) {
Log::error("Failed sending mail to $email: " . $e->getMessage());
Log::error("Failed sending mail: " . $e->getMessage());
}
}
}
4 changes: 2 additions & 2 deletions src/Data/NotificationData.php → src/Data/Scroll.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
use Illuminate\Notifications\Notifiable;
use ChijiokeIbekwe\Raven\Exceptions\RavenInvalidDataException;

class NotificationData
class Scroll
{

/**
Expand Down Expand Up @@ -151,7 +151,7 @@ public function setParams(array $params): void
}

/**
* @param mixed $attachments
* @param mixed $attachmentUrls
* @return void
*/
public function setAttachmentUrls(mixed $attachmentUrls): void
Expand Down
4 changes: 2 additions & 2 deletions src/Events/Raven.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

use Illuminate\Queue\SerializesModels;
use Illuminate\Foundation\Events\Dispatchable;
use ChijiokeIbekwe\Raven\Data\NotificationData;
use ChijiokeIbekwe\Raven\Data\Scroll;

class Raven
{
Expand All @@ -13,7 +13,7 @@ class Raven
/**
* Create a new event instance.
*/
public function __construct(public NotificationData $notificationData)
public function __construct(public Scroll $scroll)
{
//
}
Expand Down
22 changes: 13 additions & 9 deletions src/Listeners/RavenListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace ChijiokeIbekwe\Raven\Listeners;

use ChijiokeIbekwe\Raven\Data\NotificationData;
use ChijiokeIbekwe\Raven\Data\Scroll;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
use ChijiokeIbekwe\Raven\Events\Raven;
Expand All @@ -20,6 +20,7 @@ class RavenListener
{
const EMAIL_PATTERN = '#^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$#';
const PHONE_PATTERN = '#^\+?[0-9\s-()]+$#';

/**
* Create the event listener.
*/
Expand All @@ -34,7 +35,7 @@ public function __construct()
*/
public function handle(Raven $event): void
{
$data = $event->notificationData;
$data = $event->scroll;
$context_name = $data->getContextName();

$context = NotificationContext::where('name', $context_name)->first();
Expand All @@ -44,9 +45,12 @@ public function handle(Raven $event): void
$this->sendNotifications($data, $context);
}

private function sendNotifications(NotificationData $data, NotificationContext $context)
/**
* @throws \Throwable
*/
private function sendNotifications(Scroll $scroll, NotificationContext $context): void
{
$factory = new ChannelSenderFactory($data, $context);
$factory = new ChannelSenderFactory($scroll, $context);
$channels = $context->notification_channels;

foreach($channels as $channel){
Expand All @@ -56,7 +60,7 @@ private function sendNotifications(NotificationData $data, NotificationContext $

$channel_sender = $factory->getSender($channel_type);

$recipients = $data->getRecipients();
$recipients = $scroll->getRecipients();

if(!$channel_sender) {
Log::error("Notification channel $channel_type->name is not currently supported");
Expand All @@ -65,7 +69,7 @@ private function sendNotifications(NotificationData $data, NotificationContext $

Log::info("Sending notification for context $context->name through channel $channel_type->name");

if(!$data->getHasOnDemand()) {
if(!$scroll->getHasOnDemand()) {
Notification::send($recipients, $channel_sender);
continue;
}
Expand All @@ -81,19 +85,19 @@ private function sendNotifications(NotificationData $data, NotificationContext $
}
}

private function resolveRouteWithChannelSender($recipient, $channel_sender)
private function resolveRouteWithChannelSender($recipient, $channel_sender): void
{
$sender_class = get_class($channel_sender);

switch($sender_class) {
case EmailNotificationSender::class:
if(preg_match(self::EMAIL_PATTERN, $recipient)) {
Notification::route(config('raven.notification-service.email'), $recipient)->notify($channel_sender);
Notification::route(config('raven.default.email'), $recipient)->notify($channel_sender);
};
return;
case SmsNotificationSender::class:
if(preg_match(self::PHONE_PATTERN, $recipient)) {
Notification::route(config('raven.notification-service.sms'), $recipient)->notify($channel_sender);
Notification::route(config('raven.default.sms'), $recipient)->notify($channel_sender);
};
return;
case DatabaseNotificationSender::class:
Expand Down
Loading

0 comments on commit d923bfa

Please sign in to comment.