package org.ibex.mail;
+import org.ibex.crypto.*;
+import org.ibex.util.*;
+import org.ibex.mail.protocol.*;
+import org.ibex.io.*;
+import java.util.*;
+import java.net.*;
+import java.io.*;
// FEATURE: MIME RFC2045, 2046, 2049
/** This class contains logic for encoding and decoding MIME multipart messages */
public class MIME {
- public static class QuotedPrintable {
+ public static class RFC2047 {
+ public static String decode(String s) {
+ /*
+ try { while (s.indexOf("=?") != -1) {
+ String pre = s.substring(0, s.indexOf("=?"));
+ s = s.substring(s.indexOf("=?") + 2);
+
+ // MIME charset; FIXME use this
+ String charset = s.substring(0, s.indexOf('?')).toLowerCase();
+ s = s.substring(s.indexOf('?') + 1);
+
+ String encoding = s.substring(0, s.indexOf('?')).toLowerCase();
+ s = s.substring(s.indexOf('?') + 1);
+
+ String encodedText = s.substring(0, s.indexOf("?="));
+
+ if (encoding.equals("b")) encodedText = new String(Base64.decode(encodedText));
+
+ // except that ANY char can be endoed (unlike real qp)
+ else if (encoding.equals("q")) encodedText = MIME.QuotedPrintable.decode(encodedText, true);
+ else Log.warn(MIME.class, "unknown RFC2047 encoding \""+encoding+"\"");
+
+ String post = s.substring(s.indexOf("?=") + 2);
+ s = pre + encodedText + post;
+
+ // FIXME re-encode when transmitting
+
+ } } catch (Exception e) {
+ Log.warn(MIME.class, "error trying to decode RFC2047 encoded-word: \""+s+"\"");
+ Log.warn(MIME.class, e);
+ }
+ */
+ return s;
+ }
+ }
+
+ public static class QuotedPrintable {
public static String decode(String s, boolean lax) {
//
// =XX -> hex representation, must be uppercase
return s;
}
}
+
+ // multipart/mixed -- default
+ // multipart/parallel -- order of components does not matter
+ // multipart/alternative -- same data, different versions
+ // multipart/digest -- default content-type of components is message/rfc822
+ // message/rfc822 -- FIXME
+ // message/partial -- not supported; see RFC 2046, section 5.2.2
+ // message/external-body -- not supported; see RFC 2046, section 5.2.3
+ // FIXME charsets US-ASCII, ISO-8559-X,
+ public static class Content extends org.ibex.js.JSReflection {
+ public final String type;
+ public final String subtype;
+ public final String description;
+ public final String id;
+ public final String transferEncoding;
+ public final String charset;
+ public final boolean composite;
+ public final boolean alternative;
+ public final Hashtable parameters = new Hashtable();
+ public Content(String header, String description, String id, String transferEncoding) {
+ this.id = id;
+ this.description = description;
+ this.transferEncoding = transferEncoding;
+ if (header == null) { type="text"; subtype="plain"; charset="us-ascii"; alternative=false; composite=false; return; }
+ header = header.trim();
+ if (header.indexOf('/') == -1) throw new MailException.Malformed("content-type lacks a forward slash: \""+header+"\"");
+ type = header.substring(0, header.indexOf('/')).toLowerCase();
+ header = header.substring(header.indexOf('/') + 1);
+ subtype = (header.indexOf(';') == -1) ? header.toLowerCase() : header.substring(0, header.indexOf(';')).toLowerCase();
+ composite = type != null && (type.equals("message") || type.equals("multipart"));
+ alternative = composite && subtype.equals("alternative");
+ charset = parameters.get("charset") == null ? "us-ascii" : parameters.get("charset").toString();
+ if (header.indexOf(';') == -1) return;
+ StringTokenizer st = new StringTokenizer(header.substring(header.indexOf(';') + 1), ";");
+ while(st.hasMoreTokens()) {
+ String key = st.nextToken().trim();
+ if (key.indexOf('=') == -1)
+ throw new MailException.Malformed("content-type parameter lacks an equals sign: \""+key+"\"");
+ String val = key.substring(key.indexOf('=')+1).trim();
+ if (val.startsWith("\"") && val.endsWith("\"")) val = val.substring(1, val.length() - 2);
+ key = key.substring(0, key.indexOf('=')+1).toLowerCase();
+ parameters.put(key, val);
+ }
+ }
+ }
+
+ public static class Part extends org.ibex.js.JSReflection {
+ public final Content content;
+ public final boolean mime; // true iff Mime-Version is 1.0
+ public final Headers headers;
+ public final Part[] subparts;
+ public final String body;
+ public final int lines;
+ private final boolean last;
+
+ private Part[] parseParts(Stream stream) {
+ Vec v = new Vec();
+ // first part begins with a boundary delimiter
+ for(String s = stream.readln(); s != null; s = stream.readln())
+ if (s.equals("--" + content.parameters.get("boundary"))) break;
+ while(true) {
+ Part p = new Part(stream, (String)content.parameters.get("boundary"), true);
+ v.addElement(p);
+ //lines += p.lines;
+ if (p.last) break;
+ }
+ return (Part[])v.copyInto(new Part[v.size()]);
+ }
+
+ public Part(Stream stream, String boundary, boolean assumeMime) throws MailException.Malformed {
+ this.headers = new Headers(stream, assumeMime);
+ this.mime = assumeMime | (headers.gets("mime-version")!=null&&headers.gets("mime-version").trim().equals("1.0"));
+ String ctype = headers.gets("content-type");
+ String encoding = headers.gets("content-transfer-encoding");
+ if (!(encoding == null || encoding.equals("7bit") || encoding.equals("8bit") || encoding.equals("binary") ||
+ encoding.equals("quoted-printable") || encoding.equals("base64"))) {
+ Log.warn(MIME.class, "unknown TransferEncoding \"" + encoding + "\"");
+ ctype = "application/octet-stream";
+ }
+ content = new Content(ctype, headers.gets("content-description"), headers.gets("content-id"), encoding);
+ //if (content.composite) { subparts = parseParts(stream); body = null; last = false; lines = 0; return; }
+ subparts = null;
+ boolean last = false;
+ int lines = 0;
+ StringBuffer body = new StringBuffer();
+ for(String s = stream.readln(); s != null; s = stream.readln()) {
+ if (boundary != null && (s.equals(boundary) || s.equals(boundary + "--"))) {
+ body.setLength(body.length() - 2); // preceeding CRLF is part of delimiter
+ last = s.equals(boundary + "--");
+ break;
+ }
+ body.append(s);
+ body.append("\r\n");
+ lines++;
+ }
+ if ("quoted-printable".equals(encoding)) this.body = MIME.QuotedPrintable.decode(body.toString(),false);
+ else if ("base64".equals(encoding)) this.body = new String(Base64.decode(body.toString()));
+ else this.body = body.toString();
+ this.last = last;
+ this.lines = lines + headers.lines;
+ }
+ }
+
+ public static class Headers extends org.ibex.js.JSReflection {
+ private Hashtable head = new Hashtable();
+ public final int lines;
+ public final String raw;
+ public Object get(Object s) { return head.get(((String)s).toLowerCase()); }
+ public String gets(String s) { return (String)get(s); }
+ public static String uncomment(String val) {
+ boolean inquotes = false;
+ for(int i=0; i<val.length(); i++) {
+ if (val.charAt(i) == '\"') inquotes = !inquotes;
+ if (val.charAt(i) == '(' && !inquotes)
+ val = val.substring(0, i) + val.substring(val.indexOf(i--, ')') + 1);
+ }
+ return val;
+ }
+ public Headers(Stream stream, boolean assumeMime) throws MailException.Malformed {
+ StringBuffer all = new StringBuffer();
+ String key = null;
+ int lines = 0;
+ for(String s = stream.readln(); s != null && !s.equals(""); s = stream.readln()) {
+ all.append(s);
+ all.append("\r\n");
+ lines++;
+ if (Character.isSpace(s.charAt(0))) {
+ if (key == null) throw new MailException.Malformed("Message began with a blank line; no headers");
+ head.put(key, head.get(key) + " " + s.trim());
+ continue;
+ }
+ if (s.indexOf(':') == -1) throw new MailException.Malformed("Header line does not contain colon: " + s);
+ key = s.substring(0, s.indexOf(':')).toLowerCase();
+ for(int i=0; i<key.length(); i++)
+ if (key.charAt(i) < 33 || key.charAt(i) > 126)
+ throw new MailException.Malformed("Header key \""+key+"\" contains invalid character \"" + key.charAt(i) + "\"");
+ String val = s.substring(s.indexOf(':') + 1).trim();
+ if (get(key) != null) val = get(key) + " " + val; // just append it to the previous one;
+ head.put(key, val);
+ }
+ this.raw = all.toString();
+ this.lines = lines;
+
+ Enumeration e = head.keys();
+ boolean mime = assumeMime | (gets("mime-version") != null && gets("mime-version").trim().equals("1.0"));
+ while(e.hasMoreElements()) {
+ String k = (String)e.nextElement();
+ String v = (String)head.get(k);
+ if (mime) k = MIME.RFC2047.decode(k);
+ v = uncomment(v);
+ if (mime) v = MIME.RFC2047.decode(v);
+ head.put(k, v);
+ }
+ }
+ }
}