SendingState.hs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  1. {-# LANGUAGE OverloadedStrings, TypeFamilies, MultiParamTypeClasses, FlexibleInstances, UndecidableInstances, RankNTypes #-}
  2. module Network.FCMTP.SendingState (
  3. Sending,
  4. inSendingConnection,
  5. inSendingSession,
  6. inSendingCtx,
  7. peelSendingCtx,
  8. startSending,
  9. startSession,
  10. failures,
  11. extensions,
  12. quitCmd,
  13. rsetCmd,
  14. mailCmd,
  15. dataCmd,
  16. startData,
  17. dataChunk,
  18. rcptCmd
  19. ) where
  20. -- That's another StateT that encapsulates a Stream and sending data
  21. -- Only a bit more complicated, because every command leads to a response
  22. -- that may be treated in batch or individually according to the PIPELINING
  23. -- or XTUNNEL extension.
  24. import qualified Data.FCMTP.Account as Ac
  25. import qualified Data.FCMTP.Extension as Ex
  26. import qualified Data.FCMTP.Mime as Mime
  27. import System.IO (Handle)
  28. import qualified System.IO.Uniform as U
  29. import qualified System.IO.Uniform.Streamline as S
  30. import qualified Network.FCMTP.ClientError as CE
  31. import qualified Data.FCMTP.Response as Resp
  32. import Data.FCMTP.Response (Response, ResponseStatus(..))
  33. import Data.FCMTP.ResponseCode (toResponse, ResponseCode(..))
  34. import Control.Conditional
  35. import Control.Monad (ap, liftM)
  36. import Control.Monad.IO.Class
  37. import Control.Monad.Trans
  38. import Control.Monad.Trans.Interruptible
  39. import Control.Monad.Trans.Control
  40. import Control.Monad.Base
  41. import Control.Exception
  42. import Data.Maybe (isJust)
  43. import Data.Default.Class
  44. import qualified Data.Attoparsec.ByteString as A
  45. import qualified Data.ByteString as BS
  46. import qualified Data.ByteString.Lazy as LBS
  47. import Data.ByteString (ByteString)
  48. import qualified Data.ByteString.Lazy.Search as Search
  49. import Text.StringConvert
  50. import Debug.Trace
  51. chunkSize :: Int
  52. chunkSize = 100000 -- bytes - about 10 normal ethernet packages
  53. {- |
  54. How to chunk data for sending.
  55. filter text isLast = [(generatesAReply, sentText)]
  56. -}
  57. type SendingDataFilter = ByteString -> Bool -> [(Bool, ByteString)]
  58. data Data = Data {
  59. hostName :: ByteString, tlsSettings :: U.TlsSettings,
  60. getExtensions :: [Ex.Extension], activeRcpt :: [Ac.Account],
  61. failure :: [(Ac.Account, Response)],
  62. pendingCommands :: [PendingCommand], isPipelining :: Bool,
  63. dataFilter :: SendingDataFilter}
  64. newtype Sending m a = Sending {withSending :: Data -> S.Streamline m (a, Data)}
  65. -- Commands with pending replies, segregated by what failure means.
  66. data PendingCommand = DummyCommand | TotalyImportantCommand | CommandForRcpt Ac.Account
  67. dummyFilter :: SendingDataFilter
  68. dummyFilter dt _ = [(False, dt)]
  69. startSending :: MonadIO m => Maybe Handle -> Sending m ()
  70. startSending ec = do
  71. lifts $ S.echoTo ec
  72. handshake
  73. startSession :: MonadIO m => Sending m ()
  74. startSession = startSendingSession
  75. inSendingConnection :: MonadIO m => ToString host => host -> U.TlsSettings -> Sending m a -> S.Streamline m a
  76. inSendingConnection h set f = do
  77. (ret, _) <- withSending f' $ Data (s . toString $ h) set [] [] [] [] False dummyFilter
  78. return ret
  79. where
  80. f' = do
  81. handshake
  82. ret <- f
  83. quitCmd
  84. return ret
  85. startSendingSession :: MonadIO m => Sending m ()
  86. startSendingSession = Sending $ \st -> do
  87. let s' = st{activeRcpt=[]}{failure=[]}
  88. return ((), s')
  89. clearSendingSession :: MonadIO m => Sending m ()
  90. clearSendingSession = Sending $ \st -> do
  91. let s' = st{activeRcpt=[]}{failure=[]}
  92. return ((), s')
  93. inSendingSession :: MonadIO m => Sending m a -> Sending m (a, [(Ac.Account, Response)])
  94. inSendingSession f = do
  95. sync
  96. startSendingSession
  97. ret <- f
  98. rsetCmd
  99. sync
  100. ff <- failures
  101. clearSendingSession
  102. return (ret, ff)
  103. -- | Returns (success address, temporary failures, permanent failures)
  104. failures :: Monad m => Sending m [(Ac.Account, Response)]
  105. failures = Sending $ \st -> return (failure st, st)
  106. extensions :: Monad m => Sending m [Ex.Extension]
  107. extensions = Sending $ \st -> return (getExtensions st, st)
  108. quitCmd :: MonadIO m => Sending m ()
  109. quitCmd = do
  110. sync
  111. sendDummyCommand "QUIT"
  112. rsetCmd :: MonadIO m => Sending m ()
  113. rsetCmd = do
  114. sendDummyCommand "RSET"
  115. sync
  116. mailCmd :: MonadIO m => Ac.Account -> Sending m ()
  117. mailCmd from = sendTotalyCommand . BS.concat $ ["MAIL FROM:<", Ac.normalize from, ">"]
  118. rcptCmd :: MonadIO m => Ac.Account -> Sending m ()
  119. rcptCmd to = let
  120. cmd = ["RCPT TO:", s . Ac.fullAccount $ to]
  121. in sendRcptCommand (BS.concat cmd) to
  122. setDataFilter :: Monad m => SendingDataFilter -> Sending m ()
  123. setDataFilter f = Sending $ \st -> return ((), st{dataFilter=f})
  124. getDataFilter :: Monad m => Sending m SendingDataFilter
  125. getDataFilter = Sending $ \st -> return (dataFilter st, st)
  126. {- |
  127. Starts a data sending command (DATA or BDAT).
  128. -}
  129. startData :: MonadIO m => Mime.BodyEncoding -> Sending m ()
  130. startData enc = do
  131. sync
  132. ifM (hasEx Ex.CHUNKING) (
  133. setDataFilter sendBdatBody
  134. )(
  135. do
  136. ch <- canHandle enc
  137. traceShow enc $ return ()
  138. traceShow ch $ return ()
  139. ifM (canHandle enc) (
  140. do
  141. totalyDataIfActive $ case enc of
  142. Mime.B7BitEncoding -> "DATA\r\n"
  143. Mime.B8BitEncoding -> "DATA BODY=8BIT\r\n"
  144. Mime.BBinaryEncoding -> "DATA BODY=BINARY\r\n"
  145. sync
  146. setDataFilter $ \m lst ->
  147. (False, LBS.toStrict . dotStuff . LBS.fromStrict $ m) :
  148. if lst then [(True, "\r\n.\r\n")] else []
  149. ) (failAll' . toResponse $ NoConversion)
  150. )
  151. where
  152. canHandle :: Monad m => Mime.BodyEncoding -> Sending m Bool
  153. canHandle Mime.B7BitEncoding = return True
  154. canHandle Mime.B8BitEncoding = hasEx Ex.E8BITMIME
  155. canHandle Mime.BBinaryEncoding = hasEx Ex.BINARYMIME
  156. -- desiredEncoding :: Mime.BodyEncoding -> Sending m m Mime.BodyEncoding
  157. -- desiredEncoding o = case o of
  158. -- Mime.B7BitEncoding -> return Mime.B7BitEncoding
  159. -- Mime.B8BitEncoding -> ifM (hasEx Ex.E8BITMIME) (return Mime.B8BitEncoding) (
  160. -- ifM (hasEx Ex.BINARYMIME) (return Mime.BBinaryEncoding) (return Mime.B7BitEncoding)
  161. -- )
  162. -- Mime.BBinaryEncoding -> ifM (hasEx Ex.BINARYMIME) (
  163. -- return Mime.BBinaryEncoding) (return Mime.B7BitEncoding)
  164. sendBdatBody :: SendingDataFilter
  165. sendBdatBody dt lst = if lst
  166. then [(True, BS.concat ["BDAT ", s . show . BS.length $ dt, " LAST\r\n", dt])]
  167. else [(True, BS.concat ["BDAT ", s . show . BS.length $ dt, "\r\n", dt])]
  168. dataChunk :: MonadIO m => ByteString -> Bool -> Sending m ()
  169. dataChunk dt lst = do
  170. f <- getDataFilter
  171. let dt' = f dt lst
  172. mapM_ sendChunk dt'
  173. where
  174. --sendChunk :: (Bool, ByteString) -> Sending m ()
  175. sendChunk (repl, ck) = if repl
  176. then totalyCommandNoLn ck
  177. else dataIfActive ck
  178. dataCmd :: MonadIO m => Mime.BodyEncoding -> LBS.ByteString -> Sending m ()
  179. dataCmd enc m = ifM (hasEx Ex.CHUNKING) (
  180. sendBdatBody m -- No reencode is needed.
  181. ) $ do
  182. sendTotalyCommand $ case enc of
  183. Mime.B7BitEncoding -> "DATA"
  184. Mime.B8BitEncoding -> "DATA BODY=8BIT"
  185. Mime.BBinaryEncoding -> "DATA BODY=BINARY"
  186. sync
  187. totalyDataIfActive' $ LBS.append (dotStuff m) "\r\n.\r\n"
  188. where
  189. -- desiredEncoding :: Mime.BodyEncoding -> Sending m m Mime.BodyEncoding
  190. -- desiredEncoding o = case o of
  191. -- Mime.B7BitEncoding -> return Mime.B7BitEncoding
  192. -- Mime.B8BitEncoding -> ifM (hasEx Ex.E8BITMIME) (return Mime.B8BitEncoding) (
  193. -- ifM (hasEx Ex.BINARYMIME) (return Mime.BBinaryEncoding) (return Mime.B7BitEncoding)
  194. -- )
  195. -- Mime.BBinaryEncoding -> ifM (hasEx Ex.BINARYMIME) (
  196. -- return Mime.BBinaryEncoding) (return Mime.B7BitEncoding)
  197. sendBdatBody :: MonadIO m => LBS.ByteString -> Sending m ()
  198. sendBdatBody dt = do
  199. let (d, r) = LBS.splitAt (fromIntegral chunkSize) dt
  200. if LBS.null r
  201. then
  202. totalyDataIfActive $ BS.concat [
  203. "BDAT ", s . show . LBS.length $ d, " LAST\r\n", LBS.toStrict d]
  204. else do
  205. totalyDataIfActive $ BS.concat [
  206. "BDAT ", s . show . LBS.length $ d, "\r\n", LBS.toStrict d]
  207. sendBdatBody r
  208. sendDummyCommand :: MonadIO m => ByteString -> Sending m ()
  209. sendDummyCommand cmd = do
  210. lifts . S.send . BS.concat $ [cmd, "\r\n"]
  211. pipeline DummyCommand
  212. sendTotalyCommand :: MonadIO m => ByteString -> Sending m ()
  213. sendTotalyCommand cmd = totalyCommandNoLn $ BS.concat [cmd, "\r\n"]
  214. totalyCommandNoLn :: MonadIO m => ByteString -> Sending m ()
  215. totalyCommandNoLn cmd = do
  216. lifts . S.send $ cmd
  217. pipeline TotalyImportantCommand
  218. dataIfActive :: MonadIO m => ByteString -> Sending m ()
  219. dataIfActive dt = whenM hasActiveRcpt (lifts $ S.send dt)
  220. totalyDataIfActive :: MonadIO m => ByteString -> Sending m ()
  221. totalyDataIfActive dt = whenM hasActiveRcpt $ do
  222. lifts $ S.send dt
  223. pipeline TotalyImportantCommand
  224. totalyDataIfActive' :: MonadIO m => LBS.ByteString -> Sending m ()
  225. totalyDataIfActive' dt = whenM hasActiveRcpt $ do
  226. lifts $ S.send' dt
  227. pipeline TotalyImportantCommand
  228. sendRcptCommand :: MonadIO m => ByteString -> Ac.Account -> Sending m ()
  229. sendRcptCommand cmd e = do
  230. lifts . S.send . BS.concat $ [cmd, "\r\n"]
  231. Sending (\st -> let
  232. a = e : activeRcpt st
  233. st' = st{activeRcpt=a}
  234. in if e `elem` activeRcpt st then return ((), st) else return ((), st')
  235. )
  236. pipeline . CommandForRcpt $ e
  237. pipeline :: MonadIO m => PendingCommand -> Sending m ()
  238. pipeline cmd = Sending $ \st ->
  239. if isPipelining st
  240. then
  241. let p = cmd : pendingCommands st
  242. in return ((), st{pendingCommands=p})
  243. else do
  244. repl <- receiveReply
  245. let st' = resolveCommand repl cmd st
  246. return ((), st')
  247. resolveCommand :: Response -> PendingCommand -> Data -> Data
  248. resolveCommand repl cmd st = case Resp.status repl of
  249. Preliminary -> st
  250. Completion -> st
  251. Intermediate -> st
  252. TransientError -> case cmd of
  253. DummyCommand -> st
  254. TotalyImportantCommand -> failAll repl st
  255. CommandForRcpt e -> failAdd repl st e
  256. PermanentError -> case cmd of
  257. DummyCommand -> st
  258. TotalyImportantCommand -> failAll repl st
  259. CommandForRcpt e -> failAdd repl st e
  260. DataFollows -> failAll repl st -- Shouldn't appear when sending
  261. Asynchronous -> st
  262. failAdd :: Response -> Data -> Ac.Account -> Data
  263. failAdd r st e = st{activeRcpt = a}{failure = (e, r): failure st}
  264. where
  265. a = removeElem (activeRcpt st) e
  266. removeElem :: Eq a => [a] -> a -> [a]
  267. removeElem l e = filter (/= e) l
  268. failAll :: Response -> Data -> Data
  269. failAll r st = st{activeRcpt=[]}{failure = a ++ failure st}
  270. where
  271. a = map (\x -> (x, r)) $ activeRcpt st
  272. failAll' :: Monad m => Response -> Sending m ()
  273. failAll' r = Sending $ \st -> return ((), failAll r st)
  274. sync :: MonadIO m => Sending m ()
  275. sync = do
  276. cmds <- queryState pendingCommands
  277. mapM_ getReply $ reverse cmds
  278. zeroPendingCommands
  279. where
  280. getReply :: MonadIO m => PendingCommand -> Sending m ()
  281. getReply cmd = Sending $ \st -> do
  282. repl <- receiveReply
  283. let st' = resolveCommand repl cmd st
  284. return ((), st')
  285. zeroPendingCommands :: MonadIO m => Sending m ()
  286. zeroPendingCommands = Sending $ \st -> do
  287. return ((), st{pendingCommands=[]})
  288. queryState :: Monad m => (Data -> a) -> Sending m a
  289. queryState f = Sending $ \st ->
  290. return (f st, st)
  291. handshake :: MonadIO m => Sending m ()
  292. handshake = Sending (
  293. \st -> do
  294. S.recieveLine
  295. let host = hostName st
  296. exts <- getEhlo host
  297. secureLine <- S.isSecure
  298. let tls = Ex.hasExtension exts Ex.STARTTLS && not secureLine
  299. if tls
  300. then do
  301. S.send "STARTTLS\r\n"
  302. S.recieveLine
  303. S.startTls $ tlsSettings st
  304. exts' <- getEhlo host
  305. let pipe = Ex.hasExtension exts' Ex.PIPELINING
  306. let st' = st{getExtensions=exts'}{isPipelining=pipe}
  307. return ((), st')
  308. else do
  309. let pipe = Ex.hasExtension exts Ex.PIPELINING
  310. let st' = st{getExtensions=exts}{isPipelining=pipe}
  311. return ((), st')
  312. )
  313. where
  314. getEhlo host = do
  315. S.send . BS.concat $ ["EHLO ", host, "\r\n"]
  316. repl <- receiveReply
  317. if isSuccess . Resp.status $ repl
  318. then do
  319. let exts = ehloExts repl
  320. return exts
  321. else do
  322. S.send . BS.concat $ ["HELO ", host, "\r\n"]
  323. repl' <- receiveReply
  324. if isSuccess . Resp.status $ repl'
  325. then return []
  326. else liftIO . throwIO $ CE.ProtocolError
  327. --Client code needs:
  328. --A parser that reads the server response, and breaks it in:
  329. --Error with:
  330. -- Type and message
  331. --Success with:
  332. -- Message, and possibly contents, where contents are
  333. -- EHLO extension support, MNTR changes, CHSM values, or RTRV data
  334. --
  335. --Because of MNTR, the parser must be lazy, and return
  336. --before command termination.
  337. ehloExts :: Response -> [Ex.Extension]
  338. ehloExts r = map (\msg -> case A.parseOnly Ex.parseExtension msg of
  339. Right e -> e
  340. Left e -> Ex.StringExt Ex.UNRECOGNIZED $ BS.concat [s e, ": ", msg]
  341. ) $ Resp.respLines r
  342. isSuccess :: ResponseStatus -> Bool
  343. isSuccess TransientError = False
  344. isSuccess PermanentError = False
  345. isSuccess _ = True
  346. receiveReply :: MonadIO m => S.Streamline m Resp.Response
  347. receiveReply = do
  348. r <- S.runAttoparsec Resp.parseResponse
  349. case r of
  350. Left e -> return def{Resp.message=s e}
  351. Right v -> return v
  352. -- Here Sending becomes a usefull Monad
  353. instance Monad m => Monad (Sending m) where
  354. --return :: (Monad m) => a -> Sending m a
  355. return x = Sending $ \cl -> return (x, cl)
  356. --(>>=) :: Sending m a -> (a -> Sending m b) -> Sending m b
  357. a >>= b = Sending $ \cl -> do
  358. (x, cl') <- withSending a cl
  359. withSending (b x) cl'
  360. instance Monad m => Functor (Sending m) where
  361. --fmap :: (a -> b) -> Sending m a -> Sending m b
  362. fmap f m = Sending $ \cl -> do
  363. (x, cl') <- withSending m cl
  364. return (f x, cl')
  365. instance Monad m => Applicative (Sending m) where
  366. pure = return
  367. (<*>) = ap
  368. instance MonadTrans Sending where
  369. lift = lifts . lift
  370. lifts :: Monad m => S.Streamline m a -> Sending m a
  371. lifts x = Sending $ \st -> do
  372. r <- x
  373. return (r, st)
  374. instance MonadIO m => MonadIO (Sending m) where
  375. liftIO = lifts . liftIO
  376. dotStuff :: LBS.ByteString -> LBS.ByteString
  377. dotStuff m = LBS.append
  378. (LBS.fromStrict leading)
  379. (Search.replace ("\r\n."::ByteString) ("\r\n.."::ByteString) m)
  380. where
  381. leading = if LBS.take 1 m == "." then "a" else ""
  382. hasEx :: Monad m => Ex.ExtName -> Sending m Bool
  383. hasEx x = Sending $ \st -> return (isJust $ Ex.getExtension (getExtensions st) x, st)
  384. hasActiveRcpt :: Monad m => Sending m Bool
  385. hasActiveRcpt = Sending $ \st -> return (not . null . activeRcpt $ st, st)
  386. --------------------------------------------
  387. -- Interruptible and MonadControl support --
  388. --------------------------------------------
  389. instance Interruptible Sending where
  390. type RSt Sending a = RSt S.Streamline (a, Data)
  391. resume f st = resume (\(x, dt) -> withSending (f x) dt) st
  392. peelSendingCtx :: RSt Sending a -> (a, U.SomeIO, [(Ac.Account, Response)])
  393. peelSendingCtx st = let
  394. ((a, dt), io) = S.peelStreamlineCtx st
  395. in (a, io, failure dt)
  396. inSendingCtx :: (ToString host, U.UniformIO io) => host -> U.TlsSettings -> io -> a -> RSt Sending a
  397. inSendingCtx h set io a = let
  398. dt = Data (s . toString $ h) set [] [] [] [] False dummyFilter
  399. in S.inStreamlineCtx io (a, dt)
  400. instance MonadTransControl Sending where
  401. type StT Sending a = StT S.Streamline (a, Data)
  402. -- liftWith :: (Run Sending -> m a) -> Sending m a
  403. liftWith f = Sending $ \st ->
  404. liftM (\x -> (x, st)) $ liftWith $ \f' -> do
  405. f $ \(Sending t) -> do
  406. f' $ t st
  407. --restoreT :: m (St Sending a) -> Sending m a
  408. restoreT r = Sending . const . restoreT $ r
  409. --Sending $ \s -> do
  410. --(st, dt) <- r
  411. --restoreT . return $ (\(v, x) -> ((v, dt), x)) st
  412. instance MonadBase b m => MonadBase b (Sending m) where
  413. liftBase = liftBaseDefault
  414. instance MonadBaseControl b m => MonadBaseControl b (Sending m) where
  415. type StM (Sending m) a = ComposeSt Sending m a
  416. liftBaseWith = defaultLiftBaseWith
  417. restoreM = defaultRestoreM