diff --git a/apps/susimail/src/src/i2p/susi/webmail/WebMail.java b/apps/susimail/src/src/i2p/susi/webmail/WebMail.java index 716d8647000ed2f10d48b25c7a9567dde9f6f9ab..b284c9e92e2bb42fa03c85c360cd48a6cdd7d423 100644 --- a/apps/susimail/src/src/i2p/susi/webmail/WebMail.java +++ b/apps/susimail/src/src/i2p/susi/webmail/WebMail.java @@ -714,9 +714,17 @@ public class WebMail extends HttpServlet String type = mailPart.type; if (type != null && type.startsWith("image/")) { // we at least show images safely... + String name = mailPart.filename; + if (name == null) { + name = mailPart.name; + if (name == null) + name = mailPart.description; + } + name = quoteHTML(name); out.println("<img src=\"" + myself + '?' + RAW_ATTACHMENT + '=' + mailPart.hashCode() + - "&" + B64UIDL + '=' + Base64.encode(mailPart.uidl) + "\">"); + "&" + B64UIDL + '=' + Base64.encode(mailPart.uidl) + + "\" alt=\"" + name + "\">"); } else if (type != null && ( // type list from snark type.startsWith("audio/") || type.equals("application/ogg") || @@ -2077,8 +2085,21 @@ public class WebMail extends HttpServlet } if(content == null) return false; + String name = part.filename; + if (name == null) { + name = part.name; + if (name == null) { + name = part.description; + if (name == null) + name = "part" + part.hashCode(); + } + } + String name2 = sanitizeFilename(name); + String name3 = encodeFilenameRFC5987(name); if (isRaw) { try { + response.addHeader("Content-Disposition", "inline; filename=\"" + name2 + "\"; " + + "filename*=" + name3); if (part.type != null) response.setContentType(part.type); response.setContentLength(content.length); @@ -2092,16 +2113,9 @@ public class WebMail extends HttpServlet ZipOutputStream zip = null; try { zip = new ZipOutputStream( response.getOutputStream() ); - String name; - if( part.filename != null ) - name = part.filename; - else if( part.name != null ) - name = part.name; - else - name = "part" + part.hashCode(); - String name2 = sanitizeFilename(name); response.setContentType( "application/zip; name=\"" + name2 + ".zip\"" ); - response.addHeader( "Content-Disposition", "attachment; filename=\"" + name2 + ".zip\"" ); + response.addHeader("Content-Disposition", "attachment; filename=\"" + name2 + ".zip\"; " + + "filename*=" + name3 + ".zip"); ZipEntry entry = new ZipEntry( name ); zip.putNextEntry( entry ); zip.write( content.content, content.offset, content.length ); @@ -2135,12 +2149,19 @@ public class WebMail extends HttpServlet if(content == null) return false; - String name = mail.subject != null ? sanitizeFilename(mail.subject) : "message"; + String name; + if (mail.subject != null) + name = mail.subject.trim() + ".eml"; + else + name = "message.eml"; + String name2 = sanitizeFilename(name); + String name3 = encodeFilenameRFC5987(name); try { response.setContentType("message/rfc822"); response.setContentLength(content.length); // cache-control? - response.addHeader( "Content-Disposition", "attachment; filename=\"" + name + ".eml\"" ); + response.addHeader("Content-Disposition", "attachment; filename=\"" + name2 + "\"; " + + "filename*=" + name3); response.getOutputStream().write(content.content, content.offset, content.length); return true; } catch (IOException e) { @@ -2150,21 +2171,78 @@ public class WebMail extends HttpServlet } /** - * Convert the UTF-8 to ISO-8859-1 suitable for inclusion in a header. - * This will result in a bunch of ??? for non-Western languages. + * 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 * - * @param sessionObject - * @param response - * @return success * @since 0.9.18 */ private static String sanitizeFilename(String name) { - try { - name = new String(name.getBytes("ISO-8859-1"), "ISO-8859-1"); - } catch( UnsupportedEncodingException uee ) {} - // strip control chars? - name = name.replace('"', '_'); - return 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(); } /**