123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487 |
- {-# LANGUAGE OverloadedStrings, TypeFamilies, MultiParamTypeClasses, FlexibleInstances, UndecidableInstances, RankNTypes #-}
- module Network.FCMTP.SendingState (
- Sending,
- inSendingConnection,
- inSendingSession,
- inSendingCtx,
- peelSendingCtx,
- startSending,
- startSession,
- failures,
- extensions,
- quitCmd,
- rsetCmd,
- mailCmd,
- dataCmd,
- startData,
- dataChunk,
- rcptCmd
- ) where
- -- That's another StateT that encapsulates a Stream and sending data
- -- Only a bit more complicated, because every command leads to a response
- -- that may be treated in batch or individually according to the PIPELINING
- -- or XTUNNEL extension.
- import qualified Data.FCMTP.Account as Ac
- import qualified Data.FCMTP.Extension as Ex
- import qualified Data.FCMTP.Mime as Mime
- import System.IO (Handle)
- import qualified System.IO.Uniform as U
- import qualified System.IO.Uniform.Streamline as S
- import qualified Network.FCMTP.ClientError as CE
- import qualified Data.FCMTP.Response as Resp
- import Data.FCMTP.Response (Response, ResponseStatus(..))
- import Data.FCMTP.ResponseCode (toResponse, ResponseCode(..))
- import Control.Conditional
- import Control.Monad (ap, liftM)
- import Control.Monad.IO.Class
- import Control.Monad.Trans
- import Control.Monad.Trans.Interruptible
- import Control.Monad.Trans.Control
- import Control.Monad.Base
- import Control.Exception
- import Data.Maybe (isJust)
- import Data.Default.Class
- import qualified Data.Attoparsec.ByteString as A
- import qualified Data.ByteString as BS
- import qualified Data.ByteString.Lazy as LBS
- import Data.ByteString (ByteString)
- import qualified Data.ByteString.Lazy.Search as Search
- import Text.StringConvert
- import Debug.Trace
- chunkSize :: Int
- chunkSize = 100000 -- bytes - about 10 normal ethernet packages
- {- |
- How to chunk data for sending.
- filter text isLast = [(generatesAReply, sentText)]
- -}
- type SendingDataFilter = ByteString -> Bool -> [(Bool, ByteString)]
- data Data = Data {
- hostName :: ByteString, tlsSettings :: U.TlsSettings,
- getExtensions :: [Ex.Extension], activeRcpt :: [Ac.Account],
- failure :: [(Ac.Account, Response)],
- pendingCommands :: [PendingCommand], isPipelining :: Bool,
- dataFilter :: SendingDataFilter}
- newtype Sending m a = Sending {withSending :: Data -> S.Streamline m (a, Data)}
- -- Commands with pending replies, segregated by what failure means.
- data PendingCommand = DummyCommand | TotalyImportantCommand | CommandForRcpt Ac.Account
- dummyFilter :: SendingDataFilter
- dummyFilter dt _ = [(False, dt)]
- startSending :: MonadIO m => Maybe Handle -> Sending m ()
- startSending ec = do
- lifts $ S.echoTo ec
- handshake
- startSession :: MonadIO m => Sending m ()
- startSession = startSendingSession
- inSendingConnection :: MonadIO m => ToString host => host -> U.TlsSettings -> Sending m a -> S.Streamline m a
- inSendingConnection h set f = do
- (ret, _) <- withSending f' $ Data (s . toString $ h) set [] [] [] [] False dummyFilter
- return ret
- where
- f' = do
- handshake
- ret <- f
- quitCmd
- return ret
- startSendingSession :: MonadIO m => Sending m ()
- startSendingSession = Sending $ \st -> do
- let s' = st{activeRcpt=[]}{failure=[]}
- return ((), s')
- clearSendingSession :: MonadIO m => Sending m ()
- clearSendingSession = Sending $ \st -> do
- let s' = st{activeRcpt=[]}{failure=[]}
- return ((), s')
- inSendingSession :: MonadIO m => Sending m a -> Sending m (a, [(Ac.Account, Response)])
- inSendingSession f = do
- sync
- startSendingSession
- ret <- f
- rsetCmd
- sync
- ff <- failures
- clearSendingSession
- return (ret, ff)
- -- | Returns (success address, temporary failures, permanent failures)
- failures :: Monad m => Sending m [(Ac.Account, Response)]
- failures = Sending $ \st -> return (failure st, st)
- extensions :: Monad m => Sending m [Ex.Extension]
- extensions = Sending $ \st -> return (getExtensions st, st)
- quitCmd :: MonadIO m => Sending m ()
- quitCmd = do
- sync
- sendDummyCommand "QUIT"
- rsetCmd :: MonadIO m => Sending m ()
- rsetCmd = do
- sendDummyCommand "RSET"
- sync
- mailCmd :: MonadIO m => Ac.Account -> Sending m ()
- mailCmd from = sendTotalyCommand . BS.concat $ ["MAIL FROM:<", Ac.normalize from, ">"]
- rcptCmd :: MonadIO m => Ac.Account -> Sending m ()
- rcptCmd to = let
- cmd = ["RCPT TO:", s . Ac.fullAccount $ to]
- in sendRcptCommand (BS.concat cmd) to
- setDataFilter :: Monad m => SendingDataFilter -> Sending m ()
- setDataFilter f = Sending $ \st -> return ((), st{dataFilter=f})
- getDataFilter :: Monad m => Sending m SendingDataFilter
- getDataFilter = Sending $ \st -> return (dataFilter st, st)
- {- |
- Starts a data sending command (DATA or BDAT).
- -}
- startData :: MonadIO m => Mime.BodyEncoding -> Sending m ()
- startData enc = do
- sync
- ifM (hasEx Ex.CHUNKING) (
- setDataFilter sendBdatBody
- )(
- do
- ch <- canHandle enc
- traceShow enc $ return ()
- traceShow ch $ return ()
- ifM (canHandle enc) (
- do
- totalyDataIfActive $ case enc of
- Mime.B7BitEncoding -> "DATA\r\n"
- Mime.B8BitEncoding -> "DATA BODY=8BIT\r\n"
- Mime.BBinaryEncoding -> "DATA BODY=BINARY\r\n"
- sync
- setDataFilter $ \m lst ->
- (False, LBS.toStrict . dotStuff . LBS.fromStrict $ m) :
- if lst then [(True, "\r\n.\r\n")] else []
- ) (failAll' . toResponse $ NoConversion)
- )
- where
- canHandle :: Monad m => Mime.BodyEncoding -> Sending m Bool
- canHandle Mime.B7BitEncoding = return True
- canHandle Mime.B8BitEncoding = hasEx Ex.E8BITMIME
- canHandle Mime.BBinaryEncoding = hasEx Ex.BINARYMIME
- -- desiredEncoding :: Mime.BodyEncoding -> Sending m m Mime.BodyEncoding
- -- desiredEncoding o = case o of
- -- Mime.B7BitEncoding -> return Mime.B7BitEncoding
- -- Mime.B8BitEncoding -> ifM (hasEx Ex.E8BITMIME) (return Mime.B8BitEncoding) (
- -- ifM (hasEx Ex.BINARYMIME) (return Mime.BBinaryEncoding) (return Mime.B7BitEncoding)
- -- )
- -- Mime.BBinaryEncoding -> ifM (hasEx Ex.BINARYMIME) (
- -- return Mime.BBinaryEncoding) (return Mime.B7BitEncoding)
- sendBdatBody :: SendingDataFilter
- sendBdatBody dt lst = if lst
- then [(True, BS.concat ["BDAT ", s . show . BS.length $ dt, " LAST\r\n", dt])]
- else [(True, BS.concat ["BDAT ", s . show . BS.length $ dt, "\r\n", dt])]
- dataChunk :: MonadIO m => ByteString -> Bool -> Sending m ()
- dataChunk dt lst = do
- f <- getDataFilter
- let dt' = f dt lst
- mapM_ sendChunk dt'
- where
- --sendChunk :: (Bool, ByteString) -> Sending m ()
- sendChunk (repl, ck) = if repl
- then totalyCommandNoLn ck
- else dataIfActive ck
- dataCmd :: MonadIO m => Mime.BodyEncoding -> LBS.ByteString -> Sending m ()
- dataCmd enc m = ifM (hasEx Ex.CHUNKING) (
- sendBdatBody m -- No reencode is needed.
- ) $ do
- sendTotalyCommand $ case enc of
- Mime.B7BitEncoding -> "DATA"
- Mime.B8BitEncoding -> "DATA BODY=8BIT"
- Mime.BBinaryEncoding -> "DATA BODY=BINARY"
- sync
- totalyDataIfActive' $ LBS.append (dotStuff m) "\r\n.\r\n"
- where
- -- desiredEncoding :: Mime.BodyEncoding -> Sending m m Mime.BodyEncoding
- -- desiredEncoding o = case o of
- -- Mime.B7BitEncoding -> return Mime.B7BitEncoding
- -- Mime.B8BitEncoding -> ifM (hasEx Ex.E8BITMIME) (return Mime.B8BitEncoding) (
- -- ifM (hasEx Ex.BINARYMIME) (return Mime.BBinaryEncoding) (return Mime.B7BitEncoding)
- -- )
- -- Mime.BBinaryEncoding -> ifM (hasEx Ex.BINARYMIME) (
- -- return Mime.BBinaryEncoding) (return Mime.B7BitEncoding)
- sendBdatBody :: MonadIO m => LBS.ByteString -> Sending m ()
- sendBdatBody dt = do
- let (d, r) = LBS.splitAt (fromIntegral chunkSize) dt
- if LBS.null r
- then
- totalyDataIfActive $ BS.concat [
- "BDAT ", s . show . LBS.length $ d, " LAST\r\n", LBS.toStrict d]
- else do
- totalyDataIfActive $ BS.concat [
- "BDAT ", s . show . LBS.length $ d, "\r\n", LBS.toStrict d]
- sendBdatBody r
- sendDummyCommand :: MonadIO m => ByteString -> Sending m ()
- sendDummyCommand cmd = do
- lifts . S.send . BS.concat $ [cmd, "\r\n"]
- pipeline DummyCommand
- sendTotalyCommand :: MonadIO m => ByteString -> Sending m ()
- sendTotalyCommand cmd = totalyCommandNoLn $ BS.concat [cmd, "\r\n"]
- totalyCommandNoLn :: MonadIO m => ByteString -> Sending m ()
- totalyCommandNoLn cmd = do
- lifts . S.send $ cmd
- pipeline TotalyImportantCommand
- dataIfActive :: MonadIO m => ByteString -> Sending m ()
- dataIfActive dt = whenM hasActiveRcpt (lifts $ S.send dt)
- totalyDataIfActive :: MonadIO m => ByteString -> Sending m ()
- totalyDataIfActive dt = whenM hasActiveRcpt $ do
- lifts $ S.send dt
- pipeline TotalyImportantCommand
-
- totalyDataIfActive' :: MonadIO m => LBS.ByteString -> Sending m ()
- totalyDataIfActive' dt = whenM hasActiveRcpt $ do
- lifts $ S.send' dt
- pipeline TotalyImportantCommand
- sendRcptCommand :: MonadIO m => ByteString -> Ac.Account -> Sending m ()
- sendRcptCommand cmd e = do
- lifts . S.send . BS.concat $ [cmd, "\r\n"]
- Sending (\st -> let
- a = e : activeRcpt st
- st' = st{activeRcpt=a}
- in if e `elem` activeRcpt st then return ((), st) else return ((), st')
- )
- pipeline . CommandForRcpt $ e
- pipeline :: MonadIO m => PendingCommand -> Sending m ()
- pipeline cmd = Sending $ \st ->
- if isPipelining st
- then
- let p = cmd : pendingCommands st
- in return ((), st{pendingCommands=p})
- else do
- repl <- receiveReply
- let st' = resolveCommand repl cmd st
- return ((), st')
- resolveCommand :: Response -> PendingCommand -> Data -> Data
- resolveCommand repl cmd st = case Resp.status repl of
- Preliminary -> st
- Completion -> st
- Intermediate -> st
- TransientError -> case cmd of
- DummyCommand -> st
- TotalyImportantCommand -> failAll repl st
- CommandForRcpt e -> failAdd repl st e
- PermanentError -> case cmd of
- DummyCommand -> st
- TotalyImportantCommand -> failAll repl st
- CommandForRcpt e -> failAdd repl st e
- DataFollows -> failAll repl st -- Shouldn't appear when sending
- Asynchronous -> st
- failAdd :: Response -> Data -> Ac.Account -> Data
- failAdd r st e = st{activeRcpt = a}{failure = (e, r): failure st}
- where
- a = removeElem (activeRcpt st) e
- removeElem :: Eq a => [a] -> a -> [a]
- removeElem l e = filter (/= e) l
- failAll :: Response -> Data -> Data
- failAll r st = st{activeRcpt=[]}{failure = a ++ failure st}
- where
- a = map (\x -> (x, r)) $ activeRcpt st
- failAll' :: Monad m => Response -> Sending m ()
- failAll' r = Sending $ \st -> return ((), failAll r st)
- sync :: MonadIO m => Sending m ()
- sync = do
- cmds <- queryState pendingCommands
- mapM_ getReply $ reverse cmds
- zeroPendingCommands
- where
- getReply :: MonadIO m => PendingCommand -> Sending m ()
- getReply cmd = Sending $ \st -> do
- repl <- receiveReply
- let st' = resolveCommand repl cmd st
- return ((), st')
- zeroPendingCommands :: MonadIO m => Sending m ()
- zeroPendingCommands = Sending $ \st -> do
- return ((), st{pendingCommands=[]})
- queryState :: Monad m => (Data -> a) -> Sending m a
- queryState f = Sending $ \st ->
- return (f st, st)
- handshake :: MonadIO m => Sending m ()
- handshake = Sending (
- \st -> do
- S.recieveLine
- let host = hostName st
- exts <- getEhlo host
- secureLine <- S.isSecure
- let tls = Ex.hasExtension exts Ex.STARTTLS && not secureLine
- if tls
- then do
- S.send "STARTTLS\r\n"
- S.recieveLine
- S.startTls $ tlsSettings st
- exts' <- getEhlo host
- let pipe = Ex.hasExtension exts' Ex.PIPELINING
- let st' = st{getExtensions=exts'}{isPipelining=pipe}
- return ((), st')
- else do
- let pipe = Ex.hasExtension exts Ex.PIPELINING
- let st' = st{getExtensions=exts}{isPipelining=pipe}
- return ((), st')
- )
- where
- getEhlo host = do
- S.send . BS.concat $ ["EHLO ", host, "\r\n"]
- repl <- receiveReply
- if isSuccess . Resp.status $ repl
- then do
- let exts = ehloExts repl
- return exts
- else do
- S.send . BS.concat $ ["HELO ", host, "\r\n"]
- repl' <- receiveReply
- if isSuccess . Resp.status $ repl'
- then return []
- else liftIO . throwIO $ CE.ProtocolError
- --Client code needs:
- --A parser that reads the server response, and breaks it in:
- --Error with:
- -- Type and message
- --Success with:
- -- Message, and possibly contents, where contents are
- -- EHLO extension support, MNTR changes, CHSM values, or RTRV data
- --
- --Because of MNTR, the parser must be lazy, and return
- --before command termination.
- ehloExts :: Response -> [Ex.Extension]
- ehloExts r = map (\msg -> case A.parseOnly Ex.parseExtension msg of
- Right e -> e
- Left e -> Ex.StringExt Ex.UNRECOGNIZED $ BS.concat [s e, ": ", msg]
- ) $ Resp.respLines r
- isSuccess :: ResponseStatus -> Bool
- isSuccess TransientError = False
- isSuccess PermanentError = False
- isSuccess _ = True
- receiveReply :: MonadIO m => S.Streamline m Resp.Response
- receiveReply = do
- r <- S.runAttoparsec Resp.parseResponse
- case r of
- Left e -> return def{Resp.message=s e}
- Right v -> return v
- -- Here Sending becomes a usefull Monad
- instance Monad m => Monad (Sending m) where
- --return :: (Monad m) => a -> Sending m a
- return x = Sending $ \cl -> return (x, cl)
- --(>>=) :: Sending m a -> (a -> Sending m b) -> Sending m b
- a >>= b = Sending $ \cl -> do
- (x, cl') <- withSending a cl
- withSending (b x) cl'
- instance Monad m => Functor (Sending m) where
- --fmap :: (a -> b) -> Sending m a -> Sending m b
- fmap f m = Sending $ \cl -> do
- (x, cl') <- withSending m cl
- return (f x, cl')
- instance Monad m => Applicative (Sending m) where
- pure = return
- (<*>) = ap
- instance MonadTrans Sending where
- lift = lifts . lift
- lifts :: Monad m => S.Streamline m a -> Sending m a
- lifts x = Sending $ \st -> do
- r <- x
- return (r, st)
- instance MonadIO m => MonadIO (Sending m) where
- liftIO = lifts . liftIO
- dotStuff :: LBS.ByteString -> LBS.ByteString
- dotStuff m = LBS.append
- (LBS.fromStrict leading)
- (Search.replace ("\r\n."::ByteString) ("\r\n.."::ByteString) m)
- where
- leading = if LBS.take 1 m == "." then "a" else ""
- hasEx :: Monad m => Ex.ExtName -> Sending m Bool
- hasEx x = Sending $ \st -> return (isJust $ Ex.getExtension (getExtensions st) x, st)
- hasActiveRcpt :: Monad m => Sending m Bool
- hasActiveRcpt = Sending $ \st -> return (not . null . activeRcpt $ st, st)
- --------------------------------------------
- -- Interruptible and MonadControl support --
- --------------------------------------------
- instance Interruptible Sending where
- type RSt Sending a = RSt S.Streamline (a, Data)
- resume f st = resume (\(x, dt) -> withSending (f x) dt) st
- peelSendingCtx :: RSt Sending a -> (a, U.SomeIO, [(Ac.Account, Response)])
- peelSendingCtx st = let
- ((a, dt), io) = S.peelStreamlineCtx st
- in (a, io, failure dt)
- inSendingCtx :: (ToString host, U.UniformIO io) => host -> U.TlsSettings -> io -> a -> RSt Sending a
- inSendingCtx h set io a = let
- dt = Data (s . toString $ h) set [] [] [] [] False dummyFilter
- in S.inStreamlineCtx io (a, dt)
- instance MonadTransControl Sending where
- type StT Sending a = StT S.Streamline (a, Data)
- -- liftWith :: (Run Sending -> m a) -> Sending m a
- liftWith f = Sending $ \st ->
- liftM (\x -> (x, st)) $ liftWith $ \f' -> do
- f $ \(Sending t) -> do
- f' $ t st
- --restoreT :: m (St Sending a) -> Sending m a
- restoreT r = Sending . const . restoreT $ r
- --Sending $ \s -> do
- --(st, dt) <- r
- --restoreT . return $ (\(v, x) -> ((v, dt), x)) st
- instance MonadBase b m => MonadBase b (Sending m) where
- liftBase = liftBaseDefault
- instance MonadBaseControl b m => MonadBaseControl b (Sending m) where
- type StM (Sending m) a = ComposeSt Sending m a
- liftBaseWith = defaultLiftBaseWith
- restoreM = defaultRestoreM
-
|