packages feed

danecheck-1.0.0.0: Dane/Scanner/SMTP/Sock.hs

module Dane.Scanner.SMTP.Sock
  ( sockSource
  , sockSink
  ) where

import qualified Data.ByteString as B
import qualified Streaming.ByteString as Q
import           Control.Monad ((>=>))
import           Control.Monad.IO.Class (liftIO)
import           Control.Monad.Trans.Class (lift)
import           Control.Monad.Trans.State.Strict (gets, modify')
import           Network.Socket (Socket)
import           Network.Socket.ByteString (recv, send)
import           System.Timeout (timeout)

import           Dane.Scanner.SMTP.Internal

-- | Write the whole bytestring to the socket, looping if 'send'
-- only consumes a prefix.  Returns once every byte has been sent
-- or the underlying 'send' has thrown.
sockWrite :: Socket -> B.ByteString -> IO ()
sockWrite sock bs = send sock bs >>= \ case
    n | n < B.length bs -> sockWrite sock $ B.drop n bs
      | otherwise       -> pure ()

-- | Consume a 'ByteStream' and push each chunk down the plaintext
-- socket.  Errors and timeouts are recorded in 'smtpErr' and
-- stop the loop.
sockSink :: Q.ByteStream SmtpM () -> SmtpM ()
sockSink str = gets smtpConn >>= \ case
    SmtpPlain sock -> go sock str
    _              -> fail "Non-plaintext channel"
  where
    go sock = Q.unconsChunk >=> \ case
        Right (bs, bss) -> do
            tm <- timeLeft
            liftIO (timeout tm $ tryIO $ sockWrite sock bs) >>= \ case
                Just (Right _) -> go sock bss
                Just (Left e)
                    -> modify' \s -> s { smtpErr = DataErr $ ioErr "write" e }
                _   -> modify' \s -> s { smtpErr = DataErr $ timeErr "write" }
        Left ()        -> pure ()

-- | Read chunks off the plaintext socket and produce a
-- 'ByteStream' for downstream consumers.  EOF, errors and
-- timeouts are recorded in 'smtpErr' and terminate the stream.
sockSource :: Q.ByteStream SmtpM ()
sockSource = lift (gets smtpConn) >>= \ case
    SmtpPlain sock -> go sock
    _              -> error "Non-plaintext channel"
  where
    go sock = do
        tm <- lift timeLeft
        liftIO (timeout tm $ tryIO $ recv sock 65536) >>= \ case
            Just (Right bs)
                | B.length bs > 0 -> Q.chunk bs >> go sock
                | otherwise
                  -> lift $ modify' \s -> s { smtpErr = DataErr $ eofErr "read" }
            Just (Left e)
                  -> lift $ modify' \s -> s { smtpErr = DataErr $ ioErr "read" e }
            _     -> lift $ modify' \s -> s { smtpErr = DataErr $ timeErr "read" }