blob: ea50183a63465403b39298e235985f9ffe5144b5 [file] [log] [blame]
/****************************************************************
* 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) }
}