| /**************************************************************** |
| * 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(1.toChar to 8.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(0.toChar) | obsNoWsCtl | 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 |
| } |
| |
| //obs-qtext = obs-NO-WS-CTL |
| 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) } |
| |
| } |