// You may not use this file except in compliance with the License.
package org.ibex.mail;
+import static org.ibex.mail.MailException.*;
import org.ibex.crypto.*;
import org.ibex.util.*;
import org.ibex.mail.protocol.*;
-import org.ibex.io.*;
import org.ibex.js.*;
+import org.ibex.io.*;
+import org.ibex.io.Fountain;
import java.util.*;
import java.net.*;
import java.io.*;
/** This class contains logic for encoding and decoding MIME multipart messages */
public class MIME {
- 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);
+ public static class Part extends JSReflection implements Fountain {
+ public final Headers headers;
+ public final ContentType contentType;
+ private final String encoding;
+
+ private final Fountain all;
+ private final Fountain body;
+
+ public Stream getStream() { return all.getStream(); }
+ public int getNumLines() { return all.getNumLines(); }
+ public int getLength() { return all.getLength(); }
+ public Fountain getBody() { return body; }
+
+ private class BodyFountain implements Fountain {
+ public int getNumLines() { return Stream.countLines(getStream()); }
+ public int getLength() { return Part.this.getLength() - headers.getLength() - 2; }
+ public Stream getStream() {
+ return /*
+ "quoted-printable".equals(encoding) ? Encode.QuotedPrintable.decode(body.toString(),false) :
+ "base64".equals(encoding) ? Encode.fromBase64(body.toString()) :
+ */
+ Headers.Original.skip(all.getStream());
}
- */
- return s;
}
- }
-
- public static class QuotedPrintable {
- public static String decode(String s, boolean lax) {
- //
- // =XX -> hex representation, must be uppercase
- // 9, 32, 33-60, 62-126 can be literal
- // 9, 32 at end-of-line must get encoded
- // trailing whitespace must be deleted when decoding
- // =\n = soft line break
- // lines cannot be more than 76 chars long
- //
-
- // lax is used for RFC2047 headers; removes restrictions on which chars you can encode
- 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) {
- Log.warn(this, "content-type lacks a forward slash: \""+header+"\"");
- header = "text/plain";
- }
- 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 Part(Fountain all) {
+ this.headers = new Headers.Original(all.getStream());
+ String ctype = headers.get("content-type");
+ this.encoding = headers.get("content-transfer-encoding");
+ if (!(encoding == null || encoding.equals("7bit") || encoding.equals("8bit") || encoding.equals("binary") ||
+ encoding.equals("quoted-printable") || encoding.equals("base64"))) {
+ // FIXME: "7BIT" is popular
+ Log.warn(MIME.class, "unknown TransferEncoding \"" + encoding + "\"");
+ ctype = "application/octet-stream";
}
+ this.contentType = new ContentType(ctype, headers.get("content-description"), headers.get("content-id"), encoding);
+ this.all = all;
+ this.body = new BodyFountain();
}
- }
-
- 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) {
+ /*
+ public Part getPart(int i) {
+ Stream stream = body.getStream();
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;
+ if (s.equals("--" + contentType.parameters.get("boundary"))) break;
while(true) {
- Part p = new Part(stream, (String)content.parameters.get("boundary"), true);
+ Stream substream = new BoundaryStream(stream, (String)contentType.parameters.get("boundary"));
+ Part p = new Part(substream, true, null); // FIXME split off headers
v.addElement(p);
- //lines += p.lines;
- if (p.last) break;
+ if (substream.isLast()) break;
}
- return (Part[])v.copyInto(new Part[v.size()]);
+ return parts = (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();
+ */
+ }
+ /*
+ public static class Boundary implements Stream.Transformer {
+ private final String boundary;
+ private boolean done = false;
+ private boolean last = false;
+ public Boundary(String bounardy) { this.boundary = boundary; }
+ public boolean isLast() { while(!done) readln(); return last; }
+ public Stream transform(Stream stream) {
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 + "--");
+ done = true;
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(Encode.fromBase64(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 String toString() { return raw; }
- public JS get(JS s) throws JSExn { return JSU.S((String)head.get(JSU.toString(s).toLowerCase())); }
- public String gets(String s) { return (String)head.get(s.toLowerCase()); }
- 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 (gets(key) != null) val = gets(key) + " " + val; // just append it to the previous one;
- head.put(key, val);
- }
- this.raw = all.toString();
- this.lines = lines;
-
- java.util.Enumeration e = head.keys();
- boolean mime = assumeMime | (gets("mime-version") != null && gets("mime-version").trim().equals("1.0"));
- /*
- while(e.hasNext()) {
- String k = (String)e.next();
- 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);
+ //lines++;
}
- */
}
}
+ */
}