Akka Extras Email April 27, 2013
This is the e-mail Akka Extra, which allows you to easily add simple
javax.mail
support to your application. My motivatiosn were quite simple:
- Actor and non-actor API
- Flexible configuration
- No classes for
Email
,Address
and such like - Flexible error handling
The first item, actor and non-actor API was easy. I have a trait that you can directly mix in, or you can instantiate an actor that does the dirty work. The second one is yet another trait and appropriate implementations. The third and fourth points get more interesting. Let's now take a look at the code and discuss the main points.
No classes for Email, Address and such like
The challenge was to construct a system that would have sufficiently extensible system that does not drown in complex class hierarchies. As usual, traits and self-type annotations (aka the cake pattern) is the answer. When sending e-mails, we have several distinct entities:
- Subject
- Sender and recipient addresses
- Body
Translating this to a function, we arrive at a function
type Address = String
type Subject = String
type Body = String
type Result = ...
def email(sender: Address, subject: String, body: Body,
to: List[Address], cc: List[Address], bcc: List[Address]): Result
Let's leave out the type of the Result
for now. If we want to build flexible system, we would like to be able to specify the types ourselves. Traits and self-type annotations to the rescue. I will show you how to do that with the Address
types.
trait InternetAddressBuilder {
type AddressIn
def buildInternetAddress:
AddressIn => M[InternetAddress] = ???
def buildInternetAddresses:
List[AddressIn] => M[Array[InternetAddress]] =
??? // but using buildInternetAddress
}
This trait defines buildInternetAddress
that returns a function that takes the AddressIn
and returns a context M
containing the InternetAddress
.
Let's take a look at one of the implementations of this trait. It defines the abstract type AddressIn
and implements the buildInternetAddress
function.
trait SimpleInternetAddressBuilder extends InternetAddressBuilder {
type AddressIn = String
def buildInternetAddress: AddressIn => M[InternetAddress] = {
address: AddressIn =>
m(InternetAddress.parse(address, false)(0))
}
}
Excellent. The javamail module defines the InternetAddressBuilder
and its SimpleInternetAddressBuilder
implementation. However, in your code, you may want to extract email addresses from User
objects, not String
s. In that case, you will need to provide the appropriate implementation of the InternetAddressBuilder
.
Context
Let's now tackle the context in which the InternetAddressBuilder
returns the InternetAddress
es. We want to use the context to indicate successes and failures, and we would like to be able to chain these contexts together. Scalaz 7 has just the right thing. We can have a context that contains some errors or successes; left or right. Scalaz calls it EitherT[F[+_], L, R]
; F[+_]
is the container into which we pack the R
s. In our case, the L
is Throwable
to indicate errors, R
is InternetAddress
and F
is Id
.
So, let's go back to InternetAddressBuilder
and plug in the proper return type.
trait InternetAddressBuilder {
type AddressIn
def buildInternetAddress:
AddressIn => EitherT[Id, Throwable, InternetAddress] = ???
def buildInternetAddresses:
List[AddressIn] => EitherT[Id, Throwable, Array[InternetAddress]] =
??? // but using buildInternetAddress
}
To save us typing, we shall define EitherFailures[A]
as wrapper around EitherT
: type EitherFailures[A] = EitherT[Id, Throwable, A]
.
Now, this leaves us with the last task. Examine the buildInternetAddress
and buildInternetAddresses
. The first one takes one AddressIn
and returns EitherFailures[InternetAddress]
. The second one takes List[AddressIn]
and reutrn a Array[InternetAddress]
, by folding the successes and stopping on failures.
We begin by mapping the input addresses. So, we go from List[AddressIn]
into List[EitherT[Id, Throwable, InternetAddress]]
. However, we want EitherT[Id, Throwable, List[InternetAddress]]
. We seem to be migrating the List
from the front into the value on the right. Let's fold.
def buildInternetAddresses:
List[AddressIn] => EitherFailures[Array[InternetAddress]] = {
addresses =>
import scalaz.syntax.monad._
val z = List.empty[InternetAddress].point[EitherFailures]
addresses.map(buildInternetAddress).foldLeft(z) { (b, a) =>
for {
address <- a
addresses <- b
} yield address :: addresses
}.map(_.toArray)
}
We map
the input addresses.map(buildInternetAddress)
, giving us List[EitherT[...]]
. We then fold this list, starting from the empty value on the right (z
) by flat mapping the container with each value. We finally turn the List[InternetAddress]
into Array[InternetAddress]
(stupid JavaMail).
More contexts
And this is how the rest of the javamail
module operates internally. It makes the most of the cake pattern and uses the EitherT
monad transformer to build a context that, when executed, will send e-mail or report errors. Let's fast-forward to SimpleMimeMessageBuilder
, which takes some input and makes a MimeMessage
that can be transported.
trait SimpleMimeMessageBuilder extends MimeMessageBuilder {
this: EmailConfiguration with InternetAddressBuilder with MimeMessageBodyBuilder =>
type MessageIn =
(AddressIn, // from
String, // subject
MimeMessageBodyIn, // body
List[AddressIn], // to
List[AddressIn], // cc
List[AddressIn] // bcc
)
private def mimeMessage(session: Session,
from: InternetAddress, subject: String, body: MimeMultipart,
to: Array[InternetAddress], cc: Array[InternetAddress],
bcc: Array[InternetAddress]): MimeMessage = { ... }
def buildMimeMessage: MessageIn => EitherFailures[MimeMessage] = {
in: MessageIn =>
val (from, subject, body, to, cc, bcc) = in
for {
session <- getMailSession
body <- buildMimeMessageBody(body)
from <- buildInternetAddress(from)
to <- buildInternetAddresses(to)
cc <- buildInternetAddresses(cc)
bcc <- buildInternetAddresses(bcc)
} yield mimeMessage(session, from, subject, body, to, cc, bcc)
}
}
Notice how nicely we were able to combine all EitherT
contexts to produce one final context that, given MessageIn
produces either Throwable
on the left or MimeMessage
on the right. All that is needed is to transport it.
Actors
To close, let's examine how we use the low-level trait in an actor. The actor needs to be able to send MimeMessage
as well as some MessageIn
types (the conctete type of MessageIn
is driven by the mixed-in dependencies at the point of actor construction).
trait SimpleConfgiruedActorEmail extends
SimpleUnconfiguredEmail with ConfigEmailConfiguration {
this: Actor =>
def config = context.system.settings.config
}
class SimpleEmailActor extends Actor with Emailer {
this: EmailTransport with EmailConfiguration with MimeMessageBuilder with MimeMessageBodyBuilder with InternetAddressBuilder =>
def receive = {
case m: MimeMessage => transportEmailMessage(m).run
case m: MessageIn => email(m).run
}
}
That's actually all there is to it. By carefully combining the small components of the underlying transport mechanism, we are able to create a very compact actor that sends e-mails.
Tests
No code would be complete without tests. However, we would like to make sure that we can actually talk to the SMTP server. This is the job for Dumbster, which we setup to listen on port 10025
. Your e-mail testing code can then be as simple as
class JavamailEmailMessageDeliverySpec extends Specification with EmailFragments {
class Simple extends Emailer with JavamailEmailTransport with TestingEmailConfiguration with SimpleInternetAddressBuilder with SimpleMimeMessageBodyBuilder with SimpleMimeMessageBuilder
"SMTP email transport" should {
sequential
"construct and send simple emails" in {
val subject = "Subject"
val body = "Body"
val email = receiveEmails {
val from = "Jan Machacek <janm@cakesolutions.net>"
val simple = new Simple
val x = simple.email(from, subject, body, List(from), Nil, Nil)
x.run.isRight must beTrue
}.head
email.getHeaderValue("Subject") mustEqual subject
}
}
}
Notice how we assemple the layers of the cake in the Simple
class and how we then use it in the receiveEmails
block. Inside this block, we have the SMTP server running and, when the block completes, we get a List[SmtpMessage]
that we can use in our assertions.
Code
The code is available at https://github.com/eigengo/akka-extras, the binary artefacts are available on Sonatype.