Skip to content

Commit b456b96

Browse files
committed
#552: [bug] support iCalendar events with METHOD defined in body instead of Content-Type
1 parent a7b2522 commit b456b96

File tree

6 files changed

+394
-17
lines changed

6 files changed

+394
-17
lines changed

Diff for: modules/core-module/src/main/java/org/simplejavamail/internal/util/MiscUtil.java

+8-7
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,8 @@
2727
import java.lang.reflect.Method;
2828
import java.net.URL;
2929
import java.nio.charset.Charset;
30-
import java.util.AbstractMap;
31-
import java.util.ArrayList;
32-
import java.util.Collection;
33-
import java.util.HashMap;
34-
import java.util.List;
35-
import java.util.Map;
36-
import java.util.Random;
30+
import java.util.*;
31+
import java.util.regex.Matcher;
3732
import java.util.regex.Pattern;
3833
import java.util.stream.Collectors;
3934

@@ -465,4 +460,10 @@ public static List<InternetAddress> asInternetAddresses(@NotNull List<Recipient>
465460
public static InternetAddress asInternetAddress(@NotNull Recipient recipient, @NotNull Charset charset) {
466461
return new InternetAddress(recipient.getAddress(), recipient.getName(), charset.name());
467462
}
463+
464+
@NotNull
465+
public static Optional<String> findFirstMatch(@NotNull Pattern pattern, @NotNull String input) {
466+
Matcher matcher = pattern.matcher(input);
467+
return matcher.find() ? Optional.of(matcher.group(1)) : Optional.empty();
468+
}
468469
}

Diff for: modules/simple-java-mail/src/main/java/org/simplejavamail/converter/internal/mimemessage/MimeMessageParser.java

+13-10
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,7 @@
3131
import static java.lang.String.format;
3232
import static java.nio.charset.StandardCharsets.UTF_8;
3333
import static java.util.Optional.ofNullable;
34-
import static org.simplejavamail.internal.util.MiscUtil.extractCID;
35-
import static org.simplejavamail.internal.util.MiscUtil.valueNullOrEmpty;
34+
import static org.simplejavamail.internal.util.MiscUtil.*;
3635
import static org.slf4j.LoggerFactory.getLogger;
3736

3837
/**
@@ -43,6 +42,8 @@
4342
public final class MimeMessageParser {
4443

4544
private static final Logger LOGGER = getLogger(MimeMessageParser.class);
45+
private static final Pattern CONTENT_TYPE_METHOD_PATTERN = Pattern.compile("method=\"?(\\w+)");
46+
private static final Pattern CALENDAR_BODY_METHOD_PATTERN = Pattern.compile("(?i)^METHOD:(\\w+)", Pattern.MULTILINE);
4647

4748
static {
4849
MailcapCommandMap mc = (MailcapCommandMap) CommandMap.getDefaultCommandMap();
@@ -90,7 +91,7 @@ private static void parseMimePartTree(@NotNull final MimePart currentPart, @NotN
9091
checkContentTransferEncoding(currentPart, parsedComponents);
9192
} else if (isMimeType(currentPart, "text/calendar") && parsedComponents.calendarContent == null && !Part.ATTACHMENT.equalsIgnoreCase(disposition)) {
9293
parsedComponents.calendarContent = parseCalendarContent(currentPart);
93-
parsedComponents.calendarMethod = parseCalendarMethod(currentPart);
94+
parsedComponents.calendarMethod = parseCalendarMethod(currentPart, parsedComponents.calendarContent);
9495
checkContentTransferEncoding(currentPart, parsedComponents);
9596
} else if (isMimeType(currentPart, "multipart/*")) {
9697
final Multipart mp = parseContent(currentPart);
@@ -165,6 +166,7 @@ private static boolean isEmailHeader(DecodedHeader header, String emailHeaderNam
165166
}
166167

167168
@SuppressWarnings("WeakerAccess")
169+
@NotNull
168170
public static String parseFileName(@NotNull final Part currentPart) {
169171
try {
170172
if (currentPart.getFileName() != null) {
@@ -184,6 +186,7 @@ public static String parseFileName(@NotNull final Part currentPart) {
184186
/**
185187
* @return Returns the "content" part as String from the Calendar content type
186188
*/
189+
@NotNull
187190
public static String parseCalendarContent(@NotNull MimePart currentPart) {
188191
Object content = parseContent(currentPart);
189192
if (content instanceof InputStream) {
@@ -201,18 +204,18 @@ public static String parseCalendarContent(@NotNull MimePart currentPart) {
201204
* @return Returns the "method" part from the Calendar content type (such as "{@code text/calendar; charset="UTF-8"; method="REQUEST"}").
202205
*/
203206
@SuppressWarnings("WeakerAccess")
204-
public static String parseCalendarMethod(@NotNull MimePart currentPart) {
205-
Pattern compile = Pattern.compile("method=\"?(\\w+)");
206-
final String contentType;
207+
public static String parseCalendarMethod(@NotNull MimePart currentPart, @NotNull String calendarContent) {
208+
final String contentType;
207209
try {
208210
contentType = currentPart.getDataHandler().getContentType();
209211
} catch (final MessagingException e) {
210212
throw new MimeMessageParseException(MimeMessageParseException.ERROR_GETTING_CALENDAR_CONTENTTYPE, e);
211213
}
212-
Matcher matcher = compile.matcher(contentType);
213-
Preconditions.assumeTrue(matcher.find(), "Calendar METHOD not found in bodypart content type");
214-
return matcher.group(1);
215-
}
214+
215+
return findFirstMatch(CONTENT_TYPE_METHOD_PATTERN, contentType)
216+
.orElseGet(() -> findFirstMatch(CALENDAR_BODY_METHOD_PATTERN, calendarContent)
217+
.orElseThrow(() -> new IllegalArgumentException("Calendar METHOD not found in bodypart's content type or calendar content itself")));
218+
}
216219

217220
@SuppressWarnings("WeakerAccess")
218221
@Nullable

Diff for: modules/simple-java-mail/src/test/java/org/simplejavamail/converter/EmailConverterTest.java

+56
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package org.simplejavamail.converter;
22

33
import jakarta.mail.util.ByteArrayDataSource;
4+
import net.fortuna.ical4j.data.CalendarBuilder;
5+
import net.fortuna.ical4j.data.ParserException;
6+
import net.fortuna.ical4j.model.Calendar;
7+
import net.fortuna.ical4j.model.Property;
48
import org.assertj.core.api.Condition;
59
import org.jetbrains.annotations.NotNull;
610
import org.junit.jupiter.api.Test;
@@ -17,9 +21,11 @@
1721

1822
import java.io.File;
1923
import java.io.IOException;
24+
import java.io.StringReader;
2025
import java.util.ArrayList;
2126
import java.util.HashMap;
2227
import java.util.List;
28+
import java.util.Optional;
2329
import java.util.regex.Matcher;
2430

2531
import static demo.ResourceFolderHelper.determineResourceFolder;
@@ -419,6 +425,56 @@ public void testGithub551_ContentTransferEncodingEndsWithSpaceBug() {
419425
assertThat(emailMime.getContentTransferEncoding()).isEqualTo(BIT7);
420426
}
421427

428+
@Test
429+
public void testGithub552_BrokenCalendarMethod() throws ParserException, IOException {
430+
Email emailMime = EmailConverter.emlToEmail(new File(RESOURCE_TEST_MESSAGES + "/#552 broken calendar method.eml"));
431+
432+
assertThat(emailMime.getCalendarMethod()).isEqualTo(CalendarMethod.REQUEST);
433+
assertThat(emailMime.getCalendarText()).isNotEmpty();
434+
435+
Calendar calendar = new CalendarBuilder()
436+
.build(new StringReader(emailMime.getCalendarText()));
437+
438+
assertThat(getPropertyValue(calendar, "SUMMARY")).contains("TestYandex");
439+
assertThat(getPropertyValue(calendar, "DTSTART")).contains("20240813T170000");
440+
assertThat(getPropertyValue(calendar, "DTEND")).contains("20240813T173000");
441+
assertThat(getPropertyValue(calendar, "UID")).contains("141zhi60x8914s7bzxzq27i0syandex.ru");
442+
assertThat(getPropertyValue(calendar, "SEQUENCE")).contains("0");
443+
assertThat(getPropertyValue(calendar, "DTSTAMP")).contains("20240813T135030Z");
444+
assertThat(getPropertyValue(calendar, "CREATED")).contains("20240813T135030Z");
445+
assertThat(getPropertyValue(calendar, "LAST-MODIFIED")).contains("20240813T135030Z");
446+
assertThat(getPropertyValue(calendar, "ORGANIZER"))
447+
.hasValueSatisfying(org -> assertThat(org).contains("mailto:"))
448+
.hasValueSatisfying(org -> assertThat(org).contains("ipopov"));
449+
assertThat(calendar.getComponent("VEVENT")
450+
.map(e -> e.getProperties("ATTENDEE")))
451+
.hasValueSatisfying(
452+
attendees -> assertThat(attendees).satisfiesExactlyInAnyOrder(
453+
attendeeProp -> assertThat(attendeeProp.getValue()).satisfies(attendee -> {
454+
assertThat(attendee).contains("mailto:");
455+
assertThat(attendee).contains("ipopov");
456+
}),
457+
attendeeProp -> assertThat(attendeeProp.getValue()).satisfies(attendee -> {
458+
assertThat(attendee).contains("mailto:");
459+
assertThat(attendee).contains("skyvv1sp");
460+
})
461+
)
462+
);
463+
assertThat(getPropertyValue(calendar, "URL")).contains("https://calendar.yandex.ru/event?event_id=2182739972");
464+
assertThat(getPropertyValue(calendar, "TRANSP")).contains("OPAQUE");
465+
assertThat(getPropertyValue(calendar, "CATEGORIES")).contains("Мои события");
466+
assertThat(getPropertyValue(calendar, "CLASS")).contains("PRIVATE");
467+
assertThat(getPropertyValue(calendar, "DESCRIPTION")).contains("");
468+
assertThat(getPropertyValue(calendar, "LOCATION")).contains("");
469+
}
470+
471+
private static @NotNull Optional<String> getPropertyValue(Calendar calendar, String propertyName) {
472+
return calendar
473+
.getComponent("VEVENT")
474+
.flatMap(e -> e.getProperty(propertyName))
475+
.map(Property::getValue);
476+
}
477+
422478
@NotNull
423479
private List<AttachmentResource> asList(AttachmentResource attachment) {
424480
List<AttachmentResource> collectionAttachment = new ArrayList<>();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package org.simplejavamail.converter.internal.mimemessage;
2+
3+
import jakarta.activation.DataHandler;
4+
import jakarta.mail.MessagingException;
5+
import jakarta.mail.internet.MimePart;
6+
import org.junit.jupiter.api.Test;
7+
8+
import static org.assertj.core.api.Assertions.assertThat;
9+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
10+
import static org.mockito.Mockito.mock;
11+
import static org.mockito.Mockito.when;
12+
13+
public class MimeMessageParserParseCalendarMethodTest {
14+
15+
@Test
16+
public void testMethodFoundInContentType() throws Exception {
17+
MimePart mockMimePart = mock(MimePart.class);
18+
DataHandler mockDataHandler = mock(DataHandler.class);
19+
20+
// Mock Content-Type with METHOD
21+
when(mockMimePart.getDataHandler()).thenReturn(mockDataHandler);
22+
when(mockDataHandler.getContentType()).thenReturn("text/calendar; method=REQUEST; charset=UTF-8");
23+
24+
String calendarContent = "BEGIN:VCALENDAR\nMETHOD:REQUEST\nEND:VCALENDAR";
25+
26+
assertThat(MimeMessageParser.parseCalendarMethod(mockMimePart, calendarContent)).isEqualTo("REQUEST");
27+
}
28+
29+
@Test
30+
public void testMethodFoundInBody() throws Exception {
31+
MimePart mockMimePart = mock(MimePart.class);
32+
DataHandler mockDataHandler = mock(DataHandler.class);
33+
34+
// Mock Content-Type without METHOD
35+
when(mockMimePart.getDataHandler()).thenReturn(mockDataHandler);
36+
when(mockDataHandler.getContentType()).thenReturn("text/calendar; charset=UTF-8");
37+
38+
// Method only in calendar content
39+
String calendarContent = "BEGIN:VCALENDAR\nMETHOD:REQUEST\nEND:VCALENDAR";
40+
41+
assertThat(MimeMessageParser.parseCalendarMethod(mockMimePart, calendarContent)).isEqualTo("REQUEST");
42+
}
43+
44+
@Test
45+
public void testMethodNotFoundThrowsException() throws Exception {
46+
MimePart mockMimePart = mock(MimePart.class);
47+
DataHandler mockDataHandler = mock(DataHandler.class);
48+
49+
// Mock Content-Type and Body without METHOD
50+
when(mockMimePart.getDataHandler()).thenReturn(mockDataHandler);
51+
when(mockDataHandler.getContentType()).thenReturn("text/calendar; charset=UTF-8");
52+
53+
// No method in the calendar body
54+
String calendarContent = "BEGIN:VCALENDAR\nEND:VCALENDAR";
55+
56+
assertThatThrownBy(() -> MimeMessageParser.parseCalendarMethod(mockMimePart, calendarContent))
57+
.isInstanceOf(IllegalArgumentException.class)
58+
.hasMessageContaining("Calendar METHOD not found");
59+
}
60+
61+
@Test
62+
public void testMessagingExceptionThrown() throws Exception {
63+
MimePart mockMimePart = mock(MimePart.class);
64+
65+
when(mockMimePart.getDataHandler()).thenThrow(new MessagingException("Failed to retrieve content type"));
66+
67+
String calendarContent = "BEGIN:VCALENDAR\nMETHOD=REQUEST\nEND:VCALENDAR";
68+
69+
assertThatThrownBy(() -> MimeMessageParser.parseCalendarMethod(mockMimePart, calendarContent))
70+
.isInstanceOf(MimeMessageParseException.class)
71+
.hasMessageContaining(MimeMessageParseException.ERROR_GETTING_CALENDAR_CONTENTTYPE);
72+
}
73+
74+
@Test
75+
public void testMethodInBothContentTypeAndBody_ContentTypeTakesPriority() throws Exception {
76+
MimePart mockMimePart = mock(MimePart.class);
77+
DataHandler mockDataHandler = mock(DataHandler.class);
78+
79+
// Mock Content-Type with METHOD
80+
when(mockMimePart.getDataHandler()).thenReturn(mockDataHandler);
81+
when(mockDataHandler.getContentType()).thenReturn("text/calendar; method=REQUEST; charset=UTF-8");
82+
83+
// METHOD also present in calendar content, but different from Content-Type
84+
String calendarContent = "BEGIN:VCALENDAR\nMETHOD:CANCEL\nEND:VCALENDAR";
85+
86+
assertThat(MimeMessageParser.parseCalendarMethod(mockMimePart, calendarContent)).isEqualTo("REQUEST");
87+
}
88+
}

Diff for: modules/simple-java-mail/src/test/java/org/simplejavamail/internal/util/MiscUtilTest.java

+12
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@
1313
import java.util.Map;
1414

1515
import static java.nio.charset.StandardCharsets.UTF_8;
16+
import static java.util.regex.Pattern.compile;
1617
import static org.assertj.core.api.Assertions.assertThat;
1718
import static org.assertj.core.api.Assertions.assertThatThrownBy;
19+
import static org.simplejavamail.internal.util.MiscUtil.findFirstMatch;
1820

1921
public class MiscUtilTest {
2022

@@ -234,4 +236,14 @@ public void testAddRecipientByInternetAddress() {
234236
// next one is unparsable by InternetAddress#parse(), so it should be taken as is
235237
assertThat(MiscUtil.interpretRecipient(null, false, " \" m oo \" [email protected] ", null)).isEqualTo(new Recipient(null, " \" m oo \" [email protected] ", null));
236238
}
239+
240+
@Test
241+
public void testFindFirstMatch() {
242+
assertThat(findFirstMatch(compile("method=(\\w+)"), "Content-Type: text/calendar; method=REQUEST; charset=UTF-8")).hasValue("REQUEST");
243+
assertThat(findFirstMatch(compile("method=(\\w+)"), "Content-Type: text/calendar; charset=UTF-8")).isEmpty();
244+
assertThat(findFirstMatch(compile("method=(\\w+)"), "")).isEmpty();
245+
assertThat(findFirstMatch(compile("method=(\\w+)"), "Content-Type: text/calendar; method=RE$QUEST; charset=UTF-8")).isNotEmpty();
246+
assertThat(findFirstMatch(compile("method=(\\w+)"), "method=REJECT; method=REQUEST")).hasValue("REJECT");
247+
assertThat(findFirstMatch(compile("(?i)method=(\\w+)"), "Content-Type: text/calendar; METHOD=REQUEST; charset=UTF-8")).hasValue("REQUEST");
248+
}
237249
}

0 commit comments

Comments
 (0)