From ddf7fba039b8882bb1a54b35f0f7f7270ca2f189 Mon Sep 17 00:00:00 2001
From: zzz <zzz@mail.i2p>
Date: Fri, 9 Feb 2018 15:17:04 +0000
Subject: [PATCH] SusiMail: - Don't show the 'no charset' warning Filename
 encoding fixes: - Fix encoding to be hex upper case - Move encoding to new
 util class - Encode in sent mail - Implement decoding in received mail Error
 message and debug tweaks Output remainder of header line after decode fail

---
 .../src/src/i2p/susi/util/FilenameUtil.java   | 170 ++++++++++++++++++
 .../src/src/i2p/susi/util/HexTable.java       |  11 ++
 .../src/src/i2p/susi/webmail/MailPart.java    |  14 +-
 .../src/src/i2p/susi/webmail/WebMail.java     |  95 ++--------
 .../webmail/encoding/EncodingFactory.java     |   2 +-
 .../i2p/susi/webmail/encoding/HeaderLine.java |  11 +-
 .../i2p/susi/webmail/pop3/POP3MailBox.java    |   2 +-
 .../src/i2p/susi/webmail/smtp/SMTPClient.java |  13 +-
 8 files changed, 223 insertions(+), 95 deletions(-)
 create mode 100644 apps/susimail/src/src/i2p/susi/util/FilenameUtil.java

diff --git a/apps/susimail/src/src/i2p/susi/util/FilenameUtil.java b/apps/susimail/src/src/i2p/susi/util/FilenameUtil.java
new file mode 100644
index 0000000000..a3704dd7d8
--- /dev/null
+++ b/apps/susimail/src/src/i2p/susi/util/FilenameUtil.java
@@ -0,0 +1,170 @@
+package i2p.susi.util;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.Locale;
+
+import net.i2p.data.DataHelper;
+
+/**
+ * File name encoding methods
+ *
+ * @since 0.9.34 pulled out of WebMail
+ */
+public class FilenameUtil {
+
+	/**
+	 * Convert the UTF-8 to ASCII suitable for inclusion in a header
+	 * and for use as a cross-platform filename.
+	 * Replace chars likely to be illegal in filenames,
+	 * and non-ASCII chars, with _
+	 *
+	 * Ref: RFC 6266, RFC 5987, i2psnark Storage.ILLEGAL
+	 *
+	 * @since 0.9.18
+	 */
+	public static String sanitizeFilename(String name) {
+		name = name.trim();
+		StringBuilder buf = new StringBuilder(name.length());
+		for (int i = 0; i < name.length(); i++) {
+			char c = name.charAt(i);
+			// illegal filename chars
+			if (c <= 32 || c >= 0x7f ||
+			    c == '<' || c == '>' || c == ':' || c == '"' ||
+			    c == '/' || c == '\\' || c == '|' || c == '?' ||
+			    c == '*')
+				buf.append('_');
+			else
+				buf.append(c);
+		}
+		return buf.toString();
+	}
+
+	/**
+	 * Encode the UTF-8 suitable for inclusion in a header
+	 * as a RFC 5987/6266 filename* value, and for use as a cross-platform filename.
+	 * Replace chars likely to be illegal in filenames with _
+	 *
+	 * Ref: RFC 6266, RFC 5987, i2psnark Storage.ILLEGAL
+	 *
+	 * This does NOT do multiline, e.g. filename*0* (RFC 2231)
+	 *
+	 * ref: https://blog.nodemailer.com/2017/01/27/the-mess-that-is-attachment-filenames/
+	 * ref: RFC 2231
+	 *
+	 * @since 0.9.33
+	 */
+	public static String encodeFilenameRFC5987(String name) {
+		name = name.trim();
+		StringBuilder buf = new StringBuilder(name.length());
+		buf.append("utf-8''");
+		for (int i = 0; i < name.length(); i++) {
+			char c = name.charAt(i);
+			// illegal filename chars
+			if (c < 32 || (c >= 0x7f && c <= 0x9f) ||
+			    c == '<' || c == '>' || c == ':' || c == '"' ||
+			    c == '/' || c == '\\' || c == '|' || c == '?' ||
+			    c == '*' ||
+			    // unicode newlines
+			    c == 0x2028 || c == 0x2029) {
+				buf.append('_');
+			} else if (c == ' ' || c == '\'' || c == '%' ||          // not in 5987 attr-char
+			           c == '(' || c == ')' || c == '@' ||           // 2616 separators
+			           c == ',' || c == ';' || c == '[' || c == ']' ||
+			           c == '=' || c == '{' || c == '}') {
+				// single byte encoding
+				buf.append(HexTable.table[c].replace('=', '%'));
+			} else if (c < 0x7f) {
+				// single byte char, as-is
+				buf.append(c);
+			} else {
+				// multi-byte encoding
+				byte[] utf = DataHelper.getUTF8(String.valueOf(c));
+				for (int j = 0; j < utf.length; j++) {
+					int b = utf[j] & 0xff;
+					buf.append(HexTable.table[b].replace('=', '%'));
+				}
+			}
+		}
+		return buf.toString();
+	}
+
+	/**
+	 * Modified from QuotedPrintable.decode()
+	 *
+	 * @return name on error
+	 * @since 0.9.34
+	 */
+	public static String decodeFilenameRFC5987(String name) {
+		int idx = name.indexOf('\'');
+		if (idx <= 0)
+			return name;
+		String enc = name.substring(0, idx).toUpperCase(Locale.US);
+		idx = name.indexOf('\'', idx + 1);
+		if (idx <= 0)
+			return name;
+		String n = name.substring(idx + 1);
+		StringReader in = new StringReader(n);
+		ByteArrayOutputStream out = new ByteArrayOutputStream(n.length());
+		try {
+			while (true) {
+				int c = in.read();
+				if (c < 0)
+					break;
+				if( c == '%' ) {
+						int a = in.read();
+						if (a < 0) {
+							out.write(c);
+							break;
+						}
+						int b = in.read();
+						if (b < 0) {
+							out.write(c);
+							out.write(a);
+							break;
+						}
+						if( ( ( a >= '0' && a <= '9' ) || ( a >= 'A' && a <= 'F' ) ) &&
+								( ( b >= '0' && b <= '9' ) || ( b >= 'A' && b <= 'F' ) ) ) {
+							if( a >= '0' && a <= '9' )
+								a -= '0';
+							else if( a >= 'A' && a <= 'F' )
+								a = (byte) (a - 'A' + 10);
+	
+							if( b >= '0' && b <= '9' )
+								b -= '0';
+							else if( b >= 'A' && b <= 'F' )
+								b = (byte) (b - 'A' + 10);
+							
+							out.write(a*16 + b);
+						}
+						else if( a == '\r' && b == '\n' ) {
+							// ignore, shouldn't happen
+						} else {
+							// FAIL
+							out.write(c);
+							out.write(a);
+							out.write(b);
+						}
+				} else {
+					// print out everything else literally
+					out.write(c);
+				}
+			}
+			return new String(out.toByteArray(), enc);
+		} catch (IOException ioe) {
+			ioe.printStackTrace();
+			return n;
+		}
+	}
+
+/****
+	public static void main(String[] args) {
+		String in = "2018年01月25-26日(深圳)";
+		String enc = encodeFilenameRFC5987(in);
+		String dec = decodeFilenameRFC5987(enc);
+		System.out.println("in:  " + in + "\nenc: " + enc + "\ndec: " + dec +
+		                   "\nPass? " + in.equals(dec));
+	}
+****/
+}
diff --git a/apps/susimail/src/src/i2p/susi/util/HexTable.java b/apps/susimail/src/src/i2p/susi/util/HexTable.java
index 8d08e8ceb6..cf1dc88c57 100644
--- a/apps/susimail/src/src/i2p/susi/util/HexTable.java
+++ b/apps/susimail/src/src/i2p/susi/util/HexTable.java
@@ -28,6 +28,9 @@ package i2p.susi.util;
  */
 public class HexTable {
 	
+	/**
+	 *  Three character strings, upper case, e.g. "=0A"
+	 */
 	public static final String[] table = new String[256];
 	
 	static {
@@ -57,4 +60,12 @@ public class HexTable {
 			return str;
 		}
 	}
+
+/****
+	public static void main(String[] args) {
+		for( int i = 0; i < 256; i++ ) {
+			System.out.println(i + ": " + table[i]);
+		}
+	}
+****/
 }
diff --git a/apps/susimail/src/src/i2p/susi/webmail/MailPart.java b/apps/susimail/src/src/i2p/susi/webmail/MailPart.java
index 54d4eec9e8..a57a6dda5d 100644
--- a/apps/susimail/src/src/i2p/susi/webmail/MailPart.java
+++ b/apps/susimail/src/src/i2p/susi/webmail/MailPart.java
@@ -28,6 +28,7 @@ import i2p.susi.util.Buffer;
 import i2p.susi.util.CountingOutputStream;
 import i2p.susi.util.DummyOutputStream;
 import i2p.susi.util.EOFOnMatchInputStream;
+import i2p.susi.util.FilenameUtil;
 import i2p.susi.util.LimitInputStream;
 import i2p.susi.util.ReadBuffer;
 import i2p.susi.util.ReadCounter;
@@ -92,7 +93,7 @@ class MailPart {
 		this.uidl = uidl;
 		buffer = readBuffer;
 		
-		parts = new ArrayList<MailPart>();
+		parts = new ArrayList<MailPart>(4);
 
 		if (hdrlines != null) {
 			// from Mail headers
@@ -134,9 +135,14 @@ class MailPart {
 			else if( hlc.startsWith( "content-disposition: " ) ) {
 				x_disposition = getFirstAttribute( headerLines[i] ).toLowerCase(Locale.US);
 				String str;
-				str = getHeaderLineAttribute( headerLines[i], "filename" );
-				if( str != null )
-					x_name = str;
+				str = getHeaderLineAttribute(headerLines[i], "filename*");
+				if (str != null) {
+					x_name = FilenameUtil.decodeFilenameRFC5987(str);
+				} else {
+					str = getHeaderLineAttribute(headerLines[i], "filename");
+					if (str != null)
+						x_name = str;
+				}
 			}
 			else if( hlc.startsWith( "content-type: " ) ) {
 				x_type = getFirstAttribute( headerLines[i] ).toLowerCase(Locale.US);
diff --git a/apps/susimail/src/src/i2p/susi/webmail/WebMail.java b/apps/susimail/src/src/i2p/susi/webmail/WebMail.java
index 1157fd36e1..f6246af592 100644
--- a/apps/susimail/src/src/i2p/susi/webmail/WebMail.java
+++ b/apps/susimail/src/src/i2p/susi/webmail/WebMail.java
@@ -29,6 +29,7 @@ import i2p.susi.util.Config;
 import i2p.susi.util.DecodingOutputStream;
 import i2p.susi.util.EscapeHTMLOutputStream;
 import i2p.susi.util.EscapeHTMLWriter;
+import i2p.susi.util.FilenameUtil;
 import i2p.susi.util.Folder;
 import i2p.susi.util.Folder.SortOrder;
 import i2p.susi.util.Buffer;
@@ -702,8 +703,9 @@ public class WebMail extends HttpServlet
 				if( charset == null ) {
 					charset = "ISO-8859-1";
 					// don't show this in text mode which is used to include the mail in the reply or forward
-					if (html)
-						reason = _t("Warning: no charset found, fallback to US-ASCII.") + br;
+					// Too common, don't show this at all.
+					//if (html)
+					//	reason = _t("Warning: no charset found, fallback to US-ASCII.") + br;
 				}
 				try {
 					Writer escaper;
@@ -927,7 +929,7 @@ public class WebMail extends HttpServlet
 						// we do this after the initial priming above
 						mailbox.setNewMailListener(sessionObject);
 					} else {
-						sessionObject.error += mailbox.lastError();
+						sessionObject.error += mailbox.lastError() + '\n';
 						Debug.debug(Debug.DEBUG, "LOGIN FAIL, REMOVING SESSION");
 						HttpSession session = request.getSession();
 						session.removeAttribute( "sessionObject" );
@@ -1283,7 +1285,7 @@ public class WebMail extends HttpServlet
 			}
 			mailbox.refresh();
 			String error = mailbox.lastError();
-			sessionObject.error += error;
+			sessionObject.error += error + '\n';
 			sessionObject.mailCache.getMail(MailCache.FetchMode.HEADER);
 			// get through cache so we have the disk-only ones too
 			String[] uidls = sessionObject.mailCache.getUIDLs();
@@ -1487,9 +1489,7 @@ public class WebMail extends HttpServlet
 			return null;
 		
 		if( part.hashCode() == hashCode )
-{
 			return part;
-}
 		
 		if( part.multipart || part.message ) {
 			for( MailPart p : part.parts ) {
@@ -2130,8 +2130,8 @@ public class WebMail extends HttpServlet
 						name = "part" + part.hashCode();
 				}
 			}
-			String name2 = sanitizeFilename(name);
-			String name3 = encodeFilenameRFC5987(name);
+			String name2 = FilenameUtil.sanitizeFilename(name);
+			String name3 = FilenameUtil.encodeFilenameRFC5987(name);
 			if (isRaw) {
 				try {
 					response.addHeader("Content-Disposition", "inline; filename=\"" + name2 + "\"; " +
@@ -2193,8 +2193,8 @@ public class WebMail extends HttpServlet
 			name = mail.subject.trim() + ".eml";
 		else
 			name = "message.eml";
-		String name2 = sanitizeFilename(name);
-		String name3 = encodeFilenameRFC5987(name);
+		String name2 = FilenameUtil.sanitizeFilename(name);
+		String name3 = FilenameUtil.encodeFilenameRFC5987(name);
 		InputStream in = null;
 		try {
 			response.setContentType("message/rfc822");
@@ -2213,81 +2213,6 @@ public class WebMail extends HttpServlet
 		}
 	}
 
-	/**
-	 * Convert the UTF-8 to ASCII suitable for inclusion in a header
-	 * and for use as a cross-platform filename.
-	 * Replace chars likely to be illegal in filenames,
-	 * and non-ASCII chars, with _
-	 *
-	 * Ref: RFC 6266, RFC 5987, i2psnark Storage.ILLEGAL
-	 *
-	 * @since 0.9.18
-	 */
-	private static String sanitizeFilename(String name) {
-		name = name.trim();
-		StringBuilder buf = new StringBuilder(name.length());
-		for (int i = 0; i < name.length(); i++) {
-			char c = name.charAt(i);
-			// illegal filename chars
-			if (c <= 32 || c >= 0x7f ||
-			    c == '<' || c == '>' || c == ':' || c == '"' ||
-			    c == '/' || c == '\\' || c == '|' || c == '?' ||
-			    c == '*')
-				buf.append('_');
-			else
-				buf.append(c);
-		}
-		return buf.toString();
-	}
-
-	/**
-	 * Encode the UTF-8 suitable for inclusion in a header
-	 * as a RFC 5987/6266 filename* value, and for use as a cross-platform filename.
-	 * Replace chars likely to be illegal in filenames with _
-	 *
-	 * Ref: RFC 6266, RFC 5987, i2psnark Storage.ILLEGAL
-	 *
-	 * @since 0.9.33
-	 */
-	private static String encodeFilenameRFC5987(String name) {
-		name = name.trim();
-		StringBuilder buf = new StringBuilder(name.length());
-		buf.append("utf-8''");
-		for (int i = 0; i < name.length(); i++) {
-			char c = name.charAt(i);
-			// illegal filename chars
-			if (c < 32 || (c >= 0x7f && c <= 0x9f) ||
-			    c == '<' || c == '>' || c == ':' || c == '"' ||
-			    c == '/' || c == '\\' || c == '|' || c == '?' ||
-			    c == '*' ||
-			    // unicode newlines
-			    c == 0x2028 || c == 0x2029) {
-				buf.append('_');
-			} else if (c == ' ' || c == '\'' || c == '%' ||          // not in 5987 attr-char
-			           c == '(' || c == ')' || c == '@' ||           // 2616 separators
-			           c == ',' || c == ';' || c == '[' || c == ']' ||
-			           c == '=' || c == '{' || c == '}') {
-				// single byte encoding
-				buf.append('%');
-				buf.append(Integer.toHexString(c));
-			} else if (c < 0x7f) {
-				// single byte char, as-is
-				buf.append(c);
-			} else {
-				// multi-byte encoding
-				byte[] utf = DataHelper.getUTF8(String.valueOf(c));
-				for (int j = 0; j < utf.length; j++) {
-					int b = utf[j] & 0xff;
-					buf.append('%');
-					if (b < 16)
-						buf.append(0);
-					buf.append(Integer.toHexString(b));
-				}
-			}
-		}
-		return buf.toString();
-	}
-
 	/**
 	 * @param sessionObject
 	 * @param request
diff --git a/apps/susimail/src/src/i2p/susi/webmail/encoding/EncodingFactory.java b/apps/susimail/src/src/i2p/susi/webmail/encoding/EncodingFactory.java
index 6e8c680e61..d027eeaa74 100644
--- a/apps/susimail/src/src/i2p/susi/webmail/encoding/EncodingFactory.java
+++ b/apps/susimail/src/src/i2p/susi/webmail/encoding/EncodingFactory.java
@@ -58,7 +58,7 @@ public class EncodingFactory {
 					Class<?> c = Class.forName( classNames[i] );
 					Encoding e = (Encoding) (c.getDeclaredConstructor().newInstance());
 					encodings.put( e.getName(), e );
-					Debug.debug( Debug.DEBUG, "Registered " + e.getClass().getName() );
+					//Debug.debug( Debug.DEBUG, "Registered " + e.getClass().getName() );
 				}
 				catch (Exception e) {
 					Debug.debug( Debug.ERROR, "Error loading class '" + classNames[i] + "', reason: " + e.getClass().getName() );
diff --git a/apps/susimail/src/src/i2p/susi/webmail/encoding/HeaderLine.java b/apps/susimail/src/src/i2p/susi/webmail/encoding/HeaderLine.java
index c268fea366..51085598c2 100644
--- a/apps/susimail/src/src/i2p/susi/webmail/encoding/HeaderLine.java
+++ b/apps/susimail/src/src/i2p/susi/webmail/encoding/HeaderLine.java
@@ -292,14 +292,20 @@ public class HeaderLine extends Encoding {
 				//	" offset " + offset + " pushback? " + hasPushback + " pbchar(dec) " + c + '\n' +
 				//	net.i2p.util.HexDump.dump(encodedWord, 0, offset));
 				if (f4 == 0) {
-					// at most 1 byte is pushed back, the rest is discarded
+					// at most 1 byte is pushed back
 					if (f1 == 0) {
 						// This is normal
 						continue;
 					} else if (f2 == 0) {
+						// =? but no more ?
+						// output what we buffered
 						Debug.debug(Debug.DEBUG, "2nd '?' not found");
+						for (int i = 0; i < offset; i++) {
+							out.write(encodedWord[i] & 0xff);
+						}
 						continue;
 					} else if (f3 == 0) {
+						// discard what we buffered
 						Debug.debug(Debug.DEBUG, "3rd '?' not found");
 						continue;
 					} else {
@@ -326,7 +332,8 @@ public class HeaderLine extends Encoding {
 						try {
 							// System.err.println( "decode(" + (f3 + 1) + "," + ( f4 - f3 - 1 ) + ")" );
 							ReadBuffer tmpIn = new ReadBuffer(encodedWord, f3 + 1, f4 - f3 - 1);
-							MemoryBuffer tmp = new MemoryBuffer(DECODE_MAX);
+							// decoded won't be longer than encoded
+							MemoryBuffer tmp = new MemoryBuffer(f4 - f3 - 1);
 							try {
 								e.decode(tmpIn, tmp);
 							} catch (EOFException eof) {
diff --git a/apps/susimail/src/src/i2p/susi/webmail/pop3/POP3MailBox.java b/apps/susimail/src/src/i2p/susi/webmail/pop3/POP3MailBox.java
index ab6723cded..c6b80b3426 100644
--- a/apps/susimail/src/src/i2p/susi/webmail/pop3/POP3MailBox.java
+++ b/apps/susimail/src/src/i2p/susi/webmail/pop3/POP3MailBox.java
@@ -1178,7 +1178,7 @@ public class POP3MailBox implements NewMailListener {
 						sendCmd1aNoWait("QUIT");
 					}
 				} catch (IOException e) {
-					Debug.debug( Debug.DEBUG, "error closing: " + e);
+					//Debug.debug( Debug.DEBUG, "error closing: " + e);
 				} finally {
 					if (socket != null) {
 						try { socket.close(); } catch (IOException e) {}
diff --git a/apps/susimail/src/src/i2p/susi/webmail/smtp/SMTPClient.java b/apps/susimail/src/src/i2p/susi/webmail/smtp/SMTPClient.java
index c83e4232a9..762dbc9625 100644
--- a/apps/susimail/src/src/i2p/susi/webmail/smtp/SMTPClient.java
+++ b/apps/susimail/src/src/i2p/susi/webmail/smtp/SMTPClient.java
@@ -29,6 +29,7 @@ import i2p.susi.webmail.Messages;
 import i2p.susi.webmail.encoding.Encoding;
 import i2p.susi.webmail.encoding.EncodingException;
 import i2p.susi.webmail.encoding.EncodingFactory;
+import i2p.susi.util.FilenameUtil;
 
 import java.io.BufferedWriter;
 import java.io.IOException;
@@ -347,10 +348,18 @@ public class SMTPClient {
 						Encoding encoding = EncodingFactory.getEncoding(encodeTo);
 						if (encoding == null)
 							throw new EncodingException( _t("No Encoding found for {0}", encodeTo));
+						// ref: https://blog.nodemailer.com/2017/01/27/the-mess-that-is-attachment-filenames/
+						// ref: RFC 2231
+						// split Content-Disposition into 3 lines to maximize room
+						// TODO filename*0* for long names...
+						String name = attachment.getFileName();
+						String name2 = FilenameUtil.sanitizeFilename(name);
+						String name3 = FilenameUtil.encodeFilenameRFC5987(name);
 						out.write("\r\n--" + boundary +
 						          "\r\nContent-type: " + attachment.getContentType() +
-						          "\r\nContent-Disposition: attachment; filename=\"" + attachment.getFileName() +
-						          "\"\r\nContent-Transfer-Encoding: " + attachment.getTransferEncoding() +
+						          "\r\nContent-Disposition: attachment;\r\n\tfilename=\"" + name2 +
+						          "\";\r\n\tfilename*=" + name3 +
+						          "\r\nContent-Transfer-Encoding: " + attachment.getTransferEncoding() +
 						          "\r\n\r\n");
 						InputStream in = null;
 						try {
-- 
GitLab