Skip to content

Commit 22e231d

Browse files
authored
feat: Add support for all Mailgun o: options (#718)
* feat: add new method to mailgun helper and adapter that allow to add different properties to body * refactor: format files using mix format
1 parent 8ed3680 commit 22e231d

4 files changed

Lines changed: 169 additions & 0 deletions

File tree

lib/bamboo/adapters/mailgun_adapter.ex

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,22 @@ defmodule Bamboo.MailgunAdapter do
5656
@internal_fields ~w(attachments)a
5757
@mailgun_message_fields ~w(from to cc bcc subject text html template recipient-variables)a
5858
@service_name "Mailgun"
59+
@allowed_mailgun_o_options [
60+
:"o:tag",
61+
:"o:dkim",
62+
:"o:deliverytime",
63+
:"o:testmode",
64+
:"o:tracking",
65+
:"o:tracking-clicks",
66+
:"o:tracking-opens",
67+
:"o:require-tls",
68+
:"o:skip-verification",
69+
:"o:sending-ip",
70+
:"o:sending-ip-pool",
71+
:"o:tracking-pixel-location-top",
72+
:"o:secondary-dkim",
73+
:"o:secondary-dkim-public"
74+
]
5975

6076
@doc false
6177
def handle_config(config) do
@@ -151,6 +167,7 @@ defmodule Bamboo.MailgunAdapter do
151167
|> put_template_version(email)
152168
|> put_template_text(email)
153169
|> put_custom_vars(email)
170+
|> put_options(email)
154171
|> put_recipient_variables(email)
155172
|> filter_non_empty_mailgun_fields
156173
|> encode_body
@@ -196,6 +213,8 @@ defmodule Bamboo.MailgunAdapter do
196213
end)
197214
end
198215

216+
# Deprecated: use put_options instead. These are kept for backward compatibility.
217+
# Will be removed in a future release.
199218
defp put_tag(body, %Email{private: %{:"o:tag" => tag}}), do: Map.put(body, :"o:tag", tag)
200219
defp put_tag(body, %Email{}), do: body
201220

@@ -252,6 +271,15 @@ defmodule Bamboo.MailgunAdapter do
252271
{"", attachment.data, {"form-data", [{"name", ~s/"attachment"/}, {"filename", ~s/"#{attachment.filename}"/}]}, []}
253272
end
254273

274+
defp put_options(body, %Email{private: private}) do
275+
Enum.reduce(@allowed_mailgun_o_options, body, fn key, acc ->
276+
case Map.fetch(private, key) do
277+
{:ok, value} -> Map.put(acc, key, value)
278+
:error -> acc
279+
end
280+
end)
281+
end
282+
255283
def filter_non_empty_mailgun_fields(body) do
256284
body
257285
|> Enum.filter(fn {key, value} ->

lib/bamboo/adapters/mailgun_helper.ex

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,23 @@ defmodule Bamboo.MailgunHelper do
88

99
@mailgun_header_for_custom_vars "X-Mailgun-Variables"
1010

11+
@allowed_mailgun_o_options [
12+
:"o:tag",
13+
:"o:dkim",
14+
:"o:deliverytime",
15+
:"o:testmode",
16+
:"o:tracking",
17+
:"o:tracking-clicks",
18+
:"o:tracking-opens",
19+
:"o:require-tls",
20+
:"o:skip-verification",
21+
:"o:sending-ip",
22+
:"o:sending-ip-pool",
23+
:"o:tracking-pixel-location-top",
24+
:"o:secondary-dkim",
25+
:"o:secondary-dkim-public"
26+
]
27+
1128
@doc """
1229
Add a tag to outgoing email to help categorize traffic based on some
1330
criteria, perhaps separate signup emails from password recovery emails
@@ -147,4 +164,27 @@ defmodule Bamboo.MailgunHelper do
147164
encoded_value = Bamboo.json_library().encode!(value)
148165
Email.put_private(email, :mailgun_recipient_variables, encoded_value)
149166
end
167+
168+
@doc """
169+
Set a Mailgun option (`o:` parameter) on the email in a safe, validated way.
170+
171+
Only the options allowed by the Mailgun API are supported. See:
172+
https://mailgun-docs.redoc.ly/docs/mailgun/api-reference/openapi-final/tag/Messages/#tag/Messages/operation/POST-v3--domain-name--messages
173+
174+
## Example
175+
176+
email
177+
|> MailgunHelper.option(:"o:tracking", "yes")
178+
|> MailgunHelper.option(:"o:tracking-clicks", "htmlonly")
179+
180+
If you try to set an unsupported option, an ArgumentError will be raised.
181+
"""
182+
def option(email, key, value) when key in @allowed_mailgun_o_options do
183+
Email.put_private(email, key, value)
184+
end
185+
186+
def option(_email, key, _value) do
187+
raise ArgumentError,
188+
"#{inspect(key)} is not a supported Mailgun option. See the Mailgun API docs for allowed options."
189+
end
150190
end

test/lib/bamboo/adapters/mailgun_adapter_test.exs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,76 @@ defmodule Bamboo.MailgunAdapterTest do
287287
assert params["t:text"] == "yes"
288288
end
289289

290+
test "deliver/2 includes whitelisted o: options from private" do
291+
email =
292+
[from: "from@foo.com", subject: "My Subject", text_body: "TEXT BODY", html_body: "HTML BODY"]
293+
|> new_email()
294+
|> Email.put_private(:"o:tracking", "yes")
295+
|> Email.put_private(:"o:tracking-clicks", "htmlonly")
296+
|> Email.put_private(:"o:dkim", "yes")
297+
|> Email.put_private(:"o:testmode", "true")
298+
299+
MailgunAdapter.deliver(email, @config)
300+
301+
assert_receive {:fake_mailgun, %{params: params}}
302+
303+
assert params["o:tracking"] == "yes"
304+
assert params["o:tracking-clicks"] == "htmlonly"
305+
assert params["o:dkim"] == "yes"
306+
assert params["o:testmode"] == "true"
307+
end
308+
309+
test "deliver/2 ignores unsupported o: options from private" do
310+
email =
311+
[from: "from@foo.com", subject: "My Subject", text_body: "TEXT BODY", html_body: "HTML BODY"]
312+
|> new_email()
313+
|> Email.put_private(:"o:tracking", "yes")
314+
|> Email.put_private(:"o:unsupported-option", "value")
315+
|> Email.put_private(:"o:invalid", "should-be-ignored")
316+
317+
MailgunAdapter.deliver(email, @config)
318+
319+
assert_receive {:fake_mailgun, %{params: params}}
320+
321+
# Supported option should be included
322+
assert params["o:tracking"] == "yes"
323+
324+
# Unsupported options should be ignored
325+
refute Map.has_key?(params, "o:unsupported-option")
326+
refute Map.has_key?(params, "o:invalid")
327+
end
328+
329+
test "deliver/2 works with all allowed o: options" do
330+
email =
331+
[from: "from@foo.com", subject: "My Subject", text_body: "TEXT BODY", html_body: "HTML BODY"]
332+
|> new_email()
333+
|> Email.put_private(:"o:tag", ["tag1", "tag2"])
334+
|> Email.put_private(:"o:deliverytime", "Wed, 15 Nov 2023 09:30:00 +0000")
335+
|> Email.put_private(:"o:tracking-opens", "yes")
336+
|> Email.put_private(:"o:require-tls", "true")
337+
|> Email.put_private(:"o:skip-verification", "false")
338+
|> Email.put_private(:"o:sending-ip", "192.168.1.1")
339+
|> Email.put_private(:"o:sending-ip-pool", "pool-123")
340+
|> Email.put_private(:"o:tracking-pixel-location-top", "yes")
341+
|> Email.put_private(:"o:secondary-dkim", "example.com/s1")
342+
|> Email.put_private(:"o:secondary-dkim-public", "public.com/s1")
343+
344+
MailgunAdapter.deliver(email, @config)
345+
346+
assert_receive {:fake_mailgun, %{params: params}}
347+
348+
assert params["o:tag"] == ["tag1", "tag2"]
349+
assert params["o:deliverytime"] == "Wed, 15 Nov 2023 09:30:00 +0000"
350+
assert params["o:tracking-opens"] == "yes"
351+
assert params["o:require-tls"] == "true"
352+
assert params["o:skip-verification"] == "false"
353+
assert params["o:sending-ip"] == "192.168.1.1"
354+
assert params["o:sending-ip-pool"] == "pool-123"
355+
assert params["o:tracking-pixel-location-top"] == "yes"
356+
assert params["o:secondary-dkim"] == "example.com/s1"
357+
assert params["o:secondary-dkim-public"] == "public.com/s1"
358+
end
359+
290360
test "returns an error if the response is not a success" do
291361
email = new_email(from: "INVALID_EMAIL")
292362

test/lib/bamboo/adapters/mailgun_helper_test.exs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,35 @@ defmodule Bamboo.MailgunHelperTest do
7171
mailgun_recipient_variables: "{\"user1@example.com\":{\"unique_id\":\"ABC123456789\"}}"
7272
}
7373
end
74+
75+
test "option/3 adds allowed o: options to private" do
76+
email = MailgunHelper.option(new_email(), :"o:tracking", "yes")
77+
assert email |> Map.get(:private, %{}) |> Map.get(:"o:tracking") == "yes"
78+
79+
email = MailgunHelper.option(email, :"o:tracking-clicks", "htmlonly")
80+
assert email |> Map.get(:private, %{}) |> Map.get(:"o:tracking-clicks") == "htmlonly"
81+
end
82+
83+
test "option/3 raises error for unsupported o: options" do
84+
assert_raise ArgumentError, ~r/not a supported Mailgun option/, fn ->
85+
MailgunHelper.option(new_email(), :"o:unsupported", "value")
86+
end
87+
88+
assert_raise ArgumentError, ~r/not a supported Mailgun option/, fn ->
89+
MailgunHelper.option(new_email(), :"o:invalid-option", "value")
90+
end
91+
end
92+
93+
test "option/3 works with all allowed o: options" do
94+
email = new_email()
95+
96+
# Test a few more allowed options
97+
email = MailgunHelper.option(email, :"o:dkim", "yes")
98+
email = MailgunHelper.option(email, :"o:testmode", "yes")
99+
email = MailgunHelper.option(email, :"o:require-tls", "true")
100+
101+
assert email.private[:"o:dkim"] == "yes"
102+
assert email.private[:"o:testmode"] == "yes"
103+
assert email.private[:"o:require-tls"] == "true"
104+
end
74105
end

0 commit comments

Comments
 (0)