Skip to content

Instantly share code, notes, and snippets.

@Dozorengel
Created March 13, 2022 04:07
Show Gist options
  • Save Dozorengel/25d51576815d21ca5684c15d48ecc5a9 to your computer and use it in GitHub Desktop.
Save Dozorengel/25d51576815d21ca5684c15d48ecc5a9 to your computer and use it in GitHub Desktop.
Sending newsletters via Sendgrid API (Laravel)
<?php
namespace App\Console\Commands;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Carbon;
use Illuminate\Console\Command;
use App\Services\SendgridService;
use App\Models\Newsletter;
use App\Jobs\CheckMailDelivery;
use App\Events\NewsletterStarted;
use App\Events\NewsletterFailed;
/**
* Send newsletter via Sendgrid automatically.
*
* 1. Create new single send (campaign)
* 2. Create new template version
* 3. Bind the single send with the template data
* 4. Send the newsletter
*/
class NewsletterSendgridSend extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'newsletter:sendgridsend
{id : ID of the Newsletter}
{--T|test : Send only to test list}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Send newsletter via Sendgrid automatically';
/**
* Create a new command instance.
*/
public function __construct(SendgridService $sendgrid)
{
parent::__construct();
$this->sendgrid = $sendgrid;
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$message = 'Newsletter started';
$this->info($message);
Log::info($message);
$sendgrid_lists = $this->sendgrid->getSendgridLists();
$this->info('Sendgrid lists --> OK');
$newsletter = Newsletter::findOrFail($this->argument('id'));
$newsletter_lists = $this->sendgrid->getNewsletterLists(
$newsletter,
$sendgrid_lists
);
$this->info('Newsletter lists --> OK');
$newsletter_list_ids = ($this->option('test'))
? $sendgrid_lists['test']
: $this->sendgrid->getListIdsToString($newsletter_lists);
$this->info('Newsletter lists ids --> OK');
$template_title = $this->sendgrid->getTemplateTitle(
$newsletter,
$newsletter_lists
);
$this->info('Template ID and Title --> OK');
$single_send_id = $this->sendgrid->createSingleSend(
config('sendgrid.initial_single_send_id')
);
$this->info('Create single send --> OK');
$newsletter->status = Newsletter::STATUS_PENDING;
$newsletter->save();
$status = $this->sendgrid
->updateSingleSend(
$newsletter,
$template_title,
$single_send_id,
$newsletter_lists,
$newsletter_list_ids
)
->startSingleSend($single_send_id);
if ($status === 'scheduled') {
$this->info('Newsletter sent successfully --> OK');
event(new NewsletterStarted($newsletter));
$newsletter->status = Newsletter::STATUS_IN_PROGRESS;
$newsletter->mail_provider = Newsletter::MAIL_PROVIDER_SENDGRID;
$newsletter->mail_provider_id = $single_send_id;
$newsletter->save();
dispatch((new CheckMailDelivery())
->onQueue('MailDeliveryCheckerQueue')
->delay(Carbon::now()->addMinutes(Newsletter::CHECK_BOT_DELAY)));
$message = 'Newsletter has been sent';
$this->info($message);
Log::info($message);
} else {
event(new NewsletterFailed($newsletter));
$message = 'Something went wrong with newsletter sending';
$this->info($message);
Log::info($message);
}
}
}
<?php
namespace App\Services;
use Illuminate\Support\Facades\Log;
use GuzzleHttp\Client;
use App\Models\Newsletter;
use App\Models\Member;
/**
* Service for working with SendGrid.
*/
class SendgridService
{
public function __construct(array $suppression_groups, int $sender_id)
{
$this->client = new Client();
$this->headers = ['Authorization' => 'Bearer ' . config('sendgrid.api_key')];
$this->suppression_groups = $suppression_groups;
$this->sender_id = $sender_id;
}
/**
* Retrieve all list names and ids from SendGrid.
*
* @return array $lists Contact lists
*/
public function getSendgridLists(): array
{
Log::info('Start getSendgridLists()');
$url = 'https://api.sendgrid.com/v3/marketing/lists';
try {
$response = $this->client->get($url, [
'headers' => $this->headers,
]);
$content = $response->getBody()->getContents();
$remote_lists = json_decode($content, true);
foreach ($remote_lists as $items) {
$lists = [];
foreach ($items as $item) {
$lists[$item['name']] = $item['id'];
}
return $lists;
}
} catch (\Throwable $th) {
$this->logError($th, 'GET');
}
}
/**
* Determine unsubscribed members to exclude from SendGrid.
*
* @return string Set of members ids to delete
*/
public function getUnsubscribedMembersIds(): string
{
Log::info('Start getUnsubscribedMembersIds()');
$unsubscribed_members = Member::select('email')->where('status', Member::STATUS_UNSUBSCRIBED)->get();
$unsubscribed_ids = '';
foreach ($unsubscribed_members as $member) {
$searched_id = $this->search($member->email);
if ('' === $searched_id) {
continue;
}
$unsubscribed_ids .= $searched_id . ',';
}
return rtrim($unsubscribed_ids, ',');
}
/**
* Delete unsubscribed members from SendGrid.
*
* @param string $query_ids Members ids
* @return void
*/
public function deleteMembers(string $query_ids): void
{
Log::info('Start deleteMembers()');
$url = 'https://api.sendgrid.com/v3/marketing/contacts?ids=' . $query_ids;
try {
$this->client->delete($url, [
'headers' => $this->headers,
]);
} catch (\Throwable $th) {
$this->logError($th, 'DELETE');
}
}
/**
* Create/update members data to SendGrid.
*
* @param array $data Users data formed for SendGrid
* @return void
*/
public function storeMembers(array $data): void
{
Log::info('Start storeMembers()');
$url = 'https://api.sendgrid.com/v3/marketing/contacts';
try {
$this->client->put($url, [
'headers' => $this->headers,
'body' => json_encode($data),
]);
} catch (\Throwable $th) {
$this->logError($th, 'PUT');
}
}
/**
* Cut a string if it exceeds a predefined length.
*
* @param string $str
* @param int $length Default 25, if longer SendGrid throws an error
* @return string
*/
public function trimString(string $str, int $length = 25)
{
return mb_strlen($str) > $length
? mb_strimwidth(trim($str), 0, $length)
: $str;
}
/**
* Send emails manually via SendGrid.
*
* @param Newsletter $newsletter
* @param array $emails Recipients
* @return void
*/
public function sendManualNewsletter(
Newsletter $newsletter,
array $recipient_emails_list
): void {
$html = view('email.main')->with('newsletter', $newsletter)->render();
$html = str_replace('<h4>', '<h4 style="font-weight: bold;">', $html);
$html = str_ireplace('*|MC_PREVIEW_TEXT|*', $newsletter->lead, $html);
$html = str_ireplace('*|MC:SUBJECT|*', $newsletter->subject, $html);
$html = str_ireplace('*|UNSUB|*', '<%asm_group_unsubscribe_raw_url%>', $html);
$html = str_ireplace('*|UPDATE_PROFILE|*', '<%asm_preferences_raw_url%>', $html);
Log::info('[SendGrid] sending started. emails count(' . count($recipient_emails_list) . ')');
foreach ($recipient_emails_list as $recipient_email) {
$recipient_email = trim($recipient_email);
echo "--------------------------------------------------------\n";
echo '[SENDING]: ' . $recipient_email . "\n";
$email = new \SendGrid\Mail\Mail();
$email->addTo($recipient_email);
$email->setFrom(config('mailchimp.from'), 'The Company');
$email->setSubject($newsletter->subject);
$email->addContent('text/html', $html);
$sendgrid = new \SendGrid(getenv('SENDGRID_KEY'));
try {
$response = $sendgrid->send($email);
echo $response->statusCode() . "\n";
print_r($response->headers());
echo $response->body() . "\n";
Log::info("[SendGrid] sent to $recipient_email");
} catch (\Exception $e) {
echo 'Caught exception: ', $e->getMessage(), "\n";
Log::error("[SendGrid] ($recipient_email) Caught exception: " . $e->getMessage());
}
echo "--------------------------------------------------------\n";
}
Log::info('[SendGrid] sending end.');
}
/**
* Get first single send entity ID.
*
* It is needed to get the first entity specifically,
* as the first entity must contain a short mock title
* to avoid restriction of max 100 characters when duplicating
*
* @return string $id
*/
public function getFirstSingleSendId(): string
{
Log::info('Start getFirstSingleSendId()');
$url = 'https://api.sendgrid.com/v3/marketing/singlesends';
try {
$response = $this->client->get($url, [
'headers' => $this->headers,
]);
$body = $response->getBody();
$content = $body->getContents();
$content_data = json_decode($content, true);
$count = count($content_data['result']) - 1;
return $content_data['result'][$count]['id'];
} catch (\Throwable $th) {
$this->logError($th, 'GET');
}
}
/**
* Create a single send (similar to campaign).
*
* Now it duplicates an existing single send entity.
*
* @param string $single_send_id ID of the existing single send instance
* @return string $id ID of the duplicated single send instance
*/
public function createSingleSend(string $single_send_id): string
{
Log::info('Start createSingleSend()');
$url = 'https://api.sendgrid.com/v3/marketing/singlesends/' . $single_send_id;
try {
$response = $this->client->post($url, [
'headers' => $this->headers,
]);
$body = $response->getBody();
$content = $body->getContents();
$content_data = json_decode($content, true);
return $content_data['id'];
} catch (\Throwable $th) {
$this->logError($th, 'POST');
}
}
/**
* Get ID of transactional template.
*
* This method gets the ID of the latest entity specifically,
* though we could get any existing record.
*
* @return string $id
*/
public function getTemplateId(): string
{
Log::info('Start getTemplateId()');
$url = 'https://api.sendgrid.com/v3/templates?generations=dynamic';
try {
$response = $this->client->get($url, [
'headers' => $this->headers,
]);
$body = $response->getBody();
$content = $body->getContents();
$content_data = json_decode($content, true);
return $content_data['templates'][0]['id'];
} catch (\Throwable $th) {
$this->logError($th, 'GET');
}
}
/**
* Create a transactional template.
*
* An element that can be shared among more than one endpoint
* definition.
*
* Just in case method as only one template is currently using.
*
* @param string $name Title
* @param string $generation legacy/dynamic
* @return string $id Transactional template id
*/
public function createTemplate(
string $name,
string $generation = 'dynamic'
): string {
Log::info('Start createTemplate()');
$url = 'https://api.sendgrid.com/v3/templates';
$data = [
'name' => $name,
'generation' => $generation,
];
try {
$response = $this->client->post($url, [
'headers' => $this->headers,
'body' => json_encode($data),
]);
$body = $response->getBody();
$content = $body->getContents();
$content_data = json_decode($content, true);
return $content_data['id'];
} catch (\Throwable $th) {
$this->logError($th, 'POST');
}
}
/**
* Get lists from newsletter, where it is needed to send.
*
* @param Newsletter $newsletter
* @param array $sendgrid_lists All lists from SendGrid
* @return array $lists
*/
public function getNewsletterLists(
Newsletter $newsletter,
array $sendgrid_lists
): array {
Log::info('Start getNewsletterLists()');
$lists = [];
foreach ($newsletter->lists as $list_name) {
if (array_key_exists($list_name, $sendgrid_lists)) {
$lists[] = [
'name' => $list_name,
'id' => $sendgrid_lists[$list_name],
];
}
}
return $lists;
}
/**
* Get template title as joined lists names and newsletter subject.
*
* @param Newsletter $newsletter
* @param array $newsletter_lists
* @return string
*/
public function getTemplateTitle(
Newsletter $newsletter,
array $newsletter_lists
): string {
Log::info('Start getTemplateTitle()');
$template_title = '';
foreach ($newsletter_lists as $list) {
$template_title .= '[' . $list['name'] . ']';
}
$template_title .= ' ' . $newsletter->subject;
return $this->trimString($template_title, 50);
}
/**
* Create a new transactional template version.
*
* A new version of the predefined template in SendGrid.
*
* @param Newsletter $newsletter
* @param array $list
* @return self
*/
public function createTemplateVersion(
Newsletter $newsletter,
string $template_title,
string $template_id
): self {
Log::info('Start createTemplateVersion()');
$url = "https://api.sendgrid.com/v3/templates/$template_id/versions";
$html = view('email.main')->with('newsletter', $newsletter)->render();
$html = str_replace('<h4>', '<h4 style="font-weight: bold;">', $html);
$html = str_ireplace('*|MC_PREVIEW_TEXT|*', $newsletter->lead, $html);
$html = str_ireplace('*|MC:SUBJECT|*', $newsletter->subject, $html);
$html = str_ireplace('*|UNSUB|*', '<%asm_group_unsubscribe_raw_url%>', $html);
$html = str_ireplace('*|UPDATE_PROFILE|*', '<%asm_preferences_raw_url%>', $html);
$data = [
'template_id' => $template_id,
'active' => 1,
'name' => str_slug($template_title),
'html_content' => $html,
'subject' => $newsletter->subject,
];
Log::info('DATA', [json_encode([
'template_id' => $template_id,
'active' => 1,
'name' => str_slug($template_title),
'subject' => $newsletter->subject,
])]);
try {
$this->client->post($url, [
'headers' => $this->headers,
'body' => json_encode($data),
]);
return $this;
} catch (\Throwable $th) {
$this->logError($th, 'POST');
}
}
/**
* Update a single send with an active template version.
*
* This is a get-ready method to send the newsletter.
* @return self
*/
public function updateSingleSend(
Newsletter $newsletter,
string $template_title,
string $single_send_id,
array $newsletter_lists,
string $list_ids
): self {
Log::info('Start updateSingleSend()');
$url = 'https://api.sendgrid.com/v3/marketing/singlesends/' . $single_send_id;
$suppression_group_id = $this->suppression_groups[$newsletter_lists[0]['name']];
$html = view('email.main')->with('newsletter', $newsletter)->render();
$html = str_replace('<h4>', '<h4 style="font-weight: bold;">', $html);
$html = str_ireplace('*|MC_PREVIEW_TEXT|*', $newsletter->lead, $html);
$html = str_ireplace('*|MC:SUBJECT|*', $newsletter->subject, $html);
$html = str_ireplace('*|UNSUB|*', '<%asm_group_unsubscribe_raw_url%>', $html);
$html = str_ireplace('*|UPDATE_PROFILE|*', '<%asm_preferences_raw_url%>', $html);
$data = [
'name' => $template_title,
'send_to' => [
'list_ids' => [$list_ids],
],
'email_config' => [
'suppression_group_id' => $suppression_group_id,
'sender_id' => $this->sender_id,
'html_content' => $html,
'subject' => $newsletter->subject,
],
];
try {
$this->client->patch($url, [
'headers' => $this->headers,
'body' => json_encode($data),
]);
return $this;
} catch (\Throwable $th) {
$this->logError($th, 'PATCH');
}
}
/**
* Send a ready-to-send newsletter immediately.
*
* @param string $single_send_id ID of single send instance to send
* @return string 'scheduled' if success, as 'triggered' is too early to get
*/
public function startSingleSend(string $single_send_id): string
{
Log::info('Start startSingleSend()');
$url = "https://api.sendgrid.com/v3/marketing/singlesends/$single_send_id/schedule";
$data = ['send_at' => 'now'];
try {
$response = $this->client->put($url, [
'headers' => $this->headers,
'body' => json_encode($data),
]);
$body = $response->getBody();
$content = $body->getContents();
$content_data = json_decode($content, true);
return $content_data['status'];
} catch (\Throwable $th) {
$this->logError($th, 'PUT');
}
}
/**
* Get string of newsletter list ids.
* @param array $newsletter_lists
* @return string
*/
public function getListIdsToString(array $newsletter_lists): string
{
Log::info('Start getListIdsToString()');
$list_ids = '';
foreach ($newsletter_lists as $list) {
$list_ids .= $list['id'] . ',';
}
return rtrim($list_ids, ',');
}
/**
* @param string $id Entity ID
* @return array Entity
*/
public function singleSendEntity(string $id): array
{
Log::info('Start singleSendEntity()');
$url = "https://api.sendgrid.com/v3/marketing/singlesends/$id";
try {
$response = $this->client->get($url, [
'headers' => $this->headers,
]);
$body = $response->getBody();
$content = $body->getContents();
return json_decode($content, true);
} catch (\Throwable $th) {
$this->logError($th, 'GET');
}
}
/**
* Search a subscriber by the email given.
*
* @param string $email
* @return string $member_id
*/
private function search(string $email): string
{
Log::info('Start search()');
$url = 'https://api.sendgrid.com/v3/marketing/contacts/search';
$body = [
'query' => "primary_email LIKE '" . $email . "%'",
];
try {
$response = $this->client->post($url, [
'headers' => $this->headers,
'body' => json_encode($body),
]);
$content = $response->getBody()->getContents();
$remote_lists = json_decode($content, true);
if (empty($remote_lists['result'])) {
return '';
}
return $remote_lists['result'][0]['id'];
} catch (\Throwable $th) {
$this->logError($th, 'POST');
}
}
/**
* Log and print out the error.
*
* @param \Throwable $th
* @param string $method
* @return void
*/
private function logError(\Throwable $th, string $method): void
{
Log::info('Start logError()');
$error = 'Cannot send ' . $method . ' request to SendGrid';
echo "\n" . $error . "\n";
echo $th . "\n";
Log::error($error . "\n" . $th);
exit;
}
/**
* Versions of the transactional template.
*
* @param string $template_id
* @return array Template versions
*/
public function templateVersions(string $template_id): array
{
Log::info('Start templateVersions()');
$url = 'https://api.sendgrid.com/v3/templates/' . $template_id;
$body = [
'template_id' => $template_id
];
try {
$response = $this->client->get($url, [
'headers' => $this->headers,
'body' => json_encode($body),
]);
$content = $response->getBody()->getContents();
$versions = json_decode($content, true);
return $versions['versions'];
} catch (\Throwable $th) {
$this->logError($th, 'GET');
}
}
/**
* Delete a transactional template version.
*
* @param string $template_id
* @param string $version_id
* @return void
*/
public function templateVersionDelete(
string $template_id,
string $version_id
): void {
Log::info('Start templateVersionDelete()');
$url = 'https://api.sendgrid.com/v3/templates/' . $template_id;
$url .= '/versions/' . $version_id;
try {
$this->client->delete($url, [
'headers' => $this->headers,
]);
} catch (\Throwable $th) {
$this->logError($th, 'DELETE');
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment