SMTP: correct unescaping of posted messages containing lines with single dot.
authorFrantišek Kučera <franta-hg@frantovo.cz>
Sat Nov 05 00:06:09 2011 +0100 (2011-11-05)
changeset 115e5bfc969d41f
parent 114 311502c83ff8
child 116 4ddc1020a154
SMTP: correct unescaping of posted messages containing lines with single dot.
src/org/sonews/daemon/command/PostCommand.java
src/org/sonews/util/io/SMTPInputStream.java
     1.1 --- a/src/org/sonews/daemon/command/PostCommand.java	Sun Oct 30 22:15:49 2011 +0100
     1.2 +++ b/src/org/sonews/daemon/command/PostCommand.java	Sat Nov 05 00:06:09 2011 +0100
     1.3 @@ -37,6 +37,7 @@
     1.4  import org.sonews.storage.StorageManager;
     1.5  import org.sonews.feed.FeedManager;
     1.6  import org.sonews.util.Stats;
     1.7 +import org.sonews.util.io.SMTPInputStream;
     1.8  
     1.9  /**
    1.10   * Implementation of the POST command. This command requires multiple lines
    1.11 @@ -103,6 +104,7 @@
    1.12  				if ("".equals(line) || ".".equals(line)) {
    1.13  					// we finally met the blank line
    1.14  					// separating headers from body
    1.15 +					// WTF: "."
    1.16  
    1.17  					try {
    1.18  						// Parse the header using the InternetHeader class from JavaMail API
    1.19 @@ -124,6 +126,7 @@
    1.20  
    1.21  					state = PostState.ReadingBody;
    1.22  
    1.23 +					// WTF: do we need articles without bodies?
    1.24  					if (".".equals(line)) {
    1.25  						// Post an article without body
    1.26  						postArticle(conn, article);
    1.27 @@ -138,7 +141,7 @@
    1.28  					headers.setHeader(Headers.LINES, Integer.toString(lineCount));
    1.29  					headers.setHeader(Headers.BYTES, Long.toString(bodySize));
    1.30  
    1.31 -					byte[] body = bufBody.toByteArray();
    1.32 +					byte[] body = unescapeDots(bufBody.toByteArray());
    1.33  					if (body.length >= 2) {
    1.34  						// Remove trailing CRLF
    1.35  						body = Arrays.copyOf(body, body.length - 2);
    1.36 @@ -213,7 +216,7 @@
    1.37  		if (conn.getUser() != null && conn.getUser().isAuthenticated()) {
    1.38  			article.setAuthenticatedUser(conn.getUser().getUserName());
    1.39  		}
    1.40 -		
    1.41 +
    1.42  		if (article.getHeader(Headers.CONTROL)[0].length() > 0) {
    1.43  			controlMessage(conn, article);
    1.44  		} else if (article.getHeader(Headers.SUPERSEDES)[0].length() > 0) {
    1.45 @@ -273,4 +276,27 @@
    1.46  			}
    1.47  		}
    1.48  	}
    1.49 +
    1.50 +	/**
    1.51 +	 * TODO: rework, integrate into NNTPConnection
    1.52 +	 * 
    1.53 +	 * @param body message body with doubled dots
    1.54 +	 * @return message body with unescaped dots (.. → .)
    1.55 +	 */
    1.56 +	private static byte[] unescapeDots(byte[] body) throws IOException {
    1.57 +		byte[] result = new byte[body.length];
    1.58 +		int resultLength = 0;
    1.59 +
    1.60 +		ByteArrayInputStream escapedInput = new ByteArrayInputStream(body);
    1.61 +		SMTPInputStream unescapedInput = new SMTPInputStream(escapedInput);
    1.62 +
    1.63 +		int ch = unescapedInput.read();
    1.64 +		while (ch >= 0) {
    1.65 +			result[resultLength] = (byte) ch;
    1.66 +			resultLength++;
    1.67 +			ch = unescapedInput.read();
    1.68 +		}
    1.69 +
    1.70 +		return Arrays.copyOfRange(result, 0, resultLength);
    1.71 +	}
    1.72  }
     2.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     2.2 +++ b/src/org/sonews/util/io/SMTPInputStream.java	Sat Nov 05 00:06:09 2011 +0100
     2.3 @@ -0,0 +1,110 @@
     2.4 +/*
     2.5 + *   SONEWS News Server
     2.6 + *   see AUTHORS for the list of contributors
     2.7 + *
     2.8 + *   This program is free software: you can redistribute it and/or modify
     2.9 + *   it under the terms of the GNU General Public License as published by
    2.10 + *   the Free Software Foundation, either version 3 of the License, or
    2.11 + *   (at your option) any later version.
    2.12 + *
    2.13 + *   This program is distributed in the hope that it will be useful,
    2.14 + *   but WITHOUT ANY WARRANTY; without even the implied warranty of
    2.15 + *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    2.16 + *   GNU General Public License for more details.
    2.17 + *
    2.18 + *   You should have received a copy of the GNU General Public License
    2.19 + *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
    2.20 + */
    2.21 +package org.sonews.util.io;
    2.22 +
    2.23 +import java.io.FilterInputStream;
    2.24 +import java.io.IOException;
    2.25 +import java.io.InputStream;
    2.26 +
    2.27 +/**
    2.28 + * Filter input stream for reading from SMTP (or NNTP or similar) socket
    2.29 + * where lines containing single dot have special meaning – end of message.
    2.30 + * 
    2.31 + * @author František Kučera (frantovo.cz)
    2.32 + */
    2.33 +public class SMTPInputStream extends FilterInputStream {
    2.34 +
    2.35 +	public static final int CR = 0x0d;
    2.36 +	public static final int LF = 0x0a;
    2.37 +	public static final int DOT = 0x2e;
    2.38 +	protected int last;
    2.39 +
    2.40 +	public SMTPInputStream(InputStream in) {
    2.41 +		super(in);
    2.42 +	}
    2.43 +
    2.44 +	/**
    2.45 +	 * @return one byte as expected 
    2.46 +	 * or -2 if there was line with single dot (which means end of message)
    2.47 +	 * @throws IOException 
    2.48 +	 */
    2.49 +	@Override
    2.50 +	public int read() throws IOException {
    2.51 +		// read current character
    2.52 +		int ch = super.read();
    2.53 +
    2.54 +		if (ch == DOT) {
    2.55 +			if (last == LF) {
    2.56 +				int next = super.read();
    2.57 +
    2.58 +				if (next == CR || next == LF) { // There should be CRLF, but we may accept also just LF or CR with missing LF. Or should we be more strict?
    2.59 +					// <CRLF>.<CRLF> → end of current message
    2.60 +					ch = -2;
    2.61 +				} else {
    2.62 +					// <CRLF>.… → eat one dot and return next character
    2.63 +					ch = next;
    2.64 +				}
    2.65 +			}
    2.66 +		}
    2.67 +
    2.68 +		last = ch;
    2.69 +		return ch;
    2.70 +	}
    2.71 +
    2.72 +	/**
    2.73 +	 * @param buffer
    2.74 +	 * @param offset
    2.75 +	 * @param length
    2.76 +	 * @return See {@link FilterInputStream#read(byte[], int, int)} or -2 (then see {@link #read(byte[])})
    2.77 +	 * @throws IOException 
    2.78 +	 */
    2.79 +	@Override
    2.80 +	public int read(byte[] buffer, int offset, int length) throws IOException {
    2.81 +		if (buffer == null) {
    2.82 +			throw new NullPointerException("Byte array should not be null.");
    2.83 +		} else if ((offset < 0) || (offset > buffer.length) || (length < 0) || ((offset + length) > buffer.length) || ((offset + length) < 0)) {
    2.84 +			throw new IndexOutOfBoundsException("Invalid offset or length.");
    2.85 +		} else if (length == 0) {
    2.86 +			return 0;
    2.87 +		}
    2.88 +
    2.89 +		int ch = read();
    2.90 +
    2.91 +		if (ch == -1 || ch == -2) {
    2.92 +			return ch;
    2.93 +		}
    2.94 +
    2.95 +		buffer[offset] = (byte) ch;
    2.96 +
    2.97 +		int readCounter = 1;
    2.98 +
    2.99 +		for (; readCounter < length; readCounter++) {
   2.100 +			ch = read();
   2.101 +
   2.102 +			if (ch == -1 || ch == -2) {
   2.103 +				break;
   2.104 +			}
   2.105 +
   2.106 +			if (buffer != null) {
   2.107 +				buffer[offset + readCounter] = (byte) ch;
   2.108 +			}
   2.109 +		}
   2.110 +
   2.111 +		return readCounter;
   2.112 +	}
   2.113 +}