@@ -274,5 +274,285 @@ public void TestFetchInvalid()
274274 result = sim . Fetch ( "-100 BODY[1]" ) ;
275275 Assert . IsTrue ( result . StartsWith ( "A17 BAD" ) ) ;
276276 }
277+
278+ [ Test ]
279+ [ Description ( "RFC 3501: partial fetch where start offset is beyond end of content must return empty string" ) ]
280+ public void PartialFetch_StartBeyondEndReturnsEmptyString ( )
281+ {
282+ var account = SingletonProvider < TestSetup > . Instance . AddAccount ( _domain , "test@example.test" , "test" ) ;
283+ SmtpClientSimulator . StaticSend ( account . Address , account . Address , "Test" , "Body" ) ;
284+ ImapClientSimulator . AssertMessageCount ( account . Address , "test" , "Inbox" , 1 ) ;
285+
286+ var sim = new ImapClientSimulator ( ) ;
287+ sim . ConnectAndLogon ( account . Address , "test" ) ;
288+ sim . SelectFolder ( "INBOX" ) ;
289+
290+ var result = sim . Fetch ( "1 BODY[HEADER]<99999.10>" ) ;
291+ Assert . IsTrue ( result . Contains ( "BODY[HEADER]<99999>" ) , result ) ;
292+ Assert . IsTrue ( result . Contains ( "\" \" " ) , result ) ;
293+
294+ sim . Disconnect ( ) ;
295+ }
296+
297+ [ Test ]
298+ [ Description ( "RFC 3501: partial fetch where requested count exceeds remaining bytes must truncate" ) ]
299+ public void PartialFetch_RequestedSizeExceedingRemainderTruncates ( )
300+ {
301+ var account = SingletonProvider < TestSetup > . Instance . AddAccount ( _domain , "test@example.test" , "test" ) ;
302+ SmtpClientSimulator . StaticSend ( account . Address , account . Address , "Test" , "Body" ) ;
303+ ImapClientSimulator . AssertMessageCount ( account . Address , "test" , "Inbox" , 1 ) ;
304+
305+ var sim = new ImapClientSimulator ( ) ;
306+ sim . ConnectAndLogon ( account . Address , "test" ) ;
307+ sim . SelectFolder ( "INBOX" ) ;
308+
309+ var fullResult = sim . Fetch ( "1 BODY[HEADER]" ) ;
310+ var lStart = fullResult . IndexOf ( '{' ) ;
311+ var lEnd = fullResult . IndexOf ( '}' , lStart ) ;
312+ var fullSize = int . Parse ( fullResult . Substring ( lStart + 1 , lEnd - lStart - 1 ) ) ;
313+
314+ var partial = sim . Fetch ( "1 BODY[HEADER]<5.99999>" ) ;
315+ Assert . IsTrue ( partial . Contains ( "BODY[HEADER]<5>" ) , partial ) ;
316+ Assert . IsTrue ( partial . Contains ( "{" + ( fullSize - 5 ) + "}" ) , partial ) ;
317+
318+ sim . Disconnect ( ) ;
319+ }
320+
321+ [ Test ]
322+ [ Description ( "BODY.PEEK must not set the \\ Seen flag" ) ]
323+ public void BodyPeekDoesNotSetSeenFlag ( )
324+ {
325+ var account = SingletonProvider < TestSetup > . Instance . AddAccount ( _domain , "test@example.test" , "test" ) ;
326+ SmtpClientSimulator . StaticSend ( account . Address , account . Address , "Test" , "Body" ) ;
327+ ImapClientSimulator . AssertMessageCount ( account . Address , "test" , "Inbox" , 1 ) ;
328+
329+ var sim = new ImapClientSimulator ( ) ;
330+ sim . ConnectAndLogon ( account . Address , "test" ) ;
331+ sim . SelectFolder ( "INBOX" ) ;
332+
333+ sim . Fetch ( "1 BODY.PEEK[TEXT]" ) ;
334+ var flags = sim . GetFlags ( 1 ) ;
335+ Assert . IsFalse ( flags . Contains ( "\\ Seen" ) , flags ) ;
336+
337+ sim . Disconnect ( ) ;
338+ }
339+
340+ [ Test ]
341+ [ Description ( "BODY (without PEEK) must set the \\ Seen flag" ) ]
342+ public void BodyFetchSetsSeenFlag ( )
343+ {
344+ var account = SingletonProvider < TestSetup > . Instance . AddAccount ( _domain , "test@example.test" , "test" ) ;
345+ SmtpClientSimulator . StaticSend ( account . Address , account . Address , "Test" , "Body" ) ;
346+ ImapClientSimulator . AssertMessageCount ( account . Address , "test" , "Inbox" , 1 ) ;
347+
348+ var sim = new ImapClientSimulator ( ) ;
349+ sim . ConnectAndLogon ( account . Address , "test" ) ;
350+ sim . SelectFolder ( "INBOX" ) ;
351+
352+ sim . Fetch ( "1 BODY[TEXT]" ) ;
353+ var flags = sim . GetFlags ( 1 ) ;
354+ Assert . IsTrue ( flags . Contains ( "\\ Seen" ) , flags ) ;
355+
356+ sim . Disconnect ( ) ;
357+ }
358+
359+ [ Test ]
360+ [ Description ( "Partial fetch of BODY[TEXT] must return correct byte slice" ) ]
361+ public void PartialFetch_BodyText ( )
362+ {
363+ var account = SingletonProvider < TestSetup > . Instance . AddAccount ( _domain , "test@example.test" , "test" ) ;
364+ SmtpClientSimulator . StaticSend ( account . Address , account . Address , "Test" , "SampleBodyContent" ) ;
365+ ImapClientSimulator . AssertMessageCount ( account . Address , "test" , "Inbox" , 1 ) ;
366+
367+ var sim = new ImapClientSimulator ( ) ;
368+ sim . ConnectAndLogon ( account . Address , "test" ) ;
369+ sim . SelectFolder ( "INBOX" ) ;
370+
371+ var fullResult = sim . Fetch ( "1 BODY.PEEK[TEXT]" ) ;
372+ var lStart = fullResult . IndexOf ( '{' ) ;
373+ var lEnd = fullResult . IndexOf ( '}' , lStart ) ;
374+ var fullSize = int . Parse ( fullResult . Substring ( lStart + 1 , lEnd - lStart - 1 ) ) ;
375+ var contentStart = fullResult . IndexOf ( "\r \n " , lEnd ) + 2 ;
376+ var fullText = fullResult . Substring ( contentStart , fullSize ) ;
377+
378+ Assert . IsTrue ( fullSize > 5 , $ "Body too short ({ fullSize } ) for a meaningful partial test") ;
379+
380+ var partial = sim . Fetch ( "1 BODY.PEEK[TEXT]<0.5>" ) ;
381+ Assert . IsTrue ( partial . Contains ( "BODY[TEXT]<0>" ) , partial ) ;
382+ Assert . IsTrue ( partial . Contains ( "{5}" ) , partial ) ;
383+ var partialStart = partial . IndexOf ( "{5}" ) + 5 ;
384+ Assert . AreEqual ( fullText . Substring ( 0 , 5 ) , partial . Substring ( partialStart , 5 ) ) ;
385+
386+ sim . Disconnect ( ) ;
387+ }
388+
389+ [ Test ]
390+ [ Description ( "Partial fetch of BODY[HEADER.FIELDS] must return correct byte slice" ) ]
391+ public void PartialFetch_HeaderFields ( )
392+ {
393+ var account = SingletonProvider < TestSetup > . Instance . AddAccount ( _domain , "mimetest@example.test" , "test" ) ;
394+ var message = "From: Someone <someone@example.com>" + Environment . NewLine +
395+ "Subject: TestSubject" + Environment . NewLine +
396+ Environment . NewLine +
397+ "Hello" + Environment . NewLine ;
398+ new SmtpClientSimulator ( ) . SendRaw ( account . Address , account . Address , message ) ;
399+ Pop3ClientSimulator . AssertMessageCount ( account . Address , "test" , 1 ) ;
400+
401+ var sim = new ImapClientSimulator ( ) ;
402+ sim . ConnectAndLogon ( account . Address , "test" ) ;
403+ sim . SelectFolder ( "INBOX" ) ;
404+
405+ var fullResult = sim . Fetch ( "1 BODY.PEEK[HEADER.FIELDS (Subject)]" ) ;
406+ var lStart = fullResult . IndexOf ( '{' ) ;
407+ var lEnd = fullResult . IndexOf ( '}' , lStart ) ;
408+ var fullSize = int . Parse ( fullResult . Substring ( lStart + 1 , lEnd - lStart - 1 ) ) ;
409+ var contentStart = fullResult . IndexOf ( "\r \n " , lEnd ) + 2 ;
410+ var fullContent = fullResult . Substring ( contentStart , fullSize ) ;
411+
412+ Assert . IsTrue ( fullSize > 5 , $ "HEADER.FIELDS response too short ({ fullSize } ) for a meaningful partial test") ;
413+
414+ var partial = sim . Fetch ( "1 BODY.PEEK[HEADER.FIELDS (Subject)]<0.5>" ) ;
415+ Assert . IsTrue ( partial . Contains ( "BODY[HEADER.FIELDS (SUBJECT)]<0>" ) || partial . Contains ( "BODY[HEADER.FIELDS (Subject)]<0>" ) , partial ) ;
416+ Assert . IsTrue ( partial . Contains ( "{5}" ) , partial ) ;
417+ var partialStart = partial . IndexOf ( "{5}" ) + 5 ;
418+ Assert . AreEqual ( fullContent . Substring ( 0 , 5 ) , partial . Substring ( partialStart , 5 ) ) ;
419+
420+ sim . Disconnect ( ) ;
421+ }
422+
423+ [ Test ]
424+ [ Description ( "Partial fetch of BODY[HEADER.FIELDS.NOT] must return correct byte slice" ) ]
425+ public void PartialFetch_HeaderFieldsNot ( )
426+ {
427+ var account = SingletonProvider < TestSetup > . Instance . AddAccount ( _domain , "mimetest@example.test" , "test" ) ;
428+ var message = "From: Someone <someone@example.com>" + Environment . NewLine +
429+ "Subject: TestSubject" + Environment . NewLine +
430+ Environment . NewLine +
431+ "Hello" + Environment . NewLine ;
432+ new SmtpClientSimulator ( ) . SendRaw ( account . Address , account . Address , message ) ;
433+ Pop3ClientSimulator . AssertMessageCount ( account . Address , "test" , 1 ) ;
434+
435+ var sim = new ImapClientSimulator ( ) ;
436+ sim . ConnectAndLogon ( account . Address , "test" ) ;
437+ sim . SelectFolder ( "INBOX" ) ;
438+
439+ var fullResult = sim . Fetch ( "1 BODY.PEEK[HEADER.FIELDS.NOT (Subject)]" ) ;
440+ var lStart = fullResult . IndexOf ( '{' ) ;
441+ var lEnd = fullResult . IndexOf ( '}' , lStart ) ;
442+ var fullSize = int . Parse ( fullResult . Substring ( lStart + 1 , lEnd - lStart - 1 ) ) ;
443+ var contentStart = fullResult . IndexOf ( "\r \n " , lEnd ) + 2 ;
444+ var fullContent = fullResult . Substring ( contentStart , fullSize ) ;
445+
446+ Assert . IsTrue ( fullSize > 5 , $ "HEADER.FIELDS.NOT response too short ({ fullSize } ) for a meaningful partial test") ;
447+
448+ var partial = sim . Fetch ( "1 BODY.PEEK[HEADER.FIELDS.NOT (Subject)]<0.5>" ) ;
449+ Assert . IsTrue ( partial . Contains ( "BODY[HEADER.FIELDS.NOT (SUBJECT)]<0>" ) || partial . Contains ( "BODY[HEADER.FIELDS.NOT (Subject)]<0>" ) , partial ) ;
450+ Assert . IsTrue ( partial . Contains ( "{5}" ) , partial ) ;
451+ var partialStart = partial . IndexOf ( "{5}" ) + 5 ;
452+ Assert . AreEqual ( fullContent . Substring ( 0 , 5 ) , partial . Substring ( partialStart , 5 ) ) ;
453+
454+ sim . Disconnect ( ) ;
455+ }
456+
457+ [ Test ]
458+ [ Description ( "RFC822.SIZE must return a positive integer reflecting message size" ) ]
459+ public void FetchRfc822SizeReturnsPositiveInteger ( )
460+ {
461+ var account = SingletonProvider < TestSetup > . Instance . AddAccount ( _domain , "test@example.test" , "test" ) ;
462+ SmtpClientSimulator . StaticSend ( account . Address , account . Address , "Test" , "Body" ) ;
463+ ImapClientSimulator . AssertMessageCount ( account . Address , "test" , "Inbox" , 1 ) ;
464+
465+ var sim = new ImapClientSimulator ( ) ;
466+ sim . ConnectAndLogon ( account . Address , "test" ) ;
467+ sim . SelectFolder ( "INBOX" ) ;
468+
469+ var result = sim . Fetch ( "1 RFC822.SIZE" ) ;
470+ Assert . IsTrue ( result . Contains ( "RFC822.SIZE" ) , result ) ;
471+ var sizeStart = result . IndexOf ( "RFC822.SIZE " ) + "RFC822.SIZE " . Length ;
472+ var sizeEnd = result . IndexOfAny ( new [ ] { ' ' , ')' } , sizeStart ) ;
473+ var size = int . Parse ( result . Substring ( sizeStart , sizeEnd - sizeStart ) ) ;
474+ Assert . IsTrue ( size > 0 , $ "RFC822.SIZE should be positive, got { size } ") ;
475+
476+ sim . Disconnect ( ) ;
477+ }
478+
479+ [ Test ]
480+ [ Description ( "INTERNALDATE must return a non-empty quoted date string" ) ]
481+ public void FetchInternaldateReturnsNonEmptyQuotedString ( )
482+ {
483+ var account = SingletonProvider < TestSetup > . Instance . AddAccount ( _domain , "test@example.test" , "test" ) ;
484+ SmtpClientSimulator . StaticSend ( account . Address , account . Address , "Test" , "Body" ) ;
485+ ImapClientSimulator . AssertMessageCount ( account . Address , "test" , "Inbox" , 1 ) ;
486+
487+ var sim = new ImapClientSimulator ( ) ;
488+ sim . ConnectAndLogon ( account . Address , "test" ) ;
489+ sim . SelectFolder ( "INBOX" ) ;
490+
491+ var result = sim . Fetch ( "1 INTERNALDATE" ) ;
492+ Assert . IsTrue ( result . Contains ( "INTERNALDATE \" " ) , result ) ;
493+
494+ sim . Disconnect ( ) ;
495+ }
496+
497+ [ Test ]
498+ [ Description ( "UID fetch must return a positive integer" ) ]
499+ public void FetchUidReturnsPositiveInteger ( )
500+ {
501+ var account = SingletonProvider < TestSetup > . Instance . AddAccount ( _domain , "test@example.test" , "test" ) ;
502+ SmtpClientSimulator . StaticSend ( account . Address , account . Address , "Test" , "Body" ) ;
503+ ImapClientSimulator . AssertMessageCount ( account . Address , "test" , "Inbox" , 1 ) ;
504+
505+ var sim = new ImapClientSimulator ( ) ;
506+ sim . ConnectAndLogon ( account . Address , "test" ) ;
507+ sim . SelectFolder ( "INBOX" ) ;
508+
509+ var result = sim . Fetch ( "1 UID" ) ;
510+ Assert . IsTrue ( result . Contains ( "UID " ) , result ) ;
511+ var uidStart = result . IndexOf ( "UID " ) + 4 ;
512+ var uidEnd = result . IndexOfAny ( new [ ] { ' ' , ')' } , uidStart ) ;
513+ var uid = int . Parse ( result . Substring ( uidStart , uidEnd - uidStart ) ) ;
514+ Assert . IsTrue ( uid > 0 , $ "UID should be positive, got { uid } ") ;
515+
516+ sim . Disconnect ( ) ;
517+ }
518+
519+ [ Test ]
520+ [ Description ( "Issue 334, IMAP FETCH does not properly honour the <start.size> partial body clause" ) ]
521+ public void TestPartialFetch_HeaderOctetRange ( )
522+ {
523+ var account = SingletonProvider < TestSetup > . Instance . AddAccount ( _domain , "test@example.test" , "test" ) ;
524+ SmtpClientSimulator . StaticSend ( account . Address , account . Address , "Test subject" , "Body text" ) ;
525+ ImapClientSimulator . AssertMessageCount ( account . Address , "test" , "Inbox" , 1 ) ;
526+
527+ var sim = new ImapClientSimulator ( ) ;
528+ sim . ConnectAndLogon ( account . Address , "test" ) ;
529+ sim . SelectFolder ( "INBOX" ) ;
530+
531+ // Fetch full header to establish baseline size and content.
532+ var fullHeaderResult = sim . Fetch ( "1 BODY[HEADER]" ) ;
533+ var literalStart = fullHeaderResult . IndexOf ( '{' ) ;
534+ var literalEnd = fullHeaderResult . IndexOf ( '}' , literalStart ) ;
535+ var fullSize = int . Parse ( fullHeaderResult . Substring ( literalStart + 1 , literalEnd - literalStart - 1 ) ) ;
536+ Assert . IsTrue ( fullSize > 15 , $ "Header too short ({ fullSize } bytes) for a meaningful partial test") ;
537+
538+ var contentStart = fullHeaderResult . IndexOf ( "\r \n " , literalEnd ) + 2 ;
539+ var fullHeader = fullHeaderResult . Substring ( contentStart , fullSize ) ;
540+
541+ // <0.10>: 10 bytes starting at offset 0.
542+ var result0 = sim . Fetch ( "1 BODY[HEADER]<0.10>" ) ;
543+ Assert . IsTrue ( result0 . Contains ( "BODY[HEADER]<0>" ) , result0 ) ;
544+ Assert . IsTrue ( result0 . Contains ( "{10}" ) , result0 ) ;
545+ var content0Start = result0 . IndexOf ( "{10}" ) + 6 ;
546+ Assert . AreEqual ( fullHeader . Substring ( 0 , 10 ) , result0 . Substring ( content0Start , 10 ) ) ;
547+
548+ // <5.10>: 10 bytes starting at offset 5.
549+ var result5 = sim . Fetch ( "1 BODY[HEADER]<5.10>" ) ;
550+ Assert . IsTrue ( result5 . Contains ( "BODY[HEADER]<5>" ) , result5 ) ;
551+ Assert . IsTrue ( result5 . Contains ( "{10}" ) , result5 ) ;
552+ var content5Start = result5 . IndexOf ( "{10}" ) + 6 ;
553+ Assert . AreEqual ( fullHeader . Substring ( 5 , 10 ) , result5 . Substring ( content5Start , 10 ) ) ;
554+
555+ sim . Disconnect ( ) ;
556+ }
277557 }
278558}
0 commit comments