import org.ibex.util.*;
import org.ibex.mail.protocol.*;
import org.ibex.io.*;
-import org.ibex.net.*;
import java.util.*;
import java.net.*;
import java.io.*;
public final Address envelopeFrom;
public final Address envelopeTo;
+ public final boolean mime; // true iff Mime-Version is 1.0
+
public final Date arrival; // when the message first arrived at this machine; IMAP "internal date message attr"
public int size() { return allHeaders.length() + 2 /* CRLF */ + body.length(); }
public String toString() { return allHeaders + "\r\n" + body; }
+
public void dump(Stream s) {
- s.out.setNewline("\r\n");
- s.out.println("X-org.ibex.mail.headers.envelope.From: " + envelopeFrom);
- s.out.println("X-org.ibex.mail.headers.envelope.To: " + envelopeTo);
- s.out.println(allHeaders);
- s.out.println();
- s.out.println(body);
- s.out.flush();
+ s.setNewline("\r\n");
+ s.println("X-org.ibex.mail.headers.envelope.From: " + envelopeFrom);
+ s.println("X-org.ibex.mail.headers.envelope.To: " + envelopeTo);
+ s.println(allHeaders);
+ s.println();
+ s.println(body);
+ s.flush();
}
public class Trace {
final String returnPath;
final Element[] elements;
public Trace(Stream stream) throws Trace.Malformed {
- String retPath = stream.in.readln();
+ String retPath = stream.readln();
if (!retPath.startsWith("Return-Path:")) throw new Trace.Malformed("trace did not start with Return-Path header");
returnPath = retPath.substring(12).trim();
Vec el = new Vec();
while(true) {
- String s = stream.in.readln();
+ String s = stream.readln();
if (s == null) break;
- if (!s.startsWith("Received:")) { stream.in.unread(s); break; }
+ if (!s.startsWith("Received:")) { stream.unread(s); break; }
s = s.substring(9).trim();
el.addElement(new Element(s));
}
public class Malformed extends Message.Malformed { public Malformed(String s) { super(s); } }
}
- public static class Malformed extends RuntimeException { public Malformed(String s) { super(s); } }
+ public static class Malformed extends Exception { public Malformed(String s) { super(s); } }
- public Message(Address from, Address to, String s, Date arrival) { this(from, to, new Stream(s), arrival); }
- public Message(Address from, Address to, Stream in) { this(from, to, in, new Date()); }
- public Message(Address envelopeFrom, Address envelopeTo, Stream stream, Date arrival) {
+ public Message(Address from, Address to, String s, Date arrival) throws Malformed {this(from,to,new Stream(s), arrival); }
+ public Message(Address from, Address to, Stream in) throws Malformed { this(from, to, in, new Date()); }
+ public Message(Address envelopeFrom, Address envelopeTo, Stream stream, Date arrival) throws Malformed {
this.arrival = arrival;
this.headers = new CaseInsensitiveHash();
Vec envelopeToHeader = new Vec();
String subject = null, messageid = null;
Vec cc = new Vec(), bcc = new Vec(), resent = new Vec(), traces = new Vec();
int lines = 0;
- for(String s = stream.in.readln(); s != null && !s.equals(""); s = stream.in.readln()) {
+ for(String s = stream.readln(); s != null && !s.equals(""); s = stream.readln()) {
all.append(s);
lines++;
all.append("\r\n");
+
+ // FIXME RFC822 1,000-char limit per line
+
+
+ // FIXME only iff Mime 1.0 (?)
+ // RFC 2047
+ 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(this, "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(this, "error trying to decode RFC2047 encoded-word: \""+s+"\"");
+ Log.warn(this, e);
+ }
+
if (s.length() == 0 || Character.isSpace(s.charAt(0))) {
if (key == null) throw new Malformed("Message began with a blank line; no headers");
- ((CaseInsensitiveHash)headers).add(key, headers.get(key) + s);
+ ((CaseInsensitiveHash)headers).add(key, headers.get(key) + s.trim());
continue;
}
if (s.indexOf(':') == -1) throw new Malformed("Header line does not contain colon: " + s);
if (resent.size() == 0 || key.startsWith("Resent-From")) resent.addElement(new Hashtable());
((Hashtable)resent.lastElement()).put(key.substring(7), val);
} else if (key.startsWith("Return-Path")) {
- stream.in.unread(s); traces.addElement(new Trace(stream));
+ stream.unread(s); traces.addElement(new Trace(stream));
} else if (key.equals("X-org.ibex.mail.headers.envelope.From")) {
- if (envelopeFrom == null) envelopeFrom = new Address(val);
+ try { if (envelopeFrom == null) envelopeFrom = new Address(val);
+ } catch (Address.Malformed a) { Log.warn(this, a); }
} else if (key.equals("X-org.ibex.mail.headers.envelope.To")) {
- if (envelopeTo == null) envelopeTo = new Address(val);
+ try {if (envelopeTo == null) envelopeTo = new Address(val);
+ } catch (Address.Malformed a) { Log.warn(this, a); }
} else {
// just append it to the previous one; valid for Comments/Keywords
if (headers.get(key) != null) val = headers.get(key) + " " + val;
}
// FIXME what if all are null?
- this.to = headers.get("To") == null ? envelopeTo : new Address((String)headers.get("To"));
- this.from = headers.get("From") == null ? envelopeFrom : new Address((String)headers.get("From"));
+ this.to = headers.get("To") == null ? envelopeTo : Address.parse((String)headers.get("To"));
+ this.from = headers.get("From") == null ? envelopeFrom : Address.parse((String)headers.get("From"));
this.envelopeFrom = envelopeFrom == null ? this.from : envelopeFrom;
this.envelopeTo = envelopeTo == null ? this.to : envelopeTo;
+ this.mime = headers.get("mime-version") != null && headers.get("mime-version").toString().trim().startsWith("1.0");
this.date = new Date(); // FIXME (Date)headers.get("Date");
- this.replyto = headers.get("Reply-To") == null ? null : new Address((String)headers.get("Reply-To"));
+ this.replyto = headers.get("Reply-To") == null ? null : Address.parse((String)headers.get("Reply-To"));
this.subject = (String)headers.get("Subject");
this.messageid = (String)headers.get("Message-Id");
if (headers.get("Cc") != null) {
// FIXME: tokenize better
- StringTokenizer st = new StringTokenizer((String)headers.get("Cc"));
+ StringTokenizer st = new StringTokenizer((String)headers.get("Cc"), ",");
this.cc = new Address[st.countTokens()];
- for(int i=0; i<this.cc.length; i++) this.cc[i] = new Address(st.nextToken());
+ for(int i=0; i<this.cc.length; i++) this.cc[i] = Address.parse(st.nextToken());
} else {
this.cc = new Address[0];
}
if (headers.get("Bcc") != null) {
StringTokenizer st = new StringTokenizer((String)headers.get("Bcc"));
this.bcc = new Address[st.countTokens()];
- for(int i=0; i<this.bcc.length; i++) this.bcc[i] = new Address(st.nextToken());
+ for(int i=0; i<this.bcc.length; i++) this.bcc[i] = Address.parse(st.nextToken());
} else {
this.bcc = new Address[0];
}
traces.copyInto(this.traces = new Trace[traces.size()]);
allHeaders = all.toString();
StringBuffer body = new StringBuffer();
- for(String s = stream.in.readln();; s = stream.in.readln()) {
+ for(String s = stream.readln();; s = stream.readln()) {
if (s == null) break;
lines++;
body.append(s); // FIXME: we're assuming all mail messages fit in memory
this.lines = lines;
this.body = body.toString();
}
+
+ public static class Part {
+ public final String type;
+ public final String subtype;
+ public final Hash parameters = new Hash();
+ public final Part[] subparts;
+ public final String payload;
+ public final String id;
+ public final String description;
+
+ public final boolean composite;
+ public final boolean alternative;
+
+ // FIXME charsets US-ASCII, ISO-8559-X,
+ public Part(String contentTypeHeader, String transferEncoding,
+ String id, String description, String payload) throws Malformed {
+ this.id = id;
+ this.description = description;
+
+ // NOTE: it is not valid to send a multipart message as base64 or quoted-printable; you must encode each section
+ if (transferEncoding == null) transferEncoding = "7bit";
+ transferEncoding = transferEncoding.toLowerCase();
+ if (transferEncoding.equals("quoted-printable")) {
+ this.payload = MIME.QuotedPrintable.decode(payload, false);
+ subparts = null;
+ } else if (transferEncoding.equals("base64")) {
+ this.payload = new String(Base64.decode(payload));
+ subparts = null;
+ } else if (transferEncoding.equals("7bit") || transferEncoding.equals("8bit") ||
+ transferEncoding.equals("binary")) {
+ this.payload = payload;
+
+ // 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
+ // message/partial -- not supported; see RFC 2046, section 5.2.2
+ // message/external-body -- not supported; see RFC 2046, section 5.2.3
+
+ /* FIXME
+ if (composite) {
+ // FIXME: StringTokenizer doesn't really work like this
+ String boundary = "--" + parameters.get("boundary"); // CRLF, delimiter, optional whitespace, plus CRLF
+ // preceeding CRLF is part of delimiter
+ // boundary (including --) cannot be > 70 chars
+ // delimiter after final part also has a '--' on the end
+ // first part begins with a boundary delimiter
+ StringTokenizer st = new StringTokenizer(payload, boundary);
+ Vec v = new Vec();
+ while(st.hasMoreTokens()) {
+ st.nextToken();
+ Part p = new Part();
+ v.addElement(p);
+ }
+ v.copyInto(subparts = new Part[v.size()]);
+ } else {
+ }
+ */
+ subparts = null;
+ } else {
+ Log.warn(this, "unknown Content-Transfer-Encoding \"" + transferEncoding +
+ "\"; forcing Content-Type to application/octet-stream (originally \""+contentTypeHeader+"\"");
+ contentTypeHeader = "application/octet-stream";
+ this.payload = payload;
+ subparts = null;
+ }
+
+ if (contentTypeHeader == null) {
+ type = "text";
+ subtype = "plain";
+ parameters.put("charset", "us-ascii");
+ } else {
+ contentTypeHeader = contentTypeHeader.trim();
+ if (contentTypeHeader.endsWith(")")) // remove RFC822 comments
+ contentTypeHeader = contentTypeHeader.substring(0, contentTypeHeader.lastIndexOf('('));
+ if (contentTypeHeader.indexOf('/') == -1)
+ throw new Malformed("content-type header lacks a forward slash: \"" + contentTypeHeader + "\"");
+ type = contentTypeHeader.substring(0, contentTypeHeader.indexOf('/')).toLowerCase();
+ contentTypeHeader = contentTypeHeader.substring(contentTypeHeader.indexOf('/') + 1);
+ if (contentTypeHeader.indexOf(';') == -1) {
+ subtype = contentTypeHeader.toLowerCase();
+ } else {
+ subtype = contentTypeHeader.substring(0, contentTypeHeader.indexOf(';')).toLowerCase();
+ StringTokenizer st = new StringTokenizer(contentTypeHeader.substring(contentTypeHeader.indexOf(';') + 1), ";");
+ while(st.hasMoreTokens()) {
+ String key = st.nextToken().trim();
+ if (key.indexOf('=') == -1)
+ throw new 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);
+ }
+ }
+ }
+
+ composite = type.equals("message") || type.equals("multipart");
+ alternative = composite && subtype.equals("alternative");
+
+
+ // fixme: message/rfc822
+
+ // body parts need only contain Content-Foo headers (or none at all); they need not be rfc822 compliant
+
+ }
+ }
// http://www.jwz.org/doc/mid.html
private static final Random random = new Random();