postmaster-0.2: Postmaster/Main.hs
{- |
Module : Postmaster.Main
Copyright : (c) 2004-2008 by Peter Simons
License : GPL2
Maintainer : simons@cryp.to
Stability : provisional
Portability : Haskell 2-pre
Postmaster's IO driver and general initialization functions.
-}
module Postmaster.Main where
import Prelude hiding ( catch )
import Data.Maybe
import Control.Concurrent.MVar
import Control.Monad.RWS hiding ( local )
import Control.Monad.State
import System.IO
import System.Posix.Signals
import Network ( PortID(..) )
import Network.Socket
import Network.BSD ( getHostName )
import ADNS
import Foreign
import Postmaster.Base
import Postmaster.FSM
import Postmaster.FSM.DNSResolver ( setDNSResolver )
import Postmaster.FSM.EventHandler ( setEventHandler )
import Postmaster.FSM.PeerAddr ( setPeerAddr )
import Postmaster.FSM.SessionState ( setSessionState )
import Postmaster.IO
import Text.ParserCombinators.Parsec.Rfc2821
import System.Posix.Syslog
-- * Speaking ESMTP
-- |This function ties it all together to build a
-- 'BlockHandler' for "System.IO.Driver".
smtpdHandler :: WriteHandle -> GlobalEnv -> BlockHandler SmtpdState
smtpdHandler hOut theEnv buf = runSmtpd (smtpd buf >>= handler) theEnv
where
handler :: ([SmtpReply], Buffer) -> Smtpd Buffer
handler (rs, buf') = do
safeWrite (hPutStr hOut (concatMap show rs) >> hFlush hOut)
when (any isShutdown rs) (fail "shutdown")
return buf'
-- |The unified interface to dialog and data section. This
-- function relies on the fact that pipelining ends with
-- @DATA@ commands: dialog and payload must /not/ come in a
-- single buffer. See
-- <http://www.faqs.org/rfcs/rfc2920.html> section 3.1.
smtpd :: Buffer -> Smtpd ([SmtpReply], Buffer)
smtpd buf@(Buf _ _ 0) = return ([], buf)
smtpd buf@(Buf _ ptr n) = do
sst <- getSessionState
if (sst == HaveData)
then do (r, buf') <- feed buf
return (maybeToList r, buf')
else do xs <- liftIO (peekArray (fromIntegral n) ptr)
let xs' = map (toEnum . fromEnum) xs
ls' = splitList "\r\n" xs'
ls = reverse . tail $ reverse ls'
rest = head $ reverse ls'
i = length xs - length rest
rs <- mapM handleDialog ls
buf' <- liftIO $ flush (fromIntegral i) buf
return (rs, buf')
handleDialog :: String -> Smtpd SmtpReply
handleDialog line = do
sst <- getSessionState
let (e, sst') = runState (smtpdFSM (fixCRLF line)) sst
r <- trigger e
case (e, isSuccess r) of
(_, True) -> setSessionState sst'
(StartData, False) -> trigger ResetState >> setSessionState HaveHelo
_ -> return ()
return r
-- * Running 'Smtpd' Computations
-- |Run the given 'Smtpd' computation and write the logging
-- output via the given 'Logger'. Use 'withGlobalEnv' to
-- create the global environment and 'emptyEnv' to
-- initialize the 'SmtpdState'.
runSmtpd' :: Logger -> Smtpd a -> GlobalEnv -> SmtpdState
-> IO (a, SmtpdState)
runSmtpd' logger f = runStateT . withLogger logger f
-- |Convenience wrapper for 'runSmtpd'' with 'Logger'
-- hard-coded to 'syslogger'.
runSmtpd :: Smtpd a -> GlobalEnv -> SmtpdState
-> IO (a, SmtpdState)
runSmtpd = runSmtpd' syslogger
-- |Run the given computation with an initialized global
-- environment for 'Smtpd'. The environment is destroyed
-- when this function returns.
withGlobalEnv :: HostName -- ^ 'myHeloName'
-> Resolver
-> EventT
-> (GlobalEnv -> IO a)
-> IO a
withGlobalEnv myHelo dns eventT f = do
let eventH = eventT (mkEvent myHelo "/tmp/test-spool")
initEnv = do setDNSResolver dns
setEventHandler eventH
newMVar (execState initEnv emptyEnv) >>= f
-- * ESMTP Network Daemon
smtpdServer :: Capacity -> GlobalEnv -> SocketHandler
smtpdServer cap theEnv =
handleLazy ReadWriteMode $ \(h, Just sa) -> do
hSetBuffering h (BlockBuffering (Just (fromIntegral cap)))
let st = execState (setPeerAddr sa) emptyEnv
smtpdMain cap theEnv h h st >> return ()
smtpdMain :: Capacity
-> GlobalEnv
-> ReadHandle
-> WriteHandle
-> SmtpdState
-> IO SmtpdState
smtpdMain cap theEnv hIn hOut initST = do
let greet = do r <- trigger Greeting
safeReply hOut r >> safeFlush hOut
to <- getReadTimeout
sid <- mySessionID
return (r, to, sid)
((r, to, sid), st) <- runSmtpd greet theEnv initST
st' <- case r of
Reply (Code Success _ _) _ -> do
let getTO = evalState (getVarDef (mkVar "ReadTimeout") to)
yellIO = syslogger . LogMsg sid st
hMain = smtpdHandler hOut theEnv
errH e st' = yellIO (CaughtException e) >> return st'
runLoopNB getTO errH hIn cap hMain st
_ -> return st
let bye = do sst <- getSessionState
when (sst /= HaveQuit)
(trigger Shutdown >> return ())
fmap snd (runSmtpd bye theEnv st')
main' :: Capacity -> PortID -> EventT -> IO ()
main' cap port eventT = do
_ <- installHandler sigPIPE Ignore Nothing
_ <- installHandler sigCHLD (Catch (return ())) Nothing
whoami <- getHostName
withSocketsDo $
withSyslog "postmaster" [PID, PERROR] MAIL $
initResolver [NoErrPrint,NoServerWarn] $ \dns ->
withGlobalEnv whoami dns eventT $
listener port . smtpdServer cap
main :: IO ()
main = main' 1024 (PortNumber 2525) id
-- ** Logging
type Logger = LogMsg -> IO ()
-- |Bind the logging back-end to demote 'Smtpd' to an
-- ordinary 'StateT' computation.
withLogger :: Logger -> Smtpd a -> GlobalEnv -> StateT SmtpdState IO a
withLogger logger f theEnv = do
(a, st, w) <- get >>= liftIO . runRWST f theEnv
liftIO $ mapM_ logger w
put st
return a
-- |Our default logging back-end.
syslogger :: Logger
syslogger (LogMsg sid _ e) = syslog Info $
showString "SID " . shows sid . (':':) . (' ':) $ show e