1998-09-09 04:52:38 +04:00
|
|
|
/* -*- 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: Terry Weissman <terry@netscape.com>, 27 Aug 1997.
|
|
|
|
*/
|
|
|
|
|
|
|
|
package grendel.storage;
|
|
|
|
|
|
|
|
import calypso.util.ByteBuf;
|
|
|
|
import calypso.util.ByteLineBuffer;
|
|
|
|
import calypso.util.Assert;
|
|
|
|
|
|
|
|
import grendel.storage.intertwingle.Twingle;
|
1999-03-03 13:10:23 +03:00
|
|
|
import grendel.util.Constants;
|
1998-09-09 04:52:38 +04:00
|
|
|
|
|
|
|
import java.lang.Thread;
|
|
|
|
import java.lang.SecurityException;
|
|
|
|
import java.lang.InterruptedException;
|
|
|
|
import java.io.File;
|
|
|
|
import java.io.InputStream;
|
|
|
|
import java.io.FileInputStream;
|
|
|
|
import java.io.FilenameFilter;
|
|
|
|
import java.io.BufferedInputStream;
|
|
|
|
import java.io.IOException;
|
|
|
|
import java.io.RandomAccessFile;
|
|
|
|
import java.text.DecimalFormat;
|
|
|
|
import java.util.Calendar;
|
|
|
|
import java.util.Date;
|
|
|
|
import java.util.Enumeration;
|
|
|
|
import java.util.GregorianCalendar;
|
|
|
|
import java.util.Vector;
|
|
|
|
|
|
|
|
import javax.mail.FetchProfile;
|
|
|
|
import javax.mail.Folder;
|
|
|
|
import javax.mail.Message;
|
|
|
|
import javax.mail.MessagingException;
|
|
|
|
import javax.mail.Store;
|
|
|
|
import javax.mail.event.MessageChangedEvent;
|
|
|
|
import javax.mail.event.MessageCountEvent;
|
|
|
|
|
|
|
|
import javax.mail.internet.InternetHeaders;
|
|
|
|
|
|
|
|
|
|
|
|
class BerkeleyFolder extends FolderBase implements FilenameFilter {
|
|
|
|
|
|
|
|
File fFile; // the underlying disk file
|
|
|
|
|
|
|
|
Folder parent;
|
|
|
|
|
|
|
|
boolean fLoaded = false; // whether the folder has been parsed
|
|
|
|
private boolean fLoading = false; // true while in the process of parsing
|
|
|
|
|
|
|
|
private boolean summary_dirty = false; // whether changes have been made that
|
|
|
|
// would necessitate updating the
|
|
|
|
// summary file.
|
|
|
|
|
|
|
|
private boolean flags_dirty = false; // whether any of the messages in
|
|
|
|
// this folder have the FLAG_DIRTY
|
|
|
|
// bit set (meaning we need to flush
|
|
|
|
// out some X-Mozilla-Status headers.)
|
|
|
|
|
|
|
|
/* The object that knows how to read and (possibly) write the summary file.
|
|
|
|
It will be null if there is no summary file, or if the summary file
|
|
|
|
is in a format we don't understand. If fLoaded is true, this will always
|
|
|
|
be non-null. If fLoaded is false, this might be null or might not be.
|
|
|
|
*/
|
|
|
|
MailSummaryFile mailSummaryFile = null;
|
|
|
|
|
|
|
|
BerkeleyFolder(Store s, File f) {
|
|
|
|
super(s);
|
|
|
|
fFile = f;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected BerkeleyFolder(BerkeleyFolder parent, String name) {
|
|
|
|
this(parent.getStore(), new File(parent.fFile, name));
|
|
|
|
}
|
|
|
|
|
|
|
|
protected BerkeleyFolder(BerkeleyFolder parent, File f) {
|
|
|
|
super(parent.getStore());
|
|
|
|
fFile = f;
|
|
|
|
}
|
|
|
|
|
|
|
|
public char getSeparator() {
|
|
|
|
return File.separatorChar;
|
|
|
|
}
|
|
|
|
|
|
|
|
public int getType() {
|
|
|
|
return HOLDS_MESSAGES | HOLDS_FOLDERS;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Returns the file name associated with this folder. */
|
|
|
|
File getFile() {
|
|
|
|
return fFile;
|
|
|
|
}
|
|
|
|
|
|
|
|
public String getName() {
|
|
|
|
return fFile.getName();
|
|
|
|
}
|
|
|
|
|
|
|
|
public String getFullName() {
|
|
|
|
return fFile.getAbsolutePath(); // ### This isn't quite right!
|
|
|
|
}
|
|
|
|
|
|
|
|
public Folder getParent() {
|
|
|
|
return parent;
|
|
|
|
}
|
|
|
|
|
|
|
|
public boolean accept(File f, String name) {
|
|
|
|
int L = name.length();
|
|
|
|
if (L == 0) return false;
|
|
|
|
if (name.charAt(0) == '.') return false;
|
|
|
|
if (name.charAt(L-1) == '~') return false;
|
|
|
|
if (L < 4) return true;
|
|
|
|
if (name.regionMatches(true, L-4, ".SNM", 0, 4)) return false;
|
|
|
|
if (name.regionMatches(true, L-4, ".DAT", 0, 4)) return false;
|
|
|
|
if (name.regionMatches(true, L-4, ".LOG", 0, 4)) return false;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
public Folder[] list(String pattern) {
|
|
|
|
if (pattern.equals("%")) {
|
|
|
|
String list[] = fFile.list(this);
|
|
|
|
if (list == null || list.length == 0) return null;
|
|
|
|
Vector result = new Vector();
|
|
|
|
for (int i=0 ; i<list.length ; i++) {
|
|
|
|
File sub = new File(fFile, list[i]);
|
|
|
|
if (sub.isDirectory()) continue;
|
|
|
|
result.addElement(new BerkeleyFolder(this, sub));
|
|
|
|
}
|
|
|
|
Folder folders[] = new Folder[result.size()];
|
|
|
|
result.copyInto(folders);
|
|
|
|
return folders;
|
|
|
|
}
|
|
|
|
Assert.NotYetImplemented("BerkeleyFolder.list");
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
public Folder getFolder(String subfolder) {
|
|
|
|
return new BerkeleyFolder(this, subfolder);
|
|
|
|
}
|
|
|
|
|
|
|
|
public boolean create(int type) {
|
|
|
|
Assert.NotYetImplemented("Folder.create");
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
public boolean exists() {
|
|
|
|
return fFile.exists();
|
|
|
|
}
|
|
|
|
|
|
|
|
public boolean hasNewMessages() {
|
|
|
|
return getUnreadMessageCount() > 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Returns the total number of messages in the folder, or -1 if unknown.
|
|
|
|
This includes deleted and unread messages.
|
|
|
|
*/
|
|
|
|
public int getMessageCount() {
|
|
|
|
if (fMessages != null)
|
|
|
|
return fMessages.size();
|
|
|
|
ensureSummaryFileHeaderParsed();
|
|
|
|
if (mailSummaryFile != null)
|
|
|
|
return mailSummaryFile.totalMessageCount();
|
|
|
|
else if (!fFile.exists() || fFile.length() < 5)
|
|
|
|
return 0;
|
|
|
|
else
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Returns the number of non-deleted messages in the folder, or -1 if
|
|
|
|
unknown. This includes unread messages.
|
|
|
|
*/
|
|
|
|
public int getUndeletedMessageCount() {
|
|
|
|
ensureSummaryFileHeaderParsed();
|
|
|
|
if (mailSummaryFile != null)
|
|
|
|
return mailSummaryFile.undeletedMessageCount();
|
|
|
|
else if (!fFile.exists() || fFile.length() < 5)
|
|
|
|
return 0;
|
|
|
|
else
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Returns the number of unread messages in the folder, or -1 if unknown.
|
|
|
|
This does not include unread messages that are also deleted.
|
|
|
|
*/
|
|
|
|
public int getUnreadMessageCount() {
|
|
|
|
ensureSummaryFileHeaderParsed();
|
|
|
|
if (mailSummaryFile != null)
|
|
|
|
return mailSummaryFile.unreadMessageCount();
|
|
|
|
else if (!fFile.exists() || fFile.length() < 5)
|
|
|
|
return 0;
|
|
|
|
else
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Returns the number of bytes consumed by deleted but not expunged
|
|
|
|
messages in the folder, or -1 if unknown.
|
|
|
|
*/
|
|
|
|
long deletedMessageBytes() {
|
|
|
|
ensureSummaryFileHeaderParsed();
|
|
|
|
if (mailSummaryFile != null)
|
|
|
|
return mailSummaryFile.deletedMessageBytes();
|
|
|
|
else if (!fFile.exists() || fFile.length() < 5)
|
|
|
|
return 0;
|
|
|
|
else
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** If the messages are not known, parse the underlying files.
|
|
|
|
If the folder file has changed on disk since it was parsed,
|
|
|
|
re-parse it (replacing the existing message objects with new
|
|
|
|
ones.)
|
|
|
|
*/
|
|
|
|
void ensureLoaded() {
|
|
|
|
Assert.Assertion(!fLoading);
|
|
|
|
|
|
|
|
if (!fLoaded) {
|
|
|
|
synchronized (this) {
|
|
|
|
if (!fLoaded) // double check, to avoid race.
|
|
|
|
loadMessages();
|
|
|
|
}
|
|
|
|
|
|
|
|
} else { // it was already loaded once; see if we need to reload.
|
|
|
|
|
|
|
|
if (verifyFileDate(false)) // file (probably) hasn't changed
|
|
|
|
return;
|
|
|
|
|
|
|
|
synchronized (this) {
|
|
|
|
|
|
|
|
if (verifyFileDate(true)) // double check, to avoid race.
|
|
|
|
return;
|
|
|
|
|
|
|
|
System.err.println("warning: file " + fFile +
|
|
|
|
" has changed; re-loading!");
|
|
|
|
|
|
|
|
// notify all the old messages as deleted
|
|
|
|
Message[] oldlist = new Message[fMessages.size()];
|
|
|
|
fMessages.copyInto(oldlist);
|
|
|
|
notifyMessageRemovedListeners(true /*???*/, oldlist);
|
|
|
|
|
|
|
|
fMessages.setSize(0); // nuke the old messages
|
|
|
|
fLoaded = false;
|
|
|
|
summary_dirty = false;
|
|
|
|
flags_dirty = false;
|
|
|
|
loadMessages(); // read in the new messages
|
|
|
|
|
|
|
|
// notify all the new messages as added
|
|
|
|
Message[] newlist = new Message[fMessages.size()];
|
|
|
|
fMessages.copyInto(newlist);
|
|
|
|
notifyMessageAddedListeners(newlist);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private synchronized void loadMessages() {
|
|
|
|
// Do the timebomb.
|
|
|
|
// java.util.Date before = new Date(97, 9, 1, 0, 0);
|
|
|
|
// java.util.Date now = new Date();
|
|
|
|
// java.util.Date then = new Date(97, 11, 25, 12, 00);
|
|
|
|
// if (now.before(before) || now.after(then)) {
|
|
|
|
// System.err.println("This software has expired");
|
|
|
|
// System.exit(-1);
|
|
|
|
// }
|
|
|
|
|
|
|
|
Assert.Assertion(!fLoading && !fLoaded);
|
|
|
|
|
|
|
|
summary_dirty = false;
|
|
|
|
try { // make sure unlockFolderFile() gets called.
|
|
|
|
fLoading = true;
|
|
|
|
lockFolderFile();
|
|
|
|
|
|
|
|
ParseBerkeleyFolderWithSummary parser =
|
|
|
|
new ParseBerkeleyFolderWithSummary();
|
|
|
|
RandomAccessFile in = null;
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
in = new RandomAccessFile(fFile, "r");
|
|
|
|
parser.mapOverMessages(in, this);
|
|
|
|
|
|
|
|
// If there is no summary object, or an unwritable one, use Grendel.
|
|
|
|
// Do this now so that the newly-created summary object gets the
|
|
|
|
// proper date/size from the folder file (date/size that corresponds
|
|
|
|
// to the time at which we parsed the folder.)
|
|
|
|
//
|
|
|
|
// Also mark it dirty, so that the background thread will write a
|
|
|
|
// new file in Grendel format (creating a summary where there was
|
|
|
|
// none, or auto-upgrading the old format to the new one.)
|
|
|
|
//
|
|
|
|
if (mailSummaryFile == null || !mailSummaryFile.writable()) {
|
|
|
|
mailSummaryFile = new MailSummaryFileGrendel(this);
|
|
|
|
setSummaryDirty(true);
|
|
|
|
}
|
|
|
|
|
|
|
|
} finally {
|
|
|
|
if (in != null)
|
|
|
|
in.close();
|
|
|
|
}
|
|
|
|
} catch (IOException e) {
|
|
|
|
System.err.println("error parsing folder " + fFile + ": " + e);
|
|
|
|
e.printStackTrace();
|
|
|
|
throw new Error("Argh! Couldn't open mailbox folder!");
|
|
|
|
}
|
|
|
|
} finally {
|
|
|
|
unlockFolderFile();
|
|
|
|
fLoading = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
Assert.Assertion(!fLoaded);
|
|
|
|
fLoaded = true;
|
|
|
|
|
|
|
|
if (summary_dirty) {
|
|
|
|
// Kludge to deal properly with dirtying that occurs during parsing:
|
|
|
|
// delay the spawning of the background thread until after parsing
|
|
|
|
// is complete.
|
|
|
|
//
|
|
|
|
// #### Perhaps instead of spawning the thread to write the summary,
|
|
|
|
// we should just write it now? If we have parsed the folder from
|
|
|
|
// scratch, we've already done a lot of I/O, and a little more won't
|
|
|
|
// hurt.
|
|
|
|
summary_dirty = false;
|
|
|
|
setSummaryDirty(true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Assert whether the folder's summary file should be regenerated.
|
|
|
|
*/
|
|
|
|
void setSummaryDirty(boolean summary_dirty) {
|
|
|
|
if (!summary_dirty ||
|
|
|
|
(this.summary_dirty == summary_dirty)) {
|
|
|
|
// no change, or being marked as non-dirty.
|
|
|
|
this.summary_dirty = summary_dirty;
|
|
|
|
|
|
|
|
} else if (fLoading) {
|
|
|
|
// being marked as dirty while loading. Mark it dirty, but don't spawn.
|
|
|
|
this.summary_dirty = summary_dirty;
|
|
|
|
|
|
|
|
} else if (!fLoaded) {
|
|
|
|
// being marked as dirty, but not loaded (or loading.) Ignore it.
|
|
|
|
|
|
|
|
} else {
|
|
|
|
// being marked as dirty.
|
|
|
|
// Add this folder to the update-thread's list, but make sure we don't
|
|
|
|
// add it more than once.
|
|
|
|
synchronized (this) {
|
|
|
|
if (this.summary_dirty) return; // double-check that we didn't lose
|
|
|
|
// a race.
|
|
|
|
this.summary_dirty = summary_dirty;
|
|
|
|
SummaryUpdateThread t = SummaryUpdateThread.Get();
|
|
|
|
t.addDirtyFolder(this);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// The per-folder thread that flushes X-Mozilla-Status changes out to the
|
|
|
|
// file. Reads and writes of this variable must be synchronized.
|
|
|
|
Thread statusFlagsUpdateThread = null;
|
|
|
|
|
|
|
|
/** Assert whether any messages have the FLAGS_DIRTY bit set in them.
|
|
|
|
A child message should call this on its parent when any persistent
|
|
|
|
flag value is changed. If a non-null message is provided, and
|
|
|
|
flags_dirty is true, then notify any observers that this message
|
|
|
|
has changed.
|
|
|
|
|
|
|
|
@param flags_dirty Whether the flags should currently be considered
|
|
|
|
to be dirty.
|
|
|
|
|
|
|
|
@param message If flags_dirty, the Message (BerkeleyMessage) that
|
|
|
|
has become dirty.
|
|
|
|
|
|
|
|
@param old_flags If message is non-null, the previous value of its
|
|
|
|
flags (Message.flags should be the new, dirty value.)
|
|
|
|
*/
|
|
|
|
void setFlagsDirty(boolean flags_dirty, Message message, long old_flags) {
|
|
|
|
|
|
|
|
if (flags_dirty && fLoaded && !fLoading)
|
|
|
|
// This is as good a time as any to make sure the disk file hasn't
|
|
|
|
// changed (note that this is lazy: the disk isn't pegged each time
|
|
|
|
// ensureLoaded() is called.)
|
|
|
|
ensureLoaded();
|
|
|
|
|
|
|
|
// If the deleted-ness or read-ness of a message has changed, flush
|
|
|
|
// the new counts down into the summary object.
|
|
|
|
if (flags_dirty && message != null) {
|
|
|
|
long new_flags = ((MessageBase)message).flags;
|
|
|
|
long interesting = (MessageBase.FLAG_DELETED | MessageBase.FLAG_READ);
|
|
|
|
if ((old_flags & interesting) != (new_flags & interesting)) {
|
|
|
|
int msglen = ((BerkeleyMessage)message).fLength;
|
|
|
|
updateSummaryMessageCounts(old_flags, new_flags, msglen);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!flags_dirty ||
|
|
|
|
(this.flags_dirty == flags_dirty)) {
|
|
|
|
// no change, or being marked as non-dirty.
|
|
|
|
this.flags_dirty = flags_dirty;
|
|
|
|
|
|
|
|
} else if (fLoading) {
|
|
|
|
// being marked as dirty while loading. Mark it dirty, but don't spawn.
|
|
|
|
this.flags_dirty = flags_dirty;
|
|
|
|
|
|
|
|
} else if (!fLoaded) {
|
|
|
|
// being marked as dirty, but not loaded (or loading.) Ignore it.
|
|
|
|
|
|
|
|
} else {
|
|
|
|
// being marked as dirty.
|
|
|
|
// Spawn a thread to update this folder's X-Mozilla-Status headers
|
|
|
|
// in a few seconds, if there's not already one around.
|
|
|
|
synchronized (this) {
|
|
|
|
this.flags_dirty = flags_dirty;
|
|
|
|
if (statusFlagsUpdateThread == null ||
|
|
|
|
!statusFlagsUpdateThread.isAlive()) {
|
|
|
|
statusFlagsUpdateThread = new StatusFlagsUpdateThread(this);
|
|
|
|
statusFlagsUpdateThread.start();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (flags_dirty && message != null) {
|
|
|
|
notifyMessageChangedListeners(MessageChangedEvent.FLAGS_CHANGED,
|
|
|
|
message);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** If the deleted-ness or read-ness of a message has changed, flush
|
|
|
|
the new counts down into the summary object.
|
|
|
|
*/
|
|
|
|
protected void updateSummaryMessageCounts(long old_flags, long new_flags,
|
|
|
|
int msglen) {
|
|
|
|
Assert.Assertion(msglen > 0);
|
|
|
|
ensureSummaryFileHeaderParsed();
|
|
|
|
if (mailSummaryFile == null)
|
|
|
|
return;
|
|
|
|
|
|
|
|
int total = mailSummaryFile.totalMessageCount();
|
|
|
|
int undel = mailSummaryFile.undeletedMessageCount();
|
|
|
|
int unread = mailSummaryFile.unreadMessageCount();
|
|
|
|
long dbytes = mailSummaryFile.deletedMessageBytes();
|
|
|
|
|
|
|
|
if ((old_flags & MessageBase.FLAG_DELETED) !=
|
|
|
|
(new_flags & MessageBase.FLAG_DELETED)) {
|
|
|
|
//
|
|
|
|
// deleted-ness of a message changed.
|
|
|
|
//
|
|
|
|
if ((new_flags & MessageBase.FLAG_DELETED) != 0) { // became deleted
|
|
|
|
Assert.Assertion(undel > 0);
|
|
|
|
if (undel > 0) {
|
|
|
|
undel--;
|
|
|
|
dbytes += msglen;
|
|
|
|
}
|
|
|
|
} else { // became undeleted
|
|
|
|
Assert.Assertion(undel < total);
|
|
|
|
if (undel < total) {
|
|
|
|
undel++;
|
|
|
|
Assert.Assertion(dbytes >= msglen);
|
|
|
|
if (dbytes >= msglen)
|
|
|
|
dbytes -= msglen;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
} else if (((new_flags & MessageBase.FLAG_DELETED) == 0) &&
|
|
|
|
(old_flags & MessageBase.FLAG_READ) !=
|
|
|
|
(new_flags & MessageBase.FLAG_READ)) {
|
|
|
|
//
|
|
|
|
// read-ness of a non-deleted message changed.
|
|
|
|
//
|
|
|
|
if ((new_flags & MessageBase.FLAG_READ) != 0) { // became read
|
|
|
|
Assert.Assertion(unread > 0);
|
|
|
|
if (unread > 0)
|
|
|
|
unread--;
|
|
|
|
} else { // became unread
|
|
|
|
Assert.Assertion(unread < total);
|
|
|
|
if (unread < total)
|
|
|
|
unread++;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
Assert.Assertion(false);
|
|
|
|
}
|
|
|
|
|
|
|
|
mailSummaryFile.setFolderMessageCounts(total, undel, unread, dbytes);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
protected void ensureSummaryFileHeaderParsed() {
|
|
|
|
// If there isn't a summary file object, presumably this is because the
|
|
|
|
// messages of this folder aren't loaded. Examine the summary file on
|
|
|
|
// disk, if any, and make a summary object corresponding to it.
|
|
|
|
//
|
|
|
|
if (mailSummaryFile == null) {
|
|
|
|
Assert.Assertion(!fLoaded);
|
|
|
|
synchronized (this) {
|
|
|
|
if (mailSummaryFile == null) { // double-check
|
|
|
|
try { // ignore errors while reading the summary file.
|
|
|
|
InputStream stream = null;
|
|
|
|
try { // be sure to close stream
|
|
|
|
File name = summaryFileName();
|
|
|
|
stream = new BufferedInputStream(new FileInputStream(name));
|
|
|
|
// System.err.println("opened (unloaded) summary file " + name);
|
|
|
|
mailSummaryFile =
|
|
|
|
MailSummaryFileFactory.ParseFileHeader(this, stream);
|
|
|
|
} finally {
|
|
|
|
if (stream != null) stream.close();
|
|
|
|
}
|
|
|
|
} catch (IOException e) {
|
|
|
|
} catch (SecurityException e) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private long verify_file_tick = 0;
|
|
|
|
|
|
|
|
/** Returns false if the disk file has changed unexpectedly.
|
|
|
|
|
|
|
|
@param force If true, guarentee that the disk will be checked, and
|
|
|
|
the returned value will be accurate. If false, the
|
|
|
|
disk will not be checked more frequently than once
|
|
|
|
a second, so true might be returned without verification.
|
|
|
|
|
|
|
|
<P> That is, if `force' is false, and this method returns
|
|
|
|
true, the disk might or might not be in sync. However,
|
|
|
|
regardless of the value of `force', if false is returned,
|
|
|
|
the disk is definitely out of sync.
|
|
|
|
*/
|
|
|
|
protected boolean verifyFileDate(boolean force) {
|
|
|
|
if (!fLoaded) return true;
|
|
|
|
|
|
|
|
Assert.Assertion(mailSummaryFile != null);
|
|
|
|
|
|
|
|
long now = System.currentTimeMillis();
|
|
|
|
|
|
|
|
// If less than a second has passed, assume everything is ok.
|
|
|
|
if (!force &&
|
|
|
|
verify_file_tick + 1000 > now)
|
|
|
|
return true;
|
|
|
|
|
|
|
|
synchronized (this) {
|
|
|
|
verify_file_tick = now;
|
|
|
|
if (mailSummaryFile.folderDate() != fFile.lastModified() ||
|
|
|
|
mailSummaryFile.folderSize() != fFile.length())
|
|
|
|
return false;
|
|
|
|
else
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Call this to inform the summary file module that the folder file
|
|
|
|
has just been modified. (The code managing the summary file may wish
|
|
|
|
to act on this information in some way. With our current summary file
|
|
|
|
format, failing to so notice it will cause the folder to be re-parsed
|
|
|
|
the next time, since we won't be able to tell the difference between
|
|
|
|
some *other* program modifying the file instead of us.)
|
|
|
|
|
|
|
|
<P> Call this with the folder file still locked, to avoid a potential
|
|
|
|
race.
|
|
|
|
|
|
|
|
@see MailSummaryFile
|
|
|
|
@see MailSummaryFileGrendel
|
|
|
|
*/
|
|
|
|
protected void updateSummaryFile() {
|
|
|
|
|
|
|
|
ensureSummaryFileHeaderParsed();
|
|
|
|
|
|
|
|
if (mailSummaryFile != null &&
|
|
|
|
mailSummaryFile.writable()) {
|
|
|
|
synchronized (this) {
|
|
|
|
try {
|
|
|
|
mailSummaryFile.updateSummaryFile();
|
|
|
|
} catch (IOException e) {
|
|
|
|
// Ignore it, I guess...
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else if (fLoaded) {
|
|
|
|
// If there was no summary file, or if it was in an unknown format,
|
|
|
|
// or if it was in a format that we don't know how to write, then
|
|
|
|
// schedule a modern-format summary file to be written. We can
|
|
|
|
// do this only when the messages are loaded: if the messages aren't
|
|
|
|
// loaded, we might have a MailSummaryFile object, but we won't
|
|
|
|
// be able to write anything meaningful into it.
|
|
|
|
setSummaryDirty(true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Returns the name of the mail summary file used for this folder.
|
|
|
|
This name is OS-dependent; it is assumed that the data within the
|
|
|
|
file will contain versioning information.
|
|
|
|
<P>
|
|
|
|
On all systems, the summary file resides in the same directory
|
|
|
|
as the folder file.
|
|
|
|
<P>
|
|
|
|
On Unix, the summary file for <TT>FOO</TT> would be
|
|
|
|
<TT>.FOO.summary</TT> (note leading dot.)
|
|
|
|
<P>
|
|
|
|
On Windows and Mac, the summary file for <TT>FOO.MBX</TT> would be
|
|
|
|
<TT>FOO.SNM</TT>.
|
|
|
|
*/
|
|
|
|
File summaryFileName() {
|
|
|
|
File file = getFile();
|
|
|
|
String dir = file.getParent();
|
|
|
|
String name = file.getName();
|
|
|
|
|
|
|
|
if (Constants.ISUNIX) {
|
|
|
|
name = "." + name + ".summary";
|
|
|
|
} else {
|
|
|
|
int i = name.lastIndexOf(".");
|
|
|
|
if (i > 0)
|
|
|
|
name = name.substring(0, i);
|
|
|
|
name = name + ".snm";
|
|
|
|
}
|
|
|
|
|
|
|
|
return new File(dir, name);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Returns the name of a non-existent file that can be used as a temporary
|
|
|
|
file for this folder. This name is OS-dependent.
|
|
|
|
<P>
|
|
|
|
On all systems, the summary file resides in the same directory
|
|
|
|
as the folder file, and has the same "base" name.
|
|
|
|
<P>
|
|
|
|
On Unix, the summary file for <TT>FOO</TT> would be
|
|
|
|
<TT>.FOO.ns_tmp</TT> (note leading dot.)
|
|
|
|
<P>
|
|
|
|
On Windows and Mac, the summary file for <TT>FOO.MBX</TT> would be
|
|
|
|
<TT>FOO.TMP</TT>.
|
|
|
|
*/
|
|
|
|
File tempFolderName() {
|
|
|
|
File file = getFile();
|
|
|
|
String dir = file.getParent();
|
|
|
|
String name = file.getName();
|
|
|
|
|
|
|
|
if (Constants.ISUNIX) {
|
|
|
|
name = "." + name + ".ns_tmp";
|
|
|
|
} else {
|
|
|
|
int i = name.lastIndexOf(".");
|
|
|
|
if (i > 0)
|
|
|
|
name = name.substring(0, i);
|
|
|
|
name = name + ".tmp";
|
|
|
|
}
|
|
|
|
|
|
|
|
return new File(dir, name);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Write a summary file for this folder.
|
|
|
|
@see MailSummaryFile
|
|
|
|
@see MailSummaryFileGrendel
|
|
|
|
*/
|
|
|
|
synchronized void writeSummaryFile() throws IOException {
|
|
|
|
Assert.Assertion(fLoaded);
|
|
|
|
if (!fLoaded) return;
|
|
|
|
if (!summary_dirty) return;
|
|
|
|
setSummaryDirty(false); // do this first
|
|
|
|
mailSummaryFile.writeSummaryFile();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** This causes the X-Mozilla-Status headers in the folder file itself
|
|
|
|
to be updated. (Not to be confused with the summary file: this
|
|
|
|
modifies the <I>folder</I>.) If a changed message's bits in the
|
|
|
|
file include an X-Mozilla-Status header, it will be overwritten
|
|
|
|
(random-access.) If the header is not present, the message will be
|
|
|
|
unchanged (and in that situation, the flags will only live in the
|
|
|
|
summary file, not the folder itself; and if the summary file gets
|
|
|
|
blown away, the flags will be lost. That's why we try to have an
|
|
|
|
X-Mozilla-Status header on every message.)
|
|
|
|
*/
|
|
|
|
synchronized void flushMozillaStatus() throws IOException {
|
|
|
|
|
|
|
|
Assert.Assertion(!fLoading);
|
|
|
|
if (!fLoaded) return;
|
|
|
|
if (!flags_dirty) return;
|
|
|
|
|
|
|
|
// check file dates to make sure it hasn't changed from under us.
|
|
|
|
ensureLoaded();
|
|
|
|
|
|
|
|
if (!flags_dirty) return; // double-check, in case reloaded
|
|
|
|
|
|
|
|
setFlagsDirty(false, null, 0);
|
|
|
|
boolean wrote_anything = false;
|
|
|
|
|
|
|
|
try { // make sure unlockFolderFile() gets called.
|
|
|
|
lockFolderFile();
|
|
|
|
|
|
|
|
ParseBerkeleyFolder parser = new ParseBerkeleyFolder();
|
|
|
|
RandomAccessFile io = null;
|
|
|
|
try {
|
|
|
|
io = new RandomAccessFile(fFile, "rw");
|
|
|
|
|
|
|
|
for (Enumeration e = fMessages.elements(); e.hasMoreElements();) {
|
|
|
|
BerkeleyMessage m = (BerkeleyMessage) e.nextElement();
|
|
|
|
if (m.flagsAreDirty()) {
|
|
|
|
parser.flushMozillaStatus(io,
|
|
|
|
m.getStorageFolderIndex(),
|
|
|
|
m.getSize(),
|
|
|
|
(short) m.makeMozillaFlags(m));
|
|
|
|
m.setFlagsDirty(false);
|
|
|
|
wrote_anything = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
} finally {
|
|
|
|
if (io != null)
|
|
|
|
io.close();
|
|
|
|
}
|
|
|
|
|
|
|
|
// At this point, the file has been written, and closed, without error.
|
|
|
|
// Update the date/size that will be encoded into the summary file the
|
|
|
|
// next time it is written.
|
|
|
|
//
|
|
|
|
// Don't call updateSummaryFile() now, which would update the dates in
|
|
|
|
// the existing disk summary file. Wait until writeSummaryFile() is
|
|
|
|
// called before putting the above dates on disk. The reason for this
|
|
|
|
// is, if we *have* written the X-Mozilla-Status headers into the
|
|
|
|
// folder, but have *not* written a new summary file, the folder has
|
|
|
|
// more current flags than the summary, so it would be appropriate to
|
|
|
|
// discard the summary file and re-parse the folder.
|
|
|
|
//
|
|
|
|
// However, this situation (re-parsing) can only occur if the program
|
|
|
|
// dies after the StatusFlagsUpdateThread runs, but before the
|
|
|
|
// SummaryUpdateThread runs.
|
|
|
|
//
|
|
|
|
mailSummaryFile.setFolderDateAndSize(fFile.lastModified(),
|
|
|
|
fFile.length());
|
|
|
|
|
|
|
|
} finally {
|
|
|
|
unlockFolderFile();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (wrote_anything)
|
|
|
|
// Since flags changed, mark the summary file as needing (eventually)
|
|
|
|
// to be regenerated as well.
|
|
|
|
setSummaryDirty(true);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public void appendMessages(Message msgs[]) throws MessagingException {
|
|
|
|
try {
|
|
|
|
for (int i=0 ; i<msgs.length ; i++) {
|
|
|
|
addMessage(msgs[i]);
|
|
|
|
}
|
|
|
|
} catch (IOException e) {
|
|
|
|
throw new MessagingException(e.toString());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Add a message to the end of this folder.
|
|
|
|
The disk file will be updated.
|
|
|
|
*/
|
|
|
|
void addMessage(Message m) throws IOException, MessagingException {
|
|
|
|
MessageExtra mextra = MessageExtraFactory.Get(m);
|
|
|
|
|
|
|
|
|
|
|
|
// If this folder is loaded, check the file dates and re-parse it
|
|
|
|
// first, if necessary, to make sure it hasn't changed from under us.
|
|
|
|
// But if it's not loaded, don't force a load.
|
|
|
|
if (fLoaded)
|
|
|
|
ensureLoaded();
|
|
|
|
|
|
|
|
InputStream in = null;
|
|
|
|
RandomAccessFile out = null;
|
|
|
|
boolean file_existed = fFile.exists();
|
|
|
|
long old_file_size = -1;
|
|
|
|
boolean completed_normally = false;
|
|
|
|
InternetHeaders headers = null;
|
|
|
|
long bytes_written = 0;
|
|
|
|
|
|
|
|
try { // make sure unlockFolderFile() gets called.
|
|
|
|
lockFolderFile();
|
|
|
|
|
|
|
|
try { // make sure in.close() and out.close() get called.
|
|
|
|
|
|
|
|
in = mextra.getInputStreamWithHeaders();
|
|
|
|
out = new RandomAccessFile(fFile, "rw");
|
|
|
|
old_file_size = out.length();
|
|
|
|
out.seek(old_file_size);
|
|
|
|
|
|
|
|
headers = copyMessageBytes(m, in, out); // do the deed
|
|
|
|
|
|
|
|
bytes_written = out.length() - old_file_size;
|
|
|
|
|
|
|
|
completed_normally = true;
|
|
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
|
|
if (in != null)
|
|
|
|
in.close();
|
|
|
|
|
|
|
|
if (out != null) {
|
|
|
|
|
|
|
|
if (completed_normally) {
|
|
|
|
out.close();
|
|
|
|
|
|
|
|
// Else, an error happened while appending to the file. At this
|
|
|
|
// point, we have written incomplete data to the file. Try and
|
|
|
|
// clean up after our damage.
|
|
|
|
|
|
|
|
} else if (!file_existed) {
|
|
|
|
// If the file didn't exist before we started, just delete it.
|
|
|
|
out.close();
|
|
|
|
out = null;
|
|
|
|
fFile.delete();
|
|
|
|
|
|
|
|
} else if (old_file_size < 0) {
|
|
|
|
// never wrote anything. No problem.
|
|
|
|
out.close();
|
|
|
|
|
|
|
|
} else {
|
|
|
|
// Truncate the file back to the length it had before we started
|
|
|
|
// appending to it.
|
|
|
|
//
|
|
|
|
// #### call out.ftruncate(old_file_size) here!
|
|
|
|
// #### I don't know how to do this in Java.
|
|
|
|
// #### in the meantime, let's append "\n\n" to make sure
|
|
|
|
// #### we didn't write any partial lines that will cause
|
|
|
|
// #### subsequently-appended messages to be swallowed.
|
|
|
|
//
|
|
|
|
ByteBuf buf = new ByteBuf();
|
|
|
|
buf.setLength(0);
|
|
|
|
buf.append(Constants.LINEBREAK);
|
|
|
|
buf.append(">> error! truncated!");
|
|
|
|
buf.append(Constants.LINEBREAK);
|
|
|
|
buf.append(Constants.LINEBREAK);
|
|
|
|
try {
|
|
|
|
out.write(buf.toBytes(), 0, buf.length());
|
|
|
|
} catch (IOException e) {
|
|
|
|
// just ignore it; we're writing in service of error recovery.
|
|
|
|
}
|
|
|
|
|
|
|
|
out.close();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Update the folder's statistics about what is contained in it.
|
|
|
|
//
|
|
|
|
ensureSummaryFileHeaderParsed();
|
|
|
|
if (mailSummaryFile != null) {
|
|
|
|
int total = mailSummaryFile.totalMessageCount();
|
|
|
|
int undel = mailSummaryFile.undeletedMessageCount();
|
|
|
|
int unread = mailSummaryFile.unreadMessageCount();
|
|
|
|
long dbytes = mailSummaryFile.deletedMessageBytes();
|
|
|
|
|
|
|
|
total++;
|
|
|
|
if (!mextra.isRead())
|
|
|
|
unread++;
|
|
|
|
|
|
|
|
if (mextra.isDeleted())
|
|
|
|
dbytes += bytes_written;
|
|
|
|
else if (undel != -1)
|
|
|
|
undel++;
|
|
|
|
|
|
|
|
// Inform the summary file of the new statistics.
|
|
|
|
//
|
|
|
|
mailSummaryFile.setFolderMessageCounts(total, undel, unread, dbytes);
|
|
|
|
}
|
|
|
|
|
|
|
|
// At this point, the file has been written, and closed, without error.
|
|
|
|
// Update the date/size encoded into the summary file (without
|
|
|
|
// rewriting the whole summary file itself.)
|
|
|
|
//
|
|
|
|
updateSummaryFile();
|
|
|
|
|
|
|
|
} finally {
|
|
|
|
unlockFolderFile();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Everything has been written to disk without error (both folder
|
|
|
|
// and summary file.) Now, create an in-memory message representing
|
|
|
|
// the new message, and let any observers know that this message has
|
|
|
|
// been added to the folder.
|
|
|
|
//
|
|
|
|
BerkeleyMessage newmessage = new BerkeleyMessage(this, headers);
|
|
|
|
|
|
|
|
// Add this message to the list of messages in this folder, *if* this
|
|
|
|
// folder knows what messages are in it. If we haven't yet parsed
|
|
|
|
// the folder, don't make it appear that the folder has only one
|
|
|
|
// message in it; rather, just wait until it gets parsed for real.
|
|
|
|
// #### fix me wrt observers
|
|
|
|
if (fLoaded) {
|
|
|
|
fMessages.addElement(newmessage);
|
|
|
|
}
|
|
|
|
|
|
|
|
Twingle twingle = Twingle.GetSingleton();
|
|
|
|
if (twingle != null) {
|
|
|
|
twingle.add(headers, this);
|
|
|
|
}
|
|
|
|
|
|
|
|
Message list[] = { newmessage };
|
|
|
|
notifyMessageAddedListeners(list);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void fetch(Message msgs[], FetchProfile fp) {}
|
|
|
|
|
|
|
|
/** Remove all messages that are marked as deleted from the folder's file.
|
|
|
|
This is done by writing a new file, then renaming it.
|
|
|
|
|
|
|
|
@throws IOException if the file could not be expunged. A likely
|
|
|
|
cause would be running out of disk space, since
|
|
|
|
the peak file usage can be up to 2x the size of
|
|
|
|
the original folder.
|
|
|
|
*/
|
|
|
|
public Message[] expunge() throws MessagingException {
|
|
|
|
Message[] result = null;
|
|
|
|
try {
|
|
|
|
result = realExpunge();
|
|
|
|
} catch (IOException e) {
|
|
|
|
throw new MessagingException(e.toString());
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected synchronized Message[] realExpunge() throws IOException {
|
|
|
|
|
|
|
|
|
|
|
|
// To compact a folder, we must have its message list around.
|
|
|
|
// This (probably) means parsing the summary file, which is important,
|
|
|
|
// because the summary file might be the only place where the proper
|
|
|
|
// flags live. One of the things that expunging does is ensure that
|
|
|
|
// every message in the folder has an X-Mozilla-Status header. The
|
|
|
|
// absence of that header is what can cause the situation that the
|
|
|
|
// summary file contains non-recomputable data.
|
|
|
|
//
|
|
|
|
// The pessimal case here is where we're compacting a folder that doesn't
|
|
|
|
// have a summary file: that will cause it to be fully parsed, and then
|
|
|
|
// read again to compact it. There's not a lot we can do about that.
|
|
|
|
//
|
|
|
|
ensureLoaded();
|
|
|
|
Assert.Assertion(fLoaded);
|
|
|
|
Assert.Assertion(mailSummaryFile != null);
|
|
|
|
|
|
|
|
// Before compacting the folder, ensure that any in-memory changes to
|
|
|
|
// the flags have been flushed out.
|
|
|
|
if (flags_dirty)
|
|
|
|
flushMozillaStatus();
|
|
|
|
|
|
|
|
boolean everything_completed_normally = false;
|
|
|
|
|
|
|
|
Vector old_msgs = fMessages;
|
|
|
|
Assert.Assertion(old_msgs != null);
|
|
|
|
|
|
|
|
try { // if something goes wrong, restore the value of fMessages
|
|
|
|
|
|
|
|
// We need to keep two copies of the folder's Message objects:
|
|
|
|
//
|
|
|
|
// * we need the old messages around in case there is an error,
|
|
|
|
// and we need to back out our changes;
|
|
|
|
//
|
|
|
|
// * we need the new messages so that we can correctly write a
|
|
|
|
// new summary file.
|
|
|
|
//
|
|
|
|
// Since the second *should* be a subset of the first, it would be
|
|
|
|
// good if we could manage to share the data; but I don't see an easy
|
|
|
|
// way to do that, and still be able to back out the changes if an error
|
|
|
|
// occurs (the expunging process wants to record a new file-offset into
|
|
|
|
// the message objects, among other things.)
|
|
|
|
//
|
|
|
|
// Another reason for keeping them is so that, at the end, we can tell
|
|
|
|
// all the observers that the first set is gone and the new set exists.
|
|
|
|
// We don't want to tell the observers anything if the attempt to
|
|
|
|
// compact the disk file is abortive.
|
|
|
|
//
|
|
|
|
// This does not mean that we're using twice as much memory, since all
|
|
|
|
// of these messages will be sharing strings and IDs by virtue of the
|
|
|
|
// ByteStringTable and MessageIDTable.
|
|
|
|
//
|
|
|
|
fMessages = new Vector(old_msgs.size());
|
|
|
|
|
|
|
|
|
|
|
|
try { // make sure unlockFolderFile() gets called.
|
|
|
|
lockFolderFile();
|
|
|
|
|
|
|
|
File tmp_file = tempFolderName();
|
|
|
|
|
|
|
|
try { // make sure the temp file gets deleted, if there's an error
|
|
|
|
|
|
|
|
RandomAccessFile in = null;
|
|
|
|
RandomAccessFile out = null;
|
|
|
|
boolean io_completed_normally = false;
|
|
|
|
|
|
|
|
try { // make sure in.close() and out.close() get called.
|
|
|
|
|
|
|
|
in = new RandomAccessFile(fFile, "r");
|
|
|
|
|
|
|
|
// make sure the tmp file doesn't exist (we have the folder locked,
|
|
|
|
// so if it does exist, it's probably left over from a crash.)
|
|
|
|
tmp_file.delete();
|
|
|
|
|
|
|
|
out = new RandomAccessFile(tmp_file, "rw");
|
|
|
|
|
|
|
|
ParseBerkeleyFolderAndExpunge expungator =
|
|
|
|
new ParseBerkeleyFolderAndExpunge();
|
|
|
|
expungator.expungeDeletedMessages(this, in, out, // do the deed
|
|
|
|
old_msgs.elements());
|
|
|
|
|
|
|
|
int total = expungator.totalMessageCount();
|
|
|
|
int undel = expungator.undeletedMessageCount();
|
|
|
|
int unread = expungator.unreadMessageCount();
|
|
|
|
long dbytes = expungator.deletedMessageBytes();
|
|
|
|
Assert.Assertion(total == undel);
|
|
|
|
Assert.Assertion(dbytes == 0);
|
|
|
|
mailSummaryFile.setFolderMessageCounts(total, undel,
|
|
|
|
unread, dbytes);
|
|
|
|
|
|
|
|
io_completed_normally = true;
|
|
|
|
|
|
|
|
} finally { // close in and out, and (if we didn't get an error)
|
|
|
|
// move the temp-file.
|
|
|
|
if (in != null)
|
|
|
|
in.close();
|
|
|
|
if (out != null)
|
|
|
|
out.close();
|
|
|
|
|
|
|
|
if (io_completed_normally) {
|
|
|
|
// Move the temp file to the folder file. This might get an
|
|
|
|
// error; that's ok, it will be caught because tmp_file will
|
|
|
|
// not be null.
|
|
|
|
tmp_file.renameTo(fFile);
|
|
|
|
tmp_file = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
} finally {
|
|
|
|
// if temp_file is non-null, then an error occurred.
|
|
|
|
if (tmp_file != null) {
|
|
|
|
try {
|
|
|
|
tmp_file.delete();
|
|
|
|
} catch (SecurityException e) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// At this point, the folder file has been successfully overwritten
|
|
|
|
// with new data. Write out a new summary file right now (we've
|
|
|
|
// already done a ton of disk I/O, a little bit more won't hurt.)
|
|
|
|
//
|
|
|
|
Assert.Assertion(fLoaded);
|
|
|
|
summary_dirty = true;
|
|
|
|
mailSummaryFile.setFolderDateAndSize(fFile.lastModified(),
|
|
|
|
fFile.length());
|
|
|
|
writeSummaryFile();
|
|
|
|
|
|
|
|
} finally {
|
|
|
|
unlockFolderFile();
|
|
|
|
}
|
|
|
|
|
|
|
|
everything_completed_normally = true;
|
|
|
|
|
|
|
|
} finally { // if something went wrong, restore the value of fMessages
|
|
|
|
|
|
|
|
if (!everything_completed_normally)
|
|
|
|
// something blew up; put back the old messages and pretend
|
|
|
|
// we didn't do anything.
|
|
|
|
fMessages = old_msgs;
|
|
|
|
}
|
|
|
|
|
|
|
|
// At this point, the new, compacted folder is on disk, and it has a
|
|
|
|
// summary. All that remains is to let the observers (if any) know
|
|
|
|
// that the message objects have changed.
|
|
|
|
//
|
|
|
|
// #### We could concievably do something clever here, by "zippering" the
|
|
|
|
// two message lists together. What we have (above) in fMessages and
|
|
|
|
// old_msgs looks something like
|
|
|
|
//
|
|
|
|
// old_msgs: A1 B1 C1 D1 E1 F1 G1
|
|
|
|
// fMessages: A2 C2 E2 F2
|
|
|
|
//
|
|
|
|
// A1 and A2 are non-identical BerkeleyMessage objects that represent
|
|
|
|
// the same message on disk. There are two things we could do:
|
|
|
|
//
|
|
|
|
// Option 1:
|
|
|
|
// * have an observable event that says "A2 replaces A1."
|
|
|
|
// * have an observable event that says "B1 has been expunged."
|
|
|
|
//
|
|
|
|
// Option 2:
|
|
|
|
//
|
|
|
|
// * copy the new contents of A2 into A1, and discard A2.
|
|
|
|
// * have an observable event that says "A1 has changed."
|
|
|
|
// * have an observable event that say "B1 has been expunged."
|
|
|
|
//
|
|
|
|
// I think I lean towards #2. But what we do today is:
|
|
|
|
//
|
|
|
|
// * say "A1 has been deleted."
|
|
|
|
// * say "A2 has been added".
|
|
|
|
//
|
|
|
|
// Actually, all of the comment above was written before we went to
|
|
|
|
// javamail APIs. Those APIs require approach #2 (well, I don't think
|
|
|
|
// we need the "A1 has changed" part), and so I attempt to
|
|
|
|
// implement that here... -- Terry
|
|
|
|
|
|
|
|
int oldsize = old_msgs.size();
|
|
|
|
int newsize = fMessages.size();
|
|
|
|
int j=0;
|
|
|
|
NEXTMESSAGE:
|
|
|
|
for (int i=0 ; i<newsize ; i++) {
|
|
|
|
BerkeleyMessage newmsg = (BerkeleyMessage) fMessages.elementAt(i);
|
|
|
|
while (j < oldsize) {
|
|
|
|
BerkeleyMessage old = (BerkeleyMessage) old_msgs.elementAt(j);
|
|
|
|
if (old.getMessageID() == newmsg.getMessageID()
|
|
|
|
// && Need more tests! ###
|
|
|
|
) {
|
|
|
|
old.setStorageFolderIndex(newmsg.getStorageFolderIndex());
|
|
|
|
old.setSize(newmsg.getSize());
|
|
|
|
fMessages.setElementAt(old, i);
|
|
|
|
old_msgs.removeElementAt(j);
|
|
|
|
oldsize--;
|
|
|
|
continue NEXTMESSAGE;
|
|
|
|
}
|
|
|
|
j++;
|
|
|
|
}
|
|
|
|
// Yikes!!! Somehow, we got new messages that didn't line up with
|
|
|
|
// the old ones. Um, well, such is life. Send a notification out that
|
|
|
|
// new messages appeared...
|
|
|
|
Vector newlist = new Vector();
|
|
|
|
for ( ; i<newsize ; i++) {
|
|
|
|
newlist.addElement(fMessages.elementAt(i));
|
|
|
|
}
|
|
|
|
Message[] tmp = new Message[newlist.size()];
|
|
|
|
newlist.copyInto(tmp);
|
|
|
|
notifyMessageAddedListeners(tmp);
|
|
|
|
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (j=0 ; j<oldsize ; j++) {
|
|
|
|
((BerkeleyMessage) old_msgs.elementAt(j)).setDeleted(true);
|
|
|
|
}
|
|
|
|
|
|
|
|
Message[] result = new Message[old_msgs.size()];
|
|
|
|
old_msgs.copyInto(result);
|
|
|
|
notifyMessageRemovedListeners(true/*???*/, result);
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private InternetHeaders copyMessageBytes(Message m,
|
|
|
|
InputStream in,
|
|
|
|
RandomAccessFile out)
|
|
|
|
throws IOException {
|
|
|
|
InternetHeaders headers = new InternetHeaders();
|
|
|
|
ByteBuf buf = null;
|
|
|
|
ByteBuf line = null;
|
|
|
|
|
|
|
|
try {
|
|
|
|
buf = ByteBuf.Alloc();
|
|
|
|
line = ByteBuf.Alloc();
|
|
|
|
|
|
|
|
// buf.append(Constants.LINEBREAK); // assume file ends in newline
|
|
|
|
buf.append(Constants.LINEBREAK);
|
|
|
|
makeDummyEnvelope(buf);
|
|
|
|
out.write(buf.toBytes(), 0, buf.length());
|
|
|
|
|
|
|
|
ByteLineBuffer linebuf = new ByteLineBuffer();
|
|
|
|
linebuf.setOutputEOL(Constants.BYTEBUFLINEBREAK);
|
|
|
|
|
|
|
|
boolean eof = false;
|
|
|
|
boolean inheader = true;
|
|
|
|
|
|
|
|
int content_length = 0;
|
|
|
|
long content_length_header_pos = -1;
|
|
|
|
|
|
|
|
while (!eof) {
|
|
|
|
buf.setLength(0);
|
|
|
|
eof = (buf.read(in, 1024) < 0);
|
|
|
|
if (eof) {
|
|
|
|
linebuf.pushEOF();
|
|
|
|
} else {
|
|
|
|
linebuf.pushBytes(buf);
|
|
|
|
}
|
|
|
|
|
|
|
|
while (linebuf.pullLine(line)) {
|
|
|
|
if (inheader) {
|
|
|
|
byte first = line.byteAt(0);
|
|
|
|
if (first == '\r' || first == '\n') {
|
|
|
|
inheader = false;
|
|
|
|
|
|
|
|
// Once we're at the end of the headers, write out the
|
|
|
|
// updated X-Mozilla-Status header and a dummy
|
|
|
|
// Content-Length header.
|
|
|
|
//
|
|
|
|
long xms = BerkeleyMessage.makeMozillaFlags(m);
|
|
|
|
|
|
|
|
line.setLength(0);
|
|
|
|
line.append("X-Mozilla-Status: ");
|
|
|
|
String s = Long.toHexString(xms);
|
|
|
|
int i = s.length();
|
|
|
|
if (i == 1) line.append("000");
|
|
|
|
else if (i == 2) line.append("00");
|
|
|
|
else if (i == 3) line.append("0");
|
|
|
|
line.append(s);
|
|
|
|
line.append(Constants.LINEBREAK);
|
|
|
|
|
|
|
|
// Put this line (the X-Mozilla-Status header) in the headers
|
|
|
|
// object and in the file.
|
|
|
|
headers.addHeaderLine(line.toString());
|
|
|
|
out.write(line.toBytes(), 0, line.length());
|
|
|
|
|
|
|
|
|
|
|
|
// To generate the Content-Length header, we write out a
|
|
|
|
// dummy header (one containing only spaces) and then we
|
|
|
|
// overwrite it later, once we've written all the data.
|
|
|
|
// jwz thinks this is incredibly cheesy, but terry thinks
|
|
|
|
// it's simple and elegant.
|
|
|
|
//
|
|
|
|
line.setLength(0);
|
|
|
|
line.append("Content-Length: ");
|
|
|
|
out.write(line.toBytes(), 0, line.length());
|
|
|
|
|
|
|
|
// remember the position at which to drop down the number.
|
|
|
|
content_length_header_pos = out.getFilePointer();
|
|
|
|
|
|
|
|
// write out the blank header field, and terminal newline.
|
|
|
|
// leave room for 10 characters (2^32 in decimal.)
|
|
|
|
line.setLength(0);
|
|
|
|
line.append(" ");
|
|
|
|
line.append(Constants.LINEBREAK);
|
|
|
|
out.write(line.toBytes(), 0, line.length());
|
|
|
|
|
|
|
|
// Now write newline.
|
|
|
|
line.setLength(0);
|
|
|
|
line.append(Constants.LINEBREAK);
|
|
|
|
out.write(line.toBytes(), 0, line.length());
|
|
|
|
|
|
|
|
} else {
|
|
|
|
// we're still in the headers.
|
|
|
|
|
|
|
|
// Don't write out the X-Mozilla-Status or Content-Length
|
|
|
|
// headers yet.
|
|
|
|
if ((first == 'x' || first == 'X') &&
|
|
|
|
line.regionMatches(true, 0, "X-Mozilla-Status:", 0, 17)) {
|
|
|
|
// ignore existing value of this header
|
|
|
|
} else if ((first == 'c' || first == 'C') &&
|
|
|
|
line.regionMatches(true, 0, "Content-Length:",
|
|
|
|
0, 15)) {
|
|
|
|
// ignore existing value of this header
|
|
|
|
} else {
|
|
|
|
// Not header we want to ignore and regenerate -- write it.
|
|
|
|
headers.addHeaderLine(line.toString());
|
|
|
|
out.write(line.toBytes(), 0, line.length());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
} else {
|
|
|
|
// not in headers -- pass it through.
|
|
|
|
// also compute correct value for Content-Length.
|
|
|
|
|
|
|
|
byte b[] = line.toBytes();
|
|
|
|
int L = line.length();
|
|
|
|
|
|
|
|
// Mangle "From " to ">From ".
|
|
|
|
if (L > 4 &&
|
|
|
|
b[0] == 'F' && b[1] == 'r' && b[2] == 'o' && b[3] == 'm' &&
|
|
|
|
b[4] == ' ') {
|
|
|
|
out.write((int) '>');
|
|
|
|
content_length++;
|
|
|
|
}
|
|
|
|
|
|
|
|
out.write(b, 0, L);
|
|
|
|
content_length += L;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// We have written out the whole message, and now know the correct
|
|
|
|
// value for Content-Length. Go back and finish the job, overwriting
|
|
|
|
// the dummy header we left earlier.
|
|
|
|
//
|
|
|
|
if (content_length_header_pos > 0 &&
|
|
|
|
content_length >= 0) {
|
|
|
|
|
|
|
|
// write to the file if CL will fit in ten digits (pretty likely...)
|
|
|
|
//
|
|
|
|
if (content_length <= 9999999999L) {
|
|
|
|
out.seek(content_length_header_pos);
|
|
|
|
line.setLength(0);
|
|
|
|
line.append(Long.toString(content_length));
|
|
|
|
out.write(line.toBytes(), 0, line.length());
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add this new Content-Length value into the Header object.
|
|
|
|
line.setLength(0);
|
|
|
|
line.append("Content-Length: ");
|
|
|
|
line.append(Long.toString(content_length));
|
|
|
|
line.append(Constants.LINEBREAK);
|
|
|
|
headers.addHeaderLine(line.toString());
|
|
|
|
}
|
|
|
|
|
|
|
|
} finally {
|
|
|
|
if (buf != null) ByteBuf.Recycle(buf);
|
|
|
|
if (line != null) ByteBuf.Recycle(line);
|
|
|
|
}
|
|
|
|
return headers;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private Object disk_lock_object = null;
|
|
|
|
|
|
|
|
/** On those systems which support locking of disk files, this causes
|
|
|
|
a lock to be acquired on the file associated with this folder.
|
|
|
|
It is imperative that a balanced unlockFolderFile() call be made:
|
|
|
|
be sure to only use this within try/finally.
|
|
|
|
*/
|
|
|
|
void lockFolderFile() {
|
|
|
|
// System.err.println("locking file " + fFile);
|
|
|
|
|
|
|
|
if (disk_lock_object != null) {
|
|
|
|
// Nested locks aren't allowed...
|
|
|
|
Assert.Assertion(false);
|
|
|
|
|
|
|
|
} else if (false) { // #### change this test to `Constants.ISUNIX' once
|
|
|
|
// #### the UnixDotLock class actually works.
|
|
|
|
try {
|
|
|
|
|
|
|
|
disk_lock_object = new UnixDotLock(fFile);
|
|
|
|
|
|
|
|
} catch (IOException e) {
|
|
|
|
// Ignore it, I guess -- it's ok to try and visit a folder in a
|
|
|
|
// read-only directory, or in someone else's directory, and in that
|
|
|
|
// case we won't be able to acquire the lock.
|
|
|
|
// (Or would that be a SecurityException?)
|
|
|
|
System.err.println("warning: I/O error when trying to lock folder:"
|
|
|
|
+ e);
|
|
|
|
|
|
|
|
} catch (InterruptedException e) {
|
|
|
|
// #### GAG, what do I do here??
|
|
|
|
// I don't really want the fact that class UnixDotLock calls wait()
|
|
|
|
// to escape up to the higher levels. What to do...
|
|
|
|
System.err.println("exception ignored: " + e);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} else {
|
|
|
|
// Non-Unix -- put some random non-null object in the lock slot.
|
|
|
|
disk_lock_object = Boolean.TRUE;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Releases the lock obtained by lockFolderFile(). */
|
|
|
|
void unlockFolderFile() {
|
|
|
|
// System.err.println("unlocking file " + fFile);
|
|
|
|
|
|
|
|
if (disk_lock_object == null) {
|
|
|
|
// It should be locked, if we're unlocking it...
|
|
|
|
Assert.Assertion(false);
|
|
|
|
} else {
|
|
|
|
Object o = disk_lock_object;
|
|
|
|
disk_lock_object = null;
|
|
|
|
if (o != Boolean.TRUE) // see above
|
|
|
|
((UnixDotLock) o).unlock();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static private final String MONTHS[] = {"Jan", "Feb", "Mar", "Apr",
|
|
|
|
"May", "Jun", "Jul", "Aug",
|
|
|
|
"Sep", "Oct", "Nov", "Dec"};
|
|
|
|
static private final String DAYS[] = {"Mon", "Tue", "Wed", "Thu",
|
|
|
|
"Fri", "Sat", "Sun"};
|
|
|
|
|
|
|
|
static DecimalFormat TwoDigitNumFormat = new DecimalFormat("00");
|
|
|
|
protected String TwoDigitNum(int num) {
|
|
|
|
return TwoDigitNumFormat.format(num);
|
|
|
|
}
|
|
|
|
|
|
|
|
protected void makeDummyEnvelope(ByteBuf buf) {
|
|
|
|
Calendar c = new GregorianCalendar();
|
|
|
|
c.setTime(new Date());
|
|
|
|
|
|
|
|
int zone = (c.get(Calendar.ZONE_OFFSET) +
|
|
|
|
c.get(Calendar.DST_OFFSET)) / (1000 * 60);
|
|
|
|
|
|
|
|
boolean zonenegative = (zone < 0);
|
|
|
|
zone = Math.abs(zone);
|
|
|
|
int zonehours = zone / 60;
|
|
|
|
int zoneminutes = zone % 60;
|
|
|
|
|
|
|
|
buf.append("From - ");
|
|
|
|
buf.append(DAYS[c.get(Calendar.DAY_OF_WEEK) - 1]);
|
|
|
|
buf.append(", ");
|
|
|
|
buf.append(c.get(Calendar.DATE));
|
|
|
|
buf.append(" ");
|
|
|
|
buf.append(MONTHS[c.get(Calendar.MONTH)]);
|
|
|
|
buf.append(" ");
|
|
|
|
buf.append(c.get(Calendar.YEAR));
|
|
|
|
buf.append(" ");
|
|
|
|
buf.append(TwoDigitNum(c.get(Calendar.HOUR_OF_DAY)));
|
|
|
|
buf.append(":");
|
|
|
|
buf.append(TwoDigitNum(c.get(Calendar.MINUTE)));
|
|
|
|
buf.append(":");
|
|
|
|
buf.append(TwoDigitNum(c.get(Calendar.SECOND)));
|
|
|
|
buf.append(" ");
|
|
|
|
buf.append(zonenegative ? "-" : "+");
|
|
|
|
buf.append(TwoDigitNum(zonehours));
|
|
|
|
buf.append(TwoDigitNum(zoneminutes));
|
|
|
|
buf.append(Constants.LINEBREAK);
|
|
|
|
}
|
|
|
|
|
|
|
|
// ### I'm too lazy to port this test right now to the new world, so I'm
|
|
|
|
// just commenting it out. - Terry
|
|
|
|
//
|
|
|
|
// public static final void main(String av[]) throws Exception {
|
|
|
|
//
|
|
|
|
// BerkeleyFolder f3 = new BerkeleyFolder(new File("/u/jwz/tmp/f3"), "f3");
|
|
|
|
//// f3.ensureLoaded();
|
|
|
|
// f3.expunge();
|
|
|
|
//
|
|
|
|
///*
|
|
|
|
// BerkeleyFolder f1 = new BerkeleyFolder(new File("/u/jwz/tmp/f1"), "f1");
|
|
|
|
// BerkeleyFolder f2 = new BerkeleyFolder(new File("/u/jwz/tmp/f2"), "f2");
|
|
|
|
//
|
|
|
|
// Enumeration e1 = f1.getMessages();
|
|
|
|
//// Enumeration e2 = f2.getMessages();
|
|
|
|
// BerkeleyMessage m1 = (BerkeleyMessage) e1.nextElement();
|
|
|
|
//
|
|
|
|
// System.err.println("flagged = " + m1.isFlagged() + " (" + m1.flags + ")");
|
|
|
|
// m1.setFlagged(!m1.isFlagged());
|
|
|
|
// System.err.println("flagged = " + m1.isFlagged() + " (" + m1.flags + ")");
|
|
|
|
//
|
|
|
|
// f2.addMessage(m1);
|
|
|
|
// System.err.println("sleeping");
|
|
|
|
// Thread.sleep(50 * 1000);
|
|
|
|
// System.err.println("exiting");
|
|
|
|
//*/
|
|
|
|
// }
|
|
|
|
|
|
|
|
public boolean delete(boolean value) {
|
|
|
|
Assert.NotYetImplemented("BerkeleyFolder.delete");
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
public boolean renameTo(Folder f) {
|
|
|
|
Assert.NotYetImplemented("BerkeleyFolder.renameTo");
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
public void open(int mode) {
|
|
|
|
ensureLoaded();
|
|
|
|
}
|
|
|
|
|
|
|
|
public void close(boolean doExpunge) throws MessagingException {
|
|
|
|
if (doExpunge) expunge();
|
|
|
|
// ### Throw away message array and stuff?
|
|
|
|
}
|
|
|
|
|
|
|
|
public boolean isOpen() {
|
|
|
|
return true; // ### Wrong!
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** This class implements a thread which runs in the background and writes
|
|
|
|
out all modified mail summary files. This thread dies once the files
|
|
|
|
have been written; it will be re-spawned once some folder or folders
|
|
|
|
become dirty.
|
|
|
|
|
|
|
|
<P> There is only ever one of these threads: it writes the summaries for
|
|
|
|
all modified folders, serially, then exits.
|
|
|
|
*/
|
|
|
|
class SummaryUpdateThread extends Thread {
|
|
|
|
|
|
|
|
static int delay = 30; // seconds
|
|
|
|
static SummaryUpdateThread the_thread = null; // singleton
|
|
|
|
|
|
|
|
private Vector dirty_folders = new Vector();
|
|
|
|
|
|
|
|
static synchronized SummaryUpdateThread Get() {
|
|
|
|
if (the_thread == null || !the_thread.isAlive()) {
|
|
|
|
the_thread = new SummaryUpdateThread();
|
|
|
|
the_thread.setDaemon(true);
|
|
|
|
the_thread.setName(the_thread.getClass().getName());
|
|
|
|
the_thread.start();
|
|
|
|
}
|
|
|
|
return the_thread;
|
|
|
|
}
|
|
|
|
|
|
|
|
synchronized void addDirtyFolder(BerkeleyFolder f) {
|
|
|
|
if (!dirty_folders.contains(f))
|
|
|
|
dirty_folders.addElement(f);
|
|
|
|
}
|
|
|
|
|
|
|
|
public void run() {
|
|
|
|
|
|
|
|
try {
|
|
|
|
sleep(delay * 1000);
|
|
|
|
} catch (InterruptedException e) {
|
|
|
|
return; // is this the right way to do this?
|
|
|
|
}
|
|
|
|
|
|
|
|
synchronized (this.getClass()) {
|
|
|
|
// Do this first, so that if someone calls add() just as we're coming
|
|
|
|
// out of the loop, below, the thing they've added doesn't get lost.
|
|
|
|
the_thread = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (int i = 0; i < dirty_folders.size(); i++) {
|
|
|
|
BerkeleyFolder f = (BerkeleyFolder) dirty_folders.elementAt(i);
|
|
|
|
try {
|
|
|
|
|
|
|
|
System.err.println("background thread writing summary file for " + f.fFile);
|
|
|
|
|
|
|
|
f.writeSummaryFile();
|
|
|
|
} catch (Exception e) {
|
|
|
|
f.setSummaryDirty(true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** This class implements a thread which runs in the background and overwrites
|
|
|
|
the changed X-Mozilla-Status headers in a particular folder. This thread
|
|
|
|
dies once the file has been updated; it will be re-spawned once some
|
|
|
|
folder's messages become dirty again.
|
|
|
|
|
|
|
|
<P> There can be one of these threads per folder.
|
|
|
|
*/
|
|
|
|
class StatusFlagsUpdateThread extends Thread {
|
|
|
|
|
|
|
|
static int delay = 5; // seconds
|
|
|
|
BerkeleyFolder folder = null;
|
|
|
|
|
|
|
|
StatusFlagsUpdateThread(BerkeleyFolder folder) {
|
|
|
|
this.folder = folder;
|
|
|
|
setDaemon(true);
|
|
|
|
setName(getClass().getName() + " " + folder.fFile);
|
|
|
|
}
|
|
|
|
|
|
|
|
public void run() {
|
|
|
|
|
|
|
|
try {
|
|
|
|
sleep(delay * 1000);
|
|
|
|
} catch (InterruptedException e) {
|
|
|
|
return; // is this the right way to do this?
|
|
|
|
}
|
|
|
|
|
|
|
|
System.err.println("background thread updating X-Mozilla-Status headers " +
|
|
|
|
"in " + folder.fFile);
|
|
|
|
try {
|
|
|
|
synchronized (folder) {
|
|
|
|
folder.statusFlagsUpdateThread = null;
|
|
|
|
folder.flushMozillaStatus();
|
|
|
|
}
|
|
|
|
} catch (IOException e) {
|
|
|
|
// nothing to be done?
|
|
|
|
System.err.println("IOException while writing X-Mozilla-Status: " + e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|