root/trunk/LogicMail/src/org/logicprobe/LogicMail/model/MessageNode.java

Revision 701, 41.7 KB (checked in by octorian, 36 hours ago)

Refactoring to separate AccountNode into Local and Network subclasses

Line 
1/*-
2 * Copyright (c) 2008, Derek Konigsberg
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
8 *
9 * 1. Redistributions of source code must retain the above copyright
10 *    notice, this list of conditions and the following disclaimer.
11 * 2. Redistributions in binary form must reproduce the above copyright
12 *    notice, this list of conditions and the following disclaimer in the
13 *    documentation and/or other materials provided with the distribution.
14 * 3. Neither the name of the project nor the names of its
15 *    contributors may be used to endorse or promote products derived
16 *    from this software without specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
21 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
22 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
23 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
24 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
26 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
27 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
29 * OF THE POSSIBILITY OF SUCH DAMAGE.
30 */
31package org.logicprobe.LogicMail.model;
32
33import java.io.IOException;
34import java.util.Calendar;
35import java.util.Date;
36import java.util.Enumeration;
37import java.util.Hashtable;
38import java.util.Vector;
39
40import net.rim.device.api.system.EventLogger;
41import net.rim.device.api.util.Arrays;
42import net.rim.device.api.util.Comparator;
43
44import org.logicprobe.LogicMail.AppInfo;
45import org.logicprobe.LogicMail.conf.AccountConfig;
46import org.logicprobe.LogicMail.conf.ImapConfig;
47import org.logicprobe.LogicMail.mail.AbstractMailStore;
48import org.logicprobe.LogicMail.mail.MessageToken;
49import org.logicprobe.LogicMail.message.AbstractMimeMessagePartVisitor;
50import org.logicprobe.LogicMail.message.ContentPart;
51import org.logicprobe.LogicMail.message.FolderMessage;
52import org.logicprobe.LogicMail.message.Message;
53import org.logicprobe.LogicMail.message.MimeMessageContent;
54import org.logicprobe.LogicMail.message.MessageEnvelope;
55import org.logicprobe.LogicMail.message.MessageFlags;
56import org.logicprobe.LogicMail.message.MessageMimeConverter;
57import org.logicprobe.LogicMail.message.MimeMessagePart;
58import org.logicprobe.LogicMail.message.MimeMessagePartTransformer;
59import org.logicprobe.LogicMail.message.TextContent;
60import org.logicprobe.LogicMail.message.TextPart;
61import org.logicprobe.LogicMail.util.EventListenerList;
62import org.logicprobe.LogicMail.util.StringParser;
63import org.logicprobe.LogicMail.util.ThreadQueue;
64
65/**
66 * Message node for the mail data model.
67 * This node represents a mail message, and does
68 * not contain any other nodes as children.
69 */
70public class MessageNode implements Node {
71        /**
72         * Defines the flags supported by the {@link MessageNode} class.
73         */
74        public static interface Flag {
75                public static final int SEEN      = 1;
76                public static final int ANSWERED  = 2;
77                public static final int FLAGGED   = 4;
78                public static final int DELETED   = 8;
79                public static final int DRAFT     = 16;
80                public static final int RECENT    = 32;
81                public static final int JUNK      = 64;
82        public static final int FORWARDED = 128;
83        }
84       
85        private static class MessageNodeComparator implements Comparator {
86                public int compare(Object o1, Object o2) {
87                        if(o1 instanceof MessageNode && o2 instanceof MessageNode) {
88                                MessageNode message1 = (MessageNode)o1;
89                                MessageNode message2 = (MessageNode)o2;
90                                int result;
91                                //TODO: Add comparator for MessageToken objects
92                                /*
93                                if(message1.messageTag instanceof FolderMessage && message2.messageTag instanceof FolderMessage) {
94                                        // First try folder index comparison
95                                        int index1 = ((FolderMessage)message1.messageTag).getIndex();
96                                        int index2 = ((FolderMessage)message2.messageTag).getIndex();
97                                        if(index1 < index2) { result = -1; }
98                                        else if(index1 > index2) { result = 1; }
99                                        else { result = 0; }
100                                }
101                                */
102                                if(message1.date != null && message2.date != null) {
103                                        // Then try date comparison
104                                        long time1 = message1.date.getTime();
105                                        long time2 = message2.date.getTime();
106                                        if(time1 < time2) { result = -1; }
107                                        else if(time1 > time2) { result = 1; }
108                                        else { result = 0; }
109                                }
110                                else {
111                                        // Worst case, return equal
112                                        result = 0;
113                                }
114                                return result;
115                        }
116                        else {
117                                throw new ClassCastException("Cannot compare types");
118                        }
119                }
120        }
121       
122        private static String strCRLF = "\r\n";
123       
124        /** Static comparator used to compare message nodes for insertion ordering */
125        private static MessageNodeComparator comparator = new MessageNodeComparator();
126
127        /** The token object used to identify the message to the protocol layer */
128        private MessageToken messageToken;
129        /** Hash code used to verify uniqueness of message nodes */
130        private int hashCode = -1;
131        /** True if the message is up-to-date in the cache */
132        private boolean cached;
133        /** True if the message has cached content available for loading */
134        private boolean hasCachedContent;
135        /** True if the message has been verified to exist on a mail server */
136        private boolean existsOnServer;
137        /** Bit-field set of message flags. */
138        private int flags;
139        /** Date that the message was sent. */
140        private Date date;
141        /** Message subject. */
142        private String subject;
143        /** Addresses the message is from. */
144        private Address[] from;
145        /** Addresses of the message sender. */
146        private Address[] sender;
147        /** Reply-To addresses the message is from. */
148        private Address[] replyTo;
149        /** "To" recipients for the message. */
150        private Address[] to;
151        /** "CC" recipients for the message. */
152        private Address[] cc;
153        /** "BCC" recipients for the message. */
154        private Address[] bcc;
155        /** Message ID string of the message this may be a reply to. */
156        private String inReplyTo;
157        /** Message ID string from the message headers. */
158        private String messageId;
159       
160        private MailboxNode parent;
161        private MimeMessagePart messageStructure;
162        private Hashtable messageContent = new Hashtable();
163        private MimeMessagePart[] attachmentParts;
164        private String messageSource;
165        private EventListenerList listenerList = new EventListenerList();
166        private boolean refreshInProgress;
167
168        /** Thread queue for calls to MailFileManager. */
169        private static final ThreadQueue threadQueue = new ThreadQueue();
170       
171        /**
172         * Instantiates a new message node.
173         *
174         * @param folderMessage the folder message
175         */
176        MessageNode(FolderMessage folderMessage) {
177                // Populate fields corresponding to FolderMessage members
178                this.messageToken = folderMessage.getMessageToken();
179                this.flags = convertMessageFlags(folderMessage.getFlags());
180               
181                // Populate fields corresponding to MessageEnvelope members
182                MessageEnvelope envelope = folderMessage.getEnvelope();
183                this.date = envelope.date;
184                this.subject = envelope.subject;
185                this.from = createAddressArray(envelope.from);
186                this.sender = createAddressArray(envelope.sender);
187                this.replyTo = createAddressArray(envelope.replyTo);
188                this.to = createAddressArray(envelope.to);
189                this.cc = createAddressArray(envelope.cc);
190                this.bcc = createAddressArray(envelope.bcc);
191                this.inReplyTo = envelope.inReplyTo;
192                this.messageId = envelope.messageId;
193                this.messageStructure = folderMessage.getStructure();
194                if(this.messageStructure != null) {
195                        this.attachmentParts = MimeMessagePartTransformer.getAttachmentParts(this.messageStructure);
196                }
197        }
198       
199        /**
200         * Instantiates a new empty message node.
201         *
202         * @param messageToken the message token
203         */
204        MessageNode(MessageToken messageToken) {
205                this.messageToken = messageToken;
206        }
207
208        /**
209         * Instantiates a new message node.
210         * This constructor is only intended for internal use.
211         */
212        private MessageNode() {
213        }
214       
215        private static Address[] createAddressArray(String[] recipients) {
216                Address[] result;
217                if(recipients != null && recipients.length > 0) {
218                        result = new Address[recipients.length];
219                        for(int i=0; i<recipients.length; i++) {
220                                result[i] = new Address(recipients[i]);
221                        }
222                }
223                else {
224                        result = null;
225                }
226                return result;
227        }
228       
229        /**
230         * Gets the comparator used to compare message nodes for insertion ordering.
231         *
232         * @return the comparator
233         */
234        public static Comparator getComparator() {
235                return MessageNode.comparator;
236        }
237       
238        /* (non-Javadoc)
239         * @see java.lang.Object#equals(java.lang.Object)
240         */
241        public boolean equals(Object obj) {
242                return MessageNode.comparator.compare(this, obj) == 0;
243        }
244       
245        /* (non-Javadoc)
246         * @see java.lang.Object#hashCode()
247         */
248        public int hashCode() {
249                if(hashCode == -1) {
250                        if(messageToken != null) {
251                                hashCode = 31 * 7 + messageToken.hashCode();
252                        }
253                        else if(messageId != null) {
254                                hashCode = messageId.hashCode();
255                        }
256                        else {
257                                hashCode = super.hashCode();
258                        }
259                }
260                return hashCode;
261        }
262       
263        /**
264         * Checks if the message is up-to-date in the cache.
265         *
266         * @return true, if the message is cached
267         */
268        boolean isCached() {
269                return this.cached;
270        }
271       
272        /**
273         * Checks if the message is capable of being cached.
274         *
275         * @return true, if the message is associated with a mailbox from a non-local account
276         */
277        public boolean isCachable() {
278                if(parent != null
279                                && parent.getParentAccount() != null
280                                && parent.getParentAccount() instanceof NetworkAccountNode) {
281                        return true;
282                }
283                else {
284                        return false;
285                }
286        }
287       
288        /**
289         * Sets whether the message is up-to-date in the cache.
290         *
291         * @param cached the new cached state
292         */
293        void setCached(boolean cached) {
294                this.cached = cached;
295        }
296       
297        /**
298         * Sets whether this message has been verified to exist on a server.
299         *
300         * @param existsOnServer true if the message has been verified to exist on a server
301         */
302        void setExistsOnServer(boolean existsOnServer) {
303            this.existsOnServer = existsOnServer;
304        }
305       
306        /**
307         * Gets whether this message has been verified to exist on a server.
308         *
309         * @return true if the message has been verified to exist on a server or is on a local account
310         */
311        public boolean existsOnServer() {
312            return existsOnServer;
313        }
314       
315        /* (non-Javadoc)
316         * @see org.logicprobe.LogicMail.model.Node#accept(org.logicprobe.LogicMail.model.NodeVisitor)
317         */
318        public void accept(NodeVisitor visitor) {
319                visitor.visit(this);
320        }
321
322        /**
323         * Sets the mailbox which is the parent of this node.
324         *
325         * @param parent The parent mailbox.
326         */
327        void setParent(MailboxNode parent) {
328                this.parent = parent;
329                if(!isCachable()) { existsOnServer = true; }
330        }
331       
332        /**
333         * Gets the mailbox which is the parent of this node.
334         *
335         * @return The mailbox.
336         */
337        public MailboxNode getParent() {
338                return this.parent;
339        }
340       
341        /**
342         * Gets the token object used to identify the message to the protocol layer.
343         *
344         * @return the message token.
345         */
346        public MessageToken getMessageToken() {
347                return this.messageToken;
348        }
349       
350        /**
351         * Sets the token object used to identify the message to the protocol later.
352         *
353         * @param messageToken the message token.
354         */
355        protected void setMessageToken(MessageToken messageToken) {
356                cached = false;
357                this.messageToken = messageToken;
358        }
359       
360        /**
361         * Gets the bit-field set of message flags, as specified by {@link Flag}.
362         *
363         * @return the flags
364         */
365        public int getFlags() {
366                return flags;
367        }
368
369        /**
370         * Sets the bit-field set of message flags, as specified by {@link Flag}.
371         *
372         * @param flags the flags to set
373         */
374        public void setFlags(int flags) {
375                cached = false;
376                if(this.flags != flags) {
377                    this.flags = flags;
378                    if(this.getParent() != null) {
379                        this.getParent().updateUnseenMessages(true);
380                    }
381                }
382        }
383
384        /**
385         * Gets the date that the message was sent.
386         *
387         * @return the date
388         */
389        public Date getDate() {
390                return date;
391        }
392
393        /**
394         * Sets the date that the message was sent.
395         *
396         * @param date the date to set
397         */
398        public void setDate(Date date) {
399                cached = false;
400                this.date = date;
401        }
402
403        /**
404         * Gets the message subject.
405         *
406         * @return the subject
407         */
408        public String getSubject() {
409                return subject;
410        }
411
412        /**
413         * Sets the message subject.
414         *
415         * @param subject the subject to set
416         */
417        public void setSubject(String subject) {
418                cached = false;
419                this.subject = subject;
420        }
421
422        /**
423         * Gets the address the message is from.
424         *
425         * @return the from address
426         */
427        public Address[] getFrom() {
428                return from;
429        }
430
431        /**
432         * Sets the address the message is from.
433         *
434         * @param from the from address to set
435         */
436        public void setFrom(Address[] from) {
437                cached = false;
438                this.from = from;
439        }
440
441        /**
442         * Gets the address of the sender.
443         *
444         * @return the sender address
445         */
446        public Address[] getSender() {
447                return sender;
448        }
449
450        /**
451         * Sets the address of the sender.
452         *
453         * @param sender the sender address to set
454         */
455        public void setSender(Address[] sender) {
456                cached = false;
457                this.sender = sender;
458        }
459
460        /**
461         * Gets the Reply-To address of the message.
462         *
463         * @return the Reply-To address
464         */
465        public Address[] getReplyTo() {
466                return replyTo;
467        }
468
469        /**
470         * Sets the Reply-To address of the message.
471         *
472         * @param replyTo the Reply-To address to set
473         */
474        public void setReplyTo(Address[] replyTo) {
475                cached = false;
476                this.replyTo = replyTo;
477        }
478
479        /**
480         * Gets the "To" recipients of the message.
481         *
482         * @return the "To" recipients
483         */
484        public Address[] getTo() {
485                return to;
486        }
487
488        /**
489         * Sets the "To" recipients of the message.
490         *
491         * @param the "To" recipients to set
492         */
493        public void setTo(Address[] to) {
494                cached = false;
495                this.to = to;
496        }
497
498        /**
499         * Gets the "CC" recipients of the message.
500         *
501         * @return the "CC" recipients
502         */
503        public Address[] getCc() {
504                return cc;
505        }
506
507        /**
508         * Sets the "CC" recipients of the message.
509         *
510         * @param cc the "CC" recipients to set
511         */
512        public void setCc(Address[] cc) {
513                cached = false;
514                this.cc = cc;
515        }
516
517        /**
518         * Gets the "BCC" recipients of the message.
519         *
520         * @return the "BCC" recipients
521         */
522        public Address[] getBcc() {
523                return bcc;
524        }
525
526        /**
527         * Sets the "BCC" recipients of the message.
528         *
529         * @param bcc the "BCC" recipients to set
530         */
531        public void setBcc(Address[] bcc) {
532                cached = false;
533                this.bcc = bcc;
534        }
535
536        /**
537         * Gets the message ID string of the message this may be a reply to.
538         *
539         * @return the "In-Reply-To" message ID string
540         */
541        public String getInReplyTo() {
542                return inReplyTo;
543        }
544
545        /**
546         * Sets the message ID string of the message this may be a reply to.
547         *
548         * @param inReplyTo the "In-Reply-To" message ID string to set
549         */
550        public void setInReplyTo(String inReplyTo) {
551                cached = false;
552                this.inReplyTo = inReplyTo;
553        }
554
555        /**
556         * Gets the message ID string from the message headers.
557         *
558         * @return the "Message-Id" message ID string
559         */
560        public String getMessageId() {
561                return messageId;
562        }
563
564        /**
565         * Sets the message ID string from the message headers.
566         *
567         * @param messageId the "Message-Id" message ID string to set
568         */
569        public void setMessageId(String messageId) {
570                cached = false;
571                this.messageId = messageId;
572        }
573
574        /**
575         * Sets the message structure for this node.
576         *
577         * @param message The message structure.
578         */
579        void setMessageStructure(MimeMessagePart messageStructure) {
580                boolean fireEvent;
581                synchronized(messageContent) {
582                        cached = false;
583                        this.messageStructure = messageStructure;
584                        if(this.messageStructure != null) {
585                                refreshInProgress = false;
586                                this.flags &= ~Flag.RECENT; // RECENT = false
587                                this.attachmentParts = MimeMessagePartTransformer.getAttachmentParts(this.messageStructure);
588                                fireEvent = true;
589                        }
590                        else {
591                                fireEvent = false;
592                        }
593                }
594                if(fireEvent) {
595                        fireMessageStatusChanged(MessageNodeEvent.TYPE_STRUCTURE_LOADED);
596                }
597        }
598
599        /**
600         * Adds content to this message node.
601         *
602         * @param mimeMessageContent The content to add.
603         */
604        void putMessageContent(MimeMessageContent mimeMessageContent) {
605                synchronized(mimeMessageContent) {
606                        cached = false;
607                        this.messageContent.put(mimeMessageContent.getMessagePart(), mimeMessageContent);
608                }
609                fireMessageStatusChanged(MessageNodeEvent.TYPE_CONTENT_LOADED);
610        }
611
612        /**
613         * Adds content to this message node.
614         * <p>
615         * This method provides for a batch addition of content, causing a
616         * single event to be fired afterwards.
617         * </p>
618         *
619         * @param messageContent The content sections to add.
620         */
621        void putMessageContent(MimeMessageContent[] messageContent) {
622                synchronized(messageContent) {
623                        cached = false;
624                        for(int i=0; i<messageContent.length; i++) {
625                                this.messageContent.put(messageContent[i].getMessagePart(), messageContent[i]);
626                        }
627                }
628                fireMessageStatusChanged(MessageNodeEvent.TYPE_CONTENT_LOADED);
629        }
630       
631        /**
632         * Commits the current state of the message to the cache, if applicable.
633         */
634        void commitMessage() {
635            boolean writeToCache;
636                synchronized(messageContent) {
637                    writeToCache = !isCached() && isCachable();
638                }
639                if(!writeToCache) { return; }
640               
641            synchronized (threadQueue) {
642                threadQueue.invokeLater(new Runnable() {
643                    public void run() {
644                        synchronized(messageContent) {
645                            try {
646                                MailFileManager.getInstance().writeMessage(MessageNode.this);
647                                setCached(true);
648                            } catch (Throwable t) {
649                                EventLogger.logEvent(AppInfo.GUID,
650                                        ("Unable to write message to cache\r\n"
651                                                + t.getMessage()).getBytes(),
652                                                EventLogger.ERROR);
653                            }
654                        }
655                    }
656                });
657            }
658        }
659       
660        /**
661         * Gets the message structure for this node.
662         * The message structure will be null unless it has been explicitly loaded.
663         *
664         * @return The message structure.
665         */
666        public MimeMessagePart getMessageStructure() {
667        return this.messageStructure;
668        }
669
670        /**
671         * Gets message content.
672         *
673         * @param mimeMessagePart The part that represents the content's structural placement.
674         * @return The content.
675         */
676        public MimeMessageContent getMessageContent(MimeMessagePart mimeMessagePart) {
677                synchronized(messageContent) {
678                        return (MimeMessageContent)messageContent.get(mimeMessagePart);
679                }
680    }
681       
682        /**
683         * Gets whether this message has any content available.
684         * This is intended to be used as a quick check to determine
685         * whether message content needs to be loaded.
686         *
687         * @return True if content is available, false otherwise
688         */
689        public boolean hasMessageContent() {
690                synchronized(messageContent) {
691                        return (messageStructure != null) && (!messageContent.isEmpty());
692                }
693        }
694       
695        /**
696         * Sets whether this message has cached content available.
697         * This method should only be called by code which is creating
698         * a message note instance from local cache data.
699         *
700         * @param hasCachedContent True if cached content is available for loading
701         */
702        public void setCachedContent(boolean hasCachedContent) {
703            this.hasCachedContent = hasCachedContent;
704        }
705       
706        /**
707         * Gets whether this message has cached content available.
708         * This is intended to be used as a quick check to determine whether
709         * message content can be loaded from local cache, without actually
710         * having to load that content first.  It checks a variable that should
711         * be set when message headers are loaded from cache.
712         *
713         * @return True if cached content is available for loading
714         */
715        public boolean hasCachedContent() {
716            return hasCachedContent;
717        }
718       
719        /**
720         * Gets all message content.
721         *
722         * @return All the content.
723         */
724        public MimeMessageContent[] getAllMessageContent() {
725                synchronized(messageContent) {
726                        MimeMessageContent[] result = new MimeMessageContent[messageContent.size()];
727                        Enumeration e = messageContent.keys();
728                        int i = 0;
729                while(e.hasMoreElements()) {
730                        result[i++] = (MimeMessageContent)messageContent.get(e.nextElement());
731                }
732                        return result;
733                }
734        }
735       
736        /**
737         * Gets the message parts that are considered to be message attachments.
738         * <p>
739         * This is a convenience method, as it returns an array that is populated
740         * from the message structure when {@link #setMessageStructure(MimeMessagePart)}
741         * is called.  This array will contain all message parts that are not of
742         * type multi, text, or unsupported.
743         * </p>
744         *
745         * @return Message attachments.
746         */
747        public MimeMessagePart[] getAttachmentParts() {
748                synchronized(messageContent) {
749                        return this.attachmentParts;
750                }
751        }
752       
753        /**
754         * Sets the raw message source.
755         *
756         * @param messageSource The raw message source
757         */
758        void setMessageSource(String messageSource) {
759                this.messageSource = messageSource;
760        }
761       
762        /**
763         * Gets the raw message source.
764         *
765         * @return The raw message source
766         */
767        public String getMessageSource() {
768                return this.messageSource;
769        }
770       
771        /**
772         * Gets the name of this message, which should
773         * be set to the subject text.
774         *
775         * @return The name.
776         */
777        public String toString() {
778                return this.subject;
779        }
780
781        /**
782         * Converts the contents of the MessageNode into a standard
783         * MIME formatted message, headers included.  Should be similar
784         * to the results of {@link MessageNode#getMessageSource()}, however
785         * the contents are generated dynamically and not are from the
786         * mail server.
787         *
788         * @param includeUserAgent True to include the User-Agent line.
789         * @return MIME-formatted message
790         */
791        public String toMimeMessage(boolean includeUserAgent) {
792                StringBuffer buffer = new StringBuffer();
793
794                // Generate the headers
795        buffer.append(makeRecipientHeader("From:", from));
796        buffer.append(strCRLF);
797
798        buffer.append(makeRecipientHeader("To:", to));
799        buffer.append(strCRLF);
800
801        if ((cc != null) && (cc.length > 0)) {
802            buffer.append(makeRecipientHeader("Cc:", cc));
803            buffer.append(strCRLF);
804        }
805
806        if ((replyTo != null) && (replyTo.length > 0)) {
807            buffer.append(makeRecipientHeader("Reply-To:", replyTo));
808            buffer.append(strCRLF);
809        }
810
811        buffer.append("Date: ");
812        if(date != null) {
813            buffer.append(StringParser.createDateString(date));
814        }
815        else {
816            buffer.append(StringParser.createDateString(Calendar.getInstance().getTime()));
817        }
818        buffer.append(strCRLF);
819
820        if (includeUserAgent) {
821            buffer.append("User-Agent: ");
822            buffer.append(AppInfo.getName());
823            buffer.append('/');
824            buffer.append(AppInfo.getVersion());
825            buffer.append(strCRLF);
826        }
827
828        buffer.append(StringParser.createEncodedHeader("Subject:", subject));
829        buffer.append(strCRLF);
830
831        if (inReplyTo != null) {
832            buffer.append("In-Reply-To: ");
833            buffer.append(inReplyTo);
834            buffer.append(strCRLF);
835        }
836               
837        synchronized(messageContent) {
838                        // Generate the body
839                Message message = new Message(messageStructure);
840                Enumeration en = messageContent.keys();
841                while(en.hasMoreElements()) {
842                        MimeMessagePart part = (MimeMessagePart)en.nextElement();
843                        message.putContent(part, (MimeMessageContent)messageContent.get(part));
844                }
845               
846                MessageMimeConverter messageMime = new MessageMimeConverter(message);
847                buffer.append(messageMime.toMimeString());
848       
849                        // Return the result
850                        return buffer.toString();
851        }
852        }
853       
854        private static String makeRecipientHeader(String key, Address[] recipients) {
855            String[] recipientsStr = new String[recipients.length];
856            for(int i=0; i<recipients.length; i++) {
857                recipientsStr[i] = recipients[i].toString();
858            }
859            return StringParser.createEncodedRecipientHeader(key, recipientsStr);
860        }
861       
862        //TODO: Weed out duplicates from reply headers
863       
864    /**
865     * Get a message that represents a reply to the original message.
866     * @return Reply message
867     */
868        public MessageNode toReplyMessage() {
869        // Generate the reply message body
870                String senderName;
871                if(sender != null && sender.length > 0) {
872                        senderName = sender[0].getName();
873                        if(senderName == null || senderName.length() == 0) {
874                                senderName = sender[0].getAddress();
875                        }
876                }
877                else {
878                        senderName = "";
879                }
880               
881                synchronized(messageContent) {
882                FindFirstTextPartVisitor findVisitor = new FindFirstTextPartVisitor();
883                if(this.messageStructure != null) {
884                        this.messageStructure.accept(findVisitor);
885                }
886                TextPart originalTextPart = findVisitor.getFirstTextPart();
887                TextContent originalTextContent = (TextContent)messageContent.get(originalTextPart);
888                       
889                StringBuffer buf = new StringBuffer();
890               
891                // Create the first line of the reply text
892                buf.append("\r\n");
893                buf.append("On ");
894                buf.append(StringParser.createDateString(date));
895                buf.append(", ");
896                buf.append(senderName);
897                buf.append(" wrote:\r\n");
898               
899                // Generate the quoted message text
900                buf.append("> ");
901                if(originalTextContent != null) {
902                    String originalText = originalTextContent.getText();
903                    int size = originalText.length();
904                    char ch;
905                    for(int i=0; i<size; i++) {
906                        ch = originalText.charAt(i);
907                        buf.append(ch);
908                        if(ch == '\n' && i < size - 1) {
909                            buf.append("> ");
910                        }
911                    }
912                }
913               
914                MessageNode replyNode = new MessageNode();
915                String contentText = buf.toString();
916                TextPart replyPart = new TextPart("plain", "", "", "", "", "", contentText.length());
917                replyNode.messageStructure = replyPart;
918                replyNode.putMessageContent(new TextContent(replyPart, contentText));
919               
920                populateReplyEnvelope(replyNode);
921               
922                        return replyNode;
923                }
924        }
925
926        /**
927     * Get a message that represents a reply to all the recipients
928     * of the original message.
929     * @param myAddress Address of the person doing the reply-all, to avoid
930     *                  being sent a copy of the outgoing message.
931     * @return Reply-All message
932     */
933    public MessageNode toReplyAllMessage(String myAddress) {
934        MessageNode replyNode = this.toReplyMessage();
935
936        // Handle the additional fields for the reply-all case
937        // How do we get myAddress here?
938        int i;
939        if(to != null) {
940            for(i=0; i<to.length; i++) {
941                if(to[i].getAddress().toLowerCase().indexOf(myAddress) == -1) {
942                    if(replyNode.to == null) {
943                        replyNode.to = new Address[1];
944                        replyNode.to[0] = to[i];
945                    }
946                    else {
947                        Arrays.add(replyNode.to, to[i]);
948                    }
949                }
950            }
951        }
952        if(cc != null) {
953            for(i=0; i<cc.length; i++) {
954                if(cc[i].getAddress().toLowerCase().indexOf(myAddress) == -1) {
955                    if(replyNode.cc == null) {
956                        replyNode.cc = new Address[1];
957                        replyNode.cc[0] = cc[i];
958                    }
959                    else {
960                        Arrays.add(replyNode.cc, cc[i]);
961                    }
962                }
963            }
964        }
965        return replyNode;
966    }
967   
968    /**
969     * Get a message that represents a forwarding of the original message.
970     * The resulting header will be clean, aside from the subject.
971     * The resulting body will be the same as a reply message (single TextPart),
972     * aside from some of the original header fields being prepended.
973     *
974     * @return Forwarded message
975     */
976    public MessageNode toForwardMessage() {
977       
978        String fromString = StringParser.makeCsvString(StringParser.toStringArray(from));
979        String toString = StringParser.makeCsvString(StringParser.toStringArray(to));
980        String ccString = StringParser.makeCsvString(StringParser.toStringArray(cc));
981       
982        synchronized(messageContent) {
983                FindFirstTextPartVisitor findVisitor = new FindFirstTextPartVisitor();
984                if(this.messageStructure != null) {
985                        this.messageStructure.accept(findVisitor);
986                }
987                TextPart originalTextPart = findVisitor.getFirstTextPart();
988                TextContent originalTextContent = (TextContent)messageContent.get(originalTextPart);
989       
990                StringBuffer buf = new StringBuffer();
991       
992                // Create the first line of the reply text
993                buf.append("\r\n");
994                buf.append("----Original Message----\r\n");
995               
996                // Add the subject
997                buf.append("Subject: ");
998                buf.append(subject);
999                buf.append("\r\n");
1000       
1001                // Add the date
1002                buf.append("Date: ");
1003                buf.append(StringParser.createDateString(date));
1004                buf.append("\r\n");
1005               
1006                // Add the from field
1007                if(fromString != null && fromString.length() > 0) {
1008                        buf.append("From: ");
1009                        buf.append(fromString);
1010                        buf.append("\r\n");
1011                }
1012               
1013                // Add the from field
1014                if(toString != null && toString.length() > 0) {
1015                        buf.append("To: ");
1016                        buf.append(toString);
1017                        buf.append("\r\n");
1018                }
1019               
1020                // Add the CC field
1021                if(ccString != null && ccString.length() > 0) {
1022                    buf.append("Cc: ");
1023                    buf.append(ccString);
1024                    buf.append("\r\n");
1025                }
1026       
1027                // Add a blank like
1028                buf.append("\r\n");
1029               
1030                // Add the original text
1031                if(originalTextContent != null) {
1032                    buf.append(originalTextContent.getText());
1033                    buf.append("\r\n");
1034                }
1035               
1036                // Add the footer
1037                buf.append("------------------------");
1038       
1039                // Build the forward node
1040                MessageNode forwardNode = new MessageNode();
1041                String contentText = buf.toString();
1042                TextPart forwardPart = new TextPart("plain", "", "", "", "", "", contentText.length());
1043                forwardNode.messageStructure = forwardPart;
1044                forwardNode.putMessageContent(new TextContent(forwardPart, contentText));
1045       
1046                // Set the forward subject
1047                if(subject.toLowerCase().startsWith("fwd:")) {
1048                        forwardNode.subject = subject;
1049                }
1050                else {
1051                        forwardNode.subject = "Fwd: " + subject;
1052                }
1053               
1054                return forwardNode;
1055        }
1056    }
1057
1058    private static class FindFirstTextPartVisitor extends AbstractMimeMessagePartVisitor {
1059        private TextPart firstTextPart;
1060
1061        public TextPart getFirstTextPart() { return firstTextPart; }
1062       
1063                public void visitTextPart(TextPart part) {
1064                if(firstTextPart == null) {
1065                        firstTextPart = part;
1066                }
1067                }
1068    };
1069   
1070    /**
1071     * Populate the envelope for a reply to this message
1072     *
1073     * @param replyNode The MessageNode for the reply
1074     */
1075    private void populateReplyEnvelope(MessageNode replyNode) {
1076        // Set the reply subject
1077        if(subject.startsWith("Re:") || subject.startsWith("re:")) {
1078                replyNode.subject = subject;
1079        }
1080        else {
1081                replyNode.subject = "Re: " + subject;
1082        }
1083       
1084        // Set the message recipient
1085        int i;
1086        if(replyTo == null || replyTo.length == 0) {
1087            if(sender == null || sender.length == 0) {
1088                replyNode.to = new Address[from.length];
1089                for(i=0; i<from.length; i++) {
1090                        replyNode.to[i] = from[i];
1091                }
1092            }
1093            else {
1094                replyNode.to = new Address[sender.length];
1095                for(i=0; i<sender.length; i++) {
1096                        replyNode.to[i] = sender[i];
1097                }
1098            }
1099        }
1100        else {
1101                replyNode.to = new Address[replyTo.length];
1102            for(i=0; i<replyTo.length; i++) {
1103                replyNode.to[i] = replyTo[i];
1104            }
1105        }
1106
1107        // Finally, set the message in-reply-to ID
1108        replyNode.inReplyTo = messageId;
1109    }
1110   
1111    /**
1112     * Called to load the message data for this node.
1113     * <p>
1114     * This loads as much of the displayable parts of the message
1115     * as allowed within the limits defined in the configuration options.
1116     * It is intended to be a convenience request for the UI,
1117     * as individual parts can be requested for attachment download.
1118     * Since multiple parts are downloaded in response to this request,
1119     * multiple events may be fired as the retrieval process completes.
1120     * </p>
1121     *
1122     * @return True if a refresh was triggered, false otherwise
1123     */
1124        public boolean refreshMessage() {
1125                // TODO: Add code to refresh message from cache first
1126                boolean result = false;
1127                if(!refreshInProgress) {
1128                        refreshInProgress = true;
1129                        AbstractMailStore mailStore = parent.getParentAccount().getMailStore();
1130                        if(mailStore.hasMessageParts()) {
1131                                int maxSize = Integer.MAX_VALUE;
1132                                MimeMessagePart[] displayableParts = MimeMessagePartTransformer.getDisplayableParts(this.messageStructure);
1133                                if(parent.getParentAccount() instanceof NetworkAccountNode) {
1134                                AccountConfig accountConfig = ((NetworkAccountNode)parent.getParentAccount()).getAccountConfig();
1135                                if(accountConfig instanceof ImapConfig) {
1136                                        maxSize = ((ImapConfig)accountConfig).getMaxMessageSize();
1137                                }
1138                                }
1139                                Vector partsToFetch = new Vector();
1140                                int sizeRequested = 0;
1141                                for(int i=0; i<displayableParts.length; i++) {
1142                                        sizeRequested += displayableParts[i].getSize();
1143                                        if(sizeRequested <= maxSize) {
1144                                                partsToFetch.addElement(displayableParts[i]);
1145                                        }
1146                                        else {
1147                                                break;
1148                                        }
1149                                }
1150                               
1151                                if(partsToFetch.size() > 0) {
1152                                        displayableParts = new MimeMessagePart[partsToFetch.size()];
1153                                        partsToFetch.copyInto(displayableParts);
1154                                        (new Thread(new RefreshMessagePartsRunnable(displayableParts))).start();
1155                                        result = true;
1156                                }
1157                        }
1158                        else {
1159                                (new Thread(new RefreshMessageWholeRunnable())).start();
1160                                result = true;
1161                        }
1162                }
1163                return result;
1164        }
1165
1166        private class RefreshMessageWholeRunnable implements Runnable {
1167                public void run() {
1168                        boolean messageLoaded = false;
1169                        if(parent.getParentAccount().getStatus() != AccountNode.STATUS_LOCAL) {
1170                        try {
1171                                MessageNode tempNode = MailFileManager.getInstance().readMessageNode(parent, messageToken, true);
1172                                if(tempNode != null) {
1173                                    MimeMessagePart messageStructure = tempNode.getMessageStructure();
1174                                    MimeMessageContent[] messageContent = tempNode.getAllMessageContent();
1175                                   
1176                                    if(messageStructure != null && messageContent != null && messageContent.length > 0) {
1177                                                setMessageStructure(messageStructure);
1178                                                setMessageSource(tempNode.getMessageSource());
1179                                                putMessageContent(messageContent);
1180                                                messageLoaded = true;
1181                                    }
1182                                }
1183                        } catch (IOException e) {
1184                                EventLogger.logEvent(AppInfo.GUID,
1185                                ("Unable to read message from cache\r\n"
1186                                        + e.getMessage()).getBytes(),
1187                                EventLogger.ERROR);
1188                        }
1189                        }
1190                        if(!messageLoaded) {
1191                                AbstractMailStore mailStore = parent.getParentAccount().getMailStore();
1192                                mailStore.requestMessage(messageToken);
1193                        }
1194                }
1195        };
1196
1197        private class RefreshMessagePartsRunnable implements Runnable {
1198                private MimeMessagePart[] displayableParts;
1199                RefreshMessagePartsRunnable(MimeMessagePart[] displayableParts) {
1200                        this.displayableParts = displayableParts;
1201                }
1202                public void run() {
1203                        Vector contentToLoad = new Vector(displayableParts.length);
1204                        for(int i=0; i<displayableParts.length; i++) {
1205                                contentToLoad.addElement(displayableParts[i]);
1206                        }
1207                       
1208                        if(parent.getParentAccount().getStatus() != AccountNode.STATUS_LOCAL) {
1209                        try {
1210                                MimeMessageContent[] content = MailFileManager.getInstance().readMessageContent(parent, messageToken);
1211                                if(content != null && content.length > 0) {
1212                                        putMessageContent(content);
1213                                        for(int i=0; i<content.length; i++) {
1214                                                contentToLoad.removeElement(content[i].getMessagePart());
1215                                        }
1216                                }
1217                        } catch (IOException e) {
1218                                EventLogger.logEvent(AppInfo.GUID,
1219                                ("Unable to read message from cache\r\n"
1220                                        + e.getMessage()).getBytes(),
1221                                EventLogger.ERROR);
1222                        }
1223                        }
1224                       
1225                        if(!contentToLoad.isEmpty()) {
1226                                MimeMessagePart[] partsToLoad = new MimeMessagePart[contentToLoad.size()];
1227                                contentToLoad.copyInto(partsToLoad);
1228                                AbstractMailStore mailStore = parent.getParentAccount().getMailStore();
1229                                mailStore.requestMessageParts(messageToken, partsToLoad);
1230                        }
1231                }
1232        }
1233       
1234        /**
1235         * Called to load a specific message part for this node.
1236         *
1237         * @param messagePart Content part to load
1238         */
1239        public void requestContentPart(ContentPart messagePart) {
1240                AbstractMailStore mailStore = parent.getParentAccount().getMailStore();
1241                if(mailStore.hasMessageParts()) {
1242                        mailStore.requestMessageParts(messageToken, new MimeMessagePart[] { messagePart });
1243                }
1244        }
1245       
1246        /**
1247         * Called to request that the message state be changed to deleted.
1248         * Completion of this request will be indicated by a status
1249         * change event for the message flags.
1250         */
1251        public void deleteMessage() {
1252                parent.getParentAccount().getMailStore().requestMessageDelete(
1253                                messageToken,
1254                                createMessageFlags(this.flags));
1255        }
1256       
1257        /**
1258         * Called to request that the state of a deleted message be changed
1259         * back to normal.
1260         * Completion of this request will be indicated by a status
1261         * change event for the message flags.
1262         * <p>
1263         * If the mail store does not support undelete, then this method
1264         * will do nothing.
1265         * </p>
1266         */
1267        public void undeleteMessage() {
1268                AbstractMailStore mailStore = parent.getParentAccount().getMailStore();
1269                if(mailStore.hasUndelete()) {
1270                        mailStore.requestMessageUndelete(
1271                                messageToken,
1272                                createMessageFlags(this.flags));
1273                }
1274        }
1275       
1276        /**
1277         * Adds a <tt>MessageNodeListener</tt> to the message node.
1278         *
1279         * @param l The <tt>MessageNodeListener</tt> to be added.
1280         */
1281    public void addMessageNodeListener(MessageNodeListener l) {
1282        listenerList.add(MessageNodeListener.class, l);
1283    }
1284
1285    /**
1286     * Removes a <tt>MessageNodeListener</tt> from the message node.
1287     *
1288     * @param l The <tt>MessageNodeListener</tt> to be removed.
1289     */
1290    public void removeMessageNodeListener(MessageNodeListener l) {
1291        listenerList.remove(MessageNodeListener.class, l);
1292    }
1293   
1294    /**
1295     * Returns an array of all <tt>MessageNodeListener</tt>s
1296     * that have been added to this message node.
1297     *
1298     * @return All the <tt>MessageNodeListener</tt>s that have been added,
1299     * or an empty array if no listeners have been added.
1300     */
1301    public MessageNodeListener[] getMessageNodeListeners() {
1302        return (MessageNodeListener[])listenerList.getListeners(MessageNodeListener.class);
1303    }
1304   
1305    /**
1306     * Notifies all registered <tt>MessageNodeListener</tt>s that
1307     * the message status has changed.
1308     *
1309     * @param type The type of the status change.
1310     */
1311    protected void fireMessageStatusChanged(int type) {
1312        Object[] listeners = listenerList.getListeners(MessageNodeListener.class);
1313        MessageNodeEvent e = null;
1314        for(int i=0; i<listeners.length; i++) {
1315            if(e == null) {
1316                e = new MessageNodeEvent(this, type);
1317            }
1318            ((MessageNodeListener)listeners[i]).messageStatusChanged(e);
1319        }
1320    }
1321
1322    /**
1323     * Convert a protocol-later message flags object into the bit-field
1324     * representation needed for the object model.
1325     *
1326     * @param messageFlags Message flags object.
1327     * @return Bit-field message flags.
1328     */
1329    static int convertMessageFlags(MessageFlags messageFlags) {
1330                int flags = 0;
1331                if(messageFlags.isSeen()) { flags |= Flag.SEEN; }
1332                if(messageFlags.isAnswered()) { flags |= Flag.ANSWERED; }
1333                if(messageFlags.isFlagged()) { flags |= Flag.FLAGGED; }
1334                if(messageFlags.isDeleted()) { flags |= Flag.DELETED; }
1335                if(messageFlags.isDraft()) { flags |= Flag.DRAFT; }
1336                if(messageFlags.isRecent()) { flags |= Flag.RECENT; }
1337        if(messageFlags.isForwarded()) { flags |= Flag.FORWARDED; }
1338                if(messageFlags.isJunk()) { flags |= Flag.JUNK; }
1339                return flags;
1340        }
1341
1342    /**
1343     * Convert a bit-field message flag representation from the
1344     * object model into a message flags object needed by the
1345     * protocol-later.
1346     *
1347     * @param flags Bit-field message flags.
1348     * @return Message flags object.
1349     */
1350        static MessageFlags createMessageFlags(int flags) {
1351                MessageFlags messageFlags = new MessageFlags();
1352                messageFlags.setSeen((flags & Flag.SEEN) != 0);
1353                messageFlags.setAnswered((flags & Flag.ANSWERED) != 0);
1354                messageFlags.setFlagged((flags & Flag.FLAGGED) != 0);
1355                messageFlags.setDeleted((flags & Flag.DELETED) != 0);
1356                messageFlags.setDraft((flags & Flag.DRAFT) != 0);
1357                messageFlags.setRecent((flags & Flag.RECENT) != 0);
1358        messageFlags.setForwarded((flags & Flag.FORWARDED) != 0);
1359                messageFlags.setJunk((flags & Flag.JUNK) != 0);
1360                return messageFlags;
1361        }
1362}
Note: See TracBrowser for help on using the browser.