7 | 7 |
import qualified Data.Attoparsec.ByteString as Atto
|
8 | 8 |
import Data.ByteString (ByteString)
|
9 | 9 |
import qualified Data.ByteString as B
|
| 10 |
import qualified Data.ByteString.Lazy as BL
|
| 11 |
import qualified Data.ByteString.Builder as BB
|
| 12 |
import qualified Data.Char as Char
|
10 | 13 |
import qualified Data.Foldable as Fold
|
11 | 14 |
import Data.Sequence (Seq)
|
12 | 15 |
import qualified Data.Sequence as Seq
|
|
16 | 19 |
|
17 | 20 |
import Network.Courriel.Types
|
18 | 21 |
|
| 22 |
-- The comments here are taken directly from RFC 5322
|
| 23 |
|
| 24 |
|
| 25 |
-- word = atom / quoted-string
|
| 26 |
-- phrase = 1*word / obs-phrase
|
19 | 27 |
pPhrase :: Parser ByteString
|
20 | |
pPhrase = undefined
|
| 28 |
pPhrase = pQuotedString <|>
|
| 29 |
(B.intercalate (B.singleton 32) <$> Atto.many' pWord)
|
21 | 30 |
|
| 31 |
pWord :: Parser ByteString
|
| 32 |
pWord = pCfwsMb *> Atto.takeWhile1 atext <* pCfwsMb
|
| 33 |
|
| 34 |
pQuotedString :: Parser ByteString
|
| 35 |
pQuotedString = do
|
| 36 |
pCfwsMb
|
| 37 |
void (Atto.word8 34)
|
| 38 |
let ch = Atto.satisfy (\ c -> c /= 92 && c /= 34) <|> pQuotedPair
|
| 39 |
text <- Atto.many' $ do
|
| 40 |
w <- pFwsMb
|
| 41 |
c <- BB.word8 <$> ch
|
| 42 |
return (w `mappend` c)
|
| 43 |
rs <- pFwsMb
|
| 44 |
void (Atto.word8 34)
|
| 45 |
pCfwsMb
|
| 46 |
return (BL.toStrict (BB.toLazyByteString (mconcat text `mappend` rs)))
|
| 47 |
|
| 48 |
-- mailbox = name-addr / addr-spec
|
| 49 |
-- name-addr = [display-name] angle-addr
|
| 50 |
-- angle-addr = [CFWS] "<" addr-spec ">" [CFWS] /
|
| 51 |
-- obs-angle-addr
|
22 | 52 |
pMailbox :: Parser Mailbox
|
23 | 53 |
pMailbox = nameAddr <|> (Mailbox Nothing <$> pAddrSpec)
|
24 | 54 |
where nameAddr =
|
25 | |
Mailbox <$> (Just <$> pPhrase)
|
26 | |
<*> (Atto.word8 60 *> pAddrSpec <* Atto.word8 62)
|
| 55 |
Mailbox <$> ((Just <$> pPhrase) <|> pure Nothing)
|
| 56 |
<*> (pCfwsMb *> Atto.word8 60 *> pAddrSpec <* Atto.word8 62 <* pCfwsMb)
|
27 | 57 |
|
| 58 |
-- (mailbox *("," mailbox)) / obs-mbox-list
|
| 59 |
|
| 60 |
pMailboxList :: Parser (Seq Mailbox)
|
| 61 |
pMailboxList = do
|
| 62 |
m <- pMailbox
|
| 63 |
ms <- Atto.option Seq.empty (Atto.word8 44 *> pMailboxList)
|
| 64 |
return (m Seq.<| ms)
|
| 65 |
|
| 66 |
-- address-list = (address *("," address)) / obs-addr-list
|
| 67 |
|
| 68 |
pAddressList :: Parser (Seq MBGroup)
|
| 69 |
pAddressList = do
|
| 70 |
m <- pAddress
|
| 71 |
ms <- Atto.option Seq.empty (Atto.word8 44 *> pAddressList)
|
| 72 |
return (m Seq.<| ms)
|
| 73 |
|
| 74 |
pAddress :: Parser MBGroup
|
| 75 |
pAddress = MBSingle <$> pMailbox <|> MBGroup <$> pPhrase <*> grp
|
| 76 |
where grp = (Atto.word8 58 *> (Atto.option mempty pGroupList) <* Atto.word8 59 <* pCfwsMb)
|
| 77 |
|
| 78 |
pGroupList :: Parser (Seq Mailbox)
|
| 79 |
pGroupList = pMailboxList <|> (mempty <$ pCfws)
|
| 80 |
|
| 81 |
-- dot-atom = [CFWS] dot-atom-text [CFWS]
|
| 82 |
-- dot-atom-text = 1*atext *("." 1*atext)
|
28 | 83 |
pDotAtom :: Parser ByteString
|
29 | |
pDotAtom = Atto.takeWhile (\ c -> atext c || c == 46)
|
| 84 |
pDotAtom = pCfwsMb *> Atto.takeWhile (\ c -> atext c || c == 46) <* pCfwsMb
|
30 | 85 |
|
31 | 86 |
pAddrSpec :: Parser Address
|
32 | 87 |
pAddrSpec = Address <$> pLocalPart <*> (Atto.word8 64 *> pDomain)
|
|
34 | 89 |
pDomain = pDotAtom
|
35 | 90 |
|
36 | 91 |
atext :: Word8 -> Bool
|
37 | |
atext c = c < 128 && c /= 40 && c /= 41
|
38 | |
&& c /= 58 && c /= 59
|
39 | |
&& c /= 60 && c /= 62
|
40 | |
&& c /= 64 && c /= 91
|
41 | |
&& c /= 92 && c /= 93
|
42 | |
&& c /= 34
|
| 92 |
atext c =
|
| 93 |
let c' = Char.chr (fromIntegral c)
|
| 94 |
in Char.isAlpha c' || Char.isDigit c' || c' `elem` ("!#$%&'*+-/=?^_`{|}~" :: [Char])
|
43 | 95 |
|
44 | 96 |
getSubject :: RawEmail -> ByteString
|
45 | 97 |
getSubject RawEmail { _rawEmailHeaders = hs } =
|
|
67 | 119 |
"Subject" -> HeaderSubject <$> foldingHeader
|
68 | 120 |
"Comments" -> HeaderComments <$> foldingHeader
|
69 | 121 |
"Keywords" -> HeaderKeywords <$> foldingHeader
|
| 122 |
"Sender" -> HeaderSender <$> pMailbox <* pCrlf
|
| 123 |
"From" -> HeaderFrom <$> pMailboxList <* pCrlf
|
| 124 |
"Reply-To" -> HeaderReplyTo <$> pAddressList <* pCrlf
|
| 125 |
"To" -> HeaderTo <$> pAddressList <* pCrlf
|
| 126 |
"Cc" -> HeaderCc <$> pAddressList <* pCrlf
|
| 127 |
"Bcc" -> HeaderBcc <$> pAddressList <* pCrlf
|
70 | 128 |
_ -> HeaderOther name <$> foldingHeader
|
71 | 129 |
|
72 | 130 |
pDateTime :: Parser UTCTime
|
73 | 131 |
pDateTime = undefined
|
| 132 |
|
| 133 |
overFoldingHeader :: Parser a -> Parser a
|
| 134 |
overFoldingHeader parse = do
|
| 135 |
bs <- foldingHeader
|
| 136 |
case Atto.parseOnly parse bs of
|
| 137 |
Left err -> fail err
|
| 138 |
Right x -> return x
|
74 | 139 |
|
75 | 140 |
foldingHeader :: Parser ByteString
|
76 | 141 |
foldingHeader = do
|
|
88 | 153 |
isSpace :: (Num a, Eq a) => a -> Bool
|
89 | 154 |
isSpace c = c == 32 || c == 9
|
90 | 155 |
|
| 156 |
isWS :: (Num a, Eq a) => a -> Bool
|
| 157 |
isWS c = c == 32 || c == 9
|
| 158 |
|
| 159 |
pCfwsMb :: Parser ()
|
| 160 |
pCfwsMb = Atto.option () pCfws
|
| 161 |
|
| 162 |
pCfws :: Parser ()
|
| 163 |
pCfws =
|
| 164 |
(Atto.skipMany1 (pFwsMb *> pComment) *> void pFwsMb) <|> pFws
|
| 165 |
|
| 166 |
pFwsMb :: Parser BB.Builder
|
| 167 |
pFwsMb = do
|
| 168 |
next <- Atto.peekWord8'
|
| 169 |
if isSpace next
|
| 170 |
then BB.word8 32 <$ pFws
|
| 171 |
else return mempty
|
| 172 |
|
| 173 |
pFws :: Parser ()
|
| 174 |
pFws =
|
| 175 |
Atto.skipMany (Atto.skipWhile isSpace *> pCrlf) *>
|
| 176 |
Atto.skipMany1 (Atto.satisfy isSpace)
|
| 177 |
|
| 178 |
pComment :: Parser ()
|
| 179 |
pComment = void (Atto.word8 40 *> void body *> Atto.word8 41)
|
| 180 |
where body = Atto.skipMany (pFwsMb *> content) *> pFwsMb
|
| 181 |
content = void (Atto.satisfy ctext) <|> void pQuotedPair <|> pComment
|
| 182 |
ctext c = c /= 40 && c /= 41 && c /= 92
|
| 183 |
|
| 184 |
pQuotedPair :: Parser Word8
|
| 185 |
pQuotedPair = Atto.word8 92 *> Atto.anyWord8
|
| 186 |
|
91 | 187 |
pCrlf :: Parser ()
|
92 | 188 |
pCrlf = void (Atto.word8 10)
|
93 | 189 |
|