├── VERSION ├── priv └── templates │ └── welcome │ ├── welcome.html │ └── welcome.txt ├── test ├── mailer │ ├── templates │ │ ├── plain │ │ │ ├── en │ │ │ │ └── plain.txt │ │ │ └── plain.txt │ │ └── multipart │ │ │ ├── multipart.html │ │ │ └── multipart.txt │ ├── renderer_test.exs │ ├── template_locator_test.exs │ ├── mailer_test.exs │ ├── email_plain_test.exs │ └── email_multipart_test.exs ├── test_helper.exs └── app_test.exs ├── .gitignore ├── lib ├── mailer │ ├── renderer.ex │ ├── util.ex │ ├── message_id.ex │ ├── server_cfg.ex │ ├── smtp_client.ex │ ├── template_locator.ex │ ├── client_cfg.ex │ ├── email_plain.ex │ ├── smtp_server.ex │ └── email_multipart.ex ├── support │ ├── test_mail_handler.ex │ └── test_transport.ex └── mailer.ex ├── config ├── dev.exs ├── prod.exs ├── test.exs └── config.exs ├── LICENCE.txt ├── mix.exs ├── mix.lock └── README.md /VERSION: -------------------------------------------------------------------------------- 1 | 1.3.0 2 | -------------------------------------------------------------------------------- /priv/templates/welcome/welcome.html: -------------------------------------------------------------------------------- 1 | html -------------------------------------------------------------------------------- /priv/templates/welcome/welcome.txt: -------------------------------------------------------------------------------- 1 | Plain -------------------------------------------------------------------------------- /test/mailer/templates/plain/en/plain.txt: -------------------------------------------------------------------------------- 1 | Hello <%= @name %> -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | erl_crash.dump 4 | *.ez 5 | -------------------------------------------------------------------------------- /test/mailer/templates/plain/plain.txt: -------------------------------------------------------------------------------- 1 | Default <%= @name %> 2 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start([colors: [enabled: true]]) 2 | -------------------------------------------------------------------------------- /test/mailer/templates/multipart/multipart.html: -------------------------------------------------------------------------------- 1 | multipart html <%= @name %> -------------------------------------------------------------------------------- /test/mailer/templates/multipart/multipart.txt: -------------------------------------------------------------------------------- 1 | multipart plain <%= @name %> -------------------------------------------------------------------------------- /lib/mailer/renderer.ex: -------------------------------------------------------------------------------- 1 | defmodule Mail.Renderer do 2 | def render(template, data) do 3 | {:ok, file} = File.read(template) 4 | 5 | EEx.eval_string(file, assigns: data) 6 | end 7 | end -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # config :mailer, :smtp_client, 4 | # server: "127.0.0.1", 5 | # port: 2525, 6 | # hostname: "mailer" 7 | 8 | # config :mailer, :smtp_server, 9 | # server: "127.0.0.1", 10 | # port: 2525, 11 | # hostname: "mailer", 12 | # handler: Test.Mail.Handler 13 | -------------------------------------------------------------------------------- /config/prod.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # config :mailer, :smtp_client, 4 | # server: "127.0.0.1", 5 | # port: 2525, 6 | # hostname: "mailer" 7 | 8 | # config :mailer, :smtp_server, 9 | # server: "127.0.0.1", 10 | # port: 2525, 11 | # hostname: "mailer", 12 | # handler: Test.Mail.Handler 13 | 14 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :mailer, :smtp_client, 4 | server: "127.0.0.1", 5 | port: 2525, 6 | hostname: "mailer", 7 | transport: :test 8 | 9 | config :mailer, :smtp_server, 10 | server: "127.0.0.1", 11 | port: 2525, 12 | hostname: "mailer", 13 | handler: Test.Mail.Handler 14 | -------------------------------------------------------------------------------- /test/mailer/renderer_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mailer.Renderer.Test do 2 | use ExUnit.Case 3 | 4 | test "it will render a template" do 5 | 6 | test_file = "#{System.cwd}" <> "/test/mailer/templates/plain/en/plain.txt" 7 | 8 | assert "Hello John Doe" == Mail.Renderer.render(test_file, [name: "John Doe"]) 9 | end 10 | end -------------------------------------------------------------------------------- /lib/mailer/util.ex: -------------------------------------------------------------------------------- 1 | defmodule Mailer.Util do 2 | 3 | def localtime_to_str() do 4 | date = Timex.local 5 | Timex.format!(date, "%a, %d %m %Y %T %z", :strftime) 6 | end 7 | 8 | def get_domain(address) do 9 | addr = case String.split(address, ~r([<>])) do 10 | [_name, addr, ""] -> addr 11 | [addr] -> addr 12 | end 13 | [_, domain] = String.split(addr, "@") 14 | domain 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/support/test_mail_handler.ex: -------------------------------------------------------------------------------- 1 | defmodule Test.Mail.Handler do 2 | 3 | def start() do 4 | Agent.start(fn -> [] end, name: __MODULE__) 5 | end 6 | 7 | def clear do 8 | Agent.update(__MODULE__, fn(_state) -> [] end) 9 | end 10 | 11 | def save(id, raw_mail) do 12 | Agent.update(__MODULE__, fn(state) -> [{id, raw_mail} | state] end) 13 | end 14 | 15 | def get_mails do 16 | Agent.get(__MODULE__, fn(state) -> state end) 17 | end 18 | 19 | end -------------------------------------------------------------------------------- /LICENCE.txt: -------------------------------------------------------------------------------- 1 | Copyright © 2014 Antony Pinchbeck. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /lib/mailer/message_id.ex: -------------------------------------------------------------------------------- 1 | defmodule Mailer.Message.Id do 2 | @variant10 2 3 | @uuid_v4 4 4 | @doc """ 5 | Creates a uuid v4 6 | """ 7 | def create(domain) do 8 | string_uuid = create_uuid() 9 | |> uuid_to_string 10 | 11 | 12 | string_uuid <> "@" <> domain 13 | 14 | end 15 | 16 | def create_uuid do 17 | <> = :crypto.strong_rand_bytes(16) 18 | <> 19 | end 20 | 21 | def uuid_to_string(<>) do 22 | :io_lib.format("~8.16.0b-~4.16.0b-~4.16.0b-~4.16.0b-~12.16.0b", [u0, u1, u2, u3, u4]) 23 | |> to_string 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/support/test_transport.ex: -------------------------------------------------------------------------------- 1 | defmodule Test.Transport do 2 | defstruct from: "", 3 | to: "", 4 | content: "" 5 | 6 | 7 | def start() do 8 | Agent.start_link(fn -> 9 | [] 10 | end, name: __MODULE__) 11 | end 12 | 13 | def clear do 14 | Agent.update(__MODULE__, fn(_state) -> 15 | [] 16 | end) 17 | end 18 | 19 | def send(from, to, composed_email) do 20 | mail_data = %Test.Transport{from: from, to: to, content: composed_email} 21 | 22 | Agent.update(__MODULE__, fn(state) -> 23 | [mail_data | state] 24 | end) 25 | end 26 | 27 | def get_mails do 28 | Agent.get(__MODULE__, fn(state) -> state end) 29 | end 30 | end -------------------------------------------------------------------------------- /lib/mailer/server_cfg.ex: -------------------------------------------------------------------------------- 1 | defmodule Mailer.Server.Cfg do 2 | defstruct server: "127.0.0.1", 3 | port: 1025, 4 | hostname: "mailer" 5 | 6 | def create do 7 | client_cfg = Application.get_env(:mailer, :smtp_server, []) 8 | 9 | List.foldl(client_cfg, %Mailer.Server.Cfg{}, fn ({k, v}, acc) -> 10 | case k do 11 | :server -> 12 | %{acc | server: v} 13 | :port -> 14 | %{acc | port: v} 15 | :hostname -> 16 | %{acc | hostname: v} 17 | _ -> 18 | acc 19 | end 20 | end) 21 | 22 | end 23 | 24 | def to_options(server_cfg) do 25 | [ 26 | relay: server_cfg.server, 27 | port: server_cfg.port, 28 | hostname: server_cfg.hostname 29 | ] 30 | end 31 | end -------------------------------------------------------------------------------- /test/mailer/template_locator_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mailer.Template.Locator.Test do 2 | use ExUnit.Case 3 | 4 | setup_all do 5 | template_location = "#{System.cwd}" <> "/test/mailer/templates" 6 | Application.put_env(:mailer, :templates, template_location) 7 | 8 | :ok 9 | end 10 | 11 | test "will locate a plain text template with a country code" do 12 | templates = Mailer.Template.Locator.locate("plain", "en") 13 | 14 | assert 1 == length(templates) 15 | end 16 | 17 | test "will locate a multipart template without country code" do 18 | templates = Mailer.Template.Locator.locate("multipart", "") 19 | 20 | assert 2 == length(templates) 21 | end 22 | 23 | test "will locate the default template when it is not found in the language directory" do 24 | templates = Mailer.Template.Locator.locate("plain", "fr") 25 | 26 | assert 1 == length(templates) 27 | end 28 | 29 | end 30 | -------------------------------------------------------------------------------- /lib/mailer/smtp_client.ex: -------------------------------------------------------------------------------- 1 | defmodule Mailer.Smtp.Client do 2 | def send(from, to, content, server) do 3 | # remove empty cc and bcc headers which can cause errors in encoding 4 | headers = Enum.reject(elem(content, 2), fn {k, v} -> k in ["Cc", "Bcc"] and v == [] end) 5 | content = put_elem(content, 2, headers) 6 | encoded = :mimemail.encode(content) 7 | 8 | server_cfg = Application.get_env(:mailer, :smtp_client) 9 | 10 | handler = List.foldr(server_cfg, nil, fn({k,v}, acc) -> 11 | case k do 12 | :transport -> 13 | v 14 | _ -> 15 | acc 16 | end 17 | end) 18 | 19 | do_send(handler, from, to, encoded, server) 20 | end 21 | 22 | defp do_send(:smtp, from, to, encoded, server) do 23 | :gen_smtp_client.send_blocking( 24 | {from, 25 | [to], 26 | encoded}, 27 | server 28 | ) 29 | end 30 | 31 | defp do_send(_, from, to, encoded, _server) do 32 | Test.Transport.send(from, to, encoded) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/mailer/template_locator.ex: -------------------------------------------------------------------------------- 1 | defmodule Mailer.Template.Locator do 2 | 3 | def locate(template_name, country_code) do 4 | file_types = [text: ".txt", html: ".html"] 5 | 6 | Enum.reduce(file_types, [], fn({file_type, ext}, acc) -> 7 | x = locate(template_name, country_code, ext) 8 | 9 | case x do 10 | nil -> 11 | acc 12 | x -> 13 | [{file_type, x} | acc] 14 | end 15 | end) 16 | end 17 | 18 | defp locate(template_name, country_code, ext) do 19 | case maybe_locate(template_name, country_code, ext) do 20 | nil -> 21 | maybe_locate(template_name, "", ext) 22 | found -> 23 | found 24 | end 25 | end 26 | 27 | defp maybe_locate(template_name, country_code, ext) do 28 | template_location = Application.get_env(:mailer, :templates) 29 | |> Path.join(template_name) 30 | |> Path.join(country_code) 31 | |> Path.join("#{template_name}#{ext}") 32 | 33 | 34 | case File.exists?(template_location) do 35 | true -> 36 | template_location 37 | _ -> 38 | nil 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for third- 9 | # party users, it should be done in your mix.exs file. 10 | 11 | # Sample configuration: 12 | # 13 | # config :logger, :console, 14 | # level: :info, 15 | # format: "$date $time [$level] $metadata$message\n", 16 | # metadata: [:user_id] 17 | 18 | # It is also possible to import configuration files, relative to this 19 | # directory. For example, you can emulate configuration per environment 20 | # by uncommenting the line below and defining dev.exs, test.exs and such. 21 | # Configuration from the imported file will override the ones defined 22 | # here (which is why it is important to import them last). 23 | # 24 | # import_config "#{Mix.env}.exs" 25 | import_config "#{Mix.env}.exs" -------------------------------------------------------------------------------- /lib/mailer/client_cfg.ex: -------------------------------------------------------------------------------- 1 | defmodule Mailer.Client.Cfg do 2 | defstruct server: "127.0.0.1", 3 | port: 1025, 4 | hostname: "mailer", 5 | transport: :smtp, 6 | username: nil, 7 | password: nil, 8 | tls: :if_available, # always, never, if_available 9 | ssl: :false, # true or flase 10 | auth: :if_available, 11 | retries: 1 12 | 13 | def create do 14 | 15 | client_cfg = Application.get_env(:mailer, :smtp_client, []) 16 | 17 | List.foldl(client_cfg, %Mailer.Client.Cfg{}, fn ({k, v}, acc) -> 18 | case k do 19 | :server -> 20 | %{acc | server: v} 21 | :port -> 22 | %{acc | port: v} 23 | :hostname -> 24 | %{acc | hostname: v} 25 | :transport -> 26 | %{acc | transport: v} 27 | :username -> 28 | %{acc | username: v} 29 | :password -> 30 | %{acc | password: v} 31 | :ssl -> 32 | %{acc | ssl: v} 33 | :tls -> 34 | %{acc | tls: v} 35 | :auth -> 36 | %{acc | auth: v} 37 | :retries -> 38 | %{acc | retries: v} 39 | end 40 | end) 41 | end 42 | 43 | def to_options(client_cfg) do 44 | [ 45 | relay: client_cfg.server, 46 | port: client_cfg.port, 47 | hostname: client_cfg.hostname, 48 | username: client_cfg.username, 49 | password: client_cfg.password, 50 | ssl: client_cfg.ssl, 51 | tls: client_cfg.tls, 52 | auth: client_cfg.auth, 53 | retries: client_cfg.retries 54 | ] 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Mailer.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :mailer, 7 | version: version(), 8 | elixir: "~> 1.3", 9 | elixirc_paths: elixirc_paths(Mix.env), 10 | deps: deps(), 11 | description: description(), 12 | package: package(), 13 | name: "Mailer", 14 | source_url: "https://github.com/antp/mailer" 15 | ] 16 | end 17 | 18 | # Configuration for the OTP application 19 | # 20 | # Type `mix help compile.app` for more information 21 | def application do 22 | [applications: [:logger, :timex, :tzdata]] 23 | end 24 | 25 | def version do 26 | String.strip(File.read!("VERSION")) 27 | end 28 | 29 | # Specifies which paths to compile per environment 30 | defp elixirc_paths(:test), do: ["lib", "test/support"] 31 | defp elixirc_paths(_), do: ["lib"] 32 | 33 | # Dependencies can be Hex packages: 34 | # 35 | # {:mydep, "~> 0.3.0"} 36 | # 37 | # Or git/path repositories: 38 | # 39 | # {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"} 40 | # 41 | # To depend on another app inside the umbrella: 42 | # 43 | # {:myapp, in_umbrella: true} 44 | # 45 | # Type `mix help deps` for more examples and options 46 | defp deps do 47 | [ 48 | {:gen_smtp, "~> 0.11.0"}, 49 | {:timex, "~> 3.1.8"}, 50 | {:ex_doc, "~> 0.11.4", only: :dev}, 51 | {:earmark, ">= 0.2.1", only: :dev} 52 | ] 53 | end 54 | 55 | defp description do 56 | """ 57 | Mailer - A simple email client 58 | """ 59 | end 60 | 61 | defp package do 62 | [ 63 | files: ["lib", "priv", "mix.exs", "README.md", "LICENCE.txt", "VERSION"], 64 | maintainers: ["Antony Pinchbeck", "Yurii Rashkovskii", "Paul Scarrone", "sldab", "mogadget", "Miguel Martins", "Mike Janger", "Maxim Chernyak", "Marcelo Gornstein"], 65 | licenses: ["apache 2 license"], 66 | links: %{ 67 | "GitHub" => "https://github.com/antp/mailer", 68 | } 69 | ] 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"certifi": {:hex, :certifi, "1.0.0", "1c787a85b1855ba354f0b8920392c19aa1d06b0ee1362f9141279620a5be2039", [:rebar3], []}, 2 | "combine": {:hex, :combine, "0.9.6", "8d1034a127d4cbf6924c8a5010d3534d958085575fa4d9b878f200d79ac78335", [:mix], []}, 3 | "earmark": {:hex, :earmark, "0.2.1", "ba6d26ceb16106d069b289df66751734802777a3cbb6787026dd800ffeb850f3", [], []}, 4 | "ex_doc": {:hex, :ex_doc, "0.11.5", "0dc51cb84f8312162a2313d6c71573a9afa332333d8a332bb12540861b9834db", [], [{:earmark, "~> 0.1.17 or ~> 0.2", [hex: :earmark, optional: true]}]}, 5 | "gen_smtp": {:hex, :gen_smtp, "0.11.0", "d90ff2f021fc86cb2a4259b1f2b177ab6e506676265e26454bf5755855adc956", [], []}, 6 | "gettext": {:hex, :gettext, "0.13.1", "5e0daf4e7636d771c4c71ad5f3f53ba09a9ae5c250e1ab9c42ba9edccc476263", [:mix], []}, 7 | "hackney": {:hex, :hackney, "1.7.1", "e238c52c5df3c3b16ce613d3a51c7220a784d734879b1e231c9babd433ac1cb4", [:rebar3], [{:certifi, "1.0.0", [hex: :certifi, optional: false]}, {:idna, "4.0.0", [hex: :idna, optional: false]}, {:metrics, "1.0.1", [hex: :metrics, optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, optional: false]}]}, 8 | "idna": {:hex, :idna, "4.0.0", "10aaa9f79d0b12cf0def53038547855b91144f1bfcc0ec73494f38bb7b9c4961", [:rebar3], []}, 9 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [], []}, 10 | "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [], []}, 11 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [], []}, 12 | "timex": {:hex, :timex, "3.1.13", "48b33162e3ec33e9a08fb5f98e3f3c19c3e328dded3156096c1969b77d33eef0", [:mix], [{:combine, "~> 0.7", [hex: :combine, optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, optional: false]}]}, 13 | "tzdata": {:hex, :tzdata, "0.5.10", "087e8dfe8c0283473115ad8ca6974b898ecb55ca5c725427a142a79593391e90", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, optional: false]}]}} 14 | -------------------------------------------------------------------------------- /test/mailer/mailer_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mailer.Client.Test do 2 | use ExUnit.Case 3 | 4 | alias Mailer.Email.Plain 5 | alias Mailer.Email.Multipart 6 | alias Mailer.Util 7 | 8 | setup_all do 9 | Test.Transport.start 10 | 11 | :ok 12 | end 13 | 14 | test "It will send plain text email" do 15 | 16 | date = Util.localtime_to_str 17 | email = Plain.create 18 | 19 | email = Plain.add_from(email, "from@example.com") 20 | email = Plain.add_to(email, "to@example.com") 21 | email = Plain.add_subject(email, "Plain") 22 | email = Plain.add_message_id(email, "123@example.com") 23 | email = Plain.add_date(email, date) 24 | email = Plain.add_body(email, "body") 25 | 26 | composed_email = Plain.compose(email) 27 | 28 | server = Mailer.Client.Cfg.create 29 | server = Mailer.Client.Cfg.to_options(server) 30 | 31 | Test.Transport.clear 32 | Mailer.Smtp.Client.send(email.from, email.to, composed_email, server) 33 | 34 | [sent_email] = Test.Transport.get_mails 35 | 36 | assert email.from == sent_email.from 37 | assert email.to == sent_email.to 38 | assert true == String.contains?(sent_email.content, "Plain") 39 | end 40 | 41 | test "It will send multipart email" do 42 | 43 | date = Util.localtime_to_str 44 | email = Multipart.create 45 | 46 | email = Multipart.add_from(email, "from@example.com") 47 | email = Multipart.add_to(email, "to@example.com") 48 | email = Multipart.add_subject(email, "Multipart") 49 | email = Multipart.add_message_id(email, "123@example.com") 50 | email = Multipart.add_date(email, date) 51 | email = Multipart.add_text_body(email, "text body") 52 | email = Multipart.add_html_body(email, "Html body") 53 | 54 | composed_email = Multipart.compose(email) 55 | 56 | server = Mailer.Client.Cfg.create 57 | server = Mailer.Client.Cfg.to_options(server) 58 | 59 | Test.Transport.clear 60 | Mailer.Smtp.Client.send(email.from, email.to, composed_email, server) 61 | 62 | [sent_email] = Test.Transport.get_mails 63 | 64 | assert email.from == sent_email.from 65 | assert email.to == sent_email.to 66 | assert true == String.contains?(sent_email.content, "text body") 67 | assert true == String.contains?(sent_email.content, "Html body") 68 | end 69 | 70 | end 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /lib/mailer/email_plain.ex: -------------------------------------------------------------------------------- 1 | defmodule Mailer.Email.Plain do 2 | defstruct type: :plain, 3 | from: "", 4 | domain: "", 5 | to: [], 6 | cc: [], 7 | bcc: [], 8 | subject: "", 9 | headers: [], 10 | message_id: "", 11 | date: "", 12 | body: "" 13 | 14 | def create do 15 | %Mailer.Email.Plain{} 16 | end 17 | 18 | def add_from(email, from) do 19 | domain = Mailer.Util.get_domain(from) 20 | email = %{email | from: from} 21 | %{email | domain: domain} 22 | end 23 | 24 | def add_to(email, to) do 25 | %{email | to: [to | email.to]} 26 | end 27 | 28 | def add_cc(email, cc) do 29 | %{email | cc: [cc | email.cc]} 30 | end 31 | 32 | def add_bcc(email, bcc) do 33 | %{email | bcc: [bcc | email.bcc]} 34 | end 35 | 36 | def add_subject(email, subject) do 37 | %{email | subject: subject} 38 | end 39 | 40 | def add_message_id(email, message_id) do 41 | %{email | message_id: message_id} 42 | end 43 | 44 | def add_date(email, date) do 45 | %{email | date: date} 46 | end 47 | 48 | def add_body(email, body) do 49 | %{email | body: body} 50 | end 51 | 52 | def compose(email) do 53 | { 54 | "text", "plain", 55 | [ 56 | {"From", email.from}, 57 | {"To", email.to}, 58 | {"Cc", email.cc}, 59 | {"Bcc", email.bcc}, 60 | {"Subject", email.subject}, 61 | {"Message-ID", email.message_id}, 62 | {"MIME-Version", "1.0"}, 63 | {"Date", email.date} 64 | ], 65 | [ 66 | {"content-type-params"}, 67 | [ 68 | {"charset", "utf-8"}, 69 | ], 70 | {"Content-Transfer-Encoding", "quoted-printable"}, 71 | {"disposition", "inline"} 72 | ], 73 | email.body 74 | } 75 | end 76 | 77 | def decompose(msg) do 78 | { 79 | "text", "plain", 80 | [ 81 | {"From", from}, 82 | {"To", to}, 83 | {"Cc", cc}, 84 | {"Bcc", bcc}, 85 | {"Subject", subject}, 86 | {"Message-ID", msg_id}, 87 | {"MIME-Version", "1.0"}, 88 | {"Date", date} 89 | ], 90 | _, 91 | body 92 | } = msg 93 | 94 | domain = Mailer.Util.get_domain(msg_id) 95 | 96 | to = maybe_list(to) 97 | 98 | %Mailer.Email.Plain{from: from, 99 | to: to, 100 | cc: cc, 101 | bcc: bcc, 102 | subject: subject, 103 | domain: domain, 104 | message_id: msg_id, 105 | date: date, 106 | body: body 107 | } 108 | end 109 | 110 | defp maybe_list(to) when is_list(to) do 111 | to 112 | end 113 | 114 | defp maybe_list(to) do 115 | [to] 116 | end 117 | 118 | end 119 | -------------------------------------------------------------------------------- /lib/mailer/smtp_server.ex: -------------------------------------------------------------------------------- 1 | defmodule Mailer.Smtp.Server do 2 | @behaviour :gen_smtp_server_session 3 | require Logger 4 | 5 | def start_link(server) do 6 | :gen_smtp_server.start_link(__MODULE__, [server]) 7 | end 8 | 9 | def init(hostname, session_count, ip_address, options) do 10 | do_init(hostname, session_count, ip_address, options) 11 | end 12 | 13 | defp do_init(hostname, session_count, _ip_address, _options) when session_count > 20 do 14 | {:stop, :normal, ["421 #{hostname} is too busy to accept mail right now"]} 15 | end 16 | 17 | defp do_init(hostname, _session_count, _ip_address, _options) do 18 | {:ok, hostname, []} 19 | end 20 | 21 | def handle_HELO(_hostname, state) do 22 | {:ok, 655360, state} 23 | end 24 | 25 | @doc """ 26 | Make extensions available 27 | """ 28 | def handle_EHLO(_hostname, extensions, state) do 29 | # extensions = extensions ++ [{"AUTH", "PLAIN LOGIN CRAM-MD5"}, {"STARTTLS", true}] 30 | {:ok, extensions, state} 31 | end 32 | 33 | @doc """ 34 | Check the from address 35 | """ 36 | def handle_MAIL(_from, state) do 37 | {:ok, state} 38 | end 39 | 40 | def handle_MAIL_extension(_extension, state) do 41 | {:ok, state} 42 | end 43 | 44 | @doc """ 45 | Check the recipient address 46 | """ 47 | def handle_RCPT(_to, state) do 48 | {:ok, state} 49 | end 50 | 51 | def handle_RCPT_extension(_extension, state) do 52 | {:ok, state} 53 | end 54 | 55 | @doc """ 56 | If we return ok, we've accepted responsibility for the email 57 | """ 58 | def handle_DATA(_from, _to, data, state) do 59 | try do 60 | msg = :mimemail.decode(data) 61 | queued_as = Mailer.Message.Id.create_uuid 62 | |> Mailer.Message.Id.uuid_to_string 63 | 64 | server_cfg = Application.get_env(:mailer, :smtp_server) 65 | 66 | handler = List.foldr(server_cfg, nil, fn({k,v}, acc) -> 67 | case k do 68 | :handler -> 69 | v 70 | _ -> 71 | acc 72 | end 73 | end) 74 | 75 | maybe_save_msg(handler, queued_as, msg) 76 | 77 | {:ok, queued_as, state} 78 | catch 79 | e -> 80 | Logger.error("SMTP: Data error #{inspect e}") 81 | {:error, "501 Unable to decode the message", state} 82 | end 83 | end 84 | 85 | def maybe_save_msg(nil, _queued_as, _msg) do 86 | Logger.error("No SMTP handler set in configuration file.") 87 | end 88 | 89 | def maybe_save_msg(handler, queued_as, msg) do 90 | handler.save(queued_as, msg) 91 | end 92 | 93 | @doc """ 94 | Reset the internal state of the connection 95 | """ 96 | def handle_RSET(state) do 97 | state 98 | end 99 | 100 | @doc """ 101 | Disable verification of email addresses 102 | """ 103 | def handle_VRFY(_address, state) do 104 | {:error, "252 VRFY disabled by policy, just send some mail", state} 105 | end 106 | 107 | @doc """ 108 | Only called if you add AUTH to the ESMTP extensions 109 | """ 110 | def handle_AUTH(_type, _username, _password, state) do 111 | {:ok, state} 112 | end 113 | 114 | @doc """ 115 | Only called if you add STARTTLS to the ESMTP extensions 116 | """ 117 | def handle_STARTTLS(state) do 118 | state 119 | end 120 | 121 | def terminate(reason, state) do 122 | {:ok, reason, state} 123 | end 124 | 125 | def code_change(_oldVsn, state, _extra) do 126 | {:ok, state} 127 | end 128 | 129 | def handle_other(_verb, _args, state) do 130 | {["500 Error: Command not recognised"], state} 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /test/mailer/email_plain_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mailer.Email.Plain.Test do 2 | use ExUnit.Case 3 | 4 | alias Mailer.Email.Plain 5 | alias Mailer.Util 6 | import Enum, only: [sort: 1] 7 | 8 | test "can set the from and domain fields" do 9 | email = Plain.create 10 | 11 | email = Plain.add_from(email, "test@example.com") 12 | 13 | assert "test@example.com" == email.from 14 | assert "example.com" == email.domain 15 | end 16 | 17 | test "can set to field" do 18 | email = Plain.create 19 | 20 | email = Plain.add_to(email, "one@example.com") 21 | 22 | assert ["one@example.com"] == email.to 23 | end 24 | 25 | test "can set cc field" do 26 | email = Plain.create 27 | |> Plain.add_cc("mujju@example.com") 28 | |> Plain.add_cc("zainu@example.com") 29 | 30 | assert sort(["mujju@example.com", "zainu@example.com"]) == sort(email.cc) 31 | end 32 | 33 | test "can set bcc field" do 34 | email = Plain.create 35 | |> Plain.add_bcc("mujju@example.com") 36 | |> Plain.add_bcc("zainu@example.com") 37 | 38 | assert sort(["mujju@example.com", "zainu@example.com"]) == sort(email.bcc) 39 | end 40 | 41 | test "can set the subject" do 42 | email = Plain.create 43 | 44 | email = Plain.add_subject(email, "welcome") 45 | 46 | assert "welcome" == email.subject 47 | end 48 | 49 | test "can set the body" do 50 | email = Plain.create 51 | 52 | email = Plain.add_body(email, "body") 53 | 54 | assert "body" == email.body 55 | end 56 | 57 | test "can set the message id" do 58 | email = Plain.create 59 | 60 | email = Plain.add_message_id(email, "123") 61 | 62 | assert "123" == email.message_id 63 | end 64 | 65 | test "can set the message date" do 66 | date = "today" 67 | email = Plain.create 68 | 69 | email = Plain.add_date(email, date) 70 | 71 | assert date == email.date 72 | end 73 | 74 | 75 | test "will compose the email" do 76 | date = Util.localtime_to_str 77 | email = Plain.create 78 | 79 | email = Plain.add_from(email, "from@example.com") 80 | email = Plain.add_to(email, "to@example.com") 81 | email = Plain.add_subject(email, "welcome") 82 | email = Plain.add_message_id(email, "123@example.com") 83 | email = Plain.add_date(email, date) 84 | email = Plain.add_body(email, "body") 85 | 86 | composed = { 87 | "text", "plain", 88 | [ 89 | {"From", "from@example.com"}, 90 | {"To", ["to@example.com"]}, 91 | {"Cc", []}, 92 | {"Bcc", []}, 93 | {"Subject", "welcome"}, 94 | {"Message-ID", "123@example.com"}, 95 | {"MIME-Version", "1.0"}, 96 | {"Date", date} 97 | ], 98 | [ 99 | {"content-type-params"}, 100 | [ 101 | {"charset", "utf-8"} 102 | ], 103 | {"Content-Transfer-Encoding", "quoted-printable"}, 104 | {"disposition", "inline"} 105 | ], 106 | "body" 107 | } 108 | 109 | assert composed == Plain.compose(email) 110 | end 111 | 112 | test "will decompose the email" do 113 | date = Util.localtime_to_str 114 | email_src = Plain.create 115 | 116 | email_src = Plain.add_from(email_src, "from@example.com") 117 | email_src = Plain.add_to(email_src, "to@example.com") 118 | email_src = Plain.add_subject(email_src, "welcome") 119 | email_src = Plain.add_message_id(email_src, "123@example.com") 120 | email_src = Plain.add_date(email_src, date) 121 | email_src = Plain.add_body(email_src, "body") 122 | 123 | composed = Plain.compose(email_src) 124 | 125 | email_dst = Plain.decompose(composed) 126 | 127 | assert email_src == email_dst 128 | end 129 | end 130 | -------------------------------------------------------------------------------- /test/app_test.exs: -------------------------------------------------------------------------------- 1 | defmodule App.Test do 2 | use ExUnit.Case 3 | 4 | setup_all do 5 | Test.Transport.start 6 | 7 | template_location = "#{System.cwd}" <> "/test/mailer/templates" 8 | Application.put_env(:mailer, :templates, template_location) 9 | :ok 10 | end 11 | 12 | test "It will send plain text email" do 13 | 14 | data = [name: "John Doe"] 15 | composed_email = Mailer.compose_email("from@example.com", "to@example.com", "Plain", "plain", data) 16 | 17 | Test.Transport.clear 18 | Mailer.send(composed_email) 19 | 20 | [sent_email] = Test.Transport.get_mails 21 | 22 | assert true == String.contains?(sent_email.content, "John Doe") 23 | end 24 | 25 | test "It will send multipart email" do 26 | 27 | data = [name: "John Doe"] 28 | composed_email = Mailer.compose_email("from@example.com", "to@example.com", "Multipart", "multipart", data) 29 | 30 | Test.Transport.clear 31 | Mailer.send(composed_email) 32 | 33 | [sent_email] = Test.Transport.get_mails 34 | 35 | assert true == String.contains?(sent_email.content, "text/plain") 36 | assert true == String.contains?(sent_email.content, "text/html") 37 | end 38 | 39 | test "It will compose the same email with compose_email/1 and compose_email/6" do 40 | 41 | data = [name: "John Doe"] 42 | 43 | email1 = Mailer.compose_email("from@example.com", "to@example.com", "Multipart", "multipart", data) 44 | |> Map.delete(:message_id) 45 | 46 | email2 = Mailer.compose_email(from: "from@example.com", 47 | to: "to@example.com", 48 | subject: "Multipart", 49 | template: "multipart", 50 | data: data) 51 | |> Map.delete(:message_id) 52 | 53 | assert email1 == email2 54 | end 55 | 56 | test "It will take common_mail_params for compose_email/1" do 57 | 58 | data = [name: "John Doe"] 59 | 60 | Application.put_env(:mailer, :common_mail_params, [from: "from@example.com", 61 | subject: "Multipart", 62 | template: "multipart"]) 63 | 64 | email1 = Mailer.compose_email("from@example.com", "to@example.com", "Multipart", "multipart", data) 65 | |> Map.delete(:message_id) 66 | 67 | email2 = Mailer.compose_email(to: "to@example.com", data: data) 68 | |> Map.delete(:message_id) 69 | 70 | assert email1 == email2 71 | end 72 | 73 | end 74 | 75 | 76 | 77 | # defmodule App.Test do 78 | # use ExUnit.Case 79 | # 80 | # alias Mailer.Email.Plain 81 | # alias Mailer.Email.Multipart 82 | # 83 | # setup_all do 84 | # server = Mailer.Server.Cfg.create 85 | # server = Mailer.Server.Cfg.to_options(server) 86 | # 87 | # Mailer.Smtp.Server.start_link(server) 88 | # Test.Mail.Handler.start 89 | # 90 | # template_location = "#{System.cwd}" <> "/test/mailer/templates" 91 | # Application.put_env(:mailer, :templates, template_location) 92 | # 93 | # :ok 94 | # end 95 | # 96 | # test "it will send plain text email" do 97 | # data = [name: "John Doe"] 98 | # 99 | # email = Mailer.compose_email("from@example.com", "to@example.com", "Plain", "plain", data) 100 | # 101 | # Mailer.send(email) 102 | # 103 | # [{_id, raw_email}] = Test.Mail.Handler.get_mails 104 | # 105 | # rxd_email = Plain.decompose(raw_email) 106 | # 107 | # assert "Hello John Doe" == rxd_email.body 108 | # end 109 | # 110 | # test "it will send multipart email" do 111 | # data = [name: "John Doe"] 112 | # 113 | # email = Mailer.compose_email("from@example.com", "to@example.com", "Multipart", "multipart", data) 114 | # 115 | # Mailer.send(email) 116 | # 117 | # [{_id, raw_email}] = Test.Mail.Handler.get_mails 118 | # 119 | # rxd_email = Multipart.decompose(raw_email) 120 | # 121 | # assert "multipart plain John Doe" == rxd_email.plain_text_body 122 | # assert "multipart html John Doe" == rxd_email.html_text_body 123 | # end 124 | # 125 | # end 126 | -------------------------------------------------------------------------------- /lib/mailer/email_multipart.ex: -------------------------------------------------------------------------------- 1 | defmodule Mailer.Email.Multipart do 2 | defstruct type: :multipart, 3 | from: "", 4 | domain: "", 5 | to: [], 6 | cc: [], 7 | bcc: [], 8 | subject: "", 9 | headers: [], 10 | message_id: "", 11 | date: "", 12 | plain_text_body: "", 13 | html_text_body: "" 14 | 15 | def create do 16 | %Mailer.Email.Multipart{} 17 | end 18 | 19 | def add_from(email, from) do 20 | domain = Mailer.Util.get_domain(from) 21 | email = %{email | from: from} 22 | %{email | domain: domain} 23 | end 24 | 25 | def add_to(email, to) do 26 | %{email | to: [to | email.to]} 27 | end 28 | 29 | def add_cc(email, cc) do 30 | %{email | cc: [cc | email.cc]} 31 | end 32 | 33 | def add_bcc(email, bcc) do 34 | %{email | bcc: [bcc | email.bcc]} 35 | end 36 | 37 | def add_subject(email, subject) do 38 | %{email | subject: subject} 39 | end 40 | 41 | def add_message_id(email, message_id) do 42 | %{email | message_id: message_id} 43 | end 44 | 45 | def add_date(email, date) do 46 | %{email | date: date} 47 | end 48 | 49 | def add_text_body(email, body) do 50 | %{email | plain_text_body: body} 51 | end 52 | 53 | def add_html_body(email, body) do 54 | %{email | html_text_body: body} 55 | end 56 | 57 | def compose(email) do 58 | { 59 | "multipart", "alternative", 60 | [ 61 | {"From", email.from}, 62 | {"To", email.to}, 63 | {"Cc", email.cc}, 64 | {"Bcc", email.bcc}, 65 | {"Subject", email.subject}, 66 | {"Message-ID", email.message_id}, 67 | {"MIME-Version", "1.0"}, 68 | {"Date", email.date}, 69 | ], 70 | [], 71 | [ 72 | {"text","plain", 73 | [], 74 | [ 75 | {"content-type-params", 76 | [ 77 | {"charset", "utf8"}, 78 | {"format", "flowed"} 79 | ] 80 | }, 81 | {"disposition", "inline"}, 82 | {"disposition-params", []} 83 | ], 84 | email.plain_text_body 85 | }, 86 | {"text", "html", 87 | [], 88 | [ 89 | {"content-type-params", 90 | [ 91 | {"charset", "utf8"} 92 | ] 93 | }, 94 | {"disposition", "inline"}, 95 | {"disposition-params",[]} 96 | ], 97 | email.html_text_body 98 | } 99 | ] 100 | } 101 | end 102 | 103 | def decompose(msg) do 104 | {"multipart", "alternative", 105 | headers, 106 | _, 107 | [ 108 | {"text","plain", 109 | _, 110 | _, 111 | plain_text_body 112 | }, 113 | {"text", "html", 114 | _, 115 | _, 116 | html_text_body 117 | } 118 | ] 119 | } = msg 120 | 121 | 122 | email = %Mailer.Email.Multipart{} 123 | 124 | email = List.foldl(headers, email, fn(item, email) -> 125 | process_header(item, email) 126 | end) 127 | 128 | email = %{email | plain_text_body: plain_text_body} 129 | %{email | html_text_body: html_text_body} 130 | end 131 | 132 | defp process_header({"From", from}, email) do 133 | %{email | from: from} 134 | end 135 | 136 | defp process_header({"Cc", cc}, email) do 137 | %{email | cc: cc} 138 | end 139 | 140 | defp process_header({"Bcc", bcc}, email) do 141 | %{email | bcc: bcc} 142 | end 143 | 144 | defp process_header({"To", to}, email) do 145 | %{email | to: to} 146 | end 147 | 148 | defp process_header({"Subject", subject}, email) do 149 | %{email | subject: subject} 150 | end 151 | 152 | defp process_header({"Message-ID", msg_id}, email) do 153 | domain = Mailer.Util.get_domain(msg_id) 154 | 155 | email = %{email | message_id: msg_id} 156 | %{email | domain: domain} 157 | end 158 | 159 | defp process_header({"Date", date}, email) do 160 | %{email | date: date} 161 | end 162 | 163 | defp process_header(_, email) do 164 | email 165 | end 166 | end 167 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mailer 2 | 3 | A simple SMTP mailer. 4 | 5 | This can be used to send one off emails such welcome emails after signing up 6 | to a service or password recovery emails. 7 | 8 | Mailer is built upon ```gen_smtp``` and uses it to deliver emails. 9 | 10 | ## What it is not 11 | 12 | A mass mailer, just don't do it. 13 | 14 | 15 | ## Dependencies 16 | Add the following to your applications dependency block in ```mix.exs```. 17 | 18 | ```elixir 19 | {:mailer, github: "antp/mailer"} 20 | ``` 21 | 22 | Then run ```mix deps.get```. 23 | 24 | Mailer uses ```gen_smtp``` to provide the mailing infrastructure. 25 | 26 | ## Usage 27 | 28 | First compose an email with: 29 | 30 | ```elixir 31 | email = Mailer.compose_email("from@example.com", "to@example.com", "Subject", "welcome_template", template_data) 32 | ``` 33 | 34 | Then send the email with: 35 | 36 | ```elixir 37 | response = Mailer.send(email) 38 | ``` 39 | 40 | The response can be checked for failed deliveries. 41 | 42 | Successful deliveries will have OK in the response such as: 43 | 44 | ``` 45 | "2.0.0 Ok: queued as 955CBC01C2\r\n" 46 | ``` 47 | 48 | Failed deliveries will have a response similar to: 49 | 50 | ``` 51 | {:error, :retries_exceeded, 52 | {:network_failure, "xxx.xxx.xxx.xxx", {:error, :ehostunreach}}} 53 | ``` 54 | 55 | ### Configuration 56 | In your applications ```config.exs``` file you need to add two sections. 57 | 58 | #### Template configuration 59 | Add a section to detail the location of the templates 60 | 61 | ```elixir 62 | config :mailer, 63 | templates: "priv/templates" 64 | ``` 65 | 66 | The mailer will look for all templates under this path. If you pass 'welcome' as the template name, mailer will look in ```priv/templates/welcome``` to locate the template file. 67 | 68 | The path is relative to the directory that the application is run from. For a normal application setting ```priv/templates``` is correct. If you're application is part of an umbrella application then you will need to set it to the path within the ```apps``` directory such as: 69 | 70 | ```elixir 71 | config :mailer, 72 | templates: "apps/site_mailer/priv/templates" 73 | ``` 74 | if you run your application from the main umbrella directory. 75 | 76 | #### SMTP client configuration 77 | As mailer uses ```gen_smtp``` it requires a server to relay mails through. 78 | 79 | The smtp configuration is passed through to ```gen_smtp```, so all options that ```gen_smtp``` supports are available. 80 | 81 | Option: | Values: 82 | ------------- | ------------- 83 | server | Address of the email server to relay through. 84 | hostname | Hostname of your mail client 85 | transport | :smtp -> deliver mail using smtp (default)
:test -> deliver mail to a test server 86 | username | username to use in authentication 87 | password | password for the username 88 | tls | :always -> always use TLS
:never -> never use TLS
:if_available -> use TLS if available (default) 89 | ssl | :true -> use SSL
:false -> do not use SSL (default) 90 | auth | :if_available -> use authentication if available (default) 91 | retries | Number of retries before a send failure is reported
defaults to 1 92 | 93 | 94 | ## Plain text or Multipart email 95 | Mailer will automatically send multipart emails if you have both a ```.txt``` and ```.html``` in the template directory. The ```.html``` template is optional. 96 | 97 | ### To send a welcome email: 98 | Sending plain text only: 99 | 100 | ```elixir 101 | priv/templates/welcome/welcome.txt 102 | ``` 103 | 104 | Sending a multipart email: 105 | 106 | ```elixir 107 | priv/templates/welcome/welcome.txt 108 | priv/templates/welcome/welcome.html 109 | ``` 110 | 111 | ## Internationalisation 112 | When sending a mail it is possible to add a country code. When the mail is composed this will be added to the template path to further qualify the template lookup. 113 | 114 | If for example to wanted to support both English and French the template directory structure would look like the following: 115 | 116 | ```elixir 117 | priv/templates/welcome/en/welcome.txt 118 | priv/templates/welcome/en/welcome.html 119 | priv/templates/welcome/fr/welcome.txt 120 | priv/templates/welcome/fr/welcome.html 121 | ``` 122 | By including the country code in the compose call, Mailer will render the correct localised template. 123 | 124 | ```elixir 125 | Mailer.compose_email("from@example.com", "to@example.com", "Subject", "welcome", data, "en") 126 | ``` 127 | if the template files are not found in the language directory Mailer will look for a default template to send in the parent directory. 128 | 129 | ```elixir 130 | priv/templates/welcome <- default location of templates 131 | priv/templates/welcome/ <- internationalised templates 132 | ``` 133 | 134 | 135 | 136 | # Author 137 | 138 | Copyright © 2014 Component X Software, Antony Pinchbeck 139 | 140 | Released under Apache 2 License 141 | -------------------------------------------------------------------------------- /test/mailer/email_multipart_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mailer.Email.Multipart.Test do 2 | use ExUnit.Case 3 | 4 | alias Mailer.Email.Multipart, as: Email 5 | alias Mailer.Util 6 | import Enum, only: [sort: 1] 7 | 8 | test "can set the from and domain fields" do 9 | email = Email.create 10 | 11 | email = Email.add_from(email, "test@example.com") 12 | 13 | assert "test@example.com" == email.from 14 | assert "example.com" == email.domain 15 | end 16 | 17 | test "can set to field" do 18 | email = Email.create 19 | 20 | email = Email.add_to(email, "one@example.com") 21 | 22 | assert ["one@example.com"] == email.to 23 | end 24 | 25 | test "can set cc field" do 26 | email = Email.create 27 | |> Email.add_cc("mujju@example.com") 28 | |> Email.add_cc("zainu@example.com") 29 | 30 | assert sort(["mujju@example.com", "zainu@example.com"]) == sort(email.cc) 31 | end 32 | 33 | test "can set bcc field" do 34 | email = Email.create 35 | |> Email.add_bcc("mujju@example.com") 36 | |> Email.add_bcc("zainu@example.com") 37 | 38 | assert sort(["mujju@example.com", "zainu@example.com"]) == sort(email.bcc) 39 | end 40 | 41 | test "can set the subject" do 42 | email = Email.create 43 | 44 | email = Email.add_subject(email, "welcome") 45 | 46 | assert "welcome" == email.subject 47 | end 48 | 49 | test "can set the message id" do 50 | email = Email.create 51 | 52 | email = Email.add_message_id(email, "123") 53 | 54 | assert "123" == email.message_id 55 | end 56 | 57 | test "can set the message date" do 58 | date = "today" 59 | email = Email.create 60 | 61 | email = Email.add_date(email, date) 62 | 63 | assert date == email.date 64 | end 65 | 66 | test "can set the plain text body" do 67 | email = Email.create 68 | 69 | email = Email.add_text_body(email, "text body") 70 | 71 | assert "text body" == email.plain_text_body 72 | end 73 | 74 | test "can set the html text body" do 75 | email = Email.create 76 | 77 | email = Email.add_html_body(email, "html body") 78 | 79 | assert "html body" == email.html_text_body 80 | end 81 | 82 | test "will compose the email" do 83 | date = Util.localtime_to_str 84 | 85 | email = Email.create 86 | 87 | email = Email.add_from(email, "from@example.com") 88 | email = Email.add_to(email, "to@example.com") 89 | email = Email.add_cc(email, "cc@example.com") 90 | email = Email.add_bcc(email, "bcc@example.com") 91 | email = Email.add_subject(email, "welcome") 92 | email = Email.add_message_id(email, "123@example.com") 93 | email = Email.add_date(email, date) 94 | email = Email.add_text_body(email, "text body") 95 | email = Email.add_html_body(email, "html body") 96 | 97 | 98 | composed = {"multipart", "alternative", 99 | [ 100 | {"From", "from@example.com"}, 101 | {"To", ["to@example.com"]}, 102 | {"Cc", ["cc@example.com"]}, 103 | {"Bcc", ["bcc@example.com"]}, 104 | {"Subject", "welcome"}, 105 | {"Message-ID", "123@example.com"}, 106 | {"MIME-Version", "1.0"}, 107 | {"Date", date} 108 | ], 109 | [], 110 | [ 111 | {"text", "plain", 112 | [], 113 | [ 114 | {"content-type-params", 115 | [ 116 | {"charset", "utf8"}, 117 | {"format", "flowed"} 118 | ] 119 | }, 120 | {"disposition", "inline"}, 121 | {"disposition-params", []} 122 | ], 123 | "text body" 124 | }, 125 | {"text", "html", 126 | [], 127 | [ 128 | {"content-type-params", 129 | [ 130 | {"charset", "utf8"} 131 | ] 132 | }, 133 | {"disposition", "inline"}, 134 | {"disposition-params", []} 135 | ], 136 | "html body" 137 | } 138 | ] 139 | } 140 | 141 | assert composed == Email.compose(email) 142 | end 143 | 144 | test "will decompose the email" do 145 | date = Util.localtime_to_str 146 | email_src = Email.create 147 | 148 | email_src = Email.add_from(email_src, "from@example.com") 149 | email_src = Email.add_to(email_src, "to@example.com") 150 | email_src = Email.add_subject(email_src, "welcome") 151 | email_src = Email.add_message_id(email_src, "123@example.com") 152 | email_src = Email.add_date(email_src, date) 153 | email_src = Email.add_text_body(email_src, "text body") 154 | email_src = Email.add_html_body(email_src, "html body") 155 | 156 | composed = Email.compose(email_src) 157 | email_dst = Email.decompose(composed) 158 | 159 | assert email_src == email_dst 160 | end 161 | end 162 | -------------------------------------------------------------------------------- /lib/mailer.ex: -------------------------------------------------------------------------------- 1 | defmodule Mailer do 2 | alias Mailer.Email.Plain 3 | alias Mailer.Email.Multipart 4 | alias Mailer.Util 5 | 6 | @moduledoc """ 7 | A simple SMTP mailer. 8 | 9 | This can be used to send one off emails such welcome emails after signing up 10 | to a service or password recovery emails. 11 | 12 | # What it is not 13 | 14 | A mass mailer, just don't do it. 15 | 16 | # Example usage 17 | 18 | First compose an email with 19 | 20 | email = Mailer.compose_email(from: "from@example.com", 21 | to: "to@example.com", 22 | subject: "Subject", 23 | template: "welcome_template", 24 | data: template_data) 25 | 26 | Then send the email with 27 | 28 | response = Mailer.send(email) 29 | 30 | The response can be checked for failed deliveries. 31 | """ 32 | 33 | # @doc """ 34 | # Start the mailer application 35 | # """ 36 | # def start(_type, _args) do 37 | # import Supervisor.Spec, warn: false 38 | 39 | # children = [ 40 | # # Define workers and child supervisors to be supervised 41 | # # worker(Mailer.Smtp.Server, [[port: 1025, address: {127,0,0,1}, domain: "test.example.com"]]) 42 | # ] 43 | 44 | # # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html 45 | # # for other strategies and supported options 46 | # opts = [strategy: :one_for_one, name: Mailer.Supervisor] 47 | # Supervisor.start_link(children, opts) 48 | # end 49 | 50 | @doc """ 51 | Send a composed, email to the recipient 52 | """ 53 | def send(%{type: :plain} = email) do 54 | composed_email = Plain.compose(email) 55 | 56 | server = Mailer.Client.Cfg.create 57 | server = Mailer.Client.Cfg.to_options(server) 58 | 59 | Mailer.Smtp.Client.send(email.from, email.to, composed_email, server) 60 | end 61 | 62 | @doc """ 63 | Send a composed, email to the recipient 64 | """ 65 | def send(%{type: :multipart} = email) do 66 | composed_email = Multipart.compose(email) 67 | 68 | server = Mailer.Client.Cfg.create 69 | server = Mailer.Client.Cfg.to_options(server) 70 | 71 | Mailer.Smtp.Client.send(email.from, email.to, composed_email, server) 72 | end 73 | 74 | @doc """ 75 | Compose an email to a recipient. 76 | 77 | The keyword list must contain the following parameters: 78 | `:from`, `:to`, `:subject`, `:template`, `:data`. 79 | 80 | The following parameters are optional: 81 | `:country_code` 82 | 83 | Default parameters can be specified in the application config: 84 | config :mailer, 85 | common_mail_params: [from: "Example "] 86 | 87 | Given the that the templates are located in the location: 88 | 89 | priv/templates 90 | 91 | if the compose_email is called with a template named 'plain' it will look 92 | for the templates 'plain.txt' and 'plain.html' in location 93 | 94 | priv/templates/plain/ 95 | 96 | If the template resolves to a single .txt file 97 | it will send a plain text email. 98 | 99 | If the template resolves to both .html and a .txt file 100 | it will send a multipart email. 101 | 102 | The template location can be internationalised by passing an optional 103 | country code. For example passing a country code of 'en' will modify 104 | the search path to: 105 | 106 | priv/templates/plain/en/ 107 | """ 108 | @spec compose_email(list({atom(), term})) :: Email.Plain.t | Email.Multipart.t 109 | 110 | def compose_email(params) when is_list(params) do 111 | final_params = Application.get_env(:mailer, :common_mail_params, []) 112 | |> Keyword.merge(params) 113 | 114 | compose_email(Keyword.get(final_params, :from), 115 | Keyword.get(final_params, :to), 116 | Keyword.get(final_params, :subject), 117 | Keyword.get(final_params, :template), 118 | Keyword.get(final_params, :data), 119 | Keyword.get(final_params, :country_code, "")) 120 | end 121 | 122 | @doc """ 123 | Compose an email to a recipient. 124 | 125 | Similar to compose_email/1, but with parameters supplied directly. 126 | Ignores default parameters from the application config. 127 | """ 128 | def compose_email(from, to, subject, template, data, country_code \\ "") do 129 | case Mailer.Template.Locator.locate(template, country_code) do 130 | [] -> raise ArgumentError, message: "Template(s) not found: #{template}" 131 | templates -> compose_email_by_type(from, to, subject, templates, data) 132 | end 133 | end 134 | 135 | @doc false 136 | defp compose_email_by_type(from, to, subject, [{:text, plain}], data) do 137 | date = Util.localtime_to_str 138 | 139 | body = Mail.Renderer.render(plain, data) 140 | 141 | Plain.create 142 | |> Plain.add_from(from) 143 | |> Plain.add_to(to) 144 | |> Plain.add_subject(subject) 145 | |> Plain.add_message_id(Mailer.Message.Id.create(from)) 146 | |> Plain.add_date(date) 147 | |> Plain.add_body(body) 148 | 149 | end 150 | 151 | @doc false 152 | defp compose_email_by_type(from, to, subject, [{:html, html}, {:text, plain}], data) do 153 | date = Util.localtime_to_str 154 | 155 | plain_text = Mail.Renderer.render(plain, data) 156 | html_text = Mail.Renderer.render(html, data) 157 | 158 | Multipart.create 159 | |> Multipart.add_from(from) 160 | |> Multipart.add_to(to) 161 | |> Multipart.add_subject(subject) 162 | |> Multipart.add_message_id(Mailer.Message.Id.create(from)) 163 | |> Multipart.add_date(date) 164 | |> Multipart.add_text_body(plain_text) 165 | |> Multipart.add_html_body(html_text) 166 | end 167 | end 168 | --------------------------------------------------------------------------------