Skip to content

Commit 009e870

Browse files
committed
Fix: IMAP FETCH does not properly honour "<start.size>" clause #334
1 parent 4fb63e8 commit 009e870

2 files changed

Lines changed: 283 additions & 7 deletions

File tree

hmailserver/source/Server/IMAP/IMAPFetch.cpp

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,7 @@ namespace HM
380380
// Add HEADER
381381
AnsiString sHeaderContents = pBodyPart->GetHeaderContents();
382382
GetBytesToSend_(sHeaderContents.GetLength(), oPart, iByteStart, iByteCount);
383-
sHeaderContents.Mid(iByteStart, iByteCount);
383+
sHeaderContents = sHeaderContents.Mid(iByteStart, iByteCount);
384384
pOutBuf->Add((BYTE*) sHeaderContents.GetBuffer(0), sHeaderContents.GetLength());
385385
}
386386
else if (oPart.GetShowBodyText())
@@ -463,9 +463,7 @@ namespace HM
463463
sResponse += "\r\n";
464464

465465
GetBytesToSend_(sResponse.GetLength(), oPart, iByteStart, iByteCount);
466-
sResponse.Mid(iByteStart, iByteCount);
467-
468-
AnsiString sAS = sResponse;
466+
AnsiString sAS = sResponse.Mid(iByteStart, iByteCount);
469467
pOutBuf->Add((BYTE*) sAS.GetBuffer(0), sAS.GetLength());
470468

471469
}
@@ -523,9 +521,7 @@ namespace HM
523521
sResponse += "\r\n";
524522

525523
GetBytesToSend_(sResponse.GetLength(), oPart, iByteStart, iByteCount);
526-
sResponse.Mid(iByteStart,iByteCount);
527-
528-
AnsiString sAS = sResponse;
524+
AnsiString sAS = sResponse.Mid(iByteStart, iByteCount);
529525
pOutBuf->Add((BYTE*) sAS.GetBuffer(0), sAS.GetLength());
530526

531527
}

hmailserver/test/RegressionTests/IMAP/Fetch.cs

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)