Skip to content

Commit 4e36943

Browse files
committed
fix: detect imip messages from outlook.com
Adjust the parsing logic in MessageMapper to correctly detect iMIP messages in flat email structures where the text/calendar part is not wrapped in multipart/alternative. Signed-off-by: Daniel Kesselberg <[email protected]>
1 parent 2f441a7 commit 4e36943

File tree

5 files changed

+178
-6
lines changed

5 files changed

+178
-6
lines changed

REUSE.toml

+6
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,12 @@ precedence = "aggregate"
107107
SPDX-FileCopyrightText = "2024 Nextcloud GmbH and Nextcloud contributors"
108108
SPDX-License-Identifier = "AGPL-3.0-or-later"
109109

110+
[[annotations]]
111+
path = ["tests/data/imip/*"]
112+
precedence = "aggregate"
113+
SPDX-FileCopyrightText = "2025 Nextcloud GmbH and Nextcloud contributors"
114+
SPDX-License-Identifier = "AGPL-3.0-or-later"
115+
110116
[[annotations]]
111117
path = ".github/CODEOWNERS"
112118
precedence = "aggregate"

lib/IMAP/MessageMapper.php

+4-6
Original file line numberDiff line numberDiff line change
@@ -888,15 +888,13 @@ public function getBodyStructureData(Horde_Imap_Client_Socket $client,
888888
$structure = $fetchData->getStructure();
889889

890890
/** @var Horde_Mime_Part $part */
891-
foreach ($structure->getParts() as $part) {
891+
foreach ($structure->partIterator() as $part) {
892892
if ($part->isAttachment()) {
893893
$hasAttachments = true;
894894
}
895-
$bodyParts = $part->getParts();
896-
/** @var Horde_Mime_Part $bodyPart */
897-
foreach ($bodyParts as $bodyPart) {
898-
$contentParameters = $bodyPart->getAllContentTypeParameters();
899-
if ($bodyPart->getType() === 'text/calendar' && isset($contentParameters['method'])) {
895+
896+
if ($part->getType() === 'text/calendar') {
897+
if ($part->getContentTypeParameter('method') !== null) {
900898
$isImipMessage = true;
901899
}
902900
}

tests/Unit/IMAP/MessageMapperTest.php

+41
Original file line numberDiff line numberDiff line change
@@ -593,4 +593,45 @@ public function testGetFlaggedSearchResultUnexpectedStructure() {
593593
$result = $this->mapper->getFlagged($imapClient, $mailbox, $flag);
594594
$this->assertEquals($result, []);
595595
}
596+
597+
/**
598+
* This test ensures that we correctly identify iMIP messages from various
599+
* sources as valid iMIP messages. The test cases are based on original
600+
* iMIP messages from different vendors. The focus is on the MIME message
601+
* structure and verifying that we traverse the MIME tree properly.
602+
*
603+
* @dataProvider isImipMessageProvider
604+
*/
605+
public function testGetBodyStructureIsImipMessage(string $filename, bool $expected): void {
606+
$text = file_get_contents(__DIR__ . '/../../data/imip/' . $filename . '.txt');
607+
$part = \Horde_Mime_Part::parseMessage($text);
608+
609+
$fetchData = new Horde_Imap_Client_Data_Fetch();
610+
$fetchData->setStructure($part);
611+
$fetchData->setUid(100);
612+
613+
$fetchResult = new Horde_Imap_Client_Fetch_Results();
614+
$fetchResult[0] = $fetchData;
615+
616+
$imapClient = $this->createMock(Horde_Imap_Client_Socket::class);
617+
$imapClient->method('fetch')
618+
->willReturn($fetchResult);
619+
620+
$data = $this->mapper->getBodyStructureData(
621+
$imapClient,
622+
'INBOX',
623+
[100],
624+
625+
);
626+
627+
$this->assertCount(1, $data);
628+
$this->assertEquals($expected, $data[0]->isImipMessage());
629+
}
630+
631+
public function isImipMessageProvider(): array {
632+
return [
633+
'google request' => ['request_google', true],
634+
'outlook.com request' => ['request_outlook_com', true],
635+
];
636+
}
596637
}

tests/data/imip/request_google.txt

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
MIME-Version: 1.0
2+
Message-ID: <[email protected]>
3+
Date: Thu, 06 Feb 2025 16:21:41 +0000
4+
5+
6+
Content-Type: multipart/mixed; boundary="f2d8330e8efc4039bd8073c70ec6cf24"
7+
Subject: Invitation: Imip Testing @ Thu Feb 20, 2025 7pm - 8pm
8+
9+
10+
--f2d8330e8efc4039bd8073c70ec6cf24
11+
Content-Type: multipart/alternative; boundary="f55a70234308486098b936b62a6d5e33"
12+
13+
--f55a70234308486098b936b62a6d5e33
14+
Content-Type: text/plain; charset="UTF-8"; format=flowed; delsp=yes
15+
Content-Transfer-Encoding: base64
16+
17+
SW1pcCBUZXN0aW5nICh0ZXh0L3BsYWluKQo=
18+
--f55a70234308486098b936b62a6d5e33
19+
Content-Type: text/calendar; charset="UTF-8"; method=REQUEST
20+
Content-Transfer-Encoding: 7bit
21+
22+
BEGIN:VCALENDAR
23+
PRODID:-//Google Inc//Google Calendar 70.9054//EN
24+
VERSION:2.0
25+
CALSCALE:GREGORIAN
26+
METHOD:REQUEST
27+
BEGIN:VTIMEZONE
28+
TZID:Europe/Berlin
29+
X-LIC-LOCATION:Europe/Berlin
30+
BEGIN:DAYLIGHT
31+
TZOFFSETFROM:+0100
32+
TZOFFSETTO:+0200
33+
TZNAME:GMT+2
34+
DTSTART:19700329T020000
35+
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
36+
END:DAYLIGHT
37+
BEGIN:STANDARD
38+
TZOFFSETFROM:+0200
39+
TZOFFSETTO:+0100
40+
TZNAME:GMT+1
41+
DTSTART:19701025T030000
42+
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
43+
END:STANDARD
44+
END:VTIMEZONE
45+
BEGIN:VEVENT
46+
DTSTART;TZID=Europe/Berlin:20250220T190000
47+
DTEND;TZID=Europe/Berlin:20250220T200000
48+
DTSTAMP:20250206T162141Z
49+
50+
51+
ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=
52+
TRUE;[email protected];X-NUM-GUESTS=0:mailto:[email protected]
53+
ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;RSVP=TRUE
54+
;[email protected];X-NUM-GUESTS=0:mailto:[email protected]
55+
ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=
56+
TRUE;[email protected];X-NUM-GUESTS=0:mailto:[email protected]
57+
CREATED:20250206T162140Z
58+
DESCRIPTION:
59+
LAST-MODIFIED:20250206T162140Z
60+
LOCATION:
61+
SEQUENCE:0
62+
STATUS:CONFIRMED
63+
SUMMARY:Imip Testing
64+
TRANSP:OPAQUE
65+
BEGIN:VALARM
66+
ACTION:DISPLAY
67+
DESCRIPTION:This is an event reminder
68+
TRIGGER:-P0DT0H30M0S
69+
END:VALARM
70+
END:VEVENT
71+
END:VCALENDAR
72+
73+
--f55a70234308486098b936b62a6d5e33--
74+
--f2d8330e8efc4039bd8073c70ec6cf24--
+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
2+
3+
Date: Thu, 6 Feb 2025 10:26:13 +0000
4+
Message-ID: <[email protected]>
5+
Accept-Language: en-US, de-DE
6+
Content-Language: en-US
7+
Content-Type: multipart/alternative;
8+
boundary="a6ccc118fe8d4081837903c834923d7a"
9+
MIME-Version: 1.0
10+
Subject: Imip Testing
11+
12+
--a6ccc118fe8d4081837903c834923d7a
13+
Content-Type: text/plain; charset="iso-8859-1"
14+
Content-Transfer-Encoding: quoted-printable
15+
16+
17+
18+
--a6ccc118fe8d4081837903c834923d7a
19+
Content-Type: text/html; charset="iso-8859-1"
20+
Content-Transfer-Encoding: quoted-printable
21+
22+
23+
--a6ccc118fe8d4081837903c834923d7a
24+
Content-Type: text/calendar; charset="utf-8"; method=REQUEST
25+
Content-Transfer-Encoding: base64
26+
27+
QkVHSU46VkNBTEVOREFSCk1FVEhPRDpSRVFVRVNUClBST0RJRDpNaWNyb3NvZnQgRXhjaGFuZ2Ug
28+
U2VydmVyIDIwMTAKVkVSU0lPTjoyLjAKQkVHSU46VlRJTUVaT05FClRaSUQ6Vy4gRXVyb3BlIFN0
29+
YW5kYXJkIFRpbWUKQkVHSU46U1RBTkRBUkQKRFRTVEFSVDoxNjAxMDEwMVQwMzAwMDAKVFpPRkZT
30+
RVRGUk9NOiswMjAwClRaT0ZGU0VUVE86KzAxMDAKUlJVTEU6RlJFUT1ZRUFSTFk7SU5URVJWQUw9
31+
MTtCWURBWT0tMVNVO0JZTU9OVEg9MTAKRU5EOlNUQU5EQVJECkJFR0lOOkRBWUxJR0hUCkRUU1RB
32+
UlQ6MTYwMTAxMDFUMDIwMDAwClRaT0ZGU0VURlJPTTorMDEwMApUWk9GRlNFVFRPOiswMjAwClJS
33+
VUxFOkZSRVE9WUVBUkxZO0lOVEVSVkFMPTE7QllEQVk9LTFTVTtCWU1PTlRIPTMKRU5EOkRBWUxJ
34+
R0hUCkVORDpWVElNRVpPTkUKQkVHSU46VkVWRU5UCk9SR0FOSVpFUjtDTj1hbGljZUBleGFtcGxl
35+
Lm9yZzptYWlsdG86YWxpY2VAZXhhbXBsZS5vcmcKQVRURU5ERUU7Uk9MRT1SRVEtUEFSVElDSVBB
36+
TlQ7UEFSVFNUQVQ9TkVFRFMtQUNUSU9OO1JTVlA9VFJVRTtDTj1ib2JAZXhhbXBsZS5vcmc6bWFp
37+
bHRvOmJvYkBleGFtcGxlLm9yZwpERVNDUklQVElPTjtMQU5HVUFHRT1lbi1VUzpcbgpVSUQ6MUYw
38+
QkQzRjZGRUZDNDIxQUFBNUJFOTkyRDY5OTJCNkEKU1VNTUFSWTtMQU5HVUFHRT1lbi1VUzpJbWlw
39+
IFRlc3RpbmcKRFRTVEFSVDtUWklEPVcuIEV1cm9wZSBTdGFuZGFyZCBUaW1lOjIwMjUwMjI2VDA4
40+
MDAwMApEVEVORDtUWklEPVcuIEV1cm9wZSBTdGFuZGFyZCBUaW1lOjIwMjUwMjI2VDA4MzAwMApD
41+
TEFTUzpQVUJMSUMKUFJJT1JJVFk6NQpEVFNUQU1QOjIwMjUwMjA2VDEwMjYxMloKVFJBTlNQOk9Q
42+
QVFVRQpTVEFUVVM6Q09ORklSTUVEClNFUVVFTkNFOjAKTE9DQVRJT047TEFOR1VBR0U9ZW4tVVM6
43+
ClgtTUlDUk9TT0ZULUNETy1BUFBULVNFUVVFTkNFOjAKWC1NSUNST1NPRlQtQ0RPLUJVU1lTVEFU
44+
VVM6VEVOVEFUSVZFClgtTUlDUk9TT0ZULUNETy1JTlRFTkRFRFNUQVRVUzpCVVNZClgtTUlDUk9T
45+
T0ZULUNETy1BTExEQVlFVkVOVDpGQUxTRQpYLU1JQ1JPU09GVC1DRE8tSU1QT1JUQU5DRToxClgt
46+
TUlDUk9TT0ZULUNETy1JTlNUVFlQRTowClgtTUlDUk9TT0ZULURPTk9URk9SV0FSRE1FRVRJTkc6
47+
RkFMU0UKWC1NSUNST1NPRlQtRElTQUxMT1ctQ09VTlRFUjpGQUxTRQpYLU1JQ1JPU09GVC1SRVFV
48+
RVNURURBVFRFTkRBTkNFTU9ERTpERUZBVUxUClgtTUlDUk9TT0ZULUlTUkVTUE9OU0VSRVFVRVNU
49+
RUQ6VFJVRQpYLU1JQ1JPU09GVC1MT0NBVElPTlM6W10KQkVHSU46VkFMQVJNCkRFU0NSSVBUSU9O
50+
OlJFTUlOREVSClRSSUdHRVI7UkVMQVRFRD1TVEFSVDotUFQxNU0KQUNUSU9OOkRJU1BMQVkKRU5E
51+
OlZBTEFSTQpFTkQ6VkVWRU5UCkVORDpWQ0FMRU5EQVIK
52+
53+
--a6ccc118fe8d4081837903c834923d7a--

0 commit comments

Comments
 (0)