smtp-mail-ng-0.1.0.0: Network/Mail/SMTP/SMTP.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-|
Description: definition of the SMTP monad and its terms.
-}
module Network.Mail.SMTP.SMTP (
SMTP
, smtp
, command
, bytes
, expect
, expectCode
, SMTPContext
, smtpContext
, getSMTPServerHostName
, getSMTPClientHostName
, startTLS
, SMTPError(..)
) where
import Control.Exception
import Control.Monad
import Control.Applicative
import Control.Monad.IO.Class
import Control.Monad.Trans.Class
import Control.Monad.Trans.Except
import Control.Monad.Trans.State
import Network
import Network.BSD
import Network.Mail.SMTP.Types
import Network.Mail.SMTP.ReplyLine
import Network.Mail.SMTP.SMTPRaw
import Network.Mail.SMTP.SMTPParameters
-- STARTTLS support demands some TLS- and X.509-related definitions.
import Network.TLS
import Network.TLS.Extra.Cipher (ciphersuite_all)
import System.X509 (getSystemCertificateStore)
import Data.X509.CertificateStore (CertificateStore)
import Data.ByteString.Char8 (pack)
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as BL
import Crypto.Random
import System.IO
-- | An SMTP client EDSL: it can do effects, things can go wrong, and
-- it carries state.
newtype SMTP a = SMTP {
runSMTP :: ExceptT SMTPError (StateT SMTPContext IO) a
} deriving (Functor, Applicative, Monad, MonadIO)
-- | Run an expression in the SMTP monad.
-- Should be exception safe, but I am not confident in this.
smtp :: SMTPParameters -> SMTP a -> IO (Either SMTPError a)
smtp smtpParameters smtpValue = do
smtpContext <- makeSMTPContext smtpParameters
case smtpContext of
Left err -> return $ Left err
Right smtpContext -> do
x <- evalStateT (runExceptT (runSMTP smtpValue)) smtpContext
closeSMTPContext smtpContext
return x
-- | Attempt to make an SMTPContext.
makeSMTPContext :: SMTPParameters -> IO (Either SMTPError SMTPContext)
makeSMTPContext smtpParameters = do
clientHostname <- getHostName
result <- liftIO $ try (smtpConnect serverHostname (fromIntegral port))
return $ case result :: Either SomeException (SMTPRaw, Maybe Greeting) of
Left err -> Left ConnectionFailure
Right (smtpRaw, _) -> Right $ SMTPContext smtpRaw serverHostname clientHostname debug
where
serverHostname = smtpHost smtpParameters
port = smtpPort smtpParameters
debug = if smtpVerbose smtpParameters
then putStrLn
else const (return ())
-- | Attempt to close an SMTPContext, freeing its resource.
closeSMTPContext :: SMTPContext -> IO ()
closeSMTPContext smtpContext = hClose (smtpHandle (smtpRaw smtpContext))
-- | Send a command, without waiting for the reply.
command :: Command -> SMTP ()
command cmd = SMTP $ do
ctxt <- lift get
liftIO $ (smtpDebug ctxt ("Send command: " ++ show (toByteString cmd)))
result <- liftIO $ try ((smtpSendCommand (smtpRaw ctxt) cmd))
case result :: Either SomeException () of
Left err -> throwE UnknownError
Right () -> return ()
-- | Send some bytes, with a crlf inserted at the end, without waiting for
-- the reply.
bytes :: B.ByteString -> SMTP ()
bytes bs = SMTP $ do
ctxt <- lift get
liftIO $ (smtpDebug ctxt ("Send bytes: " ++ show bs))
result <- liftIO $ try ((smtpSendRaw (smtpRaw ctxt) (B.append bs crlf)))
case result :: Either SomeException () of
Left err -> throwE UnknownError
Right () -> return ()
where
crlf = pack "\r\n"
-- | Pull a response from the server, passing it through a function which
-- checks that it's an expected response. If the response doesn't parse as
-- an SMTP response, we give an UnexpectedResponse.
expect :: ([ReplyLine] -> Maybe SMTPError) -> SMTP ()
expect ok = SMTP $ do
ctxt <- lift get
let smtpraw = smtpRaw ctxt
reply <- liftIO $ smtpGetReplyLines smtpraw
liftIO $ (smtpDebug ctxt ("Receive response: " ++ show reply))
case reply of
Nothing -> throwE UnexpectedResponse
Just reply -> case ok reply of
Just err -> throwE err
Nothing -> return ()
-- | Like expect, but you give only the ReplyCode that is expected. Any other
-- reply code, or an unexpected reponse, is considered a failure.
expectCode :: ReplyCode -> SMTP ()
expectCode code = expect hasCode
where
hasCode [] = Just UnexpectedResponse
hasCode (reply : _) =
if replyCode reply == code
then Nothing
else Just UnexpectedResponse
-- | Grab the SMTPContext.
smtpContext :: SMTP SMTPContext
smtpContext = SMTP $ lift get
-- | Try to get TLS going on an SMTP connection.
startTLS :: SMTP ()
startTLS = do
context <- tlsContext
command STARTTLS
expectCode 220
tlsUpgrade context
ctxt <- smtpContext
command (EHLO (pack $ getSMTPClientHostName ctxt))
expectCode 250
-- | Attempt to create a TLS context.
tlsContext :: SMTP Context
tlsContext = SMTP $ do
ctxt <- lift get
tlsContext <- liftIO $ try (makeTLSContext (getSMTPHandle ctxt) (getSMTPServerHostName ctxt))
case tlsContext :: Either SomeException Context of
Left err -> throwE EncryptionError
Right context -> return context
-- | Upgrade to TLS. If the handshake is successful, the underlying SMTPRaw
-- will be updated so that it pushes/pulls using the TLS context rather
-- than raw Handle.
tlsUpgrade :: Context -> SMTP ()
tlsUpgrade context = SMTP $ do
result <- liftIO $ try (handshake context)
case result :: Either SomeException () of
Left err -> throwE EncryptionError
Right () -> do
-- Now that we have upgraded, we must change the means by which we push
-- and pull data to and from the pipe; we must use the TLS library's
-- sendData and recvData
let push = sendData context . BL.fromStrict
let pull = recvData context
let close = contextClose context
lift $ modify (\ctx ->
ctx { smtpRaw = SMTPRaw push pull close (smtpHandle (smtpRaw ctx)) }
)
-- | Get a TLS context on a given Handle against a given HostName.
-- We choose a big cipher suite, the SystemRNG, and the system certificate
-- store. Hopefully this will work most of the time.
-- This action may throw an exception. We are careful to handle it and
-- coerce it into a first-class value.
makeTLSContext :: Handle -> HostName -> IO Context
makeTLSContext handle hostname = do
-- Grab a random number generator.
rng <- (createEntropyPool >>= return . cprgCreate) :: IO SystemRNG
-- Find the certificate store. No error reporting if we can't find it; you'll
-- just (probably) get an error later when the TLS handshake fails due to
-- an unknown CA.
certStore <- getSystemCertificateStore
let params = tlsClientParams hostname certStore
contextNew handle params rng
-- | ClientParams are a slight variation on the default: we throw in a given
-- certificate store and widen the supported ciphers.
tlsClientParams :: HostName -> CertificateStore -> ClientParams
tlsClientParams hostname certStore = dflt {
clientSupported = supported
, clientShared = shared
}
where
dflt = defaultParamsClient hostname ""
shared = (clientShared dflt) { sharedCAStore = certStore }
supported = (clientSupported dflt) { supportedCiphers = ciphersuite_all }
-- | Description of an error in the SMTP monad evaluation.
data SMTPError
= UnexpectedResponse
| ConnectionFailure
| EncryptionError
| UnknownError
deriving (Show, Eq)
-- | Description of the state which an SMTP term needs in order to be
-- evaluated.
data SMTPContext = SMTPContext {
smtpRaw :: SMTPRaw
-- ^ Necessary for push/pull to/from some reasource (probably a Handle).
, smtpServerHostName :: HostName
, smtpClientHostName :: HostName
, smtpDebug :: String -> IO ()
-- ^ A pipe into which we can throw debug messages. const (return ()) for
-- squelching, or maybe putStrLn for verbosity.
}
-- | Access the Handle datatype buried beneath the SMTP abstraction.
-- Use with care!
getSMTPHandle :: SMTPContext -> Handle
getSMTPHandle = smtpHandle . smtpRaw
getSMTPServerHostName :: SMTPContext -> HostName
getSMTPServerHostName = smtpServerHostName
getSMTPClientHostName :: SMTPContext -> HostName
getSMTPClientHostName = smtpClientHostName