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  
21  
22  /* TODO:
23   *
24   * 1. Currently, iterating through the message collection does not
25   *    preserve the order in the file.  Change this with some form of
26   *    OrderedMap.  There is a suitable class in Jakarta Commons
27   *    Collections.
28   *
29   * 2. Optimize the remove operation.
30   *
31   * 3. Don't load entire message into memory.  This would mean computing
32   *    the hash during I/O streaming, rather than loading entire message
33   *    into memory, and using a MimeMessageWrapper with a suitable data
34   *    source.  As a strawman, the interface to MessageAction would
35   *    carry the hash, along with a size-limited stream providing the
36   *    message body.
37   *
38   * 4. Decide what to do when there are IDENTICAL messages in the file.
39   *    Right now only the last one will ever be processed, due to key
40   *    collissions.
41   *
42   * 5. isComplete()  - DONE.
43   *
44   * 6. Buffered I/O. - Partially done, and optional.
45   *
46   */
47  
48  package org.apache.james.mailrepository;
49  
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.Collections;
79  import java.util.Hashtable;
80  import java.util.Iterator;
81  import java.util.Locale;
82  import java.util.Properties;
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: 684816 $
110  */
111 
112 
113 public class MBoxMailRepository
114         extends AbstractLogEnabled
115             implements MailRepository, Configurable {
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     private boolean fifo;
147     
148     /**
149      * A callback used when a message is read from the mbox file
150      */
151     public interface MessageAction {
152         public boolean isComplete();  // *** Not valid until AFTER each call to messageAction(...)!
153         public MimeMessage messageAction(String messageSeparator, String bodyText, long messageStart);
154     }
155 
156 
157     /**
158      * Convert a MimeMessage into raw text
159      * @param mc The mime message to convert
160      * @return A string representation of the mime message
161      * @throws IOException
162      * @throws MessagingException
163      */
164     private String getRawMessage(MimeMessage mc) throws IOException, MessagingException {
165 
166         ByteArrayOutputStream rawMessage = new ByteArrayOutputStream();
167         mc.writeTo(rawMessage);
168         return rawMessage.toString();
169     }
170 
171     /**
172      * Parse a text block as an email and convert it into a mime message
173      * @param emailBody The headers and body of an email. This will be parsed into a mime message and stored
174      */
175     private MimeMessage convertTextToMimeMessage(String emailBody) {
176         //this.emailBody = emailBody;
177         MimeMessage mimeMessage = null;
178         // Parse the mime message as we have the full message now (in string format)
179         ByteArrayInputStream mb = new ByteArrayInputStream(emailBody.getBytes());
180         Properties props = System.getProperties();
181         Session session = Session.getDefaultInstance(props);
182         try {
183             mimeMessage = new MimeMessage(session, mb);
184 
185 
186         } catch (MessagingException e) {
187             getLogger().error("Unable to parse mime message!", e);
188         }
189 
190         if (mimeMessage == null && getLogger().isDebugEnabled()) {
191             StringBuffer logBuffer =
192                     new StringBuffer(128)
193                     .append(this.getClass().getName())
194                     .append(" Mime message is null");
195             getLogger().debug(logBuffer.toString());
196         }
197 
198         /*
199         String toAddr = null;
200         try {
201             // Attempt to read the TO field and see if it errors
202             toAddr = mimeMessage.getRecipients(javax.mail.Message.RecipientType.TO).toString();
203         } catch (Exception e) {
204             // It has errored, so time for plan B
205             // use the from field I suppose
206             try {
207                 mimeMessage.setRecipients(javax.mail.Message.RecipientType.TO, mimeMessage.getFrom());
208                 if (getLogger().isDebugEnabled()) {
209                     StringBuffer logBuffer =
210                             new StringBuffer(128)
211                             .append(this.getClass().getName())
212                             .append(" Patching To: field for message ")
213                             .append(" with  From: field");
214                     getLogger().debug(logBuffer.toString());
215                 }
216             } catch (MessagingException e1) {
217                 getLogger().error("Unable to set to: field to from: field", e);
218             }
219         } */
220         return mimeMessage;
221     }
222 
223     /**
224      * Generate a hex representation of an MD5 checksum on the emailbody
225      * @param emailBody
226      * @return A hex representation of the text
227      * @throws NoSuchAlgorithmException
228      */
229     private String generateKeyValue(String emailBody) throws NoSuchAlgorithmException {
230         // MD5 the email body for a reilable (ha ha) key
231         byte[] digArray = MessageDigest.getInstance("MD5").digest(emailBody.getBytes());
232         StringBuffer digest = new StringBuffer();
233         for (int i = 0; i < digArray.length; i++) {
234             digest.append(Integer.toString(digArray[i], Character.MAX_RADIX).toUpperCase(Locale.US));
235         }
236         return digest.toString();
237     }
238 
239     /**
240      * Parse the mbox file.
241      * @param ins The random access file to load. Note that the file may or may not start at offset 0 in the file
242      * @param messAct The action to take when a message is found
243      */
244     private MimeMessage parseMboxFile(RandomAccessFile ins, MessageAction messAct) {
245         if ((DEEP_DEBUG) && (getLogger().isDebugEnabled())) {
246             StringBuffer logBuffer =
247                     new StringBuffer(128)
248                     .append(this.getClass().getName())
249                     .append(" Start parsing ")
250                     .append(mboxFile);
251 
252             getLogger().debug(logBuffer.toString());
253         }
254         try {
255 
256             Perl5Compiler sepMatchCompiler = new Perl5Compiler();
257             Pattern sepMatchPattern = sepMatchCompiler.compile("^From (.*) (.*):(.*):(.*)$");
258             Perl5Matcher sepMatch = new Perl5Matcher();
259 
260             int c;
261             boolean inMessage = false;
262             StringBuffer messageBuffer = new StringBuffer();
263             String previousMessageSeparator = null;
264             boolean foundSep = false;
265 
266             long prevMessageStart = ins.getFilePointer();
267             if (BUFFERING) {
268             String line = null;
269             while ((line = ins.readLine()) != null) {
270                 foundSep = sepMatch.contains(line + "\n", sepMatchPattern);
271 
272                 if (foundSep && inMessage) {
273 //                    if ((DEEP_DEBUG) && (getLogger().isDebugEnabled())) {
274 //                        getLogger().debug(this.getClass().getName() + " Invoking " + messAct.getClass() + " at " + prevMessageStart);
275 //                    }
276                     MimeMessage endResult = messAct.messageAction(previousMessageSeparator, messageBuffer.toString(), prevMessageStart);
277                     if (messAct.isComplete()) {
278                         // I've got what I want so just exit
279                         return endResult;
280                     }
281                     previousMessageSeparator = line;
282                     prevMessageStart = ins.getFilePointer() - line.length();
283                     messageBuffer = new StringBuffer();
284                     inMessage = true;
285                 }
286                 // Only done at the start (first header)
287                 if (foundSep && !inMessage) {
288                     previousMessageSeparator = line.toString();
289                     inMessage = true;
290                 }
291                 if (!foundSep && inMessage) {
292                     messageBuffer.append(line).append("\n");
293                 }
294             }
295             } else {
296             StringBuffer line = new StringBuffer();
297             while ((c = ins.read()) != -1) {
298                 if (c == 10) {
299                     foundSep = sepMatch.contains(line.toString(), sepMatchPattern);
300                     if (foundSep && inMessage) {
301 //                        if ((DEEP_DEBUG) && (getLogger().isDebugEnabled())) {
302 //                            getLogger().debug(this.getClass().getName() + " Invoking " + messAct.getClass() + " at " + prevMessageStart);
303 //                        }
304                         MimeMessage endResult = messAct.messageAction(previousMessageSeparator, messageBuffer.toString(), prevMessageStart);
305                         if (messAct.isComplete()) {
306                             // I've got what I want so just exit
307                             return endResult;
308                         }
309                         previousMessageSeparator = line.toString();
310                         prevMessageStart = ins.getFilePointer() - line.length();
311                         messageBuffer = new StringBuffer();
312                         inMessage = true;
313                     }
314                     // Only done at the start (first header)
315                     if (foundSep && inMessage == false) {
316                         previousMessageSeparator = line.toString();
317                         inMessage = true;
318                     }
319                     if (!foundSep) {
320                         messageBuffer.append(line).append((char) c);
321                     }
322                     line = new StringBuffer(); // Reset buffer
323                 } else {
324                     line.append((char) c);
325                 }
326             }
327             }
328 
329             if (messageBuffer.length() != 0) {
330                 // process last message
331                 return messAct.messageAction(previousMessageSeparator, messageBuffer.toString(), prevMessageStart);
332             }
333         } catch (IOException ioEx) {
334             getLogger().error("Unable to write file (General I/O problem) " + mboxFile, ioEx);
335         } catch (MalformedPatternException e) {
336             getLogger().error("Bad regex passed " + mboxFile, e);
337         } finally {
338             if ((DEEP_DEBUG) && (getLogger().isDebugEnabled())) {
339                 StringBuffer logBuffer =
340                         new StringBuffer(128)
341                         .append(this.getClass().getName())
342                         .append(" Finished parsing ")
343                         .append(mboxFile);
344 
345                 getLogger().debug(logBuffer.toString());
346             }
347         }
348         return null;
349     }
350 
351     /**
352      * Find a given message
353      * This method will first use selectMessage(key) to see if the key/offset combination allows us to skip
354      * parts of the file and only load the message we are interested in
355      *
356      * @param key The key of the message to find
357      */
358     private MimeMessage findMessage(String key) {
359         MimeMessage foundMessage = null;
360 
361         // See if we can get the message by using the cache position first
362         foundMessage = selectMessage(key);
363         if (foundMessage == null) {
364             // If the message is not found something has changed from
365             // the cache.  The cache may have been invalidated by
366             // another method, or the file may have been replaced from
367             // underneath us.  Reload the cache, and try again.
368             mList = null;
369             loadKeys();
370             foundMessage = selectMessage(key);
371         }
372         return foundMessage;
373     }
374 
375     /**
376      * Quickly find a message by using the stored message offsets
377      * @param key  The key of the message to find
378      */
379     private MimeMessage selectMessage(final String key) {
380         MimeMessage foundMessage = null;
381         // Can we find the key first
382         if (mList == null || !mList.containsKey(key)) {
383             // Not initiailised so no point looking
384             if ((DEEP_DEBUG) && (getLogger().isDebugEnabled())) {
385                 StringBuffer logBuffer =
386                         new StringBuffer(128)
387                         .append(this.getClass().getName())
388                         .append(" mList - key not found ")
389                         .append(mboxFile);
390 
391                 getLogger().debug(logBuffer.toString());
392             }
393             return foundMessage;
394         }
395         long messageStart = ((Long) mList.get(key)).longValue();
396         if ((DEEP_DEBUG) && (getLogger().isDebugEnabled())) {
397             StringBuffer logBuffer =
398                     new StringBuffer(128)
399                     .append(this.getClass().getName())
400                     .append(" Load message starting at offset ")
401                     .append(messageStart)
402                     .append(" from file ")
403                     .append(mboxFile);
404 
405             getLogger().debug(logBuffer.toString());
406         }
407         // Now try and find the position in the file
408         RandomAccessFile ins = null;
409         try {
410             ins = new RandomAccessFile(mboxFile, "r");
411             if (messageStart != 0) {
412                 ins.seek(messageStart - 1);
413             }
414             MessageAction op = new MessageAction() {
415                 public boolean isComplete() { return true; }
416                 public MimeMessage messageAction(String messageSeparator, String bodyText, long messageStart) {
417                     try {
418                         if (key.equals(generateKeyValue(bodyText))) {
419                             getLogger().debug(this.getClass().getName() + " Located message. Returning MIME message");
420                             return convertTextToMimeMessage(bodyText);
421                         }
422                     } catch (NoSuchAlgorithmException e) {
423                         getLogger().error("MD5 not supported! ",e);
424                     }
425                     return null;
426                 }
427             };
428             foundMessage = this.parseMboxFile(ins, op);
429         } catch (FileNotFoundException e) {
430             getLogger().error("Unable to save(open) file (File not found) " + mboxFile, e);
431         } catch (IOException e) {
432             getLogger().error("Unable to write file (General I/O problem) " + mboxFile, e);
433         } finally {
434             if (foundMessage == null) {
435                 if ((DEEP_DEBUG) && (getLogger().isDebugEnabled())) {
436                     StringBuffer logBuffer =
437                             new StringBuffer(128)
438                             .append(this.getClass().getName())
439                             .append(" select - message not found ")
440                             .append(mboxFile);
441 
442                     getLogger().debug(logBuffer.toString());
443                 }
444             }
445             if (ins != null) try { ins.close(); } catch (IOException e) { getLogger().error("Unable to close file (General I/O problem) " + mboxFile, e); }
446         }
447         return foundMessage;
448     }
449 
450     /**
451      * Load the message keys and file pointer offsets from disk
452      */
453     private synchronized void loadKeys() {
454         if (mList!=null) {
455             return;
456         }
457         RandomAccessFile ins = null;
458         try {
459             ins = new RandomAccessFile(mboxFile, "r");
460             long initialCapacity = (ins.length() >  MLISTPRESIZEFACTOR ? ins.length() /MLISTPRESIZEFACTOR  : 0);
461             if (initialCapacity < DEFAULTMLISTCAPACITY ) {
462                 initialCapacity =  DEFAULTMLISTCAPACITY;
463             }
464             if (initialCapacity > Integer.MAX_VALUE) {
465                 initialCapacity = Integer.MAX_VALUE - 1;
466             }
467             this.mList = new Hashtable((int)initialCapacity);
468             this.parseMboxFile(ins, new MessageAction() {
469                 public boolean isComplete() { return false; }
470                 public MimeMessage messageAction(String messageSeparator, String bodyText, long messageStart) {
471                     try {
472                         String key = generateKeyValue(bodyText);
473                         mList.put(key, new Long(messageStart));
474                         if ((DEEP_DEBUG) && (getLogger().isDebugEnabled())) {
475                             getLogger().debug(this.getClass().getName() + " Key " + key + " at " + messageStart);
476                         }
477                         
478                     } catch (NoSuchAlgorithmException e) {
479                         getLogger().error("MD5 not supported! ",e);
480                     }
481                     return null;
482                 }
483             });
484             //System.out.println("Done Load keys!");
485         } catch (FileNotFoundException e) {
486             getLogger().error("Unable to save(open) file (File not found) " + mboxFile, e);
487             this.mList = new Hashtable((int)DEFAULTMLISTCAPACITY);
488         } catch (IOException e) {
489             getLogger().error("Unable to write file (General I/O problem) " + mboxFile, e);
490         } finally {
491             if (ins != null) try { ins.close(); } catch (IOException e) { getLogger().error("Unable to close file (General I/O problem) " + mboxFile, e); }
492         }
493     }
494 
495 
496     /**
497      * @see org.apache.james.services.MailRepository#store(Mail)
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     /**
546      * @see org.apache.james.services.MailRepository#list()
547      */
548     public Iterator list() {
549         loadKeys();
550         ArrayList keys =  new ArrayList(mList.keySet());
551         
552         if (keys.isEmpty() == false) {
553             // find the first message.  This is a trick to make sure that if
554             // the file is changed out from under us, we will detect it and
555             // correct for it BEFORE we return the iterator.
556             findMessage((String) keys.iterator().next());
557         }
558         if ((DEEP_DEBUG) && (getLogger().isDebugEnabled())) {
559             StringBuffer logBuffer =
560                     new StringBuffer(128)
561                     .append(this.getClass().getName())
562                     .append(" ")
563                     .append(keys.size())
564                     .append(" keys to be iterated over.");
565 
566             getLogger().debug(logBuffer.toString());
567         }
568         if (fifo) Collections.sort(keys); // Keys is a HashSet; impose FIFO for apps that need it
569         return keys.iterator();
570     }
571 
572     /**
573      * @see org.apache.james.services.MailRepository#retrieve(String)
574      */
575     public Mail retrieve(String key) {
576 
577         loadKeys();
578         MailImpl res = null;
579 
580         MimeMessage foundMessage = findMessage(key);
581         if (foundMessage == null) {
582             getLogger().error("found message is null!");
583             return null;
584         }
585         res = new MailImpl();
586         res.setMessage(foundMessage);
587         res.setName(key);
588         if ((DEEP_DEBUG) && (getLogger().isDebugEnabled())) {
589             StringBuffer logBuffer =
590                     new StringBuffer(128)
591                     .append(this.getClass().getName())
592                     .append(" Retrieving entry for key ")
593                     .append(key);
594 
595             getLogger().debug(logBuffer.toString());
596         }
597         return res;
598     }
599 
600     /**
601      * @see org.apache.james.services.MailRepository#remove(Mail)
602      */
603     public void remove(Mail mail) {
604         ArrayList remArray = new ArrayList();
605         remArray.add(mail);
606         remove(remArray);
607     }
608 
609     /**
610      * Attempt to get a lock on the mbox by creating
611      * the file mboxname.lock
612      * @throws Exception
613      */
614     private void lockMBox() throws Exception {
615         // Create the lock file (if possible)
616         String lockFileName = mboxFile + LOCKEXT;
617         int sleepCount = 0;
618         File mBoxLock = new File(lockFileName);
619         if (!mBoxLock.createNewFile()) {
620             // This is not good, somebody got the lock before me
621             // So wait for a file
622             while (!mBoxLock.createNewFile() && sleepCount < MAXSLEEPTIMES) {
623                 try {
624                     if ((DEEP_DEBUG) && (getLogger().isDebugEnabled())) {
625                         StringBuffer logBuffer =
626                                 new StringBuffer(128)
627                                 .append(this.getClass().getName())
628                                 .append(" Waiting for lock on file ")
629                                 .append(mboxFile);
630 
631                         getLogger().debug(logBuffer.toString());
632                     }
633 
634                     Thread.sleep(LOCKSLEEPDELAY);
635                     sleepCount++;
636                 } catch (InterruptedException e) {
637                     getLogger().error("File lock wait for " + mboxFile + " interrupted!",e);
638 
639                 }
640             }
641             if (sleepCount >= MAXSLEEPTIMES) {
642                 throw new Exception("Unable to get lock on file " + mboxFile);
643             }
644         }
645     }
646 
647     /**
648      * Unlock a previously locked mbox file
649      */
650     private void unlockMBox() {
651         // Just delete the MBOX file
652         String lockFileName = mboxFile + LOCKEXT;
653         File mBoxLock = new File(lockFileName);
654         if (!mBoxLock.delete()) {
655             StringBuffer logBuffer =
656                     new StringBuffer(128)
657                     .append(this.getClass().getName())
658                     .append(" Failed to delete lock file ")
659                     .append(lockFileName);
660             getLogger().error(logBuffer.toString());
661         }
662     }
663 
664 
665 
666     /**
667      * @see org.apache.james.services.MailRepository#remove(Collection)
668      */
669     public void remove(final Collection mails)
670     {
671         if ((DEEP_DEBUG) && (getLogger().isDebugEnabled())) {
672             StringBuffer logBuffer =
673                     new StringBuffer(128)
674                     .append(this.getClass().getName())
675                     .append(" Removing entry for key ")
676                     .append(mails);
677 
678             getLogger().debug(logBuffer.toString());
679         }
680         // The plan is as follows:
681         // Attempt to locate the message in the file
682         // by reading through the
683         // once we've done that then seek to the file
684         try {
685             RandomAccessFile ins = new RandomAccessFile(mboxFile, "r"); // The source
686             final RandomAccessFile outputFile = new RandomAccessFile(mboxFile + WORKEXT, "rw"); // The destination
687             parseMboxFile(ins, new MessageAction() {
688                 public boolean isComplete() { return false; }
689                 public MimeMessage messageAction(String messageSeparator, String bodyText, long messageStart) {
690                     // Write out the messages as we go, until we reach the key we want
691                     try {
692                         String currentKey=generateKeyValue(bodyText);
693                         boolean foundKey=false;
694                         Iterator mailList = mails.iterator();
695                         String key;
696                         while (mailList.hasNext()) {
697                             // Attempt to find the current key in the array
698                             key = ((Mail)mailList.next()).getName();
699                             if (key.equals(currentKey)) {
700                                 // Don't write the message to disk
701                                 foundKey = true;
702                                 break;
703                             }
704                         }
705                         if (foundKey == false)
706                         {
707                             // We didn't find the key in the array so we will keep it
708                             outputFile.writeBytes(messageSeparator + "\n");
709                             outputFile.writeBytes(bodyText);
710 
711                         }
712                     } catch (NoSuchAlgorithmException e) {
713                         getLogger().error("MD5 not supported! ",e);
714                     } catch (IOException e) {
715                         getLogger().error("Unable to write file (General I/O problem) " + mboxFile, e);
716                     }
717                     return null;
718                 }
719             });
720             ins.close();
721             outputFile.close();
722             // Delete the old mbox file
723             File mbox = new File(mboxFile);
724             mbox.delete();
725             // And rename the lock file to be the new mbox
726             mbox = new File(mboxFile + WORKEXT);
727             if (!mbox.renameTo(new File(mboxFile)))
728             {
729                  System.out.println("Failed to rename file!");
730             }
731 
732             // Now delete the keys in mails from the main hash
733             Iterator mailList = mails.iterator();
734             String key;
735             while (mailList.hasNext()) {
736                 // Attempt to find the current key in the array
737                 key = ((Mail)mailList.next()).getName();
738                 mList.remove(key);
739             }
740 
741 
742         } catch (FileNotFoundException e) {
743             getLogger().error("Unable to save(open) file (File not found) " + mboxFile, e);
744         } catch (IOException e) {
745             getLogger().error("Unable to write file (General I/O problem) " + mboxFile, e);
746         }
747     }
748 
749     /**
750      * @see org.apache.james.services.MailRepository#remove(String)
751      */
752     public void remove(String key) {
753         loadKeys();
754         try {
755             lockMBox();
756         } catch (Exception e) {
757             getLogger().error("Lock failed!",e);
758             return; // No lock, so exit
759         }
760         ArrayList keys = new ArrayList();
761         keys.add(retrieve(key));
762 
763         this.remove(keys);
764         unlockMBox();
765     }
766 
767     /**
768      * @see org.apache.james.services.MailRepository#lock(String)
769      */
770     public boolean lock(String key) {
771         return false;
772     }
773 
774     /**
775      * @see org.apache.james.services.MailRepository#unlock(String)
776      */
777     public boolean unlock(String key) {
778         return false;
779     }
780 
781     /**
782      * @see org.apache.avalon.framework.configuration.Configurable#configure(Configuration)
783      */
784     public void configure(Configuration conf) throws ConfigurationException {
785         String destination;
786         this.mList = null;
787         BUFFERING = conf.getAttributeAsBoolean("BUFFERING", true);
788         fifo = conf.getAttributeAsBoolean("FIFO", false);
789         destination = conf.getAttribute("destinationURL");
790         if (destination.charAt(destination.length() - 1) == '/') {
791             // Remove the trailing / as well as the protocol marker
792             mboxFile = destination.substring("mbox://".length(), destination.lastIndexOf("/"));
793         } else {
794             mboxFile = destination.substring("mbox://".length());
795         }
796 
797         if (getLogger().isDebugEnabled()) {
798             getLogger().debug("MBoxMailRepository.destinationURL: " + destination);
799         }
800 
801         String checkType = conf.getAttribute("type");
802         if (!(checkType.equals("MAIL") || checkType.equals("SPOOL"))) {
803             String exceptionString = "Attempt to configure MboxMailRepository as " + checkType;
804             if (getLogger().isWarnEnabled()) {
805                 getLogger().warn(exceptionString);
806             }
807             throw new ConfigurationException(exceptionString);
808         }
809     }
810 
811 }