[ANN] Mailman - a proposal of a clean way of defining and sending emails

276 views
Skip to first unread message

Kamil Ciemniewski

unread,
Mar 29, 2014, 8:33:50 AM3/29/14
to elixir-l...@googlegroups.com
Y'all

I've created a working sketch proposal of a library that is meant to be providing means of defining and sending emails from your Elixir applications.


It provides a clean way of defining mailers. It also allows you to send multi-part email messages containing text and html parts. It encodes messages with a proper quoted-printable encoding.

There are two mailing adapters included:
  • ExternalSmtpAdapter
  • TestingAdapter
I'd like with that to start a discussion if it heads in a right direction. Especially I'd like to address the following:
  • Do you think the API would benefit from using just plain Elixir datatypes, instead of relying on macros for providing easy to understand, remember and use API?
  • How do you think the API could be simplified while preserving its elasticity?
You're all welcomed to contribute, critisize, work towards a nice and solid solution we could use in our projects.

Thanks!

José Valim

unread,
Mar 29, 2014, 9:23:32 AM3/29/14
to elixir-l...@googlegroups.com
This looks really nice Kamil. It is definitely a necessary project! :D

> Do you think the API would benefit from using just plain Elixir datatypes, instead of relying on macros for providing easy to understand, remember and use API?

A DSL must always exist on top of a plain API that works with data types. When working on projects that end up having a DSL, I usually explain them in one of the two ways:

1. Start with the plain API and at the end say: "ok, all those patterns can now be written a bit more cleanly like this" and then I show the DSL;

2. Or start with the DSL and then say: all we have been doing so far just translates to this.

At the end of the day, building the DSL on top of an actual API helps keep you honest. If all you have is a DSL, it becomes very easy to just to throw more dirty under the carpet, add hidden functionality, and inject code when you are not supposed to.

(Btw, every time I say DSL in this post, I am talking about internal language DSLs targeted to developers.)

Also, I would like to point out that data structures compose really well. DSLs do not as much. Let's consider an example from the README:

defmodule RealAdapter do
  use Mailman.ExternalSmtpAdapter

  config do
    relay "smtp.gmail.com"
    username "youra...@gmail.com"
    password "Yourpassword"
    port 465
    ssl true
  end
end

What if I actually have two similar SMTP adapters where only the port changes? The DSL does not give me anyway to share this information. I have to duplicate it or define a module with a macro that is going to inject the content that I want. If all it is a data structures, I can put it in any function, call it, merge the new values, and call it a day. That's the main reason Mix only asks for data structures when defining your project configuration.

Also we need to be careful with the pattern of using "use" to introduce API into the target module. Here is a counter-proposal:

defmodule MyAdapter do
  @config [relay: "smtp.gmail.com",
           username: "youra...@gmail.com",
           ...]

  def deliver(envelope) do
    Mailman.ExternalSmtpAdapter.deliver(envelope, @config)
  end
end

It is a tiny bit more verbose but now I know exactly the API my module provides. The focus should not be on what I should "use" to get a behaviour but on what interface I should implement to expose some behaviour.

How does this help keep you honest? If you hide everything under "use", you can inject N functions into the user module. This can potentially lead to adapters with big API surfaces, which leads to more code, corner cases, etc. However, if instead you say, "all an adapter needs to implement is X and Y", you are specifying the contract publicly.

In the OO world, someone would be saying "composition over inheritance".

Let's look at another example:

defmodule ErrorNotifiersEmails do
  use Mailman.Emails, composer: EmailsComposer

  default_from "tes...@elixir.com"

  compose :general_error_notifier, error do
    subject „[Error]  - #{error.name}”
    to [ „devel...@yourapp.com” ]
    data :error, error
  end
end

There is a special DSL for defining e-mails (which is really sweet) but, as every DSL, it presents issues. What if there is a complex logic to define the subject? Or what if a bunch of e-mails have the same headers, how can I share them? I believe moving it to a private function is not going to work.

Also, why do I need to retrieve my e-mails with get/2? I already know how invoke functions passing arguments, why do I need to learn a new construct? What if the functions that compose my e-mails requires 3 arguments?

Of course, I am not saying we should get rid of all DSLs. But it is very important to keep in mind that, as everything else, DSLs impose trade-offs and we need to be quite aware of what those trade-offs are. In fact, I believe DSLs make APIs less elastic, because now I need to abide to the DSL rules.

My $0.02 :)

José Valim
Skype: jv.ptec
Founder and Lead Developer


--
You received this message because you are subscribed to the Google Groups "elixir-lang-talk" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-ta...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

José Valim

unread,
Mar 29, 2014, 10:08:18 AM3/29/14
to elixir-l...@googlegroups.com
Ah, to be clear, I have made all the "mistakes" I mentioned and not long ago! :)

Ecto.Repo injects a bunch of functions into the user module (although it is meant to be a facade, it is still a red flag). Ecto.Entity is much worse though, it injects functions in the user module and we don't define anywhere what it means to be an entity (you are locked in and you can't define your own).

That happens a lot. I assume it is because we often start with the DSL and it may take time to have the complete definition of how the inner APIs should look like.

The adapters in Ecto though are "tidy" and can be used as examples. We have defined the adapter behavior and provide callback declarations of how we expect the callbacks to look like. There we can't cheat. :)


--

Kamil Ciemniewski

unread,
Mar 31, 2014, 4:03:05 AM3/31/14
to elixir-l...@googlegroups.com, jose....@plataformatec.com.br
Hey José,

Thanks for taking the time to look at it and for all this great advice!

> At the end of the day, building the DSL on top of an actual API helps keep you honest. If all you have is a DSL, it becomes very easy to just to throw more dirty under the carpet, add hidden functionality, and inject code when you are not supposed to.

I had a feeling the current solution wasn't very clear. Also, debugging eventual bugs in those config and compose blocks could be a bit of a pain...

> Also, I would like to point out that data structures compose really well. DSLs do not as much.

Agreed. I guess I should get outside of the "Rails-box" in my thinking. 

> The focus should not be on what I should "use" to get a behaviour but on what interface I should implement to expose some behaviour.

I smell a new protocol defined in the library very soon :)

The part about a DSL for defining emails is greatly valuable too. There will be no loss of cleanliness in defining them with simple functions with the aid of a Keyword module for providing context data, headers etc.

Thanks!

Kamil Ciemniewski

unread,
Mar 31, 2014, 4:08:11 AM3/31/14
to elixir-l...@googlegroups.com, jose....@plataformatec.com.br
Hah, good that I have someone to learn from then :)

I'll take a look at Ecto's adapters for sure.

Thanks again!


W dniu sobota, 29 marca 2014 15:08:18 UTC+1 użytkownik José Valim napisał:
Ah, to be clear, I have made all the "mistakes" I mentioned and not long ago! :)

Ecto.Repo injects a bunch of functions into the user module (although it is meant to be a facade, it is still a red flag). Ecto.Entity is much worse though, it injects functions in the user module and we don't define anywhere what it means to be an entity (you are locked in and you can't define your own).

That happens a lot. I assume it is because we often start with the DSL and it may take time to have the complete definition of how the inner APIs should look like.

The adapters in Ecto though are "tidy" and can be used as examples. We have defined the adapter behavior and provide callback declarations of how we expect the callbacks to look like. There we can't cheat. :)
--

José Valim

unread,
Mar 31, 2014, 4:19:58 AM3/31/14
to elixir-l...@googlegroups.com
The part about a DSL for defining emails is greatly valuable too. There will be no loss of cleanliness in defining them with simple functions with the aid of a Keyword module for providing context data, headers etc.

Right. There are many approaches that could be taken here. Using keywords is one of them. Another one is to provide a bunch of functions that work on the envelope (using keywords when required):

import Mail.Envelope

Mail.new
|> add_header("x-title", "hello")
|> add_part("text/html", "path/to/template", [data: "for/template"])
|> ...

Maybe the Plug connection can be good inspiration here: https://github.com/elixir-lang/plug/blob/master/lib/plug/connection.ex#L3 (keep in mind though that records are being deprecated on v0.13 in favor of structs so you may want to wait until v0.13 is out).

The nice thing that comes out of this is that you can actually test of all those functions directly, without going through a DSL. And at the end of the day I suspect the DSL implementation will be much cleaner too (if you intend to keep it).

Cheers!
Reply all
Reply to author
Forward
0 new messages