/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- * * The contents of this file are subject to the Mozilla Public License * Version 1.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See * the License for the specific language governing rights and limitations * under the License. * * The Original Code is the Grendel mail/news client. * * The Initial Developer of the Original Code is Netscape Communications * Corporation. Portions created by Netscape are Copyright (C) 1997 * Netscape Communications Corporation. All Rights Reserved. */ package grendel.storage; import calypso.util.NetworkDate; import calypso.util.Assert; import calypso.util.ByteBuf; import java.io.InputStream; import java.io.IOException; import java.io.OutputStream; import java.text.DateFormat; import java.util.Date; import java.util.Enumeration; import java.util.NoSuchElementException; import java.util.Vector; import javax.activation.DataHandler; import javax.mail.Address; import javax.mail.Flags; import javax.mail.Folder; import javax.mail.Message; import javax.mail.MessagingException; import javax.mail.MethodNotSupportedException; import javax.mail.Multipart; import javax.mail.event.MessageChangedEvent; import javax.mail.IllegalWriteException; import javax.mail.Part; import javax.mail.internet.InternetAddress; import javax.mail.internet.InternetHeaders; abstract class MessageBase extends MessageReadOnly implements MessageExtra { /* ********************************************************** Class variables ********************************************************** */ /* Don't confuse these flags with the values used in X-Mozilla-Status headers: they occupy a different space, and range. There is a difference between the in-memory format, and the storage format. This is partially because there are potentially in-memory flags that don't get saved to disk. Parsing of the X-Mozilla-Status header (and conversion to this form) happens in the BerkeleyMessage class. There can be up to 64 flag-bits (the in-memory flags.) There can only be up to 16 X-Mozilla-Status flags (the persistent flags.) */ public static final long FLAG_READ = 0x00000001; public static final long FLAG_REPLIED = 0x00000002; public static final long FLAG_FORWARDED = 0x00000004; public static final long FLAG_MARKED = 0x00000008; public static final long FLAG_DELETED = 0x00000010; public static final long FLAG_HAS_RE = 0x00000020; // Subject public static final long FLAG_SIGNED = 0x00000040; // S/MIME public static final long FLAG_ENCRYPTED = 0x00000080; // S/MIME public static final long FLAG_SMTP_AUTH = 0x00000100; // Gag public static final long FLAG_PARTIAL = 0x00000200; // POP3 public static final long FLAG_QUEUED = 0x00000400; // Offline // The mapping between our internal flag bits and javamail's flag strings. // The editable entry defines whether this flag can be changed from the // outside. static class FlagMap { long flag; Flags.Flag builtin; String non_builtin; boolean editable; FlagMap(long f, String n, boolean e) { flag = f; non_builtin = n; editable = e; } FlagMap(long f, Flags.Flag n, boolean e) { flag = f; builtin = n; editable = e; } }; static FlagMap[] FLAGDEFS = new FlagMap[11]; static { int i = 0; FLAGDEFS[i++] = new FlagMap(FLAG_READ, Flags.Flag.SEEN, true); FLAGDEFS[i++] = new FlagMap(FLAG_REPLIED, Flags.Flag.ANSWERED, true); FLAGDEFS[i++] = new FlagMap(FLAG_FORWARDED, "Forwarded", true); FLAGDEFS[i++] = new FlagMap(FLAG_MARKED, "Marked", true); FLAGDEFS[i++] = new FlagMap(FLAG_DELETED, Flags.Flag.DELETED, true); FLAGDEFS[i++] = new FlagMap(FLAG_HAS_RE, "HasRe", false); FLAGDEFS[i++] = new FlagMap(FLAG_SIGNED, "Signed", false); FLAGDEFS[i++] = new FlagMap(FLAG_ENCRYPTED, "Encrypted", false); FLAGDEFS[i++] = new FlagMap(FLAG_SMTP_AUTH, "SmtpAuth", false); FLAGDEFS[i++] = new FlagMap(FLAG_PARTIAL, "Partial", false); FLAGDEFS[i++] = new FlagMap(FLAG_QUEUED, "Queued", false); Assert.Assertion(i == FLAGDEFS.length); }; // This bit, when set, means that some change has been made to the flags // which should be written out to the X-Mozilla-Status header in the // folder's disk file. This is done so that we can lazily update the // file, rather than writing it every time the flags change. public static final long FLAG_DIRTY = 0x00000800; /* Some string-constants in bytebuf form that we use for interrogating headers during parsing. */ protected static final ByteBuf FROM = new ByteBuf("from"); protected static final ByteBuf TO = new ByteBuf("to"); protected static final ByteBuf CC = new ByteBuf("cc"); protected static final ByteBuf NEWSGROUPS = new ByteBuf("newsgroups"); protected static final ByteBuf DATE = new ByteBuf("date"); protected static final ByteBuf SUBJECT = new ByteBuf("subject"); protected static final ByteBuf MESSAGE_ID = new ByteBuf("message-id"); protected static final ByteBuf REFERENCES = new ByteBuf("references"); protected static final ByteBuf IN_REPLY_TO = new ByteBuf("in-reply-to"); /* For simplifiedDate() */ private static Date scratch_date = new Date(); private static DateFormat date_format = null; /* *********************************************************** Instance variables -- add them sparingly, memory is scarce. *********************************************************** */ long flags; // see `FLAG_READ', etc, above. long sentDate; // milliseconds since the Epoch. // These slots are ints but really represent ByteStrings: they are indexes // into a ByteStringTable. It's quite likely that we could live with these // being of type `short' instead of `int'. Should memory usage be a problem, // we should consider that. // int author_name; // name (not address) of the From or Sender. int recipient_name; // name of first To, or CC, or newsgroup. int subject; // subject minus "Re:" (see `FLAG_HAS_RE'). // These slots are as above, but represent MessageID objects instead of // ByteString objects. These also could stand to be of type `short'. // int message_id; // will never be -1 (meaning null). int references[]; // may be null; else length > 0. /* ********************************************************** Methods ********************************************************** */ MessageBase(FolderBase f) { super(); this.folder = f; } MessageBase(FolderBase f, InternetHeaders h) { this(f); initialize(f, h); } MessageBase(FolderBase f, long date, long flags, ByteBuf author, ByteBuf recipient, ByteBuf subj, ByteBuf id, ByteBuf refs[]) { this(f); ByteStringTable string_table = f.getStringTable(); MessageIDTable id_table = f.getMessageIDTable(); if (id == null || id.length() == 0) { // #### In previous versions, we did this by getting the MD5 hash // #### of the whole header block. We should do that here too... if (id == null) id = new ByteBuf(); id.append(grendel.util.MessageIDGenerator.generate("missing-id")); } this.folder = f; this.flags = flags; this.sentDate = date; this.author_name = string_table.intern(author); this.recipient_name = string_table.intern(recipient); this.subject = string_table.intern(subj); this.message_id = id_table.intern(id); if (refs == null || refs.length == 0) this.references = null; else { int L = refs.length; references = new int[L]; for (int i = 0; i < L; i++) references[i] = id_table.intern(refs[i]); } } MessageBase(FolderBase f, long date, long flags, ByteBuf author, ByteBuf recipient, ByteBuf subj, MessageID id, MessageID refs[]) { this(f); ByteStringTable string_table = f.getStringTable(); MessageIDTable id_table = f.getMessageIDTable(); if (id != null) { this.message_id = id_table.intern(id); } else { // #### In previous versions, we did this by getting the MD5 hash // #### of the whole header block. We should do that here too... ByteBuf b = new ByteBuf(grendel.util.MessageIDGenerator.generate("missing-id")); this.message_id = id_table.intern(b); } this.folder = f; this.flags = flags; this.sentDate = date; this.author_name = string_table.intern(author); this.recipient_name = string_table.intern(recipient); this.subject = string_table.intern(subj); if (refs == null || refs.length == 0) this.references = null; else { int L = refs.length; references = new int[L]; for (int i = 0; i < L; i++) references[i] = id_table.intern(refs[i]); } } protected void initialize(Folder f, InternetHeaders h) { folder = f; FolderBase fb = (FolderBase) f; ByteStringTable string_table = fb.getStringTable(); MessageIDTable id_table = fb.getMessageIDTable(); String hh[]; hh = h.getHeader("From"); author_name = (hh == null || hh.length == 0 ? -1 : string_table.intern(hh[0].trim())); // #### need an address parser here... recipient_name = -1; /* hh = h.getHeader("To"); // #### deal with multiple to fields recipient_name = (hh == null || hh.length == 0 ? -1 : string_table.intern(hh[0].trim())); if (recipient == -1) { // #### deal with multiple cc fields hh = h.getHeader("CC"); recipient_name = (hh == null || hh.length == 0 ? -1 : string_table.intern(hh[0].trim())); } if (recipient == -1) { hh = h.getHeader("Newsgroups"); recipient_name = (hh == null || hh.length == 0 ? -1 : string_table.intern(hh[0].trim())); } */ hh = h.getHeader("Subject"); if (hh != null && hh.length != 0) { // Much of this code is duplicated in MessageExtraFactory. Sigh. ### ByteBuf value = new ByteBuf(hh[0]); if (value.length() > 2 && (value.byteAt(0) == 'r' || value.byteAt(0) == 'R') && (value.byteAt(1) == 'e' || value.byteAt(1) == 'E')) { byte c = value.byteAt(2); if (c == ':') { value.remove(0, 3); // Skip over "Re:" value.trim(); // Remove any whitespace after colon flags |= FLAG_HAS_RE; // yes, we found it. } else if (c == '[' || c == '(') { int i = 3; // skip over "Re[" or "Re(" // Skip forward over digits after the "[" or "(". int length = value.length(); while (i < length && value.byteAt(i) >= '0' && value.byteAt(i) <= '9') { i++; } // Now ensure that the following thing is "]:" or "):" // Only if it is do we treat this all as a "Re"-ish thing. if (i < (length-1) && (value.byteAt(i) == ']' || value.byteAt(i) == ')') && value.byteAt(i+1) == ':') { value.remove(0, i+2); // Skip the whole thing. value.trim(); // Remove any whitespace after colon flags |= FLAG_HAS_RE; // yes, we found it. } } } subject = string_table.intern(value); } hh = h.getHeader("Date"); if (hh != null && hh.length != 0) sentDate = NetworkDate.parseLong(new ByteBuf(hh[0]), true); hh = h.getHeader("Message-ID"); if (hh != null && hh.length != 0) { ByteBuf value = new ByteBuf(hh[0]); value.trim(); int length = value.length(); if (length > 0 && value.byteAt(0) == '<' && value.byteAt(length-1) == '>') { value.remove(length-1, length); value.remove(0, 1); } message_id = id_table.intern(value.trim()); } // There must be a message ID on every message. if (message_id == -1) { // #### In previous versions, we did this by getting the MD5 hash // #### of the whole header block. We should do that here too... String id = grendel.util.MessageIDGenerator.generate("missing-id"); message_id = id_table.intern(new ByteBuf(id)); } hh = h.getHeader("References"); if (hh != null && hh.length != 0) { ByteBuf value = new ByteBuf(hh[0]); references = internReferences(id_table, value.trim()); } // Only examine the In-Reply-To header if there is no References header. if (references == null) { hh = h.getHeader("In-Reply-To"); if (hh != null && hh.length != 0) { ByteBuf value = new ByteBuf(hh[0]); references = internReferences(id_table, value.trim()); } } } // Ported from akbar's "msg_intern_references", mailsum.c. protected int[] internReferences(MessageIDTable id_table, ByteBuf refs) { byte data[] = refs.toBytes(); int length = refs.length(); int s; int n_refs = 0; for (s=0 ; s') { n_refs++; } } if (n_refs == 0) return null; int result[] = new int[n_refs]; int start = 0; int cur = 0; s = 0; while (s < length) { // The old way was to skip over whitespace, then skip an optional "<". // The new way is to skip over everything up to and including "<". // This lets us deal better with In-Reply-To headers, in addition to // References headers: we can cope with // // In-Reply-To: NAME's message of TIME // In-Reply-To: article of TIME // In-Reply-To: , from NAME // // In the latter case, we're going to fuck up and think that both // of them are IDs, but headers like appear to be extremely rare. // In a survey of 22,950 mail messages with In-Reply-To headers: // // 18,396 had at least one occurence of <>-bracketed text. // 4,554 had no <>-bracketed text at all (just names and dates.) // 714 contained one <>-bracketed addr-spec and no message IDs. // 4 contained multiple message IDs. // 1 contained one message ID and one <>-bracketed addr-spec. // // The most common forms of In-Reply-To seem to be // // 31% NAME's message of TIME // 22% // 9% from NAME at "TIME" // 8% USER's message of TIME // 7% USER's message of TIME // 6% Your message of "TIME" // 17% hundreds of other variants (average 0.4% each?) // // jwz, 17 Sep 1997. // while (start < length && data[start] != '<') start++; // skip over consecutive "<" -- I've seen "<>". while (start < length && data[start] == '<') start++; s = start; while (s < length && data[s] != '>') s++; if (s > start && s < length && data[s] == '>') { result[cur++] = id_table.intern(data, start, s - start); start = s + 1; // skip over consecutive ">" -- I've seen "<>". while (start < length && data[start] == '>') start++; } else { s++; } } if (cur != n_refs) { // Whoops! Something's funny about this line, and the number of // ">" characters didn't equal the number of IDs we extracted. // This will be an extremely rare situation, so when it happens, // just make a new array. if (cur == 0) result = null; else { int r2[] = new int[cur]; System.arraycopy(result, 0, r2, 0, cur); result = r2; } } return result; } public Object getMessageID() { MessageIDTable id_table = ((FolderBase)folder).getMessageIDTable(); return (MessageID) id_table.getObject(message_id); } public String getSubject() { String result = simplifiedSubject(); if (subjectIsReply()) result = "Re: " + result; return result; } public String getAuthor() { ByteStringTable string_table = ((FolderBase)folder).getStringTable(); ByteString a = (ByteString) string_table.getObject(author_name); if (a == null) return ""; else return a.toString(); } public String getRecipient() { ByteStringTable string_table = ((FolderBase)folder).getStringTable(); ByteString r = (ByteString) string_table.getObject(recipient_name); if (r == null) return ""; else return r.toString(); } public Object[] messageThreadReferences() { if (references == null) return null; int count = references.length; if (count == 0) return null; // Note: this conses. MessageIDTable id_table = ((FolderBase)folder).getMessageIDTable(); Object result[] = new Object[count]; for (int i = 0; i < result.length; i++) result[i] = id_table.getObject(references[i]); return result; } public long getSentDateAsLong() { return sentDate; } public Date getSentDate() { return new Date(sentDate); } public Date getReceivedDate() { // ### We don't currently remember this info. Should we? return getSentDate(); } public Folder getFolder() { return folder; } // #### Warning, this is untested -- the "javax.mail.Flags" class changed // around a bunch since the last time we tried to use this code, and I had // to beat on this. public Flags getFlags() { Flags result = new Flags(); for (int i=0 ; i