/**************************************************************** * 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.stream; import java.io.IOException; import org.apache.james.mime4j.MimeException; import org.apache.james.mime4j.codec.DecodeMonitor; import org.apache.james.mime4j.io.LineReaderInputStream; import org.apache.james.mime4j.io.MaxHeaderLengthLimitException; 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 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 RawField field; private boolean endOfHeader; private int headerCount; protected final DecodeMonitor monitor; /** * 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( MutableBodyDescriptor body, int startState, int endState, MimeEntityConfig config, DecodeMonitor monitor) { this.state = startState; this.startState = startState; this.endState = endState; this.config = config; this.body = body; this.linebuf = new ByteArrayBuffer(64); this.lineCount = 0; this.endOfHeader = false; this.headerCount = 0; this.monitor = monitor; } public int getState() { return state; } /** * 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 maxHeaderLen = config.getMaxHeaderLen(); LineReaderInputStream instream = getDataStream(); ByteArrayBuffer fieldbuf = new ByteArrayBuffer(64); try { for (;;) { // If there's still data stuck in the line buffer // copy it to the field buffer int len = linebuf.length(); if (maxHeaderLen > 0 && fieldbuf.length() + len >= maxHeaderLen) { throw new MaxHeaderLengthLimitException("Maximum header 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; } } } } catch (MaxLineLimitException e) { throw new MimeException(e); } return fieldbuf; } protected boolean parseField() throws MimeException, IOException { int maxHeaderCount = config.getMaxHeaderCount(); // the loop is here to transparently skip invalid headers for (;;) { if (endOfHeader) { return false; } if (maxHeaderCount > 0 && headerCount >= maxHeaderCount) { throw new MaxHeaderLimitException("Maximum header limit exceeded"); } ByteArrayBuffer fieldbuf = fillFieldBuffer(); headerCount++; // Strip away line delimiter int origLen = fieldbuf.length(); 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); try { field = new RawField(fieldbuf); if (field.isObsoleteSyntax()) { monitor(Event.OBSOLETE_HEADER); } body.addField(field); return true; } catch (MimeException e) { monitor(Event.INVALID_HEADER); if (config.isMalformedHeaderStartsBody()) { fieldbuf.setLength(origLen); LineReaderInputStream instream = getDataStream(); if (!instream.unread(fieldbuf)) throw new MimeParseEventException(Event.INVALID_HEADER); return false; } } } } /** * <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 RawField 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 (monitor.isListening()) { String message = message(event); if (monitor.warn(message, "ignoring")) { throw new MimeParseEventException(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; } @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; } }