source: trunk/LogicMail/src/org/logicprobe/LogicMail/model/MessageNode.java @ 959

Revision 959, 44.6 KB checked in by octorian, 9 months ago (diff)

Added the ability to toggle between Plain Text and HTML display modes when viewing messages.

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