View Javadoc

1   /************************************************************************
2    * Copyright (c) 2000-2006 The Apache Software Foundation.             *
3    * All rights reserved.                                                *
4    * ------------------------------------------------------------------- *
5    * Licensed under the Apache License, Version 2.0 (the "License"); you *
6    * may not use this file except in compliance with the License. You    *
7    * may obtain a copy of the License at:                                *
8    *                                                                     *
9    *     http://www.apache.org/licenses/LICENSE-2.0                      *
10   *                                                                     *
11   * Unless required by applicable law or agreed to in writing, software *
12   * distributed under the License is distributed on an "AS IS" BASIS,   *
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or     *
14   * implied.  See the License for the specific language governing       *
15   * permissions and limitations under the License.                      *
16   ***********************************************************************/
17  
18  /* TODO:
19   *
20   * 1. Currently, iterating through the message collection does not
21   *    preserve the order in the file.  Change this with some form of
22   *    OrderedMap.  There is a suitable class in Jakarta Commons
23   *    Collections.
24   *
25   * 2. Optimize the remove operation.
26   *
27   * 3. Don't load entire message into memory.  This would mean computing
28   *    the hash during I/O streaming, rather than loading entire message
29   *    into memory, and using a MimeMessageWrapper with a suitable data
30   *    source.  As a strawman, the interface to MessageAction would
31   *    carry the hash, along with a size-limited stream providing the
32   *    message body.
33   *
34   * 4. Decide what to do when there are IDENTICAL messages in the file.
35   *    Right now only the last one will ever be processed, due to key
36   *    collissions.
37   *
38   * 5. isComplete()  - DONE.
39   *
40   * 6. Buffered I/O. - Partially done, and optional.
41   *
42   */
43  
44  package org.apache.james.mailrepository;
45  
46  import org.apache.avalon.framework.activity.Initializable;
47  import org.apache.avalon.framework.service.ServiceException;
48  import org.apache.avalon.framework.service.ServiceManager;
49  import org.apache.avalon.framework.service.Serviceable;
50  import org.apache.avalon.framework.configuration.Configurable;
51  import org.apache.avalon.framework.configuration.Configuration;
52  import org.apache.avalon.framework.configuration.ConfigurationException;
53  import org.apache.avalon.framework.logger.AbstractLogEnabled;
54  import org.apache.james.core.MailImpl;
55  import org.apache.james.services.MailRepository;
56  import org.apache.mailet.Mail;
57  import org.apache.oro.text.regex.MalformedPatternException;
58  import org.apache.oro.text.regex.Perl5Compiler;
59  import org.apache.oro.text.regex.Pattern;
60  import org.apache.oro.text.regex.Perl5Matcher;
61  
62  import javax.mail.MessagingException;
63  import javax.mail.Session;
64  import javax.mail.internet.MimeMessage;
65  
66  import java.io.ByteArrayInputStream;
67  import java.io.ByteArrayOutputStream;
68  import java.io.File;
69  import java.io.FileNotFoundException;
70  import java.io.IOException;
71  import java.io.RandomAccessFile;
72  import java.security.NoSuchAlgorithmException;
73  import java.security.MessageDigest;
74  import java.text.SimpleDateFormat;
75  import java.util.ArrayList;
76  import java.util.Calendar;
77  import java.util.Collection;
78  import java.util.Hashtable;
79  import java.util.Iterator;
80  import java.util.Locale;
81  import java.util.Properties;
82  import java.util.Vector;
83  
84  /***
85   * Implementation of a MailRepository using UNIX mbox files.
86   *
87   * <p>Requires a configuration element in the .conf.xml file of the form:
88   *  <br>&lt;repository destinationURL="mbox://&lt;directory&gt;"
89   *  <br>            type="MAIL"
90   *  <br>&lt;/directory&gt; is where the individual mbox files are read from/written to
91   * <br>Type can ONLY be MAIL (SPOOL is NOT supported)
92   *
93   * <p>Requires a logger called MailRepository.
94   *
95   * <p> Implementation notes:
96   * <p>
97   * This class keeps an internal store of the mbox file
98   * When the internal mbox file is updated (added/deleted)
99   * then the file will be re-read from disk and then written back.
100  * This is a bit inefficent but means that the file on disk
101  * should be correct.
102  * <p>
103  * The mbox store is mainly meant to be used as a one-way street.
104  * Storing new emails is very fast (append to file) whereas reading them (via POP3) is
105  * slower (read from disk and parse).
106  * Therefore this implementation is best suited to people who wish to use the mbox format
107  * for taking data out of James and into something else (IMAP server or mail list displayer)
108  *
109  * @version CVS $Revision: 454176 $
110  */
111 
112 
113 public class MBoxMailRepository
114         extends AbstractLogEnabled
115             implements MailRepository, Serviceable, Configurable, Initializable {
116 
117 
118     static final SimpleDateFormat dy = new SimpleDateFormat("EE MMM dd HH:mm:ss yyyy", Locale.US);
119     static final String LOCKEXT = ".lock";
120     static final String WORKEXT = ".work";
121     static final int LOCKSLEEPDELAY = 2000; // 2 second back off in the event of a problem with the lock file
122     static final int MAXSLEEPTIMES = 100; //
123     static final long MLISTPRESIZEFACTOR = 10 * 1024;  // The hash table will be loaded with a initial capacity of  filelength/MLISTPRESIZEFACTOR
124     static final long DEFAULTMLISTCAPACITY = 20; // Set up a hashtable to have a meaningful default
125 
126     /***
127      * Whether line buffering is turned used.
128      */
129     private static boolean BUFFERING = true;
130 
131     /***
132      * Whether 'deep debugging' is turned on.
133      */
134     private static final boolean DEEP_DEBUG = true;
135 
136     /***
137      * The internal list of the emails
138      * The key is an adapted MD5 checksum of the mail
139      */
140     private Hashtable mList = null;
141     /***
142      * The filename to read & write the mbox from/to
143      */
144     private String mboxFile;
145 
146     /***
147      * A callback used when a message is read from the mbox file
148      */
149     public interface MessageAction {
150         public boolean isComplete();  // *** Not valid until AFTER each call to messageAction(...)!
151         public MimeMessage messageAction(String messageSeparator, String bodyText, long messageStart);
152     }
153 
154 
155     /***
156      * Convert a MimeMessage into raw text
157      * @param mc The mime message to convert
158      * @return A string representation of the mime message
159      * @throws IOException
160      * @throws MessagingException
161      */
162     private String getRawMessage(MimeMessage mc) throws IOException, MessagingException {
163 
164         ByteArrayOutputStream rawMessage = new ByteArrayOutputStream();
165         mc.writeTo(rawMessage);
166         return rawMessage.toString();
167     }
168 
169     /***
170      * Parse a text block as an email and convert it into a mime message
171      * @param emailBody The headers and body of an email. This will be parsed into a mime message and stored
172      */
173     private MimeMessage convertTextToMimeMessage(String emailBody) {
174         //this.emailBody = emailBody;
175         MimeMessage mimeMessage = null;
176         // Parse the mime message as we have the full message now (in string format)
177         ByteArrayInputStream mb = new ByteArrayInputStream(emailBody.getBytes());
178         Properties props = System.getProperties();
179         Session session = Session.getDefaultInstance(props);
180         try {
181             mimeMessage = new MimeMessage(session, mb);
182 
183 
184         } catch (MessagingException e) {
185             getLogger().error("Unable to parse mime message!", e);
186         }
187 
188         if (mimeMessage == null && getLogger().isDebugEnabled()) {
189             StringBuffer logBuffer =
190                     new StringBuffer(128)
191                     .append(this.getClass().getName())
192                     .append(" Mime message is null");
193             getLogger().debug(logBuffer.toString());
194         }
195 
196         /*
197         String toAddr = null;
198         try {
199             // Attempt to read the TO field and see if it errors
200             toAddr = mimeMessage.getRecipients(javax.mail.Message.RecipientType.TO).toString();
201         } catch (Exception e) {
202             // It has errored, so time for plan B
203             // use the from field I suppose
204             try {
205                 mimeMessage.setRecipients(javax.mail.Message.RecipientType.TO, mimeMessage.getFrom());
206                 if (getLogger().isDebugEnabled()) {
207                     StringBuffer logBuffer =
208                             new StringBuffer(128)
209                             .append(this.getClass().getName())
210                             .append(" Patching To: field for message ")
211                             .append(" with  From: field");
212                     getLogger().debug(logBuffer.toString());
213                 }
214             } catch (MessagingException e1) {
215                 getLogger().error("Unable to set to: field to from: field", e);
216             }
217         } */
218         return mimeMessage;
219     }
220 
221     /***
222      * Generate a hex representation of an MD5 checksum on the emailbody
223      * @param emailBody
224      * @return A hex representation of the text
225      * @throws NoSuchAlgorithmException
226      */
227     private String generateKeyValue(String emailBody) throws NoSuchAlgorithmException {
228         // MD5 the email body for a reilable (ha ha) key
229         byte[] digArray = MessageDigest.getInstance("MD5").digest(emailBody.getBytes());
230         StringBuffer digest = new StringBuffer();
231         for (int i = 0; i < digArray.length; i++) {
232             digest.append(Integer.toString(digArray[i], Character.MAX_RADIX).toUpperCase(Locale.US));
233         }
234         return digest.toString();
235     }
236 
237     /***
238      * Parse the mbox file.
239      * @param ins The random access file to load. Note that the file may or may not start at offset 0 in the file
240      * @param messAct The action to take when a message is found
241      */
242     private MimeMessage parseMboxFile(RandomAccessFile ins, MessageAction messAct) {
243         if ((DEEP_DEBUG) && (getLogger().isDebugEnabled())) {
244             StringBuffer logBuffer =
245                     new StringBuffer(128)
246                     .append(this.getClass().getName())
247                     .append(" Start parsing ")
248                     .append(mboxFile);
249 
250             getLogger().debug(logBuffer.toString());
251         }
252         try {
253 
254             Perl5Compiler sepMatchCompiler = new Perl5Compiler();
255             Pattern sepMatchPattern = sepMatchCompiler.compile("^From (.*) (.*):(.*):(.*)$");
256             Perl5Matcher sepMatch = new Perl5Matcher();
257 
258             int c;
259             boolean inMessage = false;
260             StringBuffer messageBuffer = new StringBuffer();
261             String previousMessageSeparator = null;
262             boolean foundSep = false;
263 
264             long prevMessageStart = ins.getFilePointer();
265             if (BUFFERING) {
266             String line = null;
267             while ((line = ins.readLine()) != null) {
268                 foundSep = sepMatch.contains(line + "\n", sepMatchPattern);
269 
270                 if (foundSep && inMessage) {
271 //                    if ((DEEP_DEBUG) && (getLogger().isDebugEnabled())) {
272 //                        getLogger().debug(this.getClass().getName() + " Invoking " + messAct.getClass() + " at " + prevMessageStart);
273 //                    }
274                     MimeMessage endResult = messAct.messageAction(previousMessageSeparator, messageBuffer.toString(), prevMessageStart);
275                     if (messAct.isComplete()) {
276                         // I've got what I want so just exit
277                         return endResult;
278                     }
279                     previousMessageSeparator = line;
280                     prevMessageStart = ins.getFilePointer() - line.length();
281                     messageBuffer = new StringBuffer();
282                     inMessage = true;
283                 }
284                 // Only done at the start (first header)
285                 if (foundSep && !inMessage) {
286                     previousMessageSeparator = line.toString();
287                     inMessage = true;
288                 }
289                 if (!foundSep && inMessage) {
290                     messageBuffer.append(line).append("\n");
291                 }
292             }
293             } else {
294             StringBuffer line = new StringBuffer();
295             while ((c = ins.read()) != -1) {
296                 if (c == 10) {
297                     foundSep = sepMatch.contains(line.toString(), sepMatchPattern);
298                     if (foundSep && inMessage) {
299 //                        if ((DEEP_DEBUG) && (getLogger().isDebugEnabled())) {
300 //                            getLogger().debug(this.getClass().getName() + " Invoking " + messAct.getClass() + " at " + prevMessageStart);
301 //                        }
302                         MimeMessage endResult = messAct.messageAction(previousMessageSeparator, messageBuffer.toString(), prevMessageStart);
303                         if (messAct.isComplete()) {
304                             // I've got what I want so just exit
305                             return endResult;
306                         }
307                         previousMessageSeparator = line.toString();
308                         prevMessageStart = ins.getFilePointer() - line.length();
309                         messageBuffer = new StringBuffer();
310                         inMessage = true;
311                     }
312                     // Only done at the start (first header)
313                     if (foundSep && inMessage == false) {
314                         previousMessageSeparator = line.toString();
315                         inMessage = true;
316                     }
317                     if (!foundSep) {
318                         messageBuffer.append(line).append((char) c);
319                     }
320                     line = new StringBuffer(); // Reset buffer
321                 } else {
322                     line.append((char) c);
323                 }
324             }
325             }
326 
327             if (messageBuffer.length() != 0) {
328                 // process last message
329                 return messAct.messageAction(previousMessageSeparator, messageBuffer.toString(), prevMessageStart);
330             }
331         } catch (IOException ioEx) {
332             getLogger().error("Unable to write file (General I/O problem) " + mboxFile, ioEx);
333         } catch (MalformedPatternException e) {
334             getLogger().error("Bad regex passed " + mboxFile, e);
335         } finally {
336             if ((DEEP_DEBUG) && (getLogger().isDebugEnabled())) {
337                 StringBuffer logBuffer =
338                         new StringBuffer(128)
339                         .append(this.getClass().getName())
340                         .append(" Finished parsing ")
341                         .append(mboxFile);
342 
343                 getLogger().debug(logBuffer.toString());
344             }
345         }
346         return null;
347     }
348 
349     /***
350      * Find a given message
351      * This method will first use selectMessage(key) to see if the key/offset combination allows us to skip
352      * parts of the file and only load the message we are interested in
353      *
354      * @param key The key of the message to find
355      */
356     private MimeMessage findMessage(String key) {
357         MimeMessage foundMessage = null;
358 
359         // See if we can get the message by using the cache position first
360         foundMessage = selectMessage(key);
361         if (foundMessage == null) {
362             // If the message is not found something has changed from
363             // the cache.  The cache may have been invalidated by
364             // another method, or the file may have been replaced from
365             // underneath us.  Reload the cache, and try again.
366             mList = null;
367             loadKeys();
368             foundMessage = selectMessage(key);
369         }
370         return foundMessage;
371     }
372 
373     /***
374      * Quickly find a message by using the stored message offsets
375      * @param key  The key of the message to find
376      */
377     private MimeMessage selectMessage(final String key) {
378         MimeMessage foundMessage = null;
379         // Can we find the key first
380         if (mList == null || !mList.containsKey(key)) {
381             // Not initiailised so no point looking
382             if ((DEEP_DEBUG) && (getLogger().isDebugEnabled())) {
383                 StringBuffer logBuffer =
384                         new StringBuffer(128)
385                         .append(this.getClass().getName())
386                         .append(" mList - key not found ")
387                         .append(mboxFile);
388 
389                 getLogger().debug(logBuffer.toString());
390             }
391             return foundMessage;
392         }
393         long messageStart = ((Long) mList.get(key)).longValue();
394         if ((DEEP_DEBUG) && (getLogger().isDebugEnabled())) {
395             StringBuffer logBuffer =
396                     new StringBuffer(128)
397                     .append(this.getClass().getName())
398                     .append(" Load message starting at offset ")
399                     .append(messageStart)
400                     .append(" from file ")
401                     .append(mboxFile);
402 
403             getLogger().debug(logBuffer.toString());
404         }
405         // Now try and find the position in the file
406         RandomAccessFile ins = null;
407         try {
408             ins = new RandomAccessFile(mboxFile, "r");
409             if (messageStart != 0) {
410                 ins.seek(messageStart - 1);
411             }
412             MessageAction op = new MessageAction() {
413                 public boolean isComplete() { return true; }
414                 public MimeMessage messageAction(String messageSeparator, String bodyText, long messageStart) {
415                     try {
416                         if (key.equals(generateKeyValue(bodyText))) {
417                             getLogger().debug(this.getClass().getName() + " Located message. Returning MIME message");
418                             return convertTextToMimeMessage(bodyText);
419                         }
420                     } catch (NoSuchAlgorithmException e) {
421                         getLogger().error("MD5 not supported! ",e);
422                     }
423                     return null;
424                 }
425             };
426             foundMessage = this.parseMboxFile(ins, op);
427         } catch (FileNotFoundException e) {
428             getLogger().error("Unable to save(open) file (File not found) " + mboxFile, e);
429         } catch (IOException e) {
430             getLogger().error("Unable to write file (General I/O problem) " + mboxFile, e);
431         } finally {
432             if (foundMessage == null) {
433                 if ((DEEP_DEBUG) && (getLogger().isDebugEnabled())) {
434                     StringBuffer logBuffer =
435                             new StringBuffer(128)
436                             .append(this.getClass().getName())
437                             .append(" select - message not found ")
438                             .append(mboxFile);
439 
440                     getLogger().debug(logBuffer.toString());
441                 }
442             }
443             if (ins != null) try { ins.close(); } catch (IOException e) { getLogger().error("Unable to close file (General I/O problem) " + mboxFile, e); }
444         }
445         return foundMessage;
446     }
447 
448     /***
449      * Load the message keys and file pointer offsets from disk
450      */
451     private synchronized void loadKeys() {
452         if (mList!=null) {
453             return;
454         }
455         RandomAccessFile ins = null;
456         try {
457             ins = new RandomAccessFile(mboxFile, "r");
458             long initialCapacity = (ins.length() >  MLISTPRESIZEFACTOR ? ins.length() /MLISTPRESIZEFACTOR  : 0);
459             if (initialCapacity < DEFAULTMLISTCAPACITY ) {
460                 initialCapacity =  DEFAULTMLISTCAPACITY;
461             }
462             if (initialCapacity > Integer.MAX_VALUE) {
463                 initialCapacity = Integer.MAX_VALUE - 1;
464             }
465             this.mList = new Hashtable((int)initialCapacity);
466             this.parseMboxFile(ins, new MessageAction() {
467                 public boolean isComplete() { return false; }
468                 public MimeMessage messageAction(String messageSeparator, String bodyText, long messageStart) {
469                     try {
470                         String key = generateKeyValue(bodyText);
471                         mList.put(key, new Long(messageStart));
472                         if ((DEEP_DEBUG) && (getLogger().isDebugEnabled())) {
473                             getLogger().debug(this.getClass().getName() + " Key " + key + " at " + messageStart);
474                         }
475                         
476                     } catch (NoSuchAlgorithmException e) {
477                         getLogger().error("MD5 not supported! ",e);
478                     }
479                     return null;
480                 }
481             });
482             //System.out.println("Done Load keys!");
483         } catch (FileNotFoundException e) {
484             getLogger().error("Unable to save(open) file (File not found) " + mboxFile, e);
485             this.mList = new Hashtable((int)DEFAULTMLISTCAPACITY);
486         } catch (IOException e) {
487             getLogger().error("Unable to write file (General I/O problem) " + mboxFile, e);
488         } finally {
489             if (ins != null) try { ins.close(); } catch (IOException e) { getLogger().error("Unable to close file (General I/O problem) " + mboxFile, e); }
490         }
491     }
492 
493 
494     /***
495      * Store the given email in the current mbox file
496      * @param mc The mail to store
497      */
498     public void store(Mail mc) {
499 
500         if ((DEEP_DEBUG) && (getLogger().isDebugEnabled())) {
501             StringBuffer logBuffer =
502                     new StringBuffer(128)
503                     .append(this.getClass().getName())
504                     .append(" Will store message to file ")
505                     .append(mboxFile);
506 
507             getLogger().debug(logBuffer.toString());
508         }
509         this.mList = null;
510         // Now make up the from header
511         String fromHeader = null;
512         String message = null;
513         try {
514             message = getRawMessage(mc.getMessage());
515             // check for nullsender
516             if (mc.getMessage().getFrom() == null) {
517                 fromHeader = "From   " + dy.format(Calendar.getInstance().getTime());
518             } else {
519                 fromHeader = "From " + mc.getMessage().getFrom()[0] + " " + dy.format(Calendar.getInstance().getTime());
520             }
521             
522         } catch (IOException e) {
523             getLogger().error("Unable to parse mime message for " + mboxFile, e);
524         } catch (MessagingException e) {
525             getLogger().error("Unable to parse mime message for " + mboxFile, e);
526         }
527         // And save only the new stuff to disk
528         RandomAccessFile saveFile = null;
529         try {
530             saveFile = new RandomAccessFile(mboxFile, "rw");
531             saveFile.seek(saveFile.length()); // Move to the end
532             saveFile.writeBytes((fromHeader + "\n"));
533             saveFile.writeBytes((message + "\n"));
534             saveFile.close();
535 
536         } catch (FileNotFoundException e) {
537             getLogger().error("Unable to save(open) file (File not found) " + mboxFile, e);
538         } catch (IOException e) {
539             getLogger().error("Unable to write file (General I/O problem) " + mboxFile, e);
540         }
541     }
542 
543     /***
544      * Return the list of the current messages' keys
545      * @return A list of the keys of the emails currently loaded
546      */
547     public Iterator list() {
548         loadKeys();
549         
550         if (mList.keySet().isEmpty() == false) {
551             // find the first message.  This is a trick to make sure that if
552             // the file is changed out from under us, we will detect it and
553             // correct for it BEFORE we return the iterator.
554             findMessage((String) mList.keySet().iterator().next());
555         }
556         if ((DEEP_DEBUG) && (getLogger().isDebugEnabled())) {
557             StringBuffer logBuffer =
558                     new StringBuffer(128)
559                     .append(this.getClass().getName())
560                     .append(" ")
561                     .append(mList.size())
562                     .append(" keys to be iterated over.");
563 
564             getLogger().debug(logBuffer.toString());
565         }
566         return mList.keySet().iterator();
567     }
568 
569     /***
570      * Get a message from the backing store (disk)
571      * @param key
572      * @return The mail found from the key. Returns null if the key is not found
573      */
574     public Mail retrieve(String key) {
575 
576         loadKeys();
577         MailImpl res = null;
578         try {
579             MimeMessage foundMessage = findMessage(key);
580             if (foundMessage == null) {
581                 getLogger().error("found message is null!");
582                 return null;
583             }
584             res = new MailImpl(foundMessage);
585             res.setName(key);
586             if ((DEEP_DEBUG) && (getLogger().isDebugEnabled())) {
587                 StringBuffer logBuffer =
588                         new StringBuffer(128)
589                         .append(this.getClass().getName())
590                         .append(" Retrieving entry for key ")
591                         .append(key);
592 
593                 getLogger().debug(logBuffer.toString());
594             }
595         } catch (MessagingException e) {
596             getLogger().error("Unable to parse mime message for " + mboxFile + "\n" + e.getMessage(), e);
597         }
598         return res;
599     }
600 
601     /***
602      * Remove an existing message
603      * @param mail
604      */
605     public void remove(Mail mail) {
606         // Convert the message into a key
607         Vector delVec = new Vector();
608         delVec.addElement(mail);
609         remove(delVec);
610     }
611 
612     /***
613      * Attempt to get a lock on the mbox by creating
614      * the file mboxname.lock
615      * @throws Exception
616      */
617     private void lockMBox() throws Exception {
618         // Create the lock file (if possible)
619         String lockFileName = mboxFile + LOCKEXT;
620         int sleepCount = 0;
621         File mBoxLock = new File(lockFileName);
622         if (!mBoxLock.createNewFile()) {
623             // This is not good, somebody got the lock before me
624             // So wait for a file
625             while (!mBoxLock.createNewFile() && sleepCount < MAXSLEEPTIMES) {
626                 try {
627                     if ((DEEP_DEBUG) && (getLogger().isDebugEnabled())) {
628                         StringBuffer logBuffer =
629                                 new StringBuffer(128)
630                                 .append(this.getClass().getName())
631                                 .append(" Waiting for lock on file ")
632                                 .append(mboxFile);
633 
634                         getLogger().debug(logBuffer.toString());
635                     }
636 
637                     Thread.sleep(LOCKSLEEPDELAY);
638                     sleepCount++;
639                 } catch (InterruptedException e) {
640                     getLogger().error("File lock wait for " + mboxFile + " interrupted!",e);
641 
642                 }
643             }
644             if (sleepCount >= MAXSLEEPTIMES) {
645                 throw new Exception("Unable to get lock on file " + mboxFile);
646             }
647         }
648     }
649 
650     /***
651      * Unlock a previously locked mbox file
652      */
653     private void unlockMBox() {
654         // Just delete the MBOX file
655         String lockFileName = mboxFile + LOCKEXT;
656         File mBoxLock = new File(lockFileName);
657         if (!mBoxLock.delete()) {
658             StringBuffer logBuffer =
659                     new StringBuffer(128)
660                     .append(this.getClass().getName())
661                     .append(" Failed to delete lock file ")
662                     .append(lockFileName);
663             getLogger().error(logBuffer.toString());
664         }
665     }
666 
667 
668 
669     /***
670      * Remove a list of messages from disk
671      * The collection is simply a list of mails to delete
672      * @param mails
673      */
674     public void remove(final Collection mails)
675     {
676         if ((DEEP_DEBUG) && (getLogger().isDebugEnabled())) {
677             StringBuffer logBuffer =
678                     new StringBuffer(128)
679                     .append(this.getClass().getName())
680                     .append(" Removing entry for key ")
681                     .append(mails);
682 
683             getLogger().debug(logBuffer.toString());
684         }
685         // The plan is as follows:
686         // Attempt to locate the message in the file
687         // by reading through the
688         // once we've done that then seek to the file
689         try {
690             RandomAccessFile ins = new RandomAccessFile(mboxFile, "r"); // The source
691             final RandomAccessFile outputFile = new RandomAccessFile(mboxFile + WORKEXT, "rw"); // The destination
692             parseMboxFile(ins, new MessageAction() {
693                 public boolean isComplete() { return false; }
694                 public MimeMessage messageAction(String messageSeparator, String bodyText, long messageStart) {
695                     // Write out the messages as we go, until we reach the key we want
696                     try {
697                         String currentKey=generateKeyValue(bodyText);
698                         boolean foundKey=false;
699                         Iterator mailList = mails.iterator();
700                         String key;
701                         while (mailList.hasNext()) {
702                             // Attempt to find the current key in the array
703                             key = ((Mail)mailList.next()).getName();
704                             if (key.equals(currentKey)) {
705                                 // Don't write the message to disk
706                                 foundKey = true;
707                                 break;
708                             }
709                         }
710                         if (foundKey == false)
711                         {
712                             // We didn't find the key in the array so we will keep it
713                             outputFile.writeBytes(messageSeparator + "\n");
714                             outputFile.writeBytes(bodyText);
715 
716                         }
717                     } catch (NoSuchAlgorithmException e) {
718                         getLogger().error("MD5 not supported! ",e);
719                     } catch (IOException e) {
720                         getLogger().error("Unable to write file (General I/O problem) " + mboxFile, e);
721                     }
722                     return null;
723                 }
724             });
725             ins.close();
726             outputFile.close();
727             // Delete the old mbox file
728             File mbox = new File(mboxFile);
729             mbox.delete();
730             // And rename the lock file to be the new mbox
731             mbox = new File(mboxFile + WORKEXT);
732             if (!mbox.renameTo(new File(mboxFile)))
733             {
734                  System.out.println("Failed to rename file!");
735             }
736 
737             // Now delete the keys in mails from the main hash
738             Iterator mailList = mails.iterator();
739             String key;
740             while (mailList.hasNext()) {
741                 // Attempt to find the current key in the array
742                 key = ((Mail)mailList.next()).getName();
743                 mList.remove(key);
744             }
745 
746 
747         } catch (FileNotFoundException e) {
748             getLogger().error("Unable to save(open) file (File not found) " + mboxFile, e);
749         } catch (IOException e) {
750             getLogger().error("Unable to write file (General I/O problem) " + mboxFile, e);
751         }
752     }
753 
754     /***
755      * Remove a mail from the mbox file
756      * @param key The key of the mail to delete
757      */
758     public void remove(String key) {
759         loadKeys();
760         try {
761             lockMBox();
762         } catch (Exception e) {
763             getLogger().error("Lock failed!",e);
764             return; // No lock, so exit
765         }
766         ArrayList keys = new ArrayList();
767         keys.add(key);
768 
769         this.remove(keys);
770         unlockMBox();
771     }
772 
773     /***
774      * Not implemented
775      * @param key
776      * @return
777      */
778     public boolean lock(String key) {
779         return false;
780     }
781 
782     /***
783      * Not implemented
784      * @param key
785      * @return
786      */
787     public boolean unlock(String key) {
788         return false;
789     }
790 
791 
792     /***
793      * @see org.apache.avalon.framework.service.Serviceable#compose(ServiceManager )
794      */
795     public void service( final ServiceManager componentManager )
796             throws ServiceException {
797     }
798 
799     /***
800      * Configure the component
801      * @param conf
802      * @throws ConfigurationException
803      */
804     public void configure(Configuration conf) throws ConfigurationException {
805         String destination;
806         this.mList = null;
807         BUFFERING = conf.getAttributeAsBoolean("BUFFERING", true);
808         destination = conf.getAttribute("destinationURL");
809         if (destination.charAt(destination.length() - 1) == '/') {
810             // Remove the trailing / as well as the protocol marker
811             mboxFile = destination.substring("mbox://".length(), destination.lastIndexOf("/"));
812         } else {
813             mboxFile = destination.substring("mbox://".length());
814         }
815 
816         if (getLogger().isDebugEnabled()) {
817             getLogger().debug("MBoxMailRepository.destinationURL: " + destination);
818         }
819 
820         String checkType = conf.getAttribute("type");
821         if (!(checkType.equals("MAIL") || checkType.equals("SPOOL"))) {
822             String exceptionString = "Attempt to configure MboxMailRepository as " + checkType;
823             if (getLogger().isWarnEnabled()) {
824                 getLogger().warn(exceptionString);
825             }
826             throw new ConfigurationException(exceptionString);
827         }
828     }
829 
830 
831     /***
832      * Initialise the component
833      * @throws Exception
834      */
835     public void initialize() throws Exception {
836     }
837 
838 
839     public static void main(String[] args) {
840         // Test invocation
841         MBoxMailRepository mbx = new MBoxMailRepository();
842         mbx.mboxFile = "C://java//test//1998-05.txt";
843         Iterator mList = mbx.list();
844         while (mList.hasNext()) {
845             //String key = (String) mList.next();
846             //System.out.println("key=" + key);
847             /*MailImpl mi =  mbx.retrieve(key);
848             try
849             {
850                 System.out.println("Subject : " +  (mi.getMessage()).getSubject());
851             }
852             catch (MessagingException e)
853             {
854                 e.printStackTrace();  //To change body of catch statement use Options | File Templates.
855             } */
856 
857         }
858 
859 
860 /*        MailImpl mi = mbx.retrieve("ffffffb4ffffffe2f59fffffff291dffffffde4366243ffffff971d1f24");
861         try {
862             System.out.println("Subject : " + (mi.getMessage()).getSubject());
863         } catch (MessagingException e) {
864             e.printStackTrace();  //To change body of catch statement use Options | File Templates.
865         }
866         mbx.remove("ffffffb4ffffffe2f59fffffff291dffffffde4366243ffffff971d1f24");*/
867     }
868 }