packages feed

mandrill (empty) → 0.1.0.0

raw patch · 18 files changed

+1205/−0 lines, 18 filesdep +QuickCheckdep +aesondep +basesetup-changed

Dependencies added: QuickCheck, aeson, base, base64-bytestring, blaze-html, bytestring, containers, email-validate, lens, mandrill, mtl, old-locale, raw-strings-qq, tasty, tasty-hunit, tasty-quickcheck, text, time, wreq

Files

+ LICENSE view
@@ -0,0 +1,20 @@+Copyright (c) 2014 Alfredo Di Napoli++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.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ mandrill.cabal view
@@ -0,0 +1,78 @@+name:                mandrill+version:             0.1.0.0+synopsis:            Library for interfacing with the Mandrill JSON API+description:         Pure Haskell client for the Mandrill JSON API+license:             MIT+license-file:        LICENSE+author:              Alfredo Di Napoli+maintainer:          alfredo.dinapoli@gmail.com+category:            Network+build-type:          Simple+tested-with:         GHC == 7.4, GHC == 7.6, GHC == 7.8+cabal-version:       >=1.10++source-repository head+  type:     git+  location: https://github.com/adinapoli/mandrill++library+  exposed-modules:+    Network.API.Mandrill+    Network.API.Mandrill.Settings+    Network.API.Mandrill.Types+    Network.API.Mandrill.Trans+    Network.API.Mandrill.Users+    Network.API.Mandrill.Users.Types+    Network.API.Mandrill.Messages+    Network.API.Mandrill.Messages.Types+  other-modules:+    Network.API.Mandrill.Utils+    Network.API.Mandrill.HTTP+    Network.API.Mandrill.Orphans+  -- other-extensions:+  build-depends:+      base >=4.6 && < 5+    , containers >= 0.5.0.0+    , bytestring >= 0.9.0+    , base64-bytestring >= 1.0.0.1+    , text >= 1.0.0.0 && < 1.2+    , wreq >= 0.1.0.1+    , aeson >= 0.7.0.3 && < 0.8+    , lens >= 4.1 && < 4.6+    , blaze-html >= 0.5.0.0+    , QuickCheck >= 2.6 && < 2.8+    , mtl < 3.0+    , time+    , email-validate >= 1.0.0+    , old-locale+  hs-source-dirs:+    src+  default-language:+    Haskell2010+  ghc-options:+    -funbox-strict-fields++test-suite mandrill-tests+  type:+    exitcode-stdio-1.0+  main-is:+    Main.hs+  other-modules:+    Tests+    RawData+    Online+  hs-source-dirs:+    test+  default-language:+    Haskell2010+  build-depends:+      mandrill -any+    , base+    , aeson+    , bytestring+    , QuickCheck+    , tasty >= 0.9.0.1+    , tasty-quickcheck+    , tasty-hunit+    , raw-strings-qq < 1.2+    , text
+ src/Network/API/Mandrill.hs view
@@ -0,0 +1,101 @@++module Network.API.Mandrill (+    module M+  , sendEmail+  , emptyMessage+  , newTextMessage+  , newHtmlMessage+  , liftIO+  ) where++import Control.Monad.Reader+import Control.Lens+import Data.Time+import Text.Blaze.Html+import Network.API.Mandrill.Types as M+import Network.API.Mandrill.Messages as M+import Network.API.Mandrill.Messages.Types as M+import Network.API.Mandrill.Trans as M+import Data.Monoid+import Text.Email.Validate+import qualified Data.Text as T+import qualified Data.Aeson as JSON+++--------------------------------------------------------------------------------+emptyMessage :: EmailAddress -> EmailAddress -> MandrillMessage+emptyMessage f t = MandrillMessage {+   _mmsg_html = mempty+ , _mmsg_text = Nothing+ , _mmsg_subject = T.empty+ , _mmsg_from_email = f+ , _mmsg_from_name = Nothing+ , _mmsg_to = [newRecipient t]+ , _mmsg_headers = JSON.Null+ , _mmsg_important = Nothing+ , _mmsg_track_opens = Nothing+ , _mmsg_track_clicks = Nothing+ , _mmsg_auto_text = Nothing+ , _mmsg_auto_html = Nothing+ , _mmsg_inline_css = Nothing+ , _mmsg_url_strip_qs = Nothing+ , _mmsg_preserve_recipients = Nothing+ , _mmsg_view_content_link = Nothing+ , _mmsg_bcc_address = Nothing+ , _mmsg_tracking_domain = Nothing+ , _mmsg_signing_domain = Nothing+ , _mmsg_return_path_domain = Nothing+ , _mmsg_merge = Nothing+ , _mmsg_global_merge_vars = []+ , _mmsg_merge_vars = []+ , _mmsg_tags = []+ , _mmsg_subaccount = Nothing+ , _mmsg_google_analytics_domains = []+ , _mmsg_google_analytics_campaign = Nothing+ , _mmsg_metadata = JSON.Null+ , _mmsg_recipient_metadata = []+ , _mmsg_attachments = []+ , _mmsg_images = []+  }++++--------------------------------------------------------------------------------+newHtmlMessage :: EmailAddress+               -> EmailAddress+               -> T.Text+               -> Html+               -> MandrillMessage+newHtmlMessage f t subj html = let body = mkMandrillHtml html in+  ((mmsg_html .~ body) . (mmsg_subject .~ subj)) $ (emptyMessage f t)+++--------------------------------------------------------------------------------+newTextMessage :: EmailAddress+               -> EmailAddress+               -> T.Text+               -> T.Text+               -> MandrillMessage+newTextMessage f t subj txt = let body = unsafeMkMandrillHtml txt in+  ((mmsg_html .~ body) .+   (mmsg_text .~ Just txt) .+   (mmsg_subject .~ subj)) (emptyMessage f t)+++--------------------------------------------------------------------------------+sendEmail :: MonadIO m+          => MandrillMessage+          -> MandrillT m (MandrillResponse [MessagesResponse])+sendEmail msg = do+  key <- ask+  liftIO $ send key msg (Just True) Nothing Nothing+++--------------------------------------------------------------------------------+sendTextEmail :: MonadIO m+              => MandrillMessage+              -> MandrillT m (MandrillResponse [MessagesResponse])+sendTextEmail msg = do+  key <- ask+  now <- liftIO getCurrentTime+  liftIO $ send key msg (Just True) Nothing (Just now)
+ src/Network/API/Mandrill/HTTP.hs view
@@ -0,0 +1,19 @@+{-# LANGUAGE OverloadedStrings #-}+module Network.API.Mandrill.HTTP where++import Network.API.Mandrill.Settings+import Network.API.Mandrill.Types+import qualified Data.Text as T+import Data.Monoid+import Data.Aeson+import Network.Wreq+import Control.Lens++toMandrillResponse :: (MandrillEndpoint ep, FromJSON a, ToJSON rq)+                   => ep+                   -> rq+                   -> IO (MandrillResponse a)+toMandrillResponse ep rq = do+  let fullUrl = mandrillUrl <> toUrl ep+  res <- asJSON =<< post (T.unpack fullUrl) (toJSON rq)+  return $ res ^. responseBody
+ src/Network/API/Mandrill/Messages.hs view
@@ -0,0 +1,25 @@++module Network.API.Mandrill.Messages where++import           Network.API.Mandrill.Types+import           Network.API.Mandrill.Messages.Types+import           Network.API.Mandrill.Settings+import           Network.API.Mandrill.HTTP+import           Data.Time+import qualified Data.Text as T++--------------------------------------------------------------------------------+-- | Send a new transactional message through Mandrill+send :: MandrillKey+     -- ^ The API key+     -> MandrillMessage+     -- ^ The email message+     -> Maybe Bool+     -- ^ Enable a background sending mode that is optimized for bulk sending+     -> Maybe T.Text+     -- ^ ip_pool+     -> Maybe UTCTime+     -- ^ send_at+     -> IO (MandrillResponse [MessagesResponse])+send k msg async ip_pool send_at =+  toMandrillResponse MessagesSend (MessagesSendRq k msg async ip_pool send_at)
+ src/Network/API/Mandrill/Messages/Types.hs view
@@ -0,0 +1,44 @@+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE OverloadedStrings #-}+module Network.API.Mandrill.Messages.Types where++import           Data.Char+import           Data.Time+import qualified Data.Text as T+import           Control.Lens+import           Control.Monad+import           Data.Monoid+import           Data.Aeson+import           Data.Aeson.Types+import           Data.Aeson.TH++import           Network.API.Mandrill.Types+++--------------------------------------------------------------------------------+data MessagesSendRq = MessagesSendRq {+    _msrq_key :: MandrillKey+  , _msrq_message :: MandrillMessage+  , _msrq_async :: Maybe Bool+  , _msrq_ip_pool :: Maybe T.Text+  , _msrq_send_at :: Maybe UTCTime+  } deriving Show++makeLenses ''MessagesSendRq+deriveJSON defaultOptions { fieldLabelModifier = drop 6 } ''MessagesSendRq+++--------------------------------------------------------------------------------+data MessagesResponse = MessagesResponse {+    _mres_email :: !T.Text+    -- ^ The email address of the recipient+  , _mres_status :: MandrillEmailStatus+    -- ^ The sending status of the recipient+  , _mres_reject_reason :: Maybe MandrillRejectReason+    -- ^ The reason for the rejection if the recipient status is "rejected"+  , _mres__id    :: !T.Text+    -- ^ The message's unique id+  } deriving Show++makeLenses ''MessagesResponse+deriveJSON defaultOptions { fieldLabelModifier = drop 6 } ''MessagesResponse
+ src/Network/API/Mandrill/Orphans.hs view
@@ -0,0 +1,17 @@++module Network.API.Mandrill.Orphans where++import Data.Aeson+import Data.Aeson.Types+import Text.Email.Validate+import qualified Data.Text.Encoding as TE+++instance ToJSON EmailAddress where+  toJSON = String . TE.decodeUtf8 . toByteString++instance FromJSON EmailAddress where+  parseJSON (String s) = case validate (TE.encodeUtf8 s) of+    Left err -> fail err+    Right v  -> return v+  parseJSON o = typeMismatch "Expecting a String for EmailAddress." o
+ src/Network/API/Mandrill/Settings.hs view
@@ -0,0 +1,30 @@+{-# LANGUAGE OverloadedStrings #-}+module Network.API.Mandrill.Settings where++import qualified Data.Text as T++mandrillUrl :: T.Text+mandrillUrl = "https://mandrillapp.com/api/1.0/"++data MandrillCalls =+  -- Users API+    UsersInfo+  | UsersPing+  | UsersPing2+  | UsersSenders+  -- Messages API+  | MessagesSend+  | MessagesSendTemplate+  | MessagesSearch deriving Show++class MandrillEndpoint ep where+  toUrl :: ep -> T.Text++instance MandrillEndpoint MandrillCalls where+  toUrl UsersInfo = "users/info.json"+  toUrl UsersPing = "users/ping.json"+  toUrl UsersPing2 = "users/ping2.json"+  toUrl UsersSenders = "users/senders.json"+  toUrl MessagesSend = "messages/send.json"+  toUrl MessagesSendTemplate = "messages/send-template.json"+  toUrl MessagesSearch = "messages/search.json"
+ src/Network/API/Mandrill/Trans.hs view
@@ -0,0 +1,25 @@+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+module Network.API.Mandrill.Trans where++import Control.Monad.Reader+import Control.Applicative+import Network.API.Mandrill.Types+++--------------------------------------------------------------------------------+newtype MandrillT m a = MandrillT {+  runMandrillT :: ReaderT MandrillKey m a+  } deriving (MonadTrans, MonadReader MandrillKey,+              Functor, Applicative, Monad, MonadIO)+++--------------------------------------------------------------------------------+type Mandrill = MandrillT IO+++--------------------------------------------------------------------------------+runMandrill :: MonadIO m+            => MandrillKey+            -> MandrillT m a+            -> m a+runMandrill key action = runReaderT (runMandrillT action) key
+ src/Network/API/Mandrill/Types.hs view
@@ -0,0 +1,334 @@+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE OverloadedStrings #-}+module Network.API.Mandrill.Types where++import           Network.API.Mandrill.Utils+import           Network.API.Mandrill.Orphans()+import           Test.QuickCheck+import           Text.Email.Validate+import           Data.Char+import           Data.Maybe+import           Data.Time+import           Control.Applicative+import           System.Locale (defaultTimeLocale)+import qualified Data.ByteString as B+import qualified Data.ByteString.Base64 as Base64+import qualified Data.Text as T+import qualified Data.Text.Encoding as TL+import qualified Data.Text.Lazy as TL+import           Control.Lens+import           Data.Monoid+import           Data.Aeson+import           Data.Aeson.Types+import           Data.Aeson.TH+import qualified Text.Blaze.Html as Blaze+import qualified Text.Blaze.Html.Renderer.Text as Blaze+++--------------------------------------------------------------------------------+data MandrillError = MandrillError {+    _merr_status :: !T.Text+  , _merr_code :: !Int+  , _merr_name :: !T.Text+  , _merr_message :: !T.Text+  } deriving Show++makeLenses ''MandrillError+deriveJSON defaultOptions { fieldLabelModifier = drop 6 } ''MandrillError+++--------------------------------------------------------------------------------+data MandrillEmailStatus = ES_Sent+                         | ES_Queued+                         | ES_Scheduled+                         | ES_Rejected+                         | ES_Invalid deriving Show++deriveJSON defaultOptions { constructorTagModifier = map toLower . drop 3 } ''MandrillEmailStatus+++--------------------------------------------------------------------------------+data MandrillRejectReason = RR_HardBounce+                          | RR_SoftBounce+                          | RR_Spam+                          | RR_Unsub+                          | RR_Custom+                          | RR_InvalidSender+                          | RR_Invalid+                          | RR_TestModeLimit+                          | RR_Rule deriving Show++deriveJSON defaultOptions {+  constructorTagModifier = modRejectReason . drop 3+  } ''MandrillRejectReason+++--------------------------------------------------------------------------------+-- | The main datatypes which models the response from the Mandrill API,+-- which can be either a success or a failure.+data MandrillResponse k =+    MandrillSuccess k+  | MandrillFailure MandrillError deriving Show++instance FromJSON k => FromJSON (MandrillResponse k) where+  parseJSON v = case (parseMaybe parseJSON v) :: Maybe k of+    Just r -> return $ MandrillSuccess r+    Nothing -> do+    -- try to parse it as an error+      case (parseMaybe parseJSON v) :: Maybe MandrillError of+        Just e -> return $ MandrillFailure e+        Nothing -> fail $ show v <> " is neither a MandrillSuccess or a MandrillError."+++--------------------------------------------------------------------------------+data MandrillRecipientTag = To | Cc | Bcc deriving Show++deriveJSON defaultOptions { constructorTagModifier = map toLower } ''MandrillRecipientTag+++--------------------------------------------------------------------------------+-- | An array of recipient information.+data MandrillRecipient = MandrillRecipient {+    _mrec_email :: EmailAddress+    -- ^ The email address of the recipient+  , _mrec_name :: Maybe T.Text+    -- ^ The optional display name to use for the recipient+  , _mrec_type :: Maybe MandrillRecipientTag+    -- ^ The header type to use for the recipient.+    --   defaults to "to" if not provided+  } deriving Show++makeLenses ''MandrillRecipient+deriveJSON defaultOptions { fieldLabelModifier = drop 6 } ''MandrillRecipient++newRecipient :: EmailAddress -> MandrillRecipient+newRecipient email = MandrillRecipient email Nothing Nothing++instance Arbitrary MandrillRecipient where+  arbitrary = pure MandrillRecipient {+      _mrec_email = fromJust (emailAddress "test@example.com")+    , _mrec_name  =  Nothing+    , _mrec_type  =  Nothing+    }++--------------------------------------------------------------------------------+newtype MandrillHtml = MandrillHtml Blaze.Html++unsafeMkMandrillHtml :: T.Text -> MandrillHtml+unsafeMkMandrillHtml = MandrillHtml . Blaze.preEscapedToHtml++-- This might be slightly hairy because it violates+-- the nice encapsulation that newtypes offer.+mkMandrillHtml :: Blaze.Html -> MandrillHtml+mkMandrillHtml = MandrillHtml++instance Monoid MandrillHtml where+  mempty = MandrillHtml mempty+  mappend (MandrillHtml m1) (MandrillHtml m2) = MandrillHtml (m1 <> m2)++instance Show MandrillHtml where+  show (MandrillHtml h) = show $ Blaze.renderHtml h++instance ToJSON MandrillHtml where+  toJSON (MandrillHtml h) = String . TL.toStrict . Blaze.renderHtml $ h++instance FromJSON MandrillHtml where+  parseJSON (String h) = return $ MandrillHtml (Blaze.toHtml h)+  parseJSON v = typeMismatch "Expecting a String for MandrillHtml" v++instance Arbitrary MandrillHtml where+  arbitrary = pure $ mkMandrillHtml "<p><b>FooBar</b></p>"++--------------------------------------------------------------------------------+type MandrillTags = T.Text+++--------------------------------------------------------------------------------+type MandrillHeaders = Value+++--------------------------------------------------------------------------------+type MandrillVars = Value+++--------------------------------------------------------------------------------+data MandrillMergeVars = MandrillMergeVars {+    _mmvr_rcpt :: !T.Text+  , _mmvr_vars :: [MandrillVars]+  } deriving Show++makeLenses ''MandrillMergeVars+deriveJSON defaultOptions { fieldLabelModifier = drop 6 } ''MandrillMergeVars++--------------------------------------------------------------------------------+data MandrillMetadata = MandrillMetadata {+    _mmdt_rcpt :: !T.Text+  , _mmdt_values :: MandrillVars+  } deriving Show++makeLenses ''MandrillMetadata+deriveJSON defaultOptions { fieldLabelModifier = drop 6 } ''MandrillMetadata+++newtype Base64ByteString = B64BS B.ByteString deriving Show++instance ToJSON Base64ByteString where+  toJSON (B64BS bs) = String . TL.decodeUtf8 . Base64.encode $ bs++instance FromJSON Base64ByteString where+  parseJSON (String v) = case Base64.decode (TL.encodeUtf8 v) of+    Left err -> fail err+    Right rs -> return $ B64BS rs+  parseJSON rest = typeMismatch "Base64ByteString must be a String." rest++--------------------------------------------------------------------------------+data MandrillWebContent = MandrillWebContent {+    _mwct_type :: !T.Text+  , _mwct_name :: !T.Text+    -- ^ [for images] the Content ID of the image+    -- - use <img src="cid:THIS_VALUE"> to reference the image+    -- in your HTML content+  , _mwct_content :: !Base64ByteString+  } deriving Show++makeLenses ''MandrillWebContent+deriveJSON defaultOptions { fieldLabelModifier = drop 6 } ''MandrillWebContent++--------------------------------------------------------------------------------+-- | The information on the message to send+data MandrillMessage = MandrillMessage {+   _mmsg_html :: MandrillHtml+   -- ^ The full HTML content to be sent+ , _mmsg_text :: Maybe T.Text+   -- ^ Optional full text content to be sent+ , _mmsg_subject :: !T.Text+   -- ^ The message subject+ , _mmsg_from_email :: EmailAddress+   -- ^ The sender email address+ , _mmsg_from_name :: Maybe T.Text+   -- ^ Optional from name to be used+ , _mmsg_to :: [MandrillRecipient]+   -- ^ A list of recipient information+ , _mmsg_headers :: MandrillHeaders+   -- ^ optional extra headers to add to the message (most headers are allowed)+ , _mmsg_important :: Maybe Bool+   -- ^ whether or not this message is important, and should be delivered ahead+   -- of non-important messages+ , _mmsg_track_opens :: Maybe Bool+   -- ^ whether or not to turn on open tracking for the message+ , _mmsg_track_clicks :: Maybe Bool+   -- ^ whether or not to turn on click tracking for the message+ , _mmsg_auto_text :: Maybe Bool+   -- ^ whether or not to automatically generate a text part for messages that are not given text+ , _mmsg_auto_html :: Maybe Bool+   -- ^ whether or not to automatically generate an HTML part for messages that are not given HTML+ , _mmsg_inline_css :: Maybe Bool+   -- ^ whether or not to automatically inline all CSS styles provided in the message HTML+   -- - only for HTML documents less than 256KB in size+ , _mmsg_url_strip_qs :: Maybe Bool+   -- ^ whether or not to strip the query string from URLs when aggregating tracked URL data+ , _mmsg_preserve_recipients :: Maybe Bool+   -- ^ whether or not to expose all recipients in to "To" header for each email+ , _mmsg_view_content_link :: Maybe Bool+   -- ^ set to false to remove content logging for sensitive emails+ , _mmsg_bcc_address :: Maybe T.Text+   -- ^ an optional address to receive an exact copy of each recipient's email+ , _mmsg_tracking_domain :: Maybe T.Text+   -- ^ a custom domain to use for tracking opens and clicks instead of mandrillapp.com+ , _mmsg_signing_domain :: Maybe Bool+   -- ^ a custom domain to use for SPF/DKIM signing instead of mandrill +   -- (for "via" or "on behalf of" in email clients)+ , _mmsg_return_path_domain :: Maybe Bool+   -- ^ a custom domain to use for the messages's return-path+ , _mmsg_merge :: Maybe Bool+   -- ^ whether to evaluate merge tags in the message.+   -- Will automatically be set to true if either merge_vars+   -- or global_merge_vars are provided.+ , _mmsg_global_merge_vars :: [MandrillVars]+   -- ^ global merge variables to use for all recipients. You can override these per recipient.+ , _mmsg_merge_vars :: [MandrillMergeVars]+   -- ^ per-recipient merge variables, which override global merge variables with the same name.+ , _mmsg_tags :: [MandrillTags]+   -- ^ an array of string to tag the message with. Stats are accumulated using tags,+   -- though we only store the first 100 we see, so this should not be unique+   -- or change frequently. Tags should be 50 characters or less.+   -- Any tags starting with an underscore are reserved for internal use+   -- and will cause errors.+ , _mmsg_subaccount :: Maybe T.Text+   -- ^ the unique id of a subaccount for this message+   -- - must already exist or will fail with an error+ , _mmsg_google_analytics_domains :: [T.Text]+   -- ^ an array of strings indicating for which any matching URLs+   -- will automatically have Google Analytics parameters appended+   -- to their query string automatically.+ , _mmsg_google_analytics_campaign :: Maybe T.Text+   -- ^ optional string indicating the value to set for the utm_campaign+   -- tracking parameter. If this isn't provided the email's from address+   -- will be used instead.+ , _mmsg_metadata :: MandrillVars+   -- ^ metadata an associative array of user metadata. Mandrill will store+   -- this metadata and make it available for retrieval.+   -- In addition, you can select up to 10 metadata fields to index+   -- and make searchable using the Mandrill search api.+ , _mmsg_recipient_metadata :: [MandrillMetadata]+   -- ^ Per-recipient metadata that will override the global values+   -- specified in the metadata parameter.+ , _mmsg_attachments :: [MandrillWebContent]+   -- ^ an array of supported attachments to add to the message+ , _mmsg_images :: [MandrillWebContent]+   -- ^ an array of embedded images to add to the message+ } deriving Show++makeLenses ''MandrillMessage+deriveJSON defaultOptions { fieldLabelModifier = drop 6 } ''MandrillMessage++instance Arbitrary MandrillMessage where+  arbitrary = MandrillMessage <$> arbitrary+                              <*> pure Nothing+                              <*> pure "Test Subject"+                              <*> pure (fromJust $ emailAddress "sender@example.com")+                              <*> pure Nothing+                              <*> resize 2 arbitrary+                              <*> pure emptyObject+                              <*> pure Nothing+                              <*> pure Nothing+                              <*> pure Nothing+                              <*> pure Nothing+                              <*> pure Nothing+                              <*> pure Nothing+                              <*> pure Nothing+                              <*> pure Nothing+                              <*> pure Nothing+                              <*> pure Nothing+                              <*> pure Nothing+                              <*> pure Nothing+                              <*> pure Nothing+                              <*> pure Nothing+                              <*> pure []+                              <*> pure []+                              <*> pure []+                              <*> pure Nothing+                              <*> pure []+                              <*> pure Nothing+                              <*> pure emptyObject+                              <*> pure []+                              <*> pure []+                              <*> pure []++--------------------------------------------------------------------------------+type MandrillKey = T.Text++newtype MandrillDate = MandrillDate {+  fromMandrillDate :: UTCTime+  } deriving Show++instance ToJSON MandrillDate where+  toJSON = toJSON . fromMandrillDate++instance FromJSON MandrillDate where+  parseJSON = withText "MandrillDate" $ \t ->+      case parseTime defaultTimeLocale "%Y-%m-%d %I:%M:%S%Q" (T.unpack t) of+        Just d -> pure $ MandrillDate d+        _      -> fail "could not parse Mandrill date"
+ src/Network/API/Mandrill/Users.hs view
@@ -0,0 +1,32 @@++module Network.API.Mandrill.Users where++import           Network.API.Mandrill.Settings+import           Network.API.Mandrill.HTTP+import           Network.API.Mandrill.Types+import           Network.API.Mandrill.Users.Types+import           Data.Aeson+++--------------------------------------------------------------------------------+-- | Return the information about the API-connected user+info :: MandrillKey -> IO (MandrillResponse UsersInfoResponse)+info key = toMandrillResponse UsersInfo (UsersRq key)+++--------------------------------------------------------------------------------+-- | Validate an API key and respond to a ping+ping :: MandrillKey -> IO (MandrillResponse UsersPingResponse)+ping _ = fail "users/ping.json doesn't return valid JSON, thus is not implemented yet."+++--------------------------------------------------------------------------------+-- | Validate an API key and respond to a ping (anal JSON parser version)+ping2 :: MandrillKey -> IO (MandrillResponse UsersPing2Response)+ping2 key = toMandrillResponse UsersPing2 (UsersRq key)+++--------------------------------------------------------------------------------+-- | Return the senders that have tried to use this account, both verified and unverified+senders :: MandrillKey -> IO (MandrillResponse [UsersSendersResponse])+senders key = toMandrillResponse UsersSenders (UsersRq key)
+ src/Network/API/Mandrill/Users/Types.hs view
@@ -0,0 +1,122 @@+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE OverloadedStrings #-}+module Network.API.Mandrill.Users.Types where++import           Data.Char+import           Data.Time+import qualified Data.Text as T+import           Control.Lens+import           Control.Monad+import           Data.Monoid+import           Data.Aeson+import           Data.Aeson.Types+import           Data.Aeson.TH++import           Network.API.Mandrill.Types+++--------------------------------------------------------------------------------+data UsersRq = UsersRq {+    _ureq_key :: !MandrillKey+  } deriving Show++makeLenses ''UsersRq+deriveJSON defaultOptions { fieldLabelModifier = drop 6 } ''UsersRq++--------------------------------------------------------------------------------+data MandrillStats = MandrillStats {+    _msts_sent :: Int+  , _msts_hard_bounces :: Int+  , _msts_soft_bounces :: Int+  , _msts_rejects :: Int+  , _msts_complaints :: Int+  , _msts_unsubs :: Int+  , _msts_opens :: Int+  , _msts_unique_opens :: Int+  , _msts_clicks :: Int+  , _msts_unique_clicks :: Int+  } deriving Show++makeLenses ''MandrillStats+deriveJSON defaultOptions { fieldLabelModifier = drop 6 } ''MandrillStats++--------------------------------------------------------------------------------+data UserStats = UserStats {+    _usts_today :: MandrillStats+  , _usts_last_7_days :: MandrillStats+  , _usts_last_30_days :: MandrillStats+  , _usts_last_60_days :: MandrillStats+  , _usts_last_90_days :: MandrillStats+  , _usts_all_time :: MandrillStats+  } deriving Show++makeLenses ''UserStats+deriveJSON defaultOptions { fieldLabelModifier = drop 6 } ''UserStats++--------------------------------------------------------------------------------+data UsersInfoResponse = UsersInfoResponse {+    _usir_username :: !T.Text+  , _usir_created_at :: MandrillDate+  , _usir_public_id :: !T.Text+  , _usir_reputation :: !Int+  , _usir_hourly_quota :: !Int+  , _usir_backlog :: !Int+  , _usir_stats :: UserStats+  } deriving Show++makeLenses ''UsersInfoResponse+deriveJSON defaultOptions { fieldLabelModifier = drop 6 } ''UsersInfoResponse+++--------------------------------------------------------------------------------+newtype UsersPingResponse = UsersPingResponse T.Text deriving Show++deriveFromJSON defaultOptions ''UsersPingResponse++instance ToJSON UsersPingResponse where+  toJSON (UsersPingResponse t) = String t+++--------------------------------------------------------------------------------+data UsersPing2Response = UsersPing2Response {+    _usrr_PING :: T.Text+  } deriving Show++deriveJSON defaultOptions { fieldLabelModifier = drop 6 } ''UsersPing2Response++++--------------------------------------------------------------------------------+data UsersSendersResponse = UsersResponse {+    _usrr_address :: !T.Text+    -- ^ The sender's email address+  , _usrr_created_at :: MandrillDate+    -- ^ The date and time that the sender was first seen by Mandrill +    -- as a UTC date string in YYYY-MM-DD HH:MM:SS format+  , _usrr_sent :: !Int+    -- ^ The total number of messages sent by this sender+  , _usrr_hard_bounces :: !Int+    -- ^ The total number of hard bounces by messages by this sender+  , _usrr_soft_bounces :: !Int+    -- ^ The total number of soft bounces by messages by this sender+  , _usrr_rejects :: !Int+    -- ^ The total number of rejected messages by this sender+  , _usrr_complaints :: !Int+    -- ^ The total number of spam complaints received+    -- for messages by this sender+  , _usrr_unsubs :: !Int+    -- ^ The total number of unsubscribe requests received+    -- for messages by this sender+  , _usrr_opens :: !Int+    -- ^ The total number of times messages by this sender have been opened+  , _usrr_clicks :: !Int+    -- ^ The total number of times tracked URLs in messages+    -- by this sender have been clicked+  , _usrr_unique_opens :: !Int+    -- ^ The number of unique opens for emails sent for this sender+  , _usrr_unique_clicks :: !Int+    -- ^ The number of unique clicks for emails sent for this sender+  } deriving Show++makeLenses ''UsersSendersResponse+deriveJSON defaultOptions { fieldLabelModifier = drop 6 } ''UsersSendersResponse
+ src/Network/API/Mandrill/Utils.hs view
@@ -0,0 +1,17 @@++module Network.API.Mandrill.Utils where++import Data.Char++--------------------------------------------------------------------------------+-- | Turns camelCase strings into suitable dashified ones.+-- >>> modRejectReason "TestModeLimit"+--     "test-mode-limit"+modRejectReason :: String -> String+modRejectReason [] = []+modRejectReason (x:xs) = toLower x : go xs+  where+    go [] = []+    go (y:ys) = case isUpper y of+      False -> y : go ys+      True  -> '-' : toLower y : go ys
+ test/Main.hs view
@@ -0,0 +1,43 @@+{-# LANGUAGE OverloadedStrings #-}+module Main where++import           System.Environment+import           Data.Monoid+import           Tests+import           Online+import           Test.Tasty+import           Test.Tasty.HUnit+import           Test.Tasty.QuickCheck+import qualified Data.Text as T++----------------------------------------------------------------------+withQuickCheckDepth :: TestName -> Int -> [TestTree] -> TestTree+withQuickCheckDepth tn depth tests =+  localOption (QuickCheckTests depth) (testGroup tn tests)++----------------------------------------------------------------------+onlineTestsIfEnabled :: IO [TestTree]+onlineTestsIfEnabled = do+  apiKey <- (return . fmap T.pack) =<< lookupEnv "MANDRILL_API_KEY"+  case apiKey of+    Nothing -> return []+    Just k  -> return [+                 testGroup "Mandrill online tests" [+                   testCase "users/info.json" (testOnlineUsersInfo k)+                 , testCase "users/ping2.json" (testOnlineUsersPing2 k)+                 , testCase "users/senders.json" (testOnlineUsersSenders k)+                 , testCase "messages/send.json" (testOnlineMessagesSend k)+                 ]]++----------------------------------------------------------------------+main :: IO ()+main = do+  onlineTests <- onlineTestsIfEnabled+  defaultMainWithIngredients defaultIngredients $+    testGroup "Mandrill tests" $ onlineTests <> [+         testGroup "Mandrill offline tests" [+           testCase "users/info.json API parsing"    testUsersInfo+         , testCase "users/senders.json API parsing" testUsersSenders+         , testCase "messages/send.json API parsing" testMessagesSend+         ]+     ]
+ test/Online.hs view
@@ -0,0 +1,52 @@+{-# LANGUAGE OverloadedStrings #-}+module Online where++import           Test.QuickCheck+import           Test.Tasty.HUnit+import           Text.RawString.QQ+import           Data.Either+import           Data.Aeson+import           Network.API.Mandrill.Messages.Types+import           Network.API.Mandrill.Users.Types+import qualified Data.ByteString.Char8 as C8+import           RawData++import           Network.API.Mandrill.Types+import qualified Network.API.Mandrill.Messages as API+import qualified Network.API.Mandrill.Users as API++--+-- Users calls+--+testOnlineUsersInfo :: MandrillKey -> Assertion+testOnlineUsersInfo k = do+  res <- API.info k+  case res of+    MandrillSuccess _ -> return ()+    MandrillFailure e -> fail $ "users/info.json " ++ show e++testOnlineUsersPing2 :: MandrillKey -> Assertion+testOnlineUsersPing2 k = do+  res <- API.ping2 k+  case res of+    MandrillSuccess _ -> return ()+    MandrillFailure e -> fail $ "users/ping2.json " ++ show e++testOnlineUsersSenders :: MandrillKey -> Assertion+testOnlineUsersSenders k = do+  res <- API.senders k+  case res of+    MandrillSuccess _ -> return ()+    MandrillFailure e -> fail $ "users/senders.json " ++ show e+++--+-- Messages calls+--+testOnlineMessagesSend :: MandrillKey -> Assertion+testOnlineMessagesSend k = do+  msg <- generate arbitrary+  res <- API.send k msg Nothing Nothing Nothing+  case res of+    MandrillSuccess _ -> return ()+    MandrillFailure e -> fail $ "messages/send.json " ++ show e
+ test/RawData.hs view
@@ -0,0 +1,202 @@+{-# LANGUAGE QuasiQuotes #-}+module RawData where++import Test.QuickCheck+import Test.Tasty.HUnit+import Text.RawString.QQ+import Data.Either++usersInfoData :: String+usersInfoData = [r|+{+    "username": "foo@bar.com",+    "created_at": "2014-03-27 09:42:20.62599",+    "public_id": "barbaz",+    "reputation": 99,+    "hourly_quota": 1094,+    "backlog": 0,+    "stats": {+        "today": {+            "sent": 102,+            "hard_bounces": 0,+            "soft_bounces": 0,+            "rejects": 0,+            "complaints": 0,+            "unsubs": 0,+            "opens": 71,+            "unique_opens": 52,+            "clicks": 25,+            "unique_clicks": 19+        },+        "last_7_days": {+            "sent": 396,+            "hard_bounces": 0,+            "soft_bounces": 1,+            "rejects": 0,+            "complaints": 0,+            "unsubs": 0,+            "opens": 321,+            "unique_opens": 209,+            "clicks": 147,+            "unique_clicks": 84+        },+        "last_30_days": {+            "sent": 3022,+            "hard_bounces": 3,+            "soft_bounces": 4,+            "rejects": 0,+            "complaints": 0,+            "unsubs": 0,+            "opens": 2408,+            "unique_opens": 1540,+            "clicks": 958,+            "unique_clicks": 578+        },+        "last_60_days": {+            "sent": 6120,+            "hard_bounces": 5,+            "soft_bounces": 4,+            "rejects": 0,+            "complaints": 0,+            "unsubs": 0,+            "opens": 4869,+            "unique_opens": 3112,+            "clicks": 2004,+            "unique_clicks": 1223+        },+        "last_90_days": {+            "sent": 9267,+            "hard_bounces": 6,+            "soft_bounces": 4,+            "rejects": 1,+            "complaints": 1,+            "unsubs": 0,+            "opens": 7428,+            "unique_opens": 4772,+            "clicks": 2943,+            "unique_clicks": 1802+        },+        "all_time": {+            "sent": 15278,+            "hard_bounces": 7,+            "soft_bounces": 7,+            "rejects": 1,+            "complaints": 1,+            "unsubs": 0,+            "opens": 12782,+            "unique_opens": 7926,+            "clicks": 5336,+            "unique_clicks": 3341+        }+    }+}+|]++usersSendersData :: String+usersSendersData = [r|+{+    "address": "sender.example@mandrillapp.com",+    "created_at": "2014-08-12 20:49:42.8344",+    "sent": 42,+    "hard_bounces": 42,+    "soft_bounces": 42,+    "rejects": 42,+    "complaints": 42,+    "unsubs": 42,+    "opens": 42,+    "clicks": 42,+    "unique_opens": 42,+    "unique_clicks": 42+}+|]++sendData :: String+sendData = [r|+{+    "key": "example key",+    "message": {+        "html": "<p>Example HTML content</p>",+        "text": "Example text content",+        "subject": "example subject",+        "from_email": "message.from_email@example.com",+        "from_name": "Example Name",+        "to": [+            {+                "email": "recipient.email@example.com",+                "name": "Recipient Name",+                "type": "to"+            }+        ],+        "headers": {+            "Reply-To": "message.reply@example.com"+        },+        "important": false,+        "track_opens": null,+        "track_clicks": null,+        "auto_text": null,+        "auto_html": null,+        "inline_css": null,+        "url_strip_qs": null,+        "preserve_recipients": null,+        "view_content_link": null,+        "bcc_address": "message.bcc_address@example.com",+        "tracking_domain": null,+        "signing_domain": null,+        "return_path_domain": null,+        "merge": true,+        "global_merge_vars": [+            {+                "name": "merge1",+                "content": "merge1 content"+            }+        ],+        "merge_vars": [+            {+                "rcpt": "recipient.email@example.com",+                "vars": [+                    {+                        "name": "merge2",+                        "content": "merge2 content"+                    }+                ]+            }+        ],+        "tags": [+            "password-resets"+        ],+        "subaccount": "customer-123",+        "google_analytics_domains": [+            "example.com"+        ],+        "google_analytics_campaign": "message.from_email@example.com",+        "metadata": {+            "website": "www.example.com"+        },+        "recipient_metadata": [+            {+                "rcpt": "recipient.email@example.com",+                "values": {+                    "user_id": 123456+                }+            }+        ],+        "attachments": [+            {+                "type": "text/plain",+                "name": "myfile.txt",+                "content": "ZXhhbXBsZSBmaWxl"+            }+        ],+        "images": [+            {+                "type": "image/png",+                "name": "IMAGECID",+                "content": "ZXhhbXBsZSBmaWxl"+            }+        ]+    },+    "async": false,+    "ip_pool": "Main Pool",+    "send_at": "2014-08-17T14:23:02.954Z"+}+|]
+ test/Tests.hs view
@@ -0,0 +1,42 @@+{-# LANGUAGE CPP #-}+module Tests where++import Test.Tasty.HUnit+import Data.Either+import Data.Aeson+import Network.API.Mandrill.Messages.Types+import Network.API.Mandrill.Users.Types+import qualified Data.ByteString.Char8 as C8+import RawData++#if defined(__GLASGOW_HASKELL__) && __GLASGOW_HASKELL__ <= 706+isRight :: Either a b -> Bool+isRight (Left _) = False+isRight _ = True+#endif++testMessagesSend :: Assertion+testMessagesSend = +  assertBool ("send.json: Parsing failed! " ++ show parsePayload)+             (isRight parsePayload)+  where+    parsePayload :: Either String MessagesSendRq+    parsePayload = eitherDecodeStrict . C8.pack $ sendData++testUsersInfo :: Assertion+testUsersInfo = do+  let res = parsePayload+  assertBool ("users/info.json (response): Parsing failed: " ++ show res)+             (isRight parsePayload)+  where+    parsePayload :: Either String UsersInfoResponse+    parsePayload = eitherDecodeStrict . C8.pack $ usersInfoData++testUsersSenders :: Assertion+testUsersSenders = do+  let res = parsePayload+  assertBool ("users/senders.json (response): Parsing failed: " ++ show res)+             (isRight parsePayload)+  where+    parsePayload :: Either String UsersSendersResponse+    parsePayload = eitherDecodeStrict . C8.pack $ usersSendersData