packages feed

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

{-# LANGUAGE OverloadedStrings #-}

module Dane.Scanner.SMTP.Proto (dosmtp) where

import qualified Control.Monad.Trans.State.Strict as ST
import qualified Data.ByteString as B
import qualified Data.ByteString.Char8 as BC
import qualified Data.Text as T
import qualified Data.Text.Encoding as E
import qualified Streaming.ByteString as Q
import qualified Streaming.Prelude as Streaming
import           Control.Monad (when)
import           Control.Monad.IO.Class (liftIO)
import           Control.Monad.Trans.Class (lift)
import           Control.Monad.Trans.State.Strict (get, gets, modify')
import           Data.Function ((&))

import           Dane.Scanner.Source (strictLines)
import           Dane.Scanner.SMTP.Sock
import           Dane.Scanner.SMTP.Parse
import           Dane.Scanner.SMTP.Internal
import           Dane.Scanner.SMTP.TLS

crlf :: B.ByteString
crlf = "\r\n"

ok :: SmtpErr -> Bool
ok SmtpOK = True
ok _      = False

smtpSendHello :: SmtpM B.ByteString
smtpSendHello = do
  deadline <- liftIO . timeLimit =<< gets smtpTimeout
  modify' \s -> s { smtpState = EHLO, ioDeadline = deadline }
  name <- gets clientName
  pure $ "EHLO " <> name <> crlf

smtpGreeting :: Int -> SmtpReply -> SmtpM B.ByteString
smtpGreeting _ r
    | replyCont r = pure B.empty
    | code <- replyCode r
    , code `div` 100 /= 2
    = do modify' \s -> s { smtpErr = ProtoErr code $ replyText r }
         pure B.empty
    | otherwise = smtpSendHello

smtpHello :: Int -> SmtpReply -> SmtpM B.ByteString
smtpHello count r = do
    when (count > 0
          && "STARTTLS" == T.toUpper (E.decodeLatin1 (replyText r))) do
        modify' \s -> s { features = FeatureTLS:features s }
    if | replyCont r -> pure B.empty
       | code <- replyCode r
       , code `div` 100 /= 2
       -> do modify' \s -> s { smtpErr = ProtoErr code $ replyText r }
             pure B.empty
       | otherwise -> do
           deadline <- liftIO . timeLimit =<< gets smtpTimeout
           modify' \s -> s { ioDeadline = deadline }
           st <- get
           if | FeatureTLS `elem` features st
              , not (hasTLS st)
              -> "STARTTLS\r\n" <$ modify' \s -> s { smtpState = STARTTLS }
              | otherwise
              -> "QUIT\r\n" <$ modify' \s -> s { smtpState = QUIT }

smtpStartTLS :: Int -> SmtpReply -> SmtpM B.ByteString
smtpStartTLS _ r = do
    when (not $ replyCont r) do
        let code = replyCode r
        if | code `div` 100 == 2
           -> modify' \s -> s { smtpState = DOTLS }
           | otherwise
           -> modify' \s -> s { smtpErr = ProtoErr code $ replyText r }
    pure B.empty

smtpQuit :: Int -> SmtpReply -> SmtpM B.ByteString
smtpQuit _ r = do
    when (not $ replyCont r) do
        let code = replyCode r
        if | code `div` 100 == 2 -> do
               get >>= \st -> when (hasTLS st) endTLS
               modify' \s -> s { smtpState = DONE }
           | otherwise
           -> modify' \s -> s { smtpErr = ProtoErr code $ replyText r }
    pure B.empty

-- | SMTP state machine over a 'ByteStream' input.  Reads lines via
-- 'strictLines' (so the per-reply length cap from 'llenLimit' is
-- enforced on every line), dispatches them on 'smtpState', and
-- writes the resulting client commands back out as a 'ByteStream'
-- that the caller pipes to 'sockSink' (for plaintext) or 'tlsSink'
-- (for the post-STARTTLS phase).
proto :: Q.ByteStream SmtpM () -> Q.ByteStream SmtpM ()
proto src = lift get >>= \ st -> do
    when (smtpState st == DOTLS) do        -- Redo EHLO after TLS
        lift smtpSendHello >>= Q.chunk
    loop 0 $ strictLines (fromIntegral $ llenLimit st) src
  where
    loop count str = do
      st <- lift get
      if | DONE  <- smtpState st -> pure ()
         | DOTLS <- smtpState st -> pure ()
         | not $ ok $ smtpErr st -> pure ()
         | otherwise -> lift (Streaming.uncons str) >>= \ case
               Nothing
                   -> lift $ gets smtpErr >>= \e -> when (ok e) do
                          modify' \s -> s { smtpErr = DataErr $ eofErr "read" }
               Just (bs, bss)
                   | B.null bs
                   -> lift $ gets smtpErr >>= \e -> when (ok e) do
                          modify' \s -> s { smtpErr = DataErr $ eofErr "read" }
                   | B.length bs >= llenLimit st
                   -> lift $ gets smtpErr >>= \e -> when (ok e) do
                          modify' \s -> s { smtpErr = ProtoErr 401 $
                                            "Reply too long: " <> bs <> "..." }
                   | otherwise -> case parseReply bs of
                         Nothing -> pure ()
                         Just  r -> do
                             let count' = if replyCont r then count + 1 else 0
                             cmd <- case smtpState st of
                                 GREETING -> lift $ smtpGreeting count r
                                 EHLO     -> lift $ smtpHello count r
                                 STARTTLS -> lift $ smtpStartTLS count r
                                 QUIT     -> lift $ smtpQuit count r
                                 _        -> lift $ fail $
                                             "Unexpected SMTP state "
                                             ++ show (smtpState st)
                             when (not $ BC.null cmd) do
                                 Q.chunk cmd
                             loop count' bss

dosmtp :: ProtoState -> IO ProtoState
dosmtp start = do
  st <- ST.execStateT (sockSource & proto & sockSink) start
  case smtpErr st of
    SmtpOK
      | smtpState st == DOTLS
      -> do
         tlsst <- ST.execStateT startTLS st
         case smtpErr tlsst of
           SmtpOK -> ST.execStateT (tlsSource & proto & tlsSink) tlsst
           _      -> return tlsst
    _ -> return st