module Aws.Ses.Core
( SesError(..)
, SesMetadata(..)
, SesConfiguration(..)
, sesEuWest1
, sesUsEast
, sesUsEast1
, sesUsWest2
, sesHttpsGet
, sesHttpsPost
, sesSignQuery
, sesResponseConsumer
, RawMessage(..)
, Destination(..)
, EmailAddress
, Sender(..)
, sesAsQuery
) where
import Aws.Core
import qualified Blaze.ByteString.Builder as Blaze
import qualified Blaze.ByteString.Builder.Char8 as Blaze8
import qualified Control.Exception as C
import Control.Monad (mplus)
import Control.Monad.Trans.Resource (throwM)
import qualified Data.ByteString as B
import qualified Data.ByteString.Base64 as B64
import Data.ByteString.Char8 ()
import Data.IORef
import Data.Maybe
import Data.Monoid
import Data.Text (Text)
import qualified Data.Text.Encoding as TE
import Data.Typeable
import Prelude
import qualified Network.HTTP.Conduit as HTTP
import qualified Network.HTTP.Types as HTTP
import Text.XML.Cursor (($/), ($//))
import qualified Text.XML.Cursor as Cu
data SesError
= SesError {
sesStatusCode :: HTTP.Status
, sesErrorCode :: Text
, sesErrorMessage :: Text
}
deriving (Show, Typeable)
instance C.Exception SesError
data SesMetadata
= SesMetadata {
requestId :: Maybe Text
}
deriving (Show, Typeable)
instance Loggable SesMetadata where
toLogText (SesMetadata rid) = "SES: request ID=" `mappend` fromMaybe "<none>" rid
instance Monoid SesMetadata where
mempty = SesMetadata Nothing
SesMetadata r1 `mappend` SesMetadata r2 = SesMetadata (r1 `mplus` r2)
data SesConfiguration qt
= SesConfiguration {
sesiHttpMethod :: Method
, sesiHost :: B.ByteString
}
deriving (Show)
instance DefaultServiceConfiguration (SesConfiguration NormalQuery) where
defServiceConfig = sesHttpsPost sesUsEast1
instance DefaultServiceConfiguration (SesConfiguration UriOnlyQuery) where
defServiceConfig = sesHttpsGet sesUsEast1
sesEuWest1 :: B.ByteString
sesEuWest1 = "email.eu-west-1.amazonaws.com"
sesUsEast :: B.ByteString
sesUsEast = sesUsEast1
sesUsEast1 :: B.ByteString
sesUsEast1 = "email.us-east-1.amazonaws.com"
sesUsWest2 :: B.ByteString
sesUsWest2 = "email.us-west-2.amazonaws.com"
sesHttpsGet :: B.ByteString -> SesConfiguration qt
sesHttpsGet endpoint = SesConfiguration Get endpoint
sesHttpsPost :: B.ByteString -> SesConfiguration NormalQuery
sesHttpsPost endpoint = SesConfiguration PostQuery endpoint
sesSignQuery :: [(B.ByteString, B.ByteString)] -> SesConfiguration qt -> SignatureData -> SignedQuery
sesSignQuery query si sd
= SignedQuery {
sqMethod = sesiHttpMethod si
, sqProtocol = HTTPS
, sqHost = sesiHost si
, sqPort = defaultPort HTTPS
, sqPath = "/"
, sqQuery = HTTP.simpleQueryToQuery query'
, sqDate = Just $ signatureTime sd
, sqAuthorization = Nothing
, sqContentType = Nothing
, sqContentMd5 = Nothing
, sqAmzHeaders = amzHeaders
, sqOtherHeaders = []
, sqBody = Nothing
, sqStringToSign = stringToSign
}
where
stringToSign = fmtRfc822Time (signatureTime sd)
credentials = signatureCredentials sd
accessKeyId = accessKeyID credentials
amzHeaders = catMaybes
[ Just ("X-Amzn-Authorization", authorization)
, ("x-amz-security-token",) `fmap` iamToken credentials
]
authorization = B.concat
[ "AWS3-HTTPS AWSAccessKeyId="
, accessKeyId
, ", Algorithm=HmacSHA256, Signature="
, signature credentials HmacSHA256 stringToSign
]
query' = ("AWSAccessKeyId", accessKeyId) : query
sesResponseConsumer :: (Cu.Cursor -> Response SesMetadata a)
-> IORef SesMetadata
-> HTTPResponseConsumer a
sesResponseConsumer inner metadataRef resp = xmlCursorConsumer parse metadataRef resp
where
parse cursor = do
let requestId' = listToMaybe $ cursor $// elContent "RequestID"
tellMetadata $ SesMetadata requestId'
case cursor $/ Cu.laxElement "Error" of
[] -> inner cursor
(err:_) -> fromError err
fromError cursor = do
errCode <- force "Missing Error Code" $ cursor $// elContent "Code"
errMessage <- force "Missing Error Message" $ cursor $// elContent "Message"
throwM $ SesError (HTTP.responseStatus resp) errCode errMessage
class SesAsQuery a where
sesAsQuery :: a -> [(B.ByteString, B.ByteString)]
instance SesAsQuery a => SesAsQuery (Maybe a) where
sesAsQuery = maybe [] sesAsQuery
data RawMessage = RawMessage { rawMessageData :: B.ByteString }
deriving (Eq, Ord, Show, Typeable)
instance SesAsQuery RawMessage where
sesAsQuery = (:[]) . (,) "RawMessage.Data" . B64.encode . rawMessageData
data Destination =
Destination
{ destinationBccAddresses :: [EmailAddress]
, destinationCcAddresses :: [EmailAddress]
, destinationToAddresses :: [EmailAddress]
} deriving (Eq, Ord, Show, Typeable)
instance SesAsQuery Destination where
sesAsQuery (Destination bcc cc to) = concat [ go (s "Bcc") bcc
, go (s "Cc") cc
, go (s "To") to ]
where
go kind = zipWith f (map Blaze8.fromShow [one..])
where txt = kind `mappend` s "Addresses.member."
f n v = ( Blaze.toByteString (txt `mappend` n)
, TE.encodeUtf8 v )
s = Blaze.fromByteString
one = 1 :: Int
instance Monoid Destination where
mempty = Destination [] [] []
mappend (Destination a1 a2 a3) (Destination b1 b2 b3) =
Destination (a1 ++ b1) (a2 ++ b2) (a3 ++ b3)
type EmailAddress = Text
data Sender = Sender { senderAddress :: EmailAddress }
deriving (Eq, Ord, Show, Typeable)
instance SesAsQuery Sender where
sesAsQuery = (:[]) . (,) "Source" . TE.encodeUtf8 . senderAddress