/****************************************************************
* 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();
}
}
}