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