This is an automated email from the ASF dual-hosted git repository. rcordier pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/james-project.git
commit b494b75c30776391725b07e814bd7ac7f69fd145 Author: Matthieu Baechler <[email protected]> AuthorDate: Thu May 14 11:07:57 2020 +0200 JAMES-3176 Rewritte MDN parsing with Parboiled scala --- mdn/pom.xml | 19 +- .../main/java/org/apache/james/mdn/BaseParser.java | 37 - .../java/org/apache/james/mdn/MDNReportParser.java | 758 --------------------- .../org/apache/james/mdn/MDNReportParser.scala | 477 +++++++++++++ .../org/apache/james/mdn/MDNReportParserTest.java | 314 --------- .../org/apache/james/mdn/MDNReportParserTest.scala | 287 ++++++++ .../AutomaticallySentMailDetectorImpl.java | 5 +- .../mailet/ExtractMDNOriginalJMAPMessageId.java | 12 +- 8 files changed, 790 insertions(+), 1119 deletions(-) diff --git a/mdn/pom.xml b/mdn/pom.xml index 1f3ec88..61a91da 100644 --- a/mdn/pom.xml +++ b/mdn/pom.xml @@ -49,11 +49,6 @@ <scope>test</scope> </dependency> <dependency> - <groupId>org.parboiled</groupId> - <artifactId>parboiled-java</artifactId> - <version>1.3.1</version> - </dependency> - <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> </dependency> @@ -69,6 +64,16 @@ <groupId>javax.activation</groupId> <artifactId>javax.activation-api</artifactId> </dependency> + <dependency> + <groupId>org.parboiled</groupId> + <artifactId>parboiled_${scala.base}</artifactId> + <version>2.2.0</version> + </dependency> + <dependency> + <groupId>org.scala-lang.modules</groupId> + <artifactId>scala-java8-compat_${scala.base}</artifactId> + <scope>test</scope> + </dependency> </dependencies> <build> @@ -81,6 +86,10 @@ <forkCount>1C</forkCount> </configuration> </plugin> + <plugin> + <groupId>net.alchim31.maven</groupId> + <artifactId>scala-maven-plugin</artifactId> + </plugin> </plugins> </build> diff --git a/mdn/src/main/java/org/apache/james/mdn/BaseParser.java b/mdn/src/main/java/org/apache/james/mdn/BaseParser.java deleted file mode 100644 index f8da4d9..0000000 --- a/mdn/src/main/java/org/apache/james/mdn/BaseParser.java +++ /dev/null @@ -1,37 +0,0 @@ -/**************************************************************** - * 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.mdn; - -public abstract class BaseParser<V> extends org.parboiled.BaseParser<V> { - @SuppressWarnings("unchecked") - <T> T popT() { - return (T) pop(); - } - - @SuppressWarnings("unchecked") - <T> T peekParent() { - return (T) peek(1); - } - - @SuppressWarnings("unchecked") - <T> T peekT() { - return (T) peek(); - } -} diff --git a/mdn/src/main/java/org/apache/james/mdn/MDNReportParser.java b/mdn/src/main/java/org/apache/james/mdn/MDNReportParser.java deleted file mode 100644 index 8db2e95..0000000 --- a/mdn/src/main/java/org/apache/james/mdn/MDNReportParser.java +++ /dev/null @@ -1,758 +0,0 @@ -/**************************************************************** - * 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.mdn; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Optional; - -import org.apache.commons.io.IOUtils; -import org.apache.james.mdn.action.mode.DispositionActionMode; -import org.apache.james.mdn.fields.AddressType; -import org.apache.james.mdn.fields.Disposition; -import org.apache.james.mdn.fields.Error; -import org.apache.james.mdn.fields.ExtensionField; -import org.apache.james.mdn.fields.FinalRecipient; -import org.apache.james.mdn.fields.Gateway; -import org.apache.james.mdn.fields.OriginalMessageId; -import org.apache.james.mdn.fields.OriginalRecipient; -import org.apache.james.mdn.fields.ReportingUserAgent; -import org.apache.james.mdn.fields.Text; -import org.apache.james.mdn.modifier.DispositionModifier; -import org.apache.james.mdn.sending.mode.DispositionSendingMode; -import org.apache.james.mdn.type.DispositionType; -import org.parboiled.Parboiled; -import org.parboiled.Rule; -import org.parboiled.parserunners.ReportingParseRunner; -import org.parboiled.support.ParsingResult; - -import com.google.common.annotations.VisibleForTesting; - -public class MDNReportParser { - public MDNReportParser() { - } - - public Optional<MDNReport> parse(InputStream is, String charset) throws IOException { - return parse(IOUtils.toString(is, charset)); - } - - public Optional<MDNReport> parse(String mdnReport) { - Parser parser = Parboiled.createParser(MDNReportParser.Parser.class); - ParsingResult<Object> result = new ReportingParseRunner<>(parser.dispositionNotificationContent()).run(mdnReport); - if (result.matched) { - return Optional.of((MDNReport)result.resultValue); - } - return Optional.empty(); - } - - @VisibleForTesting - static class Parser extends BaseParser<Object> { - // CFWS = (1*([FWS] comment) [FWS]) / FWS - Rule cfws() { - return FirstOf( - Sequence( - OneOrMore(Sequence(Optional(fws()), comment())), - Optional(fws())), - fws()); - } - - // FWS = ([*WSP CRLF] 1*WSP) / obs-FWS - Rule fws() { - return FirstOf( - Sequence( - Optional(Sequence( - ZeroOrMore(wsp()), - crlf())), - OneOrMore(wsp())), - obsFWS()); - } - - // WSP = SP / HTAB - Rule wsp() { - return FirstOf(sp(), htab()); - } - - // SP = %x20 - Rule sp() { - return Ch((char)0x20); - } - - // HTAB = %x09 - Rule htab() { - return Ch((char)0x09); - } - - // CRLF = CR LF - Rule crlf() { - return Sequence(cr(), lf()); - } - - // CR = %x0D - Rule cr() { - return Ch((char)0x0D); - } - - // LF = %x0A - Rule lf() { - return Ch((char)0x0A); - } - - // obs-FWS = 1*WSP *(CRLF 1*WSP) - Rule obsFWS() { - return Sequence( - OneOrMore(wsp()), - ZeroOrMore(Sequence( - crlf(), - OneOrMore(wsp())))); - } - - // comment = "(" *([FWS] ccontent) [FWS] ")" - Rule comment() { - return Sequence( - "(", - ZeroOrMore(Sequence( - Optional(fws()), - ccontent() - )), - Optional(fws()), - ")"); - } - - // ccontent = ctext / quoted-pair / comment - Rule ccontent() { - return FirstOf(ctext(), quotedPair(), comment()); - } - - /* ctext = %d33-39 / ; Printable US-ASCII - %d42-91 / ; characters not including - %d93-126 / ; "(", ")", or "\" - obs-ctext */ - Rule ctext() { - return FirstOf( - CharRange((char)33, (char)39), - CharRange((char)42, (char)91), - CharRange((char)93, (char)126), - obsCtext()); - } - - // obs-ctext = obs-NO-WS-CTL - Rule obsCtext() { - return obsNoWsCtl(); - } - - /* obs-NO-WS-CTL = %d1-8 / ; US-ASCII control - %d11 / ; characters that do not - %d12 / ; include the carriage - %d14-31 / ; return, line feed, and - %d127 ; white space characters */ - Rule obsNoWsCtl() { - return FirstOf( - CharRange((char)1, (char)8), - Ch((char)11), - Ch((char)12), - CharRange((char)14, (char)31), - Ch((char)127)); - } - - // quoted-pair = ("\" (VCHAR / WSP)) / obs-qp - Rule quotedPair() { - return FirstOf( - Sequence( - "\\", - FirstOf(vchar(), wsp())), - obsQp()); - } - - // VCHAR = %x21-7E - Rule vchar() { - return CharRange((char)0x21, (char)0x7E); - } - - // obs-qp = "\" (%d0 / obs-NO-WS-CTL / LF / CR) - Rule obsQp() { - return Sequence( - "\\", - FirstOf( - Ch((char)0), - obsCtext(), - lf(), - cr())); - } - - // word = atom / quoted-string - Rule word() { - return FirstOf(atom(), quotedString()); - } - - // atom = [CFWS] 1*atext [CFWS] - Rule atom() { - return Sequence( - Optional(cfws()), - OneOrMore(atext()), - Optional(cfws())); - } - - /* atext = ALPHA / DIGIT / ; Printable US-ASCII - "!" / "#" / ; characters not including - "$" / "%" / ; specials. Used for atoms. - "&" / "'" / - "*" / "+" / - "-" / "/" / - "=" / "?" / - "^" / "_" / - "`" / "{" / - "|" / "}" / - "~" */ - Rule atext() { - return FirstOf( - alpha(), digit(), - "!", "#", - "$", "%", - "&", "'", - "*", "+", - "-", "/", - "=", "?", - "^", "_", - "`", "{", - "|", "}", - "~"); - } - - // ALPHA = %x41-5A / %x61-7A ; A-Z / a-z - Rule alpha() { - return FirstOf(CharRange((char)0x41, (char)0x5A), CharRange((char)0x61, (char)0x7A)); - } - - // DIGIT = %x30-39 - Rule digit() { - return CharRange((char)0x30, (char)0x39); - } - - /* quoted-string = [CFWS] - DQUOTE *([FWS] qcontent) [FWS] DQUOTE - [CFWS] */ - Rule quotedString() { - return Sequence( - Optional(cfws()), - Sequence(dquote(), ZeroOrMore(Sequence(Optional(fws()), qcontent()), Optional(fws()), dquote())), - Optional(cfws())); - } - - // DQUOTE = %x22 - Rule dquote() { - return Ch((char)0x22); - } - - // obs-qtext = obs-NO-WS-CTL - Rule obsQtext() { - return obsNoWsCtl(); - } - - /* qtext = %d33 / ; Printable US-ASCII - %d35-91 / ; characters not including - %d93-126 / ; "\" or the quote character - obs-qtext */ - Rule qtext() { - return FirstOf( - (char)33, - CharRange((char)35, (char)91), - CharRange((char)93, (char)126), - obsQtext()); - } - - // qcontent = qtext / quoted-pair - Rule qcontent() { - return FirstOf(qtext(), quotedPair()); - } - - // domain = dot-atom / domain-literal / obs-domain - Rule domain() { - return FirstOf(dotAtom(), domainLiteral(), obsDomain()); - } - - // dot-atom = [CFWS] dot-atom-text [CFWS] - Rule dotAtom() { - return Sequence(Optional(cfws()), dotAtomText(), Optional(cfws())); - } - - // dot-atom-text = 1*atext *("." 1*atext) - Rule dotAtomText() { - return Sequence(OneOrMore(atext()), ZeroOrMore(Sequence(".", OneOrMore(atext())))); - } - - // domain-literal = [CFWS] "[" *([FWS] dtext) [FWS] "]" [CFWS] - Rule domainLiteral() { - return Sequence(Optional(cfws()), "[", ZeroOrMore(Sequence(Optional(fws()), dtext()), Optional(fws()), "]", Optional(cfws()))); - } - - /* dtext = %d33-90 / ; Printable US-ASCII - %d94-126 / ; characters not including - obs-dtext ; "[", "]", or "\" */ - Rule dtext() { - return FirstOf( - CharRange((char)33, (char)90), - CharRange((char)94, (char)126), - obsDtext()); - } - - // obs-dtext = obs-NO-WS-CTL / quoted-pair - Rule obsDtext() { - return FirstOf(obsNoWsCtl(), quotedPair()); - } - - // obs-domain = atom *("." atom) - Rule obsDomain() { - return Sequence(atom(), ZeroOrMore(Sequence(".", atom()))); - } - - // local-part = dot-atom / quoted-string / obs-local-part - Rule localPart() { - return FirstOf(dotAtom(), quotedString(), obsLocalPart()); - } - - // obs-local-part = word *("." word) - Rule obsLocalPart() { - return Sequence(word(), ZeroOrMore(Sequence(".", word()))); - } - - /* disposition-notification-content = - [ reporting-ua-field CRLF ] - [ mdn-gateway-field CRLF ] - [ original-recipient-field CRLF ] - final-recipient-field CRLF - [ original-message-id-field CRLF ] - disposition-field CRLF - *( error-field CRLF ) - *( extension-field CRLF ) */ - Rule dispositionNotificationContent() { - return Sequence( - push(MDNReport.builder()), - Optional(Sequence(reportingUaField(), ACTION(setReportingUaField()), crlf())), - Optional(Sequence(mdnGatewayField(), ACTION(setMdnGatewayField()), crlf())), - Optional(Sequence(originalRecipientField(), ACTION(setOriginalRecipientField()), crlf())), - Sequence(finalRecipientField(), ACTION(setFinalRecipientField()), crlf()), - Optional(Sequence(originalMessageIdField(), ACTION(setOriginalMessageIdField()), crlf())), - Sequence(dispositionField(), ACTION(setDispositionField()), crlf()), - ZeroOrMore(Sequence(errorField(), ACTION(addErrorField()), crlf())), - ZeroOrMore(Sequence(extentionField(), ACTION(addExtensionField()), crlf())), - ACTION(buildMDNReport())); - } - - boolean setReportingUaField() { - this.<MDNReport.Builder>peekParent().reportingUserAgentField(popT()); - return true; - } - - boolean setMdnGatewayField() { - this.<MDNReport.Builder>peekParent().gatewayField(popT()); - return true; - } - - boolean setOriginalRecipientField() { - this.<MDNReport.Builder>peekParent().originalRecipientField(this.<OriginalRecipient>popT()); - return true; - } - - boolean setFinalRecipientField() { - this.<MDNReport.Builder>peekParent().finalRecipientField(this.<FinalRecipient>popT()); - return true; - } - - boolean setOriginalMessageIdField() { - this.<MDNReport.Builder>peekParent().originalMessageIdField(this.<OriginalMessageId>popT()); - return true; - } - - boolean setDispositionField() { - this.<MDNReport.Builder>peekParent().dispositionField(popT()); - return true; - } - - boolean addErrorField() { - this.<MDNReport.Builder>peekParent().addErrorField(this.<Error>popT()); - return true; - } - - boolean addExtensionField() { - this.<MDNReport.Builder>peekParent().withExtensionField(this.<ExtensionField>popT()); - return true; - } - - boolean buildMDNReport() { - push(this.<MDNReport.Builder>popT().build()); - return true; - } - - /* reporting-ua-field = "Reporting-UA" ":" OWS ua-name OWS [ - ";" OWS ua-product OWS ] */ - Rule reportingUaField() { - return Sequence( - push(ReportingUserAgent.builder()), - "Reporting-UA", ":", ows(), uaName(), ACTION(setUserAgentName()), ows(), - Optional(Sequence(";", ows(), uaProduct(), ACTION(setUserAgentProduct()), ows())), - ACTION(buildReportingUserAgent()) - ); - } - - boolean buildReportingUserAgent() { - push(this.<ReportingUserAgent.Builder>popT().build()); - return true; - } - - boolean setUserAgentName() { - this.<ReportingUserAgent.Builder>peekT().userAgentName(match()); - return true; - } - - boolean setUserAgentProduct() { - this.<ReportingUserAgent.Builder>peekT().userAgentProduct(match()); - return true; - } - - // ua-name = *text-no-semi - Rule uaName() { - return ZeroOrMore(textNoSemi()); - } - - /* text-no-semi = %d1-9 / ; "text" characters excluding NUL, CR, - %d11 / %d12 / %d14-58 / %d60-127 ; LF, or semi-colon */ - Rule textNoSemi() { - return FirstOf( - CharRange((char)1, (char)9), - Character.toChars(11), - Character.toChars(12), - CharRange((char)14, (char)58), - CharRange((char)60, (char)127)); - } - - // ua-product = *([FWS] text) - Rule uaProduct() { - return ZeroOrMore(Sequence(Optional(fws()), text())); - } - - /* text = %d1-9 / ; Characters excluding CR - %d11 / ; and LF - %d12 / - %d14-127 */ - Rule text() { - return FirstOf( - CharRange((char)1, (char)9), - Character.toChars(11), - Character.toChars(12), - CharRange((char)14, (char)127)); - } - - /* OWS = [CFWS] - ; Optional whitespace. - ; MDN generators SHOULD use "*WSP" - ; (Typically a single space or nothing. - ; It SHOULD be nothing at the end of a field.), - ; unless an RFC 5322 "comment" is required. - ; - ; MDN parsers MUST parse it as "[CFWS]". */ - Rule ows() { - return Optional(cfws()); - } - - /* mdn-gateway-field = "MDN-Gateway" ":" OWS mta-name-type OWS - ";" OWS mta-name */ - Rule mdnGatewayField() { - return Sequence( - push(Gateway.builder()), - "MDN-Gateway", ":", - ows(), - mtaNameType(), ACTION(setMtaNameType()), - ows(), - ";", - ows(), - mtaName(), ACTION(setMtaName()), - ACTION(buildGateway())); - } - - boolean setMtaNameType() { - this.<Gateway.Builder>peekT().nameType(new AddressType(match())); - return true; - } - - boolean setMtaName() { - this.<Gateway.Builder>peekT().name(Text.fromRawText(match())); - return true; - } - - boolean buildGateway() { - push(this.<Gateway.Builder>popT().build()); - return true; - } - - // mta-name-type = Atom - Rule mtaNameType() { - return atom(); - } - - // mta-name = *text - Rule mtaName() { - return ZeroOrMore(text()); - } - - /* original-recipient-field = - "Original-Recipient" ":" OWS address-type OWS - ";" OWS generic-address OWS */ - Rule originalRecipientField() { - return Sequence( - push(OriginalRecipient.builder()), - "Original-Recipient", ":", - ows(), - addressType(), ACTION(setOriginalAddressType()), - ows(), - ";", - ows(), - genericAddress(), ACTION(setOriginalGenericAddress()), - ows(), - ACTION(buildOriginalRecipient())); - } - - boolean setOriginalAddressType() { - this.<OriginalRecipient.Builder>peekT().addressType(new AddressType(match())); - return true; - } - - boolean setOriginalGenericAddress() { - this.<OriginalRecipient.Builder>peekT().originalRecipient(Text.fromRawText(match())); - return true; - } - - boolean buildOriginalRecipient() { - push(this.<OriginalRecipient.Builder>popT().build()); - return true; - } - - // address-type = Atom - Rule addressType() { - return atom(); - } - - // generic-address = *text - Rule genericAddress() { - return ZeroOrMore(text()); - } - - /* final-recipient-field = - "Final-Recipient" ":" OWS address-type OWS - ";" OWS generic-address OWS */ - Rule finalRecipientField() { - return Sequence( - push(FinalRecipient.builder()), - "Final-Recipient", ":", - ows(), - addressType(), ACTION(setFinalAddressType()), - ows(), - ";", - ows(), - genericAddress(), ACTION(setFinalGenericAddress()), - ows(), - ACTION(buildFinalRecipient())); - } - - boolean setFinalAddressType() { - this.<FinalRecipient.Builder>peekT().addressType(new AddressType(match())); - return true; - } - - boolean setFinalGenericAddress() { - this.<FinalRecipient.Builder>peekT().finalRecipient(Text.fromRawText(match())); - return true; - } - - boolean buildFinalRecipient() { - push(this.<FinalRecipient.Builder>popT().build()); - return true; - } - - // original-message-id-field = "Original-Message-ID" ":" msg-id - Rule originalMessageIdField() { - return Sequence("Original-Message-ID", ":", msgId(), push(new OriginalMessageId(match()))); - } - - // msg-id = [CFWS] "<" id-left "@" id-right ">" [CFWS] - Rule msgId() { - return Sequence(Optional(cfws()), "<", idLeft(), "@", idRight(), ">", Optional(cfws())); - } - - // id-left = dot-atom-text / obs-id-left - Rule idLeft() { - return FirstOf(dotAtomText(), obsIdLeft()); - } - - // obs-id-left = local-part - Rule obsIdLeft() { - return localPart(); - } - - // obs-id-right = domain - Rule idRight() { - return domain(); - } - - /* disposition-field = - "Disposition" ":" OWS disposition-mode OWS ";" - OWS disposition-type - [ OWS "/" OWS disposition-modifier - *( OWS "," OWS disposition-modifier ) ] OWS */ - Rule dispositionField() { - return Sequence( - push(Disposition.builder()), - "Disposition", ":", - ows(), - dispositionMode(), - ows(), - ";", - ows(), - dispositionType(), - Optional( - Sequence( - ows(), - "/", - ows(), - dispositionModifier(), ACTION(addDispositionModifier()), - ZeroOrMore( - Sequence( - ows(), - ",", - dispositionModifier(), ACTION(addDispositionModifier()))))), - ows(), - ACTION(buildDispositionField())); - } - - boolean addDispositionModifier() { - this.<Disposition.Builder>peekT().addModifier(new DispositionModifier(match())); - return true; - } - - boolean buildDispositionField() { - push(this.<Disposition.Builder>popT().build()); - return true; - } - - // disposition-mode = action-mode OWS "/" OWS sending-mode - Rule dispositionMode() { - return Sequence(actionMode(), ows(), "/", ows(), sendingMode()); - } - - // action-mode = "manual-action" / "automatic-action" - Rule actionMode() { - return FirstOf( - Sequence("manual-action", ACTION(setActionMode(DispositionActionMode.Manual))), - Sequence("automatic-action", ACTION(setActionMode(DispositionActionMode.Automatic)))); - } - - boolean setActionMode(DispositionActionMode actionMode) { - this.<Disposition.Builder>peekT().actionMode(actionMode); - return true; - } - - // sending-mode = "MDN-sent-manually" / "MDN-sent-automatically" - Rule sendingMode() { - return FirstOf( - Sequence("MDN-sent-manually", ACTION(setSendingMode(DispositionSendingMode.Manual))), - Sequence("MDN-sent-automatically", ACTION(setSendingMode(DispositionSendingMode.Automatic)))); - } - - boolean setSendingMode(DispositionSendingMode sendingMode) { - this.<Disposition.Builder>peekT().sendingMode(sendingMode); - return true; - } - - /* disposition-type = "displayed" / "deleted" / "dispatched" / - "processed" */ - Rule dispositionType() { - return FirstOf( - Sequence("displayed", ACTION(setDispositionType(DispositionType.Displayed))), - Sequence("deleted", ACTION(setDispositionType(DispositionType.Deleted))), - Sequence("dispatched", ACTION(setDispositionType(DispositionType.Dispatched))), - Sequence("processed", ACTION(setDispositionType(DispositionType.Processed)))); - } - - boolean setDispositionType(DispositionType type) { - this.<Disposition.Builder>peekT().type(type); - return true; - } - - // disposition-modifier = "error" / disposition-modifier-extension - Rule dispositionModifier() { - return FirstOf("error", dispositionModifierExtension()); - } - - // disposition-modifier-extension = Atom - Rule dispositionModifierExtension() { - return atom(); - } - - // error-field = "Error" ":" *([FWS] text) - Rule errorField() { - return Sequence( - "Error", ":", - ZeroOrMore(Sequence(Optional(fws()), text())), push(new Error(Text.fromRawText(match())))); - } - - // extension-field = extension-field-name ":" *([FWS] text) - Rule extentionField() { - return Sequence( - push(ExtensionField.builder()), - extensionFieldName(), ACTION(setExtensionFieldName()), - ":", - ZeroOrMore(Sequence(Optional(fws()), text())), ACTION(setExtensionText()), - ACTION(buildExtension())); - } - - boolean setExtensionFieldName() { - this.<ExtensionField.Builder>peekT().fieldName(match()); - return true; - } - - boolean setExtensionText() { - this.<ExtensionField.Builder>peekT().rawValue(match()); - return true; - } - - boolean buildExtension() { - push(this.<ExtensionField.Builder>popT().build()); - return true; - } - - // extension-field-name = field-name - Rule extensionFieldName() { - return fieldName(); - } - - // field-name = 1*ftext - Rule fieldName() { - return OneOrMore(ftext()); - } - - /* ftext = %d33-57 / ; Printable US-ASCII - %d59-126 ; characters not including - ; ":". */ - Rule ftext() { - return FirstOf( - CharRange((char)33, (char)57), - CharRange((char)59, (char)126)); - } - } -} diff --git a/mdn/src/main/scala/org/apache/james/mdn/MDNReportParser.scala b/mdn/src/main/scala/org/apache/james/mdn/MDNReportParser.scala new file mode 100644 index 0000000..bd5b685 --- /dev/null +++ b/mdn/src/main/scala/org/apache/james/mdn/MDNReportParser.scala @@ -0,0 +1,477 @@ +/**************************************************************** + * 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.mdn + +import java.io.InputStream + +import org.apache.commons.io.IOUtils +import org.apache.james.mdn.`type`.DispositionType +import org.apache.james.mdn.action.mode.DispositionActionMode +import org.apache.james.mdn.fields._ +import org.apache.james.mdn.modifier.DispositionModifier +import org.apache.james.mdn.sending.mode.DispositionSendingMode +import org.parboiled2._ +import org.slf4j.LoggerFactory +import shapeless.HNil + +import scala.util.{Failure, Try} + +object MDNReportParser { + private val LOGGER = LoggerFactory.getLogger(classOf[MDNReportParser]) + + def parse(is: InputStream, charset: String): Try[MDNReport] = new MDNReportParser(IOUtils.toString(is, charset)).dispositionNotificationContent.run() + + def parse(input : String): Try[MDNReport] = { + val parser = new MDNReportParser(input) + val result = parser.dispositionNotificationContent.run() + + result match { + case res@Failure(e : ParseError) => + LOGGER.debug(parser.formatError(e)) + res + case res => res + } + } +} + +class MDNReportParser(val input: ParserInput) extends Parser { + + /* disposition-notification-content = + [ reporting-ua-field CRLF ] + [ mdn-gateway-field CRLF ] + [ original-recipient-field CRLF ] + final-recipient-field CRLF + [ original-message-id-field CRLF ] + disposition-field CRLF + *( error-field CRLF ) + *( extension-field CRLF ) */ + private def dispositionNotificationContent: Rule1[MDNReport] = rule { + ( + (reportingUaField ~ crlf).? ~ + (mdnGatewayField ~ crlf).? ~ + (originalRecipientField ~ crlf).? ~ + finalRecipientField ~ crlf ~ + (originalMessageIdField ~ crlf).? ~ + dispositionField ~ crlf ~ + zeroOrMore(errorField ~ crlf) ~ + zeroOrMore(extentionField ~ crlf) + ) ~> ((reportingUserAgent : Option[ReportingUserAgent], + gateway : Option[Gateway], + originalRecipient : Option[OriginalRecipient], + finalRecipient: FinalRecipient, + originalMessageId: Option[OriginalMessageId], + disposition: Disposition, + errors: Seq[Error], + extensions: Seq[ExtensionField]) => { + val builder = MDNReport.builder() + .finalRecipientField(finalRecipient) + .dispositionField(disposition) + .addErrorFields(errors:_*) + .withExtensionFields(extensions:_*) + + val builderWithUa = reportingUserAgent.fold(builder)(builder.reportingUserAgentField) + val builderWithGateway = gateway.fold(builderWithUa)(builder.gatewayField) + val builderWithOriginalRecipent = originalRecipient.fold(builderWithGateway)(builder.originalRecipientField) + val builderWithOriginalMessageId = originalMessageId.fold(builderWithOriginalRecipent)(builder.originalMessageIdField) + builderWithOriginalMessageId.build() + }) + } + + /* reporting-ua-field = "Reporting-UA" ":" OWS ua-name OWS [ + ";" OWS ua-product OWS ] */ + private[mdn] def reportingUaField: Rule1[ReportingUserAgent] = rule { + ("Reporting-UA" ~ ":" ~ ows ~ capture(uaName) ~ ows ~ (";" ~ ows ~ capture(uaProduct) ~ ows).?) ~> ((uaName: String, uaProduct: Option[String]) => { + val builder = ReportingUserAgent.builder() + .userAgentName(uaName) + (uaProduct match { + case Some(product) => builder.userAgentProduct(product) + case None => builder + }).build() + }) + } + + // ua-name = *text-no-semi + private def uaName: Rule0 = rule { zeroOrMore(textNoSemi) } + + /* text-no-semi = %d1-9 / ; "text" characters excluding NUL, CR, + %d11 / %d12 / %d14-58 / %d60-127 ; LF, or semi-colon */ + private def textNoSemi: Rule0 = rule { + CharPredicate(1.toChar to 9.toChar) | + ch(11) | + ch(12) | + CharPredicate(14.toChar to 58.toChar) | + CharPredicate(60.toChar to 127.toChar) + } + + // ua-product = *([FWS] text) + private def uaProduct: Rule0 = rule { zeroOrMore(fws.? ~ text) } + + /* text = %d1-9 / ; Characters excluding CR + %d11 / ; and LF + %d12 / + %d14-127 */ + private def text = rule { + CharPredicate(1.toChar to 9.toChar) | + ch(11) | + ch(12) | + CharPredicate(14.toChar to 127.toChar) + } + + /* OWS = [CFWS] + ; Optional whitespace. + ; MDN generators SHOULD use "*WSP" + ; (Typically a single space or nothing. + ; It SHOULD be nothing at the end of a field.), + ; unless an RFC 5322 "comment" is required. + ; + ; MDN parsers MUST parse it as "[CFWS]". */ + private def ows = rule { + cfws.? + } + + /* mdn-gateway-field = "MDN-Gateway" ":" OWS mta-name-type OWS + ";" OWS mta-name */ + def mdnGatewayField : Rule1[Gateway] = rule { + ("MDN-Gateway" ~ ":" ~ ows ~ capture(mtaNameType) ~ ows ~ ";" ~ ows ~ capture(mtaName) ~ ows) ~> ((gatewayType : String, name : String) => Gateway + .builder() + .name(Text.fromRawText(name)) + .nameType(new AddressType(gatewayType)) + .build()) + } + + // mta-name-type = Atom + private def mtaNameType = rule { atom } + + // mta-name = *text + private def mtaName = rule { zeroOrMore(text) } + + /* original-recipient-field = + "Original-Recipient" ":" OWS address-type OWS + ";" OWS generic-address OWS */ + private[mdn] def originalRecipientField : Rule1[OriginalRecipient] = rule { + ("Original-Recipient" ~ ":" ~ ows ~ capture(addressType) ~ ows ~ ";" ~ ows ~ capture(genericAddress) ~ ows) ~> ((addrType : String, genericAddr : String) => + OriginalRecipient + .builder() + .addressType(new AddressType(addrType)) + .originalRecipient(Text.fromRawText(genericAddr)) + .build() + ) + } + + // address-type = Atom + private def addressType = rule { atom } + + // generic-address = *text + private def genericAddress = rule { zeroOrMore(text) } + + /* final-recipient-field = + "Final-Recipient" ":" OWS address-type OWS + ";" OWS generic-address OWS */ + private[mdn] def finalRecipientField : Rule1[FinalRecipient] = rule { + ("Final-Recipient" ~ ":" ~ ows ~ capture(addressType) ~ ows ~ ";" ~ ows ~ capture(genericAddress) ~ ows) ~> ((addrType : String, genericAddr : String) => + FinalRecipient + .builder() + .addressType(new AddressType(addrType)) + .finalRecipient(Text.fromRawText(genericAddr)) + .build() + ) + } + + // original-message-id-field = "Original-Message-ID" ":" msg-id + private[mdn] def originalMessageIdField: Rule1[OriginalMessageId] = rule { + "Original-Message-ID" ~ ":" ~ capture(msgId) ~> ((msgId: String) => new OriginalMessageId(msgId)) + } + + // msg-id = [CFWS] "<" id-left "@" id-right ">" [CFWS] + private def msgId: Rule0 = rule { cfws.? ~ "<" ~ idLeft ~ "@" ~ idRight ~ ">" ~ cfws.? } + + // id-left = dot-atom-text / obs-id-left + private def idLeft: Rule0 = rule { dotAtomText | obsIdLeft } + + // obs-id-left = local-part + private def obsIdLeft: Rule0 = rule { localPart } + + // obs-id-right = domain + private def idRight = rule { domain } + + /* disposition-field = + "Disposition" ":" OWS disposition-mode OWS ";" + OWS disposition-type + [ OWS "/" OWS disposition-modifier + *( OWS "," OWS disposition-modifier ) ] OWS */ + private[mdn] def dispositionField : Rule1[Disposition] = rule { + ("Disposition" ~ ":" ~ ows ~ dispositionMode ~ ows ~ ";" ~ + ows ~ dispositionType ~ + dispositionModifiers.? ~ ows) ~> ((modes: (DispositionActionMode, DispositionSendingMode), + dispositionType: DispositionType, + dispositionModifiers: Option[Seq[DispositionModifier]]) => + Disposition.builder() + .actionMode(modes._1) + .sendingMode(modes._2) + .`type`(dispositionType) + .addModifiers(dispositionModifiers.getOrElse(Nil):_*) + .build() + ) + } + + + + // disposition-mode = action-mode OWS "/" OWS sending-mode + private def dispositionMode: Rule1[(DispositionActionMode, DispositionSendingMode)] = rule { + (capture(actionMode) ~ ows ~ "/" ~ ows ~ capture(sendingMode)) ~> ((actionMode: String, sendingMode: String) => { + val action = actionMode match { + case "manual-action" => DispositionActionMode.Manual + case "automatic-action" => DispositionActionMode.Automatic + } + val sending = sendingMode match { + case "MDN-sent-manually" => DispositionSendingMode.Manual + case "MDN-sent-automatically" => DispositionSendingMode.Automatic + } + (action, sending) + }) + } + + // action-mode = "manual-action" / "automatic-action" + private def actionMode = rule { "manual-action" | "automatic-action" } + + // sending-mode = "MDN-sent-manually" / "MDN-sent-automatically" + private def sendingMode = rule {"MDN-sent-manually" | "MDN-sent-automatically" } + + /* disposition-type = "displayed" / "deleted" / "dispatched" / + "processed" */ + private def dispositionType : Rule1[DispositionType] = rule { + "displayed" ~ push(DispositionType.Displayed) | + "deleted" ~ push(DispositionType.Deleted) | + "dispatched" ~ push(DispositionType.Dispatched) | + "processed" ~ push(DispositionType.Processed) + } + //subpart of disposition-field corresponding to : + // [ OWS "/" OWS disposition-modifier + // *( OWS "," OWS disposition-modifier ) ] + private def dispositionModifiers: Rule1[Seq[DispositionModifier]] = rule { (ows ~ "/" ~ ows ~ capture(dispositionModifier) ~ + zeroOrMore(ows ~ "," ~ ows ~ capture(dispositionModifier))) ~> ((head: String, tail: Seq[String]) => + tail.prepended(head).map(new DispositionModifier(_))) + } + + + // disposition-modifier = "error" / disposition-modifier-extension + private def dispositionModifier = rule { "error" | dispositionModifierExtension } + + // disposition-modifier-extension = Atom + private def dispositionModifierExtension = rule { atom } + + // error-field = "Error" ":" *([FWS] text) + private[mdn] def errorField: Rule1[Error] = rule { ("Error" ~ ":" ~ capture(zeroOrMore(fws.? ~ text))) ~> ((error: String) => new Error(Text.fromRawText(error))) } + + // extension-field = extension-field-name ":" *([FWS] text) + private[mdn] def extentionField: Rule1[ExtensionField] = rule { capture(extensionFieldName) ~ ":" ~ capture(zeroOrMore(fws.? ~ text)) ~> ((extensionFieldName: String, text : String) => + ExtensionField.builder() + .fieldName(extensionFieldName) + .rawValue(text) + .build()) } + + // extension-field-name = field-name + private def extensionFieldName: Rule0 = rule { fieldName } + + // field-name = 1*ftext + private def fieldName: Rule0 = rule { oneOrMore(ftext) } + + /* ftext = %d33-57 / ; Printable US-ASCII + %d59-126 ; characters not including + ; ":". */ + private def ftext: Rule0 = rule { + CharPredicate(33.toChar to 57.toChar) | + CharPredicate(59.toChar to 126.toChar) + } + + // CFWS = (1*([FWS] comment) [FWS]) / FWS + private def cfws: Rule0 = rule { (oneOrMore(fws.? ~ comment) ~ fws) | fws } + + // FWS = ([*WSP CRLF] 1*WSP) / obs-FWS + private def fws: Rule0 = rule { ((zeroOrMore(wsp) ~ crlf).? ~ oneOrMore(wsp)) | obsFWS } + + // WSP = SP / HTAB + private def wsp: Rule0 = rule { sp | htab } + + // SP = %x20 + private def sp: Rule0 = rule { ch(0x20) } + + // HTAB = %x09 + private def htab: Rule0 = rule { ch(0x09) } + + // CRLF = CR LF + private def crlf: Rule0 = rule { cr ~ lf } + + // CR = %x0D + private def cr: Rule0 = rule { ch(0x0d) } + + // LF = %x0A + private def lf: Rule0 = rule { ch(0x0a) } + + // obs-FWS = 1*WSP *(CRLF 1*WSP) + private def obsFWS: Rule0 = rule { oneOrMore(wsp) ~ zeroOrMore(crlf ~ oneOrMore(wsp)) } + + // comment = "(" *([FWS] ccontent) [FWS] ")" + private def comment: Rule[HNil, HNil] = rule { "(" ~ zeroOrMore(fws.? ~ ccontent) ~ fws.? ~ ")" } + + // ccontent = ctext / quoted-pair / comment + private def ccontent: Rule[HNil, HNil] = rule { ctext | quotedPair | comment } + + /* ctext = %d33-39 / ; Printable US-ASCII + %d42-91 / ; characters not including + %d93-126 / ; "(", ")", or "\" + obs-ctext */ + private def ctext = rule { + CharPredicate(33.toChar to 39.toChar) | + CharPredicate(42.toChar to 91.toChar) | + CharPredicate(93.toChar to 126.toChar) | + obsCText + } + + // obs-ctext = obs-NO-WS-CTL + private def obsCText = rule { obsNoWsCtl } + + /* obs-NO-WS-CTL = %d1-8 / ; US-ASCII control + %d11 / ; characters that do not + %d12 / ; include the carriage + %d14-31 / ; return, line feed, and + %d127 ; white space characters */ + private def obsNoWsCtl = rule { + CharPredicate(33.toChar to 39.toChar) | + ch(11) | + ch(12) | + CharPredicate(14.toChar to 31.toChar) | + ch(127) + } + + // quoted-pair = ("\" (VCHAR / WSP)) / obs-qp + private def quotedPair: Rule0 = rule { ("\\" ~ (vchar | wsp)) | obsQp } + + // VCHAR = %x21-7E + private def vchar: Rule0 = rule { CharPredicate(21.toChar to 0x7e.toChar) } + + // obs-qp = "\" (%d0 / obs-NO-WS-CTL / LF / CR) + private def obsQp: Rule0 = rule { "\\" ~ (ch(0xd0) | obsCText | lf | cr) } + + // word = atom / quoted-string + private def word: Rule0 = rule { atom | quotedString } + + // atom = [CFWS] 1*atext [CFWS] + private def atom: Rule0 = rule { cfws.? ~ oneOrMore(atext) ~ cfws.? } + + /* atext = ALPHA / DIGIT / ; Printable US-ASCII + "!" / "#" / ; characters not including + "$" / "%" / ; specials. Used for atoms. + "&" / "'" / + "*" / "+" / + "-" / "/" / + "=" / "?" / + "^" / "_" / + "`" / "{" / + "|" / "}" / + "~" */ + private def atext: Rule0 = rule { + alpha | digit | + "!" | "#" | + "$" | "%" | + "&" | "'" | + "*" | "+" | + "-" | "/" | + "=" | "?" | + "^" | "_" | + "`" | "{" | + "|" | "}" | + "~" + } + + // ALPHA = %x41-5A / %x61-7A ; A-Z / a-z + private def alpha = rule { + CharPredicate(0x41.toChar to 0x5a.toChar) | + CharPredicate(0x61.toChar to 0x7a.toChar) + } + + // DIGIT = %x30-39 + private def digit = rule { CharPredicate(0x30.toChar to 0x39.toChar) } + + /* quoted-string = [CFWS] + DQUOTE *([FWS] qcontent) [FWS] DQUOTE + [CFWS] */ + + private def quotedString: Rule0 = rule { + cfws.? ~ + dquote ~ zeroOrMore(fws.? ~ qcontent) ~ fws.? ~ dquote ~ + cfws.? + } + + // DQUOTE = %x22 + private def dquote = rule { ch(0x22) } + + // qcontent = qtext / quoted-pair + private def qcontent: Rule0 = rule { qtext | quotedPair } + + // qtext = %d33 / ; Printable US-ASCII + // %d35-91 / ; characters not including + // %d93-126 / ; "\" or the quote character + // obs-qtext + private def qtext: Rule0 = rule { + ch(33) | + CharPredicate(35.toChar to 91.toChar) | + CharPredicate(93.toChar to 126.toChar) | + obsQtext + } + + private def obsQtext: Rule0 = obsNoWsCtl + + // domain = dot-atom / domain-literal / obs-domain + private def domain = rule { dotAtom | domainLiteral | dotAtom } + + // dot-atom = [CFWS] dot-atom-text [CFWS] + private def dotAtom = rule { cfws.? ~ dotAtomText ~ cfws.? } + + // dot-atom-text = 1*atext *("." 1*atext) + private def dotAtomText = rule { oneOrMore(atext) ~ zeroOrMore("." ~ oneOrMore(atext)) } + + // domain-literal = [CFWS] "[" *([FWS] dtext) [FWS] "]" [CFWS] + private def domainLiteral = rule { + cfws.? ~ "[" ~ zeroOrMore(fws.? ~ dtext) ~ fws.? ~ "]" ~ cfws.? + } + + /* dtext = %d33-90 / ; Printable US-ASCII + %d94-126 / ; characters not including + obs-dtext ; "[", "]", or "\" */ + private def dtext = rule { + CharPredicate(33.toChar to 90.toChar) | + CharPredicate(94.toChar to 126.toChar) | + obsDtext + } + + // obs-dtext = obs-NO-WS-CTL / quoted-pair + private def obsDtext = rule { obsNoWsCtl | quotedPair } + + // obs-domain = atom *("." atom) + private def obsDomain = rule { atom ~ zeroOrMore("." ~ atom) } + + // local-part = dot-atom / quoted-string / obs-local-part + private def localPart: Rule0 = rule { dotAtom | quotedString | obsLocalPart } + + // obs-local-part = word *("." word) + private def obsLocalPart: Rule0 = rule { word ~ zeroOrMore("." ~ word) } + +} \ No newline at end of file diff --git a/mdn/src/test/java/org/apache/james/mdn/MDNReportParserTest.java b/mdn/src/test/java/org/apache/james/mdn/MDNReportParserTest.java deleted file mode 100644 index 50c2815..0000000 --- a/mdn/src/test/java/org/apache/james/mdn/MDNReportParserTest.java +++ /dev/null @@ -1,314 +0,0 @@ -/**************************************************************** - * 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.mdn; - -import static org.assertj.core.api.Assertions.assertThat; - -import java.util.Optional; - -import org.apache.james.mdn.MDNReportParser.Parser; -import org.apache.james.mdn.action.mode.DispositionActionMode; -import org.apache.james.mdn.fields.AddressType; -import org.apache.james.mdn.fields.Disposition; -import org.apache.james.mdn.fields.Error; -import org.apache.james.mdn.fields.ExtensionField; -import org.apache.james.mdn.fields.FinalRecipient; -import org.apache.james.mdn.fields.Gateway; -import org.apache.james.mdn.fields.OriginalMessageId; -import org.apache.james.mdn.fields.OriginalRecipient; -import org.apache.james.mdn.fields.ReportingUserAgent; -import org.apache.james.mdn.fields.Text; -import org.apache.james.mdn.modifier.DispositionModifier; -import org.apache.james.mdn.sending.mode.DispositionSendingMode; -import org.apache.james.mdn.type.DispositionType; -import org.junit.Test; -import org.parboiled.Parboiled; -import org.parboiled.parserunners.ReportingParseRunner; -import org.parboiled.support.ParsingResult; - -public class MDNReportParserTest { - - @Test - public void parseShouldReturnEmptyWhenMissingFinalRecipient() { - String missing = "Disposition: automatic-action/MDN-sent-automatically;processed\r\n"; - MDNReportParser testee = new MDNReportParser(); - assertThat(testee.parse(missing)).isEmpty(); - } - - @Test - public void parseShouldReturnMdnReportWhenMaximalSubset() { - String maximal = "Reporting-UA: UA_name; UA_product\r\n" + - "MDN-Gateway: smtp; apache.org\r\n" + - "Original-Recipient: rfc822; originalRecipient\r\n" + - "Final-Recipient: rfc822; final_recipient\r\n" + - "Original-Message-ID: <[email protected]>\r\n" + - "Disposition: automatic-action/MDN-sent-automatically;processed/error,failed\r\n" + - "Error: Message1\r\n" + - "Error: Message2\r\n" + - "X-OPENPAAS-IP: 177.177.177.77\r\n" + - "X-OPENPAAS-PORT: 8000\r\n"; - Optional<MDNReport> expected = Optional.of(MDNReport.builder() - .reportingUserAgentField(ReportingUserAgent.builder() - .userAgentName("UA_name") - .userAgentProduct("UA_product") - .build()) - .gatewayField(Gateway.builder() - .nameType(new AddressType("smtp")) - .name(Text.fromRawText("apache.org")) - .build()) - .originalRecipientField("originalRecipient") - .finalRecipientField("final_recipient") - .originalMessageIdField("<[email protected]>") - .dispositionField(Disposition.builder() - .actionMode(DispositionActionMode.Automatic) - .sendingMode(DispositionSendingMode.Automatic) - .type(DispositionType.Processed) - .addModifier(DispositionModifier.Error) - .addModifier(DispositionModifier.Failed) - .build()) - .addErrorField("Message1") - .addErrorField("Message2") - .withExtensionField(ExtensionField.builder() - .fieldName("X-OPENPAAS-IP") - .rawValue(" 177.177.177.77") - .build()) - .withExtensionField(ExtensionField.builder() - .fieldName("X-OPENPAAS-PORT") - .rawValue(" 8000") - .build()) - .build()); - MDNReportParser testee = new MDNReportParser(); - Optional<MDNReport> actual = testee.parse(maximal); - assertThat(actual).isEqualTo(expected); - } - - @Test - public void parseShouldReturnMdnReportWhenMinimalSubset() { - String minimal = "Final-Recipient: rfc822; final_recipient\r\n" + - "Disposition: automatic-action/MDN-sent-automatically;processed\r\n"; - Optional<MDNReport> expected = Optional.of(MDNReport.builder() - .finalRecipientField("final_recipient") - .dispositionField(Disposition.builder() - .actionMode(DispositionActionMode.Automatic) - .sendingMode(DispositionSendingMode.Automatic) - .type(DispositionType.Processed) - .build()) - .build()); - MDNReportParser testee = new MDNReportParser(); - Optional<MDNReport> actual = testee.parse(minimal); - assertThat(actual).isEqualTo(expected); - } - - @Test - public void parseShouldReturnEmptyWhenDuplicatedFields() { - String duplicated = "Final-Recipient: rfc822; final_recipient\r\n" + - "Final-Recipient: rfc822; final_recipient\r\n" + - "Disposition: automatic-action/MDN-sent-automatically;processed\r\n"; - MDNReportParser testee = new MDNReportParser(); - assertThat(testee.parse(duplicated)).isEmpty(); - } - - @Test - public void reportingUserAgentShouldParseWithoutProduct() { - String minimal = "Reporting-UA: UA_name"; - Parser parser = Parboiled.createParser(MDNReportParser.Parser.class); - ParsingResult<Object> result = new ReportingParseRunner<>(parser.reportingUaField()).run(minimal); - assertThat(result.matched).isTrue(); - assertThat(result.resultValue).isInstanceOf(ReportingUserAgent.class); - assertThat((ReportingUserAgent)result.resultValue).isEqualTo(ReportingUserAgent.builder().userAgentName("UA_name").build()); - } - - @Test - public void reportingUserAgentShouldParseWithProduct() { - String minimal = "Reporting-UA: UA_name; UA_product"; - Parser parser = Parboiled.createParser(MDNReportParser.Parser.class); - ParsingResult<Object> result = new ReportingParseRunner<>(parser.reportingUaField()).run(minimal); - assertThat(result.matched).isTrue(); - assertThat(result.resultValue).isInstanceOf(ReportingUserAgent.class); - assertThat((ReportingUserAgent)result.resultValue).isEqualTo(ReportingUserAgent.builder().userAgentName("UA_name").userAgentProduct("UA_product").build()); - } - - @Test - public void mdnGatewayFieldShouldParse() { - String gateway = "MDN-Gateway: smtp; apache.org"; - Parser parser = Parboiled.createParser(MDNReportParser.Parser.class); - ParsingResult<Object> result = new ReportingParseRunner<>(parser.mdnGatewayField()).run(gateway); - assertThat(result.matched).isTrue(); - assertThat(result.resultValue).isInstanceOf(Gateway.class); - assertThat((Gateway)result.resultValue).isEqualTo(Gateway.builder().nameType(new AddressType("smtp")).name(Text.fromRawText("apache.org")).build()); - } - - @Test - public void originalRecipientFieldShouldParse() { - String originalRecipient = "Original-Recipient: rfc822; originalRecipient"; - Parser parser = Parboiled.createParser(MDNReportParser.Parser.class); - ParsingResult<Object> result = new ReportingParseRunner<>(parser.originalRecipientField()).run(originalRecipient); - assertThat(result.matched).isTrue(); - assertThat(result.resultValue).isInstanceOf(OriginalRecipient.class); - assertThat((OriginalRecipient)result.resultValue).isEqualTo(OriginalRecipient.builder().addressType(new AddressType("rfc822")).originalRecipient(Text.fromRawText("originalRecipient")).build()); - } - - @Test - public void finalRecipientFieldShouldParse() { - String finalRecipient = "Final-Recipient: rfc822; final_recipient"; - Parser parser = Parboiled.createParser(MDNReportParser.Parser.class); - ParsingResult<Object> result = new ReportingParseRunner<>(parser.finalRecipientField()).run(finalRecipient); - assertThat(result.matched).isTrue(); - assertThat(result.resultValue).isInstanceOf(FinalRecipient.class); - assertThat((FinalRecipient)result.resultValue).isEqualTo(FinalRecipient.builder().addressType(new AddressType("rfc822")).finalRecipient(Text.fromRawText("final_recipient")).build()); - } - - @Test - public void originalMessageIdShouldParse() { - String originalMessageId = "Original-Message-ID: <[email protected]>"; - Parser parser = Parboiled.createParser(MDNReportParser.Parser.class); - ParsingResult<Object> result = new ReportingParseRunner<>(parser.originalMessageIdField()).run(originalMessageId); - assertThat(result.matched).isTrue(); - assertThat(result.resultValue).isInstanceOf(OriginalMessageId.class); - assertThat((OriginalMessageId)result.resultValue).isEqualTo(new OriginalMessageId("<[email protected]>")); - } - - @Test - public void dispositionFieldShouldParseWhenMinimal() { - String minimal = "Disposition: automatic-action/MDN-sent-automatically;processed"; - Disposition expected = Disposition.builder() - .actionMode(DispositionActionMode.Automatic) - .sendingMode(DispositionSendingMode.Automatic) - .type(DispositionType.Processed) - .build(); - Parser parser = Parboiled.createParser(MDNReportParser.Parser.class); - ParsingResult<Object> result = new ReportingParseRunner<>(parser.dispositionField()).run(minimal); - assertThat(result.matched).isTrue(); - assertThat(result.resultValue).isInstanceOf(Disposition.class); - assertThat((Disposition)result.resultValue).isEqualTo(expected); - } - - @Test - public void dispositionFieldShouldParseWhenMaximal() { - String maximal = "Disposition: automatic-action/MDN-sent-automatically;processed/error,failed"; - Disposition expected = Disposition.builder() - .actionMode(DispositionActionMode.Automatic) - .sendingMode(DispositionSendingMode.Automatic) - .type(DispositionType.Processed) - .addModifier(DispositionModifier.Error) - .addModifier(DispositionModifier.Failed) - .build(); - Parser parser = Parboiled.createParser(MDNReportParser.Parser.class); - ParsingResult<Object> result = new ReportingParseRunner<>(parser.dispositionField()).run(maximal); - assertThat(result.matched).isTrue(); - assertThat(result.resultValue).isInstanceOf(Disposition.class); - assertThat((Disposition)result.resultValue).isEqualTo(expected); - } - - @Test - public void dispositionFieldShouldParseWhenManualAutomaticWithDisplayedType() { - String disposition = "Disposition: manual-action/MDN-sent-automatically;processed"; - Disposition expected = Disposition.builder() - .actionMode(DispositionActionMode.Manual) - .sendingMode(DispositionSendingMode.Automatic) - .type(DispositionType.Processed) - .build(); - Parser parser = Parboiled.createParser(MDNReportParser.Parser.class); - ParsingResult<Object> result = new ReportingParseRunner<>(parser.dispositionField()).run(disposition); - assertThat(result.matched).isTrue(); - assertThat(result.resultValue).isInstanceOf(Disposition.class); - assertThat((Disposition)result.resultValue).isEqualTo(expected); - } - - @Test - public void dispositionFieldShouldParseWhenAutomaticManualWithDisplayedType() { - String disposition = "Disposition: automatic-action/MDN-sent-manually;processed"; - Disposition expected = Disposition.builder() - .actionMode(DispositionActionMode.Automatic) - .sendingMode(DispositionSendingMode.Manual) - .type(DispositionType.Processed) - .build(); - Parser parser = Parboiled.createParser(MDNReportParser.Parser.class); - ParsingResult<Object> result = new ReportingParseRunner<>(parser.dispositionField()).run(disposition); - assertThat(result.matched).isTrue(); - assertThat(result.resultValue).isInstanceOf(Disposition.class); - assertThat((Disposition)result.resultValue).isEqualTo(expected); - } - - @Test - public void dispositionFieldShouldParseWhenDeletedType() { - String disposition = "Disposition: automatic-action/MDN-sent-manually;deleted"; - Disposition expected = Disposition.builder() - .actionMode(DispositionActionMode.Automatic) - .sendingMode(DispositionSendingMode.Manual) - .type(DispositionType.Deleted) - .build(); - Parser parser = Parboiled.createParser(MDNReportParser.Parser.class); - ParsingResult<Object> result = new ReportingParseRunner<>(parser.dispositionField()).run(disposition); - assertThat(result.matched).isTrue(); - assertThat(result.resultValue).isInstanceOf(Disposition.class); - assertThat((Disposition)result.resultValue).isEqualTo(expected); - } - - @Test - public void dispositionFieldShouldParseWhenDispatchedType() { - String disposition = "Disposition: automatic-action/MDN-sent-manually;dispatched"; - Disposition expected = Disposition.builder() - .actionMode(DispositionActionMode.Automatic) - .sendingMode(DispositionSendingMode.Manual) - .type(DispositionType.Dispatched) - .build(); - Parser parser = Parboiled.createParser(MDNReportParser.Parser.class); - ParsingResult<Object> result = new ReportingParseRunner<>(parser.dispositionField()).run(disposition); - assertThat(result.matched).isTrue(); - assertThat(result.resultValue).isInstanceOf(Disposition.class); - assertThat((Disposition)result.resultValue).isEqualTo(expected); - } - - @Test - public void dispositionFieldShouldParseWhenDisplayedType() { - String disposition = "Disposition: automatic-action/MDN-sent-manually;displayed"; - Disposition expected = Disposition.builder() - .actionMode(DispositionActionMode.Automatic) - .sendingMode(DispositionSendingMode.Manual) - .type(DispositionType.Displayed) - .build(); - Parser parser = Parboiled.createParser(MDNReportParser.Parser.class); - ParsingResult<Object> result = new ReportingParseRunner<>(parser.dispositionField()).run(disposition); - assertThat(result.matched).isTrue(); - assertThat(result.resultValue).isInstanceOf(Disposition.class); - assertThat((Disposition)result.resultValue).isEqualTo(expected); - } - - @Test - public void errorFieldShouldParse() { - String error = "Error: Message1"; - Parser parser = Parboiled.createParser(MDNReportParser.Parser.class); - ParsingResult<Object> result = new ReportingParseRunner<>(parser.errorField()).run(error); - assertThat(result.matched).isTrue(); - assertThat(result.resultValue).isInstanceOf(Error.class); - assertThat((Error)result.resultValue).isEqualTo(new Error(Text.fromRawText("Message1"))); - } - - @Test - public void extensionFieldShouldParse() { - String extension = "X-OPENPAAS-IP: 177.177.177.77"; - Parser parser = Parboiled.createParser(MDNReportParser.Parser.class); - ParsingResult<Object> result = new ReportingParseRunner<>(parser.extentionField()).run(extension); - assertThat(result.matched).isTrue(); - assertThat(result.resultValue).isInstanceOf(ExtensionField.class); - assertThat((ExtensionField)result.resultValue).isEqualTo(ExtensionField.builder().fieldName("X-OPENPAAS-IP").rawValue(" 177.177.177.77").build()); - } -} diff --git a/mdn/src/test/scala/org/apache/james/mdn/MDNReportParserTest.scala b/mdn/src/test/scala/org/apache/james/mdn/MDNReportParserTest.scala new file mode 100644 index 0000000..9eeda58 --- /dev/null +++ b/mdn/src/test/scala/org/apache/james/mdn/MDNReportParserTest.scala @@ -0,0 +1,287 @@ +/**************************************************************** + * 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.mdn + +import org.apache.james.mdn.`type`.DispositionType +import org.apache.james.mdn.action.mode.DispositionActionMode +import org.apache.james.mdn.fields.{AddressType, Disposition, Error, ExtensionField, FinalRecipient, Gateway, OriginalMessageId, OriginalRecipient, ReportingUserAgent, Text} +import org.apache.james.mdn.modifier.DispositionModifier +import org.apache.james.mdn.sending.mode.DispositionSendingMode +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +class MDNReportParserTest { + + @Test + def parseShouldReturnEmptyWhenMissingFinalRecipient(): Unit = { + val missing = "Disposition: automatic-action/MDN-sent-automatically;processed\r\n" + val actual = MDNReportParser.parse(missing).toOption + assertThat(actual.isEmpty) + } + + @Test + def parseShouldReturnMdnReportWhenMaximalSubset(): Unit = { + val maximal = """Reporting-UA: UA_name; UA_product + |MDN-Gateway: smtp; apache.org + |Original-Recipient: rfc822; originalRecipient + |Final-Recipient: rfc822; final_recipient + |Original-Message-ID: <[email protected]> + |Disposition: automatic-action/MDN-sent-automatically;processed/error,failed + |Error: Message1 + |Error: Message2 + |X-OPENPAAS-IP: 177.177.177.77 + |X-OPENPAAS-PORT: 8000 + |""".replaceAllLiterally(System.lineSeparator(), "\r\n") + .stripMargin + val expected = Some(MDNReport.builder + .reportingUserAgentField(ReportingUserAgent.builder + .userAgentName("UA_name") + .userAgentProduct("UA_product") + .build) + .gatewayField(Gateway.builder + .nameType(new AddressType("smtp")) + .name(Text.fromRawText("apache.org")) + .build) + .originalRecipientField("originalRecipient") + .finalRecipientField("final_recipient") + .originalMessageIdField("<[email protected]>") + .dispositionField(Disposition.builder + .actionMode(DispositionActionMode.Automatic) + .sendingMode(DispositionSendingMode.Automatic) + .`type`(DispositionType.Processed) + .addModifier(DispositionModifier.Error) + .addModifier(DispositionModifier.Failed) + .build) + .addErrorField("Message1") + .addErrorField("Message2") + .withExtensionField(ExtensionField.builder + .fieldName("X-OPENPAAS-IP") + .rawValue(" 177.177.177.77") + .build) + .withExtensionField(ExtensionField.builder + .fieldName("X-OPENPAAS-PORT") + .rawValue(" 8000") + .build) + .build) + val actual = MDNReportParser.parse(maximal).toOption + assertThat(actual).isEqualTo(expected) + } + + @Test + def parseShouldReturnMdnReportWhenMinimalSubset(): Unit = { + val minimal = """Final-Recipient: rfc822; final_recipient + |Disposition: automatic-action/MDN-sent-automatically;processed + |""".replaceAllLiterally(System.lineSeparator(), "\r\n") + .stripMargin + val disposition = Disposition.builder + .actionMode(DispositionActionMode.Automatic) + .sendingMode(DispositionSendingMode.Automatic) + .`type`(DispositionType.Processed) + .build + val expected = Some(MDNReport.builder + .finalRecipientField("final_recipient") + .dispositionField(disposition) + .build) + val actual = MDNReportParser.parse(minimal).toOption + assertThat(actual).isEqualTo(expected) + } + + @Test + def parseShouldReturnEmptyWhenDuplicatedFields(): Unit = { + val duplicated = """Final-Recipient: rfc822; final_recipient + |Final-Recipient: rfc822; final_recipient + |Disposition: automatic-action/MDN-sent-automatically;processed + |""".replaceAllLiterally(System.lineSeparator(), "\r\n") + .stripMargin + val actual = MDNReportParser.parse(duplicated).toOption + assertThat(actual.isEmpty) + } + + @Test + def reportingUserAgentShouldParseWithoutProduct(): Unit = { + val userAgent = "Reporting-UA: UA_name" + val parser = new MDNReportParser(userAgent) + val result = parser.reportingUaField.run() + assertThat(result.isSuccess).isTrue + assertThat(result.get).isEqualTo(ReportingUserAgent.builder.userAgentName("UA_name").build) + } + + @Test + def reportingUserAgentShouldParseWithProduct(): Unit = { + val userAgent = "Reporting-UA: UA_name; UA_product" + val parser = new MDNReportParser(userAgent) + val result = parser.reportingUaField.run() + assertThat(result.isSuccess).isTrue + assertThat(result.get).isEqualTo(ReportingUserAgent.builder.userAgentName("UA_name").userAgentProduct("UA_product").build) + } + + @Test + def mdnGatewayFieldShouldParse(): Unit = { + val gateway = "MDN-Gateway: smtp; apache.org" + val parser = new MDNReportParser(gateway) + val result = parser.mdnGatewayField.run() + assertThat(result.isSuccess).isTrue + assertThat(result.get).isEqualTo(Gateway.builder.nameType(new AddressType("smtp")).name(Text.fromRawText("apache.org")).build) + } + + @Test + def originalRecipientFieldShouldParse(): Unit = { + val originalRecipient = "Original-Recipient: rfc822; originalRecipient" + val parser = new MDNReportParser(originalRecipient) + val result = parser.originalRecipientField.run() + assertThat(result.isSuccess).isTrue + assertThat(result.get).isEqualTo(OriginalRecipient.builder.addressType(new AddressType("rfc822")).originalRecipient(Text.fromRawText("originalRecipient")).build) + } + + @Test + def finalRecipientFieldShouldParse(): Unit = { + val finalRecipient = "Final-Recipient: rfc822; final_recipient" + val parser = new MDNReportParser(finalRecipient) + val result = parser.finalRecipientField.run() + assertThat(result.isSuccess).isTrue + assertThat(result.get).isEqualTo(FinalRecipient.builder.addressType(new AddressType("rfc822")).finalRecipient(Text.fromRawText("final_recipient")).build) + } + + @Test + def originalMessageIdShouldParse(): Unit = { + val originalMessageId = "Original-Message-ID: <[email protected]>" + val parser = new MDNReportParser(originalMessageId) + val result = parser.originalMessageIdField.run() + assertThat(result.isSuccess).isTrue + assertThat(result.get).isEqualTo(new OriginalMessageId("<[email protected]>")) + } + + @Test + def dispositionFieldShouldParseWhenMinimal(): Unit = { + val disposition = "Disposition: automatic-action/MDN-sent-automatically;processed" + val expected = Disposition.builder + .actionMode(DispositionActionMode.Automatic) + .sendingMode(DispositionSendingMode.Automatic) + .`type`(DispositionType.Processed) + .build + val parser = new MDNReportParser(disposition) + val result = parser.dispositionField.run() + assertThat(result.isSuccess).isTrue + assertThat(result.get).isEqualTo(expected) + } + + @Test + def dispositionFieldShouldParseWhenMaximal(): Unit = { + val disposition = "Disposition: automatic-action/MDN-sent-automatically;processed/error,failed" + val expected = Disposition.builder. + actionMode(DispositionActionMode.Automatic) + .sendingMode(DispositionSendingMode.Automatic) + .`type`(DispositionType.Processed) + .addModifier(DispositionModifier.Error) + .addModifier(DispositionModifier.Failed) + .build + val parser = new MDNReportParser(disposition) + val result = parser.dispositionField.run() + assertThat(result.isSuccess).isTrue + assertThat(result.get).isEqualTo(expected) + } + + @Test + def dispositionFieldShouldParseWhenManualAutomaticWithDisplayedType(): Unit = { + val disposition = "Disposition: manual-action/MDN-sent-automatically;processed" + val expected = Disposition.builder + .actionMode(DispositionActionMode.Manual) + .sendingMode(DispositionSendingMode.Automatic) + .`type`(DispositionType.Processed) + .build + val parser = new MDNReportParser(disposition) + val result = parser.dispositionField.run() + assertThat(result.isSuccess).isTrue + assertThat(result.get).isEqualTo(expected) + } + + @Test + def dispositionFieldShouldParseWhenAutomaticManualWithDisplayedType(): Unit = { + val disposition = "Disposition: automatic-action/MDN-sent-manually;processed" + val expected = Disposition.builder + .actionMode(DispositionActionMode.Automatic) + .sendingMode(DispositionSendingMode.Manual) + .`type`(DispositionType.Processed) + .build + val parser = new MDNReportParser(disposition) + val result = parser.dispositionField.run() + assertThat(result.isSuccess).isTrue + assertThat(result.get).isEqualTo(expected) + } + + @Test + def dispositionFieldShouldParseWhenDeletedType(): Unit = { + val disposition = "Disposition: automatic-action/MDN-sent-manually;deleted" + val expected = Disposition.builder + .actionMode(DispositionActionMode.Automatic) + .sendingMode(DispositionSendingMode.Manual) + .`type`(DispositionType.Deleted) + .build + val parser = new MDNReportParser(disposition) + val result = parser.dispositionField.run() + assertThat(result.isSuccess).isTrue + assertThat(result.get).isEqualTo(expected) + } + + @Test + def dispositionFieldShouldParseWhenDispatchedType(): Unit = { + val disposition = "Disposition: automatic-action/MDN-sent-manually;dispatched" + val expected = Disposition.builder + .actionMode(DispositionActionMode.Automatic) + .sendingMode(DispositionSendingMode.Manual) + .`type`(DispositionType.Dispatched) + .build + val parser = new MDNReportParser(disposition) + val result = parser.dispositionField.run() + assertThat(result.isSuccess).isTrue + assertThat(result.get).isEqualTo(expected) + } + + @Test + def dispositionFieldShouldParseWhenDisplayedType(): Unit = { + val disposition = "Disposition: automatic-action/MDN-sent-manually;displayed" + val expected = Disposition.builder + .actionMode(DispositionActionMode.Automatic) + .sendingMode(DispositionSendingMode.Manual) + .`type`(DispositionType.Displayed) + .build + val parser = new MDNReportParser(disposition) + val result = parser.dispositionField.run() + assertThat(result.isSuccess).isTrue + assertThat(result.get).isEqualTo(expected) + } + + @Test + def errorFieldShouldParse(): Unit = { + val error = "Error: Message1" + val parser = new MDNReportParser(error) + val result = parser.errorField.run() + assertThat(result.isSuccess).isTrue + assertThat(result.get).isEqualTo(new Error(Text.fromRawText("Message1"))) + } + + @Test + def extensionFieldShouldParse(): Unit = { + val extension = "X-OPENPAAS-IP: 177.177.177.77" + val parser = new MDNReportParser(extension) + val result = parser.extentionField.run() + assertThat(result.isSuccess).isTrue + assertThat(result.get).isEqualTo(ExtensionField.builder.fieldName("X-OPENPAAS-IP").rawValue(" 177.177.177.77").build) + } +} \ No newline at end of file diff --git a/server/mailet/mailetcontainer-camel/src/main/java/org/apache/james/mailetcontainer/AutomaticallySentMailDetectorImpl.java b/server/mailet/mailetcontainer-camel/src/main/java/org/apache/james/mailetcontainer/AutomaticallySentMailDetectorImpl.java index 40e1cfa..ab9f557 100644 --- a/server/mailet/mailetcontainer-camel/src/main/java/org/apache/james/mailetcontainer/AutomaticallySentMailDetectorImpl.java +++ b/server/mailet/mailetcontainer-camel/src/main/java/org/apache/james/mailetcontainer/AutomaticallySentMailDetectorImpl.java @@ -116,10 +116,9 @@ public class AutomaticallySentMailDetectorImpl implements AutomaticallySentMailD @Override public void body(BodyDescriptor bodyDescriptor, InputStream inputStream) throws MimeException, IOException { if (bodyDescriptor.getMimeType().equalsIgnoreCase("message/disposition-notification")) { - resultCollector.setResult(new MDNReportParser() - .parse(inputStream, bodyDescriptor.getCharset()) + resultCollector.setResult(MDNReportParser.parse(inputStream, bodyDescriptor.getCharset()) .map(report -> report.getDispositionField().getSendingMode() == DispositionSendingMode.Automatic) - .orElse(false)); + .getOrElse(() -> false)); } } }; diff --git a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/mailet/ExtractMDNOriginalJMAPMessageId.java b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/mailet/ExtractMDNOriginalJMAPMessageId.java index b723f16..97f4b5b 100644 --- a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/mailet/ExtractMDNOriginalJMAPMessageId.java +++ b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/mailet/ExtractMDNOriginalJMAPMessageId.java @@ -21,6 +21,7 @@ package org.apache.james.jmap.mailet; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; import java.util.List; import java.util.Optional; @@ -54,6 +55,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.Iterables; import reactor.core.publisher.Flux; +import scala.util.Try; /** * This mailet handles MDN messages and define a header X-JAMES-MDN-JMAP-MESSAGE-ID referencing @@ -118,8 +120,14 @@ public class ExtractMDNOriginalJMAPMessageId extends GenericMailet { private Optional<MDNReport> parseReport(Entity report) { LOGGER.debug("Parsing report"); - try { - return new MDNReportParser().parse(((SingleBody)report.getBody()).getInputStream(), report.getCharset()); + try (InputStream inputStream = ((SingleBody) report.getBody()).getInputStream()) { + Try<MDNReport> result = MDNReportParser.parse(inputStream, report.getCharset()); + if (result.isSuccess()) { + return Optional.of(result.get()); + } else { + LOGGER.error("unable to parse MESSAGE_DISPOSITION_NOTIFICATION part", result.failed().get()); + return Optional.empty(); + } } catch (IOException e) { LOGGER.error("unable to parse MESSAGE_DISPOSITION_NOTIFICATION part", e); return Optional.empty(); --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
