/* * MboxFolder.java * Copyright (C) 1999 Chris Burdess * * This file is part of GNU JavaMail, a library. * * GNU JavaMail is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * GNU JavaMail 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 General Public License for more details. * * You should have received a copy of the GNU 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 * * As a special exception, if you link this library with other files to * produce an executable, this library does not by itself cause the * resulting executable to be covered by the GNU General Public License. * This exception does not however invalidate any other reasons why the * executable file might be covered by the GNU General Public License. * * Contributor(s): Daniel Thor Kristjan * close and expunge clarification. * Sverre Huseby gzipped mailboxes */ package gnu.mail.providers.mbox; import java.io.*; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; import javax.mail.Address; import javax.mail.Flags; import javax.mail.Folder; import javax.mail.Message; import javax.mail.MessagingException; import javax.mail.Store; import javax.mail.URLName; import javax.mail.event.ConnectionEvent; import javax.mail.event.FolderEvent; import javax.mail.internet.AddressException; import javax.mail.internet.InternetAddress; import javax.mail.internet.MimeMessage; import gnu.inet.util.LineInputStream; import gnu.mail.treeutil.StatusEvent; /** * The folder class implementing a UNIX mbox-format mailbox. * * @author Chris Burdess */ public class MboxFolder extends Folder { static final DateFormat df = new SimpleDateFormat("EEE MMM d H:m:s yyyy"); static final String GNU_MESSAGE_ID = "X-GNU-Message-Id"; static final String FROM = "From "; File file; MboxMessage[] messages; boolean open; boolean readOnly; int type; boolean inbox; Flags permanentFlags = null; /** * Constructor. */ protected MboxFolder(Store store, String filename, boolean inbox) { super(store); file = new File(filename); if (file.exists() && file.isDirectory()) type = HOLDS_FOLDERS; else type = HOLDS_MESSAGES; this.inbox = inbox; open = false; readOnly = true; messages = new MboxMessage[0]; } /** * Constructor. */ protected MboxFolder(Store store, String filename) { this(store, filename, false); } /** * Returns the name of this folder. */ public String getName() { if (inbox) return "INBOX"; return file.getName(); } /** * Returns the full name of this folder. */ public String getFullName() { if (inbox) return "INBOX"; return file.getPath(); } /** * Return a URLName representing this folder. */ public URLName getURLName() throws MessagingException { URLName url = super.getURLName(); return new URLName(url.getProtocol(), null, -1, url.getFile(), null, null); } /** * Returns the type of this folder. * @exception MessagingException if a messaging error occurred */ 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 file.exists(); } /** * Indicates whether this folder contains new messages. * @exception MessagingException if a messaging error occurred */ public boolean hasNewMessages() throws MessagingException { return getNewMessageCount()>0; } /** * Opens this folder. * If the folder is opened for writing, a lock must be acquired on the * mbox. If this fails a MessagingException is thrown. * @exception MessagingException if a messaging error occurred */ public void open(int mode) throws MessagingException { String filename = file.getAbsolutePath(); if (mode==READ_WRITE) { if (!file.canWrite()) throw new MessagingException("Folder is read-only"); if (!acquireLock()) throw new MessagingException("Unable to acquire lock: "+filename); readOnly = false; } if (!file.canRead()) throw new MessagingException("Can't read folder: "+file.getName()); FixedLineInputStream in = null; try { // Read messages MboxStore store = (MboxStore)this.store; store.log("reading "+filename); List acc = new ArrayList(256); in = new FixedLineInputStream(getInputStream()); int count = 1; byte[] line; String fromLine = null; ByteArrayOutputStream buf = null; // notify listeners store.processStatusEvent(new StatusEvent(store, StatusEvent.OPERATION_START, "open")); for (line = in.readLine(); line!=null; line = in.readLine()) { if (new String(line, "US-ASCII").indexOf(FROM)==0) { if (buf!=null) { byte[] bytes = buf.toByteArray(); ByteArrayInputStream bin = new ByteArrayInputStream(bytes); MboxMessage m = new MboxMessage(this, fromLine, bin, count++); acc.add(m); store.processStatusEvent(new StatusEvent(store, StatusEvent.OPERATION_UPDATE, "open", 1, StatusEvent.UNKNOWN, count-1)); } fromLine = new String(line, "US-ASCII"); buf = new ByteArrayOutputStream(); } else if (buf!=null) { // byte[] bytes = decodeFrom(line).getBytes(); // buf.write(bytes, 0, bytes.length); buf.write(line); buf.write(10); // LF } } if (buf!=null) { byte[] bytes = buf.toByteArray(); ByteArrayInputStream bin = new ByteArrayInputStream(bytes); MboxMessage m = new MboxMessage(this, fromLine, bin, count++); acc.add(m); store.processStatusEvent(new StatusEvent(store, StatusEvent.OPERATION_UPDATE, "open", 1, StatusEvent.UNKNOWN, count-1)); } messages = new MboxMessage[acc.size()]; acc.toArray(messages); buf = null; acc = null; store.processStatusEvent(new StatusEvent(store, StatusEvent.OPERATION_END, "open")); // OK open = true; notifyConnectionListeners(ConnectionEvent.OPENED); } catch (IOException e) { throw new MessagingException("Unable to open folder: "+filename, e); } finally { // release any file descriptors try { if (in!=null) in.close(); } catch (IOException e) { // we tried } } } /** * Returns the specified line with any From_ line encoding removed. */ public static String decodeFrom(String line) { if (line!=null) { int len = line.length(); for (int i=0; i<(len-5); i++) { char c = line.charAt(i); if (i>0 && (c=='F' && line.charAt(i+1)=='r' && line.charAt(i+2)=='o' && line.charAt(i+3)=='m' && line.charAt(i+4)==' ')) return line.substring(1); if (c!='>') break; } } return line; } /** * 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(); if (!readOnly) { // Save messages MboxStore store = (MboxStore)this.store; store.log("saving "+file.getAbsolutePath()); synchronized (this) { OutputStream os = null; try { os = getOutputStream(); BufferedOutputStream bos = new BufferedOutputStream(os); MboxOutputStream mos = new MboxOutputStream(bos); store.processStatusEvent(new StatusEvent(store, StatusEvent.OPERATION_START, "close")); for (int i=0; i0) { if (f[0] instanceof InternetAddress) from = ((InternetAddress)f[0]).getAddress(); else from = f[0].toString(); } } catch (AddressException e) { // these things happen... } buf.append(from); buf.append(' '); Date date = message.getSentDate(); if (date==null) date = message.getReceivedDate(); if (date==null) date = new Date(); buf.append(df.format(date)); fromLine = buf.toString(); } return fromLine; } /** * Expunges this folder. * This deletes all the messages marked as deleted. * @exception MessagingException if a messaging error occurred */ public Message[] expunge() throws MessagingException { Message[] expunged; synchronized (this) { List elist = new ArrayList(); if (open) { List mlist = new ArrayList(); for (int i=0; i0) 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 flags = new Flags(); flags.add(Flags.Flag.DELETED); flags.add(Flags.Flag.SEEN); flags.add(Flags.Flag.RECENT); permanentFlags = flags; } return permanentFlags; } /** * Returns the number of messages in this folder. * @exception MessagingException if a messaging error occurred */ public int getMessageCount() throws MessagingException { return messages.length; } /** * Returns the specified message number from this folder. * @exception MessagingException if a messaging error occurred */ public Message getMessage(int msgnum) throws MessagingException { int index = msgnum-1; if (index<0 || index>=messages.length) throw new MessagingException("No such message: "+msgnum); return messages[index]; } /** * Returns the messages in this folder. * @exception MessagingException if a messaging error occurred */ public synchronized Message[] getMessages() throws MessagingException { // Return a copy of the message array Message[] m = new Message[messages.length]; System.arraycopy(messages, 0, m, 0, messages.length); return m; } /** * Appends messages to this folder. * Only MimeMessages within the array will be appended, as we don't know * how to retrieve internet content for other kinds. * @param m an array of messages to be appended */ public synchronized void appendMessages(Message[] m) throws MessagingException { MboxMessage[] n; synchronized (this) { List appended = new ArrayList(m.length); int count = messages.length; for (int i=0; i0) { appended.toArray(n); // copy into the messages array List acc = new ArrayList(messages.length+n.length); acc.addAll(Arrays.asList(messages)); acc.addAll(Arrays.asList(n)); messages = new MboxMessage[acc.size()]; acc.toArray(messages); acc = null; } } // propagate event if (n.length>0) notifyMessageAddedListeners(n); } /** * Returns the parent folder. */ public Folder getParent() throws MessagingException { return store.getFolder(file.getParent()); } /** * Returns 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 = file.list(); Folder[] folders = new Folder[files.length]; for (int i=0; i0) return false; } if (!readOnly) releaseLock(); if (!file.delete()) return false; notifyFolderListeners(FolderEvent.DELETED); return true; } catch (SecurityException e) { throw new MessagingException("Access denied", e); } } } /** * Renames this folder. */ public boolean renameTo(Folder folder) throws MessagingException { try { String filename = folder.getFullName(); if (filename!=null) { if (!file.renameTo(new File(filename))) return false; notifyFolderRenamedListeners(folder); return true; } else throw new MessagingException("Illegal filename: null"); } catch (SecurityException e) { throw new MessagingException("Access denied", e); } } /** * Returns the subfolder of this folder with the specified name. */ public Folder getFolder(String filename) throws MessagingException { String INBOX = "INBOX"; if (INBOX.equalsIgnoreCase(filename)) { try { return store.getFolder(INBOX); } catch (MessagingException e) { // fall back to standard behaviour } } return store.getFolder(file.getAbsolutePath()+File.separator+filename); } /** * Checks if the current file is or is supposed to be * compressed. Uses the filename to figure it out. */ private boolean isGzip() { return file.getName().toLowerCase().endsWith(".gz"); } /** * Creates an input stream that possibly will decompress the * file contents. */ private InputStream getInputStream() throws IOException { InputStream in = new FileInputStream(file); if (isGzip()) in = new GZIPInputStream(in); return in; } /** * Creates an output stream that possibly will compress * whatever is sent to it, based on the current filename. */ private OutputStream getOutputStream() throws IOException { OutputStream out = new FileOutputStream(file); if (isGzip()) out = new GZIPOutputStream(out); return out; } /** * Locks this mailbox. * This uses a dotlock-like mechanism - see createNewFile(). * If the directory containing the mbox * folder is not writable, we will not be able to open the mbox for * writing either. */ public synchronized boolean acquireLock() { try { String filename = file.getPath(); String lockFilename = filename+".lock"; File lock = new File(lockFilename); MboxStore store = (MboxStore)this.store; store.log("creating "+lock.getPath()); if (lock.exists()) return false; //if (!lock.canWrite()) // return false; createNewFile(lock); return true; } catch (IOException e) { MboxStore store = (MboxStore)this.store; store.log("I/O exception acquiring lock on "+file.getPath()); } catch (SecurityException e) { MboxStore store = (MboxStore)this.store; store.log("Security exception acquiring lock on "+file.getPath()); } return false; } /** * This method creates a new file. * Because Java cannot properly dotlock a file by creating a temporary * file and hardlinking it (some platforms do not support hard links) we * must use this method to create a zero-length inode. * This is a replacement for File.createNewFile(), which only exists in * the JDK since 1.2. * The idea is simply to touch the specified file. */ private void createNewFile(File file) throws IOException { // there may be another, more efficient way to do this. // certainly just setLastModified() does not work. BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(file)); out.flush(); out.close(); } /** * Unlocks this mailbox. * This deletes any associated lockfile if it exists. It returns false if * an existing lockfile could not be deleted. */ public synchronized boolean releaseLock() { try { String filename = file.getPath(); String lockFilename = filename+".lock"; File lock = new File(lockFilename); MboxStore store = (MboxStore)this.store; store.log("removing "+lock.getPath()); if (lock.exists()) { if (!lock.delete()) return false; } return true; } catch (SecurityException e) { MboxStore store = (MboxStore)this.store; store.log("Security exception releasing lock on "+file.getPath()); } return false; } 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); String end = pattern.substring(asteriskIndex+1, pattern.length()); return (name.startsWith(start) && name.endsWith(end)); } else if (percentIndex>-1) { String start = pattern.substring(0, percentIndex); String end = pattern.substring(percentIndex+1, pattern.length()); return (directory.equals(file) && name.startsWith(start) && name.endsWith(end)); } return name.equals(pattern); } } } class FixedLineInputStream { private BufferedInputStream is; public FixedLineInputStream(InputStream is) { this.is = new BufferedInputStream(is); } public byte[] readLine() throws IOException { byte buf[] = new byte[1024]; int idx = 0; while(idx < buf.length) { int i = is.read(); if(i == '\r') continue; if(i == -1) { if(idx == 0) return null; break; } if(i == '\n') break; buf[idx++] = (byte)i; } byte res[] = new byte[idx]; for(int i = 0; i < idx; i++) res[i] = buf[i]; return res; } public void close() throws IOException { is.close(); } }