Mailman provides a clean way of defining mailers in your Elixir applications
Switch branches/tags
Clone or download
Latest commit 31a4418 Dec 10, 2018

README.md

Mailman 👮

Mailman lets you send email from your Elixir app.

  • Plain text or multi-part email (plain text and HTML)
  • Inline images in HTML part
  • Attachments (with semi-automatic MIME type detection)
  • Easy-peasy SMTP config
  • Rendering via EEx
  • Standard quoted-printable encoding
  • Automatic CC and BCC delivery
  • Custom headers
  • SMTP delivery timestamps

Mailman is a wrapper around the mighty (but rather low-level) gen_smtp, the popular Erlang SMTP library.

A quick example

  defmodule MyApp.Mailer do
    def deliver(email) do
      Mailman.deliver(email, config())
    end

    def config do
      %Mailman.Context{
        config: %Mailman.SmtpConfig{
          relay: "yourtdomain.com",
          username: "userkey-here",
          password: "passkey-here",
          port: 25,
          tls: :always,
          auth: :always,
        },
        composer: %Mailman.EexComposeConfig{}
      }
    end
  end

  # somewhere else:
  def test_email do
    email = %Mailman.Email{
      subject: "Hello Mailman!",
      from: "mailman@elixir.com",
      to: ["testy@tester123456.com"],
      cc: ["testy2#tester1234.com", "abcd@defd.com"],
      bcc: ["1234@wsd.com"],
      data: [
        name: "Yo"
      ],
      text: "Hello! <%= name %> These are Unicode: qżźół",
      html: """
<html>
<body>
 <b>Hello! <%= name %></b> These are Unicode: qżźół
</body>
</html>
      """
    }
     
    MyApp.Mailer.deliver(email)
  end

As you can see, all you need is a %Mailman.Email{} struct containing your email and a %Mailman.Context{} with the configuration data.

Rendering email bodies

You are free to pass any plaintext and HTML strings to the :html and :text fields of the Email struct.

However, for any serious emailing purposes, you will want to use a templating engine to handle the find-and-replace effort of personalized, event-triggered emails, as well as wrap your email bodies in a responsive, battle-tested HTML template (like Cerberus, Pine or Foundation). To that end, what better templating engine than Elixir's very own EEx!

There are multiple ways to use EEx with Mailman:

  1. The basic way: You can use eex strings for the :text and :html values, and pass data in via :data (as in the example);
  2. Slightly more work but much less clutter: You can use template file names for the :text and :html values, and tell Mailman where to find them (see below);
  3. Use Phoenix instead: If you are already using Phoenix, you may prefer using your existing template/ and views/ folders and calling EEx through Phoenix like so:
rendered_html = Phoenix.View.render_to_string(App.YourEmailView, "your_email_template.html", %{foo: "bar"})

Configuring the mailing context

Mailman is configured using a single %Mailman.Context{} struct containing composer and config data.

%Mailman.Context{
  composer: %Mailman.EexComposeConfig{...}
  config:   %Mailman.SmtpConfig{...},
}

Composer config (rendering your emails)

For now, only the %Mailman.EexComposeConfig{} is available for configuring the existing EexComposer (although the library is happy to instead use any other composer module you might want to implement). You can pre-configure the EexComposer with the following options:

%Mailman.EexComposeConfig{
  root_path: "",
  assets_path: "", 
  text_file: false,
  html_file: false,
  text_file_path: "",
  html_file_path: ""
}

If e.g. text_file == true, then Mailman will assume that your emails' :text value wil be the filename of your eex template in the text_file_path directory (instead of a raw, plaintext, email body string).

Adapter config – how to send the rendered email

You can set your context's :config to any of the following three structs:

  • %Mailman.SmtpConfig{} for sending from an external server,
  • %Mailman.LocalSmtpConfig{} for sending on your local machine,
  • %Mailman.TestConfig{} for testing.

Mailman's external, local or testing adapter will handle your email accordingly.

The external config struct takes the following options:

%Mailman.SmtpConfig{
  relay: "yourtdomain.com",
  username: "userkey-here",
  password: "passkey-here",
  port: 25,
  tls: :always,  # or :never
  auth: :always, # or :never
},

The local config struct looks like

%Mailman.LocalSmtpConfig{
  port: 2525 
}

The test config struct looks like

%Mailman.TestConfig{
  store_deliveries: true
}

Note that to be able to use the local and the test configs, you'll need to start either local SMTP server or the testing service, wherever you start other services in your app:

Mailman.LocalServer.start(1234)
# or:
Mailman.TestServer.start

Configuration using Mix.Config

You can pass context configuration to Mailman using Mix.Config. If you don't set a config field value in Mailman.Context{} struct, or if you set it to nil, Mailman expect to read the value from your config.exs file (or a file imported by it).

Here is an example config file snippet for Mailman:

config :mailman,
  relay: "localhost",
  port: 1025,
  auth: :never

You can also explicitely set the adapter. In this case, all the other options will be used when creating the adapter config:

config :mailman,
  adapter: MyApp.MyMailAdapter, # or e.g. Mailman.LocalSmtpAdapter
  port: 1025,
  custom_param: "something"

Defining emails

The email struct is defined as:

defstruct subject: "", 
  from: "", 
  to: [], 
  cc: [], 
  bcc: [], 
  attachments: [], # This has to be %Mailman.Attachment{}. More about attachments below
  data: %{}, # This is the context for EEx. You put here data for your <%= %> placeholders
  html: "", # Actual html template
  text: "", # Actual plain template
  delivery: nil # If the message was created through parsing of the delivered email - this holds the 'Date' header

CC and BCC

To instruct Mailman to actually send copies of your email to the listed CC and BCC recipients, use Mailman.deliver(email, config, :send_cc_and_bcc). This, unfortunately, goes against the behaviour you are probably used to from end-user email apps, but reflects how SMTP servers work.

The :cc/:bcc fields only add corresponding header lines to the rendered email source. They do not, by themselves, magically effect delivery of actual copies to those recipients – they only change what's written on the envelope, so to speak. By default, Mailman will render the email struct and deliver it to the SMTP server only once, with a single set of recipients – those in the :to list. The :send_cc_and_bcc flag is a shortcut that will cause delivery of multiple emails at once. It returns a list of Tasks you can process.

If you need even more fine-grained control over CC/BCC mechanics, you will be best served by the lower-level gen_smtp functions, e.g.

email_tuple = {
  from_address,
  [to_address],
  rendered_message,
}
result = :gen_smtp_client.send_blocking(email_tuple, %Mailman.SmtpConfig{...})

Attachments

Mailman makes it easy to attach files, whether they're on your hard drive or on the internet.

The standard way to create an attachment is to use the attach! function:

Mailman.Attachment.attach!(file_path_or_url, file_name \\ nil, mime_tuple \\ nil)

Use it when creating your email:

attachments: [
  Mailman.Attachment.attach!("test/data/blank.png")
],

file_path_or_url can be an absolute file path, or one relative to the root of your project. You can also give it a URL, in which case Mailman will download the file for you before wrapping it in the Attachment struct.

file_name (optional) allows you to change the attachment's file name in the email.

mime_tuple (optional) allows you to set the MIME type of your file. This is rarely necessary, as Mailman can often infer this information from your file's extension and an included list of common MIME types. However, if that fails, you may specify the MIME type and subtype in a 2-tuple, e.g. {"application", "vnd.openxmlformats-officedocument.wordprocessingml.document"} for a docx file.

Note that the attach! option will throw an exception if it cannot open the file; use the attach function if you want to match on {:ok, attachment}, or {:err, message} instead.

Inline images

Emails can take inline content – typically, this is used for inlined images in the HTML part of the email. To add an inline image, first attach the file using the inline! function (instead of attach! – the arguments are the same). Then reference the image in your HTML body as follows:

<img alt="foobar" src="cid:<%= URI.encode("your_filename.jpg") %>@mailman.attachment" />

The cid: prefix tells the email client that what follows is the Content-ID of an inlined attachment. The @mailman.attachment suffix is a meaningless dummy string (RFC 2392 requires Content IDs to look like email addresses).

Adding extra headers

The deliver function takes an optional third parameter (or fourth, if you are using :send_cc_and_bcc) for that purpose:

Mailman.deliver(your_email_struct, your_config, [{"X-Test-Header", "123"}])

Was the email delivered successfully?

Mailman's deliver function will return {:ok, raw_delivered_message}, which contains this information. You can turn this raw string back into a %Mailman.Email{} struct using Mailman.Email.parse!:

{:ok, message} = MyApp.Mailer.deliver(email_with_attachments)
parsed_email = Mailman.Email.parse!(message)
delivered_date = parsed_email.delivery

At this point, if the deliver function added the Date header (meaning that it was accepted by the SMTP server) — then its value should show up in the delivery field.

Inspecting deliveries when testing

When you use the TestServer you can take a look at the deliveries with:

  Mailman.TestServer.deliveries

Also, if you want to clear this list:

  Mailman.TestServer.clear_deliveries

TODOs

  • Send multiple emails using the same connection gen_smtp PR
  • Unit testing (somewhat in progress)

Contributors