# HG changeset patch
# User František Kučera <franta-hg@frantovo.cz>
# Date 1320447969 -3600
# Node ID e5bfc969d41fd5e0fd11c38a560d9da86dbd83eb
# Parent  311502c83ff83c15a80dd7773045ba02147240cd
SMTP: correct unescaping of posted messages containing lines with single dot.

diff -r 311502c83ff8 -r e5bfc969d41f src/org/sonews/daemon/command/PostCommand.java
--- a/src/org/sonews/daemon/command/PostCommand.java	Sun Oct 30 22:15:49 2011 +0100
+++ b/src/org/sonews/daemon/command/PostCommand.java	Sat Nov 05 00:06:09 2011 +0100
@@ -37,6 +37,7 @@
 import org.sonews.storage.StorageManager;
 import org.sonews.feed.FeedManager;
 import org.sonews.util.Stats;
+import org.sonews.util.io.SMTPInputStream;
 
 /**
  * Implementation of the POST command. This command requires multiple lines
@@ -103,6 +104,7 @@
 				if ("".equals(line) || ".".equals(line)) {
 					// we finally met the blank line
 					// separating headers from body
+					// WTF: "."
 
 					try {
 						// Parse the header using the InternetHeader class from JavaMail API
@@ -124,6 +126,7 @@
 
 					state = PostState.ReadingBody;
 
+					// WTF: do we need articles without bodies?
 					if (".".equals(line)) {
 						// Post an article without body
 						postArticle(conn, article);
@@ -138,7 +141,7 @@
 					headers.setHeader(Headers.LINES, Integer.toString(lineCount));
 					headers.setHeader(Headers.BYTES, Long.toString(bodySize));
 
-					byte[] body = bufBody.toByteArray();
+					byte[] body = unescapeDots(bufBody.toByteArray());
 					if (body.length >= 2) {
 						// Remove trailing CRLF
 						body = Arrays.copyOf(body, body.length - 2);
@@ -213,7 +216,7 @@
 		if (conn.getUser() != null && conn.getUser().isAuthenticated()) {
 			article.setAuthenticatedUser(conn.getUser().getUserName());
 		}
-		
+
 		if (article.getHeader(Headers.CONTROL)[0].length() > 0) {
 			controlMessage(conn, article);
 		} else if (article.getHeader(Headers.SUPERSEDES)[0].length() > 0) {
@@ -273,4 +276,27 @@
 			}
 		}
 	}
+
+	/**
+	 * TODO: rework, integrate into NNTPConnection
+	 * 
+	 * @param body message body with doubled dots
+	 * @return message body with unescaped dots (.. → .)
+	 */
+	private static byte[] unescapeDots(byte[] body) throws IOException {
+		byte[] result = new byte[body.length];
+		int resultLength = 0;
+
+		ByteArrayInputStream escapedInput = new ByteArrayInputStream(body);
+		SMTPInputStream unescapedInput = new SMTPInputStream(escapedInput);
+
+		int ch = unescapedInput.read();
+		while (ch >= 0) {
+			result[resultLength] = (byte) ch;
+			resultLength++;
+			ch = unescapedInput.read();
+		}
+
+		return Arrays.copyOfRange(result, 0, resultLength);
+	}
 }
diff -r 311502c83ff8 -r e5bfc969d41f src/org/sonews/util/io/SMTPInputStream.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/sonews/util/io/SMTPInputStream.java	Sat Nov 05 00:06:09 2011 +0100
@@ -0,0 +1,110 @@
+/*
+ *   SONEWS News Server
+ *   see AUTHORS for the list of contributors
+ *
+ *   This program is free software: you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation, either version 3 of the License, or
+ *   (at your option) any later version.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package org.sonews.util.io;
+
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Filter input stream for reading from SMTP (or NNTP or similar) socket
+ * where lines containing single dot have special meaning – end of message.
+ * 
+ * @author František Kučera (frantovo.cz)
+ */
+public class SMTPInputStream extends FilterInputStream {
+
+	public static final int CR = 0x0d;
+	public static final int LF = 0x0a;
+	public static final int DOT = 0x2e;
+	protected int last;
+
+	public SMTPInputStream(InputStream in) {
+		super(in);
+	}
+
+	/**
+	 * @return one byte as expected 
+	 * or -2 if there was line with single dot (which means end of message)
+	 * @throws IOException 
+	 */
+	@Override
+	public int read() throws IOException {
+		// read current character
+		int ch = super.read();
+
+		if (ch == DOT) {
+			if (last == LF) {
+				int next = super.read();
+
+				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?
+					// <CRLF>.<CRLF> → end of current message
+					ch = -2;
+				} else {
+					// <CRLF>.… → eat one dot and return next character
+					ch = next;
+				}
+			}
+		}
+
+		last = ch;
+		return ch;
+	}
+
+	/**
+	 * @param buffer
+	 * @param offset
+	 * @param length
+	 * @return See {@link FilterInputStream#read(byte[], int, int)} or -2 (then see {@link #read(byte[])})
+	 * @throws IOException 
+	 */
+	@Override
+	public int read(byte[] buffer, int offset, int length) throws IOException {
+		if (buffer == null) {
+			throw new NullPointerException("Byte array should not be null.");
+		} else if ((offset < 0) || (offset > buffer.length) || (length < 0) || ((offset + length) > buffer.length) || ((offset + length) < 0)) {
+			throw new IndexOutOfBoundsException("Invalid offset or length.");
+		} else if (length == 0) {
+			return 0;
+		}
+
+		int ch = read();
+
+		if (ch == -1 || ch == -2) {
+			return ch;
+		}
+
+		buffer[offset] = (byte) ch;
+
+		int readCounter = 1;
+
+		for (; readCounter < length; readCounter++) {
+			ch = read();
+
+			if (ch == -1 || ch == -2) {
+				break;
+			}
+
+			if (buffer != null) {
+				buffer[offset + readCounter] = (byte) ch;
+			}
+		}
+
+		return readCounter;
+	}
+}