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
This commit is contained in:
zzz
2018-02-09 15:17:04 +00:00
parent 3d25a9fd66
commit ddf7fba039
8 changed files with 223 additions and 95 deletions

View File

@@ -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));
}
****/
}

View File

@@ -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]);
}
}
****/
}

View File

@@ -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);

View File

@@ -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

View File

@@ -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() );

View File

@@ -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) {

View File

@@ -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) {}

View File

@@ -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 {