1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
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;
122 static final int MAXSLEEPTIMES = 100;
123 static final long MLISTPRESIZEFACTOR = 10 * 1024;
124 static final long DEFAULTMLISTCAPACITY = 20;
125
126
127
128
129 private static boolean BUFFERING = true;
130
131
132
133
134 private static final boolean DEEP_DEBUG = true;
135
136
137
138
139
140 private Hashtable mList = null;
141
142
143
144 private String mboxFile;
145
146 private boolean fifo;
147
148
149
150
151 public interface MessageAction {
152 public boolean isComplete();
153 public MimeMessage messageAction(String messageSeparator, String bodyText, long messageStart);
154 }
155
156
157
158
159
160
161
162
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
173
174
175 private MimeMessage convertTextToMimeMessage(String emailBody) {
176
177 MimeMessage mimeMessage = null;
178
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
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220 return mimeMessage;
221 }
222
223
224
225
226
227
228
229 private String generateKeyValue(String emailBody) throws NoSuchAlgorithmException {
230
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
241
242
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
274
275
276 MimeMessage endResult = messAct.messageAction(previousMessageSeparator, messageBuffer.toString(), prevMessageStart);
277 if (messAct.isComplete()) {
278
279 return endResult;
280 }
281 previousMessageSeparator = line;
282 prevMessageStart = ins.getFilePointer() - line.length();
283 messageBuffer = new StringBuffer();
284 inMessage = true;
285 }
286
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
302
303
304 MimeMessage endResult = messAct.messageAction(previousMessageSeparator, messageBuffer.toString(), prevMessageStart);
305 if (messAct.isComplete()) {
306
307 return endResult;
308 }
309 previousMessageSeparator = line.toString();
310 prevMessageStart = ins.getFilePointer() - line.length();
311 messageBuffer = new StringBuffer();
312 inMessage = true;
313 }
314
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();
323 } else {
324 line.append((char) c);
325 }
326 }
327 }
328
329 if (messageBuffer.length() != 0) {
330
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
353
354
355
356
357
358 private MimeMessage findMessage(String key) {
359 MimeMessage foundMessage = null;
360
361
362 foundMessage = selectMessage(key);
363 if (foundMessage == null) {
364
365
366
367
368 mList = null;
369 loadKeys();
370 foundMessage = selectMessage(key);
371 }
372 return foundMessage;
373 }
374
375
376
377
378
379 private MimeMessage selectMessage(final String key) {
380 MimeMessage foundMessage = null;
381
382 if (mList == null || !mList.containsKey(key)) {
383
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
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
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
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
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
512 String fromHeader = null;
513 String message = null;
514 try {
515 message = getRawMessage(mc.getMessage());
516
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
529 RandomAccessFile saveFile = null;
530 try {
531 saveFile = new RandomAccessFile(mboxFile, "rw");
532 saveFile.seek(saveFile.length());
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
547
548 public Iterator list() {
549 loadKeys();
550 ArrayList keys = new ArrayList(mList.keySet());
551
552 if (keys.isEmpty() == false) {
553
554
555
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);
569 return keys.iterator();
570 }
571
572
573
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
602
603 public void remove(Mail mail) {
604 ArrayList remArray = new ArrayList();
605 remArray.add(mail);
606 remove(remArray);
607 }
608
609
610
611
612
613
614 private void lockMBox() throws Exception {
615
616 String lockFileName = mboxFile + LOCKEXT;
617 int sleepCount = 0;
618 File mBoxLock = new File(lockFileName);
619 if (!mBoxLock.createNewFile()) {
620
621
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
649
650 private void unlockMBox() {
651
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
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
681
682
683
684 try {
685 RandomAccessFile ins = new RandomAccessFile(mboxFile, "r");
686 final RandomAccessFile outputFile = new RandomAccessFile(mboxFile + WORKEXT, "rw");
687 parseMboxFile(ins, new MessageAction() {
688 public boolean isComplete() { return false; }
689 public MimeMessage messageAction(String messageSeparator, String bodyText, long messageStart) {
690
691 try {
692 String currentKey=generateKeyValue(bodyText);
693 boolean foundKey=false;
694 Iterator mailList = mails.iterator();
695 String key;
696 while (mailList.hasNext()) {
697
698 key = ((Mail)mailList.next()).getName();
699 if (key.equals(currentKey)) {
700
701 foundKey = true;
702 break;
703 }
704 }
705 if (foundKey == false)
706 {
707
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
723 File mbox = new File(mboxFile);
724 mbox.delete();
725
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
733 Iterator mailList = mails.iterator();
734 String key;
735 while (mailList.hasNext()) {
736
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
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;
759 }
760 ArrayList keys = new ArrayList();
761 keys.add(retrieve(key));
762
763 this.remove(keys);
764 unlockMBox();
765 }
766
767
768
769
770 public boolean lock(String key) {
771 return false;
772 }
773
774
775
776
777 public boolean unlock(String key) {
778 return false;
779 }
780
781
782
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
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 }