packages feed

postie-0.6.0.2: src/Network/Mail/Postie/Pipes.hs

module Network.Mail.Postie.Pipes
  ( dataChunks,
    attoParser,
    UnexpectedEndOfInputException,
    TooMuchDataException,
  )
where

import Control.Applicative
import Control.Exception (Exception, throw)
import Control.Monad (unless)
import qualified Data.Attoparsec.ByteString as AT
import qualified Data.ByteString.Char8 as BS
import Data.Maybe (fromMaybe)
import Data.Typeable (Typeable)
import Pipes
import Pipes.Parse
import Prelude hiding (lines)

data UnexpectedEndOfInputException = UnexpectedEndOfInputException
  deriving (Show, Typeable)

data TooMuchDataException = TooMuchDataException
  deriving (Show, Typeable)

instance Exception UnexpectedEndOfInputException

instance Exception TooMuchDataException

attoParser :: AT.Parser r -> Parser BS.ByteString IO (Maybe r)
attoParser p = do
  result <- AT.parseWith draw' p ""
  case result of
    AT.Done t r -> do
      unless (BS.null t) (unDraw t)
      return (Just r)
    _ -> return Nothing
  where
    draw' = fromMaybe "" <$> draw

dataChunks :: Int -> Producer BS.ByteString IO () -> Producer BS.ByteString IO ()
dataChunks n p = lines p >-> go n
  where
    go remaining | remaining <= 0 = throw TooMuchDataException
    go remaining = do
      bs <- await
      unless (bs == ".") $ do
        yield (unescape bs)
        yield "\r\n"
        go (remaining - BS.length bs - 2)
    unescape bs
      | BS.null bs = bs
      | BS.head bs == '.' && BS.length bs > 1 = BS.tail bs
      | otherwise = bs

lines :: Producer BS.ByteString IO () -> Producer BS.ByteString IO ()
lines = go
  where
    go p = do
      (line, leftover) <- lift $ runStateT lineParser p
      yield line
      go leftover

lineParser :: Parser BS.ByteString IO BS.ByteString
lineParser = go id
  where
    go f = do
      bs <- maybe (throw UnexpectedEndOfInputException) (return . f) =<< draw
      case BS.elemIndex '\r' bs of
        Nothing -> go (BS.append bs)
        Just n -> do
          let here = killCR $ BS.take n bs
              rest = BS.drop (n + 1) bs
          unDraw rest
          return here
    killCR bs
      | BS.null bs = bs
      | BS.head bs == '\n' || BS.head bs == '\r' = killCR $ BS.tail bs
      | BS.last bs == '\n' || BS.last bs == '\r' = killCR $ BS.init bs
      | otherwise = bs