I have been experimenting with Functional Programming for quite some time now, and to strengthen my skills with F#, I developed RailMail, “A Reactive Mail dispatcher written in F#”.
In this post, I will explain the core parts of RailMail and revise what was most interesting while writing this.
What does Railmail do?
RailMail is a service that exposes a REST API for sending Email. It was originally written as an email dispatch abstraction for EntE, but was replaced by Nodemailer in the last month.
Why is it Reactive then, you might ask? It also can listen to an AMQP queue for emails to send so you can loosely couple your services.
Show me some code!
The following code snippet shows the main logic of the REST API.
let webApp: HttpHandler =
choose [
GET >=>
choose [
route "/status" >=> setStatusCode 200 >=> text "OK"
]
POST >=>
choose [
route "/mail" >=> mailHandler
]
RequestErrors.NOT_FOUND "Not Found"
]
Giraffe (a HTTP library for F#) makes the HTTP handler look really simple because of the used DSL.
It basically says:
Choose between GET
and POST
.
If it’s a GET
and goes to /status
, set the status code to 200 and respond with “OK”.
If it’s a POST
and goes to /mail
, let mailHandler
handle it.
The mailHandler
returns a task, which behaves like Promises in Javascript:
let mailHandler (next: HttpFunc) (ctx: HttpContext) =
task {
let! body = ctx.ReadBodyFromRequestAsync()
let res = processMsg body
return! match res with
| Choice1Of2 e -> RequestErrors.BAD_REQUEST e next ctx
| Choice2Of2 _ -> setStatusCode 200 next ctx
}
It first reads the body of the incoming request and then processes the it using the processMsg
function.
Depending on processMsg
s response, mailHandler
returns a 200 or an error message.
processMsg
handles the mailing logic and returns a Choice
- a type that can either be one or the other, in this case either error or success.
let processMsg msg =
let envelope = Envelope.parse msg
match envelope with
| Choice1Of2 error -> Choice1Of2 error
| Choice2Of2 e -> dispatch e; Choice2Of2 ()
It does this by first parsing the incoming message.
If parsing was successful, so if Envelope.parse
returns Choice2Of2
, the mail gets dispatched and it returns another Choice2Of2
, this time empty.
If parsing was unsuccessful, it returns a Choice1Of
.
JSON Parsing
[<CLIMutable>]
type EnvelopeBody =
{
text : string
html : string
}
member this.HasErrors() =
if isNull this.html && isNull this.text
then Some "Either text or html body must be provided."
else None
[<CLIMutable>]
type Envelope =
{
recipients : string list
subject : string
body : EnvelopeBody
}
member this.HasErrors() =
this.body.HasErrors()
let parse s =
let v = JsonConvert.DeserializeObject<Envelope> s
match v.HasErrors() with
| Some e -> Choice1Of2 e
| None -> Choice2Of2 v
JSON deserialization is suprisingly easy in F#:
You just declare your types as [<CLIMutable>]
and add some validation methods, and the you can just use the DeserializeObject
method from the Newtonsoft.JSON Library.
SMTP Dispatching
let private config = SMTPConfig.config
let private fromAddress = new MailAddress(config.sender)
let private client = new SmtpClient(config.host, config.port)
client.EnableSsl <- true
client.Credentials <- NetworkCredential(config.username, config.password)
client.DeliveryMethod <- SmtpDeliveryMethod.Network
let private mimeType = ContentType("text/html")
Here you can see interaction with standard C# libraries: OOP style mutation is possible using the <-
operator, just as Instantiation using new
.
SMTP delivery is baked into .NET, so it is really easy.
let private constructMessage (e : Envelope) =
let msg = new MailMessage()
msg.From <- fromAddress
msg.Body <- e.body.text
msg.Subject <- e.subject
for r in e.recipients do
msg.To.Add(r)
msg
constructMessage
is used to project our JSON message on to a C# MailMessage object that can then be dispatched using the mail client:
let dispatch (e : Envelope) =
use msg = constructMessage e
client.Send msg
What did I learn from this project?
Functional Programming has awesome abstractions.
The Option
type eliminates the nasty NullException completely and enables concise, easy code where other languages would have countless null
checks and garbled logic.
Replacing classes with data objects and letting logic reside inside of functions that act transform data object greatly simplifies testing and reasoning about code.
Immutability will save you countless headaches and creates algorithms that project data onto a transformation instead of mutating it.
Summary
Although programming in F# is very fun, I find myself still using Typescript as my go-to language, since the Node.JS ecosystem is so huge.
But learning F# was not worthless: the three aspects I mentioned above found their way into my normal Typescript coding where I now use libraries like Monet.js and the data mapper pattern more often. I will explain more on functional concepts in Typescript in a future post.