{-# LANGUAGE OverloadedStrings #-}

module Milter.Switch (milter) where

import Control.Applicative
import Control.Exception
import Control.Monad (unless)
import Data.ByteString.Char8 (ByteString)
import Data.IORef
import MailSpec
import Milter.Base
import Milter.Env
import Milter.Log
import Milter.Types
import Network.DomainAuth
import RPF
import System.IO

----------------------------------------------------------------

milter :: Env -> Handle -> IORef State -> IO ()
milter env hdl ref = withValidHandleDo $
    handle errorHandle $ do
      rpkt <- getPacket hdl
      switch env hdl ref rpkt
      milter env hdl ref
  where
    errorHandle (SomeException e) = logDebug env ref $ show e
    withValidHandleDo blk = do
        closed <- hIsClosed hdl
        eof <- hIsEOF hdl
        unless (eof || closed) blk

switch :: Env -> Handle -> IORef State -> Packet -> IO ()
switch env hdl ref (Packet 'O' bs) = open env hdl ref bs
switch env hdl ref (Packet 'C' bs) = conn env hdl ref bs
switch env hdl ref (Packet 'H' bs) = helo env hdl ref bs
switch env hdl ref (Packet 'M' bs) = mfro env hdl ref bs
switch _   hdl _   (Packet 'R' _ ) = continue hdl
switch _   hdl _   (Packet 'A' _ ) = continue hdl -- xxx
switch env hdl ref (Packet 'L' bs) = hedr env hdl ref bs
switch env hdl ref (Packet 'N' bs) = eohd env hdl ref bs
switch env hdl ref (Packet 'B' bs) = body env hdl ref bs
switch env hdl ref (Packet 'E' bs) = eoms env hdl ref bs
switch _   _   _   (Packet 'D'  _) = return ()
switch _   _   _   (Packet 'Q'  _) = return ()
switch env _   ref (Packet x    _) = logError env ref $ "Switch: " ++ [x]

----------------------------------------------------------------

mfilter :: Env -> Handle -> IORef State -> MailSpec -> BlockName -> IO ()
mfilter env hdl ref ms bn =
    let ply = policy env
    in case evalRPF ms ply bn of
         (l,A_Accept)   -> doit accept  "Accepted"  l
         (l,A_Discard)  -> doit discard "Discarded" l
         (l,A_Hold)     -> doit hold    "Held"      l
         (l,A_Reject)   -> doit reject  "Rejected"  l
         (_,A_Continue) -> if bn == B_Body
                           then logResult env ref "Accepted by default" >> continue hdl
                           else logMonitor env ref "continue" >> continue hdl
  where
    doit act m l = do
        logResult env ref $ m ++ " in line " ++ show l
        if logonly env
           then accept hdl
           else act hdl

----------------------------------------------------------------

type Filter = Env -> Handle -> IORef State -> ByteString -> IO ()

----------------------------------------------------------------

open :: Filter
open env hdl ref _ = do
    logMonitor env ref "Milter opened"
    negotiate hdl

----------------------------------------------------------------

conn :: Filter
conn env hdl ref bs = do
    st <- readIORef ref
    let ip = getIP bs
        ms = (mailspec st) { msPeerIP = ip }
    writeIORef ref st { mailspec = ms }
    -- after IP set for logging
    logResult env ref "SMTP connected"
    logDebug env ref $ '\t' : show ms
    mfilter env hdl ref ms B_Connect

----------------------------------------------------------------

helo :: Filter
helo env hdl ref _ = do
  logMonitor env ref "HELO"
  continue hdl

----------------------------------------------------------------

mfro :: Filter
mfro env hdl ref bs = do
    logMonitor env ref "MAIL FROM"
    let jmailfrom = extractDomain bs
    case jmailfrom of
      Nothing -> continue hdl -- xxx
      Just dom  -> do
          st <- readIORef ref
          xspf <- getSPF dom st
          let ms = (mailspec st) { msSPFResult = xspf, msMailFrom = jmailfrom }
          writeIORef ref st { mailspec = ms }
          logDebug env ref $ '\t' : show ms
          mfilter env hdl ref ms B_MailFrom
  where
    getSPF dom st = do
        let ip = msPeerIP (mailspec st)
        spf env dom ip

----------------------------------------------------------------

hedr :: Filter
hedr env hdl ref bs = do
    logMonitor env ref "DATA HEADER FIELD"
    st <- readIORef ref
    let (key,val) = getKeyVal bs
        ckey = canonicalizeKey key
        xm = pushField key val (xmail st)
        prd = pushPRD key val (prdspec st)
        (pv,ms) = checkField ckey val (parsedv st) (mailspec st)
    writeIORef ref $ st { xmail = xm, mailspec = ms, prdspec = prd, parsedv = pv}
    logDebug env ref $ "\t" ++ show xm ++ "\n\t" ++ show ms ++ "\n\t" ++ show prd
    continue hdl
  where
    checkField ckey val pv ms
      | ckey == dkFieldKey   = case parseDK val of
          Nothing -> (pv, ms)
          x@(Just pdk) -> (pv { mpdk = x},
                           ms { msSigDK = True
                              , msDKFrom = Just (dkDomain pdk) })
      | ckey == dkimFieldKey = case parseDKIM val of
          Nothing -> (pv, ms)
          x@(Just pdkim) -> (pv { mpdkim = x},
                             ms { msSigDKIM = True
                                , msDKIMFrom = Just (dkimDomain pdkim) })
      | otherwise = (pv, ms)

----------------------------------------------------------------

eohd :: Filter
eohd env hdl ref _ = do
    logMonitor env ref "DATA HEADER END"
    st <- readIORef ref
    let jfrom = getFrom st
        jprd  = decidePRD (prdspec st)
    sid <- getSenderID jprd
    let ms = (mailspec st) { msSenderIDResult = sid, msFrom = jfrom, msPRA = jprd }
    writeIORef ref st { mailspec = ms }
    mfilter env hdl ref ms B_Header
  where
    getFrom = decideFrom . prdspec
    getSenderID jprd =
        case jprd of
          Nothing  -> return DAPermError
          Just dom -> do
              ms <- mailspec <$> readIORef ref
              if jprd == msMailFrom ms
                 then return $ msSPFResult ms
                 else let ip = msPeerIP ms
                      in spf env dom ip

----------------------------------------------------------------

body :: Filter
body env hdl ref bs = do
    logMonitor env ref "DATA BODY"
    st <- readIORef ref
    let bc = getBody bs
        xm = pushBody bc (xmail st)
    writeIORef ref st { xmail = xm }
    continue hdl

----------------------------------------------------------------

eoms :: Filter
eoms env hdl ref _ = do
  logMonitor env ref "DATA BODY END"
  st <- readIORef ref
  let mail = finalizeMail (xmail st)
      mdk = mpdk (parsedv st)
      mdkim = mpdkim (parsedv st)
  xdk <- maybe (return DANone) (dk env mail) mdk
  xdkim <- maybe (return DANone) (dkim env mail) mdkim
  let ms = (mailspec st) { msDKResult = xdk, msDKIMResult = xdkim }
  mfilter env hdl ref ms B_Body
