You are viewing a plain text version of this content. The canonical link for it is here.
Posted to server-dev@james.apache.org by rc...@apache.org on 2020/06/16 09:46:05 UTC
[james-project] 02/03: JAMES-3176 Rewritte MDN parsing with
Parboiled scala
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 <ma...@apache.org>
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: <or...@message.id>\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("<or...@message.id>")
- .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: <or...@message.id>";
- 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("<or...@message.id>"));
- }
-
- @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: <or...@message.id>
+ |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("<or...@message.id>")
+ .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: <or...@message.id>"
+ val parser = new MDNReportParser(originalMessageId)
+ val result = parser.originalMessageIdField.run()
+ assertThat(result.isSuccess).isTrue
+ assertThat(result.get).isEqualTo(new OriginalMessageId("<or...@message.id>"))
+ }
+
+ @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: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org