Skip to content

Commit

Permalink
#552: [bug] support iCalendar events with METHOD defined in body inst…
Browse files Browse the repository at this point in the history
…ead of Content-Type
  • Loading branch information
bbottema committed Oct 5, 2024
1 parent a7b2522 commit b456b96
Show file tree
Hide file tree
Showing 6 changed files with 394 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,8 @@
import java.lang.reflect.Method;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

Expand Down Expand Up @@ -465,4 +460,10 @@ public static List<InternetAddress> asInternetAddresses(@NotNull List<Recipient>
public static InternetAddress asInternetAddress(@NotNull Recipient recipient, @NotNull Charset charset) {
return new InternetAddress(recipient.getAddress(), recipient.getName(), charset.name());
}

@NotNull
public static Optional<String> findFirstMatch(@NotNull Pattern pattern, @NotNull String input) {
Matcher matcher = pattern.matcher(input);
return matcher.find() ? Optional.of(matcher.group(1)) : Optional.empty();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@
import static java.lang.String.format;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Optional.ofNullable;
import static org.simplejavamail.internal.util.MiscUtil.extractCID;
import static org.simplejavamail.internal.util.MiscUtil.valueNullOrEmpty;
import static org.simplejavamail.internal.util.MiscUtil.*;
import static org.slf4j.LoggerFactory.getLogger;

/**
Expand All @@ -43,6 +42,8 @@
public final class MimeMessageParser {

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

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

@SuppressWarnings("WeakerAccess")
@NotNull
public static String parseFileName(@NotNull final Part currentPart) {
try {
if (currentPart.getFileName() != null) {
Expand All @@ -184,6 +186,7 @@ public static String parseFileName(@NotNull final Part currentPart) {
/**
* @return Returns the "content" part as String from the Calendar content type
*/
@NotNull
public static String parseCalendarContent(@NotNull MimePart currentPart) {
Object content = parseContent(currentPart);
if (content instanceof InputStream) {
Expand All @@ -201,18 +204,18 @@ public static String parseCalendarContent(@NotNull MimePart currentPart) {
* @return Returns the "method" part from the Calendar content type (such as "{@code text/calendar; charset="UTF-8"; method="REQUEST"}").
*/
@SuppressWarnings("WeakerAccess")
public static String parseCalendarMethod(@NotNull MimePart currentPart) {
Pattern compile = Pattern.compile("method=\"?(\\w+)");
final String contentType;
public static String parseCalendarMethod(@NotNull MimePart currentPart, @NotNull String calendarContent) {
final String contentType;
try {
contentType = currentPart.getDataHandler().getContentType();
} catch (final MessagingException e) {
throw new MimeMessageParseException(MimeMessageParseException.ERROR_GETTING_CALENDAR_CONTENTTYPE, e);
}
Matcher matcher = compile.matcher(contentType);
Preconditions.assumeTrue(matcher.find(), "Calendar METHOD not found in bodypart content type");
return matcher.group(1);
}

return findFirstMatch(CONTENT_TYPE_METHOD_PATTERN, contentType)
.orElseGet(() -> findFirstMatch(CALENDAR_BODY_METHOD_PATTERN, calendarContent)
.orElseThrow(() -> new IllegalArgumentException("Calendar METHOD not found in bodypart's content type or calendar content itself")));
}

@SuppressWarnings("WeakerAccess")
@Nullable
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package org.simplejavamail.converter;

import jakarta.mail.util.ByteArrayDataSource;
import net.fortuna.ical4j.data.CalendarBuilder;
import net.fortuna.ical4j.data.ParserException;
import net.fortuna.ical4j.model.Calendar;
import net.fortuna.ical4j.model.Property;
import org.assertj.core.api.Condition;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Test;
Expand All @@ -17,9 +21,11 @@

import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Optional;
import java.util.regex.Matcher;

import static demo.ResourceFolderHelper.determineResourceFolder;
Expand Down Expand Up @@ -419,6 +425,56 @@ public void testGithub551_ContentTransferEncodingEndsWithSpaceBug() {
assertThat(emailMime.getContentTransferEncoding()).isEqualTo(BIT7);
}

@Test
public void testGithub552_BrokenCalendarMethod() throws ParserException, IOException {
Email emailMime = EmailConverter.emlToEmail(new File(RESOURCE_TEST_MESSAGES + "/#552 broken calendar method.eml"));

assertThat(emailMime.getCalendarMethod()).isEqualTo(CalendarMethod.REQUEST);
assertThat(emailMime.getCalendarText()).isNotEmpty();

Calendar calendar = new CalendarBuilder()
.build(new StringReader(emailMime.getCalendarText()));

assertThat(getPropertyValue(calendar, "SUMMARY")).contains("TestYandex");
assertThat(getPropertyValue(calendar, "DTSTART")).contains("20240813T170000");
assertThat(getPropertyValue(calendar, "DTEND")).contains("20240813T173000");
assertThat(getPropertyValue(calendar, "UID")).contains("141zhi60x8914s7bzxzq27i0syandex.ru");
assertThat(getPropertyValue(calendar, "SEQUENCE")).contains("0");
assertThat(getPropertyValue(calendar, "DTSTAMP")).contains("20240813T135030Z");
assertThat(getPropertyValue(calendar, "CREATED")).contains("20240813T135030Z");
assertThat(getPropertyValue(calendar, "LAST-MODIFIED")).contains("20240813T135030Z");
assertThat(getPropertyValue(calendar, "ORGANIZER"))
.hasValueSatisfying(org -> assertThat(org).contains("mailto:"))
.hasValueSatisfying(org -> assertThat(org).contains("ipopov"));
assertThat(calendar.getComponent("VEVENT")
.map(e -> e.getProperties("ATTENDEE")))
.hasValueSatisfying(
attendees -> assertThat(attendees).satisfiesExactlyInAnyOrder(
attendeeProp -> assertThat(attendeeProp.getValue()).satisfies(attendee -> {
assertThat(attendee).contains("mailto:");
assertThat(attendee).contains("ipopov");
}),
attendeeProp -> assertThat(attendeeProp.getValue()).satisfies(attendee -> {
assertThat(attendee).contains("mailto:");
assertThat(attendee).contains("skyvv1sp");
})
)
);
assertThat(getPropertyValue(calendar, "URL")).contains("https://calendar.yandex.ru/event?event_id=2182739972");
assertThat(getPropertyValue(calendar, "TRANSP")).contains("OPAQUE");
assertThat(getPropertyValue(calendar, "CATEGORIES")).contains("Мои события");
assertThat(getPropertyValue(calendar, "CLASS")).contains("PRIVATE");
assertThat(getPropertyValue(calendar, "DESCRIPTION")).contains("");
assertThat(getPropertyValue(calendar, "LOCATION")).contains("");
}

private static @NotNull Optional<String> getPropertyValue(Calendar calendar, String propertyName) {
return calendar
.getComponent("VEVENT")
.flatMap(e -> e.getProperty(propertyName))
.map(Property::getValue);
}

@NotNull
private List<AttachmentResource> asList(AttachmentResource attachment) {
List<AttachmentResource> collectionAttachment = new ArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package org.simplejavamail.converter.internal.mimemessage;

import jakarta.activation.DataHandler;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimePart;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class MimeMessageParserParseCalendarMethodTest {

@Test
public void testMethodFoundInContentType() throws Exception {
MimePart mockMimePart = mock(MimePart.class);
DataHandler mockDataHandler = mock(DataHandler.class);

// Mock Content-Type with METHOD
when(mockMimePart.getDataHandler()).thenReturn(mockDataHandler);
when(mockDataHandler.getContentType()).thenReturn("text/calendar; method=REQUEST; charset=UTF-8");

String calendarContent = "BEGIN:VCALENDAR\nMETHOD:REQUEST\nEND:VCALENDAR";

assertThat(MimeMessageParser.parseCalendarMethod(mockMimePart, calendarContent)).isEqualTo("REQUEST");
}

@Test
public void testMethodFoundInBody() throws Exception {
MimePart mockMimePart = mock(MimePart.class);
DataHandler mockDataHandler = mock(DataHandler.class);

// Mock Content-Type without METHOD
when(mockMimePart.getDataHandler()).thenReturn(mockDataHandler);
when(mockDataHandler.getContentType()).thenReturn("text/calendar; charset=UTF-8");

// Method only in calendar content
String calendarContent = "BEGIN:VCALENDAR\nMETHOD:REQUEST\nEND:VCALENDAR";

assertThat(MimeMessageParser.parseCalendarMethod(mockMimePart, calendarContent)).isEqualTo("REQUEST");
}

@Test
public void testMethodNotFoundThrowsException() throws Exception {
MimePart mockMimePart = mock(MimePart.class);
DataHandler mockDataHandler = mock(DataHandler.class);

// Mock Content-Type and Body without METHOD
when(mockMimePart.getDataHandler()).thenReturn(mockDataHandler);
when(mockDataHandler.getContentType()).thenReturn("text/calendar; charset=UTF-8");

// No method in the calendar body
String calendarContent = "BEGIN:VCALENDAR\nEND:VCALENDAR";

assertThatThrownBy(() -> MimeMessageParser.parseCalendarMethod(mockMimePart, calendarContent))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Calendar METHOD not found");
}

@Test
public void testMessagingExceptionThrown() throws Exception {
MimePart mockMimePart = mock(MimePart.class);

when(mockMimePart.getDataHandler()).thenThrow(new MessagingException("Failed to retrieve content type"));

String calendarContent = "BEGIN:VCALENDAR\nMETHOD=REQUEST\nEND:VCALENDAR";

assertThatThrownBy(() -> MimeMessageParser.parseCalendarMethod(mockMimePart, calendarContent))
.isInstanceOf(MimeMessageParseException.class)
.hasMessageContaining(MimeMessageParseException.ERROR_GETTING_CALENDAR_CONTENTTYPE);
}

@Test
public void testMethodInBothContentTypeAndBody_ContentTypeTakesPriority() throws Exception {
MimePart mockMimePart = mock(MimePart.class);
DataHandler mockDataHandler = mock(DataHandler.class);

// Mock Content-Type with METHOD
when(mockMimePart.getDataHandler()).thenReturn(mockDataHandler);
when(mockDataHandler.getContentType()).thenReturn("text/calendar; method=REQUEST; charset=UTF-8");

// METHOD also present in calendar content, but different from Content-Type
String calendarContent = "BEGIN:VCALENDAR\nMETHOD:CANCEL\nEND:VCALENDAR";

assertThat(MimeMessageParser.parseCalendarMethod(mockMimePart, calendarContent)).isEqualTo("REQUEST");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
import java.util.Map;

import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.regex.Pattern.compile;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.simplejavamail.internal.util.MiscUtil.findFirstMatch;

public class MiscUtilTest {

Expand Down Expand Up @@ -234,4 +236,14 @@ public void testAddRecipientByInternetAddress() {
// next one is unparsable by InternetAddress#parse(), so it should be taken as is
assertThat(MiscUtil.interpretRecipient(null, false, " \" m oo \" [email protected] ", null)).isEqualTo(new Recipient(null, " \" m oo \" [email protected] ", null));
}

@Test
public void testFindFirstMatch() {
assertThat(findFirstMatch(compile("method=(\\w+)"), "Content-Type: text/calendar; method=REQUEST; charset=UTF-8")).hasValue("REQUEST");
assertThat(findFirstMatch(compile("method=(\\w+)"), "Content-Type: text/calendar; charset=UTF-8")).isEmpty();
assertThat(findFirstMatch(compile("method=(\\w+)"), "")).isEmpty();
assertThat(findFirstMatch(compile("method=(\\w+)"), "Content-Type: text/calendar; method=RE$QUEST; charset=UTF-8")).isNotEmpty();
assertThat(findFirstMatch(compile("method=(\\w+)"), "method=REJECT; method=REQUEST")).hasValue("REJECT");
assertThat(findFirstMatch(compile("(?i)method=(\\w+)"), "Content-Type: text/calendar; METHOD=REQUEST; charset=UTF-8")).hasValue("REQUEST");
}
}
Loading

0 comments on commit b456b96

Please sign in to comment.