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(); }
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);
} else if (key.startsWith("Return-Path")) {
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;
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 : Address.parse((String)headers.get("Reply-To"));
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();