Client.hs 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. {-# LANGUAGE OverloadedStrings #-}
  2. module Network.FCMTP.Client (DebugOption(..), sendEmail) where
  3. import Network.DNS (Domain)
  4. import qualified Network.DNS.Lookup as DNS
  5. import Network.DNS.Resolver (makeResolvSeed, defaultResolvConf, withResolver)
  6. import qualified Data.List as List
  7. import System.Random(randomIO)
  8. import Control.Exception
  9. import Control.Monad.Trans.Interruptible
  10. import Control.Monad.Trans.SafeIO
  11. import Control.Monad.Trans.Either
  12. import Control.Lens
  13. import Data.Function (on)
  14. import Text.Regex.TDFA ((=~))
  15. import Text.StringConvert
  16. import Network.FCMTP.Relay
  17. import Network.FCMTP.ClientError
  18. import Data.FCMTP.Constants
  19. import Data.FCMTP.Host
  20. import Data.FCMTP.Account (Account, HostName(..))
  21. import qualified Data.FCMTP.Account as Ac
  22. --import Data.FCMTP.Address (Address(..))
  23. import Data.FCMTP.ResponseCode
  24. import Data.FCMTP.Response (Response)
  25. import qualified Data.FCMTP.Mime as Mime
  26. --import qualified Data.FCMTP.Address as Add
  27. import Network.FCMTP.SendingState
  28. import qualified Data.ByteString as BS
  29. import qualified Data.ByteString.Lazy as LBS
  30. import System.IO (hPutStrLn, stderr)
  31. import System.IO.Uniform (TlsSettings, SomeIO(..))
  32. import qualified System.IO.Uniform as U
  33. import System.IO.Uniform.Std
  34. import System.IO.Uniform.Network
  35. --fetchResource :: Settings -> AuthData -> ResourceURI -> IO SealedResource
  36. --listResource :: Settings -> AuthData -> ResourceURI -> IO ResourceList
  37. --deleteResource :: Settings -> AuthData -> ResourceURI -> IO ()
  38. --updateResource :: Settings -> AuthData -> ResourceURI SealedResource -> IO ()
  39. --monitorResource :: Settings -> AuthData -> ResourceURI -> MVar ResourceURI -> IO ()
  40. data DebugOption = RunStdIO | EchoProto deriving (Eq, Ord, Read, Show, Bounded, Enum)
  41. -- sendEmail :: debugOpts -> localhostName -> tlsSettngs -> relays -> from -> [rcpt] -> Mime.BodyEncoding -> mailData -> IO failures
  42. sendEmail :: ToString host => [DebugOption] -> host -> TlsSettings -> [Relay] -> Account -> [Account] -> Mime.BodyEncoding -> LBS.ByteString -> IO [(Account, Response)]
  43. sendEmail dbg h set relays fromAdd rcptTo enc mailData = do
  44. resp <- try $! sendEmail' dbg h set relays fromAdd rcptTo enc mailData :: IO (Either IOError [(Account, Response)])
  45. case resp of
  46. Right a -> return a
  47. Left e -> do
  48. hPutStrLn stderr $ "Error sending email: " ++ show e
  49. return $ map (\x -> (x, toResponse BadConnection)) rcptTo
  50. sendEmail' :: ToString host => [DebugOption] -> host -> TlsSettings ->[Relay] -> Account -> [Account] -> Mime.BodyEncoding -> LBS.ByteString -> IO [(Account, Response)]
  51. sendEmail' _ _ _ _ _ [] _ _ = return []
  52. sendEmail' dbg h set relays fromAdd rcptTo enc mailData = do
  53. let byhost = List.groupBy ((==) `on` getHost relays) rcptTo
  54. conn <- if RunStdIO `elem` dbg
  55. then do
  56. t <- getStdIO
  57. return [inSendingCtx h set t . inEitherTCtx $ rcptTo]
  58. else mapM (connHost) byhost
  59. results <- mapM (resume2 $ startHost dbg fromAdd enc) conn >>=
  60. intercalateFold resume2 dataToHost (LBS.toChunks mailData) >>=
  61. mapM (resume2 closeHost) >>=
  62. mapM peelCtx
  63. return . concat $ zipWith resolveErrors byhost results
  64. where
  65. getHost :: [Relay] -> Account -> Host
  66. getHost [] a = ByName $ Ac.domain a
  67. getHost (r:rr) a = if null (r^.relayRules)
  68. then r^.relayHost
  69. else if (Ac.normalize a) =~ (r^.relayRules)
  70. then r^.relayHost
  71. else getHost rr a
  72. inCtx :: U.UniformIO io => io -> a -> RSt Sending a
  73. inCtx = inSendingCtx h set
  74. connHost :: [Account] -> IO (RSt Sending (RSt (EitherT ClientError) [Account]))
  75. connHost x = case x of
  76. [] -> do
  77. t <- getStdIO
  78. return . inCtx t . Left $ CanNotConnect
  79. rr@(r:_) -> do
  80. c <- openMailHost $ getHost relays r
  81. case c of
  82. Left e -> do
  83. t <- getStdIO
  84. return . inCtx t . Left $ e
  85. Right c' -> return . inCtx c' . inEitherTCtx $ rr
  86. type SendingType a = EitherT ClientError (Sending IO) a
  87. startHost :: [DebugOption] -> Account -> Mime.BodyEncoding -> [Account] -> SendingType ()
  88. startHost dbg fromAdd enc rcptTo = do
  89. let echo = if EchoProto `elem` dbg then Just stderr else Nothing
  90. safeCT $ do
  91. startSending echo
  92. startSession
  93. mailCmd fromAdd
  94. mapM_ rcptCmd rcptTo
  95. startData enc
  96. dataToHost :: BS.ByteString -> () -> SendingType ()
  97. dataToHost dt _ = safeCT $ dataChunk dt False
  98. closeHost :: () -> SendingType ()
  99. closeHost _ = safeCT $ do
  100. dataChunk BS.empty True
  101. quitCmd
  102. peelCtx :: RSt Sending (RSt (EitherT ClientError) ()) -> IO (Either ClientError [(Account, Response)])
  103. peelCtx c = do
  104. let (c', io, ff) = peelSendingCtx c
  105. U.uClose io
  106. case peelEitherTCtx c' of
  107. Left e -> return . Left $ e
  108. Right _ -> return . Right $ ff
  109. resolveErrors :: [Account] -> Either ClientError [(Account, Response)] -> [(Account, Response)]
  110. resolveErrors aa rt = case rt of
  111. Right ff -> ff
  112. Left _ -> map (\x -> (x, toResponse BadConnection)) aa
  113. -- Returns the mailservers for a given domain name, ordered by priority
  114. emailAddrs :: BS.ByteString -> IO [Domain]
  115. emailAddrs a = do
  116. let hostname = a
  117. rs <- makeResolvSeed defaultResolvConf
  118. hs <- withResolver rs $ \resolver -> DNS.lookupMX resolver hostname
  119. case hs of
  120. Right ds -> do
  121. dr <- mapM randMX ds
  122. return $ map fst3 $ List.sortBy orderMX dr
  123. Left _ -> throwIO MXLookupError
  124. randMX :: (Domain, Int) -> IO (Domain, Int, Int)
  125. randMX (d, p) = do
  126. r <- randomIO
  127. return (d, p, r)
  128. orderMX :: (Domain, Int, Int) -> (Domain, Int, Int) -> Ordering
  129. orderMX (_, pa, ra) (_, pb, rb) = if pa == pb then compare ra rb else compare pa pb
  130. fst3 :: (a, b, c) -> a
  131. fst3 (a, _, _) = a
  132. openMailHost :: Host -> IO (Either ClientError SomeIO)
  133. openMailHost host =
  134. case host of
  135. ByName (HostName h) -> do
  136. mxs <- geterr $ emailAddrs h
  137. case mxs of
  138. Right [] -> tryOpen [h]
  139. Right adds -> tryOpen adds
  140. Left _ -> tryOpen [h]
  141. ByIP ip -> tryOpen [s . show $ ip]
  142. where
  143. pp = fromIntegral relayPortNumber :: Int
  144. tryOpen :: [BS.ByteString] -> IO (Either ClientError SomeIO)
  145. tryOpen [] = return . Left $ CanNotConnect
  146. tryOpen (a:aa) = do
  147. ret <- geterr $ connectToHost (s a) pp
  148. case ret of
  149. Right con -> return . Right . SomeIO $ con
  150. Left _ -> tryOpen aa
  151. geterr :: IO a -> IO (Either ClientError a)
  152. geterr f = do
  153. r <- f
  154. return . Right $ r