{-# LANGUAGE OverloadedStrings #-}

module Network.DomainAuth.Mail.Parser (
    readMail
  , getMail
  , parseTaggedValue
  ) where

import qualified Data.ByteString as BS
import Data.Word
import Network.DomainAuth.Mail.Types
import Network.DomainAuth.Mail.XMail
import Network.DomainAuth.Utils

-- $setup
-- >>> :set -XOverloadedStrings

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

-- | Obtain 'Mail' from a file.
readMail :: FilePath -> IO Mail
readMail file = getMail <$> BS.readFile file

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

-- | Obtain 'Mail' from 'RawMail'.
--
-- >>> let out1 = finalizeMail $ pushBody "body" $ pushField "to" "val" $ pushField "from" "val" initialXMail
-- >>> getMail "from: val\nto: val\n\nbody" == out1
-- True
-- >>> let out2 = finalizeMail $ pushBody "body" $ pushField "to" "val" $ pushField "from" "val\tval" initialXMail
-- >>> getMail "from: val\tval\nto: val\n\nbody" == out2
-- True
-- >>> let out3 = finalizeMail $ pushBody "" $ pushField "to" "val" $ pushField "from" "val" initialXMail
-- >>> getMail "from: val\nto: val\n" == out3
-- True
getMail :: RawMail -> Mail
getMail bs = finalizeMail $ pushBody rbdy xmail
  where
    (rhdr,rbdy) = splitHeaderBody bs
    rflds = splitFields rhdr
    xmail = foldl push initialXMail rflds
    push m fld = let (k,v) = parseField fld
                 in pushField k v m

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

splitHeaderBody :: RawMail -> (RawHeader,RawBody)
splitHeaderBody bs = case mcnt of
    Nothing  -> (bs,"")
    Just cnt -> check (BS.splitAt cnt bs)
  where
    mcnt = findEOH bs 0
    check (hdr,bdy) = (hdr, dropSep bdy)
    dropSep bdy
      | len == 0 = ""
      | len == 1 = ""
      | otherwise = if b1 == cCR then bdy3 else bdy2
      where
        len = BS.length bdy
        b1 = BS.head bdy
        bdy2 = BS.tail bdy
        bdy3 = BS.tail bdy2

findEOH :: RawMail -> Int -> Maybe Int
findEOH "" _ = Nothing
findEOH bs cnt
  | b0 == cLF && bs1 /= "" && b1 == cLF = Just (cnt + 1)
  | b0 == cLF && bs1 /= "" && b1 == cCR
              && bs2 /= "" && b2 == cLF = Just (cnt + 1)
  | otherwise                           = findEOH bs1 (cnt + 1)
  where
    b0  = BS.head bs
    bs1 = BS.tail bs
    b1  = BS.head bs1
    bs2 = BS.tail bs1
    b2  = BS.head bs2

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

splitFields :: RawHeader -> [RawField]
splitFields "" = []
splitFields bs = fld : splitFields bs''
  where
    -- split before cLF for efficiency
    (fld,bs') = BS.splitAt (findFieldEnd bs 0 - 1) bs
    bs'' = BS.tail bs'

findFieldEnd :: RawMail -> Int -> Int
findFieldEnd bs cnt
    | bs == ""   = cnt
    | b  == cLF = begOfLine bs' (cnt + 1)
    | otherwise  = findFieldEnd bs' (cnt + 1)
  where
    b   = BS.head bs
    bs' = BS.tail bs

begOfLine :: RawMail -> Int -> Int
begOfLine bs cnt
    | bs == ""      = cnt
    | isContinued b = findFieldEnd bs' (cnt + 1)
    | otherwise     = cnt
  where
    b   = BS.head bs
    bs' = BS.tail bs

isContinued :: Word8 -> Bool
isContinued = isSpace

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

parseField :: RawField -> (RawFieldKey,RawFieldValue)
parseField bs = (k,v')
  where
    (k,v) = break' cColon bs
    -- Sendmail drops ' ' after ':'.
    v' = if v /= "" && BS.head v == cSP
         then BS.tail v
         else v

----------------------------------------------------------------
-- This breaks spaces in the note tag.

-- | Parsing field value of tag=value.
--
-- >>> parseTaggedValue " k = rsa ; p= MIGfMA0G; n=A 1024 bit key;"
-- [("k","rsa"),("p","MIGfMA0G"),("n","A1024bitkey")]
-- >>> parseTaggedValue " k = \nrsa ;\n p= MIGfMA0G;\n n=A 1024 bit key"
-- [("k","rsa"),("p","MIGfMA0G"),("n","A1024bitkey")]
parseTaggedValue :: RawFieldValue -> [(BS.ByteString,BS.ByteString)]
parseTaggedValue xs = vss
  where
    v = BS.filter (not.isSpace) xs
    vs = filter (/= "") $ BS.split cSemiColon v
    vss = map (break' cEqual) vs
