aboutsummaryrefslogtreecommitdiff
path: root/src/Mail/Parser.hs
blob: 090602c4e98bb2a03a2f6b40e4bcd7207d26bf09 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
module Mail.Parser (run, Message (..), Part (..)) where

import Common (startsWith, trim)
import Control.Monad.State (State, evalState, get, gets, modify)
import Data.Char (isSpace)
import Data.List (intercalate)
import Intro
import Mail.Header (Header)
import qualified Mail.Header as H

data Message = Message
  { headers :: [Header]
  , body :: [Part]
  }
  deriving (Show)

data Part = Part
  { headers :: [Header]
  , body :: String -- TOOD: Scoped Fields extension
  }
  deriving (Show, Eq)

data ParserState = InHeader (Maybe Header) | InBody deriving (Show, Eq)

data Parser = Parser
  { pHeaders :: [Header]
  , pBody :: [String]
  , pState :: ParserState
  , pErrors :: [String]
  }
  deriving (Show)

type MessageM = State Parser Message

type PartM = State Parser Part

newParser :: Parser
newParser = Parser [] [] (InHeader Nothing) []

run :: [String] -> Message
run xs = evalState (parseMail xs) newParser

parseMail :: [String] -> MessageM
parseMail [] = gets (evalState parseMailBody)
parseMail (x : xs) = do
  s <- get
  case pState s of
    InHeader Nothing
      | trim x == "" -> modify (setState InBody)
      | otherwise -> case kv x of
          Just (k, v) -> modify (setInHeader (k, v))
          Nothing -> modify (setInHeaderStart . pushError x)
    InHeader (Just (k, v))
      | trim x == "" -> modify (setState InBody . pushHeader (k, v))
      | startsWith isSpace x -> modify (setInHeader (k, v ++ x))
      | otherwise -> case kv x of
          Just (k', v') -> modify (setInHeader (k', v') . pushHeader (k, v))
          Nothing -> modify (setInHeaderStart . pushHeader (k, v) . pushError x)
    InBody -> modify (pushBody x)
  parseMail xs

parseMailBody :: MessageM
parseMailBody = do
  s <- get
  pure $ case getBoundary s of
    Just boundary ->
      Message
        { headers = pHeaders s
        , body = getPart <$> bodyParts (reverse $ pBody s) boundary
        }
    Nothing ->
      Message
        { headers = pHeaders s
        , body = [Part{headers = [], body = intercalate "\r\n" . reverse $ pBody s}]
        }
 where
  getBoundary s = H.boundary =<< (H.contentType . pHeaders) s
  getPart part = evalState (parsePart part) newParser

parsePart :: [String] -> PartM
parsePart [] = do
  s <- get
  pure $ Part{headers = pHeaders s, body = intercalate "\r\n" . reverse $ pBody s}
parsePart (x : xs) = do
  s <- get
  case pState s of
    InHeader Nothing
      | trim x == "" -> modify (setState InBody)
      | otherwise -> case kv x of
          Just (k, v) -> modify (setInHeader (k, v))
          Nothing -> modify (setInHeaderStart . pushError x)
    InHeader (Just (k, v))
      | trim x == "" -> modify (setState InBody . pushHeader (k, v))
      | startsWith isSpace x -> modify (setInHeader (k, v ++ x))
      | otherwise -> case kv x of
          Just (k', v') -> modify (setInHeader (k', v') . pushHeader (k, v))
          Nothing -> modify (setInHeaderStart . pushHeader (k, v) . pushError x)
    InBody -> modify (pushBody x)
  parsePart xs

pushHeader :: Header -> Parser -> Parser
pushHeader x p = p{pHeaders = (trim <$> x) : pHeaders p}

pushBody :: String -> Parser -> Parser
pushBody x p = p{pBody = trim x : pBody p}

pushError :: String -> Parser -> Parser
pushError x p = p{pErrors = x : pErrors p}

setState :: ParserState -> Parser -> Parser
setState x p = p{pState = x}

setInHeaderStart :: Parser -> Parser
setInHeaderStart p = p{pState = InHeader Nothing}

setInHeader :: Header -> Parser -> Parser
setInHeader x p = p{pState = InHeader $ Just x}

kv :: String -> Maybe (String, String)
kv x =
  case break (== ':') x of
    (k, _ : v) -> Just (k, v)
    (_, []) -> Nothing

bodyParts :: [String] -> String -> [[String]]
bodyParts lines' boundary = filter (not . isEmptyPart) $ go lines' [] []
 where
  go :: [String] -> [String] -> [[String]] -> [[String]]
  go [] _ parts = parts
  go (x : xs) acc parts
    | trim x == ("--" ++ boundary) = go xs [] (reverse acc : parts)
    | otherwise = go xs (x : acc) parts

isEmptyPart :: [String] -> Bool
isEmptyPart [""] = True
isEmptyPart _ = False