From: adam Date: Tue, 20 Jul 2004 08:49:29 +0000 (+0000) Subject: total overhaul; were using MIME and MIME.Part now X-Git-Url: http://git.megacz.com/?a=commitdiff_plain;h=53487d9d2a16e5b6274f0fcb2d4886737079ff08;p=org.ibex.mail.git total overhaul; were using MIME and MIME.Part now darcs-hash:20040720084929-5007d-4bcc298f7ccca08423905804030fcae544d99f09.gz --- diff --git a/src/org/ibex/mail/Address.java b/src/org/ibex/mail/Address.java index 0cbc494..041c598 100644 --- a/src/org/ibex/mail/Address.java +++ b/src/org/ibex/mail/Address.java @@ -39,4 +39,16 @@ public class Address extends JSReflection { Log.warn(this, "returning false"); return false; } + + public static Address[] list(String spec) { + if (spec == null) return new Address[] { }; + StringTokenizer st = new StringTokenizer((String)spec, ","); + Address[] ret = new Address[st.countTokens()]; + for(int i=0; i hex representation, must be uppercase @@ -20,4 +63,148 @@ public class MIME { 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; + 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); + 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.get("mime-version")!=null&&headers.get("mime-version").trim().equals("1.0")); + String ctype = headers.get("content-type"); + String 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"))) { + Log.warn(MIME.class, "unknown TransferEncoding \"" + encoding + "\""); + ctype = "application/octet-stream"; + } + content = new Content(ctype, headers.get("content-description"), headers.get("content-id"), encoding); + if (content.composite) { subparts = parseParts(stream); body = null; last = false; return; } + subparts = null; + boolean last = false; + 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); + } + 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; + } + } + + public static class Headers extends org.ibex.js.JSReflection { + private Hashtable head = new Hashtable(); + public final String raw; + public String get(String s) { return (String)head.get(s.toLowerCase()); } + public static String uncomment(String val) { + boolean inquotes = false; + for(int i=0; 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(); + + Enumeration e = head.keys(); + boolean mime = assumeMime | (get("mime-version") != null && get("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); + } + } + } } diff --git a/src/org/ibex/mail/Message.java b/src/org/ibex/mail/Message.java index 5fd2e2f..66c4188 100644 --- a/src/org/ibex/mail/Message.java +++ b/src/org/ibex/mail/Message.java @@ -7,8 +7,7 @@ import java.util.*; import java.net.*; import java.io.*; -// FIXME this is important -// folded headers: can insert CRLF anywhere that whitespace appears (before the whitespace) +// FIXME this is important: folded headers: can insert CRLF anywhere that whitespace appears (before the whitespace) // soft line limit (suggested): 78 chars / hard line limit: 998 chars // date/time parsing: see spec, 3.3 @@ -20,50 +19,108 @@ import java.io.*; // FEATURE: delivery status notification (and the sneaky variety) // FEATURE: threading as in http://www.jwz.org/doc/threading.html // FEATURE: lazy body +// FIXME RFC822 1,000-char limit per line /** * [immutable] This class encapsulates a message "floating in the * ether": RFC2822 data but no storage-specific flags or other * metadata. */ -public class Message extends org.ibex.js.JSReflection { +public class Message extends MIME.Part { + + // Envelope ////////////////////////////////////////////////////////////////////////////// + + public final Envelope envelope; + public static class Envelope { + public Envelope(Address to, Address from, Date arrival) { this.to = to; this.from = from; this.arrival = arrival; } + public final Date arrival; + public final Address to; + public final Address from; + public static Envelope augment(Envelope e, MIME.Headers h) { + if (e.from != null && e.to != null) return e; + Address to = e.to == null ? Address.parse(h.get("X-org.ibex.mail.headers.envelope.To")) : e.to; + Address from = e.from == null ? Address.parse(h.get("X-org.ibex.mail.headers.envelope.From")) : e.from; + return new Envelope(to, from, e.arrival); + } + } + - public final String allHeaders; // pristine headers - public final CaseInsensitiveHash headers; // hash of headers (not including resent's and traces) - public final String body; // entire body - public final int lines; // lines in the body + // Parsed Headers ////////////////////////////////////////////////////////////////////////////// - public final Date date; - public final Address to; - public final Address from; // if multiple From entries, this is sender - public final Address replyto; // if none provided, this is equal to sender - public final String subject; - public final String messageid; - public final Address[] cc; - public final Address[] bcc; + public final Address to; + public final Address from; // if multiple From entries, this is sender + public final Date date; + public final Address replyto; // if none provided, this is equal to sender + public final String subject; + public final String messageid; + public final Address[] cc; + public final Address[] bcc; public final Hashtable[] resent; - public final Trace[] traces; + public final int lines; // lines in the body FIXME not accurate anymore + + + public Message(Stream stream, Envelope envelope) throws Malformed { + super(stream, null, false); + Vec resent = new Vec(), traces = new Vec(); + this.envelope = Envelope.augment(envelope, headers); + this.to = headers.get("To") == null ? envelope.to : Address.parse(headers.get("To")); + this.from = headers.get("From") == null ? envelope.from : Address.parse(headers.get("From")); + this.replyto = headers.get("Reply-To") == null ? null : Address.parse(headers.get("Reply-To")); + this.subject = headers.get("Subject"); + this.messageid = headers.get("Message-Id"); + this.cc = Address.list(headers.get("Cc")); + this.bcc = Address.list(headers.get("BCc")); + this.date = parseDate(headers.get("Date")); + this.lines = 0; /* FIMXE */ + resent.copyInto(this.resent = new Hashtable[resent.size()]); + traces.copyInto(this.traces = new Trace[traces.size()]); + } - public final Address envelopeFrom; - public final Address envelopeTo; - public final boolean mime; // true iff Mime-Version is 1.0 + // Helpers ///////////////////////////////////////////////////////////////////////////// - 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; } + // http://www.jwz.org/doc/mid.html + private static final Random random = new Random(); + public static String generateFreshMessageId() { + StringBuffer ret = new StringBuffer(); + ret.append('<'); + ret.append(Base36.encode(System.currentTimeMillis())); + ret.append('.'); + ret.append(Base36.encode(random.nextLong())); + ret.append('.'); + try { ret.append(InetAddress.getLocalHost().getHostName()); } catch (UnknownHostException e) { /* DELIBERATE */ } + ret.append('>'); + return ret.toString(); + } + + public static Date parseDate(String s) { return null; } // FIXME!!! + + // use null-sender for error messages (don't send errors to the null addr) + public Message bounce(String reason) { throw new RuntimeException("bounce not implemented"); } // FIXME! + + public String summary() { + return + " Subject: " + subject + "\n" + + " EnvelopeFrom: " + envelope.from + "\n" + + " EnvelopeTo: " + envelope.to + "\n" + + " MessageId: " + messageid; + } public void dump(Stream s) { 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(headers.raw); s.println(); s.println(body); s.flush(); } + public int size() { return headers.raw.length() + 2 /* CRLF */ + body.length(); } + public String toString() { return headers.raw + "\r\n" + body; } + + + // SMTP Traces ////////////////////////////////////////////////////////////////////////////// + + public final Trace[] traces; public class Trace { final String returnPath; final Element[] elements; @@ -104,264 +161,13 @@ public class Message extends org.ibex.js.JSReflection { public static class Malformed extends Exception { public Malformed(String s) { super(s); } } - 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 key = null; - StringBuffer all = new StringBuffer(); - Date date = null; - Address to = null, from = null, replyto = null; - 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.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.trim()); - continue; - } - if (s.indexOf(':') == -1) throw new Malformed("Header line does not contain colon: " + s); - key = s.substring(0, s.indexOf(':')); - for(int i=0; i 126) - throw new Malformed("Header key \""+key+"\" contains invalid character \"" + key.charAt(i) + "\""); - String val = s.substring(s.indexOf(':') + 1).trim(); - while(val.length() > 0 && Character.isSpace(val.charAt(0))) val = val.substring(1); + /* if (key.startsWith("Resent-")) { 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.unread(s); traces.addElement(new Trace(stream)); - } else if (key.equals("X-org.ibex.mail.headers.envelope.From")) { - 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")) { - 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; - ((CaseInsensitiveHash)headers).add(key, val); - } - } - - // FIXME what if all are null? - 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 : 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"), ","); - this.cc = new Address[st.countTokens()]; - for(int i=0; i 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(); - public static String generateFreshMessageId() { - StringBuffer ret = new StringBuffer(); - ret.append('<'); - ret.append(Base36.encode(System.currentTimeMillis())); - ret.append('.'); - ret.append(Base36.encode(random.nextLong())); - ret.append('.'); - try { ret.append(InetAddress.getLocalHost().getHostName()); } catch (UnknownHostException e) { /* DELIBERATE */ } - ret.append('>'); - return ret.toString(); - } - - // use null-sender for error messages (don't send errors to the null addr) - public Message bounce(String reason) { throw new RuntimeException("bounce not implemented"); } // FIXME! - - public static class CaseInsensitiveHash extends org.ibex.js.JSReflection { - private Hashtable stuff = new Hashtable(); - public Object get(Object o) { return stuff.get(((String)o).toLowerCase()); } - void add(Object k, Object v) { if (k instanceof String) stuff.put(((String)k).toLowerCase(), v); else stuff.put(k, v); } - } - - public String summary() { - return - " Subject: " + subject + "\n" + - " EnvelopeFrom: " + envelopeFrom + "\n" + - " EnvelopeTo: " + envelopeTo + "\n" + - " MessageId: " + messageid; - } -} + stream.unread(s); + traces.addElement(new Trace(stream)); + */ diff --git a/src/org/ibex/mail/Query.java b/src/org/ibex/mail/Query.java index 560ac18..2865dbd 100644 --- a/src/org/ibex/mail/Query.java +++ b/src/org/ibex/mail/Query.java @@ -91,14 +91,14 @@ public class Query { else return it.num() >= min && it.num() <= max; case SENT: return (latest==null||it.cur().date.before(latest)) && (earliest==null||it.cur().date.after(earliest)); - case ARRIVAL: return (latest == null || it.cur().arrival.before(latest)) && - (earliest == null || it.cur().arrival.after(earliest)); + case ARRIVAL: return (latest == null || it.cur().envelope.arrival.before(latest)) && + (earliest == null || it.cur().envelope.arrival.after(earliest)); case SIZE: return it.cur().size() >= min && it.cur().size() <= max; case HEADER: return it.cur().headers.get(key) != null && ((String)it.cur().headers.get(key)).toLowerCase().indexOf(text.toLowerCase()) != -1; case BODY: return it.cur().body.toLowerCase().indexOf(text.toLowerCase()) != -1; case FULL: return it.cur().body.toLowerCase().indexOf(text.toLowerCase()) != -1 || - it.cur().allHeaders.indexOf(text) != -1; + it.cur().headers.raw.indexOf(text) != -1; case DELETED: return it.deleted(); case SEEN: return it.seen(); case FLAGGED: return it.flagged(); diff --git a/src/org/ibex/mail/protocol/IMAP.java b/src/org/ibex/mail/protocol/IMAP.java index a354445..e143420 100644 --- a/src/org/ibex/mail/protocol/IMAP.java +++ b/src/org/ibex/mail/protocol/IMAP.java @@ -158,7 +158,7 @@ public class IMAP { public void delete(Mailbox m) { if (!m.equals(inbox)) m.destroy(false); else throw new Bad("can't delete inbox"); } public void create(String m) { mailbox(m, true); } public void append(String m,int f,Date a,String b) { try { - mailbox(m,false).add(new Message(null,null,b,a),f|Mailbox.Flag.RECENT); + mailbox(m,false).add(new Message(new Stream(b), new Message.Envelope(null,null,a)),f|Mailbox.Flag.RECENT); } catch (Message.Malformed e) { throw new No(e.getMessage()); } } public void check() { } public void noop() { } @@ -388,10 +388,10 @@ public class IMAP { if (s.equals("BODYSTRUCTURE")) { spec|=BODYSTRUCTURE;if(e){r.append(" ");r.append(Printer.bodystructure(m));} } else if (s.equals("ENVELOPE")) { spec|=ENVELOPE; if(e){r.append(" ");r.append(Printer.envelope(m));} } else if (s.equals("FLAGS")) { spec|=FLAGS; if(e){r.append(" ");r.append(Printer.flags(flags));} - } else if (s.equals("INTERNALDATE")) { spec|=INTERNALDATE; if(e){r.append(" ");r.append(Printer.date(m.arrival));} + } else if (s.equals("INTERNALDATE")) { spec|=INTERNALDATE; if(e){r.append(" ");r.append(Printer.date(m.envelope.arrival));} } else if (s.equals("RFC822")) { spec|=RFC822; if(e){r.append(" ");r.append(Printer.message(m));} } else if (s.equals("RFC822.TEXT")) { spec|=RFC822TEXT; if(e){r.append(" ");r.append(Printer.qq(m.body));} - } else if (s.equals("RFC822.HEADER")){ spec|=HEADER;if(e){r.append(" ");r.append(Printer.qq(m.allHeaders+"\r\n"));} + } else if (s.equals("RFC822.HEADER")){ spec|=HEADER;if(e){r.append(" ");r.append(Printer.qq(m.headers.raw+"\r\n"));} } else if (s.equals("RFC822.SIZE")) { spec|=RFC822SIZE; if(e){r.append(" ");r.append(m.size());} } else if (s.equals("UID")) { spec|=UID; if(e){r.append(" ");r.append(muid); } } else if (!(s.equals("BODY.PEEK") || s.equals("BODY"))) { throw new Server.No("unknown fetch argument: " + s); @@ -408,10 +408,10 @@ public class IMAP { Parser.Token[] list = t[++i].l(); s = list.length == 0 ? "" : list[0].s.toUpperCase(); r.append(s); - if (list.length == 0) { spec |= RFC822TEXT; if(e) payload = m.allHeaders+"\r\n"+m.body; } - else if (s.equals("")) { spec |= RFC822TEXT; if(e) payload = m.allHeaders+"\r\n"+m.body; } + if (list.length == 0) { spec |= RFC822TEXT; if(e) payload = m.headers.raw+"\r\n"+m.body; } + else if (s.equals("")) { spec |= RFC822TEXT; if(e) payload = m.headers.raw+"\r\n"+m.body; } else if (s.equals("TEXT")) { spec |= RFC822TEXT; if(e) payload = m.body; } - else if (s.equals("HEADER")) { spec |= HEADER; if(e) payload = m.allHeaders+"\r\n"; } + else if (s.equals("HEADER")) { spec |= HEADER; if(e) payload = m.headers.raw+"\r\n"; } else if (s.equals("HEADER.FIELDS")) { spec |= FIELDS; payload=headers(r,t[i].l()[1].sl(),false,m,e); } else if (s.equals("HEADER.FIELDS.NOT")) { spec |= FIELDSNOT; payload=headers(r,t[i].l()[1].sl(),true,m,e); } else if (s.equals("MIME")) { throw new Server.Bad("MIME not supported"); } @@ -744,7 +744,7 @@ public class IMAP { static String date(Date d) { return d.toString(); } static String envelope(Message m) { return - "(" + quotify(m.arrival.toString()) + + "(" + quotify(m.envelope.arrival.toString()) + " " + quotify(m.subject) + " " + addressList(m.from) + " " + addressList(m.headers.get("sender")) + diff --git a/src/org/ibex/mail/protocol/NNTP.java b/src/org/ibex/mail/protocol/NNTP.java index ef5fc0c..6f0f6b9 100644 --- a/src/org/ibex/mail/protocol/NNTP.java +++ b/src/org/ibex/mail/protocol/NNTP.java @@ -104,7 +104,7 @@ public class NNTP { else a = api.article(Integer.parseInt(s), head, body); int code = (head && body) ? 220 : head ? 221 : body ? 222 : 223; conn.println(code + " " + a.num + " <" + a.messageid + "> get ready for some stuff..."); - if (head) conn.println(a.message.allHeaders); + if (head) conn.println(a.message.headers.raw); if (head && body) conn.println(); if (body) conn.println(a.message.body); conn.println("."); diff --git a/src/org/ibex/mail/protocol/SMTP.java b/src/org/ibex/mail/protocol/SMTP.java index bad681a..0c001ef 100644 --- a/src/org/ibex/mail/protocol/SMTP.java +++ b/src/org/ibex/mail/protocol/SMTP.java @@ -68,8 +68,8 @@ public class SMTP { String body = buf.toString(); Message m = null; for(int i=0; i 1000 * 60 * 60 * 24 * 5) { + if (new Date().getTime() - m.envelope.arrival.getTime() > 1000 * 60 * 60 * 24 * 5) { Log.warn(SMTP.Outgoing.class, "could not send message after 5 days; bouncing it\n" + m.summary()); accept(m.bounce("could not send for 5 days")); return true; @@ -134,8 +134,8 @@ public class SMTP { Log.info(SMTP.Outgoing.class, "connected"); check(conn.readln(), conn); // banner conn.println("HELO " + conn.vhost); check(conn.readln(), conn); - conn.println("MAIL FROM:<" + m.envelopeFrom.user + "@" + m.envelopeFrom.host+">"); check(conn.readln(), conn); - conn.println("RCPT TO:<" + m.envelopeTo.user + "@" + m.envelopeTo.host+">"); check(conn.readln(), conn); + conn.println("MAIL FROM:<" + m.envelope.from.user + "@" + m.envelope.from.host+">"); check(conn.readln(), conn); + conn.println("RCPT TO:<" + m.envelope.to.user + "@" + m.envelope.to.host+">"); check(conn.readln(), conn); conn.println("DATA"); check(conn.readln(), conn); conn.println(m.toString()); conn.println("."); diff --git a/src/org/ibex/mail/target/FileBasedMailbox.java b/src/org/ibex/mail/target/FileBasedMailbox.java index d1e0a5e..a8e2d53 100644 --- a/src/org/ibex/mail/target/FileBasedMailbox.java +++ b/src/org/ibex/mail/target/FileBasedMailbox.java @@ -103,7 +103,10 @@ public class FileBasedMailbox extends Mailbox.Default { File target = new File(name); File f = new File(target.getCanonicalPath() + "-"); FileOutputStream fo = new FileOutputStream(f); - message.dump(new Stream(fo)); + Stream stream = new Stream(fo); + stream.println("X-org.ibex.mail.headers.envelope.From: " + message.envelope.from); + stream.println("X-org.ibex.mail.headers.envelope.To: " + message.envelope.to); + message.dump(stream); fo.close(); f.renameTo(target); Log.info(this, " done writing."); @@ -122,7 +125,21 @@ public class FileBasedMailbox extends Mailbox.Default { try { File file = new File(path + File.separatorChar + names[cur]); FileInputStream fis = new FileInputStream(file); - Message ret = new Message(null, null, new Stream(fis)); + Stream stream = new Stream(fis); + Address envelopeFrom = null; + Address envelopeTo = null; + for(String s = stream.readln(); s != null; s = stream.readln()) { + if (s.startsWith("X-org.ibex.mail.headers.envelope.From: ")) + envelopeFrom = Address.parse(s.substring(38).trim()); + else if (s.startsWith("X-org.ibex.mail.headers.envelope.To: ")) + envelopeTo = Address.parse(s.substring(36).trim()); + else { + stream.unread(s + "\r\n"); + break; + } + } + Message ret = new Message(stream, + new Message.Envelope(envelopeFrom, envelopeTo, new Date(file.lastModified()))); fis.close(); return ret; } catch (IOException e) { throw new MailException.IOException(e); diff --git a/src/org/ibex/mail/target/Script.java b/src/org/ibex/mail/target/Script.java index f5e7840..42b878b 100644 --- a/src/org/ibex/mail/target/Script.java +++ b/src/org/ibex/mail/target/Script.java @@ -124,7 +124,8 @@ public class Script extends Target { if (key.equals("body")) body = val; else headers.append(key + ": " + val + "\r\n"); } - Message message = new Message(null, null, new org.ibex.io.Stream(headers.toString() + "\r\n" + body)); + Message message = new Message(new org.ibex.io.Stream(headers.toString() + "\r\n" + body), + new Message.Envelope(null, null, new Date())); //org.ibex.mail.protocol.SMTP.Outgoing.accept(message); boolean ok = org.ibex.mail.protocol.SMTP.Outgoing.attempt(message); if (!ok) throw new JSExn("SMTP server rejected message"); @@ -133,9 +134,10 @@ public class Script extends Target { if (name.equals("mail.forward")) { return new Target() { public void accept(Message m) throws MailException { try { - Message m2 = new Message(m.envelopeFrom, - new Address(a.toString()), - new org.ibex.io.Stream(m.toString())); + Message m2 = new Message(new org.ibex.io.Stream(m.toString()), + new Message.Envelope(m.envelope.from, + new Address(a.toString()), + new Date())); org.ibex.mail.protocol.SMTP.Outgoing.accept(m2); } catch (Exception e) { throw new MailException(e.toString());