/**************************************************************** * 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.parser; import java.io.IOException; import java.util.BitSet; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.james.mime4j.MimeException; import org.apache.james.mime4j.descriptor.BodyDescriptor; import org.apache.james.mime4j.descriptor.DefaultBodyDescriptor; import org.apache.james.mime4j.descriptor.MaximalBodyDescriptor; import org.apache.james.mime4j.descriptor.MutableBodyDescriptor; import org.apache.james.mime4j.io.LineReaderInputStream; import org.apache.james.mime4j.io.MaxHeaderLimitException; import org.apache.james.mime4j.io.MaxLineLimitException; import org.apache.james.mime4j.util.ByteArrayBuffer; import org.apache.james.mime4j.util.CharsetUtil; /** * Abstract MIME entity. */ public abstract class AbstractEntity implements EntityStateMachine { protected final Log log; protected final BodyDescriptor parent; protected final int startState; protected final int endState; protected final MimeEntityConfig config; protected final MutableBodyDescriptor body; protected int state; private final ByteArrayBuffer linebuf; private int lineCount; private Field field; private boolean endOfHeader; private int headerCount; private static final BitSet fieldChars = new BitSet(); static { for (int i = 0x21; i <= 0x39; i++) { fieldChars.set(i); } for (int i = 0x3b; i <= 0x7e; i++) { fieldChars.set(i); } } /** * Internal state, not exposed. */ private static final int T_IN_BODYPART = -2; /** * Internal state, not exposed. */ private static final int T_IN_MESSAGE = -3; AbstractEntity( BodyDescriptor parent, int startState, int endState, MimeEntityConfig config) { this.log = LogFactory.getLog(getClass()); this.parent = parent; this.state = startState; this.startState = startState; this.endState = endState; this.config = config; this.body = newBodyDescriptor(parent); this.linebuf = new ByteArrayBuffer(64); this.lineCount = 0; this.endOfHeader = false; this.headerCount = 0; } public int getState() { return state; } /** * Creates a new instance of {@link BodyDescriptor}. Subclasses may override * this in order to create body descriptors, that provide more specific * information. */ protected MutableBodyDescriptor newBodyDescriptor(BodyDescriptor pParent) { final MutableBodyDescriptor result; if (config.isMaximalBodyDescriptor()) { result = new MaximalBodyDescriptor(pParent); } else { result = new DefaultBodyDescriptor(pParent); } return result; } /** * Returns the current line number or <code>-1</code> if line number * information is not available. */ protected abstract int getLineNumber(); protected abstract LineReaderInputStream getDataStream(); private ByteArrayBuffer fillFieldBuffer() throws IOException, MimeException { if (endOfHeader) throw new IllegalStateException(); int maxLineLen = config.getMaxLineLen(); LineReaderInputStream instream = getDataStream(); ByteArrayBuffer fieldbuf = new ByteArrayBuffer(64); for (;;) { // If there's still data stuck in the line buffer // copy it to the field buffer int len = linebuf.length(); if (maxLineLen > 0 && fieldbuf.length() + len >= maxLineLen) { throw new MaxLineLimitException("Maximum line length limit exceeded"); } if (len > 0) { fieldbuf.append(linebuf.buffer(), 0, len); } linebuf.clear(); if (instream.readLine(linebuf) == -1) { monitor(Event.HEADERS_PREMATURE_END); endOfHeader = true; break; } len = linebuf.length(); if (len > 0 && linebuf.byteAt(len - 1) == '\n') { len--; } if (len > 0 && linebuf.byteAt(len - 1) == '\r') { len--; } if (len == 0) { // empty line detected endOfHeader = true; break; } lineCount++; if (lineCount > 1) { int ch = linebuf.byteAt(0); if (ch != CharsetUtil.SP && ch != CharsetUtil.HT) { // new header detected break; } } } return fieldbuf; } protected boolean parseField() throws MimeException, IOException { int maxHeaderLimit = config.getMaxHeaderCount(); for (;;) { if (endOfHeader) { return false; } if (headerCount >= maxHeaderLimit) { throw new MaxHeaderLimitException("Maximum header limit exceeded"); } ByteArrayBuffer fieldbuf = fillFieldBuffer(); headerCount++; // Strip away line delimiter int len = fieldbuf.length(); if (len > 0 && fieldbuf.byteAt(len - 1) == '\n') { len--; } if (len > 0 && fieldbuf.byteAt(len - 1) == '\r') { len--; } fieldbuf.setLength(len); boolean valid = true; int pos = fieldbuf.indexOf((byte) ':'); if (pos <= 0) { monitor(Event.INALID_HEADER); valid = false; } else { for (int i = 0; i < pos; i++) { if (!fieldChars.get(fieldbuf.byteAt(i) & 0xff)) { monitor(Event.INALID_HEADER); valid = false; break; } } } if (valid) { field = new RawField(fieldbuf, pos); body.addField(field); return true; } } } /** * <p>Gets a descriptor for the current entity. * This method is valid if {@link #getState()} returns:</p> * <ul> * <li>{@link EntityStates#T_BODY}</li> * <li>{@link EntityStates#T_START_MULTIPART}</li> * <li>{@link EntityStates#T_EPILOGUE}</li> * <li>{@link EntityStates#T_PREAMBLE}</li> * </ul> * @return <code>BodyDescriptor</code>, not nulls */ public BodyDescriptor getBodyDescriptor() { switch (getState()) { case EntityStates.T_BODY: case EntityStates.T_START_MULTIPART: case EntityStates.T_PREAMBLE: case EntityStates.T_EPILOGUE: case EntityStates.T_END_OF_STREAM: return body; default: throw new IllegalStateException("Invalid state :" + stateToString(state)); } } /** * This method is valid, if {@link #getState()} returns {@link EntityStates#T_FIELD}. * @return String with the fields raw contents. * @throws IllegalStateException {@link #getState()} returns another * value than {@link EntityStates#T_FIELD}. */ public Field getField() { switch (getState()) { case EntityStates.T_FIELD: return field; default: throw new IllegalStateException("Invalid state :" + stateToString(state)); } } /** * Monitors the given event. * Subclasses may override to perform actions upon events. * Base implementation logs at warn. * @param event <code>Event</code>, not null * @throws MimeException subclasses may elect to throw this exception upon * invalid content * @throws IOException subclasses may elect to throw this exception */ protected void monitor(Event event) throws MimeException, IOException { if (config.isStrictParsing()) { throw new MimeParseEventException(event); } else { warn(event); } } /** * Creates an indicative message suitable for display * based on the given event and the current state of the system. * @param event <code>Event</code>, not null * @return message suitable for use as a message in an exception * or for logging */ protected String message(Event event) { final String message; if (event == null) { message = "Event is unexpectedly null."; } else { message = event.toString(); } int lineNumber = getLineNumber(); if (lineNumber <= 0) return message; else return "Line " + lineNumber + ": " + message; } /** * Logs (at warn) an indicative message based on the given event * and the current state of the system. * @param event <code>Event</code>, not null */ protected void warn(Event event) { if (log.isWarnEnabled()) { log.warn(message(event)); } } /** * Logs (at debug) an indicative message based on the given event * and the current state of the system. * @param event <code>Event</code>, not null */ protected void debug(Event event) { if (log.isDebugEnabled()) { log.debug(message(event)); } } @Override public String toString() { return getClass().getName() + " [" + stateToString(state) + "][" + body.getMimeType() + "][" + body.getBoundary() + "]"; } /** * Renders a state as a string suitable for logging. * @param state * @return rendered as string, not null */ public static final String stateToString(int state) { final String result; switch (state) { case EntityStates.T_END_OF_STREAM: result = "End of stream"; break; case EntityStates.T_START_MESSAGE: result = "Start message"; break; case EntityStates.T_END_MESSAGE: result = "End message"; break; case EntityStates.T_RAW_ENTITY: result = "Raw entity"; break; case EntityStates.T_START_HEADER: result = "Start header"; break; case EntityStates.T_FIELD: result = "Field"; break; case EntityStates.T_END_HEADER: result = "End header"; break; case EntityStates.T_START_MULTIPART: result = "Start multipart"; break; case EntityStates.T_END_MULTIPART: result = "End multipart"; break; case EntityStates.T_PREAMBLE: result = "Preamble"; break; case EntityStates.T_EPILOGUE: result = "Epilogue"; break; case EntityStates.T_START_BODYPART: result = "Start bodypart"; break; case EntityStates.T_END_BODYPART: result = "End bodypart"; break; case EntityStates.T_BODY: result = "Body"; break; case T_IN_BODYPART: result = "Bodypart"; break; case T_IN_MESSAGE: result = "In message"; break; default: result = "Unknown"; break; } return result; } }