Skip to content

Commit 10fcd6c

Browse files
committed
Autogenerate text emails (#4674)
* autogenerate text emails * fix export email formatting * fewer changes * add full text_body test * eh * cleanup * explain * remove recursive collapse_whitespace * remove comment
1 parent c2c958f commit 10fcd6c

File tree

7 files changed

+158
-9
lines changed

7 files changed

+158
-9
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file.
44
## Unreleased
55

66
### Added
7+
8+
- Add text version to emails plausible/analytics#4674
9+
710
### Removed
811
### Changed
912
### Fixed

lib/plausible_web/email.ex

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
defmodule PlausibleWeb.Email do
22
use Plausible
3-
use Bamboo.Phoenix, view: PlausibleWeb.EmailView
3+
import Bamboo.Email
44
import Bamboo.PostmarkHelper
55

66
def mailer_email_from do
@@ -450,17 +450,71 @@ defmodule PlausibleWeb.Email do
450450
def base_email(), do: base_email(%{layout: "base_email.html"})
451451

452452
def base_email(%{layout: layout}) do
453-
mailer_from = Application.get_env(:plausible, :mailer_email)
454-
455453
new_email()
456454
|> put_param("TrackOpens", false)
457-
|> from(mailer_from)
455+
|> from(mailer_email_from())
458456
|> maybe_put_layout(layout)
459457
end
460458

461459
defp maybe_put_layout(email, nil), do: email
462460

463-
defp maybe_put_layout(email, layout) do
464-
put_html_layout(email, {PlausibleWeb.LayoutView, layout})
461+
defp maybe_put_layout(%{assigns: assigns} = email, layout) do
462+
%{email | assigns: Map.put(assigns, :layout, {PlausibleWeb.LayoutView, layout})}
463+
end
464+
465+
@doc false
466+
def render(email, template, assigns \\ []) do
467+
assigns = Map.merge(email.assigns, Map.new(assigns))
468+
html = Phoenix.View.render_to_string(PlausibleWeb.EmailView, template, assigns)
469+
email |> html_body(html) |> text_body(textify(html))
470+
end
471+
472+
defp textify(html) do
473+
Floki.parse_fragment!(html)
474+
|> traverse_and_textify()
475+
|> Floki.text()
476+
|> collapse_whitespace()
477+
end
478+
479+
defp traverse_and_textify([head | tail]) do
480+
[traverse_and_textify(head) | traverse_and_textify(tail)]
481+
end
482+
483+
defp traverse_and_textify(text) when is_binary(text) do
484+
String.replace(text, "\n", "\s")
485+
end
486+
487+
defp traverse_and_textify({"a" = tag, attrs, children}) do
488+
href = with {"href", href} <- List.keyfind(attrs, "href", 0), do: href
489+
children = traverse_and_textify(children)
490+
491+
if href do
492+
text = Floki.text(children)
493+
494+
if text == href do
495+
# avoids rendering "http://localhost:8000 (http://localhost:8000)" in base_email footer
496+
text
497+
else
498+
IO.iodata_to_binary([text, " (", href, ?)])
499+
end
500+
else
501+
{tag, attrs, children}
502+
end
503+
end
504+
505+
defp traverse_and_textify({tag, attrs, children}) do
506+
{tag, attrs, traverse_and_textify(children)}
507+
end
508+
509+
defp traverse_and_textify(other), do: other
510+
511+
defp collapse_whitespace(text) do
512+
text
513+
|> String.split("\n")
514+
|> Enum.map_join("\n", fn line ->
515+
line
516+
|> String.split(" ", trim: true)
517+
|> Enum.join(" ")
518+
end)
465519
end
466520
end

mix.exs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ defmodule Plausible.MixProject do
6363
defp deps do
6464
[
6565
{:bamboo, "~> 2.3", override: true},
66-
{:bamboo_phoenix, "~> 1.0.0"},
6766
{:bamboo_postmark, git: "https://github.com/plausible/bamboo_postmark.git", branch: "main"},
6867
{:bamboo_smtp, "~> 4.1"},
6968
{:bamboo_mua, "~> 0.2.0"},

mix.lock

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
"acceptor_pool": {:hex, :acceptor_pool, "1.0.0", "43c20d2acae35f0c2bcd64f9d2bde267e459f0f3fd23dab26485bf518c281b21", [:rebar3], [], "hexpm", "0cbcd83fdc8b9ad2eee2067ef8b91a14858a5883cb7cd800e6fcd5803e158788"},
33
"bamboo": {:hex, :bamboo, "2.3.0", "d2392a2cabe91edf488553d3c70638b532e8db7b76b84b0a39e3dfe492ffd6fc", [:mix], [{:hackney, ">= 1.15.2", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.4 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "dd0037e68e108fd04d0e8773921512c940e35d981e097b5793543e3b2f9cd3f6"},
44
"bamboo_mua": {:hex, :bamboo_mua, "0.2.2", "c50cd41ef684155669e2d99d428fbb87e13797a80829a162b47d3c0a7f7e7ecd", [:mix], [{:bamboo, "~> 2.0", [hex: :bamboo, repo: "hexpm", optional: false]}, {:mail, "~> 0.3.0", [hex: :mail, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: false]}], "hexpm", "5fe6e3676640578c6fe8f040b34dda8991ebef8566c0601a984eb4771b85b11f"},
5-
"bamboo_phoenix": {:hex, :bamboo_phoenix, "1.0.0", "f3cc591ffb163ed0bf935d256f1f4645cd870cf436545601215745fb9cc9953f", [:mix], [{:bamboo, ">= 2.0.0", [hex: :bamboo, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.3.0", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "6db88fbb26019c84a47994bb2bd879c0887c29ce6c559bc6385fd54eb8b37dee"},
65
"bamboo_postmark": {:git, "https://github.com/plausible/bamboo_postmark.git", "5eac6dfdffacd273bd9aacdd3e494a600bb0b170", [branch: "main"]},
76
"bamboo_smtp": {:hex, :bamboo_smtp, "4.2.2", "e9f57a2300df9cb496c48751bd7668a86a2b89aa2e79ccaa34e0c46a5f64c3ae", [:mix], [{:bamboo, "~> 2.2.0", [hex: :bamboo, repo: "hexpm", optional: false]}, {:gen_smtp, "~> 1.2.0", [hex: :gen_smtp, repo: "hexpm", optional: false]}], "hexpm", "28cac2ec8adaae02aed663bf68163992891a3b44cfd7ada0bebe3e09bed7207f"},
87
"bcrypt_elixir": {:hex, :bcrypt_elixir, "3.1.0", "0b110a9a6c619b19a7f73fa3004aa11d6e719a67e672d1633dc36b6b2290a0f7", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "2ad2acb5a8bc049e8d5aa267802631912bb80d5f4110a178ae7999e69dca1bf7"},

test/plausible/imported/csv_importer_test.exs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,9 @@ defmodule Plausible.Imported.CSVImporterTest do
541541
assert email.html_body =~
542542
~s[Please click <a href="http://localhost:8000/#{URI.encode_www_form(exported_site.domain)}/download/export">here</a> to start the download process.]
543543

544+
assert email.text_body =~
545+
~r[Please click here \(http://localhost:8000/#{URI.encode_www_form(exported_site.domain)}/download/export\) to start the download process.]
546+
544547
# download archive
545548
on_ee do
546549
ExAws.request!(

test/plausible_web/email_test.exs

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ defmodule PlausibleWeb.EmailTest do
1313
})
1414

1515
assert email.html_body =~ "Hey John,"
16+
assert email.text_body =~ "Hey John,"
1617
end
1718

1819
test "greets impersonally when user not in template assigns" do
@@ -21,6 +22,7 @@ defmodule PlausibleWeb.EmailTest do
2122
|> Email.render("welcome_email.html")
2223

2324
assert email.html_body =~ "Hey,"
25+
assert email.text_body =~ "Hey,"
2426
end
2527

2628
test "renders plausible link" do
@@ -29,6 +31,7 @@ defmodule PlausibleWeb.EmailTest do
2931
|> Email.render("welcome_email.html")
3032

3133
assert email.html_body =~ plausible_link()
34+
assert email.text_body =~ plausible_url()
3235
end
3336

3437
@tag :ee_only
@@ -49,6 +52,9 @@ defmodule PlausibleWeb.EmailTest do
4952

5053
refute email.html_body =~ "Hey John,"
5154
refute email.html_body =~ plausible_link()
55+
56+
refute email.text_body =~ "Hey John,"
57+
refute email.text_body =~ plausible_url()
5258
end
5359
end
5460

@@ -73,6 +79,7 @@ defmodule PlausibleWeb.EmailTest do
7379
})
7480

7581
assert email.html_body =~ "Hey John,"
82+
assert email.text_body =~ "Hey John,"
7683
end
7784

7885
test "greets impersonally when user not in template assigns" do
@@ -83,6 +90,7 @@ defmodule PlausibleWeb.EmailTest do
8390
})
8491

8592
assert email.html_body =~ "Hey,"
93+
assert email.text_body =~ "Hey,"
8694
end
8795

8896
test "renders plausible link" do
@@ -93,6 +101,7 @@ defmodule PlausibleWeb.EmailTest do
93101
})
94102

95103
assert email.html_body =~ plausible_link()
104+
assert email.text_body =~ plausible_url()
96105
end
97106

98107
test "does not render unsubscribe placeholder" do
@@ -114,6 +123,9 @@ defmodule PlausibleWeb.EmailTest do
114123

115124
refute email.html_body =~ "Hey John,"
116125
refute email.html_body =~ plausible_link()
126+
127+
refute email.text_body =~ "Hey John,"
128+
refute email.text_body =~ plausible_url()
117129
end
118130
end
119131

@@ -321,8 +333,86 @@ defmodule PlausibleWeb.EmailTest do
321333
end
322334
end
323335

336+
describe "text_body" do
337+
@tag :ee_only
338+
test "welcome_email (EE)" do
339+
email =
340+
Email.base_email()
341+
|> Email.render("welcome_email.html", %{
342+
user: build(:user, name: "John Doe"),
343+
code: "123"
344+
})
345+
346+
assert email.text_body == """
347+
Hey John,
348+
349+
We are building Plausible to provide a simple and ethical approach to tracking website visitors. We're super excited to have you on board!
350+
351+
Here's how to get the most out of your Plausible experience:
352+
353+
* Enable email reports (https://plausible.io/docs/email-reports) and notifications for traffic spikes (https://plausible.io/docs/traffic-spikes)
354+
* Integrate with Search Console (https://plausible.io/docs/google-search-console-integration) to get keyword phrases people find your site with
355+
* Invite team members and other collaborators (https://plausible.io/docs/users-roles)
356+
* Set up easy goals including 404 error pages (https://plausible.io/docs/error-pages-tracking-404), file downloads (https://plausible.io/docs/file-downloads-tracking) and outbound link clicks (https://plausible.io/docs/outbound-link-click-tracking)
357+
* Opt out from counting your own visits (https://plausible.io/docs/excluding)
358+
* If you're concerned about adblockers, set up a proxy to bypass them (https://plausible.io/docs/proxy/introduction)
359+
360+
361+
Then you're ready to start exploring your fast loading, ethical and actionable Plausible dashboard (https://plausible.io/sites).
362+
363+
Have a question, feedback or need some guidance? Do reply back to this email.
364+
365+
Regards,
366+
The Plausible Team 💌
367+
368+
--
369+
370+
http://localhost:8000
371+
{{{ pm:unsubscribe }}}\
372+
"""
373+
end
374+
375+
@tag :ce_build_only
376+
test "welcome_email (CE)" do
377+
email =
378+
Email.base_email()
379+
|> Email.render("welcome_email.html", %{
380+
user: build(:user, name: "John Doe"),
381+
code: "123"
382+
})
383+
384+
assert email.text_body == """
385+
Hey John,
386+
387+
We are building Plausible to provide a simple and ethical approach to tracking website visitors. We're super excited to have you on board!
388+
389+
Here's how to get the most out of your Plausible experience:
390+
391+
* Enable email reports (https://plausible.io/docs/email-reports) and notifications for traffic spikes (https://plausible.io/docs/traffic-spikes)
392+
* Integrate with Search Console (https://plausible.io/docs/google-search-console-integration) to get keyword phrases people find your site with
393+
* Invite team members and other collaborators (https://plausible.io/docs/users-roles)
394+
* Set up easy goals including 404 error pages (https://plausible.io/docs/error-pages-tracking-404), file downloads (https://plausible.io/docs/file-downloads-tracking) and outbound link clicks (https://plausible.io/docs/outbound-link-click-tracking)
395+
* Opt out from counting your own visits (https://plausible.io/docs/excluding)
396+
* If you're concerned about adblockers, set up a proxy to bypass them (https://plausible.io/docs/proxy/introduction)
397+
398+
399+
Then you're ready to start exploring your fast loading, ethical and actionable Plausible dashboard (https://plausible.io/sites).
400+
401+
Have a question, feedback or need some guidance? Do reply back to this email.
402+
403+
--
404+
405+
http://localhost:8000
406+
"""
407+
end
408+
end
409+
410+
def plausible_url do
411+
PlausibleWeb.EmailView.plausible_url()
412+
end
413+
324414
def plausible_link() do
325-
plausible_url = PlausibleWeb.EmailView.plausible_url()
415+
plausible_url = plausible_url()
326416
"<a href=\"#{plausible_url}\">#{plausible_url}</a>"
327417
end
328418
end

test/workers/notify_exported_analytics_test.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ defmodule Plausible.Workers.NotifyExportedAnalyticsTest do
2727

2828
assert_receive {:delivered_email, email}
2929
assert email.html_body =~ "was unsuccessful."
30+
assert email.text_body =~ "was unsuccessful."
3031
end
3132
end
3233
end

0 commit comments

Comments
 (0)