diff --git a/.env-sample b/.env-sample index 52603f04..6572a498 100644 --- a/.env-sample +++ b/.env-sample @@ -1,4 +1,25 @@ +# ---------------------------# +# Sentry +# ---------------------------# SENTRY_DNS= SENTRY_ENV= +# ---------------------------# +# Oban +# ---------------------------# START_BAN_JOBS= + +# ---------------------------# +# Google Cloud +# ---------------------------# +GCP_CLIENT_ID= +GCP_CLIENT_SECRET= + +# ---------------------------# +# Mailer +# ---------------------------# +MAILSERVICE= +MAIL_SERVER= +MAIL_USERNAME= +MAIL_PASSWORD= +MAIL_PORT= diff --git a/README.org b/README.org index 474c5c94..34c12c29 100644 --- a/README.org +++ b/README.org @@ -93,6 +93,48 @@ docker-compose run --rm fuschia mix ecto.setup ----- +** Aplicações + +Esse projeto está dividio em diversas sub-aplicações que possuem diferentes responsabilidades. + +#+begin_example + +#+end_example + +*** Fuschia.Mailer + +Responsável pelo processamento e envio/disparo dos emails. Estamos utilizando o [[Swoosh][https://github.com/swoosh/swoosh]]. + +Para testar o preview de email, siga a seguinte documentação: + +**** Variáveis de ambiente +Necessárias em produção: +- =MAIL_SERVER=: Server do smtp (default: =smtp.gmail.com=) +- =MAIL_USERNAME=: User do smtp (default: =notificacoes-noreply@peapescarte.uenf.br=) +- =MAIL_PASSWORD=: Senha do smtp +- =MAIL_PORT=: Porta do smtp (default: =587=) +- =MAIL_SERVICE=: O serviço de email a ser usado. Pode ser =gmail= ou =local=. + (default prod: =gmail=, default dev: =local=) + +*Observação*: Em ambiente de desenvolvimento, toda vez que a variável de ambiente =MAIL_SERVICE= é alterada +para trocar o adapter, toda a aplicação deve ser compilada usando + +#+begin_src sh +mix compile --force +#+end_src + +**** Rodando localmente +Essa aplicação também conta com um servidor para visualizar os emails enviados usando o adaptador local, +basta entrar na aplicação/container e utilizar: + +#+begin_src sh +iex -S mix swoosh.mailbox.server +#+end_src + +Que um servidor no local https://127.0.0.1:4001 vai apresentar uma página web listando os emails +enviados localmente através do `iex` que ficou aberto com o comando anterior. + +----- ** Rodando os testes diff --git a/config/runtime.exs b/config/runtime.exs index 688816d9..1cb0a653 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -25,10 +25,37 @@ config :sentry, # ---------------------------# config :fuschia, Oban, repo: Fuschia.Repo, - queues: [mailers: 5] + queues: [mailer: 5] config :fuschia, :jobs, start: System.get_env("START_OBAN_JOBS", "true") +# ---------------------------# +# Mailer +# ---------------------------# +adapter = + case System.get_env("MAIL_SERVICE", "local") do + "gmail" -> Swoosh.Adapters.SMTP + _ -> Swoosh.Adapters.Local + end + +if adapter == Swoosh.Adapters.Local do + config :swoosh, serve_mailbox: true, preview_port: 4001 +end + +config :fuschia, Fuschia.Mailer, + adapter: adapter, + relay: System.get_env("MAIL_SERVER", "smtp.gmail.com"), + username: System.get_env("MAIL_USERNAME", "notificacoes-noreply@peapescarte.uenf.br"), + password: System.get_env("MAIL_PASSWORD", ""), + ssl: false, + tls: :always, + auth: :always, + port: System.get_env("MAIL_PORT", "587") + +config :fuschia, :pea_pescarte_contact, + notifications_mail: "notifications-noreply@peapescarte.uenf.br", + telephone: " 0800 026 2828" + # ---------------------------# # Timex # ---------------------------# diff --git a/lib/fuschia/jobs/mailer_job.ex b/lib/fuschia/jobs/mailer_job.ex new file mode 100644 index 00000000..f8de6595 --- /dev/null +++ b/lib/fuschia/jobs/mailer_job.ex @@ -0,0 +1,46 @@ +defmodule Fuschia.Jobs.MailerJob do + @moduledoc false + + use Oban.Worker, queue: :mailer, max_attempts: 4 + + require Logger + + alias Fuschia.{Mailer, Parser} + + @doc """ + Deliver an email to partner + Example: + iex> %{ + to: "test_user@gmail.com", + subject: "some subject", + layout: "notificacao", + template: "nova_midia", + assigns: %{user_cpf: "999.999.999-99"} + } + |> Fuschia.Jobs.MailerJob.new() + |> Oban.insert!() + """ + @impl Oban.Worker + def perform(%{args: args}) do + Logger.info("==> [MailerJob] Sending email...") + + Mailer.new_email( + args["to"], + args["subject"], + args["layout"], + args["template"], + Parser.atomize_map(args["assigns"]), + args["base"], + args["bcc"] + ) + |> Mailer.deliver!() + + Logger.info("==> [MailerJob] Sent email") + + :ok + rescue + error -> + Logger.error(Exception.format(:error, error, __STACKTRACE__)) + {:error, error} + end +end diff --git a/lib/fuschia/mailer.ex b/lib/fuschia/mailer.ex new file mode 100644 index 00000000..e08543ee --- /dev/null +++ b/lib/fuschia/mailer.ex @@ -0,0 +1,48 @@ +defmodule Fuschia.Mailer do + @moduledoc """ + Mailer public API + """ + + use Swoosh.Mailer, otp_app: :fuschia + + alias Fuschia.Mailer.HTML + alias Swoosh.Email + + @doc """ + Returns an email structure populated with a `recipient` and a + `subject` and assembles the email's html body based on the given + templates `layout` and `email` and given `assigns`. + """ + @spec new_email( + String.t() | {String.t(), String.t()} | [String.t()] | [{String.t(), String.t()}], + String.t(), + String.t(), + String.t(), + map, + String.t(), + String.t() | {String.t(), String.t()} | [String.t()] | [{String.t(), String.t()}] | [] + ) :: Email.t() + def new_email(recipient, subject, layout, template, assigns \\ %{}, base \\ "base", bcc \\ []) + when is_map(assigns) do + body = HTML.assemble_body(layout, template, assigns, base) + + Email.new() + |> Email.to(recipient) + |> Email.bcc(bcc) + |> Email.from({"Plataforma PEA Pescarte", notifications_mail()}) + |> Email.subject("[Plataforma PEA Pescarte] #{subject}") + |> Email.html_body(body) + end + + @doc """ + Add a new attachment to the email. + """ + @spec add_attachment(Email.t(), String.t()) :: Email.t() + def add_attachment(%Email{} = struct, file) when is_binary(file) do + Email.attachment(struct, file) + end + + defp notifications_mail do + Application.get_env(:fuschia, :pea_pescarte_contact)[:notifications_mail] + end +end diff --git a/lib/fuschia/mailer/html.ex b/lib/fuschia/mailer/html.ex new file mode 100644 index 00000000..3617b9f3 --- /dev/null +++ b/lib/fuschia/mailer/html.ex @@ -0,0 +1,48 @@ +defmodule Fuschia.Mailer.HTML do + @moduledoc false + + @doc """ + Inject the `assigns` values into the `email` template's .eex tags + from the "homologacao" or "financiamento" `project` and then render + the resulting HTML page into a base template. + Returns an HTML page in string format. + """ + @spec assemble_body(String.t(), String.t(), map) :: any + def assemble_body(project, email, assigns, base \\ "base") when is_map(assigns) do + assigns + |> rewrite_name() + |> rewrite_email_address() + |> render_email(project, email) + |> render_layout(base) + end + + def templates_path, do: "#{:code.priv_dir(:fuschia)}/templates" + + defp pea_pescarte_contact do + :fuschia + |> Application.get_env(:pea_pescarte_contact) + |> Map.new() + end + + defp rewrite_name(%{user_nome_completo: name} = assigns), do: Map.put(assigns, :nome, name) + defp rewrite_name(%{nome: name} = assigns), do: Map.put(assigns, :nome, name) + defp rewrite_name(assigns), do: Map.put(assigns, :nome, "") + + defp rewrite_email_address(%{email: email} = assigns), do: Map.put(assigns, :email, email) + defp rewrite_email_address(assigns), do: Map.put(assigns, :email, "") + + defp render_email(assigns, project, email) do + EEx.eval_file("#{templates_path()}/email/#{project}/#{email}.html.eex", + assigns: assigns, + pea_pescarte: pea_pescarte_contact() + ) + end + + defp render_layout(inner_content, nil), do: render_layout(inner_content, "base") + + defp render_layout(inner_content, base) do + EEx.eval_file("#{templates_path()}/layout/#{base}.html.eex", + assigns: [inner_content: inner_content] + ) + end +end diff --git a/lib/fuschia/parser.ex b/lib/fuschia/parser.ex index 35ae9f55..a0a6da58 100644 --- a/lib/fuschia/parser.ex +++ b/lib/fuschia/parser.ex @@ -106,6 +106,8 @@ defmodule Fuschia.Parser do |> Map.new() end + def atomize_map(value), do: value + @doc ~S""" Converts an antom map to a string map diff --git a/lib/fuschia_web/swagger/schemas/auth.ex b/lib/fuschia_web/swagger/schemas/auth.ex index a8aa8df1..bbe2564c 100644 --- a/lib/fuschia_web/swagger/schemas/auth.ex +++ b/lib/fuschia_web/swagger/schemas/auth.ex @@ -142,7 +142,7 @@ defmodule FuschiaWeb.Swagger.AuthSchemas do "user" => %{ "dataNasc" => "2021-07-27", "cpf" => "999.999.999-99", - "email" => "teste@solfacil.com.br", + "email" => "teste@pescarte.com.br", "nomeCompleto" => "Joãozinho Testinho", "perfil" => "pesquisador", "permissoes" => %{ diff --git a/mix.exs b/mix.exs index 87d0a835..3c984047 100644 --- a/mix.exs +++ b/mix.exs @@ -50,6 +50,8 @@ defmodule Fuschia.MixProject do [ {:phoenix, "~> 1.5.9"}, {:phoenix_ecto, "~> 4.1"}, + {:swoosh, "~> 1.4"}, + {:mail, ">= 0.0.0"}, {:bcrypt_elixir, "~> 2.0"}, {:paginator, "~> 1.0.3"}, {:ecto_sql, "~> 3.4"}, diff --git a/mix.lock b/mix.lock index c9f1aea0..78eb3ea6 100644 --- a/mix.lock +++ b/mix.lock @@ -28,6 +28,7 @@ "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"}, "jose": {:hex, :jose, "1.11.1", "59da64010c69aad6cde2f5b9248b896b84472e99bd18f246085b7b9fe435dcdb", [:mix, :rebar3], [], "hexpm", "078f6c9fb3cd2f4cfafc972c814261a7d1e8d2b3685c0a76eb87e158efff1ac5"}, + "mail": {:hex, :mail, "0.2.3", "2c6bb5f8a5f74845fa50ecd0fb45ea16b164026f285f45104f1c4c078cd616d4", [:mix], [], "hexpm", "932b398fa9c69fdf290d7ff63175826e0f1e24414d5b0763bb00a2acfc6c6bf5"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, @@ -50,6 +51,7 @@ "recase": {:hex, :recase, "0.7.0", "3f2f719f0886c7a3b7fe469058ec539cb7bbe0023604ae3bce920e186305e5ae", [:mix], [], "hexpm", "36f5756a9f552f4a94b54a695870e32f4e72d5fad9c25e61bc4a3151c08a4e0c"}, "sentry": {:hex, :sentry, "8.0.5", "5ca922b9238a50c7258b52f47364b2d545beda5e436c7a43965b34577f1ef61f", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.6", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, "~> 2.3", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm", "4972839fdbf52e886d7b3e694c8adf421f764f2fa79036b88fb4742049bd4b7c"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, + "swoosh": {:hex, :swoosh, "1.5.0", "2be4cfc1be10f2203d1854c85b18d8c7be0321445a782efd53ef0b2b88f03ce4", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b53891359e3ddca263ece784051243de84c9244c421a0dee1bff1d52fc5ca420"}, "telemetry": {:hex, :telemetry, "0.4.3", "a06428a514bdbc63293cd9a6263aad00ddeb66f608163bdec7c8995784080818", [:rebar3], [], "hexpm", "eb72b8365ffda5bed68a620d1da88525e326cb82a75ee61354fc24b844768041"}, "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"}, "telemetry_poller": {:hex, :telemetry_poller, "0.5.1", "21071cc2e536810bac5628b935521ff3e28f0303e770951158c73eaaa01e962a", [:rebar3], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4cab72069210bc6e7a080cec9afffad1b33370149ed5d379b81c7c5f0c663fd4"}, diff --git a/priv/templates/email/user/confirmation.html.eex b/priv/templates/email/user/confirmation.html.eex new file mode 100644 index 00000000..d2c349e3 --- /dev/null +++ b/priv/templates/email/user/confirmation.html.eex @@ -0,0 +1,24 @@ +

Olá, <%= @nome %>!

+

Você recebeu um convite para se cadastrar na plataforma PEA Pescarte!

+

Ao clicar no link abaixo, você irá se cadastrar como pesquisador!

+ + CONFIRMAR CADASTRO + diff --git a/priv/templates/layout/base.html.eex b/priv/templates/layout/base.html.eex new file mode 100644 index 00000000..e6a077df --- /dev/null +++ b/priv/templates/layout/base.html.eex @@ -0,0 +1,257 @@ + + + + + Plataforma PEA Pescarte + + + + + + + + + + + + + + + + +
+ + +
+
+ + + + + + +
+ +
+ + + + + + +
+ + + + + + +
+ + + + + + +
Plataforma PEA Pescarte
+
+
+
+ +
+
+
+ +
+ + + + + + +
+ +
+ + + + + + +
+ + + + + + +
+
+ + <%= @inner_content %> + +
+
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + +
+ + + + + + +
+ + + + + +
Solfácil + pea-pescarte.uenf.br +
+
+
+
+ +
+
+ +
+ + + diff --git a/test/fuschia/jobs/mailer_job_test.exs b/test/fuschia/jobs/mailer_job_test.exs new file mode 100644 index 00000000..3ecec416 --- /dev/null +++ b/test/fuschia/jobs/mailer_job_test.exs @@ -0,0 +1,32 @@ +defmodule FuschiaJobs.MailerJobTest do + use Fuschia.DataCase + use Oban.Testing, repo: Fuschia.Repo + + import ExUnit.CaptureLog + + alias Fuschia.Jobs.MailerJob + + describe "perform/1" do + test "successfully execute mailer job" do + args = %{ + to: "test_user@gmail.com", + subject: "some subject", + layout: "user", + template: "confirmation", + assigns: %{name: "Juninho testinho", link: "https://teste-peapescarte.uenf.br/confirm"}, + base: "base", + bcc: [] + } + + assert :ok == perform_job(MailerJob, args) + end + + test "failure execute mailer job" do + args = %{bad: :arg} + + assert capture_log(fn -> + perform_job(MailerJob, args) + end) =~ "Mailer.new_email(nil, nil, nil, nil, nil, nil, nil)" + end + end +end diff --git a/test/fuschia/mailer_test.exs b/test/fuschia/mailer_test.exs new file mode 100644 index 00000000..913f34e1 --- /dev/null +++ b/test/fuschia/mailer_test.exs @@ -0,0 +1,55 @@ +defmodule Fuschia.MailerTest.GenerateTests do + defmacro __using__(_opts) do + quote do + use ExUnit.Case + + alias Fuschia.{Mailer, MailerTest} + alias Swoosh.Email + + @general_assigns %{ + id: "1", + link: "http://link.dev", + comentario: "Esse é um comentário", + user_nome_completo: "Usuário Joaozinho", + user_contato_email: "joaozinho@contato.com", + endereco_street: "Av. Paulista, 9876", + endereco_neighborhood: "Bela Vista", + endereco_cep: "01310-100", + endereco_city: "São Paulo", + endereco_state: "SP" + } + + for project <- ["user"] do + for email <- MailerTest.GenerateTests.emails(project) do + @tag email: email, project: project + test ~s(new_email/7 in #{project} using #{email} template returns an email structure), + %{email: email, project: project} do + assert( + %Email{} = Mailer.new_email(nil, nil, project, email, @general_assigns, "base", nil) + ) + end + end + end + end + end + + def emails(project) do + template_path = "#{:code.priv_dir(:fuschia)}/templates" + path = Path.expand("#{template_path}/email/#{project}", __DIR__) + + Path.wildcard(path <> "/*") + |> Enum.map(&String.replace(&1, [path <> "/", ".html.eex"], "")) + end +end + +defmodule Fuschia.MailerTest do + use Fuschia.MailerTest.GenerateTests + + alias Fuschia.Mailer + + test "add_attachment/2 always returns the email structure" do + assert %Email{} = + Mailer.new_email(nil, nil, "user", "confirmation", @general_assigns) + |> Mailer.add_attachment("") + end +end