/**************************************************************** * 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.mime4j.io; import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; import java.util.List; import java.util.ArrayList; import java.util.Collections; import java.util.regex.Pattern; import java.util.regex.Matcher; /** * InputStream used by the parser to wrap the original user * supplied stream. This stream keeps track of the current line number. */ public class LineNumberInputStream extends FilterInputStream implements LineNumberSource { /** * Creates a new LineNumberInputStream. * * @param is the stream to read from. */ public LineNumberInputStream(InputStream is) { super(is); currentEntity = new Entity(); currentEntity.startLine = 1; rootEntity = currentEntity; } @Override public int read() throws IOException { int b = in.read(); if(b >= 0) { process(b); } return b; } @Override public int read(byte[] b, int off, int len) throws IOException { int n = in.read(b, off, len); if(n >= 0) { for (int i = off; i < off + n; i++) { process(b[i]); } } return n; } //========================================================================= private static final int CR = 13; private static final int LF = 10; private int lineNumber = 1; private int lineOffset = 0; private int offset = 0; private boolean pendingCR = false; private boolean insideBody = false; private StringBuilder lineBuilder = new StringBuilder(4096); private StringBuilder fieldBuilder = new StringBuilder(8192); private Entity currentEntity; private final Entity rootEntity; public Entity getRootEntity() { return rootEntity; } public int getLineNumber() { return lineNumber; } private void process(int b) { if(pendingCR) { if(b == LF) { ++offset; processLine(); } else { processLine(); process(b); } } else { ++offset; if(b == CR) { pendingCR = true; } else if(b == LF) { processLine(); } else { lineBuilder.append((char)b); } } } private void processLine() { if(insideBody) { processBodyLine(); } else { processHeaderLine(); } pendingCR = false; lineBuilder.setLength(0); lineOffset = offset; ++lineNumber; } private void processHeaderLine() { String line = lineBuilder.toString(); if(line.trim().isEmpty()) { //reach the separator line between header and body processField(); currentEntity.separatorLine = lineNumber; currentEntity.bodyOffset = offset; if(currentEntity.isMessageMimeType()) { if(currentEntity.isBase64Encoded()) { insideBody = true; } currentEntity = currentEntity.getEmbeddedMessage(); currentEntity.startLine = lineNumber + 1; } else { insideBody = true; } } else if(Character.isWhitespace(line.charAt(0))) { fieldBuilder.append(line); } else { processField(); fieldBuilder.append(line); } } private static final Pattern base64EncodedPattern = Pattern.compile("^content-transfer-encoding(\\s*):(\\s*)(base64)", Pattern.CASE_INSENSITIVE); private static final Pattern compositeTypePattern = Pattern.compile("^content-type(\\s*):(\\s*)((message)|(multipart))/([\\w-]+)", Pattern.CASE_INSENSITIVE); private static final Pattern boundaryPattern = Pattern.compile(";(\\s*)boundary(\\s*)=(\\s*)([^\"\\s;]+|\"[^\"\\\\\\r\\n]*(?:\\\\.[^\"\\\\\\r\\n]*)*\")", Pattern.CASE_INSENSITIVE); private void processField() { if(fieldBuilder.length() == 0) { return; } Matcher base64Matcher = base64EncodedPattern.matcher(fieldBuilder); if(base64Matcher.lookingAt()) { currentEntity.base64Encoded = true; } Matcher compositeMatcher = compositeTypePattern.matcher(fieldBuilder); if(compositeMatcher.lookingAt()) { if(compositeMatcher.group(4) != null) { currentEntity.messageMimeType = true; } else { currentEntity.multipartMimeType = true; Matcher boundaryMatcher = boundaryPattern.matcher(fieldBuilder.subSequence(compositeMatcher.end(6), fieldBuilder.length())); if(boundaryMatcher.lookingAt()) { String boundary = boundaryMatcher.group(4); if(boundary.charAt(0) == '\"') { boundary = boundary.substring(1, boundary.length() - 1); } currentEntity.boundary = boundary; } else { throw new RuntimeException("boundary parameter not found"); } } } fieldBuilder.setLength(0); } private void processBodyLine() { if((lineBuilder.length() < 2) || (lineBuilder.charAt(0) != '-') || (lineBuilder.charAt(1) != '-')) { return; } Entity ancestorEntity; if(currentEntity.isMultipartMimeType() && (currentEntity.parts == null)) { ancestorEntity = currentEntity; } else { ancestorEntity = currentEntity.parent; while(ancestorEntity != null) { if(ancestorEntity.isMultipartMimeType()) { break; } ancestorEntity = ancestorEntity.parent; } } if((ancestorEntity == null) || ((ancestorEntity.boundary.length() + 2) > lineBuilder.length())) { return; } for(int i = 0, j = 2; i < ancestorEntity.boundary.length(); i++, j++) { if(ancestorEntity.boundary.charAt(i) != lineBuilder.charAt(j)) { return; } } int k = 0; for(int i = 0, j = ancestorEntity.boundary.length() + 2; j < lineBuilder.length(); i++, j++) { if((i == 0) && (lineBuilder.charAt(j) == '-')) { k = 1; continue; } else if((i == 1) && (k == 1)) { if(lineBuilder.charAt(j) == '-') { k = 2; continue; } return; } else if(Character.isWhitespace(lineBuilder.charAt(j))){ continue; } return; } if(k == 1) { return; } //it's a boundary line ! if(ancestorEntity.parts != null) { do { currentEntity.endLine = lineNumber - 1; currentEntity.bodyLength = lineOffset - currentEntity.bodyOffset; currentEntity = currentEntity.parent; } while(currentEntity != ancestorEntity); } if(k == 0) { currentEntity = currentEntity.getLocalNextPart(); currentEntity.startLine = lineNumber + 1; insideBody = false; } } public void endOfStream() { if(lineBuilder.length() > 0) { if(insideBody) { processBodyLine(); } else { processHeaderLine(); } } for(;;) { currentEntity.endLine = lineNumber - 1; currentEntity.bodyLength = lineOffset - currentEntity.bodyOffset; if(currentEntity.parent == null) { break; } currentEntity = currentEntity.parent; } } public class Entity { private int startLine = -1; private int separatorLine = -1; private int endLine = -1; private int bodyOffset = -1; private int bodyLength = -1; public int getStartLine() { return startLine; } public int getSeparatorLine() { return separatorLine; } public int getEndLine() { return endLine; } public int getBodyOffset() { return bodyOffset; } public int getBodyLength() { return bodyLength; } //----------------------------- private boolean base64Encoded; public boolean isBase64Encoded() { return base64Encoded; } //----------------------------- private Entity parent; public Entity getParent() { return parent; } //----------------------------- private boolean multipartMimeType; private String boundary; private List parts; private int nextPart = 0; private int localNextPart = 0; public boolean isMultipartMimeType() { return multipartMimeType; } public String getBoundary() { return boundary; } public synchronized Entity getNextPart() { if(parts == null) { parts = new ArrayList(); multipartMimeType = true; } while(parts.size() <= nextPart) { Entity bodyPart = new Entity(); bodyPart.parent = this; parts.add(bodyPart); } return parts.get(nextPart++); } private synchronized Entity getLocalNextPart() { if(parts == null) { parts = new ArrayList(); multipartMimeType = true; } while(parts.size() <= localNextPart) { Entity bodyPart = new Entity(); bodyPart.parent = this; parts.add(bodyPart); } return parts.get(localNextPart++); } //----------------------------- private boolean messageMimeType; private Entity embeddedMessage; public boolean isMessageMimeType() { return messageMimeType; } public synchronized Entity getEmbeddedMessage() { if(embeddedMessage == null) { embeddedMessage = new Entity(); embeddedMessage.parent = this; messageMimeType = true; } return embeddedMessage; } //----------------------------- private void dump(String label, String indent, StringBuilder sb) { sb.append(indent).append(label); if(this == currentEntity) { sb.append(" #{").append(hashCode()).append("}#"); } else { sb.append(" {").append(hashCode()).append("}"); } sb.append(" (startLine:").append(startLine) .append(", separatorLine:").append(separatorLine) .append(", endLine:").append(endLine) .append(", bodyOffset:").append(bodyOffset) .append(", bodyLength:").append(bodyLength); if(boundary != null) { sb.append(", boundary:").append(boundary); } sb.append(")\n"); if(isMessageMimeType()) { if(embeddedMessage == null) { sb.append(indent + " Embedded: null"); } else { embeddedMessage.dump("Embedded", indent + " ", sb); } } else if(isMultipartMimeType()) { if(parts == null) { sb.append(indent + " Parts: null"); } else { for(int i = 0; i < parts.size(); i++) { parts.get(i).dump("Part[" + (i+1) + "]", indent + " ", sb); } } } } public String dump(String label) { StringBuilder sb = new StringBuilder(); dump(label, "", sb); return sb.toString(); } } }