]> git.argeo.org Git - gpl/argeo-slc.git/blob - ext/javax.mail.mbox/src/com/sun/mail/mbox/MboxFolder.java
Make logging synchronous during native image build
[gpl/argeo-slc.git] / ext / javax.mail.mbox / src / com / sun / mail / mbox / MboxFolder.java
1 /*
2 * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved.
3 *
4 * This program and the accompanying materials are made available under the
5 * terms of the Eclipse Public License v. 2.0, which is available at
6 * http://www.eclipse.org/legal/epl-2.0.
7 *
8 * This Source Code may also be made available under the following Secondary
9 * Licenses when the conditions for such availability set forth in the
10 * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
11 * version 2 with the GNU Classpath Exception, which is available at
12 * https://www.gnu.org/software/classpath/license.html.
13 *
14 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15 */
16
17 package com.sun.mail.mbox;
18
19 import javax.mail.*;
20 import javax.mail.event.*;
21 import javax.mail.internet.*;
22 import javax.mail.util.*;
23 import java.io.*;
24 import java.util.*;
25 import com.sun.mail.util.LineInputStream;
26
27 /**
28 * This class represents a mailbox file containing RFC822 style email messages.
29 *
30 * @author John Mani
31 * @author Bill Shannon
32 */
33
34 public class MboxFolder extends Folder {
35
36 private String name; // null => the default folder
37 private boolean is_inbox = false;
38 private int total; // total number of messages in mailbox
39 private volatile boolean opened = false;
40 private List<MessageMetadata> messages;
41 private TempFile temp;
42 private MboxStore mstore;
43 private MailFile folder;
44 private long file_size; // the size the last time we read or wrote it
45 private long saved_file_size; // size at the last open, close, or expunge
46 private boolean special_imap_message;
47
48 private static final boolean homeRelative =
49 Boolean.getBoolean("mail.mbox.homerelative");
50
51 /**
52 * Metadata for each message, to avoid instantiating MboxMessage
53 * objects for messages we're not going to look at. <p>
54 *
55 * MboxFolder keeps an array of these objects corresponding to
56 * each message in the folder. Access to the array elements is
57 * synchronized, but access to contents of this object is not.
58 * The metadata stored here is only accessed if the message field
59 * is null; otherwise the MboxMessage object contains the metadata.
60 */
61 static final class MessageMetadata {
62 public long start; // offset in temp file of start of this message
63 // public long end; // offset in temp file of end of this message
64 public long dataend; // offset of end of message data, <= "end"
65 public MboxMessage message; // the message itself
66 public boolean recent; // message is recent?
67 public boolean deleted; // message is marked deleted?
68 public boolean imap; // special imap message?
69 }
70
71 public MboxFolder(MboxStore store, String name) {
72 super(store);
73 this.mstore = store;
74 this.name = name;
75
76 if (name != null && name.equalsIgnoreCase("INBOX"))
77 is_inbox = true;
78
79 folder = mstore.getMailFile(name == null ? "~" : name);
80 if (folder.exists())
81 saved_file_size = folder.length();
82 else
83 saved_file_size = -1;
84 }
85
86 public char getSeparator() {
87 return File.separatorChar;
88 }
89
90 public Folder[] list(String pattern) throws MessagingException {
91 if (!folder.isDirectory())
92 throw new MessagingException("not a directory");
93
94 if (name == null)
95 return list(null, pattern, true);
96 else
97 return list(name + File.separator, pattern, false);
98 }
99
100 /*
101 * Version of list shared by MboxStore and MboxFolder.
102 */
103 protected Folder[] list(String ref, String pattern, boolean fromStore)
104 throws MessagingException {
105 if (ref != null && ref.length() == 0)
106 ref = null;
107 int i;
108 String refdir = null;
109 String realdir = null;
110
111 pattern = canonicalize(ref, pattern);
112 if ((i = indexOfAny(pattern, "%*")) >= 0) {
113 refdir = pattern.substring(0, i);
114 } else {
115 refdir = pattern;
116 }
117 if ((i = refdir.lastIndexOf(File.separatorChar)) >= 0) {
118 // get rid of anything after directory name
119 refdir = refdir.substring(0, i + 1);
120 realdir = mstore.mb.filename(mstore.user, refdir);
121 } else if (refdir.length() == 0 || refdir.charAt(0) != '~') {
122 // no separator and doesn't start with "~" => home or cwd
123 refdir = null;
124 if (homeRelative)
125 realdir = mstore.home;
126 else
127 realdir = ".";
128 } else {
129 realdir = mstore.mb.filename(mstore.user, refdir);
130 }
131 List<String> flist = new ArrayList<String>();
132 listWork(realdir, refdir, pattern, fromStore ? 0 : 1, flist);
133 if (Match.path("INBOX", pattern, '\0'))
134 flist.add("INBOX");
135
136 Folder fl[] = new Folder[flist.size()];
137 for (i = 0; i < fl.length; i++) {
138 fl[i] = createFolder(mstore, flist.get(i));
139 }
140 return fl;
141 }
142
143 public String getName() {
144 if (name == null)
145 return "";
146 else if (is_inbox)
147 return "INBOX";
148 else
149 return folder.getName();
150 }
151
152 public String getFullName() {
153 if (name == null)
154 return "";
155 else
156 return name;
157 }
158
159 public Folder getParent() {
160 if (name == null)
161 return null;
162 else if (is_inbox)
163 return createFolder(mstore, null);
164 else
165 // XXX - have to recognize other folders under default folder
166 return createFolder(mstore, folder.getParent());
167 }
168
169 public boolean exists() {
170 return folder.exists();
171 }
172
173 public int getType() {
174 if (folder.isDirectory())
175 return HOLDS_FOLDERS;
176 else
177 return HOLDS_MESSAGES;
178 }
179
180 public Flags getPermanentFlags() {
181 return MboxStore.permFlags;
182 }
183
184 public synchronized boolean hasNewMessages() {
185 if (folder instanceof UNIXFile) {
186 UNIXFile f = (UNIXFile)folder;
187 if (f.length() > 0) {
188 long atime = f.lastAccessed();
189 long mtime = f.lastModified();
190 //System.out.println(name + " atime " + atime + " mtime " + mtime);
191 return atime < mtime;
192 }
193 return false;
194 }
195 long current_size;
196 if (folder.exists())
197 current_size = folder.length();
198 else
199 current_size = -1;
200 // if we've never opened the folder, remember the size now
201 // (will cause us to return false the first time)
202 if (saved_file_size < 0)
203 saved_file_size = current_size;
204 return current_size > saved_file_size;
205 }
206
207 public synchronized Folder getFolder(String name)
208 throws MessagingException {
209 if (folder.exists() && !folder.isDirectory())
210 throw new MessagingException("not a directory");
211 return createFolder(mstore,
212 (this.name == null ? "~" : this.name) + File.separator + name);
213 }
214
215 public synchronized boolean create(int type) throws MessagingException {
216 switch (type) {
217 case HOLDS_FOLDERS:
218 if (!folder.mkdirs()) {
219 return false;
220 }
221 break;
222
223 case HOLDS_MESSAGES:
224 if (folder.exists()) {
225 return false;
226 }
227 try {
228 (new FileOutputStream((File)folder)).close();
229 } catch (FileNotFoundException fe) {
230 File parent = new File(folder.getParent());
231 if (!parent.mkdirs())
232 throw new
233 MessagingException("can't create folder: " + name);
234 try {
235 (new FileOutputStream((File)folder)).close();
236 } catch (IOException ex3) {
237 throw new
238 MessagingException("can't create folder: " + name, ex3);
239 }
240 } catch (IOException e) {
241 throw new
242 MessagingException("can't create folder: " + name, e);
243 }
244 break;
245
246 default:
247 throw new MessagingException("type not supported");
248 }
249 notifyFolderListeners(FolderEvent.CREATED);
250 return true;
251 }
252
253 public synchronized boolean delete(boolean recurse)
254 throws MessagingException {
255 checkClosed();
256 if (name == null)
257 throw new MessagingException("can't delete default folder");
258 boolean ret = true;
259 if (recurse && folder.isDirectory())
260 ret = delete(new File(folder.getPath()));
261 if (ret && folder.delete()) {
262 notifyFolderListeners(FolderEvent.DELETED);
263 return true;
264 }
265 return false;
266 }
267
268 /**
269 * Recursively delete the specified file/directory.
270 */
271 private boolean delete(File f) {
272 File[] files = f.listFiles();
273 boolean ret = true;
274 for (int i = 0; ret && i < files.length; i++) {
275 if (files[i].isDirectory())
276 ret = delete(files[i]);
277 else
278 ret = files[i].delete();
279 }
280 return ret;
281 }
282
283 public synchronized boolean renameTo(Folder f)
284 throws MessagingException {
285 checkClosed();
286 if (name == null)
287 throw new MessagingException("can't rename default folder");
288 if (!(f instanceof MboxFolder))
289 throw new MessagingException("can't rename to: " + f.getName());
290 String newname = ((MboxFolder)f).folder.getPath();
291 if (folder.renameTo(new File(newname))) {
292 notifyFolderRenamedListeners(f);
293 return true;
294 }
295 return false;
296 }
297
298 /* Ensure the folder is open */
299 void checkOpen() throws IllegalStateException {
300 if (!opened)
301 throw new IllegalStateException("Folder is not Open");
302 }
303
304 /* Ensure the folder is not open */
305 private void checkClosed() throws IllegalStateException {
306 if (opened)
307 throw new IllegalStateException("Folder is Open");
308 }
309
310 /*
311 * Check that the given message number is within the range
312 * of messages present in this folder. If the message
313 * number is out of range, we check to see if new messages
314 * have arrived.
315 */
316 private void checkRange(int msgno) throws MessagingException {
317 if (msgno < 1) // message-numbers start at 1
318 throw new IndexOutOfBoundsException("message number < 1");
319
320 if (msgno <= total)
321 return;
322
323 // Out of range, let's check if there are any new messages.
324 getMessageCount();
325
326 if (msgno > total) // Still out of range ? Throw up ...
327 throw new IndexOutOfBoundsException(msgno + " > " + total);
328 }
329
330 /* Ensure the folder is open & readable */
331 private void checkReadable() throws IllegalStateException {
332 if (!opened || (mode != READ_ONLY && mode != READ_WRITE))
333 throw new IllegalStateException("Folder is not Readable");
334 }
335
336 /* Ensure the folder is open & writable */
337 private void checkWritable() throws IllegalStateException {
338 if (!opened || mode != READ_WRITE)
339 throw new IllegalStateException("Folder is not Writable");
340 }
341
342 public boolean isOpen() {
343 return opened;
344 }
345
346 /*
347 * Open the folder in the specified mode.
348 */
349 public synchronized void open(int mode) throws MessagingException {
350 if (opened)
351 throw new IllegalStateException("Folder is already Open");
352
353 if (!folder.exists())
354 throw new FolderNotFoundException(this, "Folder doesn't exist: " +
355 folder.getPath());
356 this.mode = mode;
357 switch (mode) {
358 case READ_WRITE:
359 default:
360 if (!folder.canWrite())
361 throw new MessagingException("Open Failure, can't write: " +
362 folder.getPath());
363 // fall through...
364
365 case READ_ONLY:
366 if (!folder.canRead())
367 throw new MessagingException("Open Failure, can't read: " +
368 folder.getPath());
369 break;
370 }
371
372 if (is_inbox && folder instanceof InboxFile) {
373 InboxFile inf = (InboxFile)folder;
374 if (!inf.openLock(mode == READ_WRITE ? "rw" : "r"))
375 throw new MessagingException("Failed to lock INBOX");
376 }
377 if (!folder.lock("r"))
378 throw new MessagingException("Failed to lock folder: " + name);
379 messages = new ArrayList<MessageMetadata>();
380 total = 0;
381 Message[] msglist = null;
382 try {
383 temp = new TempFile(null);
384 saved_file_size = folder.length();
385 msglist = load(0L, false);
386 } catch (IOException e) {
387 throw new MessagingException("IOException", e);
388 } finally {
389 folder.unlock();
390 }
391 notifyConnectionListeners(ConnectionEvent.OPENED);
392 if (msglist != null)
393 notifyMessageAddedListeners(msglist);
394 opened = true; // now really opened
395 }
396
397 public synchronized void close(boolean expunge) throws MessagingException {
398 checkOpen();
399
400 try {
401 if (mode == READ_WRITE) {
402 try {
403 writeFolder(true, expunge);
404 } catch (IOException e) {
405 throw new MessagingException("I/O Exception", e);
406 }
407 }
408 messages = null;
409 } finally {
410 opened = false;
411 if (is_inbox && folder instanceof InboxFile) {
412 InboxFile inf = (InboxFile)folder;
413 inf.closeLock();
414 }
415 temp.close();
416 temp = null;
417 notifyConnectionListeners(ConnectionEvent.CLOSED);
418 }
419 }
420
421 /**
422 * Re-write the folder with the current contents of the messages.
423 * If closing is true, turn off the RECENT flag. If expunge is
424 * true, don't write out deleted messages (only used from close()
425 * when the message cache won't be accessed again).
426 *
427 * Return the number of messages written.
428 */
429 protected int writeFolder(boolean closing, boolean expunge)
430 throws IOException, MessagingException {
431
432 /*
433 * First, see if there have been any changes.
434 */
435 int modified = 0, deleted = 0, recent = 0;
436 for (int msgno = 1; msgno <= total; msgno++) {
437 MessageMetadata md = messages.get(messageIndexOf(msgno));
438 MboxMessage msg = md.message;
439 if (msg != null) {
440 Flags flags = msg.getFlags();
441 if (msg.isModified() || !msg.origFlags.equals(flags))
442 modified++;
443 if (flags.contains(Flags.Flag.DELETED))
444 deleted++;
445 if (flags.contains(Flags.Flag.RECENT))
446 recent++;
447 } else {
448 if (md.deleted)
449 deleted++;
450 if (md.recent)
451 recent++;
452 }
453 }
454 if ((!closing || recent == 0) && (!expunge || deleted == 0) &&
455 modified == 0)
456 return 0;
457
458 /*
459 * Have to save any new mail that's been appended to the
460 * folder since we last loaded it.
461 */
462 if (!folder.lock("rw"))
463 throw new MessagingException("Failed to lock folder: " + name);
464 int oldtotal = total; // XXX
465 Message[] msglist = null;
466 if (folder.length() != file_size)
467 msglist = load(file_size, !closing);
468 // don't use the folder's FD, need to re-open in order to trunc the file
469 OutputStream os =
470 new BufferedOutputStream(new FileOutputStream((File)folder));
471 int wr = 0;
472 boolean keep = true;
473 try {
474 if (special_imap_message)
475 appendStream(getMessageStream(0), os);
476 for (int msgno = 1; msgno <= total; msgno++) {
477 MessageMetadata md = messages.get(messageIndexOf(msgno));
478 MboxMessage msg = md.message;
479 if (msg != null) {
480 if (expunge && msg.isSet(Flags.Flag.DELETED))
481 continue; // skip it;
482 if (closing && msgno <= oldtotal &&
483 msg.isSet(Flags.Flag.RECENT))
484 msg.setFlag(Flags.Flag.RECENT, false);
485 writeMboxMessage(msg, os);
486 } else {
487 if (expunge && md.deleted)
488 continue; // skip it;
489 if (closing && msgno <= oldtotal && md.recent) {
490 // have to instantiate message so that we can
491 // clear the recent flag
492 msg = (MboxMessage)getMessage(msgno);
493 msg.setFlag(Flags.Flag.RECENT, false);
494 writeMboxMessage(msg, os);
495 } else {
496 appendStream(getMessageStream(msgno), os);
497 }
498 }
499 folder.touchlock();
500 wr++;
501 }
502 // If no messages in the mailbox, and we're closing,
503 // maybe we should remove the mailbox.
504 if (wr == 0 && closing) {
505 String skeep = ((MboxStore)store).getSession().
506 getProperty("mail.mbox.deleteEmpty");
507 if (skeep != null && skeep.equalsIgnoreCase("true"))
508 keep = false;
509 }
510 } catch (IOException e) {
511 throw e;
512 } catch (MessagingException e) {
513 throw e;
514 } catch (Exception e) {
515 e.printStackTrace();
516 throw new MessagingException("unexpected exception " + e);
517 } finally {
518 // close the folder, flushing out the data
519 try {
520 os.close();
521 file_size = saved_file_size = folder.length();
522 if (!keep) {
523 folder.delete();
524 file_size = 0;
525 }
526 } catch (IOException ex) {}
527
528 if (keep) {
529 // make sure the access time is greater than the mod time
530 // XXX - would be nice to have utime()
531 try {
532 Thread.sleep(1000); // sleep for a second
533 } catch (InterruptedException ex) {}
534 InputStream is = null;
535 try {
536 is = new FileInputStream((File)folder);
537 is.read(); // read a byte
538 } catch (IOException ex) {} // ignore errors
539 try {
540 if (is != null)
541 is.close();
542 is = null;
543 } catch (IOException ex) {} // ignore errors
544 }
545
546 folder.unlock();
547 if (msglist != null)
548 notifyMessageAddedListeners(msglist);
549 }
550 return wr;
551 }
552
553 /**
554 * Append the input stream to the output stream, closing the
555 * input stream when done.
556 */
557 private static final void appendStream(InputStream is, OutputStream os)
558 throws IOException {
559 try {
560 byte[] buf = new byte[64 * 1024];
561 int len;
562 while ((len = is.read(buf)) > 0)
563 os.write(buf, 0, len);
564 } finally {
565 is.close();
566 }
567 }
568
569 /**
570 * Write a MimeMessage to the specified OutputStream in a
571 * format suitable for a UNIX mailbox, i.e., including a correct
572 * Content-Length header and with the local platform's line
573 * terminating convention. <p>
574 *
575 * If the message is really a MboxMessage, use its writeToFile
576 * method, which has access to the UNIX From line. Otherwise, do
577 * all the work here, creating an appropriate UNIX From line.
578 */
579 public static void writeMboxMessage(MimeMessage msg, OutputStream os)
580 throws IOException, MessagingException {
581 try {
582 if (msg instanceof MboxMessage) {
583 ((MboxMessage)msg).writeToFile(os);
584 } else {
585 // XXX - modify the message to preserve the flags in headers
586 MboxMessage.setHeadersFromFlags(msg);
587 ContentLengthCounter cos = new ContentLengthCounter();
588 NewlineOutputStream nos = new NewlineOutputStream(cos);
589 msg.writeTo(nos);
590 nos.flush();
591 os = new NewlineOutputStream(os, true);
592 os = new ContentLengthUpdater(os, cos.getSize());
593 PrintStream pos = new PrintStream(os, false, "iso-8859-1");
594 pos.println(getUnixFrom(msg));
595 msg.writeTo(pos);
596 pos.flush();
597 }
598 } catch (MessagingException me) {
599 throw me;
600 } catch (IOException ioe) {
601 throw ioe;
602 }
603 }
604
605 /**
606 * Construct an appropriately formatted UNIX From line using
607 * the sender address and the date in the message.
608 */
609 protected static String getUnixFrom(MimeMessage msg) {
610 Address[] afrom;
611 String from;
612 Date ddate;
613 String date;
614 try {
615 if ((afrom = msg.getFrom()) == null ||
616 !(afrom[0] instanceof InternetAddress) ||
617 (from = ((InternetAddress)afrom[0]).getAddress()) == null)
618 from = "UNKNOWN";
619 if ((ddate = msg.getReceivedDate()) == null ||
620 (ddate = msg.getSentDate()) == null)
621 ddate = new Date();
622 } catch (MessagingException e) {
623 from = "UNKNOWN";
624 ddate = new Date();
625 }
626 date = ddate.toString();
627 // date is of the form "Sat Aug 12 02:30:00 PDT 1995"
628 // need to strip out the timezone
629 return "From " + from + " " +
630 date.substring(0, 20) + date.substring(24);
631 }
632
633 public synchronized int getMessageCount() throws MessagingException {
634 if (!opened)
635 return -1;
636
637 boolean locked = false;
638 Message[] msglist = null;
639 try {
640 if (folder.length() != file_size) {
641 if (!folder.lock("r"))
642 throw new MessagingException("Failed to lock folder: " +
643 name);
644 locked = true;
645 msglist = load(file_size, true);
646 }
647 } catch (IOException e) {
648 throw new MessagingException("I/O Exception", e);
649 } finally {
650 if (locked) {
651 folder.unlock();
652 if (msglist != null)
653 notifyMessageAddedListeners(msglist);
654 }
655 }
656 return total;
657 }
658
659 /**
660 * Get the specified message. Note that messages are numbered
661 * from 1.
662 */
663 public synchronized Message getMessage(int msgno)
664 throws MessagingException {
665 checkReadable();
666 checkRange(msgno);
667
668 MessageMetadata md = messages.get(messageIndexOf(msgno));
669 MboxMessage m = md.message;
670 if (m == null) {
671 InputStream is = getMessageStream(msgno);
672 try {
673 m = loadMessage(is, msgno, mode == READ_WRITE);
674 } catch (IOException ex) {
675 MessagingException mex =
676 new MessageRemovedException("mbox message gone", ex);
677 throw mex;
678 }
679 md.message = m;
680 }
681 return m;
682 }
683
684 private final int messageIndexOf(int msgno) {
685 return special_imap_message ? msgno : msgno - 1;
686 }
687
688 private InputStream getMessageStream(int msgno) {
689 int index = messageIndexOf(msgno);
690 MessageMetadata md = messages.get(index);
691 return temp.newStream(md.start, md.dataend);
692 }
693
694 public synchronized void appendMessages(Message[] msgs)
695 throws MessagingException {
696 if (!folder.lock("rw"))
697 throw new MessagingException("Failed to lock folder: " + name);
698
699 OutputStream os = null;
700 boolean err = false;
701 try {
702 os = new BufferedOutputStream(
703 new FileOutputStream(((File)folder).getPath(), true));
704 // XXX - should use getAbsolutePath()?
705 for (int i = 0; i < msgs.length; i++) {
706 if (msgs[i] instanceof MimeMessage) {
707 writeMboxMessage((MimeMessage)msgs[i], os);
708 } else {
709 err = true;
710 continue;
711 }
712 folder.touchlock();
713 }
714 } catch (IOException e) {
715 throw new MessagingException("I/O Exception", e);
716 } catch (MessagingException e) {
717 throw e;
718 } catch (Exception e) {
719 e.printStackTrace();
720 throw new MessagingException("unexpected exception " + e);
721 } finally {
722 if (os != null)
723 try {
724 os.close();
725 } catch (IOException e) {
726 // ignored
727 }
728 folder.unlock();
729 }
730 if (opened)
731 getMessageCount(); // loads new messages as a side effect
732 if (err)
733 throw new MessagingException("Can't append non-Mime message");
734 }
735
736 public synchronized Message[] expunge() throws MessagingException {
737 checkWritable();
738
739 /*
740 * First, write out the folder to make sure we have permission,
741 * disk space, etc.
742 */
743 int wr = total; // number of messages written out
744 try {
745 wr = writeFolder(false, true);
746 } catch (IOException e) {
747 throw new MessagingException("expunge failed", e);
748 }
749 if (wr == total) // wrote them all => nothing expunged
750 return new Message[0];
751
752 /*
753 * Now, actually get rid of the expunged messages.
754 */
755 int del = 0;
756 Message[] msglist = new Message[total - wr];
757 int msgno = 1;
758 while (msgno <= total) {
759 MessageMetadata md = messages.get(messageIndexOf(msgno));
760 MboxMessage msg = md.message;
761 if (msg != null) {
762 if (msg.isSet(Flags.Flag.DELETED)) {
763 msg.setExpunged(true);
764 msglist[del] = msg;
765 del++;
766 messages.remove(messageIndexOf(msgno));
767 total--;
768 } else {
769 msg.setMessageNumber(msgno); // update message number
770 msgno++;
771 }
772 } else {
773 if (md.deleted) {
774 // have to instantiate it for the notification
775 msg = (MboxMessage)getMessage(msgno);
776 msg.setExpunged(true);
777 msglist[del] = msg;
778 del++;
779 messages.remove(messageIndexOf(msgno));
780 total--;
781 } else {
782 msgno++;
783 }
784 }
785 }
786 if (del != msglist.length) // this is really an assert
787 throw new MessagingException("expunge delete count wrong");
788 notifyMessageRemovedListeners(true, msglist);
789 return msglist;
790 }
791
792 /*
793 * Load more messages from the folder starting at the specified offset.
794 */
795 private Message[] load(long offset, boolean notify)
796 throws MessagingException, IOException {
797 int oldtotal = total;
798 MessageLoader loader = new MessageLoader(temp);
799 int loaded = loader.load(folder.getFD(), offset, messages);
800 total += loaded;
801 file_size = folder.length();
802
803 if (offset == 0 && loaded > 0) {
804 /*
805 * If the first message is the special message that the
806 * IMAP server adds to the mailbox, remember that we've
807 * seen it so it won't be shown to the user.
808 */
809 MessageMetadata md = messages.get(0);
810 if (md.imap) {
811 special_imap_message = true;
812 total--;
813 }
814 }
815 if (notify) {
816 Message[] msglist = new Message[total - oldtotal];
817 for (int i = oldtotal, j = 0; i < total; i++, j++)
818 msglist[j] = getMessage(i + 1);
819 return msglist;
820 } else
821 return null;
822 }
823
824 /**
825 * Parse the input stream and return an appropriate message object.
826 * The InputStream must be a SharedInputStream.
827 */
828 private MboxMessage loadMessage(InputStream is, int msgno,
829 boolean writable) throws MessagingException, IOException {
830 LineInputStream in = new LineInputStream(is);
831
832 /*
833 * Read lines until a UNIX From line,
834 * skipping blank lines.
835 */
836 String line;
837 String unix_from = null;
838 while ((line = in.readLine()) != null) {
839 if (line.trim().length() == 0)
840 continue;
841 if (line.startsWith("From ")) {
842 /*
843 * A UNIX From line looks like:
844 * From address Day Mon DD HH:MM:SS YYYY
845 */
846 unix_from = line;
847 int i;
848 // find the space after the address, before the date
849 i = unix_from.indexOf(' ', 5);
850 if (i < 0)
851 continue; // not a valid UNIX From line
852 break;
853 }
854 throw new MessagingException("Garbage in mailbox: " + line);
855 }
856
857 if (unix_from == null)
858 throw new EOFException("end of mailbox");
859
860 /*
861 * Now load the RFC822 headers into an InternetHeaders object.
862 */
863 InternetHeaders hdrs = new InternetHeaders(is);
864
865 // the rest is the message content
866 SharedInputStream sis = (SharedInputStream)is;
867 InputStream stream = sis.newStream(sis.getPosition(), -1);
868 return new MboxMessage(this, hdrs, stream, msgno, unix_from, writable);
869 }
870
871 /*
872 * Only here to make accessible to MboxMessage.
873 */
874 protected void notifyMessageChangedListeners(int type, Message m) {
875 super.notifyMessageChangedListeners(type, m);
876 }
877
878
879 /**
880 * this is an exact duplicate of the Folder.getURL except it doesn't
881 * add a beginning '/' to the URLName.
882 */
883 public URLName getURLName() {
884 // XXX - note: this should not be done this way with the
885 // new javax.mail apis.
886
887 URLName storeURL = getStore().getURLName();
888 if (name == null)
889 return storeURL;
890
891 char separator = getSeparator();
892 String fullname = getFullName();
893 StringBuilder encodedName = new StringBuilder();
894
895 // We need to encode each of the folder's names, and replace
896 // the store's separator char with the URL char '/'.
897 StringTokenizer tok = new StringTokenizer(
898 fullname, Character.toString(separator), true);
899
900 while (tok.hasMoreTokens()) {
901 String s = tok.nextToken();
902 if (s.charAt(0) == separator)
903 encodedName.append("/");
904 else
905 // XXX - should encode, but since there's no decoder...
906 //encodedName.append(java.net.URLEncoder.encode(s));
907 encodedName.append(s);
908 }
909
910 return new URLName(storeURL.getProtocol(), storeURL.getHost(),
911 storeURL.getPort(), encodedName.toString(),
912 storeURL.getUsername(),
913 null /* no password */);
914 }
915
916 /**
917 * Create an MboxFolder object, or a subclass thereof.
918 * Can be overridden by subclasses of MboxFolder so that
919 * the appropriate subclass is created by the list method.
920 */
921 protected Folder createFolder(MboxStore store, String name) {
922 return new MboxFolder(store, name);
923 }
924
925 /*
926 * Support routines for list().
927 */
928
929 /**
930 * Return a canonicalized pattern given a reference name and a pattern.
931 */
932 private static String canonicalize(String ref, String pat) {
933 if (ref == null)
934 return pat;
935 try {
936 if (pat.length() == 0) {
937 return ref;
938 } else if (pat.charAt(0) == File.separatorChar) {
939 return ref.substring(0, ref.indexOf(File.separatorChar)) + pat;
940 } else {
941 return ref + pat;
942 }
943 } catch (StringIndexOutOfBoundsException e) {
944 return pat;
945 }
946 }
947
948 /**
949 * Return the first index of any of the characters in "any" in "s",
950 * or -1 if none are found.
951 *
952 * This should be a method on String.
953 */
954 private static int indexOfAny(String s, String any) {
955 try {
956 int len = s.length();
957 for (int i = 0; i < len; i++) {
958 if (any.indexOf(s.charAt(i)) >= 0)
959 return i;
960 }
961 return -1;
962 } catch (StringIndexOutOfBoundsException e) {
963 return -1;
964 }
965 }
966
967 /**
968 * The recursive part of generating the list of mailboxes.
969 * realdir is the full pathname to the directory to search.
970 * dir is the name the user uses, often a relative name that's
971 * relative to the user's home directory. dir (if not null) always
972 * has a trailing file separator character.
973 *
974 * @param realdir real pathname of directory to start looking in
975 * @param dir user's name for realdir
976 * @param pat pattern to match against
977 * @param level level of the directory hierarchy we're in
978 * @param flist list to which to add folder names that match
979 */
980 // Derived from the c-client listWork() function.
981 private void listWork(String realdir, String dir, String pat,
982 int level, List<String> flist) {
983 String sl[];
984 File fdir = new File(realdir);
985 try {
986 sl = fdir.list();
987 } catch (SecurityException e) {
988 return; // can't read it, ignore it
989 }
990
991 if (level == 0 && dir != null &&
992 Match.path(dir, pat, File.separatorChar))
993 flist.add(dir);
994
995 if (sl == null)
996 return; // nothing return, we're done
997
998 if (realdir.charAt(realdir.length() - 1) != File.separatorChar)
999 realdir += File.separator;
1000
1001 for (int i = 0; i < sl.length; i++) {
1002 if (sl[i].charAt(0) == '.')
1003 continue; // ignore all "dot" files for now
1004 String md = realdir + sl[i];
1005 File mf = new File(md);
1006 if (!mf.exists())
1007 continue;
1008 String name;
1009 if (dir != null)
1010 name = dir + sl[i];
1011 else
1012 name = sl[i];
1013 if (mf.isDirectory()) {
1014 if (Match.path(name, pat, File.separatorChar)) {
1015 flist.add(name);
1016 name += File.separator;
1017 } else {
1018 name += File.separator;
1019 if (Match.path(name, pat, File.separatorChar))
1020 flist.add(name);
1021 }
1022 if (Match.dir(name, pat, File.separatorChar))
1023 listWork(md, name, pat, level + 1, flist);
1024 } else {
1025 if (Match.path(name, pat, File.separatorChar))
1026 flist.add(name);
1027 }
1028 }
1029 }
1030 }
1031
1032 /**
1033 * Pattern matching support class for list().
1034 * Should probably be more public.
1035 */
1036 // Translated from the c-client functions pmatch_full() and dmatch().
1037 class Match {
1038 /**
1039 * Pathname pattern match
1040 *
1041 * @param s base string
1042 * @param pat pattern string
1043 * @param delim delimiter character
1044 * @return true if base matches pattern
1045 */
1046 static public boolean path(String s, String pat, char delim) {
1047 try {
1048 return path(s, 0, s.length(), pat, 0, pat.length(), delim);
1049 } catch (StringIndexOutOfBoundsException e) {
1050 return false;
1051 }
1052 }
1053
1054 static private boolean path(String s, int s_index, int s_len,
1055 String pat, int p_index, int p_len, char delim)
1056 throws StringIndexOutOfBoundsException {
1057
1058 while (p_index < p_len) {
1059 char c = pat.charAt(p_index);
1060 switch (c) {
1061 case '%':
1062 if (++p_index >= p_len) // % at end of pattern
1063 // ok if no delimiters
1064 return delim == 0 || s.indexOf(delim, s_index) < 0;
1065 // scan remainder until delimiter
1066 do {
1067 if (path(s, s_index, s_len, pat, p_index, p_len, delim))
1068 return true;
1069 } while (s.charAt(s_index) != delim && ++s_index < s_len);
1070 // ran into a delimiter or ran out of string without a match
1071 return false;
1072
1073 case '*':
1074 if (++p_index >= p_len) // end of pattern?
1075 return true; // unconditional match
1076 do {
1077 if (path(s, s_index, s_len, pat, p_index, p_len, delim))
1078 return true;
1079 } while (++s_index < s_len);
1080 // ran out of string without a match
1081 return false;
1082
1083 default:
1084 // if ran out of string or no match, fail
1085 if (s_index >= s_len || c != s.charAt(s_index))
1086 return false;
1087
1088 // try the next string and pattern characters
1089 s_index++;
1090 p_index++;
1091 }
1092 }
1093 return s_index >= s_len;
1094 }
1095
1096 /**
1097 * Directory pattern match
1098 *
1099 * @param s base string
1100 * @param pat pattern string
1101 * @return true if base is a matching directory of pattern
1102 */
1103 static public boolean dir(String s, String pat, char delim) {
1104 try {
1105 return dir(s, 0, s.length(), pat, 0, pat.length(), delim);
1106 } catch (StringIndexOutOfBoundsException e) {
1107 return false;
1108 }
1109 }
1110
1111 static private boolean dir(String s, int s_index, int s_len,
1112 String pat, int p_index, int p_len, char delim)
1113 throws StringIndexOutOfBoundsException {
1114
1115 while (p_index < p_len) {
1116 char c = pat.charAt(p_index);
1117 switch (c) {
1118 case '%':
1119 if (s_index >= s_len) // end of base?
1120 return true; // subset match
1121 if (++p_index >= p_len) // % at end of pattern?
1122 return false; // no inferiors permitted
1123 do {
1124 if (dir(s, s_index, s_len, pat, p_index, p_len, delim))
1125 return true;
1126 } while (s.charAt(s_index) != delim && ++s_index < s_len);
1127
1128 if (s_index + 1 == s_len) // s ends with a delimiter
1129 return true; // must be a subset of pattern
1130 return dir(s, s_index, s_len, pat, p_index, p_len, delim);
1131
1132 case '*':
1133 return true; // unconditional match
1134
1135 default:
1136 if (s_index >= s_len) // end of base?
1137 return c == delim; // matched if at delimiter
1138
1139 if (c != s.charAt(s_index))
1140 return false;
1141
1142 // try the next string and pattern characters
1143 s_index++;
1144 p_index++;
1145 }
1146 }
1147 return s_index >= s_len;
1148 }
1149 }
1150
1151 /**
1152 * A ByteArrayOutputStream that allows us to share the byte array
1153 * rather than copy it. Eventually could replace this with something
1154 * that doesn't require a single contiguous byte array.
1155 */
1156 class SharedByteArrayOutputStream extends ByteArrayOutputStream {
1157 public SharedByteArrayOutputStream(int size) {
1158 super(size);
1159 }
1160
1161 public InputStream toStream() {
1162 return new SharedByteArrayInputStream(buf, 0, count);
1163 }
1164 }