JAMES-2560 Introduce a mailet for sanitizing header only ICS
Project: http://git-wip-us.apache.org/repos/asf/james-project/repo Commit: http://git-wip-us.apache.org/repos/asf/james-project/commit/ac82719f Tree: http://git-wip-us.apache.org/repos/asf/james-project/tree/ac82719f Diff: http://git-wip-us.apache.org/repos/asf/james-project/diff/ac82719f Branch: refs/heads/master Commit: ac82719fd460f5bc8a7306fb944cb4d7715b9dac Parents: c7b1600 Author: Benoit Tellier <[email protected]> Authored: Thu Oct 11 11:38:34 2018 +0700 Committer: Benoit Tellier <[email protected]> Committed: Fri Oct 12 15:26:53 2018 +0700 ---------------------------------------------------------------------- mailet/icalendar/pom.xml | 20 +++- .../james/transport/mailets/ICSSanitizer.java | 110 +++++++++++++++++ .../transport/mailets/ICSSanitizerTest.java | 118 +++++++++++++++++++ .../src/test/resources/ics_in_header.eml | 46 ++++++++ 4 files changed, 289 insertions(+), 5 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/james-project/blob/ac82719f/mailet/icalendar/pom.xml ---------------------------------------------------------------------- diff --git a/mailet/icalendar/pom.xml b/mailet/icalendar/pom.xml index 78cddde..a7d03f4 100644 --- a/mailet/icalendar/pom.xml +++ b/mailet/icalendar/pom.xml @@ -80,11 +80,6 @@ <scope>test</scope> </dependency> <dependency> - <groupId>junit</groupId> - <artifactId>junit</artifactId> - <scope>test</scope> - </dependency> - <dependency> <groupId>net.javacrumbs.json-unit</groupId> <artifactId>json-unit-assertj</artifactId> <scope>test</scope> @@ -104,6 +99,21 @@ <scope>test</scope> </dependency> <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-engine</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.junit.platform</groupId> + <artifactId>junit-platform-launcher</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.junit.vintage</groupId> + <artifactId>junit-vintage-engine</artifactId> + <scope>test</scope> + </dependency> + <dependency> <groupId>org.mnode.ical4j</groupId> <artifactId>ical4j</artifactId> <version>3.0.1</version> http://git-wip-us.apache.org/repos/asf/james-project/blob/ac82719f/mailet/icalendar/src/main/java/org/apache/james/transport/mailets/ICSSanitizer.java ---------------------------------------------------------------------- diff --git a/mailet/icalendar/src/main/java/org/apache/james/transport/mailets/ICSSanitizer.java b/mailet/icalendar/src/main/java/org/apache/james/transport/mailets/ICSSanitizer.java new file mode 100644 index 0000000..a2b268d --- /dev/null +++ b/mailet/icalendar/src/main/java/org/apache/james/transport/mailets/ICSSanitizer.java @@ -0,0 +1,110 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.transport.mailets; + +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import javax.mail.BodyPart; +import javax.mail.Header; +import javax.mail.MessagingException; +import javax.mail.Multipart; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMessage; +import javax.mail.internet.MimeMultipart; + +import org.apache.mailet.Mail; +import org.apache.mailet.base.GenericMailet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.github.fge.lambdas.Throwing; + +public class ICSSanitizer extends GenericMailet { + private static final Logger LOGGER = LoggerFactory.getLogger(ICSSanitizer.class); + private static final int TEXT_PREFIX_SIZE = 5; + + @Override + public void service(Mail mail) { + try { + MimeMessage mimeMessage = mail.getMessage(); + + if (mimeMessage.getContent() instanceof Multipart) { + Multipart multipart = (Multipart) mimeMessage.getContent(); + + if (needsSanitizing(multipart)) { + mimeMessage.setContent(sanitize(multipart)); + mimeMessage.saveChanges(); + } + } + } catch (Exception e) { + LOGGER.warn("Could not sanitize {}", mail.getName(), e); + } + } + + private boolean needsSanitizing(Multipart multipart) throws MessagingException { + return bodyPartStream(multipart) + .anyMatch(Throwing.predicate(this::needsSanitizing)); + } + + private boolean needsSanitizing(BodyPart bodyPart) throws MessagingException { + return bodyPart.isMimeType("text/calendar") && bodyPart.getSize() <= 0; + } + + private MimeMultipart sanitize(Multipart multipart) throws MessagingException { + MimeMultipart mimeMultipart = new MimeMultipart(); + bodyPartStream(multipart) + .map(Throwing.function(this::sanitize)) + .forEach(Throwing.consumer(mimeMultipart::addBodyPart)); + return mimeMultipart; + } + + private BodyPart sanitize(BodyPart bodyPart) throws MessagingException { + if (needsSanitizing(bodyPart)) { + if (bodyPart instanceof MimeBodyPart) { + MimeBodyPart mimeBodyPart = (MimeBodyPart) bodyPart; + mimeBodyPart.setText( + computeBodyFromOriginalCalendar(bodyPart), + StandardCharsets.UTF_8.name(), + bodyPart.getContentType().substring(TEXT_PREFIX_SIZE)); + } + } + return bodyPart; + } + + private String computeBodyFromOriginalCalendar(BodyPart bodyPart) throws MessagingException { + return headerStream(bodyPart) + .map(header -> header.getName() + ": " + header.getValue()) + .collect(Collectors.joining("\r\n")); + } + + private Stream<Header> headerStream(BodyPart bodyPart) throws MessagingException { + return Collections.list(bodyPart.getAllHeaders()).stream(); + } + + private Stream<BodyPart> bodyPartStream(Multipart multipart) throws MessagingException { + return IntStream.range(0, multipart.getCount()) + .boxed() + .map(Throwing.function(multipart::getBodyPart)); + } +} http://git-wip-us.apache.org/repos/asf/james-project/blob/ac82719f/mailet/icalendar/src/test/java/org/apache/james/transport/mailets/ICSSanitizerTest.java ---------------------------------------------------------------------- diff --git a/mailet/icalendar/src/test/java/org/apache/james/transport/mailets/ICSSanitizerTest.java b/mailet/icalendar/src/test/java/org/apache/james/transport/mailets/ICSSanitizerTest.java new file mode 100644 index 0000000..1e39fa2 --- /dev/null +++ b/mailet/icalendar/src/test/java/org/apache/james/transport/mailets/ICSSanitizerTest.java @@ -0,0 +1,118 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.transport.mailets; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.nio.charset.StandardCharsets; + +import javax.mail.Multipart; +import javax.mail.util.SharedByteArrayInputStream; + +import org.apache.commons.io.IOUtils; +import org.apache.james.core.builder.MimeMessageBuilder; +import org.apache.james.util.ClassLoaderUtils; +import org.apache.james.util.MimeMessageUtil; +import org.apache.mailet.base.test.FakeMail; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class ICSSanitizerTest { + private ICSSanitizer testee; + + @BeforeEach + void setUp() { + testee = new ICSSanitizer(); + } + + @Test + void serviceShouldEnhanceTextCalendarOnlyHeaders() throws Exception { + FakeMail mail = FakeMail.builder() + .mimeMessage(MimeMessageUtil.mimeMessageFromStream(ClassLoaderUtils.getSystemResourceAsSharedStream("ics_in_header.eml"))) + .build(); + + testee.service(mail); + + Object content = mail.getMessage().getContent(); + + assertThat(content).isInstanceOf(Multipart.class); + Multipart multipart = (Multipart) content; + assertThat(multipart.getCount()).isEqualTo(1); + + assertThat(multipart.getBodyPart(0).getContent()) + .isEqualTo("BEGIN: VCALENDAR\r\n" + + "PRODID: -//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\r\n" + + "VERSION: 2.0\r\n" + + "METHOD: REPLY\r\n" + + "BEGIN: VEVENT\r\n" + + "CREATED: 20180911T144134Z\r\n" + + "LAST-MODIFIED: 20180912T085818Z\r\n" + + "DTSTAMP: 20180912T085818Z\r\n" + + "UID: f1514f44bf39311568d64072945fc3b2973debebb0d550e8c841f3f0604b2481e047fe\r\n" + + " b2aab16e43439a608f28671ab7c10e754cbbe63441a01ba232a553df751eb0931728d67672\r\n" + + " \r\n" + + "SUMMARY: Point Produit\r\n" + + "PRIORITY: 5\r\n" + + "ORGANIZER;CN=Bob;X-OBM-ID=348: mailto:[email protected]\r\n" + + "ATTENDEE;CN=Alice;PARTSTAT=ACCEPTED;CUTYPE=INDIVIDUAL;X-OBM-ID=810: mailto:[email protected]\r\n" + + "DTSTART: 20180919T123000Z\r\n" + + "DURATION: PT1H\r\n" + + "TRANSP: OPAQUE\r\n" + + "SEQUENCE: 0\r\n" + + "X-LIC-ERROR;X-LIC-ERRORTYPE=VALUE-PARSE-ERROR: No value for X property. Rem\r\n" + + " oving entire property:\r\n" + + "CLASS: PUBLIC\r\n" + + "X-OBM-DOMAIN: linagora.com\r\n" + + "X-OBM-DOMAIN-UUID: 02874f7c-d10e-102f-acda-0015176f7922\r\n" + + "LOCATION: TÃ\u0083élÃ\u0083éphone\r\n" + + "END: VEVENT\r\n" + + "END: VCALENDAR\r\n" + + "Content-class: urn:content-classes:calendarmessage\r\n" + + "Content-type: text/calendar; method=REPLY; charset=UTF-8\r\n" + + "Content-transfer-encoding: 8BIT\r\n" + + "Content-Disposition: attachment"); + assertThat(multipart.getBodyPart(0).getContentType()).startsWith("text/calendar; method=REPLY; charset=UTF-8"); + } + + @Test + void validTextCalendarShouldNotBeSanitized() throws Exception { + FakeMail mail = FakeMail.builder() + .mimeMessage( + MimeMessageBuilder.mimeMessageBuilder() + .setMultipartWithBodyParts( + MimeMessageBuilder.bodyPartBuilder() + .type("text/calendar") + .data("Not empty") + .addHeader("X-CUSTOM", "Because it is a valid ICS it should not be pushed in body"))) + .build(); + + testee.service(mail); + + Object content = mail.getMessage().getContent(); + + assertThat(content).isInstanceOf(Multipart.class); + Multipart multipart = (Multipart) content; + assertThat(multipart.getCount()).isEqualTo(1); + + SharedByteArrayInputStream inputStream = (SharedByteArrayInputStream) multipart.getBodyPart(0).getContent(); + assertThat(IOUtils.toString(inputStream, StandardCharsets.UTF_8)) + .doesNotContain("X-CUSTOM"); + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/james-project/blob/ac82719f/mailet/icalendar/src/test/resources/ics_in_header.eml ---------------------------------------------------------------------- diff --git a/mailet/icalendar/src/test/resources/ics_in_header.eml b/mailet/icalendar/src/test/resources/ics_in_header.eml new file mode 100644 index 0000000..76ca1b7 --- /dev/null +++ b/mailet/icalendar/src/test/resources/ics_in_header.eml @@ -0,0 +1,46 @@ +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="----=_Part_80_888179120.1536742699653" +From: Alice <[email protected]> +Message-ID: <[email protected]> +To: Bob <[email protected]> +Date: Wed, 12 Sep 2018 10:58:18 +0200 +Subject: Event Invitation Reply (Accepted): + Point Produit + +------=_Part_80_888179120.1536742699653 +BEGIN:VCALENDAR +PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN +VERSION:2.0 +METHOD:REPLY +BEGIN:VEVENT +CREATED:20180911T144134Z +LAST-MODIFIED:20180912T085818Z +DTSTAMP:20180912T085818Z +UID:f1514f44bf39311568d64072945fc3b2973debebb0d550e8c841f3f0604b2481e047fe + b2aab16e43439a608f28671ab7c10e754cbbe63441a01ba232a553df751eb0931728d67672 + +SUMMARY:Point Produit +PRIORITY:5 +ORGANIZER;CN=Bob;X-OBM-ID=348:mailto:[email protected] +ATTENDEE;CN=Alice;PARTSTAT=ACCEPTED;CUTYPE=INDIVIDUAL;X-OBM-ID=810: + mailto:[email protected] +DTSTART:20180919T123000Z +DURATION:PT1H +TRANSP:OPAQUE +SEQUENCE:0 +X-LIC-ERROR;X-LIC-ERRORTYPE=VALUE-PARSE-ERROR:No value for X property. Rem + oving entire property: +CLASS:PUBLIC +X-OBM-DOMAIN:linagora.com +X-OBM-DOMAIN-UUID:02874f7c-d10e-102f-acda-0015176f7922 +LOCATION:Téléphone +END:VEVENT +END:VCALENDAR +Content-class: urn:content-classes:calendarmessage +Content-type: text/calendar; method=REPLY; charset=UTF-8 +Content-transfer-encoding: 8BIT +Content-Disposition: attachment + + +------=_Part_80_888179120.1536742699653-- --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
