View Javadoc

1   /*****************************************************************
2    * Licensed to the Apache Software Foundation (ASF) under one   *
3    * or more contributor license agreements.  See the NOTICE file *
4    * distributed with this work for additional information        *
5    * regarding copyright ownership.  The ASF licenses this file   *
6    * to you under the Apache License, Version 2.0 (the            *
7    * "License"); you may not use this file except in compliance   *
8    * with the License.  You may obtain a copy of the License at   *
9    *                                                              *
10   *   http://www.apache.org/licenses/LICENSE-2.0                 *
11   *                                                              *
12   * Unless required by applicable law or agreed to in writing,   *
13   * software distributed under the License is distributed on an  *
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
15   * KIND, either express or implied.  See the License for the    *
16   * specific language governing permissions and limitations      *
17   * under the License.                                           *
18   ****************************************************************/
19  
20  /* TODO:
21   *
22   * 1. Currently, iterating through the message collection does not
23   *    preserve the order in the file.  Change this with some form of
24   *    OrderedMap.  There is a suitable class in Jakarta Commons
25   *    Collections.
26   *
27   * 2. Optimize the remove operation.
28   *
29   * 3. Don't load entire message into memory.  This would mean computing
30   *    the hash during I/O streaming, rather than loading entire message
31   *    into memory, and using a MimeMessageWrapper with a suitable data
32   *    source.  As a strawman, the interface to MessageAction would
33   *    carry the hash, along with a size-limited stream providing the
34   *    message body.
35   *
36   * 4. Decide what to do when there are IDENTICAL messages in the file.
37   *    Right now only the last one will ever be processed, due to key
38   *    collissions.
39   *
40   * 5. isComplete()  - DONE.
41   *
42   * 6. Buffered I/O. - Partially done, and optional.
43   *
44   */
45  
46  package org.apache.james.mailrepository;
47  
48  import org.apache.avalon.framework.activity.Initializable;
49  import org.apache.avalon.framework.service.ServiceException;
50  import org.apache.avalon.framework.service.ServiceManager;
51  import org.apache.avalon.framework.service.Serviceable;
52  import org.apache.avalon.framework.configuration.Configurable;
53  import org.apache.avalon.framework.configuration.Configuration;
54  import org.apache.avalon.framework.configuration.ConfigurationException;
55  import org.apache.avalon.framework.logger.AbstractLogEnabled;
56  import org.apache.james.core.MailImpl;
57  import org.apache.james.services.MailRepository;
58  import org.apache.mailet.Mail;
59  import org.apache.oro.text.regex.MalformedPatternException;
60  import org.apache.oro.text.regex.Perl5Compiler;
61  import org.apache.oro.text.regex.Pattern;
62  import org.apache.oro.text.regex.Perl5Matcher;
63  
64  import javax.mail.MessagingException;
65  import javax.mail.Session;
66  import javax.mail.internet.MimeMessage;
67  
68  import java.io.ByteArrayInputStream;
69  import java.io.ByteArrayOutputStream;
70  import java.io.File;
71  import java.io.FileNotFoundException;
72  import java.io.IOException;
73  import java.io.RandomAccessFile;
74  import java.security.NoSuchAlgorithmException;
75  import java.security.MessageDigest;
76  import java.text.SimpleDateFormat;
77  import java.util.ArrayList;
78  import java.util.Calendar;
79  import java.util.Collection;
80  import java.util.Hashtable;
81  import java.util.Iterator;
82  import java.util.Locale;
83  import java.util.Properties;
84  
85  /***
86   * Implementation of a MailRepository using UNIX mbox files.
87   *
88   * <p>Requires a configuration element in the .conf.xml file of the form:
89   *  <br>&lt;repository destinationURL="mbox://&lt;directory&gt;"
90   *  <br>            type="MAIL"
91   *  <br>&lt;/directory&gt; is where the individual mbox files are read from/written to
92   * <br>Type can ONLY be MAIL (SPOOL is NOT supported)
93   *
94   * <p>Requires a logger called MailRepository.
95   *
96   * <p> Implementation notes:
97   * <p>
98   * This class keeps an internal store of the mbox file
99   * When the internal mbox file is updated (added/deleted)
100  * then the file will be re-read from disk and then written back.
101  * This is a bit inefficent but means that the file on disk
102  * should be correct.
103  * <p>
104  * The mbox store is mainly meant to be used as a one-way street.
105  * Storing new emails is very fast (append to file) whereas reading them (via POP3) is
106  * slower (read from disk and parse).
107  * Therefore this implementation is best suited to people who wish to use the mbox format
108  * for taking data out of James and into something else (IMAP server or mail list displayer)
109  *
110  * @version CVS $Revision: 495537 $
111  */
112 
113 
114 public class MBoxMailRepository
115         extends AbstractLogEnabled
116             implements MailRepository, Serviceable, Configurable, Initializable {
117 
118 
119     static final SimpleDateFormat dy = new SimpleDateFormat("EE MMM dd HH:mm:ss yyyy", Locale.US);
120     static final String LOCKEXT = ".lock";
121     static final String WORKEXT = ".work";
122     static final int LOCKSLEEPDELAY = 2000; // 2 second back off in the event of a problem with the lock file
123     static final int MAXSLEEPTIMES = 100; //
124     static final long MLISTPRESIZEFACTOR = 10 * 1024;  // The hash table will be loaded with a initial capacity of  filelength/MLISTPRESIZEFACTOR
125     static final long DEFAULTMLISTCAPACITY = 20; // Set up a hashtable to have a meaningful default
126 
127     /***
128      * Whether line buffering is turned used.
129      */
130     private static boolean BUFFERING = true;
131 
132     /***
133      * Whether 'deep debugging' is turned on.
134      */
135     private static final boolean DEEP_DEBUG = true;
136 
137     /***
138      * The internal list of the emails
139      * The key is an adapted MD5 checksum of the mail
140      */
141     private Hashtable mList = null;
142     /***
143      * The filename to read & write the mbox from/to
144      */
145     private String mboxFile;
146 
147     /***
148      * A callback used when a message is read from the mbox file
149      */
150     public interface MessageAction {
151         public boolean isComplete();  // *** Not valid until AFTER each call to messageAction(...)!
152         public MimeMessage messageAction(String messageSeparator, String bodyText, long messageStart);
153     }
154 
155 
156     /***
157      * Convert a MimeMessage into raw text
158      * @param mc The mime message to convert
159      * @return A string representation of the mime message
160      * @throws IOException
161      * @throws MessagingException
162      */
163     private String getRawMessage(MimeMessage mc) throws IOException, MessagingException {
164 
165         ByteArrayOutputStream rawMessage = new ByteArrayOutputStream();
166         mc.writeTo(rawMessage);
167         return rawMessage.toString();
168     }
169 
170     /***
171      * Parse a text block as an email and convert it into a mime message
172      * @param emailBody The headers and body of an email. This will be parsed into a mime message and stored
173      */
174     private MimeMessage convertTextToMimeMessage(String emailBody) {
175         //this.emailBody = emailBody;
176         MimeMessage mimeMessage = null;
177         // Parse the mime message as we have the full message now (in string format)
178         ByteArrayInputStream mb = new ByteArrayInputStream(emailBody.getBytes());
179         Properties props = System.getProperties();
180         Session session = Session.getDefaultInstance(props);
181         try {
182             mimeMessage = new MimeMessage(session, mb);
183 
184 
185         } catch (MessagingException e) {
186             getLogger().error("Unable to parse mime message!", e);
187         }
188 
189         if (mimeMessage == null && getLogger().isDebugEnabled()) {
190             StringBuffer logBuffer =
191                     new StringBuffer(128)
192                     .append(this.getClass().getName())
193                     .append(" Mime message is null");
194             getLogger().debug(logBuffer.toString());
195         }
196 
197         /*
198         String toAddr = null;
199         try {
200             // Attempt to read the TO field and see if it errors
201             toAddr = mimeMessage.getRecipients(javax.mail.Message.RecipientType.TO).toString();
202         } catch (Exception e) {
203             // It has errored, so time for plan B
204             // use the from field I suppose
205             try {
206                 mimeMessage.setRecipients(javax.mail.Message.RecipientType.TO, mimeMessage.getFrom());
207                 if (getLogger().isDebugEnabled()) {
208                     StringBuffer logBuffer =
209                             new StringBuffer(128)
210                             .append(this.getClass().getName())
211                             .append(" Patching To: field for message ")
212                             .append(" with  From: field");
213                     getLogger().debug(logBuffer.toString());
214                 }
215             } catch (MessagingException e1) {
216                 getLogger().error("Unable to set to: field to from: field", e);
217             }
218         } */
219         return mimeMessage;
220     }
221 
222     /***
223      * Generate a hex representation of an MD5 checksum on the emailbody
224      * @param emailBody
225      * @return A hex representation of the text
226      * @throws NoSuchAlgorithmException
227      */
228     private String generateKeyValue(String emailBody) throws NoSuchAlgorithmException {
229         // MD5 the email body for a reilable (ha ha) key
230         byte[] digArray = MessageDigest.getInstance("MD5").digest(emailBody.getBytes());
231         StringBuffer digest = new StringBuffer();
232         for (int i = 0; i < digArray.length; i++) {
233             digest.append(Integer.toString(digArray[i], Character.MAX_RADIX).toUpperCase(Locale.US));
234         }
235         return digest.toString();
236     }
237 
238     /***
239      * Parse the mbox file.
240      * @param ins The random access file to load. Note that the file may or may not start at offset 0 in the file
241      * @param messAct The action to take when a message is found
242      */
243     private MimeMessage parseMboxFile(RandomAccessFile ins, MessageAction messAct) {
244         if ((DEEP_DEBUG) && (getLogger().isDebugEnabled())) {
245             StringBuffer logBuffer =
246                     new StringBuffer(128)
247                     .append(this.getClass().getName())
248                     .append(" Start parsing ")
249                     .append(mboxFile);
250 
251             getLogger().debug(logBuffer.toString());
252         }
253         try {
254 
255             Perl5Compiler sepMatchCompiler = new Perl5Compiler();
256             Pattern sepMatchPattern = sepMatchCompiler.compile("^From (.*) (.*):(.*):(.*)$");
257             Perl5Matcher sepMatch = new Perl5Matcher();
258 
259             int c;
260             boolean inMessage = false;
261             StringBuffer messageBuffer = new StringBuffer();
262             String previousMessageSeparator = null;
263             boolean foundSep = false;
264 
265             long prevMessageStart = ins.getFilePointer();
266             if (BUFFERING) {
267             String line = null;
268             while ((line = ins.readLine()) != null) {
269                 foundSep = sepMatch.contains(line + "\n", sepMatchPattern);
270 
271                 if (foundSep && inMessage) {
272 //                    if ((DEEP_DEBUG) && (getLogger().isDebugEnabled())) {
273 //                        getLogger().debug(this.getClass().getName() + " Invoking " + messAct.getClass() + " at " + prevMessageStart);
274 //                    }
275                     MimeMessage endResult = messAct.messageAction(previousMessageSeparator, messageBuffer.toString(), prevMessageStart);
276                     if (messAct.isComplete()) {
277                         // I've got what I want so just exit
278                         return endResult;
279                     }
280                     previousMessageSeparator = line;
281                     prevMessageStart = ins.getFilePointer() - line.length();
282                     messageBuffer = new StringBuffer();
283                     inMessage = true;
284                 }
285                 // Only done at the start (first header)
286                 if (foundSep && !inMessage) {
287                     previousMessageSeparator = line.toString();
288                     inMessage = true;
289                 }
290                 if (!foundSep && inMessage) {
291                     messageBuffer.append(line).append("\n");
292                 }
293             }
294             } else {
295             StringBuffer line = new StringBuffer();
296             while ((c = ins.read()) != -1) {
297                 if (c == 10) {
298                     foundSep = sepMatch.contains(line.toString(), sepMatchPattern);
299                     if (foundSep && inMessage) {
300 //                        if ((DEEP_DEBUG) && (getLogger().isDebugEnabled())) {
301 //                            getLogger().debug(this.getClass().getName() + " Invoking " + messAct.getClass() + " at " + prevMessageStart);
302 //                        }
303                         MimeMessage endResult = messAct.messageAction(previousMessageSeparator, messageBuffer.toString(), prevMessageStart);
304                         if (messAct.isComplete()) {
305                             // I've got what I want so just exit
306                             return endResult;
307                         }
308                         previousMessageSeparator = line.toString();
309                         prevMessageStart = ins.getFilePointer() - line.length();
310                         messageBuffer = new StringBuffer();
311                         inMessage = true;
312                     }
313                     // Only done at the start (first header)
314                     if (foundSep && inMessage == false) {
315                         previousMessageSeparator = line.toString();
316                         inMessage = true;
317                     }
318                     if (!foundSep) {
319                         messageBuffer.append(line).append((char) c);
320                     }
321                     line = new StringBuffer(); // Reset buffer
322                 } else {
323                     line.append((char) c);
324                 }
325             }
326             }
327 
328             if (messageBuffer.length() != 0) {
329                 // process last message
330                 return messAct.messageAction(previousMessageSeparator, messageBuffer.toString(), prevMessageStart);
331             }
332         } catch (IOException ioEx) {
333             getLogger().error("Unable to write file (General I/O problem) " + mboxFile, ioEx);
334         } catch (MalformedPatternException e) {
335             getLogger().error("Bad regex passed " + mboxFile, e);
336         } finally {
337             if ((DEEP_DEBUG) && (getLogger().isDebugEnabled())) {
338                 StringBuffer logBuffer =
339                         new StringBuffer(128)
340                         .append(this.getClass().getName())
341                         .append(" Finished parsing ")
342                         .append(mboxFile);
343 
344                 getLogger().debug(logBuffer.toString());
345             }
346         }
347         return null;
348     }
349 
350     /***
351      * Find a given message
352      * This method will first use selectMessage(key) to see if the key/offset combination allows us to skip
353      * parts of the file and only load the message we are interested in
354      *
355      * @param key The key of the message to find
356      */
357     private MimeMessage findMessage(String key) {
358         MimeMessage foundMessage = null;
359 
360         // See if we can get the message by using the cache position first
361         foundMessage = selectMessage(key);
362         if (foundMessage == null) {
363             // If the message is not found something has changed from
364             // the cache.  The cache may have been invalidated by
365             // another method, or the file may have been replaced from
366             // underneath us.  Reload the cache, and try again.
367             mList = null;
368             loadKeys();
369             foundMessage = selectMessage(key);
370         }
371         return foundMessage;
372     }
373 
374     /***
375      * Quickly find a message by using the stored message offsets
376      * @param key  The key of the message to find
377      */
378     private MimeMessage selectMessage(final String key) {
379         MimeMessage foundMessage = null;
380         // Can we find the key first
381         if (mList == null || !mList.containsKey(key)) {
382             // Not initiailised so no point looking
383             if ((DEEP_DEBUG) && (getLogger().isDebugEnabled())) {
384                 StringBuffer logBuffer =
385                         new StringBuffer(128)
386                         .append(this.getClass().getName())
387                         .append(" mList - key not found ")
388                         .append(mboxFile);
389 
390                 getLogger().debug(logBuffer.toString());
391             }
392             return foundMessage;
393         }
394         long messageStart = ((Long) mList.get(key)).longValue();
395         if ((DEEP_DEBUG) && (getLogger().isDebugEnabled())) {
396             StringBuffer logBuffer =
397                     new StringBuffer(128)
398                     .append(this.getClass().getName())
399                     .append(" Load message starting at offset ")
400                     .append(messageStart)
401                     .append(" from file ")
402                     .append(mboxFile);
403 
404             getLogger().debug(logBuffer.toString());
405         }
406         // Now try and find the position in the file
407         RandomAccessFile ins = null;
408         try {
409             ins = new RandomAccessFile(mboxFile, "r");
410             if (messageStart != 0) {
411                 ins.seek(messageStart - 1);
412             }
413             MessageAction op = new MessageAction() {
414                 public boolean isComplete() { return true; }
415                 public MimeMessage messageAction(String messageSeparator, String bodyText, long messageStart) {
416                     try {
417                         if (key.equals(generateKeyValue(bodyText))) {
418                             getLogger().debug(this.getClass().getName() + " Located message. Returning MIME message");
419                             return convertTextToMimeMessage(bodyText);
420                         }
421                     } catch (NoSuchAlgorithmException e) {
422                         getLogger().error("MD5 not supported! ",e);
423                     }
424                     return null;
425                 }
426             };
427             foundMessage = this.parseMboxFile(ins, op);
428         } catch (FileNotFoundException e) {
429             getLogger().error("Unable to save(open) file (File not found) " + mboxFile, e);
430         } catch (IOException e) {
431             getLogger().error("Unable to write file (General I/O problem) " + mboxFile, e);
432         } finally {
433             if (foundMessage == null) {
434                 if ((DEEP_DEBUG) && (getLogger().isDebugEnabled())) {
435                     StringBuffer logBuffer =
436                             new StringBuffer(128)
437                             .append(this.getClass().getName())
438                             .append(" select - message not found ")
439                             .append(mboxFile);
440 
441                     getLogger().debug(logBuffer.toString());
442                 }
443             }
444             if (ins != null) try { ins.close(); } catch (IOException e) { getLogger().error("Unable to close file (General I/O problem) " + mboxFile, e); }
445         }
446         return foundMessage;
447     }
448 
449     /***
450      * Load the message keys and file pointer offsets from disk
451      */
452     private synchronized void loadKeys() {
453         if (mList!=null) {
454             return;
455         }
456         RandomAccessFile ins = null;
457         try {
458             ins = new RandomAccessFile(mboxFile, "r");
459             long initialCapacity = (ins.length() >  MLISTPRESIZEFACTOR ? ins.length() /MLISTPRESIZEFACTOR  : 0);
460             if (initialCapacity < DEFAULTMLISTCAPACITY ) {
461                 initialCapacity =  DEFAULTMLISTCAPACITY;
462             }
463             if (initialCapacity > Integer.MAX_VALUE) {
464                 initialCapacity = Integer.MAX_VALUE - 1;
465             }
466             this.mList = new Hashtable((int)initialCapacity);
467             this.parseMboxFile(ins, new MessageAction() {
468                 public boolean isComplete() { return false; }
469                 public MimeMessage messageAction(String messageSeparator, String bodyText, long messageStart) {
470                     try {
471                         String key = generateKeyValue(bodyText);
472                         mList.put(key, new Long(messageStart));
473                         if ((DEEP_DEBUG) && (getLogger().isDebugEnabled())) {
474                             getLogger().debug(this.getClass().getName() + " Key " + key + " at " + messageStart);
475                         }
476                         
477                     } catch (NoSuchAlgorithmException e) {
478                         getLogger().error("MD5 not supported! ",e);
479                     }
480                     return null;
481                 }
482             });
483             //System.out.println("Done Load keys!");
484         } catch (FileNotFoundException e) {
485             getLogger().error("Unable to save(open) file (File not found) " + mboxFile, e);
486             this.mList = new Hashtable((int)DEFAULTMLISTCAPACITY);
487         } catch (IOException e) {
488             getLogger().error("Unable to write file (General I/O problem) " + mboxFile, e);
489         } finally {
490             if (ins != null) try { ins.close(); } catch (IOException e) { getLogger().error("Unable to close file (General I/O problem) " + mboxFile, e); }
491         }
492     }
493 
494 
495     /***
496      * Store the given email in the current mbox file
497      * @param mc The mail to store
498      */
499     public void store(Mail mc) {
500 
501         if ((DEEP_DEBUG) && (getLogger().isDebugEnabled())) {
502             StringBuffer logBuffer =
503                     new StringBuffer(128)
504                     .append(this.getClass().getName())
505                     .append(" Will store message to file ")
506                     .append(mboxFile);
507 
508             getLogger().debug(logBuffer.toString());
509         }
510         this.mList = null;
511         // Now make up the from header
512         String fromHeader = null;
513         String message = null;
514         try {
515             message = getRawMessage(mc.getMessage());
516             // check for nullsender
517             if (mc.getMessage().getFrom() == null) {
518                 fromHeader = "From   " + dy.format(Calendar.getInstance().getTime());
519             } else {
520                 fromHeader = "From " + mc.getMessage().getFrom()[0] + " " + dy.format(Calendar.getInstance().getTime());
521             }
522             
523         } catch (IOException e) {
524             getLogger().error("Unable to parse mime message for " + mboxFile, e);
525         } catch (MessagingException e) {
526             getLogger().error("Unable to parse mime message for " + mboxFile, e);
527         }
528         // And save only the new stuff to disk
529         RandomAccessFile saveFile = null;
530         try {
531             saveFile = new RandomAccessFile(mboxFile, "rw");
532             saveFile.seek(saveFile.length()); // Move to the end
533             saveFile.writeBytes((fromHeader + "\n"));
534             saveFile.writeBytes((message + "\n"));
535             saveFile.close();
536 
537         } catch (FileNotFoundException e) {
538             getLogger().error("Unable to save(open) file (File not found) " + mboxFile, e);
539         } catch (IOException e) {
540             getLogger().error("Unable to write file (General I/O problem) " + mboxFile, e);
541         }
542     }
543 
544     /***
545      * Return the list of the current messages' keys
546      * @return A list of the keys of the emails currently loaded
547      */
548     public Iterator list() {
549         loadKeys();
550         
551         if (mList.keySet().isEmpty() == false) {
552             // find the first message.  This is a trick to make sure that if
553             // the file is changed out from under us, we will detect it and
554             // correct for it BEFORE we return the iterator.
555             findMessage((String) mList.keySet().iterator().next());
556         }
557         if ((DEEP_DEBUG) && (getLogger().isDebugEnabled())) {
558             StringBuffer logBuffer =
559                     new StringBuffer(128)
560                     .append(this.getClass().getName())
561                     .append(" ")
562                     .append(mList.size())
563                     .append(" keys to be iterated over.");
564 
565             getLogger().debug(logBuffer.toString());
566         }
567         return mList.keySet().iterator();
568     }
569 
570     /***
571      * Get a message from the backing store (disk)
572      * @param key
573      * @return The mail found from the key. Returns null if the key is not found
574      */
575     public Mail retrieve(String key) {
576 
577         loadKeys();
578         MailImpl res = null;
579         try {
580             MimeMessage foundMessage = findMessage(key);
581             if (foundMessage == null) {
582                 getLogger().error("found message is null!");
583                 return null;
584             }
585             res = new MailImpl(foundMessage);
586             res.setName(key);
587             if ((DEEP_DEBUG) && (getLogger().isDebugEnabled())) {
588                 StringBuffer logBuffer =
589                         new StringBuffer(128)
590                         .append(this.getClass().getName())
591                         .append(" Retrieving entry for key ")
592                         .append(key);
593 
594                 getLogger().debug(logBuffer.toString());
595             }
596         } catch (MessagingException e) {
597             getLogger().error("Unable to parse mime message for " + mboxFile + "\n" + e.getMessage(), e);
598         }
599         return res;
600     }
601 
602     /***
603      * Remove an existing message
604      * @param mail
605      */
606     public void remove(Mail mail) {
607         ArrayList remArray = new ArrayList();
608         remArray.add(mail);
609         remove(remArray);
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(retrieve(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 }