VBT.Mailer behaviour (vbt v0.1.0) View Source
Helper for simpler e-mail sending using Bamboo and Phoenix templates.
This module simplifies the implementation of a typical e-mail sending logic, by conflating mailer, composer, database-backed queue, and templating concerns into a single module.
Example
In a typical scenario, it is advised to use VBT.Mailer
together with Oban based persistent
queue. The queue improves delivery guarantees, reducing the chance of e-mail not being sent
if something goes wrong (for example, if the mail server is not reachable).
To do this, you need to first create a new migration which initializes Oban tables:
defmodule MyProject.Migrations.InitializeOban do
use Ecto.Migration
def up, do: Oban.Migrations.up()
def down, do: Oban.Migrations.down()
end
Then you need to configure the email queue in config.exs
# `email: 10` defines the queue called "emails" with the maximum concurrency of 10
config :my_project, Oban, repo: MyRepo, queues: [email: 10]
Make sure to disable queues in config/test.exs
:
config :my_project, Oban, crontab: false, queues: false, plugins: false
Next, you need to start the oban process tree in your application:
defmodule MyProject.Application do
# ...
def start(_type, _args) do
children = [
# ...
MyRepo,
# make sure to start oban after the repo
{Oban, Application.fetch_env!(:my_project, Oban)},
# ...
]
# ...
end
end
Finally, you can define the mailer module in your context:
defmodule MyMailer do
use VBT.Mailer,
oban_worker: [queue: "email"],
templates: "templates",
adapter: Bamboo.SendGridAdapter
@spec send_password_reset(String.t(), String.t()) ::
{:ok, Oban.Job.t} | {:error, Ecto.Changeset.t}
def send_password_reset(email, password_reset_link) do
VBT.Mailer.enqueue(
__MODULE__,
"sender@x.y.z",
"recipient@x.y.z",
"Reset your password",
%{
layout: :some_layout,
template: :some_template,
password_reset_link: password_reset_link
}
)
end
@impl VBT.Mailer
def config(), do: %{api_key: System.fetch_env!("SENDGRID_API_KEY")}
end
Mailer is a wrapper around Bamboo, so it can use any conforming adapter. The adapter will only
be used in :prod
. Mailer always uses Bamboo.LocalAdapter
in :dev
, and Bamboo.TestAdapter
in :test
.
If you prefer to test the real adapter in development mode, you can pass the
dev_adapter: Bamboo.SendGridAdapter
option (or any other real adapter you're using). However,
it's advised to instead test the real mailer by running the :prod
-compiled version locally.
Transactions
If you need to send an e-mail inside a transaction, you can invoke enqueue/5
from within
Ecto.Multi.run/3
:
Ecto.Multi.run(multi, :enqueue_mail, fn _repo, _arg -> VBT.Mailer.enqueue(...) end)
In this case, the e-mail will be enqueued once the transaction is committed. If the transaction is rolled back, the e-mail will not be enqueued.
Queue options
The :oban_worker
options are passed to Oban.Worker.
By default, the :max_attempts
value and the auto generated backoff strategy will cause the
queue to retry for at most 24 hours.
Body templates
The template files (.eex) should be placed into the templates
folder, relative to the mailer
file.
The code above expects both .text and .html files to exist. If you want to use the text format only, you need to provide template as a string, and have it end with the .text suffix:
%{layout: :some_layout, template: "some_template.text"}
Note that layout is always provided as atom, without the extension. Bamboo will correctly
use some_layout.text.eex
to render the layout.
All other data in the map is forwarded as assigns to the template. In the example above,
a template can reference @password_reset_link
Using without templates
It is also possible to use mailer without Phoenix templates. In this case, you don't need to
provide the :templates
option in use
. When sending an e-mail, you can provide plain string
for the body argument. This string is used as the text body. If you wish to provide both text
and the html body, you can use %{text: text_body, html: html_body}
.
Immediate sending
If you want to skip the queue, you can use send!/5
, which will send the mail synchronously.
In this case, you don't need to provide the :oban_worker
option when using this module.
Testing
If e-mails are sent through the queue, you need to manually drain the queue before checking
for the delivery. For this purpose, a helper drain_queue
function is injected into your
mailer module, but only in the :test
environment:
test "sends reset password email" do
MyMailer.send_password_reset(email, reset_links)
MyMailer.drain_queue()
mail = assert_delivered_email(to: [{_, ^email}])
assert mail.text_body =~ reset_link
end
Dynamic repos
If you need to run multiple instances of mailer, pass the :name
option to Oban instance. Then
you need to pass the same name as an option to enqueue/6
and drain_queue/0
.
Link to this section Summary
Link to this section Types
Specs
Specs
Specs
Specs
opts() :: [ attachments: [Bamboo.Attachment.t()], cc: [address_list()], bcc: [address_list()], headers: %{required(String.t()) => String.t()}, name: GenServer.server() ]
Link to this section Functions
Specs
enqueue(module(), address(), address(), String.t(), body(), opts :: opts()) :: {:ok, Oban.Job.t()} | {:error, Ecto.Changeset.t()}
Enqueues the mail for sending.
Specs
send!(module(), address_list(), address_list(), String.t(), body(), opts()) :: :ok
Composes the email and sends it to the target address.
Link to this section Callbacks
Specs
config() :: map()