//$Header: /cvsroot-fuse/mec-as2/39/mendelson/comm/as2/message/MDNParser.java,v 1.1 2012/04/18 14:10:30 heller Exp $ package de.mendelson.comm.as2.message; import de.mendelson.comm.as2.server.AS2Server; import de.mendelson.util.MecResourceBundle; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.util.MissingResourceException; import java.util.Properties; import java.util.ResourceBundle; import java.util.logging.Logger; import javax.mail.BodyPart; import javax.mail.MessagingException; import javax.mail.Multipart; import javax.mail.Part; import javax.mail.Session; import javax.mail.internet.MimeMessage; import javax.mail.internet.MimeMultipart; import javax.mail.internet.MimeUtility; import javax.mail.util.ByteArrayDataSource; /* * Copyright (C) mendelson-e-commerce GmbH Berlin Germany * * This software is subject to the license agreement set forth in the license. * Please read and agree to all terms before using this software. * Other product and brand names are trademarks of their respective owners. */ /** * Parses MDNs, this is NOT thread safe! * @author S.Heller * @version $Revision: 1.1 $ */ public class MDNParser { private Logger logger = Logger.getLogger(AS2Server.SERVER_LOGGER_NAME); /**Contains the details message of the MDN*/ private String mdnDetails; private Properties dispositionProperties = new Properties(); private String dispositionState; private MecResourceBundle rb; /**contains the parsed MIC is it has been transfered*/ private String mic = null; /**contains the related message for the MDN, from sync MDN this is a SHOULD value*/ private String relatedMessageId = null; public MDNParser() { //Load resourcebundle try { this.rb = (MecResourceBundle) ResourceBundle.getBundle( ResourceBundleMDNParser.class.getName()); } //load up resourcebundle catch (MissingResourceException e) { throw new RuntimeException("Oops..resource bundle " + e.getClassName() + " not found."); } } /**Checks if the pass raw data is an MDN or a message. Will return null if this is NO MDN */ public AS2MDNInfo parseMDNData(byte[] data, String contentType) throws Exception { //no content type defined? Throw an exception if (contentType == null || contentType.trim().length() == 0) { throw new Exception(this.rb.getResourceString("invalid.mdn.nocontenttype")); } //encrypted AS2 message found, MDNs are not encrypted if (contentType.startsWith("application/pkcs7-mime")) { return (null); } ByteArrayInputStream inStream = new ByteArrayInputStream(data); MimeMultipart multipart = new MimeMultipart(new ByteArrayDataSource(data, contentType)); inStream.close(); MimeMessage messagePart = new MimeMessage(Session.getInstance(System.getProperties(), null)); messagePart.setContent(multipart, multipart.getContentType()); messagePart.saveChanges(); Part reportPart = this.parsePartsForReport(messagePart); //it is NO MDN, there is no report part if (reportPart == null) { return (null); } //new inbound MDN identified AS2MDNInfo info = new AS2MDNInfo(); info.setDirection(AS2MessageInfo.DIRECTION_IN); this.computeMessageDispositionDetailsFromMDN(reportPart); this.relatedMessageId = this.dispositionProperties.getProperty("original-message-id"); info.setRelatedMessageId(this.relatedMessageId); //RFC 4130, section 7.4.3 //The "Received-content-MIC" extension field is set when the integrity of the received //message is verified. The MIC is the base64-encoded message-digest computed over the received //message with a hash function. This field is required for signed receipts but optional for unsigned receipts. // //This field will be taken anyway, if it does not exist a null will be taken this.mic = this.dispositionProperties.getProperty("received-content-mic"); info.setReceivedContentMIC(this.mic); return (info); } /**Reads the content of a body part and returns it als byte array. If a content transfer encoding is set this * is computed */ private byte[] bodypartContentToByteArrayEncoded(BodyPart body) throws Exception { //check if a content transfer encoding is set. Process it if so String contentTransferEncoding = null; String[] encodingHeader = body.getHeader("content-transfer-encoding"); if( encodingHeader != null && encodingHeader.length > 0 ){ contentTransferEncoding = encodingHeader[0]; } Object content = body.getContent(); if (content instanceof InputStream) { InputStream inStream = (InputStream) body.getContent(); ByteArrayOutputStream memOut = new ByteArrayOutputStream(); this.copyStreams(inStream, memOut); memOut.flush(); memOut.close(); inStream.close(); byte[] rawData = memOut.toByteArray(); return(this.decodeContentTransferEncoding(rawData, contentTransferEncoding)); }else if( content instanceof String){ //in the case of casting the content transfer encoding processing is performed by the API //automatically. There is no need to call decodeContentTransferEncoding here String data = (String) content; byte[] rawData = data.getBytes(); return (rawData); }else throw new Exception( "Unable to process body part content - unexpected content Object " + content.getClass().getName()); } /**Copies all data from one stream to another*/ private void copyStreams(InputStream in, OutputStream out) throws IOException { BufferedInputStream inStream = new BufferedInputStream(in); BufferedOutputStream outStream = new BufferedOutputStream(out); //copy the contents to an output stream byte[] buffer = new byte[2048]; int read = 2048; //a read of 0 must be allowed, sometimes it takes time to //extract data from the input while (read != -1) { read = inStream.read(buffer); if (read > 0) { outStream.write(buffer, 0, read); } } outStream.flush(); } /**Decodes data by its content transfer encoding and returns it*/ private byte[] decodeContentTransferEncoding(byte[] encodedData, String contentTransferEncoding) throws Exception { if( contentTransferEncoding == null ){ return( encodedData ); } ByteArrayInputStream bais = new ByteArrayInputStream(encodedData); InputStream b64is = MimeUtility.decode(bais, contentTransferEncoding); byte[] tmp = new byte[encodedData.length]; int n = b64is.read(tmp); byte[] res = new byte[n]; System.arraycopy(tmp, 0, res, 0, n); return res; } /**Returns the details of the report part as properties */ private void computeMessageDispositionDetailsFromMDN(Part reportPart) throws Exception { if (reportPart.isMimeType("multipart/*")) { Multipart multiPart = (Multipart) reportPart.getContent(); try { int count = multiPart.getCount(); for (int i = 0; i < count; i++) { BodyPart body = multiPart.getBodyPart(i); byte[] bodypartData = this.bodypartContentToByteArrayEncoded(body); if (body.getContentType().toLowerCase().startsWith("text/plain")) { this.mdnDetails = new String(bodypartData).trim(); } else if (body.getContentType().toLowerCase().startsWith("message/disposition-notification")) { InputStream inStream = new ByteArrayInputStream(bodypartData); BufferedReader reader = new BufferedReader(new InputStreamReader(inStream)); String line = ""; while (line != null) { line = reader.readLine(); if (line != null) { int index = line.indexOf(':'); if (index > 0) { String key = line.substring(0, index).toLowerCase(); String value = line.substring(index + 1).trim(); this.dispositionProperties.setProperty(key, value); if (key.equals("disposition")) { this.computeDispositionState(value); } } } } inStream.close(); } } } catch (MessagingException structureException) { throw new Exception(this.rb.getResourceString("structure.failure.mdn", structureException.getMessage())); } } } /**Parses the passed message an returns the report body type if this is an MDN */ private Part parsePartsForReport(Part part) throws Exception { if (part.getContentType().toLowerCase().startsWith("multipart/report")) { return (part); } if (part.isMimeType("multipart/*")) { Multipart multiPart = (Multipart) part.getContent(); int count = multiPart.getCount(); for (int i = 0; i < count; i++) { Part foundPart = parsePartsForReport(multiPart.getBodyPart(i)); if (foundPart != null) { return (foundPart); } } } //nothing found, no MDN return (null); } public String getMdnDetails() { return mdnDetails; } public void setMdnDetails(String mdnDetails) { this.mdnDetails = mdnDetails; } public Properties getDispositionProperties() { return dispositionProperties; } public String getDispositionState() { return dispositionState; } private void computeDispositionState(String dispositionValue) { int index = dispositionValue.indexOf(';'); if (index > 0) { this.dispositionState = dispositionValue.substring(index + 1).trim(); } } /** * Returns the MIC if it has been transferred (available after the parsing process) * @return the mic */ public String getMIC() { return mic; } /** * @return the relatedMessageId */ public String getRelatedMessageId() { return relatedMessageId; } }