Skip to content

Commit bb73a34

Browse files
committed
Harden streaming fetch parser coverage
1 parent ceb90ce commit bb73a34

2 files changed

Lines changed: 86 additions & 6 deletions

File tree

src/Network/HaskellNet/IMAP.hs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -431,11 +431,19 @@ fetchRPeek conn range =
431431
do ls <- fetchByByteStringR conn range "BODY.PEEK[]"
432432
return $ map (\(uid, vs) -> (uid, fromMaybe BS.empty $ lookup' "BODY[]" vs)) ls
433433

434+
-- | Fetch arbitrary data items and return values as 'String's.
435+
--
436+
-- This is kept for compatibility. Prefer 'fetchByByteString' for message
437+
-- bodies or other data that may be large or non-textual.
434438
fetchByString :: IMAPConnection -> UID -> String
435439
-> IO [(String, String)]
436440
fetchByString conn uid command =
437441
map (\(key, value) -> (key, BS.unpack value)) <$> fetchByByteString conn uid command
438442

443+
-- | Fetch arbitrary data items and return raw 'ByteString' values.
444+
--
445+
-- Literal values are read directly from the stream instead of first building a
446+
-- full response buffer, so this is the preferred API for large messages.
439447
fetchByByteString :: IMAPConnection -> UID -> String
440448
-> IO [(String, ByteString)]
441449
fetchByByteString conn uid command =
@@ -444,13 +452,15 @@ fetchByByteString conn uid command =
444452
(_, pairs):_ -> return pairs
445453
[] -> return []
446454

455+
-- | Range variant of 'fetchByString'.
447456
fetchByStringR :: IMAPConnection -> (UID, UID) -> String
448457
-> IO [(UID, [(String, String)])]
449458
fetchByStringR conn (s, e) command =
450459
map unpackFetch <$> fetchByByteStringR conn (s, e) command
451460
where unpackFetch (uid, pairs) =
452461
(uid, map (\(key, value) -> (key, BS.unpack value)) pairs)
453462

463+
-- | Range variant of 'fetchByByteString'.
454464
fetchByByteStringR :: IMAPConnection -> (UID, UID) -> String
455465
-> IO [(UID, [(String, ByteString)])]
456466
fetchByByteStringR conn (s, e) command =
@@ -476,6 +486,8 @@ fetchCommandBS conn command proc =
476486
BAD _ msg -> fail ("BAD: " ++ msg)
477487
PREAUTH _ msg -> fail ("preauth: " ++ msg)
478488

489+
-- Streaming FETCH response parser. Literal payloads are read with 'bsGet' so
490+
-- large message bodies do not require buffering the complete server response.
479491
getFetchResponseBS :: BSStream -> String
480492
-> IO (ServerResponse, MboxUpdate, [(Integer, [(String, ByteString)])])
481493
getFetchResponseBS s tag = go Nothing Nothing []
@@ -652,12 +664,7 @@ parseQuotedValueBS input =
652664
parseAtomValueBS :: ByteString -> Maybe (ByteString, ByteString)
653665
parseAtomValueBS input =
654666
let (value, rest) = BS.span isAtomValueChar input
655-
in if BS.null value
656-
then Nothing
657-
else let normalized = if BS.map toUpper value == BS.pack "NIL"
658-
then BS.empty
659-
else value
660-
in Just (normalized, rest)
667+
in if BS.null value then Nothing else Just (value, rest)
661668
where
662669
isAtomValueChar c = not $ c `elem` " (){%*\"\\]\r\n"
663670

test/IMAPParsersTest.hs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,79 @@ imapFetchTest =
255255
]
256256
fetched <- IMAP.fetch conn 42
257257
body @=? fetched
258+
, "fetchPeek reads body response without setting Seen" ~: TestCase $ do
259+
let body = BS.pack "peeked"
260+
conn <- scriptedConnection
261+
[ line ("* 12 FETCH (BODY[] {" ++ show (BS.length body) ++ "}")
262+
, ReadBytes body
263+
, line " UID 42)"
264+
, okLine "FETCH completed"
265+
]
266+
fetched <- IMAP.fetchPeek conn 42
267+
body @=? fetched
268+
, "fetchSize parses scalar size responses" ~: TestCase $ do
269+
conn <- scriptedConnection
270+
[ line "* 12 FETCH (RFC822.SIZE 12345 UID 42)"
271+
, okLine "FETCH completed"
272+
]
273+
fetched <- IMAP.fetchSize conn 42
274+
12345 @=? fetched
275+
, "fetchFlags parses parenthesized flag responses" ~: TestCase $ do
276+
conn <- scriptedConnection
277+
[ line "* 12 FETCH (FLAGS (\\Seen \\Deleted) UID 42)"
278+
, okLine "FETCH completed"
279+
]
280+
fetched <- IMAP.fetchFlags conn 42
281+
[Seen, Deleted] @=? fetched
282+
, "fetchHeaderFields matches normalized body section keys" ~: TestCase $ do
283+
let headers = BS.pack "Subject: Hi\r\nFrom: a@example.com\r\n\r\n"
284+
conn <- scriptedConnection
285+
[ line ("* 12 FETCH (BODY[HEADER.FIELDS (SUBJECT FROM)] {"
286+
++ show (BS.length headers) ++ "}")
287+
, ReadBytes headers
288+
, line " UID 42)"
289+
, okLine "FETCH completed"
290+
]
291+
fetched <- IMAP.fetchHeaderFields conn 42 ["Subject", "From"]
292+
headers @=? fetched
293+
, "fetchR maps sequence numbers to response UIDs" ~: TestCase $ do
294+
let firstBody = BS.pack "one"
295+
secondBody = BS.pack "two"
296+
conn <- scriptedConnection
297+
[ line ("* 1 FETCH (BODY[] {" ++ show (BS.length firstBody) ++ "}")
298+
, ReadBytes firstBody
299+
, line " UID 101)"
300+
, line ("* 2 FETCH (BODY[] {" ++ show (BS.length secondBody) ++ "}")
301+
, ReadBytes secondBody
302+
, line " UID 102)"
303+
, okLine "FETCH completed"
304+
]
305+
fetched <- IMAP.fetchR conn (1, 2)
306+
[(101, firstBody), (102, secondBody)] @=? fetched
307+
, "fetchByString keeps scalar and literal values compatible" ~: TestCase $ do
308+
let headers = BS.pack "hello"
309+
conn <- scriptedConnection
310+
[ line ("* 12 FETCH (RFC822.SIZE 123 FLAGS (\\Seen) BODY[HEADER] {"
311+
++ show (BS.length headers) ++ "}")
312+
, ReadBytes headers
313+
, line " NILKEY NIL QUOTED \"world\" UID 42)"
314+
, okLine "FETCH completed"
315+
]
316+
fetched <- IMAP.fetchByString conn 42
317+
"RFC822.SIZE FLAGS BODY[HEADER] NILKEY QUOTED"
318+
[ ("RFC822.SIZE", "123")
319+
, ("FLAGS", "(\\Seen)")
320+
, ("BODY[HEADER]", "hello")
321+
, ("NILKEY", "NIL")
322+
, ("QUOTED", "\"world\"")
323+
, ("UID", "42")
324+
] @=? fetched
325+
, "store accepts FETCH data in STORE responses" ~: TestCase $ do
326+
conn <- scriptedConnection
327+
[ line "* 12 FETCH (UID 42 FLAGS (\\Seen))"
328+
, okLine "STORE completed"
329+
]
330+
IMAP.store conn 42 (IMAP.PlusFlags [Seen])
258331
]
259332

260333
testData = [ "base" ~: baseTest

0 commit comments

Comments
 (0)