mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-12-27 19:09:47 +00:00
535 lines
19 KiB
Java
535 lines
19 KiB
Java
/* -*- 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.
|
|
*
|
|
* Created: Jamie Zawinski <jwz@netscape.com>, 28 Sep 1997.
|
|
*/
|
|
|
|
package grendel.storage;
|
|
|
|
import calypso.util.ByteBuf;
|
|
import calypso.util.Assert;
|
|
import java.util.Vector;
|
|
import java.util.Enumeration;
|
|
import java.util.Hashtable;
|
|
import java.io.File;
|
|
import java.io.InputStream;
|
|
import java.io.OutputStream;
|
|
import java.io.BufferedInputStream;
|
|
import java.io.FileInputStream;
|
|
import java.io.FileOutputStream;
|
|
import java.io.BufferedOutputStream;
|
|
import java.io.RandomAccessFile;
|
|
import java.io.IOException;
|
|
|
|
/** This class knows how to read and write a mail folder summary file.
|
|
This is a different format than that used by any previous version of
|
|
Mozilla; it's similar to the 2.0/3.0 format, and totally unlike the
|
|
4.0/5.0 format.
|
|
*/
|
|
|
|
/* The format is as follows:
|
|
|
|
MAGIC_NUMBER VERSION FOLDER_SIZE FOLDER_DATE PARSED_THRU NMSGS NVALIDMSGS
|
|
NUNREADMSGS DELETED_BYTES STRING_TABLE ID_TABLE [ MSG_DESC ]*
|
|
|
|
MAGIC_NUMBER := '# Netscape folder cache\r\n'
|
|
VERSION := uint32 (file format version number; this is version 6)
|
|
|
|
FOLDER_SIZE := uint32 (the size of the folder itself, so that we can
|
|
easily detect an out-of-date summary file)
|
|
FOLDER_DATE := uint32 (time_t of the folder; same purpose as above)
|
|
PARSED_THRU := uint32 (the last position in the folder that we have
|
|
parsed data for. Normally the same as
|
|
FOLDER_SIZE, but if we append messages to a file,
|
|
we can just update FOLDER_SIZE and FOLDER_SIZE and
|
|
use this field to tell us what part of the
|
|
folder needs to be parsed.)
|
|
NMSGS := uint32 (total number of messages in this folder, including
|
|
expunged messages)
|
|
NVALIDMSGS := uint32 (total number of non-expunged messages in this
|
|
folder)
|
|
NUNREADMSGS := uint32 (total number of unread messages in this folder)
|
|
DELETED_BYTES := uint32 (number of bytes of messages which have been
|
|
deleted from the summary, but not yet compacted out
|
|
of the folder file itself. The messages in question
|
|
have the DELETED bit set in their flags.)
|
|
|
|
STRING_TABLE := ST_LENGTH [ ST_STRING ]+
|
|
ST_LENGTH := uint32
|
|
ST_STRING := null terminated string
|
|
|
|
ID_TABLE := ID_LENGTH [ MSG_ID ]+
|
|
ID_LENGTH := uint32
|
|
MSG_ID := uint64 (64-bit hash of a Message-ID string)
|
|
|
|
MSG_DESC := AUTHOR RECIPIENT SUBJECT DATE ID REFERENCES
|
|
FLAGS POS LENGTH
|
|
AUTHOR := SINDEX (index into STRING_TABLE)
|
|
RECIPIENT := SINDEX (index into STRING_TABLE)
|
|
SUBJECT := SINDEX (index into STRING_TABLE)
|
|
DATE := uint32 (time_t)
|
|
FLAGS := uint32 (bitfield of other attributes of the message;
|
|
this is in `X-Mozilla-Status' format.)
|
|
POS := uint32 (byte index into file)
|
|
LENGTH := uint32 (length in bytes from envelope to end of last line)
|
|
ID := IINDEX (index into ID_TABLE)
|
|
REFERENCES := RLENGTH [ ID ]*
|
|
RLENGTH := uint16
|
|
|
|
SINDEX := uint16 -or- uint32 (depending on whether ST_LENGTH is
|
|
able to be represented with 16 bits.)
|
|
IINDEX := uint16 -or- uint32 (same, but for ID_LENGTH.)
|
|
*/
|
|
|
|
class MailSummaryFileGrendel extends MailSummaryFile {
|
|
|
|
MailSummaryFileGrendel(BerkeleyFolder folder) {
|
|
super(folder);
|
|
}
|
|
|
|
/** Parses a Grendel (version 6) mail summary file, and adds the described
|
|
messages to the associated Folder object.
|
|
*/
|
|
synchronized long readSummaryFile(InputStream sum) throws IOException {
|
|
|
|
// At the point at which this method is called, MAGIC_NUMBER and VERSION
|
|
// have already been read.
|
|
|
|
long folder_size = readUnsignedInteger(sum);
|
|
long folder_date = readUnsignedInteger(sum);
|
|
long old_thru = readUnsignedInteger(sum); // old parsed_thru
|
|
long nmsgs = readUnsignedInteger(sum);
|
|
long nvalidmsgs = readUnsignedInteger(sum);
|
|
long nunreadmsgs = readUnsignedInteger(sum);
|
|
long deletedbytes = readUnsignedInteger(sum);
|
|
|
|
boolean pessimistic = false;
|
|
|
|
if (!checkFolderDate(folder_date)) {
|
|
System.err.println("warning: folder " + folder.getName() +
|
|
" is newer than summary file" +
|
|
// " (" + this.file_date + ", " +
|
|
// folder_date*1000L + ")" +
|
|
"; attempting salvage.");
|
|
pessimistic = true;
|
|
|
|
} else if (!checkFolderSize(folder_size)) {
|
|
System.err.println("warning: folder " + folder.getName() + " is not " +
|
|
folder_size + " bytes, as summary file expected; " +
|
|
"\n\t attempting salvage.");
|
|
pessimistic = true;
|
|
}
|
|
|
|
|
|
long string_table_length = readUnsignedInteger(sum);
|
|
ByteBuf string_table[] = (pessimistic
|
|
? null
|
|
: new ByteBuf[(int) string_table_length]);
|
|
|
|
// Read N null-terminated strings.
|
|
for (int i = 0; i < string_table_length; i++) {
|
|
ByteBuf bb = new ByteBuf();
|
|
for (int j = 0;; j++) {
|
|
int b = sum.read();
|
|
if (b == 0) break;
|
|
if (b == -1) throw new IOException();
|
|
bb.append((byte) b);
|
|
}
|
|
if (!pessimistic)
|
|
string_table[i] = bb;
|
|
}
|
|
|
|
long id_table_length = readUnsignedInteger(sum);
|
|
MessageID id_table[] = new MessageID[(int) id_table_length];
|
|
|
|
// Read N 64-bit numbers (hashes of the Message-IDs.)
|
|
for (int i = 0; i < id_table_length; i++) {
|
|
long b0 = readUnsignedInteger(sum);
|
|
long b1 = readUnsignedInteger(sum);
|
|
id_table[i] = new MessageID((b0 << 32) | b1);
|
|
}
|
|
|
|
Vector msgs = (pessimistic
|
|
? null
|
|
: new Vector((int) nmsgs));
|
|
|
|
if (pessimistic)
|
|
salvage = new Hashtable((int) nmsgs);
|
|
|
|
boolean success = false;
|
|
try { // if we fail for some reason, set up the `salvage' table
|
|
|
|
// Read N message descriptors.
|
|
for (int i = 0; i < nmsgs; i++) {
|
|
|
|
long author, recipient, subject, id;
|
|
if (string_table_length <= 0xFFFF) {
|
|
author = readUnsignedShort(sum);
|
|
recipient = readUnsignedShort(sum);
|
|
subject = readUnsignedShort(sum);
|
|
} else {
|
|
author = readUnsignedInteger(sum);
|
|
recipient = readUnsignedInteger(sum);
|
|
subject = readUnsignedInteger(sum);
|
|
}
|
|
long date = readUnsignedInteger(sum);
|
|
long flags = readUnsignedInteger(sum);
|
|
long pos = readUnsignedInteger(sum);
|
|
long length = readUnsignedInteger(sum);
|
|
if (id_table_length <= 0xFFFF) {
|
|
id = readUnsignedShort(sum);
|
|
} else {
|
|
id = readUnsignedInteger(sum);
|
|
}
|
|
|
|
int rlength = readUnsignedShort(sum);
|
|
MessageID refs[] = null;
|
|
|
|
if (rlength > 0) {
|
|
refs = new MessageID[rlength];
|
|
if (id_table_length <= 0xFFFF) {
|
|
for (int j = 0; j < rlength; j++)
|
|
refs[j] = id_table[readUnsignedShort(sum)];
|
|
} else {
|
|
for (int j = 0; j < rlength; j++)
|
|
refs[j] = id_table[(int) readUnsignedInteger(sum)];
|
|
}
|
|
}
|
|
|
|
if (pessimistic) {
|
|
refs = null;
|
|
salvage.put(id_table[(int) id], new Long(flags));
|
|
} else {
|
|
flags = BerkeleyMessage.mozillaFlagsToInternalFlags(flags);
|
|
BerkeleyMessage m =
|
|
new BerkeleyMessage(folder,
|
|
date * 1000L,
|
|
flags,
|
|
string_table[(int) author],
|
|
string_table[(int) recipient],
|
|
string_table[(int) subject],
|
|
id_table[(int) id],
|
|
refs);
|
|
refs = null;
|
|
m.setStorageFolderIndex((int) pos);
|
|
m.setSize((int) length);
|
|
msgs.addElement(m);
|
|
}
|
|
}
|
|
|
|
success = true;
|
|
|
|
} finally {
|
|
string_table = null;
|
|
id_table = null;
|
|
|
|
if (!success &&
|
|
msgs != null) {
|
|
// We got some kind of error while reading the message data.
|
|
// The `msgs' vector contains the messages we were able to parse,
|
|
// but since we got an error, we're not going to actually add them
|
|
// to the folder. But, let's pull the flags out of the messages
|
|
// and populate `salvage' with it (see doc of salvageMessages()
|
|
// for rationale.)
|
|
//
|
|
// Note that if we realized very early that this file was out of
|
|
// date, salvage will have already been populated, and `msgs'
|
|
// will be null. This branch is taken only if the file at first
|
|
// appears to be up to date, but later turns out to be corrupted
|
|
// or truncated.
|
|
//
|
|
if (salvage == null)
|
|
salvage = new Hashtable((int) nmsgs);
|
|
for (Enumeration e = msgs.elements(); e.hasMoreElements();) {
|
|
BerkeleyMessage m = (BerkeleyMessage) e.nextElement();
|
|
salvage.put(m.getMessageID(),
|
|
new Long(m.internalFlagsToMozillaFlags(m.flags)));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (pessimistic)
|
|
// Ok, we got what we needed. Tell the caller to go and reparse
|
|
// the folder. It will then call salvageMessages() which will
|
|
// pull stuff out of the hash table we just populated.
|
|
return 0;
|
|
|
|
this.total_message_count = (int) nmsgs;
|
|
this.undeleted_message_count = (int) nvalidmsgs;
|
|
this.unread_message_count = (int) nunreadmsgs;
|
|
this.deleted_message_bytes = deletedbytes;
|
|
|
|
// Successfully parsed it all; feed the messages into the folder.
|
|
for (Enumeration e = msgs.elements(); e.hasMoreElements();)
|
|
folder.noticeInitialMessage((BerkeleyMessage) e.nextElement());
|
|
msgs = null;
|
|
|
|
// Return the byte at which parsing in the folder should begin
|
|
// (the last byte covered by this summary file.)
|
|
return old_thru;
|
|
}
|
|
|
|
|
|
/** Assumes the salvage table contains Long values which are flags
|
|
in X-Mozilla-Status form. */
|
|
protected void salvageMessage(BerkeleyMessage m, Object salvage_object) {
|
|
long flags = ((Long)salvage_object).longValue();
|
|
m.flags |= m.mozillaFlagsToInternalFlags(flags);
|
|
}
|
|
|
|
|
|
/** Read only enough of the file to fill in the values of
|
|
total_message_count, undeleted_message_count, unread_message_count,
|
|
and deleted_message_bytes. */
|
|
synchronized protected void getMessageCounts() {
|
|
InputStream in = null;
|
|
try {
|
|
File sum = folder.summaryFileName();
|
|
in = new BufferedInputStream(new FileInputStream(sum));
|
|
|
|
// header
|
|
for (int i = 0;
|
|
i < MailSummaryFileFactory.SUMMARY_MAGIC_NUMBER.length;
|
|
i++) {
|
|
if (in.read() != MailSummaryFileFactory.SUMMARY_MAGIC_NUMBER[i])
|
|
return;
|
|
}
|
|
|
|
// version number
|
|
if (readUnsignedInteger(in) != MailSummaryFileFactory.VERSION_GRENDEL)
|
|
return;
|
|
|
|
readUnsignedInteger(in); // FOLDER_SIZE
|
|
readUnsignedInteger(in); // FOLDER_DATE
|
|
readUnsignedInteger(in); // PARSED_THRU
|
|
|
|
total_message_count = (int) readUnsignedInteger(in); // NMSGS
|
|
undeleted_message_count = (int) readUnsignedInteger(in); // NVALIDMSGS
|
|
unread_message_count = (int) readUnsignedInteger(in); // NUNREADMSGS
|
|
deleted_message_bytes = readUnsignedInteger(in); // DELETED_BYTES
|
|
|
|
} catch (IOException e) {
|
|
// ignore it.
|
|
} finally {
|
|
if (in != null)
|
|
try {
|
|
in.close();
|
|
} catch (IOException e) {
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** Returns true, since this class implements writeSummaryFile(). */
|
|
boolean writable() { return true; }
|
|
|
|
/** Write a summary file for the associated folder, in the Grendel format
|
|
(version 6.)
|
|
*/
|
|
void writeSummaryFile() throws IOException {
|
|
File sum = folder.summaryFileName();
|
|
OutputStream out = null;
|
|
boolean success = false;
|
|
try {
|
|
out = new BufferedOutputStream(new FileOutputStream(sum));
|
|
writeSummaryData(out);
|
|
success = true;
|
|
} finally {
|
|
// always close the file.
|
|
// if there was an error, delete the partial file we just wrote.
|
|
if (out != null) {
|
|
out.close();
|
|
if (!success)
|
|
sum.delete();
|
|
}
|
|
}
|
|
}
|
|
|
|
/* subroutine of writeSummaryFile, broken out for clarity. */
|
|
protected void writeSummaryData(OutputStream out)
|
|
throws IOException, SecurityException {
|
|
|
|
Vector msgs = folder.fMessages;
|
|
int nmsgs = msgs.size();
|
|
int nvalidmsgs = 0;
|
|
int nunreadmsgs = 0;
|
|
int deletedbytes = 0;
|
|
|
|
// Walk over the messages to count up the unread, deleted, and bytes.
|
|
for (int i = 0; i < nmsgs; i++) {
|
|
BerkeleyMessage m = (BerkeleyMessage) msgs.elementAt(i);
|
|
if (m.isDeleted()) {
|
|
deletedbytes += m.fLength;
|
|
} else {
|
|
nvalidmsgs++;
|
|
if (!m.isRead())
|
|
nunreadmsgs++;
|
|
}
|
|
}
|
|
|
|
// These *should* be in sync, but if they're not, correct them.
|
|
/*
|
|
Assert.Assertion(nmsgs == this.total_message_count);
|
|
Assert.Assertion(nvalidmsgs == this.undeleted_message_count);
|
|
Assert.Assertion(nunreadmsgs == this.unread_message_count);
|
|
Assert.Assertion(deletedbytes == this.deleted_message_bytes);
|
|
*/
|
|
this.total_message_count = nmsgs;
|
|
this.undeleted_message_count = nvalidmsgs;
|
|
this.unread_message_count = nunreadmsgs;
|
|
this.deleted_message_bytes = deletedbytes;
|
|
|
|
|
|
ByteStringTable string_table = folder.getStringTable();
|
|
MessageIDTable id_table = folder.getMessageIDTable();
|
|
|
|
int string_table_length = string_table.size();
|
|
int id_table_length = id_table.size();
|
|
|
|
out.write(MailSummaryFileFactory.SUMMARY_MAGIC_NUMBER);
|
|
writeUnsignedInteger(out, MailSummaryFileFactory.VERSION_GRENDEL);
|
|
|
|
writeUnsignedInteger(out, this.file_size);
|
|
writeUnsignedInteger(out, (this.file_date / 1000L));
|
|
writeUnsignedInteger(out, this.file_size); // parsed_thru
|
|
writeUnsignedInteger(out, nmsgs);
|
|
writeUnsignedInteger(out, nvalidmsgs);
|
|
writeUnsignedInteger(out, nunreadmsgs);
|
|
writeUnsignedInteger(out, deletedbytes);
|
|
|
|
writeUnsignedInteger(out, string_table_length + 1);
|
|
out.write(0); // first elt in table is a null string, for simplicity
|
|
for (int i = 0; i < string_table_length; i++) {
|
|
ByteString s = (ByteString) string_table.getObject(i);
|
|
out.write(s.toBytes());
|
|
out.write(0);
|
|
}
|
|
|
|
writeUnsignedInteger(out, id_table_length);
|
|
for (int i = 0; i < id_table_length; i++) {
|
|
MessageID id = (MessageID) id_table.getObject(i);
|
|
long h = id.hash;
|
|
writeUnsignedInteger(out, h >> 32);
|
|
writeUnsignedInteger(out, h & 0xFFFFFFFF);
|
|
}
|
|
|
|
for (int i = 0; i < nmsgs; i++) {
|
|
|
|
BerkeleyMessage m = (BerkeleyMessage) msgs.elementAt(i);
|
|
int author_name = m.author_name + 1;
|
|
int recipient_name = m.recipient_name + 1;
|
|
int subject = m.subject + 1;
|
|
|
|
if (string_table_length <= 0xFFFF) {
|
|
writeUnsignedShort(out, author_name);
|
|
writeUnsignedShort(out, recipient_name);
|
|
writeUnsignedShort(out, subject);
|
|
} else {
|
|
writeUnsignedInteger(out, author_name);
|
|
writeUnsignedInteger(out, recipient_name);
|
|
writeUnsignedInteger(out, subject);
|
|
}
|
|
|
|
writeUnsignedInteger(out, (m.sentDate / 1000L));
|
|
writeUnsignedInteger(out, m.internalFlagsToMozillaFlags(m.flags));
|
|
writeUnsignedInteger(out, m.getStorageFolderIndex());
|
|
writeUnsignedInteger(out, m.getSize());
|
|
|
|
if (id_table_length <= 0xFFFF) {
|
|
writeUnsignedShort(out, m.message_id);
|
|
} else {
|
|
writeUnsignedInteger(out, m.message_id);
|
|
}
|
|
|
|
int refs[] = m.references;
|
|
if (refs == null) {
|
|
writeUnsignedShort(out, 0);
|
|
} else {
|
|
int rlength = refs.length;
|
|
writeUnsignedShort(out, rlength);
|
|
if (id_table_length <= 0xFFFF) {
|
|
for (int j = 0; j < rlength; j++)
|
|
writeUnsignedShort(out, refs[j]);
|
|
} else {
|
|
for (int j = 0; j < rlength; j++)
|
|
writeUnsignedInteger(out, refs[j]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Called when the folder's disk file has been appended to by <I>this</I>
|
|
program. (As opposed to, an unexpected, unknown change by some other
|
|
program.) This overwrites certain fields in a Grendel-format summary
|
|
file. Those fields are:
|
|
<UL>
|
|
<LI> FOLDER_SIZE
|
|
<LI> FOLDER_DATE
|
|
<LI> NMSGS
|
|
<LI> NVALIDMSGS
|
|
<LI> NUNREADMSGS
|
|
<LI> DELETED_BYTES
|
|
</UL>
|
|
*/
|
|
synchronized void updateSummaryFile() throws IOException {
|
|
super.updateSummaryFile();
|
|
|
|
RandomAccessFile io = null;
|
|
try {
|
|
io = new RandomAccessFile(folder.summaryFileName(), "rw");
|
|
boolean ok = true;
|
|
|
|
// First, ensure the file is in the format we expect it to be in.
|
|
//
|
|
for (int i = 0;
|
|
i < MailSummaryFileFactory.SUMMARY_MAGIC_NUMBER.length;
|
|
i++)
|
|
if (io.read() != MailSummaryFileFactory.SUMMARY_MAGIC_NUMBER[i]) {
|
|
ok = false;
|
|
break;
|
|
}
|
|
|
|
if (ok && (readUnsignedInteger(io) !=
|
|
MailSummaryFileFactory.VERSION_GRENDEL))
|
|
ok = false;
|
|
|
|
if (ok) {
|
|
// We are now positioned just before the FOLDER_SIZE and FOLDER_DATE
|
|
// slots. Overwrite them with the current size/date of the file.
|
|
// (Note that super.updateSummaryFile() called setFolderDateAndSize().)
|
|
writeUnsignedInteger(io, this.file_size);
|
|
writeUnsignedInteger(io, (this.file_date / 1000L));
|
|
|
|
// Skip over the PARSED_THRU token
|
|
io.seek(io.getFilePointer() + 4);
|
|
|
|
// Now overwrite the NMSGS, NVALIDMSGS and NUNREADMSGS tokens.
|
|
writeUnsignedInteger(io, this.total_message_count);
|
|
writeUnsignedInteger(io, this.undeleted_message_count);
|
|
writeUnsignedInteger(io, this.unread_message_count);
|
|
writeUnsignedInteger(io, this.deleted_message_bytes);
|
|
}
|
|
|
|
} finally {
|
|
if (io != null)
|
|
io.close();
|
|
}
|
|
}
|
|
}
|