diff --git a/README.md b/README.md index 8be45ea..ae7d3a9 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,48 @@ Resend.Emails.send(client, %{ View additional documentation at . +## Templates + +Resend supports [templates](https://resend.com/docs/dashboard/templates/introduction) for sending transactional emails. You can use templates with both the direct API and the Swoosh adapter. + +### Using Templates with the Direct API + +```ex +Resend.Emails.send(%{ + from: "Acme <[email protected]>", + to: "[email protected]", + template: %{ + id: "order-confirmation", + variables: %{ + PRODUCT: "Vintage Macintosh", + PRICE: 499 + } + } +}) +``` + +### Using Templates with Swoosh + +To use templates with the Swoosh adapter, use `put_provider_option/3` to add the template information: + +```ex +import Swoosh.Email + +new() +|> from({"Acme", "[email protected]"}) +|> to("[email protected]") +|> put_provider_option(:template, %{ + id: "order-confirmation", + variables: %{ + PRODUCT: "Vintage Macintosh", + PRICE: 499 + } +}) +|> MyApp.Mailer.deliver() +``` + +When using templates, you don't need to set the `html_body` or `text_body` as the template will provide the content. + ## Swoosh Adapter This library includes a Swoosh adapter to make using Resend with a new Phoenix project as easy as diff --git a/lib/resend/emails.ex b/lib/resend/emails.ex index 609a0bc..2c55504 100644 --- a/lib/resend/emails.ex +++ b/lib/resend/emails.ex @@ -20,14 +20,29 @@ defmodule Resend.Emails do * `:html` - The HTML-formatted body of the email * `:text` - The text-formatted body of the email * `:attachments` - List of attachments to include in the email + * `:template` - Template to use for the email, a map with `:id` and `:variables` keys - You must include one or both of the `:html` and `:text` options. + You must include one or both of the `:html` and `:text` options, or use a `:template`. + + ## Template Example + + Resend.Emails.send(%{ + from: "Acme <[email protected]>", + to: "[email protected]", + template: %{ + id: "order-confirmation", + variables: %{ + PRODUCT: "Vintage Macintosh", + PRICE: 499 + } + } + }) """ @spec send(map()) :: Resend.Client.response(Email.t()) @spec send(Resend.Client.t(), map()) :: Resend.Client.response(Email.t()) def send(client \\ Resend.client(), opts) do - Resend.Client.post(client, Email, "/emails", %{ + payload = %{ subject: opts[:subject], to: opts[:to], from: opts[:from], @@ -38,7 +53,16 @@ defmodule Resend.Emails do html: opts[:html], text: opts[:text], attachments: opts[:attachments] - }) + } + + payload = + if opts[:template] do + Map.put(payload, :template, opts[:template]) + else + payload + end + + Resend.Client.post(client, Email, "/emails", payload) end @doc """ diff --git a/lib/resend/emails/email.ex b/lib/resend/emails/email.ex index ed07ed2..b0144d2 100644 --- a/lib/resend/emails/email.ex +++ b/lib/resend/emails/email.ex @@ -20,6 +20,7 @@ defmodule Resend.Emails.Email do text: String.t() | nil, html: String.t() | nil, attachments: list(Attachment.t()) | nil, + template: map() | nil, last_event: String.t() | nil, created_at: DateTime.t() | nil } @@ -37,6 +38,7 @@ defmodule Resend.Emails.Email do :text, :html, :attachments, + :template, :last_event, :created_at ] @@ -55,6 +57,7 @@ defmodule Resend.Emails.Email do text: map["text"], html: map["html"], attachments: map["attachments"], + template: map["template"], last_event: map["last_event"], created_at: Util.parse_iso8601(map["created_at"]) } diff --git a/lib/resend/swoosh/adapter.ex b/lib/resend/swoosh/adapter.ex index 4cca05f..1b06437 100644 --- a/lib/resend/swoosh/adapter.ex +++ b/lib/resend/swoosh/adapter.ex @@ -28,13 +28,36 @@ defmodule Resend.Swoosh.Adapter do ``` And just like that, you should be all set to send emails with Resend! + + ## Using Templates + + To use Resend templates with Swoosh, add the template information using `put_provider_option/3`: + + ```ex + import Swoosh.Email + + new() + |> from({"Acme", "[email protected]"}) + |> to("[email protected]") + |> put_provider_option(:template, %{ + id: "order-confirmation", + variables: %{ + PRODUCT: "Vintage Macintosh", + PRICE: 499 + } + }) + |> MyApp.Mailer.deliver() + ``` + + When using templates, you don't need to set the `html_body` or `text_body` as the template + will provide the content. """ @behaviour Swoosh.Adapter @impl true def deliver(%Swoosh.Email{} = email, config) do - Resend.Emails.send(Resend.client(config), %{ + payload = %{ subject: email.subject, from: format_sender(email.from), to: format_recipients(email.to), @@ -45,7 +68,16 @@ defmodule Resend.Swoosh.Adapter do html: email.html_body, text: email.text_body, attachments: format_attachments(email.attachments) - }) + } + + payload = + if template = email.provider_options[:template] do + Map.put(payload, :template, template) + else + payload + end + + Resend.Emails.send(Resend.client(config), payload) end @impl true diff --git a/test/resend/emails_test.exs b/test/resend/emails_test.exs index 74150ae..a7ab4dc 100644 --- a/test/resend/emails_test.exs +++ b/test/resend/emails_test.exs @@ -56,6 +56,97 @@ defmodule Resend.EmailsTest do end end + test "Sends an email with a template", context do + to = context.to + from = context.from + template_id = "order-confirmation" + variables = %{PRODUCT: "Vintage Macintosh", PRICE: 499} + + opts = [ + to: to, + from: from, + template: %{ + id: template_id, + variables: variables + } + ] + + Tesla.Mock.mock(fn request -> + assert request.method == :post + assert request.url == "https://api.resend.com/emails" + + body = Jason.decode!(request.body) + + # Assert template fields are present + assert body["template"]["id"] == template_id + assert body["template"]["variables"]["PRODUCT"] == "Vintage Macintosh" + assert body["template"]["variables"]["PRICE"] == 499 + + %Tesla.Env{status: 200, body: %{"id" => context.sent_email_id}} + end) + + assert {:ok, %Resend.Emails.Email{}} = Resend.Emails.send(Map.new(opts)) + end + + test "Sends email with template via Swoosh adapter", context do + # Create Swoosh email with template in provider_options + email = + Swoosh.Email.new() + |> Swoosh.Email.to(context.to) + |> Swoosh.Email.from(context.from) + |> Swoosh.Email.put_provider_option(:template, %{ + id: "welcome-email", + variables: %{ + NAME: "John Doe", + COMPANY: "Acme Corp" + } + }) + + # Mock the API call and assert template is sent + Tesla.Mock.mock(fn request -> + assert request.method == :post + assert request.url == "https://api.resend.com/emails" + + body = Jason.decode!(request.body) + + # Assert template fields are present + assert body["template"]["id"] == "welcome-email" + assert body["template"]["variables"]["NAME"] == "John Doe" + assert body["template"]["variables"]["COMPANY"] == "Acme Corp" + + %Tesla.Env{status: 200, body: %{"id" => context.sent_email_id}} + end) + + # Deliver via Swoosh adapter + config = [api_key: context.api_key] + assert {:ok, _} = Resend.Swoosh.Adapter.deliver(email, config) + end + + test "Sends email without template when not present in Swoosh email", context do + # Create regular Swoosh email without template + email = + Swoosh.Email.new() + |> Swoosh.Email.to(context.to) + |> Swoosh.Email.from(context.from) + |> Swoosh.Email.subject("Regular Email") + |> Swoosh.Email.text_body("This is a regular email") + + # Mock the API call + Tesla.Mock.mock(fn request -> + body = Jason.decode!(request.body) + + # Assert template field is not present + refute Map.has_key?(body, "template") + assert body["text"] == "This is a regular email" + + %Tesla.Env{status: 200, body: %{"id" => context.sent_email_id}} + end) + + # Deliver via Swoosh adapter + config = [api_key: context.api_key] + assert {:ok, _} = Resend.Swoosh.Adapter.deliver(email, config) + end + test "Attachment struct accepts content_id field" do attachment = %Resend.Emails.Attachment{ filename: "logo.jpg",