Latest news about Bitcoin and all cryptocurrencies. Your daily crypto news habit.
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 branch on our Github repository. If this article sparks your curiosity for more Haskell libraries, you should download our Production Checklist!
Making an Account
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.
Basic Email
Now letâs get a feel for the Hailgun code by sending a basic email. All this occurs in the simple IOmonad. 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 = [âverified@mail.comâ] } ) []
If there are issues, the hailgunMessage function can throw an error, as can the sendEmailfunction 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!
Effify Email
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 ()
Extending our SMSÂ Handler
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â!
Attaching a File
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!
HTML Content
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.
Conclusion
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!
Mailing it out with Mailgun! was originally published in Hacker Noon on Medium, where people are continuing the conversation by highlighting and responding to this story.
Disclaimer
The views and opinions expressed in this article are solely those of the authors and do not reflect the views of Bitcoin Insider. Every investment and trading move involves risk - this is especially true for cryptocurrencies given their volatility. We strongly advise our readers to conduct their own research when making a decision.