View Javadoc
1   /*
2    * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
3    * Copyright (C) 2009  Mickael Guessant
4    *
5    * This program is free software; you can redistribute it and/or
6    * modify it under the terms of the GNU General Public License
7    * as published by the Free Software Foundation; either version 2
8    * of the License, or (at your option) any later version.
9    *
10   * This program is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU General Public License for more details.
14   *
15   * You should have received a copy of the GNU General Public License
16   * along with this program; if not, write to the Free Software
17   * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
18   */
19  package davmail.exchange;
20  
21  import davmail.BundleMessage;
22  import davmail.Settings;
23  import davmail.exception.DavMailException;
24  import davmail.exception.HttpNotFoundException;
25  import davmail.http.URIUtil;
26  import davmail.ui.NotificationDialog;
27  import davmail.util.StringUtil;
28  import org.apache.log4j.Logger;
29  
30  import javax.mail.MessagingException;
31  import javax.mail.internet.InternetAddress;
32  import javax.mail.internet.InternetHeaders;
33  import javax.mail.internet.MimeMessage;
34  import javax.mail.internet.MimeMultipart;
35  import javax.mail.internet.MimePart;
36  import javax.mail.util.SharedByteArrayInputStream;
37  import java.io.ByteArrayOutputStream;
38  import java.io.File;
39  import java.io.FileOutputStream;
40  import java.io.IOException;
41  import java.io.InputStream;
42  import java.io.OutputStreamWriter;
43  import java.io.StringReader;
44  import java.net.NoRouteToHostException;
45  import java.net.UnknownHostException;
46  import java.nio.charset.StandardCharsets;
47  import java.text.ParseException;
48  import java.text.SimpleDateFormat;
49  import java.util.*;
50  
51  /**
52   * Exchange session through Outlook Web Access (DAV)
53   */
54  public abstract class ExchangeSession {
55  
56      protected static final Logger LOGGER = Logger.getLogger("davmail.exchange.ExchangeSession");
57  
58      /**
59       * Reference GMT timezone to format dates
60       */
61      public static final SimpleTimeZone GMT_TIMEZONE = new SimpleTimeZone(0, "GMT");
62  
63      protected static final int FREE_BUSY_INTERVAL = 15;
64  
65      protected static final String PUBLIC_ROOT = "/public/";
66      protected static final String CALENDAR = "calendar";
67      protected static final String TASKS = "tasks";
68      /**
69       * Contacts folder logical name
70       */
71      public static final String CONTACTS = "contacts";
72      protected static final String ADDRESSBOOK = "addressbook";
73      protected static final String INBOX = "INBOX";
74      protected static final String LOWER_CASE_INBOX = "inbox";
75      protected static final String MIXED_CASE_INBOX = "Inbox";
76      protected static final String SENT = "Sent";
77      protected static final String SENDMSG = "##DavMailSubmissionURI##";
78      protected static final String DRAFTS = "Drafts";
79      protected static final String TRASH = "Trash";
80      protected static final String JUNK = "Junk";
81      protected static final String UNSENT = "Unsent Messages";
82  
83      protected static final List<String> SPECIAL = Arrays.asList(SENT, DRAFTS, TRASH, JUNK);
84  
85      static {
86          // Adjust Mime decoder settings
87          System.setProperty("mail.mime.ignoreunknownencoding", "true");
88          System.setProperty("mail.mime.decodetext.strict", "false");
89      }
90  
91      protected String publicFolderUrl;
92  
93      /**
94       * Base user mailboxes path (used to select folder)
95       */
96      protected String mailPath;
97      protected String rootPath;
98      protected String email;
99      protected String alias;
100     /**
101      * Lower case Caldav path to current user mailbox.
102      * /users/<i>email</i>
103      */
104     protected String currentMailboxPath;
105 
106     protected String userName;
107 
108     protected String serverVersion;
109 
110     protected static final String YYYY_MM_DD_HH_MM_SS = "yyyy/MM/dd HH:mm:ss";
111     private static final String YYYYMMDD_T_HHMMSS_Z = "yyyyMMdd'T'HHmmss'Z'";
112     protected static final String YYYY_MM_DD_T_HHMMSS_Z = "yyyy-MM-dd'T'HH:mm:ss'Z'";
113     private static final String YYYY_MM_DD = "yyyy-MM-dd";
114     private static final String YYYY_MM_DD_T_HHMMSS_SSS_Z = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
115 
116     public ExchangeSession() {
117         // empty constructor
118     }
119 
120     /**
121      * Close session.
122      * Shutdown http client connection manager
123      */
124     public abstract void close();
125 
126     /**
127      * Format date to exchange search format.
128      *
129      * @param date date object
130      * @return formatted search date
131      */
132     public abstract String formatSearchDate(Date date);
133 
134     /**
135      * Return standard zulu date formatter.
136      *
137      * @return zulu date formatter
138      */
139     public static SimpleDateFormat getZuluDateFormat() {
140         SimpleDateFormat dateFormat = new SimpleDateFormat(YYYYMMDD_T_HHMMSS_Z, Locale.ENGLISH);
141         dateFormat.setTimeZone(GMT_TIMEZONE);
142         return dateFormat;
143     }
144 
145     protected static SimpleDateFormat getVcardBdayFormat() {
146         SimpleDateFormat dateFormat = new SimpleDateFormat(YYYY_MM_DD, Locale.ENGLISH);
147         dateFormat.setTimeZone(GMT_TIMEZONE);
148         return dateFormat;
149     }
150 
151     protected static SimpleDateFormat getExchangeDateFormat(String value) {
152         SimpleDateFormat dateFormat;
153         if (value.length() == 8) {
154             dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.ENGLISH);
155             dateFormat.setTimeZone(GMT_TIMEZONE);
156         } else if (value.length() == 15) {
157             dateFormat = new SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.ENGLISH);
158             dateFormat.setTimeZone(GMT_TIMEZONE);
159         } else if (value.length() == 16) {
160             dateFormat = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.ENGLISH);
161             dateFormat.setTimeZone(GMT_TIMEZONE);
162         } else {
163             dateFormat = ExchangeSession.getExchangeZuluDateFormat();
164         }
165         return dateFormat;
166     }
167 
168     protected static SimpleDateFormat getExchangeZuluDateFormat() {
169         SimpleDateFormat dateFormat = new SimpleDateFormat(YYYY_MM_DD_T_HHMMSS_Z, Locale.ENGLISH);
170         dateFormat.setTimeZone(GMT_TIMEZONE);
171         return dateFormat;
172     }
173 
174     protected static SimpleDateFormat getExchangeZuluDateFormatMillisecond() {
175         SimpleDateFormat dateFormat = new SimpleDateFormat(YYYY_MM_DD_T_HHMMSS_SSS_Z, Locale.ENGLISH);
176         dateFormat.setTimeZone(GMT_TIMEZONE);
177         return dateFormat;
178     }
179 
180     protected static Date parseDate(String dateString) throws ParseException {
181         SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd");
182         dateFormat.setTimeZone(GMT_TIMEZONE);
183         return dateFormat.parse(dateString);
184     }
185 
186 
187     /**
188      * Test if the session expired.
189      *
190      * @return true this session expired
191      * @throws NoRouteToHostException on error
192      * @throws UnknownHostException   on error
193      */
194     public boolean isExpired() throws NoRouteToHostException, UnknownHostException {
195         boolean isExpired = false;
196         try {
197             getFolder("");
198         } catch (UnknownHostException | NoRouteToHostException exc) {
199             throw exc;
200         } catch (IOException e) {
201             isExpired = true;
202         }
203 
204         return isExpired;
205     }
206 
207     protected abstract void buildSessionInfo(java.net.URI uri) throws IOException;
208 
209     /**
210      * Create message in specified folder.
211      * Will overwrite an existing message with same subject in the same folder
212      *
213      * @param folderPath  Exchange folder path
214      * @param messageName message name
215      * @param properties  message properties (flags)
216      * @param mimeMessage MIME message
217      * @throws IOException when unable to create message
218      */
219     public abstract Message createMessage(String folderPath, String messageName, HashMap<String, String> properties, MimeMessage mimeMessage) throws IOException;
220 
221     /**
222      * Update given properties on message.
223      *
224      * @param message    Exchange message
225      * @param properties Webdav properties map
226      * @throws IOException on error
227      */
228     public abstract void updateMessage(Message message, Map<String, String> properties) throws IOException;
229 
230 
231     /**
232      * Delete Exchange message.
233      *
234      * @param message Exchange message
235      * @throws IOException on error
236      */
237     public abstract void deleteMessage(Message message) throws IOException;
238 
239     /**
240      * Get raw MIME message content
241      *
242      * @param message Exchange message
243      * @return message body
244      * @throws IOException on error
245      */
246     protected abstract byte[] getContent(Message message) throws IOException;
247 
248     protected static final Set<String> POP_MESSAGE_ATTRIBUTES = new HashSet<>();
249 
250     static {
251         POP_MESSAGE_ATTRIBUTES.add("uid");
252         POP_MESSAGE_ATTRIBUTES.add("imapUid");
253         POP_MESSAGE_ATTRIBUTES.add("messageSize");
254     }
255 
256     /**
257      * Return folder message list with id and size only (for POP3 listener).
258      *
259      * @param folderName Exchange folder name
260      * @return folder message list
261      * @throws IOException on error
262      */
263     public MessageList getAllMessageUidAndSize(String folderName) throws IOException {
264         return searchMessages(folderName, POP_MESSAGE_ATTRIBUTES, null);
265     }
266 
267     protected static final Set<String> IMAP_MESSAGE_ATTRIBUTES = new HashSet<>();
268 
269     static {
270         IMAP_MESSAGE_ATTRIBUTES.add("permanenturl");
271         IMAP_MESSAGE_ATTRIBUTES.add("urlcompname");
272         IMAP_MESSAGE_ATTRIBUTES.add("uid");
273         IMAP_MESSAGE_ATTRIBUTES.add("messageSize");
274         IMAP_MESSAGE_ATTRIBUTES.add("imapUid");
275         IMAP_MESSAGE_ATTRIBUTES.add("junk");
276         IMAP_MESSAGE_ATTRIBUTES.add("flagStatus");
277         IMAP_MESSAGE_ATTRIBUTES.add("messageFlags");
278         IMAP_MESSAGE_ATTRIBUTES.add("lastVerbExecuted");
279         IMAP_MESSAGE_ATTRIBUTES.add("read");
280         IMAP_MESSAGE_ATTRIBUTES.add("deleted");
281         IMAP_MESSAGE_ATTRIBUTES.add("date");
282         IMAP_MESSAGE_ATTRIBUTES.add("lastmodified");
283         // OSX IMAP requests content-class
284         IMAP_MESSAGE_ATTRIBUTES.add("contentclass");
285         IMAP_MESSAGE_ATTRIBUTES.add("keywords");
286     }
287 
288     protected static final Set<String> UID_MESSAGE_ATTRIBUTES = new HashSet<>();
289 
290     static {
291         UID_MESSAGE_ATTRIBUTES.add("uid");
292     }
293 
294     /**
295      * Get all folder messages.
296      *
297      * @param folderPath Exchange folder name
298      * @return message list
299      * @throws IOException on error
300      */
301     public MessageList searchMessages(String folderPath) throws IOException {
302         return searchMessages(folderPath, IMAP_MESSAGE_ATTRIBUTES, null);
303     }
304 
305     /**
306      * Search folder for messages matching conditions, with attributes needed by IMAP listener.
307      *
308      * @param folderName Exchange folder name
309      * @param condition  search filter
310      * @return message list
311      * @throws IOException on error
312      */
313     public MessageList searchMessages(String folderName, Condition condition) throws IOException {
314         return searchMessages(folderName, IMAP_MESSAGE_ATTRIBUTES, condition);
315     }
316 
317     /**
318      * Search folder for messages matching conditions, with given attributes.
319      *
320      * @param folderName Exchange folder name
321      * @param attributes requested Webdav attributes
322      * @param condition  search filter
323      * @return message list
324      * @throws IOException on error
325      */
326     public abstract MessageList searchMessages(String folderName, Set<String> attributes, Condition condition) throws IOException;
327 
328     /**
329      * Get server version (Exchange2003, Exchange2007 or Exchange2010)
330      *
331      * @return server version
332      */
333     public String getServerVersion() {
334         return serverVersion;
335     }
336 
337     public enum Operator {
338         Or, And, Not, IsEqualTo,
339         IsGreaterThan, IsGreaterThanOrEqualTo,
340         IsLessThan, IsLessThanOrEqualTo,
341         IsNull, IsTrue, IsFalse,
342         Like, StartsWith, Contains
343     }
344 
345     /**
346      * Exchange search filter.
347      */
348     public interface Condition {
349         /**
350          * Append condition to buffer.
351          *
352          * @param buffer search filter buffer
353          */
354         void appendTo(StringBuilder buffer);
355 
356         /**
357          * True if condition is empty.
358          *
359          * @return true if condition is empty
360          */
361         boolean isEmpty();
362 
363         /**
364          * Test if the contact matches current condition.
365          *
366          * @param contact Exchange Contact
367          * @return true if contact matches condition
368          */
369         boolean isMatch(ExchangeSession.Contact contact);
370     }
371 
372     /**
373      * Attribute condition.
374      */
375     public abstract static class AttributeCondition implements Condition {
376         protected final String attributeName;
377         protected final Operator operator;
378         protected final String value;
379 
380         protected AttributeCondition(String attributeName, Operator operator, String value) {
381             this.attributeName = attributeName;
382             this.operator = operator;
383             this.value = value;
384         }
385 
386         public boolean isEmpty() {
387             return false;
388         }
389 
390         /**
391          * Get attribute name.
392          *
393          * @return attribute name
394          */
395         public String getAttributeName() {
396             return attributeName;
397         }
398 
399         /**
400          * Condition value.
401          *
402          * @return value
403          */
404         public String getValue() {
405             return value;
406         }
407 
408     }
409 
410     /**
411      * Multiple condition.
412      */
413     public abstract static class MultiCondition implements Condition {
414         protected final Operator operator;
415         protected final List<Condition> conditions;
416 
417         protected MultiCondition(Operator operator, Condition... conditions) {
418             this.operator = operator;
419             this.conditions = new ArrayList<>();
420             for (Condition condition : conditions) {
421                 if (condition != null) {
422                     this.conditions.add(condition);
423                 }
424             }
425         }
426 
427         /**
428          * Conditions list.
429          *
430          * @return conditions
431          */
432         public List<Condition> getConditions() {
433             return conditions;
434         }
435 
436         /**
437          * Condition operator.
438          *
439          * @return operator
440          */
441         public Operator getOperator() {
442             return operator;
443         }
444 
445         /**
446          * Add a new condition.
447          *
448          * @param condition single condition
449          */
450         public void add(Condition condition) {
451             if (condition != null) {
452                 conditions.add(condition);
453             }
454         }
455 
456         public boolean isEmpty() {
457             boolean isEmpty = true;
458             for (Condition condition : conditions) {
459                 if (!condition.isEmpty()) {
460                     isEmpty = false;
461                     break;
462                 }
463             }
464             return isEmpty;
465         }
466 
467         public boolean isMatch(ExchangeSession.Contact contact) {
468             if (operator == Operator.And) {
469                 for (Condition condition : conditions) {
470                     if (!condition.isMatch(contact)) {
471                         return false;
472                     }
473                 }
474                 return true;
475             } else if (operator == Operator.Or) {
476                 for (Condition condition : conditions) {
477                     if (condition.isMatch(contact)) {
478                         return true;
479                     }
480                 }
481                 return false;
482             } else {
483                 return false;
484             }
485         }
486 
487     }
488 
489     /**
490      * Not condition.
491      */
492     public abstract static class NotCondition implements Condition {
493         protected final Condition condition;
494 
495         protected NotCondition(Condition condition) {
496             this.condition = condition;
497         }
498 
499         public boolean isEmpty() {
500             return condition.isEmpty();
501         }
502 
503         public boolean isMatch(ExchangeSession.Contact contact) {
504             return !condition.isMatch(contact);
505         }
506     }
507 
508     /**
509      * Single search filter condition.
510      */
511     public abstract static class MonoCondition implements Condition {
512         protected final String attributeName;
513         protected final Operator operator;
514 
515         protected MonoCondition(String attributeName, Operator operator) {
516             this.attributeName = attributeName;
517             this.operator = operator;
518         }
519 
520         public boolean isEmpty() {
521             return false;
522         }
523 
524         public boolean isMatch(ExchangeSession.Contact contact) {
525             String actualValue = contact.get(attributeName);
526             return (operator == Operator.IsNull && actualValue == null) ||
527                     (operator == Operator.IsFalse && "false".equals(actualValue)) ||
528                     (operator == Operator.IsTrue && "true".equals(actualValue));
529         }
530     }
531 
532     /**
533      * And search filter.
534      *
535      * @param condition search conditions
536      * @return condition
537      */
538     public abstract MultiCondition and(Condition... condition);
539 
540     /**
541      * Or search filter.
542      *
543      * @param condition search conditions
544      * @return condition
545      */
546     public abstract MultiCondition or(Condition... condition);
547 
548     /**
549      * Not search filter.
550      *
551      * @param condition search condition
552      * @return condition
553      */
554     public abstract Condition not(Condition condition);
555 
556     /**
557      * Equals condition.
558      *
559      * @param attributeName logical Exchange attribute name
560      * @param value         attribute value
561      * @return condition
562      */
563     public abstract Condition isEqualTo(String attributeName, String value);
564 
565     /**
566      * Equals condition.
567      *
568      * @param attributeName logical Exchange attribute name
569      * @param value         attribute value
570      * @return condition
571      */
572     public abstract Condition isEqualTo(String attributeName, int value);
573 
574     /**
575      * MIME header equals condition.
576      *
577      * @param headerName MIME header name
578      * @param value      attribute value
579      * @return condition
580      */
581     public abstract Condition headerIsEqualTo(String headerName, String value);
582 
583     /**
584      * Greater than or equals condition.
585      *
586      * @param attributeName logical Exchange attribute name
587      * @param value         attribute value
588      * @return condition
589      */
590     public abstract Condition gte(String attributeName, String value);
591 
592     /**
593      * Greater than condition.
594      *
595      * @param attributeName logical Exchange attribute name
596      * @param value         attribute value
597      * @return condition
598      */
599     public abstract Condition gt(String attributeName, String value);
600 
601     /**
602      * Lower than condition.
603      *
604      * @param attributeName logical Exchange attribute name
605      * @param value         attribute value
606      * @return condition
607      */
608     public abstract Condition lt(String attributeName, String value);
609 
610     /**
611      * Lower than or equals condition.
612      *
613      * @param attributeName logical Exchange attribute name
614      * @param value         attribute value
615      * @return condition
616      */
617     @SuppressWarnings({"UnusedDeclaration"})
618     public abstract Condition lte(String attributeName, String value);
619 
620     /**
621      * Contains condition.
622      *
623      * @param attributeName logical Exchange attribute name
624      * @param value         attribute value
625      * @return condition
626      */
627     public abstract Condition contains(String attributeName, String value);
628 
629     /**
630      * Starts with condition.
631      *
632      * @param attributeName logical Exchange attribute name
633      * @param value         attribute value
634      * @return condition
635      */
636     public abstract Condition startsWith(String attributeName, String value);
637 
638     /**
639      * Is null condition.
640      *
641      * @param attributeName logical Exchange attribute name
642      * @return condition
643      */
644     public abstract Condition isNull(String attributeName);
645 
646     /**
647      * Exists condition.
648      *
649      * @param attributeName logical Exchange attribute name
650      * @return condition
651      */
652     public abstract Condition exists(String attributeName);
653 
654     /**
655      * Is true condition.
656      *
657      * @param attributeName logical Exchange attribute name
658      * @return condition
659      */
660     public abstract Condition isTrue(String attributeName);
661 
662     /**
663      * Is false condition.
664      *
665      * @param attributeName logical Exchange attribute name
666      * @return condition
667      */
668     public abstract Condition isFalse(String attributeName);
669 
670     /**
671      * Search mail and generic folders under given folder.
672      * Exclude calendar and contacts folders
673      *
674      * @param folderName Exchange folder name
675      * @param recursive  deep search if true
676      * @return list of folders
677      * @throws IOException on error
678      */
679     public List<Folder> getSubFolders(String folderName, boolean recursive, boolean wildcard) throws IOException {
680         MultiCondition folderCondition = and();
681         if (!Settings.getBooleanProperty("davmail.imapIncludeSpecialFolders", false)) {
682             folderCondition.add(or(isEqualTo("folderclass", "IPF.Note"),
683                     isEqualTo("folderclass", "IPF.Note.Microsoft.Conversation"),
684                     isNull("folderclass")));
685         }
686         if (wildcard) {
687             folderCondition.add(startsWith("displayname", folderName));
688             folderName = "";
689         }
690         List<Folder> results = getSubFolders(folderName, folderCondition,
691                 recursive);
692         // need to include base folder in recursive search, except on root
693         if (recursive && folderName.length() > 0) {
694             results.add(getFolder(folderName));
695         }
696 
697         return results;
698     }
699 
700     /**
701      * Search calendar folders under given folder.
702      *
703      * @param folderName Exchange folder name
704      * @param recursive  deep search if true
705      * @return list of folders
706      * @throws IOException on error
707      */
708     public List<Folder> getSubCalendarFolders(String folderName, boolean recursive) throws IOException {
709         return getSubFolders(folderName, isEqualTo("folderclass", "IPF.Appointment"), recursive);
710     }
711 
712     /**
713      * Search folders under given folder matching filter.
714      *
715      * @param folderName Exchange folder name
716      * @param condition  search filter
717      * @param recursive  deep search if true
718      * @return list of folders
719      * @throws IOException on error
720      */
721     public abstract List<Folder> getSubFolders(String folderName, Condition condition, boolean recursive) throws IOException;
722 
723     /**
724      * Delete oldest messages in trash.
725      * keepDelay is the number of days to keep messages in trash before delete
726      *
727      * @throws IOException when unable to purge messages
728      */
729     public void purgeOldestTrashAndSentMessages() throws IOException {
730         int keepDelay = Settings.getIntProperty("davmail.keepDelay");
731         if (keepDelay != 0) {
732             purgeOldestFolderMessages(TRASH, keepDelay);
733         }
734         // this is a new feature, default is : do nothing
735         int sentKeepDelay = Settings.getIntProperty("davmail.sentKeepDelay");
736         if (sentKeepDelay != 0) {
737             purgeOldestFolderMessages(SENT, sentKeepDelay);
738         }
739     }
740 
741     protected void purgeOldestFolderMessages(String folderPath, int keepDelay) throws IOException {
742         Calendar cal = Calendar.getInstance();
743         cal.add(Calendar.DAY_OF_MONTH, -keepDelay);
744         LOGGER.debug("Delete messages in " + folderPath + " not modified since " + cal.getTime());
745 
746         MessageList messages = searchMessages(folderPath, UID_MESSAGE_ATTRIBUTES,
747                 lt("lastmodified", formatSearchDate(cal.getTime())));
748 
749         for (Message message : messages) {
750             message.delete();
751         }
752     }
753 
754     protected void convertResentHeader(MimeMessage mimeMessage, String headerName) throws MessagingException {
755         String[] resentHeader = mimeMessage.getHeader("Resent-" + headerName);
756         if (resentHeader != null) {
757             mimeMessage.removeHeader("Resent-" + headerName);
758             mimeMessage.removeHeader(headerName);
759             for (String value : resentHeader) {
760                 mimeMessage.addHeader(headerName, value);
761             }
762         }
763     }
764 
765     protected String lastSentMessageId;
766 
767     /**
768      * Send message in reader to recipients.
769      * Detect visible recipients in message body to determine bcc recipients
770      *
771      * @param rcptToRecipients recipients list
772      * @param mimeMessage      mime message
773      * @throws IOException        on error
774      * @throws MessagingException on error
775      */
776     public void sendMessage(List<String> rcptToRecipients, MimeMessage mimeMessage) throws IOException, MessagingException {
777         // detect duplicate send command
778         String messageId = mimeMessage.getMessageID();
779         if (lastSentMessageId != null && lastSentMessageId.equals(messageId)) {
780             LOGGER.debug("Dropping message id " + messageId + ": already sent");
781             return;
782         }
783         lastSentMessageId = messageId;
784 
785         convertResentHeader(mimeMessage, "From");
786         convertResentHeader(mimeMessage, "To");
787         convertResentHeader(mimeMessage, "Cc");
788         convertResentHeader(mimeMessage, "Bcc");
789         convertResentHeader(mimeMessage, "Message-Id");
790 
791         // do not allow send as another user on Exchange 2003
792         if ("Exchange2003".equals(serverVersion) || Settings.getBooleanProperty("davmail.smtpStripFrom", false)) {
793             mimeMessage.removeHeader("From");
794         }
795 
796         // remove visible recipients from list
797         Set<String> visibleRecipients = new HashSet<>();
798         List<InternetAddress> recipients = getAllRecipients(mimeMessage);
799         for (InternetAddress address : recipients) {
800             visibleRecipients.add((address.getAddress().toLowerCase()));
801         }
802         for (String recipient : rcptToRecipients) {
803             if (!visibleRecipients.contains(recipient.toLowerCase())) {
804                 mimeMessage.addRecipient(javax.mail.Message.RecipientType.BCC, new InternetAddress(recipient));
805             }
806         }
807         sendMessage(mimeMessage);
808 
809     }
810 
811     static final String[] RECIPIENT_HEADERS = {"to", "cc", "bcc"};
812 
813     protected List<InternetAddress> getAllRecipients(MimeMessage mimeMessage) throws MessagingException {
814         List<InternetAddress> recipientList = new ArrayList<>();
815         for (String recipientHeader : RECIPIENT_HEADERS) {
816             final String recipientHeaderValue = mimeMessage.getHeader(recipientHeader, ",");
817             if (recipientHeaderValue != null) {
818                 // parse headers in non strict mode
819                 recipientList.addAll(Arrays.asList(InternetAddress.parseHeader(recipientHeaderValue, false)));
820             }
821 
822         }
823         return recipientList;
824     }
825 
826     /**
827      * Send Mime message.
828      *
829      * @param mimeMessage MIME message
830      * @throws IOException        on error
831      * @throws MessagingException on error
832      */
833     public abstract void sendMessage(MimeMessage mimeMessage) throws IOException, MessagingException;
834 
835     /**
836      * Get folder object.
837      * Folder name can be logical names INBOX, Drafts, Trash or calendar,
838      * or a path relative to user base folder or absolute path.
839      *
840      * @param folderPath folder path
841      * @return Folder object
842      * @throws IOException on error
843      */
844     public ExchangeSession.Folder getFolder(String folderPath) throws IOException {
845         Folder folder = internalGetFolder(folderPath);
846         if (isMainCalendar(folderPath)) {
847             Folder taskFolder = internalGetFolder(TASKS);
848             folder.ctag += taskFolder.ctag;
849         }
850         return folder;
851     }
852 
853     protected abstract Folder internalGetFolder(String folderName) throws IOException;
854 
855     /**
856      * Check folder ctag and reload messages as needed.
857      *
858      * @param currentFolder current folder
859      * @return true if folder changed
860      * @throws IOException on error
861      */
862     public boolean refreshFolder(Folder currentFolder) throws IOException {
863         Folder newFolder = getFolder(currentFolder.folderPath);
864         if (currentFolder.ctag == null || !currentFolder.ctag.equals(newFolder.ctag)
865                 // ctag stamp is limited to second, check message count
866                 || !(currentFolder.count == newFolder.count)
867         ) {
868             if (LOGGER.isDebugEnabled()) {
869                 LOGGER.debug("Contenttag or count changed on " + currentFolder.folderPath +
870                         " ctag: " + currentFolder.ctag + " => " + newFolder.ctag +
871                         " count: " + currentFolder.count + " => " + newFolder.count
872                         + ", reloading messages");
873             }
874             currentFolder.hasChildren = newFolder.hasChildren;
875             currentFolder.noInferiors = newFolder.noInferiors;
876             currentFolder.unreadCount = newFolder.unreadCount;
877             currentFolder.ctag = newFolder.ctag;
878             currentFolder.etag = newFolder.etag;
879             if (newFolder.uidNext > currentFolder.uidNext) {
880                 currentFolder.uidNext = newFolder.uidNext;
881             }
882             currentFolder.loadMessages();
883             return true;
884         } else {
885             return false;
886         }
887     }
888 
889     /**
890      * Create Exchange message folder.
891      *
892      * @param folderName logical folder name
893      * @throws IOException on error
894      */
895     public void createMessageFolder(String folderName) throws IOException {
896         createFolder(folderName, "IPF.Note", null);
897     }
898 
899     /**
900      * Create Exchange calendar folder.
901      *
902      * @param folderName logical folder name
903      * @param properties folder properties
904      * @return status
905      * @throws IOException on error
906      */
907     public int createCalendarFolder(String folderName, Map<String, String> properties) throws IOException {
908         return createFolder(folderName, "IPF.Appointment", properties);
909     }
910 
911     /**
912      * Create Exchange contact folder.
913      *
914      * @param folderName logical folder name
915      * @param properties folder properties
916      * @throws IOException on error
917      */
918     public void createContactFolder(String folderName, Map<String, String> properties) throws IOException {
919         createFolder(folderName, "IPF.Contact", properties);
920     }
921 
922     /**
923      * Create Exchange folder with given folder class.
924      *
925      * @param folderName  logical folder name
926      * @param folderClass folder class
927      * @param properties  folder properties
928      * @return status
929      * @throws IOException on error
930      */
931     public abstract int createFolder(String folderName, String folderClass, Map<String, String> properties) throws IOException;
932 
933     /**
934      * Update Exchange folder properties.
935      *
936      * @param folderName logical folder name
937      * @param properties folder properties
938      * @return status
939      * @throws IOException on error
940      */
941     public abstract int updateFolder(String folderName, Map<String, String> properties) throws IOException;
942 
943     /**
944      * Delete Exchange folder.
945      *
946      * @param folderName logical folder name
947      * @throws IOException on error
948      */
949     public abstract void deleteFolder(String folderName) throws IOException;
950 
951     /**
952      * Copy message to target folder
953      *
954      * @param message      Exchange message
955      * @param targetFolder target folder
956      * @throws IOException on error
957      */
958     public abstract void copyMessage(Message message, String targetFolder) throws IOException;
959 
960     public void copyMessages(List<Message> messages, String targetFolder) throws IOException {
961         for (Message message : messages) {
962             copyMessage(message, targetFolder);
963         }
964     }
965 
966 
967     /**
968      * Move message to target folder
969      *
970      * @param message      Exchange message
971      * @param targetFolder target folder
972      * @throws IOException on error
973      */
974     public abstract void moveMessage(Message message, String targetFolder) throws IOException;
975 
976     public void moveMessages(List<Message> messages, String targetFolder) throws IOException {
977         for (Message message : messages) {
978             moveMessage(message, targetFolder);
979         }
980     }
981 
982     /**
983      * Move folder to target name.
984      *
985      * @param folderName current folder name/path
986      * @param targetName target folder name/path
987      * @throws IOException on error
988      */
989     public abstract void moveFolder(String folderName, String targetName) throws IOException;
990 
991     /**
992      * Move item from source path to target path.
993      *
994      * @param sourcePath item source path
995      * @param targetPath item target path
996      * @throws IOException on error
997      */
998     public abstract void moveItem(String sourcePath, String targetPath) throws IOException;
999 
1000     protected abstract void moveToTrash(Message message) throws IOException;
1001 
1002     /**
1003      * Convert keyword value to IMAP flag.
1004      *
1005      * @param value keyword value
1006      * @return IMAP flag
1007      */
1008     public String convertKeywordToFlag(String value) {
1009         // first test for keyword in settings
1010         Properties flagSettings = Settings.getSubProperties("davmail.imapFlags");
1011         Enumeration<?> flagSettingsEnum = flagSettings.propertyNames();
1012         while (flagSettingsEnum.hasMoreElements()) {
1013             String key = (String) flagSettingsEnum.nextElement();
1014             if (value.equalsIgnoreCase(flagSettings.getProperty(key))) {
1015                 return key;
1016             }
1017         }
1018 
1019         ResourceBundle flagBundle = ResourceBundle.getBundle("imapflags");
1020         Enumeration<String> flagBundleEnum = flagBundle.getKeys();
1021         while (flagBundleEnum.hasMoreElements()) {
1022             String key = flagBundleEnum.nextElement();
1023             if (value.equalsIgnoreCase(flagBundle.getString(key))) {
1024                 return key;
1025             }
1026         }
1027 
1028         // fall back to raw value
1029         return value;
1030     }
1031 
1032     /**
1033      * Convert IMAP flag to keyword value.
1034      *
1035      * @param value IMAP flag
1036      * @return keyword value
1037      */
1038     public String convertFlagToKeyword(String value) {
1039         // first test for flag in settings
1040         Properties flagSettings = Settings.getSubProperties("davmail.imapFlags");
1041         // case insensitive lookup
1042         for (String key : flagSettings.stringPropertyNames()) {
1043             if (key.equalsIgnoreCase(value)) {
1044                 return flagSettings.getProperty(key);
1045             }
1046         }
1047 
1048         // fall back to predefined flags
1049         ResourceBundle flagBundle = ResourceBundle.getBundle("imapflags");
1050         for (String key : flagBundle.keySet()) {
1051             if (key.equalsIgnoreCase(value)) {
1052                 return flagBundle.getString(key);
1053             }
1054         }
1055 
1056         // fall back to raw value
1057         return value;
1058     }
1059 
1060     /**
1061      * Convert IMAP flags to keyword value.
1062      *
1063      * @param flags IMAP flags
1064      * @return keyword value
1065      */
1066     public String convertFlagsToKeywords(HashSet<String> flags) {
1067         HashSet<String> keywordSet = new HashSet<>();
1068         for (String flag : flags) {
1069             keywordSet.add(decodeKeyword(convertFlagToKeyword(flag)));
1070         }
1071         return StringUtil.join(keywordSet, ",");
1072     }
1073 
1074     protected String decodeKeyword(String keyword) {
1075         String result = keyword;
1076         if (keyword.contains("_x0028_") || keyword.contains("_x0029_")) {
1077             result = result.replaceAll("_x0028_", "(")
1078                     .replaceAll("_x0029_", ")");
1079         }
1080         return result;
1081     }
1082 
1083     protected String encodeKeyword(String keyword) {
1084         String result = keyword;
1085         if (keyword.indexOf('(') >= 0|| keyword.indexOf(')') >= 0) {
1086             result = result.replaceAll("\\(", "_x0028_")
1087                     .replaceAll("\\)", "_x0029_" );
1088         }
1089         return result;
1090     }
1091 
1092     /**
1093      * Exchange folder with IMAP properties
1094      */
1095     public class Folder {
1096         /**
1097          * Logical (IMAP) folder path.
1098          */
1099         public String folderPath;
1100 
1101         /**
1102          * Display Name.
1103          */
1104         public String displayName;
1105         /**
1106          * Folder class (PR_CONTAINER_CLASS).
1107          */
1108         public String folderClass;
1109         /**
1110          * Folder message count.
1111          */
1112         public int count;
1113         /**
1114          * Folder unread message count.
1115          */
1116         public int unreadCount;
1117         /**
1118          * true if folder has subfolders (DAV:hassubs).
1119          */
1120         public boolean hasChildren;
1121         /**
1122          * true if folder has no subfolders (DAV:nosubs).
1123          */
1124         public boolean noInferiors;
1125         /**
1126          * Folder content tag (to detect folder content changes).
1127          */
1128         public String ctag;
1129         /**
1130          * Folder etag (to detect folder object changes).
1131          */
1132         public String etag;
1133         /**
1134          * Next IMAP uid
1135          */
1136         public long uidNext;
1137         /**
1138          * recent count
1139          */
1140         public int recent;
1141 
1142         /**
1143          * Folder message list, empty before loadMessages call.
1144          */
1145         public ExchangeSession.MessageList messages;
1146         /**
1147          * Permanent uid (PR_SEARCH_KEY) to IMAP UID map.
1148          */
1149         private final HashMap<String, Long> permanentUrlToImapUidMap = new HashMap<>();
1150 
1151         /**
1152          * Get IMAP folder flags.
1153          *
1154          * @return folder flags in IMAP format
1155          */
1156         public String getFlags() {
1157             String specialFlag = "";
1158             if (isSpecial()) {
1159                 specialFlag = "\\" + folderPath + " ";
1160             }
1161             if (noInferiors) {
1162                 return specialFlag + "\\NoInferiors";
1163             } else if (hasChildren) {
1164                 return specialFlag + "\\HasChildren";
1165             } else {
1166                 return specialFlag + "\\HasNoChildren";
1167             }
1168         }
1169 
1170         /**
1171          * Special folder flag (Sent, Drafts, Trash, Junk).
1172          * @return true if folder is special
1173          */
1174         public boolean isSpecial() {
1175             return SPECIAL.contains(folderPath);
1176         }
1177 
1178         /**
1179          * Load folder messages.
1180          *
1181          * @throws IOException on error
1182          */
1183         public void loadMessages() throws IOException {
1184             messages = ExchangeSession.this.searchMessages(folderPath, null);
1185             fixUids(messages);
1186             recent = 0;
1187             for (Message message : messages) {
1188                 if (message.recent) {
1189                     recent++;
1190                 }
1191             }
1192             long computedUidNext = 1;
1193             if (!messages.isEmpty()) {
1194                 computedUidNext = messages.get(messages.size() - 1).getImapUid() + 1;
1195             }
1196             if (computedUidNext > uidNext) {
1197                 uidNext = computedUidNext;
1198             }
1199         }
1200 
1201         /**
1202          * Search messages in folder matching query.
1203          *
1204          * @param condition search query
1205          * @return message list
1206          * @throws IOException on error
1207          */
1208         public MessageList searchMessages(Condition condition) throws IOException {
1209             MessageList localMessages = ExchangeSession.this.searchMessages(folderPath, condition);
1210             fixUids(localMessages);
1211             return localMessages;
1212         }
1213 
1214         /**
1215          * Restore previous uids changed by a PROPPATCH (flag change).
1216          *
1217          * @param messages message list
1218          */
1219         protected void fixUids(MessageList messages) {
1220             boolean sortNeeded = false;
1221             for (Message message : messages) {
1222                 if (permanentUrlToImapUidMap.containsKey(message.getPermanentId())) {
1223                     long previousUid = permanentUrlToImapUidMap.get(message.getPermanentId());
1224                     if (message.getImapUid() != previousUid) {
1225                         LOGGER.debug("Restoring IMAP uid " + message.getImapUid() + " -> " + previousUid + " for message " + message.getPermanentId());
1226                         message.setImapUid(previousUid);
1227                         sortNeeded = true;
1228                     }
1229                 } else {
1230                     // add message to uid map
1231                     permanentUrlToImapUidMap.put(message.getPermanentId(), message.getImapUid());
1232                 }
1233             }
1234             if (sortNeeded) {
1235                 Collections.sort(messages);
1236             }
1237         }
1238 
1239         /**
1240          * Folder message count.
1241          *
1242          * @return message count
1243          */
1244         public int count() {
1245             if (messages == null) {
1246                 return count;
1247             } else {
1248                 return messages.size();
1249             }
1250         }
1251 
1252         /**
1253          * Compute IMAP uidnext.
1254          *
1255          * @return max(messageuids)+1
1256          */
1257         public long getUidNext() {
1258             return uidNext;
1259         }
1260 
1261         /**
1262          * Get message at index.
1263          *
1264          * @param index message index
1265          * @return message
1266          */
1267         public Message get(int index) {
1268             return messages.get(index);
1269         }
1270 
1271         /**
1272          * Get current folder messages imap uids and flags
1273          *
1274          * @return imap uid list
1275          */
1276         public TreeMap<Long, String> getImapFlagMap() {
1277             TreeMap<Long, String> imapFlagMap = new TreeMap<>();
1278             for (ExchangeSession.Message message : messages) {
1279                 imapFlagMap.put(message.getImapUid(), message.getImapFlags());
1280             }
1281             return imapFlagMap;
1282         }
1283 
1284         /**
1285          * Calendar folder flag.
1286          *
1287          * @return true if this is a calendar folder
1288          */
1289         public boolean isCalendar() {
1290             return "IPF.Appointment".equals(folderClass);
1291         }
1292 
1293         /**
1294          * Contact folder flag.
1295          *
1296          * @return true if this is a calendar folder
1297          */
1298         public boolean isContact() {
1299             return "IPF.Contact".equals(folderClass);
1300         }
1301 
1302         /**
1303          * Task folder flag.
1304          *
1305          * @return true if this is a task folder
1306          */
1307         public boolean isTask() {
1308             return "IPF.Task".equals(folderClass);
1309         }
1310 
1311         /**
1312          * drop cached message
1313          */
1314         public void clearCache() {
1315             messages.cachedMimeContent = null;
1316             messages.cachedMimeMessage = null;
1317             messages.cachedMessageImapUid = 0;
1318         }
1319     }
1320 
1321     /**
1322      * Exchange message.
1323      */
1324     public abstract class Message implements Comparable<Message> {
1325         /**
1326          * enclosing message list
1327          */
1328         public MessageList messageList;
1329         /**
1330          * Message url.
1331          */
1332         public String messageUrl;
1333         /**
1334          * Message permanent url (does not change on message move).
1335          */
1336         public String permanentUrl;
1337         /**
1338          * Message uid.
1339          */
1340         public String uid;
1341         /**
1342          * Message content class.
1343          */
1344         public String contentClass;
1345         /**
1346          * Message keywords (categories).
1347          */
1348         public String keywords;
1349         /**
1350          * Message IMAP uid, unique in folder (x0e230003).
1351          */
1352         public long imapUid;
1353         /**
1354          * MAPI message size.
1355          */
1356         public int size;
1357         /**
1358          * Message date (urn:schemas:mailheader:date).
1359          */
1360         public String date;
1361 
1362         /**
1363          * Message flag: read.
1364          */
1365         public boolean read;
1366         /**
1367          * Message flag: deleted.
1368          */
1369         public boolean deleted;
1370         /**
1371          * Message flag: junk.
1372          */
1373         public boolean junk;
1374         /**
1375          * Message flag: flagged.
1376          */
1377         public boolean flagged;
1378         /**
1379          * Message flag: recent.
1380          */
1381         public boolean recent;
1382         /**
1383          * Message flag: draft.
1384          */
1385         public boolean draft;
1386         /**
1387          * Message flag: answered.
1388          */
1389         public boolean answered;
1390         /**
1391          * Message flag: fowarded.
1392          */
1393         public boolean forwarded;
1394 
1395         /**
1396          * Unparsed message content.
1397          */
1398         protected byte[] mimeContent;
1399 
1400         /**
1401          * Message content parsed in a MIME message.
1402          */
1403         protected MimeMessage mimeMessage;
1404 
1405         /**
1406          * Get permanent message id.
1407          * permanentUrl over WebDav or IitemId over EWS
1408          *
1409          * @return permanent id
1410          */
1411         public abstract String getPermanentId();
1412 
1413         /**
1414          * IMAP uid , unique in folder (x0e230003)
1415          *
1416          * @return IMAP uid
1417          */
1418         public long getImapUid() {
1419             return imapUid;
1420         }
1421 
1422         /**
1423          * Set IMAP uid.
1424          *
1425          * @param imapUid new uid
1426          */
1427         public void setImapUid(long imapUid) {
1428             this.imapUid = imapUid;
1429         }
1430 
1431         /**
1432          * Exchange uid.
1433          *
1434          * @return uid
1435          */
1436         public String getUid() {
1437             return uid;
1438         }
1439 
1440         /**
1441          * Return message flags in IMAP format.
1442          *
1443          * @return IMAP flags
1444          */
1445         public String getImapFlags() {
1446             StringBuilder buffer = new StringBuilder();
1447             if (read) {
1448                 buffer.append("\\Seen ");
1449             }
1450             if (deleted) {
1451                 buffer.append("\\Deleted ");
1452             }
1453             if (recent) {
1454                 buffer.append("\\Recent ");
1455             }
1456             if (flagged) {
1457                 buffer.append("\\Flagged ");
1458             }
1459             if (junk) {
1460                 buffer.append("Junk ");
1461             }
1462             if (draft) {
1463                 buffer.append("\\Draft ");
1464             }
1465             if (answered) {
1466                 buffer.append("\\Answered ");
1467             }
1468             if (forwarded) {
1469                 buffer.append("$Forwarded ");
1470             }
1471             if (keywords != null) {
1472                 for (String keyword : keywords.split(",")) {
1473                     buffer.append(encodeKeyword(convertKeywordToFlag(keyword))).append(" ");
1474                 }
1475             }
1476             return buffer.toString().trim();
1477         }
1478 
1479         /**
1480          * Load message content in a Mime message
1481          *
1482          * @throws IOException        on error
1483          * @throws MessagingException on error
1484          */
1485         public void loadMimeMessage() throws IOException, MessagingException {
1486             if (mimeMessage == null) {
1487                 // try to get message content from cache
1488                 if (this.imapUid == messageList.cachedMessageImapUid
1489                         // make sure we never return null even with broken 0 uid message
1490                         && messageList.cachedMimeContent != null
1491                         && messageList.cachedMimeMessage != null) {
1492                     mimeContent = messageList.cachedMimeContent;
1493                     mimeMessage = messageList.cachedMimeMessage;
1494                     LOGGER.debug("Got message content for " + imapUid + " from cache");
1495                 } else {
1496                     // load and parse message
1497                     mimeContent = getContent(this);
1498                     mimeMessage = new MimeMessage(null, new SharedByteArrayInputStream(mimeContent));
1499                     // workaround for Exchange 2003 ActiveSync bug
1500                     if (mimeMessage.getHeader("MAIL FROM") != null) {
1501                         // find start of actual message
1502                         byte[] mimeContentCopy = new byte[((SharedByteArrayInputStream) mimeMessage.getRawInputStream()).available()];
1503                         int offset = mimeContent.length - mimeContentCopy.length;
1504                         // remove unwanted header
1505                         System.arraycopy(mimeContent, offset, mimeContentCopy, 0, mimeContentCopy.length);
1506                         mimeContent = mimeContentCopy;
1507                         mimeMessage = new MimeMessage(null, new SharedByteArrayInputStream(mimeContent));
1508                     }
1509                     LOGGER.debug("Downloaded full message content for IMAP UID " + imapUid + " (" + mimeContent.length + " bytes)");
1510                 }
1511             }
1512         }
1513 
1514         /**
1515          * Get message content as a Mime message.
1516          *
1517          * @return mime message
1518          * @throws IOException        on error
1519          * @throws MessagingException on error
1520          */
1521         public MimeMessage getMimeMessage() throws IOException, MessagingException {
1522             loadMimeMessage();
1523             return mimeMessage;
1524         }
1525 
1526         public Enumeration<?> getMatchingHeaderLinesFromHeaders(String[] headerNames) throws MessagingException {
1527             Enumeration<?> result = null;
1528             if (mimeMessage == null) {
1529                 // message not loaded, try to get headers only
1530                 InputStream headers = getMimeHeaders();
1531                 if (headers != null) {
1532                     InternetHeaders internetHeaders = new InternetHeaders(headers);
1533                     if (internetHeaders.getHeader("Subject") == null) {
1534                         // invalid header content
1535                         return null;
1536                     }
1537                     if (headerNames == null) {
1538                         result = internetHeaders.getAllHeaderLines();
1539                     } else {
1540                         result = internetHeaders.getMatchingHeaderLines(headerNames);
1541                     }
1542                 }
1543             }
1544             return result;
1545         }
1546 
1547         public Enumeration<?> getMatchingHeaderLines(String[] headerNames) throws MessagingException, IOException {
1548             Enumeration<?> result = getMatchingHeaderLinesFromHeaders(headerNames);
1549             if (result == null) {
1550                 if (headerNames == null) {
1551                     result = getMimeMessage().getAllHeaderLines();
1552                 } else {
1553                     result = getMimeMessage().getMatchingHeaderLines(headerNames);
1554                 }
1555 
1556             }
1557             return result;
1558         }
1559 
1560         protected abstract InputStream getMimeHeaders();
1561 
1562         /**
1563          * Get message body size.
1564          *
1565          * @return mime message size
1566          * @throws IOException        on error
1567          * @throws MessagingException on error
1568          */
1569         public int getMimeMessageSize() throws IOException, MessagingException {
1570             loadMimeMessage();
1571             return mimeContent.length;
1572         }
1573 
1574         /**
1575          * Get message body input stream.
1576          *
1577          * @return mime message InputStream
1578          * @throws IOException        on error
1579          * @throws MessagingException on error
1580          */
1581         public InputStream getRawInputStream() throws IOException, MessagingException {
1582             loadMimeMessage();
1583             return new SharedByteArrayInputStream(mimeContent);
1584         }
1585 
1586 
1587         /**
1588          * Drop mime message to avoid keeping message content in memory,
1589          * keep a single message in MessageList cache to handle chunked fetch.
1590          */
1591         public void dropMimeMessage() {
1592             // update single message cache
1593             if (mimeMessage != null) {
1594                 messageList.cachedMessageImapUid = imapUid;
1595                 messageList.cachedMimeContent = mimeContent;
1596                 messageList.cachedMimeMessage = mimeMessage;
1597             }
1598             // drop curent message body to save memory
1599             mimeMessage = null;
1600             mimeContent = null;
1601         }
1602 
1603         public boolean isLoaded() {
1604             // check and retrieve cached content
1605             if (imapUid == messageList.cachedMessageImapUid) {
1606                 mimeContent = messageList.cachedMimeContent;
1607                 mimeMessage = messageList.cachedMimeMessage;
1608             }
1609             return mimeMessage != null;
1610         }
1611 
1612         /**
1613          * Delete message.
1614          *
1615          * @throws IOException on error
1616          */
1617         public void delete() throws IOException {
1618             deleteMessage(this);
1619         }
1620 
1621         /**
1622          * Move message to trash, mark message read.
1623          *
1624          * @throws IOException on error
1625          */
1626         public void moveToTrash() throws IOException {
1627             markRead();
1628 
1629             ExchangeSession.this.moveToTrash(this);
1630         }
1631 
1632         /**
1633          * Mark message as read.
1634          *
1635          * @throws IOException on error
1636          */
1637         public void markRead() throws IOException {
1638             HashMap<String, String> properties = new HashMap<>();
1639             properties.put("read", "1");
1640             updateMessage(this, properties);
1641         }
1642 
1643         /**
1644          * Comparator to sort messages by IMAP uid
1645          *
1646          * @param message other message
1647          * @return imapUid comparison result
1648          */
1649         public int compareTo(Message message) {
1650             long compareValue = (imapUid - message.imapUid);
1651             if (compareValue > 0) {
1652                 return 1;
1653             } else if (compareValue < 0) {
1654                 return -1;
1655             } else {
1656                 return 0;
1657             }
1658         }
1659 
1660         /**
1661          * Override equals, compare IMAP uids
1662          *
1663          * @param message other message
1664          * @return true if IMAP uids are equal
1665          */
1666         @Override
1667         public boolean equals(Object message) {
1668             return message instanceof Message && imapUid == ((Message) message).imapUid;
1669         }
1670 
1671         /**
1672          * Override hashCode, return imapUid hashcode.
1673          *
1674          * @return imapUid hashcode
1675          */
1676         @Override
1677         public int hashCode() {
1678             return (int) (imapUid ^ (imapUid >>> 32));
1679         }
1680 
1681         public String removeFlag(String flag) {
1682             if (keywords != null) {
1683                 final String exchangeFlag = convertFlagToKeyword(flag);
1684                 Set<String> keywordSet = new HashSet<>();
1685                 String[] keywordArray = keywords.split(",");
1686                 for (String value : keywordArray) {
1687                     if (!value.equalsIgnoreCase(exchangeFlag)) {
1688                         keywordSet.add(value);
1689                     }
1690                 }
1691                 keywords = StringUtil.join(keywordSet, ",");
1692             }
1693             return keywords;
1694         }
1695 
1696         public String addFlag(String flag) {
1697             final String exchangeFlag = convertFlagToKeyword(flag);
1698             HashSet<String> keywordSet = new HashSet<>();
1699             boolean hasFlag = false;
1700             if (keywords != null) {
1701                 String[] keywordArray = keywords.split(",");
1702                 for (String value : keywordArray) {
1703                     keywordSet.add(value);
1704                     if (value.equalsIgnoreCase(exchangeFlag)) {
1705                         hasFlag = true;
1706                     }
1707                 }
1708             }
1709             if (!hasFlag) {
1710                 keywordSet.add(exchangeFlag);
1711             }
1712             keywords = StringUtil.join(keywordSet, ",");
1713             return keywords;
1714         }
1715 
1716         public String setFlags(HashSet<String> flags) {
1717             keywords = convertFlagsToKeywords(flags);
1718             return keywords;
1719         }
1720 
1721     }
1722 
1723     /**
1724      * Message list, includes a single messsage cache
1725      */
1726     public static class MessageList extends ArrayList<Message> {
1727         /**
1728          * Cached message content parsed in a MIME message.
1729          */
1730         protected transient MimeMessage cachedMimeMessage;
1731         /**
1732          * Cached message uid.
1733          */
1734         protected transient long cachedMessageImapUid;
1735         /**
1736          * Cached unparsed message
1737          */
1738         protected transient byte[] cachedMimeContent;
1739 
1740     }
1741 
1742     /**
1743      * Generic folder item.
1744      */
1745     public abstract static class Item extends HashMap<String, String> {
1746         protected String folderPath;
1747         protected String itemName;
1748         protected String permanentUrl;
1749         /**
1750          * Display name.
1751          */
1752         public String displayName;
1753         /**
1754          * item etag
1755          */
1756         public String etag;
1757         protected String noneMatch;
1758 
1759         /**
1760          * Build item instance.
1761          *
1762          * @param folderPath folder path
1763          * @param itemName   item name class
1764          * @param etag       item etag
1765          * @param noneMatch  none match flag
1766          */
1767         public Item(String folderPath, String itemName, String etag, String noneMatch) {
1768             this.folderPath = folderPath;
1769             this.itemName = itemName;
1770             this.etag = etag;
1771             this.noneMatch = noneMatch;
1772         }
1773 
1774         /**
1775          * Default constructor.
1776          */
1777         protected Item() {
1778         }
1779 
1780         /**
1781          * Return item content type
1782          *
1783          * @return content type
1784          */
1785         public abstract String getContentType();
1786 
1787         /**
1788          * Retrieve item body from Exchange
1789          *
1790          * @return item body
1791          * @throws IOException on error
1792          */
1793         public abstract String getBody() throws IOException;
1794 
1795         /**
1796          * Get event name (file name part in URL).
1797          *
1798          * @return event name
1799          */
1800         public String getName() {
1801             return itemName;
1802         }
1803 
1804         /**
1805          * Get event etag (last change tag).
1806          *
1807          * @return event etag
1808          */
1809         public String getEtag() {
1810             return etag;
1811         }
1812 
1813         /**
1814          * Set item href.
1815          *
1816          * @param href item href
1817          */
1818         public void setHref(String href) {
1819             int index = href.lastIndexOf('/');
1820             if (index >= 0) {
1821                 folderPath = href.substring(0, index);
1822                 itemName = href.substring(index + 1);
1823             } else {
1824                 throw new IllegalArgumentException(href);
1825             }
1826         }
1827 
1828         /**
1829          * Return item href.
1830          *
1831          * @return item href
1832          */
1833         public String getHref() {
1834             return folderPath + '/' + itemName;
1835         }
1836 
1837         public void setItemName(String itemName) {
1838             this.itemName = itemName;
1839         }
1840     }
1841 
1842     /**
1843      * Contact object
1844      */
1845     public abstract class Contact extends Item {
1846 
1847         protected ArrayList<String> distributionListMembers = null;
1848         protected String vCardVersion;
1849 
1850         public Contact(String folderPath, String itemName, Map<String, String> properties, String etag, String noneMatch) {
1851             super(folderPath, itemName.endsWith(".vcf") ? itemName.substring(0, itemName.length() - 3) + "EML" : itemName, etag, noneMatch);
1852             this.putAll(properties);
1853         }
1854 
1855         protected Contact() {
1856         }
1857 
1858         public void setVCardVersion(String vCardVersion) {
1859             this.vCardVersion = vCardVersion;
1860         }
1861 
1862         public abstract ItemResult createOrUpdate() throws IOException;
1863 
1864         /**
1865          * Convert EML extension to vcf.
1866          *
1867          * @return item name
1868          */
1869         @Override
1870         public String getName() {
1871             String name = super.getName();
1872             if (name.endsWith(".EML")) {
1873                 name = name.substring(0, name.length() - 3) + "vcf";
1874             }
1875             return name;
1876         }
1877 
1878         /**
1879          * Set contact name
1880          *
1881          * @param name contact name
1882          */
1883         public void setName(String name) {
1884             this.itemName = name;
1885         }
1886 
1887         /**
1888          * Compute vcard uid from name.
1889          *
1890          * @return uid
1891          */
1892         public String getUid() {
1893             String uid = getName();
1894             int dotIndex = uid.lastIndexOf('.');
1895             if (dotIndex > 0) {
1896                 uid = uid.substring(0, dotIndex);
1897             }
1898             return URIUtil.encodePath(uid);
1899         }
1900 
1901         @Override
1902         public String getContentType() {
1903             return "text/vcard";
1904         }
1905 
1906         public void addMember(String member) {
1907             if (distributionListMembers == null) {
1908                 distributionListMembers = new ArrayList<>();
1909             }
1910             distributionListMembers.add(member);
1911         }
1912 
1913 
1914         @Override
1915         public String getBody() {
1916             // build RFC 2426 VCard from contact information
1917             VCardWriter writer = new VCardWriter();
1918             writer.startCard(vCardVersion);
1919             writer.appendProperty("UID", getUid());
1920             // common name
1921             String cn = get("cn");
1922             if (cn == null) {
1923                 cn = get("displayname");
1924             }
1925             String sn = get("sn");
1926             if (sn == null) {
1927                 sn = cn;
1928             }
1929             writer.appendProperty("FN", cn);
1930             // RFC 2426: Family Name, Given Name, Additional Names, Honorific Prefixes, and Honorific Suffixes
1931             writer.appendProperty("N", sn, get("givenName"), get("middlename"), get("personaltitle"), get("namesuffix"));
1932 
1933             if (distributionListMembers != null) {
1934                 writer.appendProperty("KIND", "group");
1935                 for (String member : distributionListMembers) {
1936                     writer.appendProperty("MEMBER", member);
1937                 }
1938             }
1939 
1940             writer.appendProperty("TEL;TYPE=cell", get("mobile"));
1941             writer.appendProperty("TEL;TYPE=work", get("telephoneNumber"));
1942             writer.appendProperty("TEL;TYPE=home", get("homePhone"));
1943             writer.appendProperty("TEL;TYPE=fax", get("facsimiletelephonenumber"));
1944             writer.appendProperty("TEL;TYPE=pager", get("pager"));
1945             writer.appendProperty("TEL;TYPE=car", get("othermobile"));
1946             writer.appendProperty("TEL;TYPE=home,fax", get("homefax"));
1947             writer.appendProperty("TEL;TYPE=isdn", get("internationalisdnnumber"));
1948             writer.appendProperty("TEL;TYPE=msg", get("otherTelephone"));
1949 
1950             // The structured type value corresponds, in sequence, to the post office box; the extended address;
1951             // the street address; the locality (e.g., city); the region (e.g., state or province);
1952             // the postal code; the country name
1953             writer.appendProperty("ADR;TYPE=home",
1954                     get("homepostofficebox"), null, get("homeStreet"), get("homeCity"), get("homeState"), get("homePostalCode"), get("homeCountry"));
1955             writer.appendProperty("ADR;TYPE=work",
1956                     get("postofficebox"), get("roomnumber"), get("street"), get("l"), get("st"), get("postalcode"), get("co"));
1957             writer.appendProperty("ADR;TYPE=other",
1958                     get("otherpostofficebox"), null, get("otherstreet"), get("othercity"), get("otherstate"), get("otherpostalcode"), get("othercountry"));
1959 
1960             writer.appendProperty("EMAIL;TYPE=work", get("smtpemail1"));
1961             writer.appendProperty("EMAIL;TYPE=home", get("smtpemail2"));
1962             writer.appendProperty("EMAIL;TYPE=other", get("smtpemail3"));
1963 
1964             writer.appendProperty("ORG", get("o"), get("department"));
1965             writer.appendProperty("URL;TYPE=work", get("businesshomepage"));
1966             writer.appendProperty("URL;TYPE=home", get("personalHomePage"));
1967             writer.appendProperty("TITLE", get("title"));
1968             writer.appendProperty("NOTE", get("description"));
1969 
1970             writer.appendProperty("CUSTOM1", get("extensionattribute1"));
1971             writer.appendProperty("CUSTOM2", get("extensionattribute2"));
1972             writer.appendProperty("CUSTOM3", get("extensionattribute3"));
1973             writer.appendProperty("CUSTOM4", get("extensionattribute4"));
1974 
1975             writer.appendProperty("ROLE", get("profession"));
1976             writer.appendProperty("NICKNAME", get("nickname"));
1977             writer.appendProperty("X-AIM", get("im"));
1978 
1979             writer.appendProperty("BDAY", convertZuluDateToBday(get("bday")));
1980             writer.appendProperty("ANNIVERSARY", convertZuluDateToBday(get("anniversary")));
1981 
1982             String gender = get("gender");
1983             if ("1".equals(gender)) {
1984                 writer.appendProperty("SEX", "2");
1985             } else if ("2".equals(gender)) {
1986                 writer.appendProperty("SEX", "1");
1987             }
1988 
1989             writer.appendProperty("CATEGORIES", get("keywords"));
1990 
1991             writer.appendProperty("FBURL", get("fburl"));
1992 
1993             if ("1".equals(get("private"))) {
1994                 writer.appendProperty("CLASS", "PRIVATE");
1995             }
1996 
1997             writer.appendProperty("X-ASSISTANT", get("secretarycn"));
1998             writer.appendProperty("X-MANAGER", get("manager"));
1999             writer.appendProperty("X-SPOUSE", get("spousecn"));
2000 
2001             writer.appendProperty("REV", get("lastmodified"));
2002 
2003             ContactPhoto contactPhoto = null;
2004 
2005             if (Settings.getBooleanProperty("davmail.carddavReadPhoto", true)) {
2006                 if (("true".equals(get("haspicture")))) {
2007                     try {
2008                         contactPhoto = getContactPhoto(this);
2009                     } catch (IOException e) {
2010                         LOGGER.warn("Unable to get photo from contact " + this.get("cn"));
2011                     }
2012                 }
2013 
2014                 if (contactPhoto == null) {
2015                     contactPhoto = getADPhoto(get("smtpemail1"));
2016                 }
2017             }
2018 
2019             if (contactPhoto != null) {
2020                 writer.writeLine("PHOTO;TYPE=" + contactPhoto.contentType + ";ENCODING=BASE64:");
2021                 writer.writeLine(contactPhoto.content, true);
2022             }
2023 
2024             writer.appendProperty("KEY1;X509;ENCODING=BASE64", get("msexchangecertificate"));
2025             writer.appendProperty("KEY2;X509;ENCODING=BASE64", get("usersmimecertificate"));
2026 
2027             writer.endCard();
2028             return writer.toString();
2029         }
2030     }
2031 
2032     /**
2033      * Calendar event object.
2034      */
2035     public abstract class Event extends Item {
2036         protected String contentClass;
2037         protected String subject;
2038         protected VCalendar vCalendar;
2039 
2040         public Event(String folderPath, String itemName, String contentClass, String itemBody, String etag, String noneMatch) throws IOException {
2041             super(folderPath, itemName, etag, noneMatch);
2042             this.contentClass = contentClass;
2043             fixICS(itemBody.getBytes(StandardCharsets.UTF_8), false);
2044             // fix task item name
2045             if (vCalendar.isTodo() && this.itemName.endsWith(".ics")) {
2046                 this.itemName = itemName.substring(0, itemName.length() - 3) + "EML";
2047             }
2048         }
2049 
2050         protected Event() {
2051         }
2052 
2053         @Override
2054         public String getContentType() {
2055             return "text/calendar;charset=UTF-8";
2056         }
2057 
2058         @Override
2059         public String getBody() throws IOException {
2060             if (vCalendar == null) {
2061                 fixICS(getEventContent(), true);
2062             }
2063             return vCalendar.toString();
2064         }
2065 
2066         protected HttpNotFoundException buildHttpNotFoundException(Exception e) {
2067             String message = "Unable to get event " + getName() + " subject: " + subject + " at " + permanentUrl + ": " + e.getMessage();
2068             LOGGER.warn(message);
2069             return new HttpNotFoundException(message);
2070         }
2071 
2072         /**
2073          * Retrieve item body from Exchange
2074          *
2075          * @return item content
2076          * @throws IOException on error
2077          */
2078         public abstract byte[] getEventContent() throws IOException;
2079 
2080         protected static final String TEXT_CALENDAR = "text/calendar";
2081         protected static final String APPLICATION_ICS = "application/ics";
2082 
2083         protected boolean isCalendarContentType(String contentType) {
2084             return TEXT_CALENDAR.regionMatches(true, 0, contentType, 0, TEXT_CALENDAR.length()) ||
2085                     APPLICATION_ICS.regionMatches(true, 0, contentType, 0, APPLICATION_ICS.length());
2086         }
2087 
2088         protected MimePart getCalendarMimePart(MimeMultipart multiPart) throws IOException, MessagingException {
2089             MimePart bodyPart = null;
2090             for (int i = 0; i < multiPart.getCount(); i++) {
2091                 String contentType = multiPart.getBodyPart(i).getContentType();
2092                 if (isCalendarContentType(contentType)) {
2093                     bodyPart = (MimePart) multiPart.getBodyPart(i);
2094                     break;
2095                 } else if (contentType.startsWith("multipart")) {
2096                     Object content = multiPart.getBodyPart(i).getContent();
2097                     if (content instanceof MimeMultipart) {
2098                         bodyPart = getCalendarMimePart((MimeMultipart) content);
2099                     }
2100                 }
2101             }
2102 
2103             return bodyPart;
2104         }
2105 
2106         /**
2107          * Load ICS content from MIME message input stream
2108          *
2109          * @param mimeInputStream mime message input stream
2110          * @return mime message ics attachment body
2111          * @throws IOException        on error
2112          * @throws MessagingException on error
2113          */
2114         protected byte[] getICS(InputStream mimeInputStream) throws IOException, MessagingException {
2115             byte[] result;
2116             MimeMessage mimeMessage = new MimeMessage(null, mimeInputStream);
2117             String[] contentClassHeader = mimeMessage.getHeader("Content-class");
2118             // task item, return null
2119             if (contentClassHeader != null && contentClassHeader.length > 0 && "urn:content-classes:task".equals(contentClassHeader[0])) {
2120                 return null;
2121             }
2122             Object mimeBody = mimeMessage.getContent();
2123             MimePart bodyPart = null;
2124             if (mimeBody instanceof MimeMultipart) {
2125                 bodyPart = getCalendarMimePart((MimeMultipart) mimeBody);
2126             } else if (isCalendarContentType(mimeMessage.getContentType())) {
2127                 // no multipart, single body
2128                 bodyPart = mimeMessage;
2129             }
2130 
2131             if (bodyPart != null) {
2132                 ByteArrayOutputStream baos = new ByteArrayOutputStream();
2133                 bodyPart.getDataHandler().writeTo(baos);
2134                 baos.close();
2135                 result = baos.toByteArray();
2136             } else {
2137                 ByteArrayOutputStream baos = new ByteArrayOutputStream();
2138                 mimeMessage.writeTo(baos);
2139                 baos.close();
2140                 throw new DavMailException("EXCEPTION_INVALID_MESSAGE_CONTENT", new String(baos.toByteArray(), StandardCharsets.UTF_8));
2141             }
2142             return result;
2143         }
2144 
2145         protected void fixICS(byte[] icsContent, boolean fromServer) throws IOException {
2146             if (LOGGER.isDebugEnabled() && fromServer) {
2147                 dumpIndex++;
2148                 String icsBody = new String(icsContent, StandardCharsets.UTF_8);
2149                 ICSCalendarValidator.ValidationResult vr = ICSCalendarValidator.validateWithDetails(icsBody);
2150                 dumpICS(icsBody, true, false);
2151                 LOGGER.debug("Vcalendar body ValidationResult: "+ vr.isValid() +" "+ vr.showReason());
2152                 LOGGER.debug("Vcalendar body received from server:\n" + icsBody);
2153             }
2154             vCalendar = new VCalendar(icsContent, getEmail(), getVTimezone());
2155             vCalendar.fixVCalendar(fromServer);
2156             if (LOGGER.isDebugEnabled() && !fromServer) {
2157                 String resultString = vCalendar.toString();
2158                 ICSCalendarValidator.ValidationResult vr = ICSCalendarValidator.validateWithDetails(resultString);
2159                 LOGGER.debug("Fixed Vcalendar body ValidationResult: "+ vr.isValid() +" "+ vr.showReason());
2160                 LOGGER.debug("Fixed Vcalendar body to server:\n" + resultString);
2161                 dumpICS(resultString, false, true);
2162             }
2163         }
2164 
2165         protected void dumpICS(String icsBody, boolean fromServer, boolean after) {
2166             String logFileDirectory = Settings.getLogFileDirectory();
2167 
2168             // additional setting to activate ICS dump (not available in GUI)
2169             int dumpMax = Settings.getIntProperty("davmail.dumpICS");
2170             if (dumpMax > 0) {
2171                 if (dumpIndex > dumpMax) {
2172                     // Delete the oldest dump file
2173                     final int oldest = dumpIndex - dumpMax;
2174                     try {
2175                         File[] oldestFiles = (new File(logFileDirectory)).listFiles((dir, name) -> {
2176                             if (name.endsWith(".ics")) {
2177                                 int dashIndex = name.indexOf('-');
2178                                 if (dashIndex > 0) {
2179                                     try {
2180                                         int fileIndex = Integer.parseInt(name.substring(0, dashIndex));
2181                                         return fileIndex < oldest;
2182                                     } catch (NumberFormatException nfe) {
2183                                         // ignore
2184                                     }
2185                                 }
2186                             }
2187                             return false;
2188                         });
2189                         if (oldestFiles != null) {
2190                             for (File file : oldestFiles) {
2191                                 if (!file.delete()) {
2192                                     LOGGER.warn("Unable to delete " + file.getAbsolutePath());
2193                                 }
2194                             }
2195                         }
2196                     } catch (Exception ex) {
2197                         LOGGER.warn("Error deleting ics dump: " + ex.getMessage());
2198                     }
2199                 }
2200 
2201                 StringBuilder filePath = new StringBuilder();
2202                 filePath.append(logFileDirectory).append('/')
2203                         .append(dumpIndex)
2204                         .append(after ? "-to" : "-from")
2205                         .append((after ^ fromServer) ? "-server" : "-client")
2206                         .append(".ics");
2207                 if ((icsBody != null) && (icsBody.length() > 0)) {
2208                     OutputStreamWriter writer = null;
2209                     try {
2210                         writer = new OutputStreamWriter(new FileOutputStream(filePath.toString()), StandardCharsets.UTF_8);
2211                         writer.write(icsBody);
2212                     } catch (IOException e) {
2213                         LOGGER.error(e);
2214                     } finally {
2215                         if (writer != null) {
2216                             try {
2217                                 writer.close();
2218                             } catch (IOException e) {
2219                                 LOGGER.error(e);
2220                             }
2221                         }
2222                     }
2223 
2224 
2225                 }
2226             }
2227 
2228         }
2229 
2230         /**
2231          * Build Mime body for event or event message.
2232          *
2233          * @return mimeContent as byte array or null
2234          * @throws IOException on error
2235          */
2236         public byte[] createMimeContent() throws IOException {
2237             String boundary = UUID.randomUUID().toString();
2238             ByteArrayOutputStream baos = new ByteArrayOutputStream();
2239             MimeOutputStreamWriter writer = new MimeOutputStreamWriter(baos);
2240 
2241             writer.writeHeader("Content-Transfer-Encoding", "7bit");
2242             writer.writeHeader("Content-class", contentClass);
2243             // append date
2244             writer.writeHeader("Date", new Date());
2245 
2246             // Make sure invites have a proper subject line
2247             String vEventSubject = vCalendar.getFirstVeventPropertyValue("SUMMARY");
2248             if (vEventSubject == null) {
2249                 vEventSubject = BundleMessage.format("MEETING_REQUEST");
2250             }
2251 
2252             // Write a part of the message that contains the
2253             // ICS description so that invites contain the description text
2254             String description = vCalendar.getFirstVeventPropertyValue("DESCRIPTION");
2255 
2256             // handle notifications
2257             if ("urn:content-classes:calendarmessage".equals(contentClass)) {
2258                 // need to parse attendees and organizer to build recipients
2259                 VCalendar.Recipients recipients = vCalendar.getRecipients(true);
2260                 String to;
2261                 String cc;
2262                 String notificationSubject;
2263                 if (email.equalsIgnoreCase(recipients.organizer)) {
2264                     // current user is organizer => notify all
2265                     to = recipients.attendees;
2266                     cc = recipients.optionalAttendees;
2267                     notificationSubject = subject;
2268                 } else {
2269                     String status = vCalendar.getAttendeeStatus();
2270                     // notify only organizer
2271                     to = recipients.organizer;
2272                     cc = null;
2273                     notificationSubject = (status != null) ? (BundleMessage.format(status) + vEventSubject) : subject;
2274                     description = "";
2275                 }
2276 
2277                 // Allow end user notification edit
2278                 if (Settings.getBooleanProperty("davmail.caldavEditNotifications")) {
2279                     // create notification edit dialog
2280                     NotificationDialog notificationDialog = new NotificationDialog(to,
2281                             cc, notificationSubject, description);
2282                     if (!notificationDialog.getSendNotification()) {
2283                         LOGGER.debug("Notification canceled by user");
2284                         return null;
2285                     }
2286                     // get description from dialog
2287                     to = notificationDialog.getTo();
2288                     cc = notificationDialog.getCc();
2289                     notificationSubject = notificationDialog.getSubject();
2290                     description = notificationDialog.getBody();
2291                 }
2292 
2293                 // do not send notification if no recipients found
2294                 if ((to == null || to.length() == 0) && (cc == null || cc.length() == 0)) {
2295                     return null;
2296                 }
2297 
2298                 writer.writeHeader("To", to);
2299                 writer.writeHeader("Cc", cc);
2300                 writer.writeHeader("Subject", notificationSubject);
2301 
2302 
2303                 if (LOGGER.isDebugEnabled()) {
2304                     StringBuilder logBuffer = new StringBuilder("Sending notification ");
2305                     if (to != null) {
2306                         logBuffer.append("to: ").append(to);
2307                     }
2308                     if (cc != null) {
2309                         logBuffer.append("cc: ").append(cc);
2310                     }
2311                     LOGGER.debug(logBuffer.toString());
2312                 }
2313             } else {
2314                 // need to parse attendees and organizer to build recipients
2315                 VCalendar.Recipients recipients = vCalendar.getRecipients(false);
2316                 // storing appointment, full recipients header
2317                 if (recipients.attendees != null) {
2318                     writer.writeHeader("To", recipients.attendees);
2319                 } else {
2320                     // use current user as attendee
2321                     writer.writeHeader("To", email);
2322                 }
2323                 writer.writeHeader("Cc", recipients.optionalAttendees);
2324 
2325                 if (recipients.organizer != null) {
2326                     writer.writeHeader("From", recipients.organizer);
2327                 } else {
2328                     writer.writeHeader("From", email);
2329                 }
2330             }
2331             if (vCalendar.getMethod() == null) {
2332                 vCalendar.setPropertyValue("METHOD", "REQUEST");
2333             }
2334             writer.writeHeader("MIME-Version", "1.0");
2335             writer.writeHeader("Content-Type", "multipart/alternative;\r\n" +
2336                     "\tboundary=\"----=_NextPart_" + boundary + '\"');
2337             writer.writeLn();
2338             writer.writeLn("This is a multi-part message in MIME format.");
2339             writer.writeLn();
2340             writer.writeLn("------=_NextPart_" + boundary);
2341 
2342             if (description != null && description.length() > 0) {
2343                 writer.writeHeader("Content-Type", "text/plain;\r\n" +
2344                         "\tcharset=\"utf-8\"");
2345                 writer.writeHeader("content-transfer-encoding", "8bit");
2346                 writer.writeLn();
2347                 writer.flush();
2348                 baos.write(description.getBytes(StandardCharsets.UTF_8));
2349                 writer.writeLn();
2350                 writer.writeLn("------=_NextPart_" + boundary);
2351             }
2352             writer.writeHeader("Content-class", contentClass);
2353             writer.writeHeader("Content-Type", "text/calendar;\r\n" +
2354                     "\tmethod=" + vCalendar.getMethod() + ";\r\n" +
2355                     "\tcharset=\"utf-8\""
2356             );
2357             writer.writeHeader("Content-Transfer-Encoding", "8bit");
2358             writer.writeLn();
2359             writer.flush();
2360             baos.write(vCalendar.toString().getBytes(StandardCharsets.UTF_8));
2361             writer.writeLn();
2362             writer.writeLn("------=_NextPart_" + boundary + "--");
2363             writer.close();
2364             return baos.toByteArray();
2365         }
2366 
2367         /**
2368          * Create or update item
2369          *
2370          * @return action result
2371          * @throws IOException on error
2372          */
2373         public abstract ItemResult createOrUpdate() throws IOException;
2374 
2375     }
2376 
2377     protected abstract Set<String> getItemProperties();
2378 
2379     /**
2380      * Search contacts in provided folder.
2381      *
2382      * @param folderPath Exchange folder path
2383      * @param includeDistList include distribution lists
2384      * @return list of contacts
2385      * @throws IOException on error
2386      */
2387     public List<ExchangeSession.Contact> getAllContacts(String folderPath, boolean includeDistList) throws IOException {
2388         return searchContacts(folderPath, ExchangeSession.CONTACT_ATTRIBUTES, isEqualTo("outlookmessageclass", "IPM.Contact"), 0);
2389     }
2390 
2391 
2392     /**
2393      * Search contacts in provided folder matching the search query.
2394      *
2395      * @param folderPath Exchange folder path
2396      * @param attributes requested attributes
2397      * @param condition  Exchange search query
2398      * @param maxCount   maximum item count
2399      * @return list of contacts
2400      * @throws IOException on error
2401      */
2402     public abstract List<Contact> searchContacts(String folderPath, Set<String> attributes, Condition condition, int maxCount) throws IOException;
2403 
2404     /**
2405      * Search calendar messages in provided folder.
2406      *
2407      * @param folderPath Exchange folder path
2408      * @return list of calendar messages as Event objects
2409      * @throws IOException on error
2410      */
2411     public abstract List<Event> getEventMessages(String folderPath) throws IOException;
2412 
2413     /**
2414      * Search calendar events in provided folder.
2415      *
2416      * @param folderPath Exchange folder path
2417      * @return list of calendar events
2418      * @throws IOException on error
2419      */
2420     public List<Event> getAllEvents(String folderPath) throws IOException {
2421         List<Event> results = searchEvents(folderPath, getCalendarItemCondition(getPastDelayCondition("dtstart")));
2422 
2423         if (!Settings.getBooleanProperty("davmail.caldavDisableTasks", false) && isMainCalendar(folderPath)) {
2424             // retrieve tasks from main tasks folder
2425             results.addAll(searchTasksOnly(TASKS));
2426         }
2427 
2428         return results;
2429     }
2430 
2431     protected abstract Condition getCalendarItemCondition(Condition dateCondition);
2432 
2433     protected Condition getPastDelayCondition(String attribute) {
2434         int caldavPastDelay = Settings.getIntProperty("davmail.caldavPastDelay");
2435         Condition dateCondition = null;
2436         if (caldavPastDelay != 0) {
2437             Calendar cal = Calendar.getInstance();
2438             cal.add(Calendar.DAY_OF_MONTH, -caldavPastDelay);
2439             dateCondition = gt(attribute, formatSearchDate(cal.getTime()));
2440         }
2441         return dateCondition;
2442     }
2443 
2444     protected Condition getRangeCondition(String timeRangeStart, String timeRangeEnd) throws IOException {
2445         try {
2446             SimpleDateFormat parser = getZuluDateFormat();
2447             ExchangeSession.MultiCondition andCondition = and();
2448             if (timeRangeStart != null) {
2449                 andCondition.add(gt("dtend", formatSearchDate(parser.parse(timeRangeStart))));
2450             }
2451             if (timeRangeEnd != null) {
2452                 andCondition.add(lt("dtstart", formatSearchDate(parser.parse(timeRangeEnd))));
2453             }
2454             return andCondition;
2455         } catch (ParseException e) {
2456             throw new IOException(e + " " + e.getMessage());
2457         }
2458     }
2459 
2460     /**
2461      * Search events between start and end.
2462      *
2463      * @param folderPath     Exchange folder path
2464      * @param timeRangeStart date range start in zulu format
2465      * @param timeRangeEnd   date range start in zulu format
2466      * @return list of calendar events
2467      * @throws IOException on error
2468      */
2469     public List<Event> searchEvents(String folderPath, String timeRangeStart, String timeRangeEnd) throws IOException {
2470         Condition dateCondition = getRangeCondition(timeRangeStart, timeRangeEnd);
2471         Condition condition = getCalendarItemCondition(dateCondition);
2472 
2473         return searchEvents(folderPath, condition);
2474     }
2475 
2476     /**
2477      * Search events between start and end, exclude tasks.
2478      *
2479      * @param folderPath     Exchange folder path
2480      * @param timeRangeStart date range start in zulu format
2481      * @param timeRangeEnd   date range start in zulu format
2482      * @return list of calendar events
2483      * @throws IOException on error
2484      */
2485     public List<Event> searchEventsOnly(String folderPath, String timeRangeStart, String timeRangeEnd) throws IOException {
2486         Condition dateCondition = getRangeCondition(timeRangeStart, timeRangeEnd);
2487         return searchEvents(folderPath, getCalendarItemCondition(dateCondition));
2488     }
2489 
2490     /**
2491      * Search tasks only (VTODO).
2492      *
2493      * @param folderPath Exchange folder path
2494      * @return list of tasks
2495      * @throws IOException on error
2496      */
2497     public List<Event> searchTasksOnly(String folderPath) throws IOException {
2498         return searchEvents(folderPath, and(isEqualTo("outlookmessageclass", "IPM.Task"),
2499                 or(isNull("datecompleted"), getPastDelayCondition("datecompleted"))));
2500     }
2501 
2502     /**
2503      * Search calendar events in provided folder.
2504      *
2505      * @param folderPath Exchange folder path
2506      * @param filter     search filter
2507      * @return list of calendar events
2508      * @throws IOException on error
2509      */
2510     public List<Event> searchEvents(String folderPath, Condition filter) throws IOException {
2511 
2512         Condition privateCondition = null;
2513         if (isSharedFolder(folderPath) && Settings.getBooleanProperty("davmail.excludePrivateEvents", true)) {
2514             LOGGER.debug("Shared or public calendar: exclude private events");
2515             privateCondition = isEqualTo("sensitivity", 0);
2516         }
2517 
2518         return searchEvents(folderPath, getItemProperties(),
2519                 and(filter, privateCondition));
2520     }
2521 
2522     /**
2523      * Search calendar events or messages in provided folder matching the search query.
2524      *
2525      * @param folderPath Exchange folder path
2526      * @param attributes requested attributes
2527      * @param condition  Exchange search query
2528      * @return list of calendar messages as Event objects
2529      * @throws IOException on error
2530      */
2531     public abstract List<Event> searchEvents(String folderPath, Set<String> attributes, Condition condition) throws IOException;
2532 
2533     /**
2534      * convert vcf extension to EML.
2535      *
2536      * @param itemName item name
2537      * @return EML item name
2538      */
2539     protected String convertItemNameToEML(String itemName) {
2540         if (itemName.endsWith(".vcf")) {
2541             return itemName.substring(0, itemName.length() - 3) + "EML";
2542         } else {
2543             return itemName;
2544         }
2545     }
2546 
2547     /**
2548      * Get item named eventName in folder
2549      *
2550      * @param folderPath Exchange folder path
2551      * @param itemName   event name
2552      * @return event object
2553      * @throws IOException on error
2554      */
2555     public abstract Item getItem(String folderPath, String itemName) throws IOException;
2556 
2557     /**
2558      * Contact picture
2559      */
2560     public static class ContactPhoto {
2561         /**
2562          * Contact picture content type (always image/jpeg on read)
2563          */
2564         public String contentType;
2565         /**
2566          * Base64 encoded picture content
2567          */
2568         public String content;
2569     }
2570 
2571     /**
2572      * Retrieve contact photo attached to contact
2573      *
2574      * @param contact address book contact
2575      * @return contact photo
2576      * @throws IOException on error
2577      */
2578     public abstract ContactPhoto getContactPhoto(Contact contact) throws IOException;
2579 
2580     /**
2581      * Retrieve contact photo from AD
2582      *
2583      * @param email address book contact
2584      * @return contact photo
2585      */
2586     public ContactPhoto getADPhoto(String email) {
2587         return null;
2588     }
2589 
2590     /**
2591      * Delete event named itemName in folder
2592      *
2593      * @param folderPath Exchange folder path
2594      * @param itemName   item name
2595      * @throws IOException on error
2596      */
2597     public abstract void deleteItem(String folderPath, String itemName) throws IOException;
2598 
2599     /**
2600      * Mark event processed named eventName in folder
2601      *
2602      * @param folderPath Exchange folder path
2603      * @param itemName   item name
2604      * @throws IOException on error
2605      */
2606     public abstract void processItem(String folderPath, String itemName) throws IOException;
2607 
2608 
2609     private static int dumpIndex;
2610 
2611     /**
2612      * Replace iCal4 (Snow Leopard) principal paths with mailto expression
2613      *
2614      * @param value attendee value or ics line
2615      * @return fixed value
2616      */
2617     protected String replaceIcal4Principal(String value) {
2618         if (value != null && value.contains("/principals/__uuids__/")) {
2619             return value.replaceAll("/principals/__uuids__/([^/]*)__AT__([^/]*)/", "mailto:$1@$2");
2620         } else {
2621             return value;
2622         }
2623     }
2624 
2625     /**
2626      * Event result object to hold HTTP status and event etag from an event creation/update.
2627      */
2628     public static class ItemResult {
2629         /**
2630          * HTTP status
2631          */
2632         public int status;
2633         /**
2634          * Event etag from response HTTP header
2635          */
2636         public String etag;
2637         /**
2638          * Created item name
2639          */
2640         public String itemName;
2641     }
2642 
2643     /**
2644      * Build and send the MIME message for the provided ICS event.
2645      *
2646      * @param icsBody event in iCalendar format
2647      * @return HTTP status
2648      * @throws IOException on error
2649      */
2650     public abstract int sendEvent(String icsBody) throws IOException;
2651 
2652     /**
2653      * Create or update item (event or contact) on the Exchange server
2654      *
2655      * @param folderPath Exchange folder path
2656      * @param itemName   event name
2657      * @param itemBody   event body in iCalendar format
2658      * @param etag       previous event etag to detect concurrent updates
2659      * @param noneMatch  if-none-match header value
2660      * @return HTTP response event result (status and etag)
2661      * @throws IOException on error
2662      */
2663     public ItemResult createOrUpdateItem(String folderPath, String itemName, String itemBody, String etag, String noneMatch) throws IOException {
2664         if (itemBody.startsWith("BEGIN:VCALENDAR")) {
2665             return internalCreateOrUpdateEvent(folderPath, itemName, "urn:content-classes:appointment", itemBody, etag, noneMatch);
2666         } else if (itemBody.startsWith("BEGIN:VCARD")) {
2667             return createOrUpdateContact(folderPath, itemName, itemBody, etag, noneMatch);
2668         } else {
2669             throw new IOException(BundleMessage.format("EXCEPTION_INVALID_MESSAGE_CONTENT", itemBody));
2670         }
2671     }
2672 
2673     static final String[] VCARD_N_PROPERTIES = {"sn", "givenName", "middlename", "personaltitle", "namesuffix"};
2674     static final String[] VCARD_ADR_HOME_PROPERTIES = {"homepostofficebox", null, "homeStreet", "homeCity", "homeState", "homePostalCode", "homeCountry"};
2675     static final String[] VCARD_ADR_WORK_PROPERTIES = {"postofficebox", "roomnumber", "street", "l", "st", "postalcode", "co"};
2676     static final String[] VCARD_ADR_OTHER_PROPERTIES = {"otherpostofficebox", null, "otherstreet", "othercity", "otherstate", "otherpostalcode", "othercountry"};
2677     static final String[] VCARD_ORG_PROPERTIES = {"o", "department"};
2678 
2679     protected void convertContactProperties(Map<String, String> properties, String[] contactProperties, List<String> values) {
2680         for (int i = 0; i < values.size() && i < contactProperties.length; i++) {
2681             if (contactProperties[i] != null) {
2682                 properties.put(contactProperties[i], values.get(i));
2683             }
2684         }
2685     }
2686 
2687     protected ItemResult createOrUpdateContact(String folderPath, String itemName, String itemBody, String etag, String noneMatch) throws IOException {
2688         // parse VCARD body to build contact property map
2689         Map<String, String> properties = new HashMap<>();
2690 
2691         VObject vcard = new VObject(new ICSBufferedReader(new StringReader(itemBody)));
2692         if ("group".equalsIgnoreCase(vcard.getPropertyValue("KIND"))) {
2693             properties.put("outlookmessageclass", "IPM.DistList");
2694             properties.put("displayname", vcard.getPropertyValue("FN"));
2695         } else {
2696             properties.put("outlookmessageclass", "IPM.Contact");
2697 
2698             for (VProperty property : vcard.getProperties()) {
2699                 if ("FN".equals(property.getKey())) {
2700                     properties.put("cn", property.getValue());
2701                     properties.put("subject", property.getValue());
2702                     properties.put("fileas", property.getValue());
2703 
2704                 } else if ("N".equals(property.getKey())) {
2705                     convertContactProperties(properties, VCARD_N_PROPERTIES, property.getValues());
2706                 } else if ("NICKNAME".equals(property.getKey())) {
2707                     properties.put("nickname", property.getValue());
2708                 } else if ("TEL".equals(property.getKey())) {
2709                     if (property.hasParam("TYPE", "cell") || property.hasParam("X-GROUP", "cell")) {
2710                         properties.put("mobile", property.getValue());
2711                     } else if (property.hasParam("TYPE", "work") || property.hasParam("X-GROUP", "work")) {
2712                         properties.put("telephoneNumber", property.getValue());
2713                     } else if (property.hasParam("TYPE", "home") || property.hasParam("X-GROUP", "home")) {
2714                         properties.put("homePhone", property.getValue());
2715                     } else if (property.hasParam("TYPE", "fax")) {
2716                         if (property.hasParam("TYPE", "home")) {
2717                             properties.put("homefax", property.getValue());
2718                         } else {
2719                             properties.put("facsimiletelephonenumber", property.getValue());
2720                         }
2721                     } else if (property.hasParam("TYPE", "pager")) {
2722                         properties.put("pager", property.getValue());
2723                     } else if (property.hasParam("TYPE", "car")) {
2724                         properties.put("othermobile", property.getValue());
2725                     } else {
2726                         properties.put("otherTelephone", property.getValue());
2727                     }
2728                 } else if ("ADR".equals(property.getKey())) {
2729                     // address
2730                     if (property.hasParam("TYPE", "home")) {
2731                         convertContactProperties(properties, VCARD_ADR_HOME_PROPERTIES, property.getValues());
2732                     } else if (property.hasParam("TYPE", "work")) {
2733                         convertContactProperties(properties, VCARD_ADR_WORK_PROPERTIES, property.getValues());
2734                         // any other type goes to other address
2735                     } else {
2736                         convertContactProperties(properties, VCARD_ADR_OTHER_PROPERTIES, property.getValues());
2737                     }
2738                 } else if ("EMAIL".equals(property.getKey())) {
2739                     if (property.hasParam("TYPE", "home")) {
2740                         properties.put("email2", property.getValue());
2741                         properties.put("smtpemail2", property.getValue());
2742                     } else if (property.hasParam("TYPE", "other")) {
2743                         properties.put("email3", property.getValue());
2744                         properties.put("smtpemail3", property.getValue());
2745                     } else {
2746                         properties.put("email1", property.getValue());
2747                         properties.put("smtpemail1", property.getValue());
2748                     }
2749                 } else if ("ORG".equals(property.getKey())) {
2750                     convertContactProperties(properties, VCARD_ORG_PROPERTIES, property.getValues());
2751                 } else if ("URL".equals(property.getKey())) {
2752                     if (property.hasParam("TYPE", "work")) {
2753                         properties.put("businesshomepage", property.getValue());
2754                     } else if (property.hasParam("TYPE", "home")) {
2755                         properties.put("personalHomePage", property.getValue());
2756                     } else {
2757                         // default: set personal home page
2758                         properties.put("personalHomePage", property.getValue());
2759                     }
2760                 } else if ("TITLE".equals(property.getKey())) {
2761                     properties.put("title", property.getValue());
2762                 } else if ("NOTE".equals(property.getKey())) {
2763                     properties.put("description", property.getValue());
2764                 } else if ("CUSTOM1".equals(property.getKey())) {
2765                     properties.put("extensionattribute1", property.getValue());
2766                 } else if ("CUSTOM2".equals(property.getKey())) {
2767                     properties.put("extensionattribute2", property.getValue());
2768                 } else if ("CUSTOM3".equals(property.getKey())) {
2769                     properties.put("extensionattribute3", property.getValue());
2770                 } else if ("CUSTOM4".equals(property.getKey())) {
2771                     properties.put("extensionattribute4", property.getValue());
2772                 } else if ("ROLE".equals(property.getKey())) {
2773                     properties.put("profession", property.getValue());
2774                 } else if ("X-AIM".equals(property.getKey())) {
2775                     properties.put("im", property.getValue());
2776                 } else if ("BDAY".equals(property.getKey())) {
2777                     properties.put("bday", convertBDayToZulu(property.getValue()));
2778                 } else if ("ANNIVERSARY".equals(property.getKey()) || "X-ANNIVERSARY".equals(property.getKey())) {
2779                     properties.put("anniversary", convertBDayToZulu(property.getValue()));
2780                 } else if ("CATEGORIES".equals(property.getKey())) {
2781                     properties.put("keywords", property.getValue());
2782                 } else if ("CLASS".equals(property.getKey())) {
2783                     if ("PUBLIC".equals(property.getValue())) {
2784                         properties.put("sensitivity", "0");
2785                         properties.put("private", "false");
2786                     } else {
2787                         properties.put("sensitivity", "2");
2788                         properties.put("private", "true");
2789                     }
2790                 } else if ("SEX".equals(property.getKey())) {
2791                     String propertyValue = property.getValue();
2792                     if ("1".equals(propertyValue)) {
2793                         properties.put("gender", "2");
2794                     } else if ("2".equals(propertyValue)) {
2795                         properties.put("gender", "1");
2796                     }
2797                 } else if ("FBURL".equals(property.getKey())) {
2798                     properties.put("fburl", property.getValue());
2799                 } else if ("X-ASSISTANT".equals(property.getKey())) {
2800                     properties.put("secretarycn", property.getValue());
2801                 } else if ("X-MANAGER".equals(property.getKey())) {
2802                     properties.put("manager", property.getValue());
2803                 } else if ("X-SPOUSE".equals(property.getKey())) {
2804                     properties.put("spousecn", property.getValue());
2805                 } else if ("PHOTO".equals(property.getKey())) {
2806                     properties.put("photo", property.getValue());
2807                     properties.put("haspicture", "true");
2808                 } else if ("KEY1".equals(property.getKey())) {
2809                     properties.put("msexchangecertificate", property.getValue());
2810                 } else if ("KEY2".equals(property.getKey())) {
2811                     properties.put("usersmimecertificate", property.getValue());
2812                 }
2813             }
2814             LOGGER.debug("Create or update contact " + itemName + ": " + properties);
2815             // reset missing properties to null
2816             for (String key : CONTACT_ATTRIBUTES) {
2817                 if (!"imapUid".equals(key) && !"etag".equals(key) && !"urlcompname".equals(key)
2818                         && !"lastmodified".equals(key) && !"sensitivity".equals(key) &&
2819                         !properties.containsKey(key)) {
2820                     properties.put(key, null);
2821                 }
2822             }
2823         }
2824 
2825         Contact contact = buildContact(folderPath, itemName, properties, etag, noneMatch);
2826         for (VProperty property : vcard.getProperties()) {
2827             if ("MEMBER".equals(property.getKey())) {
2828                 String member = property.getValue();
2829                 if (member.startsWith("urn:uuid:")) {
2830                     Item item = getItem(folderPath, member.substring(9) + ".EML");
2831                     if (item != null) {
2832                         if (item.get("smtpemail1") != null) {
2833                             member = "mailto:" + item.get("smtpemail1");
2834                         } else if (item.get("smtpemail2") != null) {
2835                             member = "mailto:" + item.get("smtpemail2");
2836                         } else if (item.get("smtpemail3") != null) {
2837                             member = "mailto:" + item.get("smtpemail3");
2838                         }
2839                     }
2840                 }
2841                 contact.addMember(member);
2842             }
2843         }
2844         return contact.createOrUpdate();
2845     }
2846 
2847     protected String convertZuluDateToBday(String value) {
2848         String result = null;
2849         if (value != null && value.length() > 0) {
2850             try {
2851                 SimpleDateFormat parser = ExchangeSession.getZuluDateFormat();
2852                 Calendar cal = Calendar.getInstance();
2853                 cal.setTime(parser.parse(value));
2854                 cal.add(Calendar.HOUR_OF_DAY, 12);
2855                 result = ExchangeSession.getVcardBdayFormat().format(cal.getTime());
2856             } catch (ParseException e) {
2857                 LOGGER.warn("Invalid date: " + value);
2858             }
2859         }
2860         return result;
2861     }
2862 
2863     protected String convertBDayToZulu(String value) {
2864         String result = null;
2865         if (value != null && value.length() > 0) {
2866             try {
2867                 SimpleDateFormat parser;
2868                 if (value.length() == 10) {
2869                     parser = ExchangeSession.getVcardBdayFormat();
2870                 } else if (value.length() == 15) {
2871                     parser = new SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.ENGLISH);
2872                     parser.setTimeZone(GMT_TIMEZONE);
2873                 } else {
2874                     parser = ExchangeSession.getExchangeZuluDateFormat();
2875                 }
2876                 result = ExchangeSession.getExchangeZuluDateFormatMillisecond().format(parser.parse(value));
2877             } catch (ParseException e) {
2878                 LOGGER.warn("Invalid date: " + value);
2879             }
2880         }
2881 
2882         return result;
2883     }
2884 
2885 
2886     protected abstract Contact buildContact(String folderPath, String itemName, Map<String, String> properties, String etag, String noneMatch) throws IOException;
2887 
2888     protected abstract ItemResult internalCreateOrUpdateEvent(String folderPath, String itemName, String contentClass, String icsBody, String etag, String noneMatch) throws IOException;
2889 
2890     /**
2891      * Get current Exchange alias name from login name
2892      *
2893      * @return user name
2894      */
2895     public String getAliasFromLogin() {
2896         // login is email, not alias
2897         if (this.userName.indexOf('@') >= 0) {
2898             return null;
2899         }
2900         String result = this.userName;
2901         // remove domain name
2902         int index = Math.max(result.indexOf('\\'), result.indexOf('/'));
2903         if (index >= 0) {
2904             result = result.substring(index + 1);
2905         }
2906         return result;
2907     }
2908 
2909     /**
2910      * Test if folderPath is inside user mailbox.
2911      *
2912      * @param folderPath absolute folder path
2913      * @return true if folderPath is a public or shared folder
2914      */
2915     public abstract boolean isSharedFolder(String folderPath);
2916 
2917     /**
2918      * Test if folderPath is main calendar.
2919      *
2920      * @param folderPath absolute folder path
2921      * @return true if folderPath is a public or shared folder
2922      */
2923     public abstract boolean isMainCalendar(String folderPath) throws IOException;
2924 
2925     protected static final String MAILBOX_BASE = "/cn=";
2926 
2927     /**
2928      * Get current user email
2929      *
2930      * @return user email
2931      */
2932     public String getEmail() {
2933         return email;
2934     }
2935 
2936     /**
2937      * Get current user alias
2938      *
2939      * @return user email
2940      */
2941     public String getAlias() {
2942         return alias;
2943     }
2944 
2945     /**
2946      * Search global address list
2947      *
2948      * @param condition           search filter
2949      * @param returningAttributes returning attributes
2950      * @param sizeLimit           size limit
2951      * @return matching contacts from gal
2952      * @throws IOException on error
2953      */
2954     public abstract Map<String, Contact> galFind(Condition condition, Set<String> returningAttributes, int sizeLimit) throws IOException;
2955 
2956     /**
2957      * Full Contact attribute list
2958      */
2959     public static final Set<String> CONTACT_ATTRIBUTES = new HashSet<>();
2960 
2961     static {
2962         CONTACT_ATTRIBUTES.add("imapUid");
2963         CONTACT_ATTRIBUTES.add("etag");
2964         CONTACT_ATTRIBUTES.add("urlcompname");
2965 
2966         CONTACT_ATTRIBUTES.add("extensionattribute1");
2967         CONTACT_ATTRIBUTES.add("extensionattribute2");
2968         CONTACT_ATTRIBUTES.add("extensionattribute3");
2969         CONTACT_ATTRIBUTES.add("extensionattribute4");
2970         CONTACT_ATTRIBUTES.add("bday");
2971         CONTACT_ATTRIBUTES.add("anniversary");
2972         CONTACT_ATTRIBUTES.add("businesshomepage");
2973         CONTACT_ATTRIBUTES.add("personalHomePage");
2974         CONTACT_ATTRIBUTES.add("cn");
2975         CONTACT_ATTRIBUTES.add("co");
2976         CONTACT_ATTRIBUTES.add("department");
2977         CONTACT_ATTRIBUTES.add("smtpemail1");
2978         CONTACT_ATTRIBUTES.add("smtpemail2");
2979         CONTACT_ATTRIBUTES.add("smtpemail3");
2980         CONTACT_ATTRIBUTES.add("facsimiletelephonenumber");
2981         CONTACT_ATTRIBUTES.add("givenName");
2982         CONTACT_ATTRIBUTES.add("homeCity");
2983         CONTACT_ATTRIBUTES.add("homeCountry");
2984         CONTACT_ATTRIBUTES.add("homePhone");
2985         CONTACT_ATTRIBUTES.add("homePostalCode");
2986         CONTACT_ATTRIBUTES.add("homeState");
2987         CONTACT_ATTRIBUTES.add("homeStreet");
2988         CONTACT_ATTRIBUTES.add("homepostofficebox");
2989         CONTACT_ATTRIBUTES.add("l");
2990         CONTACT_ATTRIBUTES.add("manager");
2991         CONTACT_ATTRIBUTES.add("mobile");
2992         CONTACT_ATTRIBUTES.add("namesuffix");
2993         CONTACT_ATTRIBUTES.add("nickname");
2994         CONTACT_ATTRIBUTES.add("o");
2995         CONTACT_ATTRIBUTES.add("pager");
2996         CONTACT_ATTRIBUTES.add("personaltitle");
2997         CONTACT_ATTRIBUTES.add("postalcode");
2998         CONTACT_ATTRIBUTES.add("postofficebox");
2999         CONTACT_ATTRIBUTES.add("profession");
3000         CONTACT_ATTRIBUTES.add("roomnumber");
3001         CONTACT_ATTRIBUTES.add("secretarycn");
3002         CONTACT_ATTRIBUTES.add("sn");
3003         CONTACT_ATTRIBUTES.add("spousecn");
3004         CONTACT_ATTRIBUTES.add("st");
3005         CONTACT_ATTRIBUTES.add("street");
3006         CONTACT_ATTRIBUTES.add("telephoneNumber");
3007         CONTACT_ATTRIBUTES.add("title");
3008         CONTACT_ATTRIBUTES.add("description");
3009         CONTACT_ATTRIBUTES.add("im");
3010         CONTACT_ATTRIBUTES.add("middlename");
3011         CONTACT_ATTRIBUTES.add("lastmodified");
3012         CONTACT_ATTRIBUTES.add("otherstreet");
3013         CONTACT_ATTRIBUTES.add("otherstate");
3014         CONTACT_ATTRIBUTES.add("otherpostofficebox");
3015         CONTACT_ATTRIBUTES.add("otherpostalcode");
3016         CONTACT_ATTRIBUTES.add("othercountry");
3017         CONTACT_ATTRIBUTES.add("othercity");
3018         CONTACT_ATTRIBUTES.add("haspicture");
3019         CONTACT_ATTRIBUTES.add("keywords");
3020         CONTACT_ATTRIBUTES.add("othermobile");
3021         CONTACT_ATTRIBUTES.add("otherTelephone");
3022         CONTACT_ATTRIBUTES.add("gender");
3023         CONTACT_ATTRIBUTES.add("private");
3024         CONTACT_ATTRIBUTES.add("sensitivity");
3025         CONTACT_ATTRIBUTES.add("fburl");
3026         CONTACT_ATTRIBUTES.add("msexchangecertificate");
3027         CONTACT_ATTRIBUTES.add("usersmimecertificate");
3028     }
3029 
3030     protected static final Set<String> DISTRIBUTION_LIST_ATTRIBUTES = new HashSet<>();
3031 
3032     static {
3033         DISTRIBUTION_LIST_ATTRIBUTES.add("imapUid");
3034         DISTRIBUTION_LIST_ATTRIBUTES.add("etag");
3035         DISTRIBUTION_LIST_ATTRIBUTES.add("urlcompname");
3036 
3037         DISTRIBUTION_LIST_ATTRIBUTES.add("cn");
3038         DISTRIBUTION_LIST_ATTRIBUTES.add("members");
3039     }
3040 
3041     /**
3042      * Get freebusy data string from Exchange.
3043      *
3044      * @param attendee attendee email address
3045      * @param start    start date in Exchange zulu format
3046      * @param end      end date in Exchange zulu format
3047      * @param interval freebusy interval in minutes
3048      * @return freebusy data or null
3049      * @throws IOException on error
3050      */
3051     protected abstract String getFreeBusyData(String attendee, String start, String end, int interval) throws IOException;
3052 
3053     /**
3054      * Get freebusy info for attendee between start and end date.
3055      *
3056      * @param attendee       attendee email
3057      * @param startDateValue start date
3058      * @param endDateValue   end date
3059      * @return FreeBusy info
3060      * @throws IOException on error
3061      */
3062     public FreeBusy getFreebusy(String attendee, String startDateValue, String endDateValue) throws IOException {
3063         // replace ical encoded attendee name
3064         attendee = replaceIcal4Principal(attendee);
3065 
3066         // then check that email address is valid to avoid InvalidSmtpAddress error
3067         if (attendee == null || attendee.indexOf('@') < 0 || attendee.charAt(attendee.length() - 1) == '@') {
3068             return null;
3069         }
3070 
3071         if (attendee.startsWith("mailto:") || attendee.startsWith("MAILTO:")) {
3072             attendee = attendee.substring("mailto:".length());
3073         }
3074 
3075         SimpleDateFormat exchangeZuluDateFormat = getExchangeZuluDateFormat();
3076         SimpleDateFormat icalDateFormat = getZuluDateFormat();
3077 
3078         Date startDate;
3079         Date endDate;
3080         try {
3081             if (startDateValue.length() == 8) {
3082                 startDate = parseDate(startDateValue);
3083             } else {
3084                 startDate = icalDateFormat.parse(startDateValue);
3085             }
3086             if (endDateValue.length() == 8) {
3087                 endDate = parseDate(endDateValue);
3088             } else {
3089                 endDate = icalDateFormat.parse(endDateValue);
3090             }
3091         } catch (ParseException e) {
3092             throw new DavMailException("EXCEPTION_INVALID_DATES", e.getMessage());
3093         }
3094 
3095         FreeBusy freeBusy = null;
3096         String fbdata = getFreeBusyData(attendee, exchangeZuluDateFormat.format(startDate), exchangeZuluDateFormat.format(endDate), FREE_BUSY_INTERVAL);
3097         if (fbdata != null) {
3098             freeBusy = new FreeBusy(icalDateFormat, startDate, fbdata);
3099         }
3100 
3101         if (freeBusy != null && freeBusy.knownAttendee) {
3102             return freeBusy;
3103         } else {
3104             return null;
3105         }
3106     }
3107 
3108     /**
3109      * Exchange to iCalendar Free/Busy parser.
3110      * Free time returns 0, Tentative returns 1, Busy returns 2, and Out of Office (OOF) returns 3
3111      */
3112     public static final class FreeBusy {
3113         final SimpleDateFormat icalParser;
3114         boolean knownAttendee = true;
3115         static final HashMap<Character, String> FBTYPES = new HashMap<>();
3116 
3117         static {
3118             FBTYPES.put('1', "BUSY-TENTATIVE");
3119             FBTYPES.put('2', "BUSY");
3120             FBTYPES.put('3', "BUSY-UNAVAILABLE");
3121         }
3122 
3123         final HashMap<String, StringBuilder> busyMap = new HashMap<>();
3124 
3125         StringBuilder getBusyBuffer(char type) {
3126             String fbType = FBTYPES.get(type);
3127             StringBuilder buffer = busyMap.get(fbType);
3128             if (buffer == null) {
3129                 buffer = new StringBuilder();
3130                 busyMap.put(fbType, buffer);
3131             }
3132             return buffer;
3133         }
3134 
3135         void startBusy(char type, Calendar currentCal) {
3136             if (type == '4') {
3137                 knownAttendee = false;
3138             } else if (type != '0') {
3139                 StringBuilder busyBuffer = getBusyBuffer(type);
3140                 if (busyBuffer.length() > 0) {
3141                     busyBuffer.append(',');
3142                 }
3143                 busyBuffer.append(icalParser.format(currentCal.getTime()));
3144             }
3145         }
3146 
3147         void endBusy(char type, Calendar currentCal) {
3148             if (type != '0' && type != '4') {
3149                 getBusyBuffer(type).append('/').append(icalParser.format(currentCal.getTime()));
3150             }
3151         }
3152 
3153         FreeBusy(SimpleDateFormat icalParser, Date startDate, String fbdata) {
3154             this.icalParser = icalParser;
3155             if (fbdata.length() > 0) {
3156                 Calendar currentCal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
3157                 currentCal.setTime(startDate);
3158 
3159                 startBusy(fbdata.charAt(0), currentCal);
3160                 for (int i = 1; i < fbdata.length() && knownAttendee; i++) {
3161                     currentCal.add(Calendar.MINUTE, FREE_BUSY_INTERVAL);
3162                     char previousState = fbdata.charAt(i - 1);
3163                     char currentState = fbdata.charAt(i);
3164                     if (previousState != currentState) {
3165                         endBusy(previousState, currentCal);
3166                         startBusy(currentState, currentCal);
3167                     }
3168                 }
3169                 currentCal.add(Calendar.MINUTE, FREE_BUSY_INTERVAL);
3170                 endBusy(fbdata.charAt(fbdata.length() - 1), currentCal);
3171             }
3172         }
3173 
3174         /**
3175          * Append freebusy information to buffer.
3176          *
3177          * @param buffer String buffer
3178          */
3179         public void appendTo(StringBuilder buffer) {
3180             for (Map.Entry<String, StringBuilder> entry : busyMap.entrySet()) {
3181                 buffer.append("FREEBUSY;FBTYPE=").append(entry.getKey())
3182                         .append(':').append(entry.getValue()).append((char) 13).append((char) 10);
3183             }
3184         }
3185     }
3186 
3187     protected VObject vTimezone;
3188 
3189     /**
3190      * Load and return current user OWA timezone.
3191      *
3192      * @return current timezone
3193      */
3194     public VObject getVTimezone() {
3195         if (vTimezone == null) {
3196             // need to load Timezone info from OWA
3197             loadVtimezone();
3198         }
3199         return vTimezone;
3200     }
3201 
3202     public void clearVTimezone() {
3203         vTimezone = null;
3204     }
3205 
3206     protected abstract void loadVtimezone();
3207 
3208 }