Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 79 additions & 14 deletions spec/carbon_sendgrid_adapter_spec.cr
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require "./spec_helper"
require "./support/email_with_sendgrid_features"

describe Carbon::SendGridAdapter do
{% if flag?("with-integration") %}
Expand All @@ -17,10 +18,10 @@ describe Carbon::SendGridAdapter do
{% end %}

describe "errors" do
it "raises SendGridInvalidTemplateError if no template is defined in params" do
it "raises SendGridInvalidTemplateError if no template is defined" do
expect_raises(Carbon::SendGridInvalidTemplateError) do
email = FakeEmail.new
Carbon::SendGridAdapter::Email.new(email, api_key: "fake_key").params
Carbon::SendGridAdapter::Email.new(email, api_key: "fake_key").sendgrid_options
end
end
end
Expand Down Expand Up @@ -111,24 +112,24 @@ describe Carbon::SendGridAdapter do
end

it "sets the content" do
params_for(text_body: "text")["content"].should eq [{"type" => "text/plain", "value" => "text"}]
params_for(html_body: "html")["content"].should eq [{"type" => "text/html", "value" => "html"}]
params_for(text_body: "text", html_body: "html")["content"].should eq [
sendgrid_options_for(text_body: "text")["content"].should eq JSON.parse([{"type" => "text/plain", "value" => "text"}].to_json)
sendgrid_options_for(html_body: "html")["content"].should eq JSON.parse([{"type" => "text/html", "value" => "html"}].to_json)
sendgrid_options_for(text_body: "text", html_body: "html")["content"].should eq JSON.parse([
{"type" => "text/plain", "value" => "text"},
{"type" => "text/html", "value" => "html"},
]
].to_json)
end

it "allows for a custom template_id" do
custom_email = CustomTemplateEmail.new
params = Carbon::SendGridAdapter::Email.new(custom_email, api_key: "fake_key").params
options = Carbon::SendGridAdapter::Email.new(custom_email, api_key: "fake_key").sendgrid_options

params["template_id"].should eq("welcome-abc-123")
options["template_id"].should eq(JSON::Any.new("welcome-abc-123"))

normal_email = FakeEmail.new(text_body: "0")
params = Carbon::SendGridAdapter::Email.new(normal_email, api_key: "fake_key").params
options = Carbon::SendGridAdapter::Email.new(normal_email, api_key: "fake_key").sendgrid_options

params.has_key?("template_id").should eq(false)
options.has_key?("template_id").should eq(false)
end

it "allows for custom template data" do
Expand All @@ -145,14 +146,14 @@ describe Carbon::SendGridAdapter do

it "passes over asm data on how to handle unsubscribes" do
custom_email = CustomTemplateEmail.new
params = Carbon::SendGridAdapter::Email.new(custom_email, api_key: "fake_key").params
options = Carbon::SendGridAdapter::Email.new(custom_email, api_key: "fake_key").sendgrid_options

params["personalizations"].as(Array).first["dynamic_template_data"].should_not eq(nil)
options["asm"].should eq(JSON.parse({"group_id" => 1234, "groups_to_display" => [1234]}.to_json))

email = FakeEmail.new(text_body: "0")
params = Carbon::SendGridAdapter::Email.new(email, api_key: "fake_key").params
options = Carbon::SendGridAdapter::Email.new(email, api_key: "fake_key").sendgrid_options

params["personalizations"].as(Array).first.has_key?("asm").should eq(false)
options.has_key?("asm").should eq(false)
end

it "handles attachments" do
Expand All @@ -163,6 +164,65 @@ describe Carbon::SendGridAdapter do
attachments.first["filename"].should eq("contract.pdf")
Base64.decode_string(attachments.first["content"].to_s).should eq("Sign here")
end

describe "sendgrid specific features" do
it "includes categories in sendgrid_options when defined" do
email = EmailWithSendGridFeatures.new(text_body: "0")
options = Carbon::SendGridAdapter::Email.new(email, api_key: "fake_key").sendgrid_options

options["categories"].should eq(JSON.parse(["welcome", "onboarding", "transactional"].to_json))
end

it "does not include categories when not defined" do
email = FakeEmail.new(text_body: "0")
options = Carbon::SendGridAdapter::Email.new(email, api_key: "fake_key").sendgrid_options

options.has_key?("categories").should eq(false)
end

it "includes send_at in sendgrid_options when defined" do
email = EmailWithSendGridFeatures.new(text_body: "0")
options = Carbon::SendGridAdapter::Email.new(email, api_key: "fake_key").sendgrid_options

options["send_at"].should eq(JSON::Any.new(1704067200_i64))
end

it "does not include send_at when not defined" do
email = FakeEmail.new(text_body: "0")
options = Carbon::SendGridAdapter::Email.new(email, api_key: "fake_key").sendgrid_options

options.has_key?("send_at").should eq(false)
end

it "includes asm in sendgrid_options when defined" do
custom_email = CustomTemplateEmail.new
options = Carbon::SendGridAdapter::Email.new(custom_email, api_key: "fake_key").sendgrid_options

options["asm"].should eq(JSON.parse({"group_id" => 1234, "groups_to_display" => [1234]}.to_json))
end

it "does not include asm when not defined" do
email = FakeEmail.new(text_body: "0")
options = Carbon::SendGridAdapter::Email.new(email, api_key: "fake_key").sendgrid_options

options.has_key?("asm").should eq(false)
end

it "includes template_id in sendgrid_options when defined" do
custom_email = CustomTemplateEmail.new
options = Carbon::SendGridAdapter::Email.new(custom_email, api_key: "fake_key").sendgrid_options

options["template_id"].should eq(JSON::Any.new("welcome-abc-123"))
end

it "includes content in sendgrid_options when template_id not defined" do
email = FakeEmail.new(text_body: "hello")
options = Carbon::SendGridAdapter::Email.new(email, api_key: "fake_key").sendgrid_options

options["content"].should eq(JSON.parse([{"type" => "text/plain", "value" => "hello"}].to_json))
options.has_key?("template_id").should eq(false)
end
end
end
end

Expand All @@ -171,6 +231,11 @@ private def params_for(**email_attrs)
Carbon::SendGridAdapter::Email.new(email, api_key: "fake_key").params
end

private def sendgrid_options_for(**email_attrs)
email = FakeEmail.new(**email_attrs)
Carbon::SendGridAdapter::Email.new(email, api_key: "fake_key").sendgrid_options
end

private def send_email_to_send_grid(**email_attrs)
api_key = ENV.fetch("SEND_GRID_API_KEY")
email = FakeEmail.new(**email_attrs)
Expand Down
8 changes: 4 additions & 4 deletions spec/support/custom_template_email.cr
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,18 @@ class CustomTemplateEmail < Carbon::Email
@headers = {} of String => String,
@subject = "subject",
@text_body : String? = nil,
@html_body : String? = nil
@html_body : String? = nil,
)
end

def template_id
"welcome-abc-123"
end

def asm_data
def asm
{
"group_id" => 12345,
"groups_to_display" => [12345],
"group_id" => 1234,
"groups_to_display" => [1234],
}
end

Expand Down
39 changes: 39 additions & 0 deletions spec/support/email_with_sendgrid_features.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
class EmailWithSendGridFeatures < Carbon::Email
getter text_body, html_body

def initialize(
@from = Carbon::Address.new("from@example.com"),
@to = [] of Carbon::Address,
@cc = [] of Carbon::Address,
@bcc = [] of Carbon::Address,
@headers = {} of String => String,
@subject = "subject",
@text_body : String? = nil,
@html_body : String? = nil,
)
end

def template_id
"d-1234567890"
end

def dynamic_template_data
{
"name" => "Test User",
}
end

def categories : Array(String)
["welcome", "onboarding", "transactional"]
end

def send_at : Int64
1704067200_i64 # 2024-01-01 00:00:00 UTC
end

from @from
to @to
cc @cc
bcc @bcc
subject @subject
end
2 changes: 1 addition & 1 deletion spec/support/fake_email.cr
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class FakeEmail < Carbon::Email
@headers = {} of String => String,
@subject = "subject",
@text_body : String? = nil,
@html_body : String? = nil
@html_body : String? = nil,
)
end

Expand Down
2 changes: 1 addition & 1 deletion spec/support/fake_email_with_attachments.cr
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class FakeEmailWithAttachments < Carbon::Email
@headers = {} of String => String,
@subject = "subject",
@text_body : String? = nil,
@html_body : String? = nil
@html_body : String? = nil,
)
end

Expand Down
52 changes: 38 additions & 14 deletions src/carbon_sendgrid_adapter.cr
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,13 @@ class Carbon::SendGridAdapter < Carbon::Adapter
end

def deliver
body = params.to_json
# Merge core params with SendGrid-specific options
json_data = JSON.parse(params.to_json).as_h
sendgrid_options.each do |key, value|
json_data[key] = value
end
body = json_data.to_json

client.post(MAIL_SEND_PATH, body: body).tap do |response|
unless response.success?
raise SendGridResponseFailedError.new(response.body)
Expand All @@ -36,14 +42,14 @@ class Carbon::SendGridAdapter < Carbon::Adapter
end

# :nodoc:
# Core email structure - SendGrid-specific options handled separately in sendgrid_options
def params
data = {
"personalizations" => [personalizations],
"subject" => email.subject,
"from" => from,
"headers" => headers,
"reply_to" => reply_to_params,
"asm" => {"group_id" => 0, "groups_to_display" => [] of Int32},
"mail_settings" => {sandbox_mode: {enable: sandbox?}},
"attachments" => attachments,
}.compact
Expand All @@ -53,17 +59,29 @@ class Carbon::SendGridAdapter < Carbon::Adapter
data.delete("attachments")
end

data
end

# :nodoc:
# All SendGrid-specific optional fields consolidated here.
# These are merged into the JSON body in deliver to handle flexible typing
# and keep the core params method clean.
def sendgrid_options
options = {} of String => JSON::Any

# ASM (unsubscribe groups)
# https://docs.sendgrid.com/ui/sending-email/unsubscribe-groups
if asm_data = email.asm
data = data.merge!({"asm" => asm_data})
else
data.delete("asm")
options["asm"] = JSON.parse(asm_data.to_json)
end

# Template ID or content (required: one or the other)
# https://docs.sendgrid.com/ui/sending-email/how-to-send-an-email-with-dynamic-transactional-templates
if template_id = email.template_id
data = data.merge!({"template_id" => template_id})
options["template_id"] = JSON::Any.new(template_id)
else
if content.size > 0
data = data.merge({"content" => content})
options["content"] = JSON.parse(content.to_json)
else
raise SendGridInvalidTemplateError.new <<-ERROR
Unless a valid template_id is provided, a template is required.
Expand All @@ -80,7 +98,19 @@ class Carbon::SendGridAdapter < Carbon::Adapter
end
end

data
# Categories for analytics
# https://docs.sendgrid.com/ui/analytics-and-reporting/categories
if categories = email.categories
options["categories"] = JSON::Any.new(categories.map { |c| JSON::Any.new(c) })
end

# Scheduled send time
# https://docs.sendgrid.com/ui/sending-email/scheduling-parameters
if send_at = email.send_at
options["send_at"] = JSON::Any.new(send_at)
end

options
end

private def reply_to_params : Hash(String, String)?
Expand Down Expand Up @@ -131,12 +161,6 @@ class Carbon::SendGridAdapter < Carbon::Adapter
to_send_grid_address([email.from]).first
end

private def asm_data : Hash(String, String)?
if asm_data = email.asm
{"asm" => asm_data}
end
end

private def content : Array(Hash(String, String))
[
text_content,
Expand Down
14 changes: 14 additions & 0 deletions src/carbon_sendgrid_extensions.cr
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,20 @@ module Carbon::SendGridExtensions
def asm
nil
end

# Define categories for your email to organize
# and track analytics by category.
# https://docs.sendgrid.com/ui/analytics-and-reporting/categories
def categories : Array(String)?
nil
end

# Define a unix timestamp to schedule when
# the email should be sent.
# https://docs.sendgrid.com/ui/sending-email/scheduling-parameters
def send_at : Int64?
nil
end
end

class Carbon::Email
Expand Down