/* * The contents of this file are subject to the Mozilla Public License * Version 1.1 (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 Kowari Metadata Store. * * The Initial Developer of the Original Code is Plugged In Software Pty * Ltd (http://www.pisoftware.com, mailto:info@pisoftware.com). Portions * created by Plugged In Software Pty Ltd are Copyright (C) 2001,2002 * Plugged In Software Pty Ltd. All Rights Reserved. * * Contributor(s): N/A. * * [NOTE: The text of this Exhibit A may differ slightly from the text * of the notices in the Source Code files of the Original Code. You * should use the text of this Exhibit A rather than the text found in the * Original Code Source Code for Your Modifications.] * */ package org.mulgara.content.mbox.parser; import java.io.*; import java.net.*; import java.text.*; import java.util.*; import javax.mail.*; import javax.mail.event.*; import javax.mail.internet.*; import javax.mail.search.*; import org.apache.log4j.*; /* * MboxFolder.java * Copyright (C) 1999 dog <dog@dog.net.uk> * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * You also have permission to link it with the Sun Microsystems, Inc. * JavaMail(tm) extension and run that combination. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * You may retrieve the latest version of this library from * http://www.dog.net.uk/knife/ * * Contributor(s): Daniel Thor Kristjan <danielk@cat.nyu.edu> close and expunge clarification. * Sverre Huseby <sverrehu@online.no> gzipped mailboxes */ /** * The folder class implementing a UNIX mbox-format mailbox. * <P/> * * This code is derived from the 'knife' mail and news client * at http://www.dog.net.uk/knife. * <P/> * * @created 2001-8-21 * * @author dog@dog.net.uk * @author Andrew Newman * @author Ben Warren * @author Mark Ludlow * * @version $Revision: 1.8 $ * * @modified $Date: 2005/01/05 04:57:41 $ * * @maintenanceAuthor $Author: newmana $ * * @company <A href="mailto:info@PIsoftware.com">Plugged In Software</A> * * @copyright © 2001 * <A href="http://www.PIsoftware.com/">Plugged In Software Pty Ltd</A> * * @licence <A href="{@docRoot}/../../LICENCE_LGPL.txt">Licence description</A> */ public class MboxFolderImpl extends Folder implements MboxFolder { /** The category to log to. */ private static final Logger logger = Logger.getLogger(MboxFolderImpl.class); static final DateFormat df = new SimpleDateFormat("EEE MMM d H:m:s yyyy"); public Flags permanentFlags = null; private MboxReader reader; private ArrayList<Message> messages; private boolean open = false; private boolean readOnly = true; private int type = HOLDS_MESSAGES; private boolean inbox = false; private long fileLastModified = 0; /** * Constructor. */ protected MboxFolderImpl(Store store, String filename, boolean isInbox) { super(store); reader = new MboxReader(this, filename); if (reader.getFile().exists() && reader.getFile().isDirectory()) { type = HOLDS_FOLDERS; } inbox = isInbox; } /** * Constructor. */ protected MboxFolderImpl(Store store, String filename) { this(store, filename, false); } /** * Return the file used to access/store the mail box. * * @return the file used to access/store the mail box. */ public File getFile() { return reader.getFile(); } /** * Return the name of the mail box. * * @return the name of the mail box. */ public String getName() { if (!inbox) { return getFile().getName(); } else { return ("INBOX"); } } /** * Return the full path of the mail box. * * @return the full path of the mail box. */ public String getFullName() { if (!inbox) { return getFile().getAbsolutePath(); } else { return ("INBOX"); } } /** * Return the mail folder type (HOLDS_MESSAGES). * * @return the mail folder type (HOLDS_MESSAGES). */ public int getType() throws MessagingException { return type; } /** * Indicates whether this folder exists. * * @exception MessagingException if a messaging error occurred */ public boolean exists() throws MessagingException { return getFile().exists(); } /** * Indicates whether this folder contains new messages. * * @exception MessagingException if a messaging error occurred */ public boolean hasNewMessages() throws MessagingException { return getNewMessageCount() > 0; } /**s * Opens this folder. * * @exception MessagingException if a messaging error occurred */ public void open(int mode) throws MessagingException { switch (mode) { case READ_WRITE: { if (!reader.canWrite()) { throw new MessagingException("Folder is read-only"); } readOnly = false; break; } case READ_ONLY: { //Call the index method which will index the file if there hasn't //been one created. reader.indexMbox(); } } if (!reader.canRead()) { throw new MessagingException("Can't read folder: " + getFile().getAbsolutePath()); } if (getFile().length() == 0) { // empty file //log.info("Mail file is empty!" + getFile().getAbsolutePath()); } else { if (!reader.isMboxFormat()) { throw new MessagingException("Mailbox format error", new ProtocolException()); } } open = true; notifyConnectionListeners(ConnectionEvent.OPENED); } /** * Closes this folder. * @param expunge if the folder is to be expunged before it is closed * @exception MessagingException if a messaging error occurred */ public void close(boolean expunge) throws MessagingException { if (open) { if (expunge) { expunge(); } open = false; notifyConnectionListeners(ConnectionEvent.CLOSED); if (!readOnly) { synchronizeMessages(); saveMessages(); } } if (((MboxStore)store).getSession().getDebug()) logger.debug("mbox: closing "+getFile().getAbsolutePath()); } /** * Expunges this folder. * This deletes all the messages marked as deleted. * @exception MessagingException if a messaging error occurred */ public synchronized Message[] expunge() throws MessagingException { ArrayList<Message> ve = new ArrayList<Message>(); if (open && this.messages != null) { ArrayList<Message> vm = new ArrayList<Message>(); Iterator<Message> iter = messages.iterator(); while (iter.hasNext()) { Message message = iter.next(); Flags flags = message.getFlags(); if (flags.contains(Flags.Flag.DELETED)) { ve.add(message); } else { vm.add(message); } } messages = vm; } Message[] expunged = collectionToMessageArray(ve); if (expunged.length > 0) { notifyMessageRemovedListeners(true, expunged); } return expunged; } /** * Indicates whether this folder is open. */ public boolean isOpen() { return open; } /** * Returns the permanent flags for this folder. */ public Flags getPermanentFlags() { if (permanentFlags == null) { Flags tmpFlags = new Flags(); tmpFlags.add(Flags.Flag.DELETED); tmpFlags.add(Flags.Flag.SEEN); tmpFlags.add(Flags.Flag.RECENT); permanentFlags = tmpFlags; } return permanentFlags; } /** * Returns the number of messages in this folder. Calls the getMessageCount. * * @return the number of messages in this folder. * @exception MessagingException if a messaging error occurred */ public int getMessageCount() throws MessagingException { return reader.getMessageCount(); } /** * Returns the specified message number from this folder. * * @exception MessagingException if a messaging error occurred */ public Message getMessage(int msgnum) throws MessagingException { if (logger.isDebugEnabled()) { logger.debug("Retrieving message number " + msgnum); } // Use a reader to get that getFile(). try { return reader.getMessage(msgnum - 1); } catch (ArrayIndexOutOfBoundsException e) { throw new MessagingException("No such message", e); } } /** * Returns the messages in this folder. * * @exception MessagingException if a messaging error occurred */ public synchronized Message[] getMessages() throws MessagingException { this.synchronizeMessages(); this.saveMessages(); return this.collectionToMessageArray(messages); } /** * Appends messages to this folder. */ public synchronized void appendMessages(Message[] messages) throws MessagingException { synchronizeMessages(); ArrayList<Message> added = new ArrayList<Message>(); for (int i = 0; i < messages.length; i++) { if (messages[i] instanceof MimeMessage) { MboxMessage message = new MboxMessage(this, (MimeMessage)messages[i], i); added.add(message); this.messages.add(message); } } if (added.size() > 0) { Message[] n = this.collectionToMessageArray(added); notifyMessageAddedListeners(n); } saveMessages(); } /** * Does nothing. * The messages <i>must</i> be fetched in their entirety by getMessages() - * this is the nature of the Mbox protocol. * * @exception MessagingException ignore */ public void fetch(Message amessage[], FetchProfile fetchprofile) throws MessagingException { } /** * Returns the parent folder. * * @return the parent folder. */ public Folder getParent() throws MessagingException { return store.getFolder(getFile().getParent()); } /** * Returns the subfolders of this folder. * * @return the subfolders of this folder. */ public Folder[] list() throws MessagingException { if (type != HOLDS_FOLDERS) { throw new MessagingException("This folder can't contain subfolders"); } try { String[] files = getFile().list(); Folder[] folders = new Folder[files.length]; for (int i = 0; i < files.length; i++) { folders[i] = store.getFolder(getFile().getAbsolutePath() + File.separator + files[i]); } return folders; } catch (SecurityException e) { throw new MessagingException("Access denied", e); } } /** * Returns the subfolders of this folder matching the specified pattern. */ public Folder[] list(String pattern) throws MessagingException { if (type!=HOLDS_FOLDERS) { throw new MessagingException("This folder can't contain subfolders"); } try { String[] files = getFile().list(new MboxFilenameFilter(pattern)); Folder[] folders = new Folder[files.length]; for (int i=0; i<files.length; i++) { folders[i] = store.getFolder(getFile().getAbsolutePath() + File.separator + files[i]); } return folders; } catch (SecurityException e) { throw new MessagingException("Access denied", e); } } /** * Returns the separator character. * * @return the separator character. */ public char getSeparator() throws MessagingException { return File.separatorChar; } /** * Creates this folder in the store. */ public boolean create(int type) throws MessagingException { if (getFile().exists()) throw new MessagingException("Folder already exists"); switch (type) { case HOLDS_FOLDERS: try { if (getFile().mkdirs()) { this.type = type; notifyFolderListeners(FolderEvent.CREATED); return true; } throw new MessagingException("Could not create folder: " + getFile()); } catch (SecurityException e) { throw new MessagingException("Access denied", e); } case HOLDS_MESSAGES: try { // save the changes synchronized (this) { if (messages == null) messages = new ArrayList<Message>(); OutputStream os = new BufferedOutputStream(getOutputStream()); try { Message[] m = this.collectionToMessageArray(this.messages); for (int i = 0; i < m.length; i++) { Address[] f = m[i].getFrom(); String top = "From " + ((f.length>0) ? f[0].toString() : "-") + " "+ df.format(m[i].getSentDate()) + "\n"; os.write(top.getBytes()); m[i].writeTo(os); } } finally { os.close(); } } this.type = type; notifyFolderListeners(FolderEvent.CREATED); return true; } catch (IOException e) { throw new MessagingException("I/O error writing mailbox", e); } catch (SecurityException e) { throw new MessagingException("Access denied", e); } } return false; } /** * Deletes this folder. */ public boolean delete(boolean recurse) throws MessagingException { if (recurse) { try { if (type == HOLDS_FOLDERS) { Folder[] folders = list(); for (int i = 0; i < folders.length; i++) { if (!folders[i].delete(recurse)) return false; } } getFile().delete(); notifyFolderListeners(FolderEvent.DELETED); return true; } catch (SecurityException e) { throw new MessagingException("Access denied", e); } } else { try { if (type == HOLDS_FOLDERS) { Folder[] folders = list(); if (folders.length > 0) return false; } getFile().delete(); notifyFolderListeners(FolderEvent.DELETED); return true; } catch (SecurityException e) { throw new MessagingException("Access denied", e); } } } /** * Mbox folders cannot be created, deleted, or renamed. */ public boolean renameTo(Folder folder) throws MessagingException { try { String filename = folder.getFullName(); if (filename != null) { getFile().renameTo(new File(filename)); ((MboxStoreImpl)store).folders.clear(); notifyFolderListeners(FolderEvent.RENAMED); return true; } else { throw new MessagingException("Illegal filename: null"); } } catch (SecurityException e) { throw new MessagingException("Access denied", e); } } /** * Mbox folders cannot contain subfolders. */ public Folder getFolder(String filename) throws MessagingException { return store.getFolder(getFile().getAbsolutePath() + File.separator + filename); } public Message[] search(SearchTerm term) throws MessagingException { return super.search(term); } public Message[] search(SearchTerm term, Message[] messages) throws MessagingException { return super.search(term, messages); } /** * Locks this mailbox. Not implementented yet. */ public synchronized boolean acquireLock() { return true; } /** * Unlocks this mailbox. Not implemented yet. */ public synchronized boolean releaseLock() { return true; } // Reads messages from the disk getFile(). private ArrayList<Message> readMessages() throws MessagingException { synchronized (this) { return reader.getMessagesAsArrayList(0, reader.getMessageCount() - 1); } } /** * Synchronizes the source file with the current message list. */ private void synchronizeMessages() throws MessagingException { // Modified this to speed it up. if (getFile().lastModified() != fileLastModified) { fileLastModified = getFile().lastModified(); // we should never be in the position where we've removed messages // that haven't been removed from the file itself. at least, let's // hope so. :) // // it should also be the case that messages are only appended to // the file, so if we find a message that doesn't correspond to a // current message, then it should be both a new message, and have // no old messages after it. // // FIXME: these are both really, really, really bad assumptions. // ArrayList<Message> tmpMessages = readMessages(); if (messages == null) { messages = tmpMessages; } else { ArrayList<Message> messagesAdded = new ArrayList<Message>(); ArrayList<Message> finalMessages = new ArrayList<Message>(); int j = -1; MboxMessage tmpMessage; for (int index =0; index < tmpMessages.size(); index++) { tmpMessage = (MboxMessage) tmpMessages.get(index); String tmpUniqueId = tmpMessage.getMessageID(); String uniqueId = null; while (uniqueId != tmpUniqueId && (!tmpUniqueId.equals(uniqueId)) && j < messages.size() - 1) { uniqueId = ((MboxMessage)messages.get(++j)).getMessageID(); } if (j < messages.size() -1) { finalMessages.add(messages.get(j)); } else { Message newMessage = tmpMessages.get(index); finalMessages.add(newMessage); messagesAdded.add(newMessage); } } messages = finalMessages; if (messagesAdded.size() > 0) { Message[] n = collectionToMessageArray(messagesAdded); notifyMessageAddedListeners(n); saveMessages(); } } } } // Saves messages to the disk getFile(). private void saveMessages() throws MessagingException { if (readOnly) { return; } synchronized (this) { if (messages != null) { try { Message[] m = this.collectionToMessageArray(messages); // make sure content has been retrieved for all messages for (int i = 0; i < m.length; i++) { if (m[i] instanceof MboxMessage) { ((MboxMessage)m[i]).retrieveContent(); } } OutputStream os = new BufferedOutputStream(getOutputStream()); try { MboxOutputStream mos = new MboxOutputStream(os); for (int i = 0; i < m.length; i++) { Address[] f = m[i].getFrom(); String from = "-"; if (f.length > 0) { if (f[0] instanceof InternetAddress) { from = ((InternetAddress)f[0]).getAddress(); } else { from = f[0].toString(); } } Date date = m[i].getSentDate(); if (date == null) date = m[i].getReceivedDate(); if (date == null) date = new Date(); String top = "From " + from + " " + df.format(date) + "\n"; os.write(top.getBytes()); m[i].writeTo(mos); mos.flush(); } } finally { os.close(); } fileLastModified = getFile().lastModified(); } catch (IOException e) { throw new MessagingException("I/O error writing mailbox", e); } } } } /** * Creates an output stream that possibly will compress * whatever is sent to it, based on the current filename. */ private OutputStream getOutputStream() throws IOException { return reader.getOutputStream(); } /** * A helper method that converts the collection to a message array. * * @param source the source collection (array list usually), which is assumed * to contain Message objects. * @return the list of messages contained in the collection. */ private Message[] collectionToMessageArray(Collection<Message> source) { return source.toArray(new Message[source.size()]); } /** * A filter to return a list of files based on patters on * and %. */ class MboxFilenameFilter implements FilenameFilter { String pattern; int asteriskIndex, percentIndex; MboxFilenameFilter(String pattern) { this.pattern = pattern; asteriskIndex = pattern.indexOf('*'); percentIndex = pattern.indexOf('%'); } public boolean accept(File directory, String name) { if (asteriskIndex > -1) { String start = pattern.substring(0, asteriskIndex), end = pattern.substring(asteriskIndex+1, pattern.length()); return (name.startsWith(start) && name.endsWith(end)); } else if (percentIndex > -1) { String start = pattern.substring(0, percentIndex), end = pattern.substring(percentIndex+1, pattern.length()); return (directory.equals(getFile()) && name.startsWith(start) && name.endsWith(end)); } return name.equals(pattern); } } }