forked from I2P_Developers/i2p.i2p
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:
170
apps/susimail/src/src/i2p/susi/util/FilenameUtil.java
Normal file
170
apps/susimail/src/src/i2p/susi/util/FilenameUtil.java
Normal 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));
|
||||
}
|
||||
****/
|
||||
}
|
@@ -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]);
|
||||
}
|
||||
}
|
||||
****/
|
||||
}
|
||||
|
@@ -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);
|
||||
|
@@ -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
|
||||
|
@@ -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() );
|
||||
|
@@ -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) {
|
||||
|
@@ -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) {}
|
||||
|
@@ -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 {
|
||||
|
Reference in New Issue
Block a user