postie-0.6.0.2: src/Network/Mail/Postie/Session.hs
module Network.Mail.Postie.Session
( runSession,
mkSessionEnv,
mkSessionID,
)
where
import Control.Applicative
import Control.Arrow ((&&&))
import Control.Monad.Reader
import Control.Monad.State
import Data.ByteString (ByteString)
import qualified Network.TLS as TLS
import qualified Pipes.Parse as P
import Network.Mail.Postie.Address
import Network.Mail.Postie.Connection
import Network.Mail.Postie.Pipes
import Network.Mail.Postie.Protocol (Event (..), Reply, renderReply, reply, reply')
import qualified Network.Mail.Postie.Protocol as SMTP
import Network.Mail.Postie.SessionID
import Network.Mail.Postie.Settings
import Network.Mail.Postie.Types
import Prelude hiding (lines)
data SessionEnv
= SessionEnv
{ sessionID :: SessionID,
sessionApp :: Application,
sessionSettings :: Settings,
sessionConnection :: Connection,
sessionServerParams :: Maybe TLS.ServerParams
}
data SessionState
= SessionState
{ sessionProtocol :: SMTP.SmtpFSM,
sessionTransaction :: Transaction
}
type SessionM a = ReaderT SessionEnv (StateT SessionState IO) a
data Transaction
= TxnInitial
| TxnHaveAuth ByteString
| TxnHaveMailFrom (Maybe ByteString) Address
| TxnHaveRecipient (Maybe ByteString) Address [Address]
mkSessionEnv :: SessionID -> Application -> Settings -> Connection -> Maybe TLS.ServerParams -> SessionEnv
mkSessionEnv = SessionEnv
runSession :: SessionEnv -> IO ()
runSession env = evalStateT (runReaderT startSession env) session
where
session =
SessionState
{ sessionProtocol = SMTP.initSmtpFSM,
sessionTransaction = TxnInitial
}
startSession :: SessionM ()
startSession = do
sendReply $ reply 220 "hello!"
sessionLoop
sessionLoop :: SessionM ()
sessionLoop = do
(event, fsm') <- SMTP.step <$> getSmtpFsm <*> getCommand <*> getTlsStatus <*> getAuthStatus
case event of
WantQuit -> do
sendReply $ reply 221 "goodbye"
return ()
_ -> do
modify (\ss -> ss {sessionProtocol = fsm'})
handleEvent event >> sessionLoop
where
getSmtpFsm = gets sessionProtocol
getTlsStatus = do
SessionEnv
{ sessionConnection = conn,
sessionSettings = settings
} <-
ask
isSecure <- liftIO (connIsSecure conn)
return $ case settingsStartTLSPolicy settings of
Just p
| isSecure -> SMTP.Active
| p == AllowStartTLS -> SMTP.Permitted
| p == DemandStartTLS -> SMTP.Required
_ -> SMTP.Forbidden
getAuthStatus = do
reqAuth <- asks (settingsRequireAuth . sessionSettings)
txn <- gets sessionTransaction
return $ case txn of
TxnInitial -> if reqAuth then SMTP.AuthRequired else SMTP.NoAuth
TxnHaveAuth _ -> SMTP.Authed
TxnHaveMailFrom (Just _) _ -> SMTP.Authed
TxnHaveRecipient (Just _) _ _ -> SMTP.Authed
_ -> SMTP.NoAuth
preserveAuth :: (Maybe ByteString -> Transaction) -> Transaction -> Transaction
preserveAuth f t = case t of
TxnInitial -> f Nothing
TxnHaveAuth d -> f (Just d)
TxnHaveMailFrom a _ -> f a
TxnHaveRecipient a _ _ -> f a
handleHelo :: ByteString -> SessionM HandlerResponse
handleHelo x = do
SessionEnv
{ sessionID = sid,
sessionSettings = settings
} <-
ask
let handler = settingsOnHello settings
liftIO $ handler sid x
handleEvent :: SMTP.Event -> SessionM ()
handleEvent (SayHelo x) = do
result <- handleHelo x
handlerResponse result (sendReply ok)
handleEvent (SayEhlo x) = do
result <- handleHelo x
handlerResponse result $
sendReply =<< ehloAdvertisement
handleEvent (SayEhloAgain _) = sendReply ok
handleEvent (SayHeloAgain _) = sendReply ok
handleEvent SayOK = sendReply ok
handleEvent (SetMailFrom x) = do
SessionEnv
{ sessionID = sid,
sessionSettings = settings
} <-
ask
let handler = settingsOnMailFrom settings
result <- liftIO $ handler sid x
handlerResponse result $ do
modify (\ss -> ss {sessionTransaction = preserveAuth (`TxnHaveMailFrom` x) (sessionTransaction ss)})
sendReply ok
handleEvent (AddRcptTo x) = do
SessionEnv
{ sessionID = sid,
sessionSettings = settings
} <-
ask
let handler = settingsOnRecipient settings
result <- liftIO $ handler sid x
handlerResponse result $ do
txn <- gets sessionTransaction
let txn' = case txn of
(TxnHaveMailFrom a y) -> TxnHaveRecipient a y [x]
(TxnHaveRecipient a y xs) -> TxnHaveRecipient a y (x : xs)
_ -> error "impossible"
modify (\ss -> ss {sessionTransaction = txn'})
sendReply ok
handleEvent StartData = do
sendReply $ reply 354 "End data with <CR><LF>.<CR><LF>"
SessionEnv
{ sessionID = sid,
sessionApp = app,
sessionSettings = settings,
sessionConnection = conn
} <-
ask
(TxnHaveRecipient auth sender recipients) <- gets sessionTransaction
let chunks = dataChunks (settingsMaxDataSize settings) (toProducer conn)
let mail = Mail sid auth sender recipients chunks
result <- liftIO $ app mail
handlerResponse result $ do
sendReply ok
modify (\ss -> ss {sessionTransaction = TxnInitial})
handleEvent WantTls = do
SessionEnv
{ sessionID = sid,
sessionConnection = conn,
sessionSettings = settings,
sessionServerParams = Just serverParams
} <-
ask
let handler = settingsOnStartTLS settings
liftIO $ handler sid
sendReply ok
liftIO $ connSetSecure conn serverParams
modify (\ss -> ss {sessionTransaction = TxnInitial})
handleEvent (WantAuth d) = do
(sid, settings) <- asks (sessionID &&& sessionSettings)
let handler = settingsOnAuth settings
result <- liftIO $ handler sid d
handlerResponse result $ do
sendReply ok
modify (\ss -> ss {sessionTransaction = TxnHaveAuth d})
handleEvent WantReset = do
sendReply ok
modify (\ss -> ss {sessionTransaction = TxnInitial})
handleEvent TlsAlreadyActive =
sendReply $ reply 454 "STARTTLS not supported (already active)"
handleEvent TlsNotSupported =
sendReply $ reply 454 "STARTTLS not supported"
handleEvent NeedStartTlsFirst =
sendReply $ reply 530 "Issue STARTTLS first"
handleEvent NeedAuthFirst =
sendReply $ reply 530 "5.7.1 Authentication required"
handleEvent NeedHeloFirst =
sendReply $ reply 503 "Need EHLO first"
handleEvent NeedMailFromFirst =
sendReply $ reply 503 "Need MAIL FROM first"
handleEvent NeedRcptToFirst =
sendReply $ reply 503 "Need RCPT TO first"
handleEvent _ = error "impossible"
handlerResponse :: HandlerResponse -> SessionM () -> SessionM ()
handlerResponse Accepted action = action
handlerResponse Rejected _ = sendReply reject
getCommand :: SessionM SMTP.Command
getCommand = do
input <- toProducer `fmap` asks sessionConnection
result <- liftIO $ P.evalStateT (attoParser SMTP.parseCommand) input
case result of
Nothing -> do
sendReply $ reply 500 "Syntax error, command unrecognized"
getCommand
Just command -> return command
ehloAdvertisement :: SessionM Reply
ehloAdvertisement = do
stls <- startTls
let extensions = "8BITMIME" : stls
return $ reply' 250 (extensions ++ ["OK"])
where
startTls = do
SessionEnv
{ sessionConnection = conn,
sessionSettings = settings
} <-
ask
secure <- liftIO (connIsSecure conn)
return
[ "STARTTLS"
| not secure
&& ( case settingsStartTLSPolicy settings of
Just _ -> True
_ -> False
)
]
ok :: Reply
ok = reply 250 "OK"
reject :: Reply
reject = reply 554 "Transaction failed"
sendReply :: Reply -> SessionM ()
sendReply r = do
conn <- asks sessionConnection
liftIO $ connSend conn (renderReply r)