package org.subethamail.smtp.internal.io;

/***********************************************************************
 * Copyright (c) 2000-2006 The Apache Software Foundation.             *
 * All rights reserved.                                                *
 * ------------------------------------------------------------------- *
 * Licensed 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.                      *
 ***********************************************************************/

import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
/**
 * A Reader for use with SMTP or other protocols in which lines
 * must end with CRLF.  Extends Reader and overrides its
 * readLine() method.  The Reader readLine() method cannot
 * serve for SMTP because it ends lines with either CR or LF alone.
 *
 * JSS: The readline() method of this class has been 'enchanced' from
 * the Apache JAMES version to throw an IOException if the line is
 * greater than or equal to MAX_LINE_LENGTH (998) which is defined
 * in <a href="http://rfc.net/rfc2822.html#s2.1.1.">RFC 2822</a>.
 */
public final class CRLFTerminatedReader extends Reader
{
	private static final int MAX_LINE_LENGTH = 998;

	@SuppressWarnings("serial")
	public static final class TerminationException extends IOException
	{
		private int where;

		public TerminationException(int where)
		{
			super();
			this.where = where;
		}

		public TerminationException(String s, int where)
		{
			super(s);
			this.where = where;
		}

		public int position()
		{
			return this.where;
		}
	}

	@SuppressWarnings("serial")
	public static final class MaxLineLengthException extends IOException
	{
		public MaxLineLengthException()
		{
			super();
		}

		public MaxLineLengthException(String s)
		{
			super(s);
		}
	}

	private final InputStream in;

	/**
	 * Constructs this CRLFTerminatedReader.
	 *
	 * @param in
	 *            an InputStream
	 * @throws UnsupportedEncodingException
	 *             if the named charset is not supported
	 */

	public CRLFTerminatedReader(InputStream in)
	{
		this.in = in;
	}

	private final StringBuffer lineBuffer = new StringBuffer();
	private final static int
			EOF = -1,
			CR  = 13,
			LF  = 10;

	private int tainted = -1;

	/**
	 * Read a line of text which is terminated by CRLF.  The concluding
	 * CRLF characters are not returned with the String, but if either CR
	 * or LF appears in the text in any other sequence it is returned
	 * in the String like any other character.  Some characters at the
	 * end of the stream may be lost if they are in a "line" not
	 * terminated by CRLF.
	 *
	 * @return either a String containing the contents of a
	 * line which must end with CRLF, or null if the end of the
	 * stream has been reached, possibly discarding some characters
	 * in a line not terminated with CRLF.
	 * @throws IOException if an I/O error occurs.
	 */
	public String readLine() throws IOException
	{
		//start with the StringBuffer empty
		this.lineBuffer.delete(0, this.lineBuffer.length());

		/* This boolean tells which state we are in,
		 * depending upon whether or not we got a CR
		 * in the preceding read().
		 */
		boolean cr_just_received = false;

		while (true)
		{
			int inChar = this.read();

			if (!cr_just_received)
			{
				//the most common case, somewhere before the end of a line
				switch (inChar)
				{
					case CR:
						cr_just_received = true;
						break;
					case EOF:
						return null; // premature EOF -- discards data(?)
					case LF: //the normal ending of a line
						if (this.tainted == -1)
							this.tainted = this.lineBuffer.length();
					// intentional fall-through
					default:
						this.lineBuffer.append((char) inChar);
				}
			}
			else
			{
				// CR has been received, we may be at end of line
				switch (inChar)
				{
					case LF: // LF without a preceding CR
						if (this.tainted != -1)
						{
							int pos = this.tainted;
							this.tainted = -1;
							throw new TerminationException(
									"\"bare\" CR or LF in data stream", pos);
						}
						return this.lineBuffer.toString();
					case EOF:
						return null; // premature EOF -- discards data(?)
					case CR: //we got two (or more) CRs in a row
						if (this.tainted == -1)
							this.tainted = this.lineBuffer.length();
						this.lineBuffer.append((char) CR);
						break;
					default: //we got some other character following a CR
						if (this.tainted == -1)
							this.tainted = this.lineBuffer.length();
						this.lineBuffer.append((char) CR);
						this.lineBuffer.append((char) inChar);
						cr_just_received = false;
				}
			}
			if (this.lineBuffer.length() >= MAX_LINE_LENGTH)
			{
				throw new MaxLineLengthException("Input line length is too long!");
			}
		}
	}

	@Override
	public int read() throws IOException
	{
		return this.in.read();
	}

	@Override
	public boolean ready() throws IOException
	{
		return this.in.available() > 0;
	}

	@Override
	public int read(char[] cbuf, int off, int len) throws IOException
	{
		byte[] temp = new byte[len];
		int result = this.in.read(temp, 0, len);
		for (int i = 0; i < result; i++)
			cbuf[i] = (char) temp[i];
		return result;
	}

	@Override
	public void close() throws IOException
	{
		this.in.close();
	}
}
