hailgun (empty) → 0.1.0.0
raw patch · 6 files changed
+523/−0 lines, 6 filesdep +aesondep +basedep +bytestringsetup-changed
Dependencies added: aeson, base, bytestring, configurator, email-validate, hailgun, http-client, http-client-tls, http-types, text, transformers
Files
- LICENSE +20/−0
- Mail/Hailgun.hs +239/−0
- README.markdown +48/−0
- Setup.hs +2/−0
- hailgun.cabal +76/−0
- src/Send.hs +138/−0
+ LICENSE view
@@ -0,0 +1,20 @@+Copyright (c) 2014 Robert Massaioli++Permission is hereby granted, free of charge, to any person obtaining+a copy of this software and associated documentation files (the+"Software"), to deal in the Software without restriction, including+without limitation the rights to use, copy, modify, merge, publish,+distribute, sublicense, and/or sell copies of the Software, and to+permit persons to whom the Software is furnished to do so, subject to+the following conditions:++The above copyright notice and this permission notice shall be included+in all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ Mail/Hailgun.hs view
@@ -0,0 +1,239 @@+-- | Hailgun is a Haskell wrapper around the <http://documentation.mailgun.com/api_reference.html Mailgun api's> that use+-- type safety to ensure that you are sending a valid request to the Mailgun API's. Mailgun is a+-- service that lets you send emails. It also contains a number of other email handling API's that+-- will be implimented in the future.+module Mail.Hailgun + ( sendEmail+ , hailgunMessage+ , HailgunMessage+ , HailgunContext(..)+ , MessageSubject+ , MessageContent(..)+ , MessageRecipients(..)+ , emptyMessageRecipients+ , UnverifiedEmailAddress+ , HailgunSendResponse(..)+ , HailgunErrorMessage+ , HailgunErrorResponse(..)+ ) where++import Control.Applicative ((<$>), (<*>))+import Control.Monad (mzero)+import Control.Monad.IO.Class+import Data.Aeson+import qualified Data.ByteString as B+import qualified Data.ByteString.Char8 as BC+import qualified Data.Text as T+import Text.Email.Validate+import Network.HTTP.Client (Request(..), parseUrl, httpLbs, withManager, responseStatus, responseBody, applyBasicAuth)+import Network.HTTP.Client.MultipartFormData (Part(..), formDataBody, partBS)+import Network.HTTP.Client.TLS (tlsManagerSettings)+import qualified Network.HTTP.Types.Status as NT+import qualified Network.HTTP.Types.Method as NM++{- + - The basic rest API's look like this when used in curl:+ -+ - curl -s --user 'api:key-3ax6xnjp29jd6fds4gc373sgvjxteol0' \+ - https://api.mailgun.net/v2/samples.mailgun.org/messages \+ - -F from='Excited User <me@samples.mailgun.org>' \+ - -F to=baz@example.com \+ - -F to=bar@example.com \+ - -F subject='Hello' \+ - -F text='Testing some Mailgun awesomness!'+ -+ - This is what we need to emulate with this library.+ -}++type UnverifiedEmailAddress = B.ByteString -- ^ Represents an email address that is not yet verified.+type MessageSubject = String -- ^ Represents a message subject.++-- | Any email content that you wish to send should be encoded into these types before it is sent.+-- Currently, according to the API, you should always send a Text Only part in the email and you can+-- optionally add a nicely formatted HTML version of that email to the sent message. +-- +-- @+-- It is best to send multi-part emails using both text and HTML or text only. Sending HTML only+-- email is not well received by ESPs.+-- @+-- (<http://documentation.mailgun.com/best_practices.html#email-content Source>)+--+-- This API mirrors that advice so that you can always get it right.+data MessageContent+ -- | The Text only version of the message content.+ = TextOnly+ { textContent :: B.ByteString + }+ -- | A message that contains both a Text version of the email content and a HTML version of the email content.+ | TextAndHTML+ { textContent :: B.ByteString -- ^ The text content that you wish to send (please note that many clients will take the HTML version first if it is present but that the text version is a great fallback).+ , htmlContent :: B.ByteString -- ^ The HTML content that you wish to send.+ }++-- | A Hailgun Email message that may be sent. It contains important information such as the address+-- that the email is from, the addresses that it should be going to, the subject of the message and+-- the content of the message. Any email that you wish to send via this api must be converted into+-- this structure first. To create a message then please use the hailgunMessage interface.+data HailgunMessage = HailgunMessage+ { messageSubject :: MessageSubject+ , messageContent :: MessageContent+ , messageFrom :: EmailAddress+ , messageTo :: [EmailAddress]+ , messageCC :: [EmailAddress]+ , messageBCC :: [EmailAddress]+ }+ -- TODO support sending attachments in the future+ -- TODO inline image support for the future+ -- TODO o:tag support+ -- TODO o:campaign support+ -- messageDKIMSupport :: Bool TODO o:dkim support+ -- TODO o:deliverytime support for up to three days in the future+ -- TODO o:testmode support+ -- TODO o:tracking support+ -- TODO o:tracking-clicks support+ -- TODO o:tracking-opens support+ -- TODO custom mime header support+ -- TODO custome message data support++-- | No recipients for your email. Useful singleton instance to avoid boilerplate in your code. For+-- example: +--+-- @+-- toBob = emptyMessageRecipients { recipientsTo = [\"bob\@bob.test\"] }+-- @+emptyMessageRecipients :: MessageRecipients+emptyMessageRecipients = MessageRecipients [] [] []++-- | A collection of unverified email recipients separated into the To, CC and BCC groupings that+-- email supports.+data MessageRecipients = MessageRecipients + { recipientsTo :: [UnverifiedEmailAddress] -- ^ The people to email directly.+ , recipientsCC :: [UnverifiedEmailAddress] -- ^ The people to \"Carbon Copy\" into the email. Honestly, why is that term not deprecated yet?+ , recipientsBCC :: [UnverifiedEmailAddress] -- ^ The people to \"Blind Carbon Copy\" into the email. There really needs to be a better name for this too.+ }++-- | A generic error message that is returned by the Hailgun library.+type HailgunErrorMessage = String++-- | A method to construct a HailgunMessage. You require a subject, content, From address and people+-- to send the email to and it will give you back a valid Hailgun email message. Or it will error+-- out while trying.+hailgunMessage + :: MessageSubject -- ^ The purpose of the email surmised.+ -> MessageContent -- ^ The full body of the email.+ -> UnverifiedEmailAddress -- ^ The email account that the recipients should respond to in order to get back to us.+ -> MessageRecipients -- ^ The people that should recieve this email.+ -> Either HailgunErrorMessage HailgunMessage -- ^ Either an error while trying to create a valid message or a valid message.+hailgunMessage subject content sender recipients = do+ from <- validate sender+ to <- mapM validate (recipientsTo recipients)+ cc <- mapM validate (recipientsCC recipients)+ bcc <- mapM validate (recipientsBCC recipients)+ return HailgunMessage + { messageSubject = subject+ , messageContent = content+ , messageFrom = from+ , messageTo = to+ , messageCC = cc+ , messageBCC = bcc+ }++toPostVars :: HailgunMessage -> [(BC.ByteString, BC.ByteString)]+toPostVars message = + [ (BC.pack "from", toByteString . messageFrom $ message)+ , (BC.pack "subject", BC.pack $ messageSubject message)+ ] ++ to + ++ cc + ++ bcc + ++ fromContent (messageContent message)+ where+ to = convertEmails (BC.pack "to") . messageTo $ message+ cc = convertEmails (BC.pack "cc") . messageCC $ message+ bcc = convertEmails (BC.pack "bcc") . messageBCC $ message++ fromContent :: MessageContent -> [(BC.ByteString, BC.ByteString)]+ fromContent t@(TextOnly _) = [ (BC.pack "text", textContent t) ]+ fromContent th@(TextAndHTML {}) = (BC.pack "html", htmlContent th) : fromContent (TextOnly . textContent $ th)++ convertEmails :: BC.ByteString -> [EmailAddress] -> [(BC.ByteString, BC.ByteString)]+ convertEmails prefix = fmap ((,) prefix . toByteString)++-- | When comunnicating to the Mailgun service you need to have some common pieces of information to+-- authenticate successfully. This context encapsulates that required information.+data HailgunContext = HailgunContext+ -- TODO better way to represent a domain+ { hailgunDomain :: String -- ^ The domain of the mailgun account that you wish to send the emails through.+ , hailgunApiKey :: String -- ^ The API key for the mailgun account so that you can successfully make requests. Please note that it should include the 'key' prefix.+ }++-- TODO replace with MailgunSendResponse+-- | The response to an email being accepted by the Mailgun API.+data HailgunSendResponse = HailgunSendResponse+ { hsrMessage :: String -- ^ The freeform message from the mailgun API.+ , hsrId :: String -- ^ The ID of the message that has been accepted by the Mailgun api.+ }++-- TODO make this Hailgun specific and different for the Mailgun api. That way there is the correct+-- separation of concerns.+-- | An error that comes from Mailgun or the Hailgun API.+data HailgunErrorResponse = HailgunErrorResponse+ { herMessage :: String -- ^ A generic message describing the error.+ }++toHailgunError :: String -> HailgunErrorResponse+toHailgunError = HailgunErrorResponse++instance FromJSON HailgunSendResponse where+ parseJSON (Object v) = HailgunSendResponse+ <$> v .: T.pack "message"+ <*> v .: T.pack "id"+ parseJSON _ = mzero++instance FromJSON HailgunErrorResponse where+ parseJSON (Object v) = HailgunErrorResponse+ <$> v .: T.pack "message"+ parseJSON _ = mzero++encodeFormData :: MonadIO m => [(BC.ByteString, BC.ByteString)] -> Request -> m Request+encodeFormData fields = formDataBody (map toPart fields)+ where+ toPart :: (BC.ByteString, BC.ByteString) -> Part+ toPart (name, content) = partBS (T.pack . BC.unpack $ name) content++-- | Send an email using the Mailgun API's. This method is capable of sending a message over the+-- Mailgun service. All it needs is the appropriate context.+sendEmail + :: HailgunContext -- ^ The Mailgun context to operate in.+ -> HailgunMessage -- ^ The Hailgun message to be sent.+ -> IO (Either HailgunErrorResponse HailgunSendResponse) -- ^ The result of the sent email. Either a sent email or a successful send.+sendEmail context message = do+ initRequest <- parseUrl url+ let request = initRequest { method = NM.methodPost, checkStatus = \_ _ _ -> Nothing }+ requestWithBody <- encodeFormData (toPostVars message) request+ let authedRequest = applyBasicAuth (BC.pack "api") (BC.pack . hailgunApiKey $ context) requestWithBody+ response <- withManager tlsManagerSettings (httpLbs authedRequest)+ case NT.statusCode . responseStatus $ response of+ 200 -> return . convertGood . eitherDecode' . responseBody $ response+ 400 -> return . Left . convertBad . eitherDecode' . responseBody $ response+ 401 -> return . Left . convertBad . eitherDecode' . responseBody $ response+ 402 -> return . Left . convertBad . eitherDecode' . responseBody $ response+ 404 -> return . Left . convertBad . eitherDecode' . responseBody $ response+ 500 -> serverError+ 502 -> serverError+ 503 -> serverError+ 504 -> serverError+ c -> retError . unexpectedError $ c+ where+ url = "https://api.mailgun.net/v2/" ++ hailgunDomain context ++ "/messages"+ retError = return . Left . toHailgunError++ serverError = retError "Server Errors - something is wrong on Mailgun’s end"+ unexpectedError x = "Unexpected Non-Standard Mailgun Error: " ++ show x++convertGood :: Either String HailgunSendResponse -> Either HailgunErrorResponse HailgunSendResponse+convertGood (Left error) = Left . toHailgunError $ error+convertGood (Right response) = Right response++convertBad :: Either String HailgunErrorResponse -> HailgunErrorResponse+convertBad (Left error) = toHailgunError error+convertBad (Right e) = e
+ README.markdown view
@@ -0,0 +1,48 @@+# Hailgun++The hailgun library is a Haskell library that integrates with Mailgun. Mailgun is a service that+allows controlled sending of emails. The intial focus of the API is on sending emails rather than +recieving them.++This library was implimented based upon the information provided on the [Mailgun API Reference][1].+The goal of this library is match this API 1-1.++## Building this library++This library just uses [cabal][5] to build itself. To do so for development then follow the following+steps:++ cabal sandbox init+ cabal install --only-dependencies+ cabal build++At that point in time you should have a working version of the library so you could run the example+hailgun-send command by:++ cabal run hailgun-send -- --help++And you can continue trying this libary from here.++## Trying this library++You probably want to know that this library works to prove that it would be a good choice for your+project. Follow these steps to get started with the library and Mailgun and send a test email:++ 1. [Sign up for Mailgun][2].+ 1. Mailgun will give you options to use [Curl][3] to send emails to yourself right after signup.+ You should do so to test that your account works. (Optional)+ 1. Grab the sandbox domain that was generated and the API key by visiting the [Mailgun control panel][4].+ 1. Create a file caled hailgun.send.conf in the current directory and put the API Key and the+ mailgun sandbox domain in the file. You can copy the hailgun.send.conf.default file to see the+ correct format.+ 1. Now you may use hailgun-send to send emails by passing in the correct command line arguments.+ You can see the command line argument options with `cabal run hailgun-send -- --help`++If you are lucky this has only taken you a few minutes to get working and you are now sending emails+through the Mailgun API from the terminal.++ [1]: http://documentation.mailgun.com/api_reference.html+ [2]: https://mailgun.com/signup+ [3]: http://man.cx/curl+ [4]: https://mailgun.com/cp+ [5]: http://www.haskell.org/cabal/
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ hailgun.cabal view
@@ -0,0 +1,76 @@+-- Initial hailgun.cabal generated by cabal init. For further +-- documentation, see http://haskell.org/cabal/users-guide/++-- The name of the package.+name: hailgun++-- The package version. See the Haskell package versioning policy (PVP) +-- for standards guiding when and how versions should be incremented.+-- http://www.haskell.org/haskellwiki/Package_versioning_policy+-- PVP summary: +-+------- breaking API changes+-- | | +----- non-breaking API additions+-- | | | +--- code changes with no API change+version: 0.1.0.0++-- A short (one-line) description of the package.+synopsis: Mailgun REST api interface for Haskell.++-- A longer description of the package.+description: Mailgun is an online service that sends emails. It is a great point of+ integration for many SaaS services and this Haskell library cleanly interfaces+ with Mailgun so that you can send emails from your Haskell applications.++-- The license under which the package is released.+license: MIT++-- The file containing the license text.+license-file: LICENSE++-- The package author(s).+author: Robert Massaioli++-- An email address to which users can send suggestions, bug reports, and +-- patches.+maintainer: robertmassaioli@gmail.com++-- A copyright notice.+copyright: (c) 2014 Robert Massaioli++category: Network++build-type: Simple++-- Extra files to be distributed with the package, such as examples or a +-- README.+extra-source-files: README.markdown++-- Constraint on the version of Cabal needed to build this package.+cabal-version: >=1.10+++library+ exposed-modules: Mail.Hailgun+ build-depends: base >=4.6 && <4.7+ , bytestring >= 0.9 && <= 0.11+ , aeson == 0.7.*+ , text == 0.11.*+ , transformers == 0.3.*+ , http-client == 0.3.*+ , http-client-tls == 0.2.*+ , email-validate ==2.0.*+ , http-types == 0.8.*++ ghc-options: -W+ default-language: Haskell2010+ +executable hailgun-send+ hs-source-dirs: src+ main-is: Send.hs+ default-language: Haskell2010+ ghc-options: -W++ build-depends: base == 4.6.*+ , hailgun+ , bytestring >= 0.9 && <= 0.11+ , text == 0.11.*+ , configurator == 0.3.*
+ src/Send.hs view
@@ -0,0 +1,138 @@+module Main where++import Mail.Hailgun++import Control.Applicative ((<$>))+import qualified Data.ByteString.Char8 as BC+import Data.Configurator (load, require, Worth(..))+import qualified Data.List as DL+import qualified Data.Text as T+import System.Console.GetOpt (getOpt, OptDescr(..), ArgDescr(..), ArgOrder(..), usageInfo)+import System.Environment (getArgs)+import System.Exit (exitFailure)++-- The purpose of this module is to provide a way to send emails to the Mailgun service using the+-- command line. This is mainly for the purposes of testing initially. Using this executable should+-- be the basis for our integration tests. It should also be the starting point for everybody that+-- wishes to test this library out.++data Flag + = Help+ | From { email :: UnverifiedEmailAddress }+ | To { email :: UnverifiedEmailAddress }+ | Subject { subject :: MessageSubject }+ | TextMessage { textFilePath :: FilePath }+ | HTMLMessage { htmlFilePath :: FilePath }+ deriving (Eq, Show)+ +options :: [OptDescr Flag]+options =+ [ Option "h" ["help"] (NoArg Help) "displays this help message"+ , Option "f" ["from"] (ReqArg fromP "me@test.test") "You are required to provide sender of this email."+ , Option "t" ["to"] (ReqArg toP "them@test.test") "You will need to provide atleast one person that you wish to send the email to."+ -- TODO Confirm that this is required.+ , Option "s" ["subject"] (ReqArg Subject "subject") "You need to send an email subject."+ , Option "x" ["text"] (ReqArg TextMessage "email.text") "You need to provide a text email file at a minimum."+ , Option "m" ["html"] (ReqArg HTMLMessage "email.html") "You can provide a HTML version of the email to send."+ ]+ where+ fromP = From . BC.pack+ toP = To . BC.pack++hailgunConfFile :: FilePath+hailgunConfFile = "hailgun.send.conf"++mailgunDomainLabel = T.pack "mailgun-domain"+mailgunApiKeyLabel = T.pack "mailgun-api-key"++loadHailgunContext :: FilePath -> IO HailgunContext+loadHailgunContext configFile = do+ hailgunConf <- load [Required configFile]+ domain <- require hailgunConf mailgunDomainLabel+ apiKey <- require hailgunConf mailgunApiKeyLabel+ return HailgunContext + { hailgunDomain = domain+ , hailgunApiKey = apiKey+ }++handleSend :: [Flag] -> MessageContent -> Either HailgunErrorMessage HailgunMessage+handleSend flags emailBody =+ case (unverifiedFrom, subjects) of+ ([from], [subject]) -> hailgunMessage subject emailBody from simpleRecipients+ ([], []) -> fail "You need to provide both a from address and a subject to send an email."+ (_ , []) -> fail "You have more than one from address and only one is allowed"+ ([], _ ) -> fail "You have more than one subject and only one is allowed"+ _ -> fail "You have too many from adresses and subjects, you should only have one of each."+ where+ unverifiedTo = fmap email . filter isTo $ flags+ unverifiedFrom = fmap email . filter isFrom $ flags+ subjects = fmap subject . filter isSubject $ flags++ simpleRecipients = emptyMessageRecipients { recipientsTo = unverifiedTo }++isSubject :: Flag -> Bool+isSubject (Subject _) = True+isSubject _ = False++isTo :: Flag -> Bool+isTo (To _) = True+isTo _ = False++isFrom :: Flag -> Bool+isFrom (From _) = True+isFrom _ = False++isTextMessage :: Flag -> Bool+isTextMessage (TextMessage _) = True+isTextMessage _ = False++isHtmlMessage :: Flag -> Bool+isHtmlMessage (HTMLMessage _) = True+isHtmlMessage _ = False++sendMessage :: HailgunMessage -> IO ()+sendMessage message = do+ hailgunContext <- loadHailgunContext hailgunConfFile+ response <- sendEmail hailgunContext message+ case response of+ Left error -> putStrLn $ "Failed to send email: " ++ herMessage error+ Right result -> do+ putStrLn "Sent Email!"+ putStrLn $ "Id: " ++ hsrId result+ putStrLn $ "Message: " ++ hsrMessage result++usageMessage = "Send emails using the Mailgun api."++main :: IO ()+main = do+ arguments <- getArgs+ case getOpt Permute options arguments of+ (flags, _, []) -> if Help `elem` flags+ then printUsage+ else do+ potentialEmailBody <- prepareEmailBody flags+ case potentialEmailBody of+ Nothing -> putStrLn "At the very least you must provide a text file to send as the email body."+ (Just messageContent) -> case handleSend flags messageContent of+ Left error -> putStrLn $ "Error generating mail: " ++ error + Right message -> sendMessage message+ (_, _, xs) -> do+ putStrLn "Error parsing arguments:"+ mapM_ putStrLn xs+ printUsage+ exitFailure+ where+ printUsage = putStrLn $ usageInfo usageMessage options++prepareEmailBody :: [Flag] -> IO (Maybe MessageContent)+prepareEmailBody flags = case (DL.find isTextMessage flags, DL.find isHtmlMessage flags) of+ (Nothing, _) -> return Nothing+ (Just (TextMessage textPath), Nothing) -> Just . TextOnly <$> BC.readFile textPath+ (Just (TextMessage textPath), Just (HTMLMessage htmlPath)) -> do+ textContents <- BC.readFile textPath+ htmlContents <- BC.readFile htmlPath+ return . Just $ TextAndHTML + { textContent = textContents+ , htmlContent = htmlContents+ }+ _ -> return Nothing -- The type safety should prevent this scenario.