Last week, we started our exploration of the world of APIs by integrating Haskell with Twilio. We were able to send a basic SMS message, and then create a server that could respond to a user’s message. This week, we’re going to venture into another type of effect: sending emails. We’ll be using Mailgun for this task, along with the Hailgun Haskell API for it.
You can take a look at the full code for this article by looking at the [mailgun](https://github.com/jhb563/HaskellApisSeries/tree/mailgun)
branch on our Github repository. If this article sparks your curiosity for more Haskell libraries, you should download our Production Checklist!
To start with, we’ll need a mailgun account obviously. Signing up is free and straightforward. It will ask you for an email domain, but you don’t need one to get started. As long as you’re in testing mode, you can use a sandbox domain they provide to host your mail server.
With Twilio, we had to specify a “verified” phone number that we could message in testing mode. Similarly, you will also need to designate a verified email address. Your sandboxed domain will only be able to send to this address. You’ll also need to save a couple pieces of information about your Mailgun account. In particular, you need your API Key, the sandboxed email domain, and the reply address for your emails to use. Save these as environment variables on your local system and remote machine.
Now let’s get a feel for the Hailgun code by sending a basic email. All this occurs in the simple IO
monad. We ultimately want to use the function sendEmail
, which requires both a HailgunContext
and a HailgunMessage
:
sendEmail :: HailgunContext -> HailgunMessage -> IO (Either HailgunErrorResponse HailgunSendResponse)
We’ll start by retrieving our environment variables. With our domain and API key, we can build the HailgunContext
we’ll need to pass as an argument.
import Data.ByteString.Char8 (pack)
sendMail :: IO ()sendMail = do domain <- getEnv “MAILGUN_DOMAIN” apiKey <- getEnv “MAILGUN_API_KEY” replyAddress <- pack <$> getEnv “MAILGUN_REPLY_ADDRESS” -- Last argument is an optional proxy let context = HailgunContext domain apiKey Nothing ...
Now to build the message itself, we’ll use a builder function hailgunMessage
. It takes several different parameters:
hailgunMessage :: MessageSubject -> MessageContent -> UnverifiedEmailAddress -- Reply Address, just a ByteString -> MessageRecipients -> [Attachment] -> Either HailgunErrorMessage HailgunMessage
These are all very easy to fill in. The MessageSubject
is Text
and then we’ll pass our reply address from above. For the content, we’ll start by using the TextOnly
constructor for a plain text email. We’ll see an example later of how we can use HTML in the content:
sendMail :: IO ()sendMail = do … replyAddress <- pack <$> getEnv “MAILGUN_REPLY_ADDRESS” let msg = mkMessage replyAddress … where mkMessage replyAddress = hailgunMessage “Hello Mailgun!” (TextOnly “This is a test message.”) replyAddress ...
The MessageRecipients
type has three fields. First are the direct recipients, then the CC’d emails, and then the BCC’d users. We're only sending to a single user at the moment. So we can take the emptyMessageRecipients
item and modify it. We’ll wrap up our construction by providing an empty list of attachments for now:
where mkMessage replyAddress = hailgunMessage “Hello Mailgun!” (TextOnly “This is a test message.”) replyAddress (emptyMessageRecipients { recipientsTo = [“[email protected]”] } ) []
If there are issues, the hailgunMessage
function can throw an error, as can the sendEmail
function itself. But as long as we check these errors, we’re in good shape to send out the email!
createAndSendEmail :: IO ()createAndSendEmail = do domain <- getEnv “MAILGUN_DOMAIN” apiKey <- getEnv “MAILGUN_API_KEY” replyAddress <- pack <$> getEnv “MAILGUN_REPLY_ADDRESS” let context = HailgunContext domain apiKey Nothing let msg = mkMessage replyAddress case msg of Left err -> putStrLn (“Making failed: “ ++ show err) Right msg’ -> do result <- sendEmail context msg case result of Left err -> putStrLn (“Sending failed: “ ++ show err) Right resp -> putStrLn (“Sending succeeded: “ ++ show rep)
Notice how it’s very easy to build all our functions up when we start with the type definitions. We can work through each type and figure out what it needs. I reflect on this idea some more in this article on Compile Driven Learning, which is part of our Haskell Brain Series for newcomers to Haskell!
Now we’d like to incorporate sending an email into our server. As you’ll note from looking at the source code, I revamped the Servant server to use free monads. There are many different effects in our system, and this helps us keep them straight. Check out this article for more details on free monads and the Eff library. To start, we want to describe our email sending as an effect. We’ll start with a simple data type that has a single constructor:
data Email a where SendSubscribeEmail :: Text -> Email (Either String ())
sendSubscribeEmail :: (Member Email r) => Text -> Eff r (Either String ())sendSubscribeEmail email = send (SendSubscribeEmail email)
Now we need a way to peel the Email
effect off our stack, which we can do as long as we have IO
. We’ll mimic the sendEmail
function we already wrote as the transformation. We now take the user’s email we’re sending to as an input!
runEmail :: (Member IO r) => Eff (Email ': r) a -> Eff r arunEmail = runNat emailToIO where emailToIO :: Email a -> IO a emailToIO (SendSubscribeEmail subEmail) = do domain <- getEnv "MAILGUN_DOMAIN" apiKey <- getEnv "MAILGUN_API_KEY" replyEmail <- pack <$> getEnv "MAILGUN_REPLY_ADDRESS" let context = HailgunContext domain apiKey Nothing case mkSubscribeMessage replyEmail (encodeUtf8 subEmail) of Left err -> return $ Left err Right msg -> do result <- sendEmail context msg case result of Left err -> return $ Left (show err) Right resp -> return $ Right ()
Now that we’ve properly described sending an email as an effect, let’s incorporate it into our server! We’ll start by writing another data type that will represent the potential commands a user might text to us. For now, it will only have the “subscribe” command.
data SMSCommand = SubscribeCommand Text
Now let’s write a function that will take their message and interpret it as a command. If they text subscribe {email}
, we’ll send them an email!
messageToCommand :: Text -> Maybe SMSCommandmessageToCommand messageBody = case splitOn " " messageBody of ["subscribe", email] -> Just $ SubscribeCommand email _ -> Nothing
Now we’ll extend our server handler to reply. If we interpret their command correctly, we’ll send the email! Otherwise, we’ll send them back a text saying we couldn’t understand them. Notice how our SMS
effect and Email
effect are part of this handler:
smsHandler :: (Member SMS r, Member Email r) => IncomingMessage -> Eff r ()smsHandler msg = case messageToCommand (body msg) of Nothing -> sendText (fromNumber msg) "Sorry, we didn't understand that request!" Just (SubscribeCommand email) -> do _ <- sendSubscribeEmail email return ()
And now our server will be able to send the email when the user “subscribes”!
Let’s make our email a little more complicated. Right now we’re only sending a very basic email. Let’s modify it so it has an attachment. We can build an attachment by providing a path to a file as well as a string describing it. To get this file, our message making function will need the current running directory. We’ll also change the body a little bit.
mkSubscribeMessage :: ByteString -> ByteString -> FilePath -> Either HailgunErrorMessage HailgunMessagemkSubscribeMessage replyAddress subscriberAddress currentDir = hailgunMessage "Thanks for signing up!" content replyAddress (emptyMessageRecipients { recipientsTo = [subscriberAddress] }) -- Notice the attachment! [ Attachment (rewardFilepath currentDir) (AttachmentBS "Your Reward") ] where content = TextOnly "Here's your reward!”
rewardFilepath :: FilePath -> FilePathrewardFilepath currentDir = currentDir ++ "/attachments/reward.txt"
Now when our user signs up, they’ll get whatever attachment file we’ve specified!
To show off one more feature, let’s change the content of our email so that it contains some HTML instead of only text! In particular, we’ll give them the chance to confirm their subscription by clicking a link to our server. All that changes here is that we’ll use the TextAndHTML
constructor instead of TextOnly
. We do want to provide a plain text interpretation of our email in case HTML can’t be rendered for whatever reason. Notice the use of the <a>
tags for the link:
content = TextAndHTML textOnly ("Here's your reward! To confirm your subscription, click " <> link <> "!") where textOnly = "Here's your reward! To confirm your subscription, go to " <> "https://haskell-apis.herokuapp.com/api/subscribe/" <> subscriberAddress <> " and we'll sign you up!" link = "<a href=\"https://haskell-apis.herokuapp.com/api/subscribe/" <> subscriberAddress <> "\">this link</a>"
Now we’ll add another endpoint that will capture the email as a parameter and save it to a database. The Database
effect very much resembles the one from the Eff article. It’ll save the email in a database table.
type ServerAPI = "api" :> "ping" :> Get '[JSON] String :<|> "api" :> "sms" :> ReqBody '[FormUrlEncoded] IncomingMessage :> Post '[JSON] () :<|> "api" :> "subscribe" :> Capture "email" Text :> Get '[JSON] ()
subscribeHandler :: (Member Database r) => Text -> Eff r ()subscribeHandler email = registerUser email
Now if we wanted to write a function that would email everyone in our system, it’s not hard at all! We extend our effect types for both Email
and Database
. The Database
function will retrieve all the subscribers in our system. Meanwhile the Email
effect will send the specified email to the whole list.
data Database a where RegisterUser :: Text -> Database () RetrieveSubscribers :: Database [Text]
data Email a where SendSubscribeEmail :: Text -> Email (Either String ()) -- First parameter is (Subject line, Text content, HTML Context) SendEmailToList :: (Text, ByteString, Maybe ByteString) -> [Text] -> Email (Either String ())
And combining these just requires using both effects:
sendEmailToList :: (Member Email r, Member Database r) => ByteString -> ByteString -> Eff r ()sendEmailToList = do list <- retrieveSubscribers void $ sendEmailToList list
Notice the absence of any lift
calls! This is one of the cool strengths of Eff
.
As we’ve seen in this article, sending emails with Haskell isn’t too scary. The Hailgun
API is quite intuitive and when you break things down piece by piece and look at the types involved. This article brought together ideas from both compile driven development and the Eff framework. In particular, we can see in this series how convenient it is to separate our effects with Eff
so that we aren’t doing a lot of messy lifts.
There’s a lot of advanced material in this article, so if you think you need to backtrack, don’t worry, we’ve got you covered! Our Haskell Web Skills Series will teach you how to use libraries like Persistent for database management and Servant for making an API. For some more libraries you can use to write enhanced Haskell, download our Production Checklist!
If you’ve never programmed in Haskell at all, you should try it out! Download our Haskell Beginner’s Checklist or read our Liftoff Series!