View Javadoc
1   /*
2    * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
3    * Copyright (C) 2010  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.ews;
20  
21  import davmail.BundleMessage;
22  import davmail.Settings;
23  import davmail.exception.DavMailAuthenticationException;
24  import davmail.exception.DavMailException;
25  import davmail.exception.HttpNotFoundException;
26  import davmail.exchange.ExchangeSession;
27  import davmail.exchange.VCalendar;
28  import davmail.exchange.VObject;
29  import davmail.exchange.VProperty;
30  import davmail.exchange.auth.O365Token;
31  import davmail.http.HttpClientAdapter;
32  import davmail.http.request.GetRequest;
33  import davmail.ui.NotificationDialog;
34  import davmail.util.DateUtil;
35  import davmail.util.IOUtil;
36  import davmail.util.StringUtil;
37  import org.apache.http.HttpStatus;
38  import org.apache.http.client.methods.CloseableHttpResponse;
39  
40  import javax.mail.MessagingException;
41  import javax.mail.Session;
42  import javax.mail.internet.InternetAddress;
43  import javax.mail.internet.MimeMessage;
44  import javax.mail.internet.MimeUtility;
45  import javax.mail.util.SharedByteArrayInputStream;
46  import java.io.BufferedReader;
47  import java.io.ByteArrayInputStream;
48  import java.io.ByteArrayOutputStream;
49  import java.io.IOException;
50  import java.io.InputStream;
51  import java.io.InputStreamReader;
52  import java.net.HttpURLConnection;
53  import java.net.URI;
54  import java.nio.charset.StandardCharsets;
55  import java.text.ParseException;
56  import java.text.SimpleDateFormat;
57  import java.util.*;
58  import java.util.regex.Pattern;
59  
60  /**
61   * EWS Exchange adapter.
62   * Compatible with Exchange 2007, 2010 and 2013.
63   */
64  public class EwsExchangeSession extends ExchangeSession {
65  
66      protected static final int PAGE_SIZE = 500;
67  
68      protected static final String ARCHIVE_ROOT = "/archive/";
69  
70      public static final Map<String, String> vTodoToTaskStatusMap = new HashMap<>();
71      public static final Map<String, String> taskTovTodoStatusMap = new HashMap<>();
72      static {
73          //taskTovTodoStatusMap.put("NotStarted", null);
74          taskTovTodoStatusMap.put("InProgress", "IN-PROCESS");
75          taskTovTodoStatusMap.put("Completed", "COMPLETED");
76          taskTovTodoStatusMap.put("WaitingOnOthers", "NEEDS-ACTION");
77          taskTovTodoStatusMap.put("Deferred", "CANCELLED");
78  
79          //vTodoToTaskStatusMap.put(null, "NotStarted");
80          vTodoToTaskStatusMap.put("IN-PROCESS", "InProgress");
81          vTodoToTaskStatusMap.put("COMPLETED", "Completed");
82          vTodoToTaskStatusMap.put("NEEDS-ACTION", "WaitingOnOthers");
83          vTodoToTaskStatusMap.put("CANCELLED", "Deferred");
84  
85      }
86  
87      /**
88       * Message types.
89       *
90       * @see <a href="http://msdn.microsoft.com/en-us/library/aa565652%28v=EXCHG.140%29.aspx">
91       * http://msdn.microsoft.com/en-us/library/aa565652%28v=EXCHG.140%29.aspx</a>;
92       */
93      protected static final Set<String> MESSAGE_TYPES = new HashSet<>();
94  
95      static {
96          MESSAGE_TYPES.add("Message");
97          MESSAGE_TYPES.add("CalendarItem");
98  
99          MESSAGE_TYPES.add("MeetingMessage");
100         MESSAGE_TYPES.add("MeetingRequest");
101         MESSAGE_TYPES.add("MeetingResponse");
102         MESSAGE_TYPES.add("MeetingCancellation");
103 
104         MESSAGE_TYPES.add("Item");
105         MESSAGE_TYPES.add("PostItem");
106 
107         // exclude types from IMAP
108         //MESSAGE_TYPES.add("Contact");
109         //MESSAGE_TYPES.add("DistributionList");
110         //MESSAGE_TYPES.add("Task");
111 
112         //ReplyToItem
113         //ForwardItem
114         //ReplyAllToItem
115         //AcceptItem
116         //TentativelyAcceptItem
117         //DeclineItem
118         //CancelCalendarItem
119         //RemoveItem
120         //PostReplyItem
121         //SuppressReadReceipt
122         //AcceptSharingInvitation
123     }
124 
125     static final Map<String, String> partstatToResponseMap = new HashMap<>();
126     static final Map<String, String> responseTypeToPartstatMap = new HashMap<>();
127     static final Map<String, String> statusToBusyStatusMap = new HashMap<>();
128 
129     static {
130         partstatToResponseMap.put("ACCEPTED", "AcceptItem");
131         partstatToResponseMap.put("TENTATIVE", "TentativelyAcceptItem");
132         partstatToResponseMap.put("DECLINED", "DeclineItem");
133         partstatToResponseMap.put("NEEDS-ACTION", "ReplyToItem");
134 
135         responseTypeToPartstatMap.put("Accept", "ACCEPTED");
136         responseTypeToPartstatMap.put("Tentative", "TENTATIVE");
137         responseTypeToPartstatMap.put("Decline", "DECLINED");
138         responseTypeToPartstatMap.put("NoResponseReceived", "NEEDS-ACTION");
139         responseTypeToPartstatMap.put("Unknown", "NEEDS-ACTION");
140 
141         statusToBusyStatusMap.put("TENTATIVE", "Tentative");
142         statusToBusyStatusMap.put("CONFIRMED", "Busy");
143         // Unable to map CANCELLED: cancelled events are directly deleted on Exchange
144     }
145 
146     static final String UTC_TIMEZONE = "UTC";
147 
148     static String resolveCalendarTimezone(VObject vEvent, String propertyName) {
149         VProperty property = vEvent.getProperty(propertyName);
150         String timezone = null;
151         if (property != null) {
152             String value = property.getValue();
153             if (value != null && value.endsWith("Z")) {
154                 return UTC_TIMEZONE;
155             }
156             timezone = property.getParamValue("TZID");
157         }
158         if (timezone != null && timezone.isEmpty()) {
159             timezone = null;
160         }
161         return timezone;
162     }
163 
164     protected HttpClientAdapter httpClient;
165 
166     protected Map<String, String> folderIdMap;
167     protected boolean directEws;
168 
169     /**
170      * Oauth2 token
171      */
172     private O365Token token;
173 
174     protected class Folder extends ExchangeSession.Folder {
175         public FolderId folderId;
176     }
177 
178     protected static class FolderPath {
179         protected final String parentPath;
180         protected final String folderName;
181 
182         protected FolderPath(String folderPath) {
183             int slashIndex = folderPath.lastIndexOf('/');
184             if (slashIndex < 0) {
185                 parentPath = "";
186                 folderName = folderPath;
187             } else {
188                 parentPath = folderPath.substring(0, slashIndex);
189                 folderName = folderPath.substring(slashIndex + 1);
190             }
191         }
192     }
193 
194     public EwsExchangeSession(HttpClientAdapter httpClient, String userName) throws IOException {
195         this.httpClient = httpClient;
196         this.userName = userName;
197         if (userName.contains("@")) {
198             this.email = userName;
199         }
200         buildSessionInfo(null);
201     }
202 
203     public EwsExchangeSession(HttpClientAdapter httpClient, URI uri, String userName) throws IOException {
204         this.httpClient = httpClient;
205         this.userName = userName;
206         if (userName.contains("@")) {
207             this.email = userName;
208             this.alias = userName.substring(0, userName.indexOf('@'));
209         }
210         buildSessionInfo(uri);
211     }
212 
213     public EwsExchangeSession(HttpClientAdapter httpClient, O365Token token, String userName) throws IOException {
214         this.httpClient = httpClient;
215         this.userName = userName;
216         if (userName.contains("@")) {
217             this.email = userName;
218             this.alias = userName.substring(0, userName.indexOf('@'));
219         }
220         this.token = token;
221         buildSessionInfo(null);
222     }
223 
224     public EwsExchangeSession(URI uri, O365Token token, String userName) throws IOException {
225         this(new HttpClientAdapter(uri, true), token, userName);
226     }
227 
228     public EwsExchangeSession(String url, String userName, String password) throws IOException {
229         this(new HttpClientAdapter(url, userName, password, true), userName);
230     }
231 
232     /**
233      * EWS fetch page size.
234      *
235      * @return page size
236      */
237     private static int getPageSize() {
238         return Settings.getIntProperty("davmail.folderFetchPageSize", PAGE_SIZE);
239     }
240 
241     /**
242      * Check endpoint url.
243      *
244      * @throws IOException on error
245      */
246     protected void checkEndPointUrl() throws IOException {
247         GetFolderMethod checkMethod = new GetFolderMethod(BaseShape.ID_ONLY,
248                 DistinguishedFolderId.getInstance(null, DistinguishedFolderId.Name.root), null);
249         int status = executeMethod(checkMethod);
250 
251         if (status == HttpStatus.SC_UNAUTHORIZED) {
252             throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED");
253         } else if (status != HttpStatus.SC_OK) {
254             throw new IOException("Ews endpoint not available at " + checkMethod.getURI().toString() + " status " + status);
255         }
256     }
257 
258     @Override
259     public void buildSessionInfo(java.net.URI uri) throws IOException {
260         // send a first request to get server version
261         checkEndPointUrl();
262 
263         // new approach based on ConvertId to find primary email address
264         if (email == null || alias == null) {
265             try {
266                 GetFolderMethod getFolderMethod = new GetFolderMethod(BaseShape.ID_ONLY,
267                         DistinguishedFolderId.getInstance(null, DistinguishedFolderId.Name.root),
268                         null);
269                 executeMethod(getFolderMethod);
270                 EWSMethod.Item item = getFolderMethod.getResponseItem();
271                 String folderId = item.get("FolderId");
272 
273                 ConvertIdMethod convertIdMethod = new ConvertIdMethod(folderId);
274                 executeMethod(convertIdMethod);
275                 EWSMethod.Item convertIdItem = convertIdMethod.getResponseItem();
276                 if (convertIdItem != null && !convertIdItem.isEmpty()) {
277                     email = convertIdItem.get("Mailbox");
278                     alias = email.substring(0, email.indexOf('@'));
279                 } else {
280                     LOGGER.error("Unable to resolve email from root folder");
281                     throw new IOException();
282                 }
283 
284             } catch (IOException e) {
285                 throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED");
286             }
287         }
288 
289         directEws = uri == null
290                 || "/ews/services.wsdl".equalsIgnoreCase(uri.getPath())
291                 || "/ews/exchange.asmx".equalsIgnoreCase(uri.getPath());
292 
293         currentMailboxPath = "/users/" + email.toLowerCase();
294 
295         try {
296             folderIdMap = new HashMap<>();
297             // load actual well-known folder ids
298             folderIdMap.put(internalGetFolder(INBOX).folderId.value, INBOX);
299             folderIdMap.put(internalGetFolder(CALENDAR).folderId.value, CALENDAR);
300             folderIdMap.put(internalGetFolder(CONTACTS).folderId.value, CONTACTS);
301             folderIdMap.put(internalGetFolder(SENT).folderId.value, SENT);
302             folderIdMap.put(internalGetFolder(DRAFTS).folderId.value, DRAFTS);
303             folderIdMap.put(internalGetFolder(TRASH).folderId.value, TRASH);
304             folderIdMap.put(internalGetFolder(JUNK).folderId.value, JUNK);
305             folderIdMap.put(internalGetFolder(UNSENT).folderId.value, UNSENT);
306         } catch (IOException e) {
307             LOGGER.error(e.getMessage(), e);
308             throw new DavMailAuthenticationException("EXCEPTION_EWS_NOT_AVAILABLE");
309         }
310         LOGGER.debug("Current user email is " + email + ", alias is " + alias + " on " + serverVersion);
311     }
312 
313     protected String getEmailSuffixFromHostname() {
314         String domain = httpClient.getHost();
315         int start = domain.lastIndexOf('.', domain.lastIndexOf('.') - 1);
316         if (start >= 0) {
317             return '@' + domain.substring(start + 1);
318         } else {
319             return '@' + domain;
320         }
321     }
322 
323     protected void resolveEmailAddress(String userName) {
324         String searchValue = userName;
325         int index = searchValue.indexOf('\\');
326         if (index >= 0) {
327             searchValue = searchValue.substring(index + 1);
328         }
329         ResolveNamesMethod resolveNamesMethod = new ResolveNamesMethod(searchValue);
330         try {
331             // send a fake request to get server version
332             internalGetFolder("");
333             executeMethod(resolveNamesMethod);
334             List<EWSMethod.Item> responses = resolveNamesMethod.getResponseItems();
335             if (responses.size() == 1) {
336                 email = responses.get(0).get("EmailAddress");
337             }
338 
339         } catch (IOException e) {
340             // ignore
341         }
342     }
343 
344     class Message extends ExchangeSession.Message {
345         // message item id
346         ItemId itemId;
347 
348         @Override
349         public String getPermanentId() {
350             return itemId.id;
351         }
352 
353         @Override
354         protected InputStream getMimeHeaders() {
355             InputStream result = null;
356             try {
357                 GetItemMethod getItemMethod = new GetItemMethod(BaseShape.ID_ONLY, itemId, false);
358                 getItemMethod.addAdditionalProperty(Field.get("messageheaders"));
359                 getItemMethod.addAdditionalProperty(Field.get("from"));
360                 executeMethod(getItemMethod);
361                 EWSMethod.Item item = getItemMethod.getResponseItem();
362 
363                 String messageHeaders = item.get(Field.get("messageheaders").getResponseName());
364                 if (messageHeaders != null
365                         // workaround for broken message headers on Exchange 2010
366                         && messageHeaders.toLowerCase().contains("message-id:")) {
367                     // workaround for messages in Sent folder
368                     if (!messageHeaders.contains("From:")) {
369                         String from = item.get(Field.get("from").getResponseName());
370                         if (from != null) {
371                             messageHeaders = "From: " + MimeUtility.encodeText(from, "UTF-8", null) + '\r' + '\n' + messageHeaders;
372                         }
373                     }
374 
375                     result = new ByteArrayInputStream(messageHeaders.getBytes(StandardCharsets.UTF_8));
376                 }
377             } catch (Exception e) {
378                 LOGGER.warn(e.getMessage());
379             }
380 
381             return result;
382         }
383     }
384 
385     /**
386      * Message create/update properties
387      *
388      * @param properties flag values map
389      * @return field values
390      */
391     protected List<FieldUpdate> buildProperties(Map<String, String> properties) {
392         ArrayList<FieldUpdate> list = new ArrayList<>();
393         for (Map.Entry<String, String> entry : properties.entrySet()) {
394             if ("read".equals(entry.getKey())) {
395                 list.add(Field.createFieldUpdate("read", Boolean.toString("1".equals(entry.getValue()))));
396             } else if ("junk".equals(entry.getKey())) {
397                 list.add(Field.createFieldUpdate("junk", entry.getValue()));
398             } else if ("flagged".equals(entry.getKey())) {
399                 list.add(Field.createFieldUpdate("flagStatus", entry.getValue()));
400             } else if ("answered".equals(entry.getKey())) {
401                 list.add(Field.createFieldUpdate("lastVerbExecuted", entry.getValue()));
402                 if ("102".equals(entry.getValue())) {
403                     list.add(Field.createFieldUpdate("iconIndex", "261"));
404                 }
405             } else if ("forwarded".equals(entry.getKey())) {
406                 list.add(Field.createFieldUpdate("lastVerbExecuted", entry.getValue()));
407                 if ("104".equals(entry.getValue())) {
408                     list.add(Field.createFieldUpdate("iconIndex", "262"));
409                 }
410             } else if ("draft".equals(entry.getKey())) {
411                 // note: draft is readonly after creation
412                 list.add(Field.createFieldUpdate("messageFlags", entry.getValue()));
413             } else if ("deleted".equals(entry.getKey())) {
414                 list.add(Field.createFieldUpdate("deleted", entry.getValue()));
415             } else if ("datereceived".equals(entry.getKey())) {
416                 list.add(Field.createFieldUpdate("datereceived", entry.getValue()));
417             } else if ("keywords".equals(entry.getKey())) {
418                 list.add(Field.createFieldUpdate("keywords", entry.getValue()));
419             }
420         }
421         return list;
422     }
423 
424     @Override
425     public ExchangeSession.Message createMessage(String folderPath, String messageName, HashMap<String, String> properties, MimeMessage mimeMessage) throws IOException {
426         EWSMethod.Item item = new EWSMethod.Item();
427         item.type = "Message";
428         ByteArrayOutputStream baos = new ByteArrayOutputStream();
429         try {
430             mimeMessage.writeTo(baos);
431         } catch (MessagingException e) {
432             throw new IOException(e.getMessage());
433         }
434         baos.close();
435         item.mimeContent = IOUtil.encodeBase64(baos.toByteArray());
436 
437         List<FieldUpdate> fieldUpdates = buildProperties(properties);
438         if (!properties.containsKey("draft")) {
439             // need to force draft flag to false
440             if (properties.containsKey("read")) {
441                 fieldUpdates.add(Field.createFieldUpdate("messageFlags", "1"));
442             } else {
443                 fieldUpdates.add(Field.createFieldUpdate("messageFlags", "0"));
444             }
445         }
446         fieldUpdates.add(Field.createFieldUpdate("urlcompname", messageName));
447         item.setFieldUpdates(fieldUpdates);
448         CreateItemMethod createItemMethod = new CreateItemMethod(MessageDisposition.SaveOnly, getFolderId(folderPath), item);
449         executeMethod(createItemMethod);
450 
451         ItemId newItemId = new ItemId(createItemMethod.getResponseItem());
452         GetItemMethod getItemMethod = new GetItemMethod(BaseShape.ID_ONLY, newItemId, false);
453         for (String attribute : IMAP_MESSAGE_ATTRIBUTES) {
454             getItemMethod.addAdditionalProperty(Field.get(attribute));
455         }
456         executeMethod(getItemMethod);
457 
458         return buildMessage(getItemMethod.getResponseItem());
459 
460     }
461 
462     @Override
463     public void updateMessage(ExchangeSession.Message message, Map<String, String> properties) throws IOException {
464         if (properties.containsKey("read") && "urn:content-classes:appointment".equals(message.contentClass)) {
465             properties.remove("read");
466         }
467         if (!properties.isEmpty()) {
468             UpdateItemMethod updateItemMethod = new UpdateItemMethod(MessageDisposition.SaveOnly,
469                     ConflictResolution.AlwaysOverwrite,
470                     SendMeetingInvitationsOrCancellations.SendToNone,
471                     ((EwsExchangeSession.Message) message).itemId, buildProperties(properties));
472             executeMethod(updateItemMethod);
473         }
474     }
475 
476     @Override
477     public void deleteMessage(ExchangeSession.Message message) throws IOException {
478         LOGGER.debug("Delete " + message.imapUid);
479         DeleteItemMethod deleteItemMethod = new DeleteItemMethod(((EwsExchangeSession.Message) message).itemId, DeleteType.HardDelete, SendMeetingCancellations.SendToNone);
480         executeMethod(deleteItemMethod);
481     }
482 
483 
484     protected void sendMessage(String itemClass, byte[] messageBody) throws IOException {
485         EWSMethod.Item item = new EWSMethod.Item();
486         item.type = "Message";
487         item.mimeContent = IOUtil.encodeBase64(messageBody);
488         if (itemClass != null) {
489             item.put("ItemClass", itemClass);
490         }
491 
492         MessageDisposition messageDisposition;
493         if (Settings.getBooleanProperty("davmail.smtpSaveInSent", true)) {
494             messageDisposition = MessageDisposition.SendAndSaveCopy;
495         } else {
496             messageDisposition = MessageDisposition.SendOnly;
497         }
498 
499         CreateItemMethod createItemMethod = new CreateItemMethod(messageDisposition, getFolderId(SENT), item);
500         executeMethod(createItemMethod);
501     }
502 
503     @Override
504     public void sendMessage(MimeMessage mimeMessage) throws IOException, MessagingException {
505         String itemClass = null;
506         if (mimeMessage.getContentType().startsWith("multipart/report")) {
507             itemClass = "REPORT.IPM.Note.IPNRN";
508         }
509 
510         ByteArrayOutputStream baos = new ByteArrayOutputStream();
511         try {
512             mimeMessage.writeTo(baos);
513         } catch (MessagingException e) {
514             throw new IOException(e.getMessage());
515         }
516         sendMessage(itemClass, baos.toByteArray());
517     }
518 
519     /**
520      * @inheritDoc
521      */
522     @Override
523     protected byte[] getContent(ExchangeSession.Message message) throws IOException {
524         return getContent(((EwsExchangeSession.Message) message).itemId);
525     }
526 
527     /**
528      * Get item content.
529      *
530      * @param itemId EWS item id
531      * @return item content as a byte array
532      * @throws IOException on error
533      */
534     protected byte[] getContent(ItemId itemId) throws IOException {
535         GetItemMethod getItemMethod = new GetItemMethod(BaseShape.ID_ONLY, itemId, true);
536         byte[] mimeContent = null;
537         try {
538             executeMethod(getItemMethod);
539             mimeContent = getItemMethod.getMimeContent();
540         } catch (EWSException e) {
541             LOGGER.warn("GetItem with MimeContent failed: " + e.getMessage());
542         }
543         if (getItemMethod.getStatusCode() == HttpStatus.SC_NOT_FOUND) {
544             throw new HttpNotFoundException("Item " + itemId + " not found");
545         }
546         if (mimeContent == null) {
547             LOGGER.warn("MimeContent not available, trying to rebuild from properties");
548             try {
549                 ByteArrayOutputStream baos = new ByteArrayOutputStream();
550                 getItemMethod = new GetItemMethod(BaseShape.ID_ONLY, itemId, false);
551                 getItemMethod.addAdditionalProperty(Field.get("contentclass"));
552                 getItemMethod.addAdditionalProperty(Field.get("message-id"));
553                 getItemMethod.addAdditionalProperty(Field.get("from"));
554                 getItemMethod.addAdditionalProperty(Field.get("to"));
555                 getItemMethod.addAdditionalProperty(Field.get("cc"));
556                 getItemMethod.addAdditionalProperty(Field.get("subject"));
557                 getItemMethod.addAdditionalProperty(Field.get("date"));
558                 getItemMethod.addAdditionalProperty(Field.get("body"));
559                 executeMethod(getItemMethod);
560                 EWSMethod.Item item = getItemMethod.getResponseItem();
561 
562                 if (item == null) {
563                     throw new HttpNotFoundException("Item " + itemId + " not found");
564                 }
565 
566                 MimeMessage mimeMessage = new MimeMessage((Session) null);
567                 mimeMessage.addHeader("Content-class", item.get(Field.get("contentclass").getResponseName()));
568                 mimeMessage.setSentDate(parseDateFromExchange(item.get(Field.get("date").getResponseName())));
569                 mimeMessage.addHeader("From", item.get(Field.get("from").getResponseName()));
570                 mimeMessage.addHeader("To", item.get(Field.get("to").getResponseName()));
571                 mimeMessage.addHeader("Cc", item.get(Field.get("cc").getResponseName()));
572                 mimeMessage.setSubject(item.get(Field.get("subject").getResponseName()));
573                 String propertyValue = item.get(Field.get("body").getResponseName());
574                 if (propertyValue == null) {
575                     propertyValue = "";
576                 }
577                 mimeMessage.setContent(propertyValue, "text/html; charset=UTF-8");
578 
579                 mimeMessage.writeTo(baos);
580                 if (LOGGER.isDebugEnabled()) {
581                     LOGGER.debug("Rebuilt message content: " + new String(baos.toByteArray(), StandardCharsets.UTF_8));
582                 }
583                 mimeContent = baos.toByteArray();
584 
585             } catch (IOException | MessagingException e2) {
586                 LOGGER.warn(e2);
587             }
588             if (mimeContent == null) {
589                 throw new IOException("GetItem returned null MimeContent");
590             }
591         }
592         return mimeContent;
593     }
594 
595     protected ExchangeSession.Message buildMessage(EWSMethod.Item response) throws DavMailException {
596         Message message = new Message();
597 
598         // get item id
599         message.itemId = new ItemId(response);
600 
601         message.permanentUrl = response.get(Field.get("permanenturl").getResponseName());
602 
603         message.size = response.getInt(Field.get("messageSize").getResponseName());
604         message.uid = response.get(Field.get("uid").getResponseName());
605         message.contentClass = response.get(Field.get("contentclass").getResponseName());
606         message.imapUid = response.getLong(Field.get("imapUid").getResponseName());
607         message.read = response.getBoolean(Field.get("read").getResponseName());
608         message.junk = response.getBoolean(Field.get("junk").getResponseName());
609         message.flagged = "2".equals(response.get(Field.get("flagStatus").getResponseName()));
610         message.draft = (response.getInt(Field.get("messageFlags").getResponseName()) & 8) != 0;
611         String lastVerbExecuted = response.get(Field.get("lastVerbExecuted").getResponseName());
612         message.answered = "102".equals(lastVerbExecuted) || "103".equals(lastVerbExecuted);
613         message.forwarded = "104".equals(lastVerbExecuted);
614         message.date = convertDateFromExchange(response.get(Field.get("date").getResponseName()));
615         message.deleted = "1".equals(response.get(Field.get("deleted").getResponseName()));
616 
617         String lastmodified = convertDateFromExchange(response.get(Field.get("lastmodified").getResponseName()));
618         message.recent = !message.read && lastmodified != null && lastmodified.equals(message.date);
619 
620         message.keywords = response.get(Field.get("keywords").getResponseName());
621 
622         if (LOGGER.isDebugEnabled()) {
623             StringBuilder buffer = new StringBuilder();
624             buffer.append("Message");
625             if (message.imapUid != 0) {
626                 buffer.append(" IMAP uid: ").append(message.imapUid);
627             }
628             if (message.uid != null) {
629                 buffer.append(" uid: ").append(message.uid);
630             }
631             buffer.append(" ItemId: ").append(message.itemId.id);
632             buffer.append(" ChangeKey: ").append(message.itemId.changeKey);
633             LOGGER.debug(buffer.toString());
634         }
635         return message;
636     }
637 
638     @Override
639     public MessageList searchMessages(String folderPath, Set<String> attributes, Condition condition) throws IOException {
640         MessageList messages = new MessageList();
641         int maxCount = Settings.getIntProperty("davmail.folderSizeLimit", 0);
642         List<EWSMethod.Item> responses = searchItems(folderPath, attributes, condition, FolderQueryTraversal.SHALLOW, maxCount);
643 
644         for (EWSMethod.Item response : responses) {
645             if (MESSAGE_TYPES.contains(response.type)) {
646                 ExchangeSession.Message message = buildMessage(response);
647                 message.messageList = messages;
648                 messages.add(message);
649             }
650         }
651         Collections.sort(messages);
652         return messages;
653     }
654 
655     protected List<EWSMethod.Item> searchItems(String folderPath, Set<String> attributes, Condition condition, FolderQueryTraversal folderQueryTraversal, int maxCount) throws IOException {
656         if (maxCount == 0) {
657             // unlimited search
658             return searchItems(folderPath, attributes, condition, folderQueryTraversal);
659         }
660         // limited search, do not use paged search, limit with maxCount, sort by imapUid descending to get latest items
661         int resultCount;
662         FindItemMethod findItemMethod;
663 
664         // search items in folder, do not retrieve all properties
665         findItemMethod = new FindItemMethod(folderQueryTraversal, BaseShape.ID_ONLY, getFolderId(folderPath), 0, maxCount);
666         for (String attribute : attributes) {
667             findItemMethod.addAdditionalProperty(Field.get(attribute));
668         }
669         // make sure imapUid is available
670         if (!attributes.contains("imapUid")) {
671             findItemMethod.addAdditionalProperty(Field.get("imapUid"));
672         }
673 
674         // always sort items by imapUid descending to retrieve recent messages first
675         findItemMethod.setFieldOrder(new FieldOrder(Field.get("imapUid"), FieldOrder.Order.Descending));
676 
677         if (condition != null && !condition.isEmpty()) {
678             findItemMethod.setSearchExpression((SearchExpression) condition);
679         }
680         executeMethod(findItemMethod);
681         List<EWSMethod.Item> results = new ArrayList<>(findItemMethod.getResponseItems());
682         resultCount = results.size();
683         if (resultCount > 0 && LOGGER.isDebugEnabled()) {
684             LOGGER.debug("Folder " + folderPath + " - Search items count: " + resultCount + " maxCount: " + maxCount
685                     + " highest uid: " + results.get(0).getLong(Field.get("imapUid").getResponseName())
686                     + " lowest uid: " + results.get(resultCount - 1).getLong(Field.get("imapUid").getResponseName()));
687         }
688 
689 
690         return results;
691     }
692 
693     /**
694      * Paged search, retrieve all items.
695      *
696      * @param folderPath           folder path
697      * @param attributes           attributes
698      * @param condition            search condition
699      * @param folderQueryTraversal search mode
700      * @return items
701      * @throws IOException on error
702      */
703     protected List<EWSMethod.Item> searchItems(String folderPath, Set<String> attributes, Condition condition, FolderQueryTraversal folderQueryTraversal) throws IOException {
704         int resultCount = 0;
705         List<EWSMethod.Item> results = new ArrayList<>();
706         FolderId folderId = getFolderId(folderPath);
707         FindItemMethod findItemMethod;
708         do {
709             // search items in folder, do not retrieve all properties
710             findItemMethod = new FindItemMethod(folderQueryTraversal, BaseShape.ID_ONLY, folderId, resultCount, getPageSize());
711             for (String attribute : attributes) {
712                 findItemMethod.addAdditionalProperty(Field.get(attribute));
713             }
714             // make sure imapUid is available
715             if (!attributes.contains("imapUid")) {
716                 findItemMethod.addAdditionalProperty(Field.get("imapUid"));
717             }
718 
719             // always sort items by imapUid ascending to retrieve pages in creation order
720             findItemMethod.setFieldOrder(new FieldOrder(Field.get("imapUid"), FieldOrder.Order.Ascending));
721 
722             if (condition != null && !condition.isEmpty()) {
723                 findItemMethod.setSearchExpression((SearchExpression) condition);
724             }
725             executeMethod(findItemMethod);
726             if (findItemMethod.getStatusCode() == HttpStatus.SC_FORBIDDEN) {
727                 throw new EWSException(findItemMethod.errorDetail);
728             }
729 
730             long highestUid = 0;
731             if (resultCount > 0) {
732                 highestUid = results.get(resultCount - 1).getLong(Field.get("imapUid").getResponseName());
733             }
734             // Only add new result if not already available (concurrent folder changes issue)
735             for (EWSMethod.Item item : findItemMethod.getResponseItems()) {
736                 long imapUid = item.getLong(Field.get("imapUid").getResponseName());
737                 if (imapUid > highestUid) {
738                     results.add(item);
739                 }
740             }
741             resultCount = results.size();
742             if (resultCount > 0 && LOGGER.isDebugEnabled()) {
743                 LOGGER.debug("Folder " + folderPath + " - Search items current count: " + resultCount + " fetchCount: " + getPageSize()
744                         + " highest uid: " + results.get(resultCount - 1).getLong(Field.get("imapUid").getResponseName())
745                         + " lowest uid: " + results.get(0).getLong(Field.get("imapUid").getResponseName()));
746             }
747             if (Thread.interrupted()) {
748                 LOGGER.debug("Folder " + folderPath + " - Search items failed: Interrupted by client");
749                 throw new IOException("Search items failed: Interrupted by client");
750             }
751         } while (!(findItemMethod.includesLastItemInRange));
752         return results;
753     }
754 
755     protected static class MultiCondition extends ExchangeSession.MultiCondition implements SearchExpression {
756         protected MultiCondition(Operator operator, Condition... condition) {
757             super(operator, condition);
758         }
759 
760         public void appendTo(StringBuilder buffer) {
761             int actualConditionCount = getActualConditionCount();
762             if (actualConditionCount > 0) {
763                 if (actualConditionCount > 1) {
764                     buffer.append("<t:").append(operator.toString()).append('>');
765                 }
766 
767                 for (Condition condition : conditions) {
768                     condition.appendTo(buffer);
769                 }
770 
771                 if (actualConditionCount > 1) {
772                     buffer.append("</t:").append(operator).append('>');
773                 }
774             }
775         }
776     }
777 
778     protected static class NotCondition extends ExchangeSession.NotCondition implements SearchExpression {
779         protected NotCondition(Condition condition) {
780             super(condition);
781         }
782 
783         public void appendTo(StringBuilder buffer) {
784             buffer.append("<t:Not>");
785             condition.appendTo(buffer);
786             buffer.append("</t:Not>");
787         }
788     }
789 
790 
791     protected static class AttributeCondition extends ExchangeSession.AttributeCondition implements SearchExpression {
792         protected ContainmentMode containmentMode;
793         protected ContainmentComparison containmentComparison;
794 
795         protected AttributeCondition(String attributeName, Operator operator, String value) {
796             super(attributeName, operator, value);
797         }
798 
799         protected AttributeCondition(String attributeName, Operator operator, String value,
800                                      ContainmentMode containmentMode, ContainmentComparison containmentComparison) {
801             super(attributeName, operator, value);
802             this.containmentMode = containmentMode;
803             this.containmentComparison = containmentComparison;
804         }
805 
806         protected FieldURI getFieldURI() {
807             FieldURI fieldURI = Field.get(attributeName);
808             // check to detect broken field mapping
809             //noinspection ConstantConditions
810             if (fieldURI == null) {
811                 throw new IllegalArgumentException("Unknown field: " + attributeName);
812             }
813             return fieldURI;
814         }
815 
816         protected Operator getOperator() {
817             return operator;
818         }
819 
820         public void appendTo(StringBuilder buffer) {
821             buffer.append("<t:").append(operator.toString());
822             if (containmentMode != null) {
823                 containmentMode.appendTo(buffer);
824             }
825             if (containmentComparison != null) {
826                 containmentComparison.appendTo(buffer);
827             }
828             buffer.append('>');
829             FieldURI fieldURI = getFieldURI();
830             fieldURI.appendTo(buffer);
831 
832             if (operator != Operator.Contains) {
833                 buffer.append("<t:FieldURIOrConstant>");
834             }
835             buffer.append("<t:Constant Value=\"");
836             // encode urlcompname
837             if (fieldURI instanceof ExtendedFieldURI && "0x10f3".equals(((ExtendedFieldURI) fieldURI).propertyTag)) {
838                 buffer.append(StringUtil.xmlEncodeAttribute(StringUtil.encodeUrlcompname(value)));
839             } else if (fieldURI instanceof ExtendedFieldURI
840                     && ((ExtendedFieldURI) fieldURI).propertyType == ExtendedFieldURI.PropertyType.Integer) {
841                 // check value
842                 try {
843                     Integer.parseInt(value);
844                     buffer.append(value);
845                 } catch (NumberFormatException e) {
846                     // invalid value, replace value with 0
847                     buffer.append('0');
848                 }
849             } else {
850                 buffer.append(StringUtil.xmlEncodeAttribute(value));
851             }
852             buffer.append("\"/>");
853             if (operator != Operator.Contains) {
854                 buffer.append("</t:FieldURIOrConstant>");
855             }
856 
857             buffer.append("</t:").append(operator).append('>');
858         }
859 
860         public boolean isMatch(ExchangeSession.Contact contact) {
861             String lowerCaseValue = value.toLowerCase();
862 
863             String actualValue = contact.get(attributeName);
864             if (actualValue == null) {
865                 return false;
866             }
867             actualValue = actualValue.toLowerCase();
868             if (operator == Operator.IsEqualTo) {
869                 return lowerCaseValue.equals(actualValue);
870             } else {
871                 return operator == Operator.Contains && ((containmentMode.equals(ContainmentMode.Substring) && actualValue.contains(lowerCaseValue)) ||
872                         (containmentMode.equals(ContainmentMode.Prefixed) && actualValue.startsWith(lowerCaseValue)));
873             }
874         }
875 
876     }
877 
878     protected static class HeaderCondition extends AttributeCondition {
879 
880         protected HeaderCondition(String attributeName, String value) {
881             super(attributeName, Operator.Contains, value);
882             containmentMode = ContainmentMode.Substring;
883             containmentComparison = ContainmentComparison.IgnoreCase;
884         }
885 
886         @Override
887         protected FieldURI getFieldURI() {
888             return new ExtendedFieldURI(ExtendedFieldURI.DistinguishedPropertySetType.InternetHeaders, attributeName);
889         }
890 
891     }
892 
893     protected static class IsNullCondition implements ExchangeSession.Condition, SearchExpression {
894         protected final String attributeName;
895 
896         protected IsNullCondition(String attributeName) {
897             this.attributeName = attributeName;
898         }
899 
900         public void appendTo(StringBuilder buffer) {
901             buffer.append("<t:Not><t:Exists>");
902             Field.get(attributeName).appendTo(buffer);
903             buffer.append("</t:Exists></t:Not>");
904         }
905 
906         public boolean isEmpty() {
907             return false;
908         }
909 
910         public boolean isMatch(ExchangeSession.Contact contact) {
911             String actualValue = contact.get(attributeName);
912             return actualValue == null;
913         }
914 
915     }
916 
917     protected static class ExistsCondition implements ExchangeSession.Condition, SearchExpression {
918         protected final String attributeName;
919 
920         protected ExistsCondition(String attributeName) {
921             this.attributeName = attributeName;
922         }
923 
924         public void appendTo(StringBuilder buffer) {
925             buffer.append("<t:Exists>");
926             Field.get(attributeName).appendTo(buffer);
927             buffer.append("</t:Exists>");
928         }
929 
930         public boolean isEmpty() {
931             return false;
932         }
933 
934         public boolean isMatch(ExchangeSession.Contact contact) {
935             String actualValue = contact.get(attributeName);
936             return actualValue != null;
937         }
938 
939     }
940 
941     @Override
942     public ExchangeSession.MultiCondition and(Condition... condition) {
943         return new MultiCondition(Operator.And, condition);
944     }
945 
946     @Override
947     public ExchangeSession.MultiCondition or(Condition... condition) {
948         return new MultiCondition(Operator.Or, condition);
949     }
950 
951     @Override
952     public Condition not(Condition condition) {
953         return new NotCondition(condition);
954     }
955 
956     @Override
957     public Condition isEqualTo(String attributeName, String value) {
958         return new AttributeCondition(attributeName, Operator.IsEqualTo, value);
959     }
960 
961     @Override
962     public Condition isEqualTo(String attributeName, int value) {
963         return new AttributeCondition(attributeName, Operator.IsEqualTo, String.valueOf(value));
964     }
965 
966     @Override
967     public Condition headerIsEqualTo(String headerName, String value) {
968         if (serverVersion.startsWith("Exchange201")) {
969             if ("from".equals(headerName)
970                     || "to".equals(headerName)
971                     || "cc".equals(headerName)) {
972                 return new AttributeCondition("msg" + headerName, Operator.Contains, value, ContainmentMode.Substring, ContainmentComparison.IgnoreCase);
973             } else if ("message-id".equals(headerName)
974                     || "bcc".equals(headerName)) {
975                 return new AttributeCondition(headerName, Operator.Contains, value, ContainmentMode.Substring, ContainmentComparison.IgnoreCase);
976             } else {
977                 // Exchange 2010 does not support header search, use PR_TRANSPORT_MESSAGE_HEADERS instead
978                 return new AttributeCondition("messageheaders", Operator.Contains, headerName + ": " + value, ContainmentMode.Substring, ContainmentComparison.IgnoreCase);
979             }
980         } else {
981             return new HeaderCondition(headerName, value);
982         }
983     }
984 
985     @Override
986     public Condition gte(String attributeName, String value) {
987         return new AttributeCondition(attributeName, Operator.IsGreaterThanOrEqualTo, value);
988     }
989 
990     @Override
991     public Condition lte(String attributeName, String value) {
992         return new AttributeCondition(attributeName, Operator.IsLessThanOrEqualTo, value);
993     }
994 
995     @Override
996     public Condition lt(String attributeName, String value) {
997         return new AttributeCondition(attributeName, Operator.IsLessThan, value);
998     }
999 
1000     @Override
1001     public Condition gt(String attributeName, String value) {
1002         return new AttributeCondition(attributeName, Operator.IsGreaterThan, value);
1003     }
1004 
1005     @Override
1006     public Condition contains(String attributeName, String value) {
1007         // workaround for from: and to: headers not searchable over EWS
1008         if ("from".equals(attributeName)) {
1009             attributeName = "msgfrom";
1010         } else if ("to".equals(attributeName)) {
1011             attributeName = "displayto";
1012         } else if ("cc".equals(attributeName)) {
1013             attributeName = "displaycc";
1014         }
1015         return new AttributeCondition(attributeName, Operator.Contains, value, ContainmentMode.Substring, ContainmentComparison.IgnoreCase);
1016     }
1017 
1018     @Override
1019     public Condition startsWith(String attributeName, String value) {
1020         return new AttributeCondition(attributeName, Operator.Contains, value, ContainmentMode.Prefixed, ContainmentComparison.IgnoreCase);
1021     }
1022 
1023     @Override
1024     public Condition isNull(String attributeName) {
1025         return new IsNullCondition(attributeName);
1026     }
1027 
1028     @Override
1029     public Condition exists(String attributeName) {
1030         return new ExistsCondition(attributeName);
1031     }
1032 
1033     @Override
1034     public Condition isTrue(String attributeName) {
1035         return new AttributeCondition(attributeName, Operator.IsEqualTo, "true");
1036     }
1037 
1038     @Override
1039     public Condition isFalse(String attributeName) {
1040         return new AttributeCondition(attributeName, Operator.IsEqualTo, "false");
1041     }
1042 
1043     protected static final HashSet<FieldURI> FOLDER_PROPERTIES = new HashSet<>();
1044 
1045     static {
1046         FOLDER_PROPERTIES.add(Field.get("urlcompname"));
1047         FOLDER_PROPERTIES.add(Field.get("parentfolderid"));
1048         FOLDER_PROPERTIES.add(Field.get("folderDisplayName"));
1049         FOLDER_PROPERTIES.add(Field.get("lastmodified"));
1050         FOLDER_PROPERTIES.add(Field.get("folderclass"));
1051         FOLDER_PROPERTIES.add(Field.get("ctag"));
1052         FOLDER_PROPERTIES.add(Field.get("count"));
1053         FOLDER_PROPERTIES.add(Field.get("unread"));
1054         FOLDER_PROPERTIES.add(Field.get("hassubs"));
1055         FOLDER_PROPERTIES.add(Field.get("uidNext"));
1056         FOLDER_PROPERTIES.add(Field.get("highestUid"));
1057     }
1058 
1059     protected Folder buildFolder(EWSMethod.Item item) {
1060         Folder folder = new Folder();
1061         folder.folderId = new FolderId(item);
1062         folder.displayName = encodeFolderName(item.get(Field.get("folderDisplayName").getResponseName()));
1063         folder.folderClass = item.get(Field.get("folderclass").getResponseName());
1064         folder.etag = item.get(Field.get("lastmodified").getResponseName());
1065         folder.ctag = item.get(Field.get("ctag").getResponseName());
1066         folder.messageCount = item.getInt(Field.get("count").getResponseName());
1067         folder.unreadCount = item.getInt(Field.get("unread").getResponseName());
1068         // fake recent value
1069         folder.recent = folder.unreadCount;
1070         folder.hasChildren = item.getBoolean(Field.get("hassubs").getResponseName());
1071         // noInferiors not implemented
1072         folder.uidNext = item.getInt(Field.get("uidNext").getResponseName());
1073         return folder;
1074     }
1075 
1076     /**
1077      * @inheritDoc
1078      */
1079     @Override
1080     public List<ExchangeSession.Folder> getSubFolders(String folderPath, Condition condition, boolean recursive) throws IOException {
1081         String baseFolderPath = folderPath;
1082         if (baseFolderPath.startsWith("/users/")) {
1083             int index = baseFolderPath.indexOf('/', "/users/".length());
1084             if (index >= 0) {
1085                 baseFolderPath = baseFolderPath.substring(index + 1);
1086             }
1087         }
1088         List<ExchangeSession.Folder> folders = new ArrayList<>();
1089         appendSubFolders(folders, baseFolderPath, getFolderId(folderPath), condition, recursive);
1090         return folders;
1091     }
1092 
1093     protected void appendSubFolders(List<ExchangeSession.Folder> folders,
1094                                     String parentFolderPath, FolderId parentFolderId,
1095                                     Condition condition, boolean recursive) throws IOException {
1096         if (recursive) {
1097             appendSubFoldersDeep(folders, parentFolderPath, parentFolderId, condition);
1098         } else {
1099             appendSubFoldersShallow(folders, parentFolderPath, parentFolderId, condition);
1100         }
1101     }
1102 
1103     protected void appendSubFoldersShallow(List<ExchangeSession.Folder> folders,
1104                                            String parentFolderPath, FolderId parentFolderId,
1105                                            Condition condition) throws IOException {
1106         int resultCount = 0;
1107         FindFolderMethod findFolderMethod;
1108         do {
1109             findFolderMethod = new FindFolderMethod(FolderQueryTraversal.SHALLOW,
1110                     BaseShape.ID_ONLY, parentFolderId, FOLDER_PROPERTIES, (SearchExpression) condition, resultCount, getPageSize());
1111             executeMethod(findFolderMethod);
1112             for (EWSMethod.Item item : findFolderMethod.getResponseItems()) {
1113                 resultCount++;
1114                 Folder folder = buildFolder(item);
1115                 if (!parentFolderPath.isEmpty()) {
1116                     if (parentFolderPath.endsWith("/")) {
1117                         folder.folderPath = parentFolderPath + folder.displayName;
1118                     } else {
1119                         folder.folderPath = parentFolderPath + '/' + folder.displayName;
1120                     }
1121                 } else if (folderIdMap.get(folder.folderId.value) != null) {
1122                     folder.folderPath = folderIdMap.get(folder.folderId.value);
1123                 } else {
1124                     folder.folderPath = folder.displayName;
1125                 }
1126                 folders.add(folder);
1127             }
1128         } while (!(findFolderMethod.includesLastItemInRange));
1129     }
1130 
1131     protected void appendSubFoldersDeep(List<ExchangeSession.Folder> folders,
1132                                         String parentFolderPath, FolderId parentFolderId,
1133                                         Condition condition) throws IOException {
1134         Map<String, String> parentByFolderId = new HashMap<>();
1135         List<Folder> deepFolders = new ArrayList<>();
1136         int resultCount = 0;
1137         FindFolderMethod findFolderMethod;
1138         do {
1139             findFolderMethod = new FindFolderMethod(FolderQueryTraversal.DEEP,
1140                     BaseShape.ID_ONLY, parentFolderId, FOLDER_PROPERTIES, (SearchExpression) condition, resultCount, getPageSize());
1141             executeMethod(findFolderMethod);
1142             for (EWSMethod.Item item : findFolderMethod.getResponseItems()) {
1143                 resultCount++;
1144                 Folder folder = buildFolder(item);
1145                 deepFolders.add(folder);
1146                 parentByFolderId.put(folder.folderId.value, item.get("ParentFolderId"));
1147             }
1148         } while (!(findFolderMethod.includesLastItemInRange));
1149 
1150         // Build a map of parent -> children for top-down traversal
1151         Map<String, List<Folder>> childrenByParentId = new HashMap<>();
1152         for (Folder folder : deepFolders) {
1153             String parentId = parentByFolderId.get(folder.folderId.value);
1154             childrenByParentId.computeIfAbsent(parentId, k -> new ArrayList<>()).add(folder);
1155         }
1156 
1157         // Iterate from root, assigning paths top-down
1158         String rootId = parentFolderId.value;
1159         // if root is a DistinguishedFolderId, need to fetch actual id for parent id mapping
1160         if (parentFolderId instanceof DistinguishedFolderId) {
1161             rootId = internalGetFolder(parentFolderId, parentFolderPath).folderId.value;
1162         }
1163 
1164         Deque<Map.Entry<String, String>> queue = new ArrayDeque<>();
1165         // Seed: children of the root folder
1166         queue.add(new AbstractMap.SimpleEntry<>(rootId, parentFolderPath));
1167 
1168         while (!queue.isEmpty()) {
1169             Map.Entry<String, String> entry = queue.poll();
1170             String currentParentId = entry.getKey();
1171             String currentParentPath = entry.getValue();
1172 
1173             List<Folder> children = childrenByParentId.get(currentParentId);
1174             if (children != null) {
1175                 for (Folder childFolder : children) {
1176                     String childFolderId = childFolder.folderId.value;
1177 
1178                     // Determine child folder path
1179                     if (folderIdMap.get(childFolderId) != null) {
1180                         // use special foldes name
1181                         childFolder.folderPath = folderIdMap.get(childFolderId);
1182                     } else if (currentParentPath.isEmpty()) {
1183                         childFolder.folderPath = childFolder.displayName;
1184                     } else if (currentParentPath.endsWith("/")) {
1185                         childFolder.folderPath = currentParentPath + childFolder.displayName;
1186                     } else {
1187                         childFolder.folderPath = currentParentPath + '/' + childFolder.displayName;
1188                     }
1189                     folders.add(childFolder);
1190 
1191                     // Enqueue this folder so its children get the correct prefix
1192                     queue.add(new AbstractMap.SimpleEntry<>(childFolderId, childFolder.folderPath));
1193                 }
1194             }
1195         }
1196 
1197     }
1198 
1199     /**
1200      * Get folder by path.
1201      *
1202      * @param folderPath folder path
1203      * @return folder object
1204      * @throws IOException on error
1205      */
1206     @Override
1207     protected EwsExchangeSession.Folder internalGetFolder(String folderPath) throws IOException {
1208         FolderId folderId = getFolderId(folderPath);
1209         return internalGetFolder(folderId, folderPath);
1210     }
1211 
1212     protected EwsExchangeSession.Folder internalGetFolder(FolderId folderId, String folderPath) throws IOException {
1213         GetFolderMethod getFolderMethod = new GetFolderMethod(BaseShape.ID_ONLY, folderId, FOLDER_PROPERTIES);
1214         executeMethod(getFolderMethod);
1215         EWSMethod.Item item = getFolderMethod.getResponseItem();
1216         Folder folder;
1217         if (item != null) {
1218             folder = buildFolder(item);
1219             folder.folderPath = folderPath;
1220         } else {
1221             throw new HttpNotFoundException("Folder " + folderPath + " not found");
1222         }
1223         return folder;
1224     }
1225 
1226     /**
1227      * @inheritDoc
1228      */
1229     @Override
1230     public int createFolder(String folderPath, String folderClass, Map<String, String> properties) throws IOException {
1231         FolderPath path = new FolderPath(folderPath);
1232         EWSMethod.Item folder = new EWSMethod.Item();
1233         if ("IPF.Contact".equals(folderClass)) {
1234             folder.type = "ContactsFolder";
1235         } else if ("IPF.Appointment".equals(folderClass)) {
1236             folder.type = "CalendarFolder";
1237         } else if ("IPF.Task".equals(folderClass)) {
1238             folder.type = "TasksFolder";
1239         } else {
1240             folder.put("FolderClass", folderClass);
1241             folder.type = "Folder";
1242         }
1243         folder.put("DisplayName", decodeFolderName(path.folderName));
1244 
1245         CreateFolderMethod createFolderMethod = new CreateFolderMethod(getFolderId(path.parentPath), folder);
1246         executeMethod(createFolderMethod);
1247         return HttpStatus.SC_CREATED;
1248     }
1249 
1250     /**
1251      * @inheritDoc
1252      */
1253     @Override
1254     public int updateFolder(String folderPath, Map<String, String> properties) throws IOException {
1255         ArrayList<FieldUpdate> updates = new ArrayList<>();
1256         for (Map.Entry<String, String> entry : properties.entrySet()) {
1257             updates.add(new FieldUpdate(Field.get(entry.getKey()), entry.getValue()));
1258         }
1259         UpdateFolderMethod updateFolderMethod = new UpdateFolderMethod(internalGetFolder(folderPath).folderId, updates);
1260 
1261         executeMethod(updateFolderMethod);
1262         return HttpStatus.SC_CREATED;
1263     }
1264 
1265     /**
1266      * @inheritDoc
1267      */
1268     @Override
1269     public void deleteFolder(String folderPath) throws IOException {
1270         FolderId folderId = getFolderIdIfExists(folderPath);
1271         if (folderId != null) {
1272             DeleteFolderMethod deleteFolderMethod = new DeleteFolderMethod(folderId);
1273             executeMethod(deleteFolderMethod);
1274         } else {
1275             LOGGER.debug("Folder " + folderPath + " not found");
1276         }
1277     }
1278 
1279     /**
1280      * @inheritDoc
1281      */
1282     @Override
1283     public void moveMessage(ExchangeSession.Message message, String targetFolder) throws IOException {
1284         MoveItemMethod moveItemMethod = new MoveItemMethod(((EwsExchangeSession.Message) message).itemId, getFolderId(targetFolder));
1285         executeMethod(moveItemMethod);
1286     }
1287 
1288     /**
1289      * @inheritDoc
1290      */
1291     @Override
1292     public void moveMessages(List<ExchangeSession.Message> messages, String targetFolder) throws IOException {
1293         ArrayList<ItemId> itemIds = new ArrayList<>();
1294         for (ExchangeSession.Message message : messages) {
1295             itemIds.add(((EwsExchangeSession.Message) message).itemId);
1296         }
1297 
1298         MoveItemMethod moveItemMethod = new MoveItemMethod(itemIds, getFolderId(targetFolder));
1299         executeMethod(moveItemMethod);
1300     }
1301 
1302     /**
1303      * @inheritDoc
1304      */
1305     @Override
1306     public void copyMessage(ExchangeSession.Message message, String targetFolder) throws IOException {
1307         CopyItemMethod copyItemMethod = new CopyItemMethod(((EwsExchangeSession.Message) message).itemId, getFolderId(targetFolder));
1308         executeMethod(copyItemMethod);
1309     }
1310 
1311     /**
1312      * @inheritDoc
1313      */
1314     @Override
1315     public void copyMessages(List<ExchangeSession.Message> messages, String targetFolder) throws IOException {
1316         ArrayList<ItemId> itemIds = new ArrayList<>();
1317         for (ExchangeSession.Message message : messages) {
1318             itemIds.add(((EwsExchangeSession.Message) message).itemId);
1319         }
1320 
1321         CopyItemMethod copyItemMethod = new CopyItemMethod(itemIds, getFolderId(targetFolder));
1322         executeMethod(copyItemMethod);
1323     }
1324 
1325     /**
1326      * @inheritDoc
1327      */
1328     @Override
1329     public void moveFolder(String folderPath, String targetFolderPath) throws IOException {
1330         FolderPath path = new FolderPath(folderPath);
1331         FolderPath targetPath = new FolderPath(targetFolderPath);
1332         FolderId folderId = getFolderId(folderPath);
1333         FolderId toFolderId = getFolderId(targetPath.parentPath);
1334         toFolderId.changeKey = null;
1335         // move folder
1336         if (!path.parentPath.equals(targetPath.parentPath)) {
1337             MoveFolderMethod moveFolderMethod = new MoveFolderMethod(folderId, toFolderId);
1338             executeMethod(moveFolderMethod);
1339         }
1340         // rename folder
1341         if (!path.folderName.equals(targetPath.folderName)) {
1342             ArrayList<FieldUpdate> updates = new ArrayList<>();
1343             updates.add(new FieldUpdate(Field.get("folderDisplayName"), targetPath.folderName));
1344             UpdateFolderMethod updateFolderMethod = new UpdateFolderMethod(folderId, updates);
1345             executeMethod(updateFolderMethod);
1346         }
1347     }
1348 
1349     @Override
1350     public void moveItem(String sourcePath, String targetPath) throws IOException {
1351         FolderPath sourceFolderPath = new FolderPath(sourcePath);
1352         Item item = getItem(sourceFolderPath.parentPath, sourceFolderPath.folderName);
1353         FolderPath targetFolderPath = new FolderPath(targetPath);
1354         FolderId toFolderId = getFolderId(targetFolderPath.parentPath);
1355         MoveItemMethod moveItemMethod = new MoveItemMethod(((Event) item).itemId, toFolderId);
1356         executeMethod(moveItemMethod);
1357     }
1358 
1359     /**
1360      * @inheritDoc
1361      */
1362     @Override
1363     protected void moveToTrash(ExchangeSession.Message message) throws IOException {
1364         MoveItemMethod moveItemMethod = new MoveItemMethod(((EwsExchangeSession.Message) message).itemId, getFolderId(TRASH));
1365         executeMethod(moveItemMethod);
1366     }
1367 
1368     protected class Contact extends ExchangeSession.Contact {
1369         // item id
1370         ItemId itemId;
1371 
1372         protected Contact(EWSMethod.Item response) throws DavMailException {
1373             itemId = new ItemId(response);
1374 
1375             permanentUrl = response.get(Field.get("permanenturl").getResponseName());
1376             etag = response.get(Field.get("etag").getResponseName());
1377             displayName = response.get(Field.get("displayname").getResponseName());
1378             // prefer urlcompname (client provided item name) for contacts
1379             itemName = StringUtil.decodeUrlcompname(response.get(Field.get("urlcompname").getResponseName()));
1380             // if urlcompname is empty, this is a server created item
1381             // if urlcompname is an itemId, something went wrong, ignore
1382             if (itemName == null || isItemId(itemName)) {
1383                 itemName = StringUtil.base64ToUrl(itemId.id) + ".EML";
1384             }
1385             for (String attributeName : CONTACT_ATTRIBUTES) {
1386                 String value = response.get(Field.get(attributeName).getResponseName());
1387                 if (value != null && !value.isEmpty()) {
1388                     if ("bday".equals(attributeName) || "anniversary".equals(attributeName) || "lastmodified".equals(attributeName) || "datereceived".equals(attributeName)) {
1389                         value = convertDateFromExchange(value);
1390                     }
1391                     put(attributeName, value);
1392                 }
1393             }
1394 
1395             if (response.getMembers() != null) {
1396                 for (String member : response.getMembers()) {
1397                     addMember(member);
1398                 }
1399             }
1400         }
1401 
1402         protected Contact(String folderPath, String itemName, Map<String, String> properties, String etag, String noneMatch) {
1403             super(folderPath, itemName, properties, etag, noneMatch);
1404         }
1405 
1406         /**
1407          * Empty constructor for GalFind
1408          */
1409         protected Contact() {
1410         }
1411 
1412         protected void buildFieldUpdates(List<FieldUpdate> updates, boolean create) {
1413             for (Map.Entry<String, String> entry : entrySet()) {
1414                 if ("photo".equals(entry.getKey())) {
1415                     updates.add(Field.createFieldUpdate("haspicture", "true"));
1416                 } else if (!entry.getKey().startsWith("email") && !entry.getKey().startsWith("smtpemail")
1417                         && !"fileas".equals(entry.getKey())) {
1418                     updates.add(Field.createFieldUpdate(entry.getKey(), entry.getValue()));
1419                 }
1420             }
1421             if (create && get("fileas") != null) {
1422                 updates.add(Field.createFieldUpdate("fileas", get("fileas")));
1423             }
1424             // handle email addresses
1425             IndexedFieldUpdate emailFieldUpdate = null;
1426             for (Map.Entry<String, String> entry : entrySet()) {
1427                 if (entry.getKey().startsWith("smtpemail")) {
1428                     if (emailFieldUpdate == null) {
1429                         emailFieldUpdate = new IndexedFieldUpdate("EmailAddresses");
1430                     }
1431                     emailFieldUpdate.addFieldValue(Field.createFieldUpdate(entry.getKey(), entry.getValue()));
1432                 }
1433             }
1434             if (emailFieldUpdate != null) {
1435                 updates.add(emailFieldUpdate);
1436             }
1437             // handle list members
1438             MultiValuedFieldUpdate memberFieldUpdate = null;
1439             if (distributionListMembers != null) {
1440                 for (String member : distributionListMembers) {
1441                     if (memberFieldUpdate == null) {
1442                         memberFieldUpdate = new MultiValuedFieldUpdate(Field.get("members"));
1443                     }
1444                     memberFieldUpdate.addValue(member);
1445                 }
1446             }
1447             if (memberFieldUpdate != null) {
1448                 updates.add(memberFieldUpdate);
1449             }
1450         }
1451 
1452 
1453         /**
1454          * Create or update contact
1455          *
1456          * @return action result
1457          * @throws IOException on error
1458          */
1459         @Override
1460         public ItemResult createOrUpdate() throws IOException {
1461             String photo = get("photo");
1462 
1463             ItemResult itemResult = new ItemResult();
1464             EWSMethod createOrUpdateItemMethod;
1465 
1466             // first try to load existing event
1467             String currentEtag = null;
1468             ItemId currentItemId = null;
1469             FileAttachment currentFileAttachment = null;
1470             EWSMethod.Item currentItem = getEwsItem(folderPath, itemName, ITEM_PROPERTIES);
1471             if (currentItem != null) {
1472                 currentItemId = new ItemId(currentItem);
1473                 currentEtag = currentItem.get(Field.get("etag").getResponseName());
1474 
1475                 // load current picture
1476                 GetItemMethod getItemMethod = new GetItemMethod(BaseShape.ID_ONLY, currentItemId, false);
1477                 getItemMethod.addAdditionalProperty(Field.get("attachments"));
1478                 executeMethod(getItemMethod);
1479                 EWSMethod.Item item = getItemMethod.getResponseItem();
1480                 if (item != null) {
1481                     currentFileAttachment = item.getAttachmentByName("ContactPicture.jpg");
1482                 }
1483             }
1484             if ("*".equals(noneMatch)) {
1485                 // create requested
1486                 //noinspection VariableNotUsedInsideIf
1487                 if (currentItemId != null) {
1488                     itemResult.status = HttpStatus.SC_PRECONDITION_FAILED;
1489                     return itemResult;
1490                 }
1491             } else if (etag != null) {
1492                 // update requested
1493                 if (currentItemId == null || !etag.equals(currentEtag)) {
1494                     itemResult.status = HttpStatus.SC_PRECONDITION_FAILED;
1495                     return itemResult;
1496                 }
1497             }
1498 
1499             List<FieldUpdate> fieldUpdates = new ArrayList<>();
1500             if (currentItemId != null) {
1501                 buildFieldUpdates(fieldUpdates, false);
1502                 // update
1503                 createOrUpdateItemMethod = new UpdateItemMethod(MessageDisposition.SaveOnly,
1504                         ConflictResolution.AlwaysOverwrite,
1505                         SendMeetingInvitationsOrCancellations.SendToNone,
1506                         currentItemId, fieldUpdates);
1507             } else {
1508                 // create
1509                 EWSMethod.Item newItem = new EWSMethod.Item();
1510                 if ("IPM.DistList".equals(get("outlookmessageclass"))) {
1511                     newItem.type = "DistributionList";
1512                 } else {
1513                     newItem.type = "Contact";
1514                 }
1515                 // force urlcompname on creation
1516                 fieldUpdates.add(Field.createFieldUpdate("urlcompname", convertItemNameToEML(itemName)));
1517                 buildFieldUpdates(fieldUpdates, true);
1518                 newItem.setFieldUpdates(fieldUpdates);
1519                 createOrUpdateItemMethod = new CreateItemMethod(MessageDisposition.SaveOnly, getFolderId(folderPath), newItem);
1520             }
1521             executeMethod(createOrUpdateItemMethod);
1522 
1523             itemResult.status = createOrUpdateItemMethod.getStatusCode();
1524             if (itemResult.status == HttpURLConnection.HTTP_OK) {
1525                 //noinspection VariableNotUsedInsideIf
1526                 if (currentItemId == null) {
1527                     itemResult.status = HttpStatus.SC_CREATED;
1528                     LOGGER.debug("Created contact " + getHref());
1529                 } else {
1530                     LOGGER.debug("Updated contact " + getHref());
1531                 }
1532             } else {
1533                 return itemResult;
1534             }
1535 
1536             ItemId newItemId = new ItemId(createOrUpdateItemMethod.getResponseItem());
1537 
1538             // disable contact picture handling on Exchange 2007
1539             if (!"Exchange2007_SP1".equals(serverVersion)
1540                     // prefer the user provided photo
1541                     && getADPhoto(get("smtpemail1")) == null) {
1542                 // first delete current picture
1543                 if (currentFileAttachment != null) {
1544                     DeleteAttachmentMethod deleteAttachmentMethod = new DeleteAttachmentMethod(currentFileAttachment.attachmentId);
1545                     executeMethod(deleteAttachmentMethod);
1546                 }
1547 
1548                 if (photo != null) {
1549                     // convert image to jpeg
1550                     byte[] resizedImageBytes = IOUtil.resizeImage(IOUtil.decodeBase64(photo), 90);
1551 
1552                     FileAttachment attachment = new FileAttachment("ContactPicture.jpg", "image/jpeg", IOUtil.encodeBase64AsString(resizedImageBytes));
1553                     attachment.setIsContactPhoto(true);
1554 
1555                     // update photo attachment
1556                     CreateAttachmentMethod createAttachmentMethod = new CreateAttachmentMethod(newItemId, attachment);
1557                     executeMethod(createAttachmentMethod);
1558                 }
1559             }
1560 
1561             GetItemMethod getItemMethod = new GetItemMethod(BaseShape.ID_ONLY, newItemId, false);
1562             getItemMethod.addAdditionalProperty(Field.get("etag"));
1563             executeMethod(getItemMethod);
1564             itemResult.etag = getItemMethod.getResponseItem().get(Field.get("etag").getResponseName());
1565 
1566             return itemResult;
1567         }
1568     }
1569 
1570     protected class Event extends ExchangeSession.Event {
1571         // item id
1572         ItemId itemId;
1573         String type;
1574         boolean isException;
1575 
1576         protected Event(String folderPath, EWSMethod.Item response) {
1577             this.folderPath = folderPath;
1578             itemId = new ItemId(response);
1579 
1580             type = response.type;
1581 
1582             permanentUrl = response.get(Field.get("permanenturl").getResponseName());
1583             etag = response.get(Field.get("etag").getResponseName());
1584             displayName = response.get(Field.get("displayname").getResponseName());
1585             subject = response.get(Field.get("subject").getResponseName());
1586             // ignore urlcompname and use item id
1587             itemName = StringUtil.base64ToUrl(itemId.id) + ".EML";
1588             String instancetype = response.get(Field.get("instancetype").getResponseName());
1589             isException = "3".equals(instancetype);
1590         }
1591 
1592         protected Event(String folderPath, String itemName, String contentClass, String itemBody, String etag, String noneMatch) throws IOException {
1593             super(folderPath, itemName, contentClass, itemBody, etag, noneMatch);
1594         }
1595 
1596         /**
1597          * Handle excluded dates (deleted occurrences).
1598          *
1599          * @param currentItemId the current item id to iterate over occurrences
1600          * @param vCalendar     vCalendar object
1601          * @throws DavMailException on error
1602          */
1603         protected void handleExcludedDates(ItemId currentItemId, VCalendar vCalendar) throws DavMailException {
1604             List<VProperty> excludedDates = vCalendar.getFirstVeventProperties("EXDATE");
1605             if (excludedDates != null) {
1606                 for (VProperty property : excludedDates) {
1607                     List<String> values = property.getValues();
1608                     for (String value : values) {
1609                         String convertedValue;
1610                         try {
1611                             convertedValue = vCalendar.convertCalendarDateToExchangeZulu(value, property.getParamValue("TZID"));
1612                         } catch (IOException e) {
1613                             throw new DavMailException("EXCEPTION_INVALID_DATE", value);
1614                         }
1615                         LOGGER.debug("Looking for occurrence " + convertedValue);
1616 
1617                         int instanceIndex = 0;
1618 
1619                         // let's try to find occurrence
1620                         while (true) {
1621                             instanceIndex++;
1622                             try {
1623                                 GetItemMethod getItemMethod = new GetItemMethod(BaseShape.ID_ONLY,
1624                                         new OccurrenceItemId(currentItemId.id, instanceIndex)
1625                                         , false);
1626                                 getItemMethod.addAdditionalProperty(Field.get("originalstart"));
1627                                 executeMethod(getItemMethod);
1628                                 if (getItemMethod.getResponseItem() != null) {
1629                                     String itemOriginalStart = getItemMethod.getResponseItem().get(Field.get("originalstart").getResponseName());
1630                                         LOGGER.debug("Occurrence " + instanceIndex + " itemOriginalStart " + itemOriginalStart + " looking for " + convertedValue);
1631                                     if (convertedValue.equals(itemOriginalStart)) {
1632                                         // found item, delete it
1633                                         DeleteItemMethod deleteItemMethod = new DeleteItemMethod(new ItemId(getItemMethod.getResponseItem()),
1634                                                 DeleteType.HardDelete, SendMeetingCancellations.SendToAllAndSaveCopy);
1635                                         executeMethod(deleteItemMethod);
1636                                         break;
1637                                     } else if (convertedValue.compareTo(itemOriginalStart) < 0) {
1638                                         // current item is after searched item => probably already deleted
1639                                         break;
1640                                     }
1641                                 }
1642                             } catch (IOException e) {
1643                                 LOGGER.warn("Error looking for occurrence " + convertedValue + ": " + e.getMessage());
1644                                 // after end of recurrence
1645                                 break;
1646                             }
1647                         }
1648                     }
1649                 }
1650             }
1651 
1652 
1653         }
1654 
1655         /**
1656          * Handle modified occurrences.
1657          *
1658          * @param currentItemId the current item id to iterate over occurrences
1659          * @param vCalendar     vCalendar object
1660          * @throws DavMailException on error
1661          */
1662         protected void handleModifiedOccurrences(ItemId currentItemId, VCalendar vCalendar, SendMeetingInvitationsOrCancellations sendMeetingInvitationsOrCancellations) throws DavMailException {
1663             for (VObject modifiedOccurrence : vCalendar.getModifiedOccurrences()) {
1664                 VProperty originalDateProperty = modifiedOccurrence.getProperty("RECURRENCE-ID");
1665                 String convertedValue;
1666                 try {
1667                     convertedValue = vCalendar.convertCalendarDateToExchangeZulu(originalDateProperty.getValue(), originalDateProperty.getParamValue("TZID"));
1668                 } catch (IOException e) {
1669                     throw new DavMailException("EXCEPTION_INVALID_DATE", originalDateProperty.getValue());
1670                 }
1671                 LOGGER.debug("Looking for occurrence " + convertedValue);
1672                 int instanceIndex = 0;
1673 
1674                 // let's try to find occurrence
1675                 while (true) {
1676                     instanceIndex++;
1677                     try {
1678                         GetItemMethod getItemMethod = new GetItemMethod(BaseShape.ID_ONLY,
1679                                 new OccurrenceItemId(currentItemId.id, instanceIndex)
1680                                 , false);
1681                         getItemMethod.addAdditionalProperty(Field.get("originalstart"));
1682                         executeMethod(getItemMethod);
1683                         if (getItemMethod.getResponseItem() != null) {
1684                             String itemOriginalStart = getItemMethod.getResponseItem().get(Field.get("originalstart").getResponseName());
1685                             if (convertedValue.equals(itemOriginalStart)) {
1686                                 // found item, update it
1687                                 UpdateItemMethod updateItemMethod = new UpdateItemMethod(MessageDisposition.SaveOnly,
1688                                         ConflictResolution.AutoResolve,
1689                                         sendMeetingInvitationsOrCancellations,
1690                                         new ItemId(getItemMethod.getResponseItem()), buildFieldUpdates(vCalendar, modifiedOccurrence, false));
1691                                 // force context Timezone on Exchange 2010 and 2013
1692                                 if (serverVersion != null && serverVersion.startsWith("Exchange201")) {
1693                                     updateItemMethod.setTimezoneContext(EwsExchangeSession.this.getVTimezone().getPropertyValue("TZID"));
1694                                 }
1695                                 executeMethod(updateItemMethod);
1696 
1697                                 break;
1698                             } else if (convertedValue.compareTo(itemOriginalStart) < 0) {
1699                                 // current item is after searched item => probably already deleted
1700                                 break;
1701                             }
1702                         }
1703                     } catch (IOException e) {
1704                         LOGGER.warn("Error looking for occurrence " + convertedValue + ": " + e.getMessage());
1705                         // after end of recurrence
1706                         break;
1707                     }
1708                 }
1709             }
1710         }
1711 
1712         protected List<FieldUpdate> buildFieldUpdates(VCalendar vCalendar, VObject vEvent, boolean isMozDismiss) throws DavMailException {
1713             boolean isShared = !email.equalsIgnoreCase(vCalendar.getCalendarEmail());
1714 
1715             List<FieldUpdate> updates = new ArrayList<>();
1716 
1717             if (isMozDismiss || "1".equals(vEvent.getPropertyValue("X-MOZ-FAKED-MASTER"))) {
1718                 String xMozLastack = vCalendar.getFirstVeventPropertyValue("X-MOZ-LASTACK");
1719                 if (xMozLastack != null) {
1720                     updates.add(Field.createFieldUpdate("xmozlastack", xMozLastack));
1721                 }
1722                 String xMozSnoozeTime = vCalendar.getFirstVeventPropertyValue("X-MOZ-SNOOZE-TIME");
1723                 if (xMozSnoozeTime != null) {
1724                     updates.add(Field.createFieldUpdate("xmozsnoozetime", xMozSnoozeTime));
1725                 }
1726                 return updates;
1727             }
1728 
1729             // if we are not organizer, update only reminder info
1730             if (!vCalendar.isMeeting() || vCalendar.isMeetingOrganizer()) {
1731                 // TODO: update all event fields and handle other occurrences
1732                 updates.add(Field.createFieldUpdate("dtstart", convertCalendarDateToExchange(vEvent.getPropertyValue("DTSTART"))));
1733                 updates.add(Field.createFieldUpdate("dtend", convertCalendarDateToExchange(vEvent.getPropertyValue("DTEND"))));
1734                 if ("Exchange2007_SP1".equals(serverVersion)) {
1735                     String meetingtimezone = resolveCalendarTimezone(vEvent, "DTSTART");
1736                     if (meetingtimezone != null) {
1737                         updates.add(Field.createFieldUpdate("meetingtimezone", meetingtimezone));
1738                     }
1739                 } else {
1740                     String starttimezone = resolveCalendarTimezone(vEvent, "DTSTART");
1741                     String endtimezone = starttimezone;
1742                     if (vEvent.getProperty("DTEND") != null) {
1743                         endtimezone = resolveCalendarTimezone(vEvent, "DTEND");
1744                     }
1745                     if (starttimezone != null) {
1746                         updates.add(Field.createFieldUpdate("starttimezone", starttimezone));
1747                     }
1748                     if (endtimezone != null) {
1749                         updates.add(Field.createFieldUpdate("endtimezone", endtimezone));
1750                     }
1751                 }
1752 
1753                 String status = statusToBusyStatusMap.get(vEvent.getPropertyValue("STATUS"));
1754                 if (status != null) {
1755                     updates.add(Field.createFieldUpdate("busystatus", status));
1756                 }
1757 
1758                 updates.add(Field.createFieldUpdate("isalldayevent", Boolean.toString(vCalendar.isCdoAllDay())));
1759 
1760                 String eventClass = vEvent.getPropertyValue("CLASS");
1761                 if ("PRIVATE".equals(eventClass)) {
1762                     eventClass = "Private";
1763                 } else if ("CONFIDENTIAL".equals(eventClass)) {
1764                     eventClass = "Confidential";
1765                 } else {
1766                     // PUBLIC
1767                     eventClass = "Normal";
1768                 }
1769                 updates.add(Field.createFieldUpdate("itemsensitivity", eventClass));
1770 
1771                 updates.add(Field.createFieldUpdate("description", vEvent.getPropertyValue("DESCRIPTION")));
1772                 updates.add(Field.createFieldUpdate("subject", vEvent.getPropertyValue("SUMMARY")));
1773                 updates.add(Field.createFieldUpdate("location", vEvent.getPropertyValue("LOCATION")));
1774                 // Collect categories on multiple lines
1775                 List<VProperty> categories = vEvent.getProperties("CATEGORIES");
1776                 if (categories != null) {
1777                     HashSet<String> categoryValues = new HashSet<>();
1778                     for (VProperty category : categories) {
1779                         categoryValues.add(category.getValue());
1780                     }
1781                     updates.add(Field.createFieldUpdate("keywords", StringUtil.join(categoryValues, ",")));
1782                 }
1783 
1784                 convertRruleToRecurrenceFieldUpdate(vEvent, updates);
1785 
1786                 MultiValuedFieldUpdate requiredAttendees = new MultiValuedFieldUpdate(Field.get("requiredattendees"));
1787                 MultiValuedFieldUpdate optionalAttendees = new MultiValuedFieldUpdate(Field.get("optionalattendees"));
1788 
1789                 updates.add(requiredAttendees);
1790                 updates.add(optionalAttendees);
1791 
1792                 List<VProperty> attendees = vEvent.getProperties("ATTENDEE");
1793                 if (attendees != null) {
1794                     for (VProperty property : attendees) {
1795                         String attendeeEmail = vCalendar.getEmailValue(property);
1796                         if (attendeeEmail != null && attendeeEmail.indexOf('@') >= 0) {
1797                             if (!vCalendar.getCalendarEmail().equals(attendeeEmail)) {
1798                                 String attendeeRole = property.getParamValue("ROLE");
1799                                 if ("REQ-PARTICIPANT".equals(attendeeRole)) {
1800                                     requiredAttendees.addValue(attendeeEmail);
1801                                 } else {
1802                                     optionalAttendees.addValue(attendeeEmail);
1803                                 }
1804                             }
1805                         }
1806                     }
1807                 }
1808 
1809                 // store mozilla invitations option
1810                 String xMozSendInvitations = vCalendar.getFirstVeventPropertyValue("X-MOZ-SEND-INVITATIONS");
1811                 if (xMozSendInvitations != null) {
1812                     updates.add(Field.createFieldUpdate("xmozsendinvitations", xMozSendInvitations));
1813                 }
1814             }
1815 
1816             // TODO: check with recurrence
1817             updates.add(Field.createFieldUpdate("reminderset", String.valueOf(vCalendar.hasVAlarm())));
1818             if (vCalendar.hasVAlarm()) {
1819                 updates.add(Field.createFieldUpdate("reminderminutesbeforestart", vCalendar.getReminderMinutesBeforeStart()));
1820             }
1821 
1822             // handle mozilla alarm
1823             String xMozLastack = vCalendar.getFirstVeventPropertyValue("X-MOZ-LASTACK");
1824             if (xMozLastack != null) {
1825                 updates.add(Field.createFieldUpdate("xmozlastack", xMozLastack));
1826             }
1827             String xMozSnoozeTime = vCalendar.getFirstVeventPropertyValue("X-MOZ-SNOOZE-TIME");
1828             if (xMozSnoozeTime != null) {
1829                 updates.add(Field.createFieldUpdate("xmozsnoozetime", xMozSnoozeTime));
1830             }
1831 
1832             return updates;
1833         }
1834 
1835         private void convertRruleToRecurrenceFieldUpdate(VObject vEvent, List<FieldUpdate> updates) throws DavMailException {
1836             VProperty rrule = vEvent.getProperty("RRULE");
1837             if (rrule != null) {
1838                 RecurrenceFieldUpdate recurrenceFieldUpdate = new RecurrenceFieldUpdate();
1839                 String freq = null;
1840                 String byDay = null;
1841                 String byMonthDay = null;
1842                 String byMonth = null;
1843                 List<String> rruleValues = rrule.getValues();
1844                 for (String rruleValue : rruleValues) {
1845                     int index = rruleValue.indexOf("=");
1846                     if (index >= 0) {
1847                         String key = rruleValue.substring(0, index);
1848                         String value = rruleValue.substring(index + 1);
1849                         switch (key) {
1850                             case "FREQ":
1851                                 freq = value;
1852                                 break;
1853                             case "UNTIL":
1854                                 String untilValue = value.endsWith("Z") ? value.substring(0, value.length() - 1) : value;
1855                                 recurrenceFieldUpdate.setEndDate(parseDateFromExchange(convertCalendarDateToExchange(untilValue) + "Z"));
1856                                 break;
1857                             case "COUNT":
1858                                 recurrenceFieldUpdate.setCount(value);
1859                                 break;
1860                             case "BYDAY":
1861                                 byDay = value;
1862                                 break;
1863                             case "BYMONTHDAY":
1864                                 byMonthDay = value;
1865                                 break;
1866                             case "BYMONTH":
1867                                 byMonth = value;
1868                                 break;
1869                             case "INTERVAL":
1870                                 recurrenceFieldUpdate.setRecurrenceInterval(value);
1871                                 break;
1872                             case "WKST":
1873                                 String wkstDay = RecurrenceFieldUpdate.calDayToDayOfWeek.get(value);
1874                                 if (wkstDay != null) {
1875                                     recurrenceFieldUpdate.setFirstDayOfWeek(wkstDay);
1876                                 }
1877                                 break;
1878                         }
1879                     }
1880                 }
1881                 // determine if BYDAY contains a positional prefix (relative pattern, e.g. "2MO", "-1FR")
1882                 boolean isRelative = byDay != null && Character.isDigit(byDay.charAt(0)) ||
1883                         byDay != null && byDay.charAt(0) == '-';
1884                 if ("MONTHLY".equals(freq)) {
1885                     if (isRelative) {
1886                         recurrenceFieldUpdate.setRecurrencePattern(RecurrenceFieldUpdate.RecurrencePattern.RelativeMonthlyRecurrence);
1887                         recurrenceFieldUpdate.setDayOfWeekIndexFromByDay(byDay);
1888                     } else {
1889                         recurrenceFieldUpdate.setRecurrencePattern(RecurrenceFieldUpdate.RecurrencePattern.AbsoluteMonthlyRecurrence);
1890                         if (byMonthDay != null) recurrenceFieldUpdate.setDayOfMonth(byMonthDay);
1891                     }
1892                 } else if ("YEARLY".equals(freq)) {
1893                     if (isRelative) {
1894                         recurrenceFieldUpdate.setRecurrencePattern(RecurrenceFieldUpdate.RecurrencePattern.RelativeYearlyRecurrence);
1895                         recurrenceFieldUpdate.setDayOfWeekIndexFromByDay(byDay);
1896                     } else {
1897                         recurrenceFieldUpdate.setRecurrencePattern(RecurrenceFieldUpdate.RecurrencePattern.AbsoluteYearlyRecurrence);
1898                         if (byMonthDay != null) recurrenceFieldUpdate.setDayOfMonth(byMonthDay);
1899                     }
1900                     if (byMonth != null) recurrenceFieldUpdate.setMonthFromByMonth(byMonth);
1901                 } else if (freq != null) {
1902                     recurrenceFieldUpdate.setRecurrencePattern(freq);
1903                 }
1904                 if (byDay != null && (isRelative || "WEEKLY".equals(freq) || "DAILY".equals(freq))) {
1905                     recurrenceFieldUpdate.setByDay(byDay.split(","));
1906                 }
1907                 recurrenceFieldUpdate.setStartDate(parseDateFromExchange(convertCalendarDateToExchange(vEvent.getPropertyValue("DTSTART")) + "Z"));
1908                 updates.add(recurrenceFieldUpdate);
1909             }
1910         }
1911 
1912         @Override
1913         public ItemResult createOrUpdate() throws IOException {
1914             if (vCalendar.isTodo() && isMainCalendar(folderPath)) {
1915                 // task item, move to tasks folder
1916                 folderPath = TASKS;
1917             }
1918 
1919             ItemResult itemResult = new ItemResult();
1920             EWSMethod createOrUpdateItemMethod = null;
1921 
1922             // first try to load existing event
1923             String currentEtag = null;
1924             ItemId currentItemId = null;
1925             boolean isMeetingResponse = false;
1926             boolean isMozSendInvitations = true;
1927             boolean isMozDismiss = false;
1928 
1929             HashMap<ItemId, String> responseStatusUpdates = null;
1930 
1931             HashSet<String> itemRequestProperties = CALENDAR_ITEM_REQUEST_PROPERTIES;
1932             if (vCalendar.isTodo()) {
1933                 itemRequestProperties = EVENT_REQUEST_PROPERTIES;
1934             }
1935 
1936             boolean isOrganizer = vCalendar.isOrganizer();
1937             if (isOrganizer) {
1938                 LOGGER.debug(vCalendar.getCalendarEmail() +" is meeting organizer");
1939             }
1940 
1941             EWSMethod.Item currentItem = getEwsItem(folderPath, itemName, itemRequestProperties);
1942             if (currentItem == null) {
1943                 boolean isMeeting = vCalendar.isMeeting();
1944                 if (isMeeting && !isOrganizer) {
1945                     throw new IOException("Detected meeting response, but event does not exist, aborting");
1946                 }
1947             } else {
1948                 currentItemId = new ItemId(currentItem);
1949                 currentEtag = currentItem.get(Field.get("etag").getResponseName());
1950 
1951                 responseStatusUpdates = buildResponseStatusUpdates(currentItem, vCalendar);
1952 
1953                 isMeetingResponse = vCalendar.isMeeting() && !isOrganizer && !responseStatusUpdates.isEmpty();
1954 
1955 
1956                 // Check mozilla last ack and snooze
1957                 String newmozlastack = vCalendar.getFirstVeventPropertyValue("X-MOZ-LASTACK");
1958                 String currentmozlastack = currentItem.get(Field.get("xmozlastack").getResponseName());
1959                 boolean ismozack = newmozlastack != null && !newmozlastack.equals(currentmozlastack);
1960 
1961                 String newmozsnoozetime = vCalendar.getFirstVeventPropertyValue("X-MOZ-SNOOZE-TIME");
1962                 String currentmozsnoozetime = currentItem.get(Field.get("xmozsnoozetime").getResponseName());
1963                 boolean ismozsnooze = newmozsnoozetime != null && !newmozsnoozetime.equals(currentmozsnoozetime);
1964 
1965                 isMozSendInvitations = (newmozlastack == null && newmozsnoozetime == null) // not thunderbird
1966                         || !(ismozack || ismozsnooze);
1967                 isMozDismiss = ismozack || ismozsnooze;
1968 
1969                 LOGGER.debug("Existing item found with etag: " + currentEtag + " client etag: " + etag + " id: " + currentItemId.id);
1970             }
1971             if (isMeetingResponse) {
1972                 LOGGER.debug("Ignore etag check, meeting response");
1973             } else if ("*".equals(noneMatch) && !Settings.getBooleanProperty("davmail.ignoreNoneMatchStar", true)) {
1974                 // create requested
1975                 //noinspection VariableNotUsedInsideIf
1976                 if (currentItemId != null) {
1977                     itemResult.status = HttpStatus.SC_PRECONDITION_FAILED;
1978                     return itemResult;
1979                 }
1980             } else if (etag != null) {
1981                 // update requested
1982                 if (currentItemId == null || !etag.equals(currentEtag)) {
1983                     itemResult.status = HttpStatus.SC_PRECONDITION_FAILED;
1984                     return itemResult;
1985                 }
1986             }
1987 
1988             // by default no notifications
1989             SendMeetingInvitationsOrCancellations sendMeetingInvitationsOrCancellations = SendMeetingInvitationsOrCancellations.SendToNone;
1990 
1991             if (vCalendar.isTodo()) {
1992                 // create or update task method
1993                 EWSMethod.Item newItem = new EWSMethod.Item();
1994                 newItem.type = "Task";
1995                 List<FieldUpdate> updates = new ArrayList<>();
1996                 updates.add(Field.createFieldUpdate("importance", convertPriorityToExchange(vCalendar.getFirstVeventPropertyValue("PRIORITY"))));
1997                 updates.add(Field.createFieldUpdate("calendaruid", vCalendar.getFirstVeventPropertyValue("UID")));
1998                 // force urlcompname
1999                 updates.add(Field.createFieldUpdate("urlcompname", convertItemNameToEML(itemName)));
2000                 updates.add(Field.createFieldUpdate("subject", vCalendar.getFirstVeventPropertyValue("SUMMARY")));
2001                 updates.add(Field.createFieldUpdate("description", vCalendar.getFirstVeventPropertyValue("DESCRIPTION")));
2002 
2003                 // handle multiple keywords/categories
2004                 List<VProperty> categories = vCalendar.getFirstVeventProperties("CATEGORIES");
2005                 if (categories != null) {
2006                     HashSet<String> categoryValues = new HashSet<>();
2007                     for (VProperty category : categories) {
2008                         categoryValues.add(category.getValue());
2009                     }
2010                     updates.add(Field.createFieldUpdate("keywords", StringUtil.join(categoryValues, ",")));
2011                 }
2012 
2013                 updates.add(Field.createFieldUpdate("startdate", convertTaskDateToZulu(vCalendar.getFirstVeventPropertyValue("DTSTART"))));
2014                 updates.add(Field.createFieldUpdate("duedate", convertTaskDateToZulu(vCalendar.getFirstVeventPropertyValue("DUE"))));
2015                 updates.add(Field.createFieldUpdate("datecompleted", convertTaskDateToZulu(vCalendar.getFirstVeventPropertyValue("COMPLETED"))));
2016 
2017                 updates.add(Field.createFieldUpdate("commonstart", convertTaskDateToZulu(vCalendar.getFirstVeventPropertyValue("DTSTART"))));
2018                 updates.add(Field.createFieldUpdate("commonend", convertTaskDateToZulu(vCalendar.getFirstVeventPropertyValue("DUE"))));
2019 
2020                 String percentComplete = vCalendar.getFirstVeventPropertyValue("PERCENT-COMPLETE");
2021                 if (percentComplete == null) {
2022                     percentComplete = "0";
2023                 }
2024                 updates.add(Field.createFieldUpdate("percentcomplete", percentComplete));
2025                 String vTodoStatus = vCalendar.getFirstVeventPropertyValue("STATUS");
2026                 if (vTodoStatus == null) {
2027                     updates.add(Field.createFieldUpdate("taskstatus", "NotStarted"));
2028                 } else {
2029                     updates.add(Field.createFieldUpdate("taskstatus", vTodoToTaskStatusMap.get(vTodoStatus)));
2030                 }
2031 
2032                 //updates.add(Field.createFieldUpdate("iscomplete", "COMPLETED".equals(vTodoStatus)?"True":"False"));
2033 
2034                 if (currentItemId != null) {
2035                     // update
2036                     createOrUpdateItemMethod = new UpdateItemMethod(MessageDisposition.SaveOnly,
2037                             ConflictResolution.AutoResolve,
2038                             SendMeetingInvitationsOrCancellations.SendToNone,
2039                             currentItemId, updates);
2040                 } else {
2041                     newItem.setFieldUpdates(updates);
2042                     // create
2043                     createOrUpdateItemMethod = new CreateItemMethod(MessageDisposition.SaveOnly, SendMeetingInvitations.SendToNone, getFolderId(folderPath), newItem);
2044                 }
2045 
2046             } else {
2047 
2048                 // update existing item
2049                 if (currentItemId != null) {
2050                     boolean isShared = !email.equalsIgnoreCase(vCalendar.getCalendarEmail());
2051                     if (isMeetingResponse && Settings.getBooleanProperty("davmail.caldavAutoSchedule", true)) {
2052                         // meeting response with server managed notifications
2053                         SendMeetingInvitations sendMeetingInvitations = SendMeetingInvitations.SendToAllAndSaveCopy;
2054                         MessageDisposition messageDisposition = MessageDisposition.SendAndSaveCopy;
2055 
2056                         if (responseStatusUpdates.isEmpty()) {
2057                             throw new IOException("No response status updates found");
2058                         }
2059                         // assume a single meeting response
2060                         Map.Entry<ItemId, String> responseStatusUpdate = responseStatusUpdates.entrySet().iterator().next();
2061                         String attendeeStatus = responseStatusUpdate.getValue();
2062                         ItemId instanceItemId = responseStatusUpdate.getKey();
2063 
2064                         String body = null;
2065                         // This is a meeting response, let user edit notification message
2066                         if (Settings.getBooleanProperty("davmail.caldavEditNotifications")) {
2067                             String vEventSubject = vCalendar.getFirstVeventPropertyValue("SUMMARY");
2068                             if (vEventSubject == null) {
2069                                 vEventSubject = BundleMessage.format("MEETING_REQUEST");
2070                             }
2071 
2072                             String notificationSubject = (attendeeStatus != null) ? (BundleMessage.format(attendeeStatus) + vEventSubject) : subject;
2073 
2074                             NotificationDialog notificationDialog = new NotificationDialog(notificationSubject, "");
2075                             if (!notificationDialog.getSendNotification()) {
2076                                 LOGGER.debug("Notification canceled by user");
2077                                 sendMeetingInvitations = SendMeetingInvitations.SendToNone;
2078                                 messageDisposition = MessageDisposition.SaveOnly;
2079                             }
2080                             // get description from dialog
2081                             body = notificationDialog.getBody();
2082                         }
2083                         EWSMethod.Item item = new EWSMethod.Item();
2084 
2085                         item.type = partstatToResponseMap.get(attendeeStatus);
2086                         item.referenceItemId = new ItemId("ReferenceItemId", instanceItemId.id, instanceItemId.changeKey);
2087                         if (body != null && !body.isEmpty()) {
2088                             item.put("Body", body);
2089                         }
2090                         createOrUpdateItemMethod = new CreateItemMethod(messageDisposition,
2091                                 sendMeetingInvitations,
2092                                 getFolderId(SENT),
2093                                 item
2094                         );
2095 
2096                     } else if (Settings.getBooleanProperty("davmail.caldavAutoSchedule", true)) {
2097                         // other changes with server side managed notifications
2098                         MessageDisposition messageDisposition = MessageDisposition.SaveOnly;
2099 
2100                         if (vCalendar.isMeeting() && vCalendar.isMeetingOrganizer() && isMozSendInvitations) {
2101                             messageDisposition = MessageDisposition.SendAndSaveCopy;
2102                             sendMeetingInvitationsOrCancellations = SendMeetingInvitationsOrCancellations.SendToAllAndSaveCopy;
2103                         }
2104                         createOrUpdateItemMethod = new UpdateItemMethod(messageDisposition,
2105                                 ConflictResolution.AutoResolve,
2106                                 sendMeetingInvitationsOrCancellations,
2107                                 currentItemId, buildFieldUpdates(vCalendar, vCalendar.getFirstVevent(), isMozDismiss));
2108 
2109                         // force context Timezone on Exchange 2010 and 2013
2110                         if (serverVersion != null && serverVersion.startsWith("Exchange201")) {
2111                             createOrUpdateItemMethod.setTimezoneContext(EwsExchangeSession.this.getVTimezone().getPropertyValue("TZID"));
2112                         }
2113                     } else {
2114                         // old hard/delete approach on update, used with client side notifications
2115                         DeleteItemMethod deleteItemMethod = new DeleteItemMethod(currentItemId, DeleteType.HardDelete, SendMeetingCancellations.SendToNone);
2116                         executeMethod(deleteItemMethod);
2117                     }
2118                 }
2119 
2120                 if (createOrUpdateItemMethod == null) {
2121                     // create item from VCALENDAR content, set additional properties as workaround to Exchange handling issues
2122                     EWSMethod.Item newItem = new EWSMethod.Item();
2123                     newItem.type = "CalendarItem";
2124                     newItem.mimeContent = IOUtil.encodeBase64(vCalendar.toString());
2125                     ArrayList<FieldUpdate> updates = new ArrayList<>();
2126                     if (!vCalendar.hasVAlarm()) {
2127                         updates.add(Field.createFieldUpdate("reminderset", "false"));
2128                     }
2129                     //updates.add(Field.createFieldUpdate("outlookmessageclass", "IPM.Appointment"));
2130                     // force urlcompname
2131                     updates.add(Field.createFieldUpdate("urlcompname", convertItemNameToEML(itemName)));
2132                     if (vCalendar.isMeeting()) {
2133                         if (vCalendar.isMeetingOrganizer()) {
2134                             updates.add(Field.createFieldUpdate("apptstateflags", "1"));
2135                         } else {
2136                             updates.add(Field.createFieldUpdate("apptstateflags", "3"));
2137                         }
2138                     } else {
2139                         updates.add(Field.createFieldUpdate("apptstateflags", "0"));
2140                     }
2141                     // store mozilla invitations option
2142                     String xMozSendInvitations = vCalendar.getFirstVeventPropertyValue("X-MOZ-SEND-INVITATIONS");
2143                     if (xMozSendInvitations != null) {
2144                         updates.add(Field.createFieldUpdate("xmozsendinvitations", xMozSendInvitations));
2145                     }
2146                     // handle mozilla alarm
2147                     String xMozLastack = vCalendar.getFirstVeventPropertyValue("X-MOZ-LASTACK");
2148                     if (xMozLastack != null) {
2149                         updates.add(Field.createFieldUpdate("xmozlastack", xMozLastack));
2150                     }
2151                     String xMozSnoozeTime = vCalendar.getFirstVeventPropertyValue("X-MOZ-SNOOZE-TIME");
2152                     if (xMozSnoozeTime != null) {
2153                         updates.add(Field.createFieldUpdate("xmozsnoozetime", xMozSnoozeTime));
2154                     }
2155 
2156                     if (vCalendar.isMeeting() && "Exchange2007_SP1".equals(serverVersion)) {
2157                         // Exchange 2007 does not properly handle attendees
2158                         Set<String> requiredAttendees = new HashSet<>();
2159                         Set<String> optionalAttendees = new HashSet<>();
2160                         List<VProperty> attendeeProperties = vCalendar.getFirstVeventProperties("ATTENDEE");
2161                         if (attendeeProperties != null) {
2162                             for (VProperty property : attendeeProperties) {
2163                                 String attendeeEmail = vCalendar.getEmailValue(property);
2164                                 if (attendeeEmail != null && attendeeEmail.indexOf('@') >= 0) {
2165                                     InternetAddress internetAddress = new InternetAddress(attendeeEmail, property.getParamValue("CN"));
2166                                     String attendeeRole = property.getParamValue("ROLE");
2167                                     if ("REQ-PARTICIPANT".equals(attendeeRole)) {
2168                                         requiredAttendees.add(internetAddress.toString());
2169                                     } else {
2170                                         optionalAttendees.add(internetAddress.toString());
2171                                     }
2172                                 }
2173                             }
2174                         }
2175                         // add organizer as FROM recipient
2176                         List<VProperty> organizerProperties = vCalendar.getFirstVeventProperties("ORGANIZER");
2177                         if (organizerProperties != null) {
2178                             VProperty property = organizerProperties.get(0);
2179                             String organizerEmail = vCalendar.getEmailValue(property);
2180                             if (organizerEmail != null && organizerEmail.indexOf('@') >= 0) {
2181                                 updates.add(Field.createFieldUpdate("from", organizerEmail));
2182                             }
2183                         }
2184 
2185                         // add required attendes as TO recipients
2186                         if (!requiredAttendees.isEmpty()) {
2187                             updates.add(Field.createFieldUpdate("to", StringUtil.join(requiredAttendees, ", ")));
2188                         }
2189                         // add optional attendees as CC recipients
2190                         if (!optionalAttendees.isEmpty()) {
2191                             updates.add(Field.createFieldUpdate("cc", StringUtil.join(optionalAttendees, ", ")));
2192                         }
2193                     }
2194 
2195                     // patch allday date values, only on 2007
2196                     if ("Exchange2007_SP1".equals(serverVersion) && vCalendar.isCdoAllDay()) {
2197                         updates.add(Field.createFieldUpdate("dtstart", convertCalendarDateToExchange(vCalendar.getFirstVeventPropertyValue("DTSTART"))));
2198                         updates.add(Field.createFieldUpdate("dtend", convertCalendarDateToExchange(vCalendar.getFirstVeventPropertyValue("DTEND"))));
2199                     }
2200 
2201                     String status = vCalendar.getFirstVeventPropertyValue("STATUS");
2202                     if ("TENTATIVE".equals(status)) {
2203                         // this is a tentative event
2204                         updates.add(Field.createFieldUpdate("busystatus", "Tentative"));
2205                     } else {
2206                         // otherwise, we use the same value as before, as received from the server
2207                         // however, the case matters, so we still have to transform it "BUSY" -> "Busy"
2208                         updates.add(Field.createFieldUpdate("busystatus", "BUSY".equals(vCalendar.getFirstVeventPropertyValue("X-MICROSOFT-CDO-BUSYSTATUS")) ? "Busy" : "Free"));
2209                     }
2210 
2211                     if ("Exchange2007_SP1".equals(serverVersion) && vCalendar.isCdoAllDay()) {
2212                         updates.add(Field.createFieldUpdate("meetingtimezone", vCalendar.getVTimezone().getPropertyValue("TZID")));
2213                     }
2214 
2215                     newItem.setFieldUpdates(updates);
2216                     MessageDisposition messageDisposition = MessageDisposition.SaveOnly;
2217                     SendMeetingInvitations sendMeetingInvitations = SendMeetingInvitations.SendToNone;
2218                     if (vCalendar.isMeeting() && vCalendar.isMeetingOrganizer() && isMozSendInvitations
2219                             && Settings.getBooleanProperty("davmail.caldavAutoSchedule", true)) {
2220                         // meeting request creation with server managed notifications
2221                         messageDisposition = MessageDisposition.SendAndSaveCopy;
2222                         sendMeetingInvitations = SendMeetingInvitations.SendToAllAndSaveCopy;
2223                     }
2224                     createOrUpdateItemMethod = new CreateItemMethod(messageDisposition, sendMeetingInvitations, getFolderId(folderPath), newItem);
2225                     // force context Timezone on Exchange 2010 and 2013
2226                     if (serverVersion != null && serverVersion.startsWith("Exchange201")) {
2227                         createOrUpdateItemMethod.setTimezoneContext(EwsExchangeSession.this.getVTimezone().getPropertyValue("TZID"));
2228                     }
2229                 }
2230             }
2231 
2232             executeMethod(createOrUpdateItemMethod);
2233 
2234             itemResult.status = createOrUpdateItemMethod.getStatusCode();
2235             if (itemResult.status == HttpURLConnection.HTTP_OK) {
2236                 //noinspection VariableNotUsedInsideIf
2237                 if (currentItemId == null) {
2238                     itemResult.status = HttpStatus.SC_CREATED;
2239                     LOGGER.debug("Created event " + getHref());
2240                 } else if (isMeetingResponse) {
2241                     LOGGER.debug("Sent meeting response for event " + getHref());
2242                 } else {
2243                     LOGGER.debug("Updated event " + getHref());
2244                 }
2245             }
2246 
2247             // handle deleted occurrences
2248             if (!vCalendar.isTodo() && currentItemId != null && !isMeetingResponse && !isMozDismiss) {
2249                 handleExcludedDates(currentItemId, vCalendar);
2250                 handleModifiedOccurrences(currentItemId, vCalendar, sendMeetingInvitationsOrCancellations);
2251             }
2252 
2253 
2254             // update etag
2255             if (createOrUpdateItemMethod.getResponseItem() != null) {
2256                 ItemId newItemId = new ItemId(createOrUpdateItemMethod.getResponseItem());
2257                 GetItemMethod getItemMethod = new GetItemMethod(BaseShape.ID_ONLY, newItemId, false);
2258                 getItemMethod.addAdditionalProperty(Field.get("etag"));
2259                 executeMethod(getItemMethod);
2260                 itemResult.etag = getItemMethod.getResponseItem().get(Field.get("etag").getResponseName());
2261                 itemResult.itemName = StringUtil.base64ToUrl(newItemId.id) + ".EML";
2262             }
2263 
2264             return itemResult;
2265 
2266         }
2267 
2268         private HashMap<ItemId, String> buildResponseStatusUpdates(EWSMethod.Item masterEvent, VCalendar vCalendar) throws IOException {
2269             HashMap<String, String> attendeeOccurrenceStatusMap = vCalendar.getAttendeeOccurrenceStatusMap();
2270             HashMap<ItemId, String> responseStatusUpdates = new HashMap<>();
2271 
2272             if (!attendeeOccurrenceStatusMap.isEmpty()) {
2273                 for (Map.Entry<String, String> entry : attendeeOccurrenceStatusMap.entrySet()) {
2274                     String instanceId = entry.getKey();
2275                     String attendeeStatus = entry.getValue();
2276                     EWSMethod.Item occurrence;
2277                     if ("master".equals(instanceId)) {
2278                         occurrence = masterEvent;
2279                     } else {
2280                         occurrence = findOccurrenceItemId(new ItemId(masterEvent), instanceId);
2281                     }
2282                     if (occurrence != null) {
2283                         ItemId occurrenceId = new ItemId(occurrence);
2284                         String currentAttendeeStatus = responseTypeToPartstatMap.get(occurrence.get("MyResponseType"));
2285                         if (!attendeeStatus.equals(currentAttendeeStatus)) {
2286                             LOGGER.debug("Attendee " + vCalendar.getCalendarEmail() + " status " + currentAttendeeStatus + " => " + attendeeStatus + " on instance " + instanceId);
2287                             responseStatusUpdates.put(occurrenceId, attendeeStatus);
2288                         } else {
2289                             LOGGER.debug("Attendee " + vCalendar.getCalendarEmail() + " status unchanged " + currentAttendeeStatus + " on instance " + instanceId);
2290                         }
2291                     } else {
2292                         throw new IOException("Unable to find occurrence for id " + instanceId);
2293                     }
2294                 }
2295             }
2296             return responseStatusUpdates;
2297         }
2298 
2299         @Override
2300         public byte[] getEventContent() throws IOException {
2301             byte[] content;
2302             if (LOGGER.isDebugEnabled()) {
2303                 LOGGER.debug("Get event: " + itemName);
2304             }
2305             try {
2306                 GetItemMethod getItemMethod;
2307                 if ("Task".equals(type)) {
2308                     getItemMethod = new GetItemMethod(BaseShape.ID_ONLY, itemId, false);
2309                     getItemMethod.addAdditionalProperty(Field.get("importance"));
2310                     getItemMethod.addAdditionalProperty(Field.get("subject"));
2311                     getItemMethod.addAdditionalProperty(Field.get("created"));
2312                     getItemMethod.addAdditionalProperty(Field.get("lastmodified"));
2313                     getItemMethod.addAdditionalProperty(Field.get("calendaruid"));
2314                     getItemMethod.addAdditionalProperty(Field.get("description"));
2315                     if (isExchange2013OrLater()) {
2316                         getItemMethod.addAdditionalProperty(Field.get("textbody"));
2317                     }
2318                     getItemMethod.addAdditionalProperty(Field.get("percentcomplete"));
2319                     getItemMethod.addAdditionalProperty(Field.get("taskstatus"));
2320                     getItemMethod.addAdditionalProperty(Field.get("startdate"));
2321                     getItemMethod.addAdditionalProperty(Field.get("duedate"));
2322                     getItemMethod.addAdditionalProperty(Field.get("datecompleted"));
2323                     getItemMethod.addAdditionalProperty(Field.get("keywords"));
2324 
2325                 } else if (!"Message".equals(type)
2326                         && !"MeetingCancellation".equals(type)
2327                         && !"MeetingResponse".equals(type)) {
2328                     getItemMethod = new GetItemMethod(BaseShape.ID_ONLY, itemId, true);
2329                     getItemMethod.addAdditionalProperty(Field.get("lastmodified"));
2330                     getItemMethod.addAdditionalProperty(Field.get("reminderset"));
2331                     getItemMethod.addAdditionalProperty(Field.get("calendaruid"));
2332                     getItemMethod.addAdditionalProperty(Field.get("myresponsetype"));
2333                     getItemMethod.addAdditionalProperty(Field.get("organizer"));
2334                     getItemMethod.addAdditionalProperty(Field.get("requiredattendees"));
2335                     getItemMethod.addAdditionalProperty(Field.get("optionalattendees"));
2336                     getItemMethod.addAdditionalProperty(Field.get("modifiedoccurrences"));
2337                     getItemMethod.addAdditionalProperty(Field.get("xmozlastack"));
2338                     getItemMethod.addAdditionalProperty(Field.get("xmozsnoozetime"));
2339                     getItemMethod.addAdditionalProperty(Field.get("xmozsendinvitations"));
2340                 } else {
2341                     getItemMethod = new GetItemMethod(BaseShape.ID_ONLY, itemId, true);
2342                 }
2343 
2344                 executeMethod(getItemMethod);
2345                 if ("Task".equals(type)) {
2346                     VCalendar localVCalendar = new VCalendar();
2347                     localVCalendar.setPropertyValue("VERSION", "2.0");
2348                     VObject vTodo = new VObject();
2349                     vTodo.type = "VTODO";
2350                     localVCalendar.setTimezone(getVTimezone());
2351                     vTodo.setPropertyValue("LAST-MODIFIED", convertDateFromExchange(getItemMethod.getResponseItem().get(Field.get("lastmodified").getResponseName())));
2352                     vTodo.setPropertyValue("CREATED", convertDateFromExchange(getItemMethod.getResponseItem().get(Field.get("created").getResponseName())));
2353                     String calendarUid = getItemMethod.getResponseItem().get(Field.get("calendaruid").getResponseName());
2354                     if (calendarUid == null) {
2355                         // use item id as uid for Exchange created tasks
2356                         calendarUid = itemId.id;
2357                     }
2358                     vTodo.setPropertyValue("UID", calendarUid);
2359                     vTodo.setPropertyValue("SUMMARY", getItemMethod.getResponseItem().get(Field.get("subject").getResponseName()));
2360                     String description = getItemMethod.getResponseItem().get(Field.get("description").getResponseName());
2361                     if (description == null) {
2362                         // Exchange 2013: try to get description from body
2363                         description = getItemMethod.getResponseItem().get(Field.get("textbody").getResponseName());
2364                     }
2365                     vTodo.setPropertyValue("DESCRIPTION", description);
2366                     vTodo.setPropertyValue("PRIORITY", convertPriorityFromExchange(getItemMethod.getResponseItem().get(Field.get("importance").getResponseName())));
2367                     vTodo.setPropertyValue("PERCENT-COMPLETE", getItemMethod.getResponseItem().get(Field.get("percentcomplete").getResponseName()));
2368                     vTodo.setPropertyValue("STATUS", taskTovTodoStatusMap.get(getItemMethod.getResponseItem().get(Field.get("taskstatus").getResponseName())));
2369 
2370                     String taskDueDate = convertDateFromExchangeToTaskDate(getItemMethod.getResponseItem().get(Field.get("duedate").getResponseName()));
2371                     String taskStartDate = convertDateFromExchangeToTaskDate(getItemMethod.getResponseItem().get(Field.get("startdate").getResponseName()));
2372 
2373                     vTodo.setPropertyValue("DUE;VALUE=DATE", taskDueDate);
2374 
2375                     if (taskDueDate != null && taskStartDate != null && taskStartDate.compareTo(taskDueDate) > 0) {
2376                         LOGGER.warn("Task start date " + taskStartDate + " is after due date " + taskDueDate);
2377                     } else {
2378                         vTodo.setPropertyValue("DTSTART;VALUE=DATE", taskStartDate);
2379                     }
2380                     vTodo.setPropertyValue("COMPLETED;VALUE=DATE", convertDateFromExchangeToTaskDate(getItemMethod.getResponseItem().get(Field.get("datecompleted").getResponseName())));
2381 
2382                     vTodo.setPropertyValue("CATEGORIES", getItemMethod.getResponseItem().get(Field.get("keywords").getResponseName()));
2383 
2384                     localVCalendar.addVObject(vTodo);
2385                     content = localVCalendar.toString().getBytes(StandardCharsets.UTF_8);
2386                 } else {
2387                     content = getItemMethod.getMimeContent();
2388                     if (content == null) {
2389                         throw new IOException("empty event body");
2390                     }
2391                     if (!"CalendarItem".equals(type)) {
2392                         content = getICS(new SharedByteArrayInputStream(content));
2393                     }
2394                     VCalendar localVCalendar = new VCalendar(content, getCalendarEmail(folderPath), getVTimezone());
2395 
2396                     String calendaruid = getItemMethod.getResponseItem().get(Field.get("calendaruid").getResponseName());
2397 
2398                     if ("Exchange2007_SP1".equals(serverVersion)) {
2399                         // remove additional reminder
2400                         if (!"true".equals(getItemMethod.getResponseItem().get(Field.get("reminderset").getResponseName()))) {
2401                             localVCalendar.removeVAlarm();
2402                         }
2403                         if (calendaruid != null) {
2404                             localVCalendar.setFirstVeventPropertyValue("UID", calendaruid);
2405                         }
2406                     }
2407                     // Exchange does not always set organizer property properly
2408                     VProperty organizerProperty = fixOrganizer(getItemMethod, localVCalendar.getFirstVevent());
2409 
2410                     fixAttendees(getItemMethod, localVCalendar.getFirstVevent());
2411                     // fix UID and RECURRENCE-ID, broken at least on Exchange 2007
2412                     List<EWSMethod.Occurrence> occurrences = getItemMethod.getResponseItem().getOccurrences();
2413                     if (occurrences != null) {
2414                         Iterator<VObject> modifiedOccurrencesIterator = localVCalendar.getModifiedOccurrences().iterator();
2415                         for (EWSMethod.Occurrence occurrence : occurrences) {
2416                             if (modifiedOccurrencesIterator.hasNext()) {
2417                                 VObject modifiedOccurrence = modifiedOccurrencesIterator.next();
2418                                 // fix modified occurrences attendees
2419                                 GetItemMethod getOccurrenceMethod = new GetItemMethod(BaseShape.ID_ONLY, occurrence.itemId, false);
2420                                 getOccurrenceMethod.addAdditionalProperty(Field.get("requiredattendees"));
2421                                 getOccurrenceMethod.addAdditionalProperty(Field.get("optionalattendees"));
2422                                 getOccurrenceMethod.addAdditionalProperty(Field.get("modifiedoccurrences"));
2423                                 getOccurrenceMethod.addAdditionalProperty(Field.get("lastmodified"));
2424                                 getOccurrenceMethod.addAdditionalProperty(Field.get("organizer"));
2425                                 executeMethod(getOccurrenceMethod);
2426                                 if (organizerProperty != null && modifiedOccurrence.getProperties("ORGANIZER") == null) {
2427                                     // ensure organizer is set on all occurrences
2428                                     modifiedOccurrence.addProperty(organizerProperty);
2429                                 }
2430                                 fixAttendees(getOccurrenceMethod, modifiedOccurrence);
2431                                 // LAST-MODIFIED is missing in event content
2432                                 modifiedOccurrence.setPropertyValue("LAST-MODIFIED", convertDateFromExchange(getOccurrenceMethod.getResponseItem().get(Field.get("lastmodified").getResponseName())));
2433 
2434                                 // fix caldav uid, should be the same as main VEVENT
2435                                 if (calendaruid != null) {
2436                                     modifiedOccurrence.setPropertyValue("UID", calendaruid);
2437                                 }
2438 
2439                                 VProperty recurrenceId = modifiedOccurrence.getProperty("RECURRENCE-ID");
2440                                 if (recurrenceId != null) {
2441                                     recurrenceId.removeParam("TZID");
2442                                     recurrenceId.getValues().set(0, convertDateFromExchange(occurrence.originalStart));
2443                                 }
2444                             }
2445                         }
2446                     }
2447                     // LAST-MODIFIED is missing in event content
2448                     localVCalendar.setFirstVeventPropertyValue("LAST-MODIFIED", convertDateFromExchange(getItemMethod.getResponseItem().get(Field.get("lastmodified").getResponseName())));
2449 
2450                     // restore mozilla invitations option
2451                     localVCalendar.setFirstVeventPropertyValue("X-MOZ-SEND-INVITATIONS",
2452                             getItemMethod.getResponseItem().get(Field.get("xmozsendinvitations").getResponseName()));
2453                     // restore mozilla alarm status
2454                     localVCalendar.setFirstVeventPropertyValue("X-MOZ-LASTACK",
2455                             getItemMethod.getResponseItem().get(Field.get("xmozlastack").getResponseName()));
2456                     localVCalendar.setFirstVeventPropertyValue("X-MOZ-SNOOZE-TIME",
2457                             getItemMethod.getResponseItem().get(Field.get("xmozsnoozetime").getResponseName()));
2458                     // overwrite method
2459                     // localVCalendar.setPropertyValue("METHOD", "REQUEST");
2460                     content = localVCalendar.toString().getBytes(StandardCharsets.UTF_8);
2461                 }
2462             } catch (IOException | MessagingException e) {
2463                 throw buildHttpNotFoundException(e);
2464             }
2465             return content;
2466         }
2467 
2468         protected VProperty fixOrganizer(GetItemMethod getItemMethod, VObject vEvent) throws IOException {
2469             VProperty organizerProperty = null;
2470             if (getItemMethod.getResponseItem() != null) {
2471                 String myResponseType = getItemMethod.getResponseItem().get("MyResponseType");
2472                 if ("Organizer".equals(myResponseType) && vEvent.getProperties("ORGANIZER") == null) {
2473                     // user is organizer on event, but Organizer property is missing from event
2474                     String calendarEmail = getCalendarEmail(folderPath);
2475                     String organizerName = getItemMethod.getResponseItem().get("Organizer");
2476                     organizerProperty = new VProperty("ORGANIZER", "mailto:" + calendarEmail);
2477                     organizerProperty.addParam("CN", organizerName);
2478                     vEvent.addProperty(organizerProperty);
2479                 }
2480             }
2481             return organizerProperty;
2482         }
2483 
2484         protected void fixAttendees(GetItemMethod getItemMethod, VObject vEvent) throws IOException {
2485             if (getItemMethod.getResponseItem() != null) {
2486                 List<EWSMethod.Attendee> attendees = getItemMethod.getResponseItem().getAttendees();
2487                 if (attendees != null) {
2488                     if (vEvent.getProperties("ATTENDEE") != null) {
2489                         // clear Exchange created attendees
2490                         for (VProperty vAttendee : vEvent.getProperties("ATTENDEE")) {
2491                             vEvent.removeProperty(vAttendee);
2492                         }
2493                     }
2494                     // rebuild attendees from EWS information
2495                     String organizerEmail = getItemMethod.getResponseItem().get("EmailAddress");
2496                     String organizerName = getItemMethod.getResponseItem().get("Organizer");
2497                     if (vEvent.getProperties("ORGANIZER") == null && organizerEmail != null) {
2498                         // organizer is missing from event master or instance, rebuild
2499                         VProperty organizerProperty = new VProperty("ORGANIZER", "mailto:" + organizerEmail);
2500                         organizerProperty.addParam("CN", organizerName);
2501                         vEvent.addProperty(organizerProperty);
2502                     }
2503 
2504                     // Exchange did not provide attendees, rebuild from item attendees
2505                     for (EWSMethod.Attendee attendee : attendees) {
2506                         VProperty attendeeProperty = new VProperty("ATTENDEE", "mailto:" + attendee.email);
2507                         attendeeProperty.addParam("CN", attendee.name);
2508                         attendeeProperty.addParam("PARTSTAT", attendee.partstat);
2509                         attendeeProperty.addParam("ROLE", attendee.role);
2510                         vEvent.addProperty(attendeeProperty);
2511                     }
2512                 }
2513             }
2514         }
2515     }
2516 
2517     private boolean isExchange2013OrLater() {
2518         return "Exchange2013".compareTo(serverVersion) <= 0;
2519     }
2520 
2521     private EWSMethod.Item findOccurrenceItemId(ItemId masterItemId, String originalDateZulu) {
2522         int instanceIndex = 0;
2523         while (true) {
2524             instanceIndex++;
2525             try {
2526                 GetItemMethod getItemMethod = new GetItemMethod(BaseShape.ID_ONLY,
2527                         new OccurrenceItemId(masterItemId.id, instanceIndex), false);
2528                 getItemMethod.addAdditionalProperty(Field.get("originalstart"));
2529                 getItemMethod.addAdditionalProperty(Field.get("myresponsetype"));
2530                 executeMethod(getItemMethod);
2531                 if (getItemMethod.getResponseItem() != null) {
2532                     String itemOriginalStart = getItemMethod.getResponseItem().get(
2533                             Field.get("originalstart").getResponseName());
2534                     if (originalDateZulu.equals(itemOriginalStart)) {
2535                         return getItemMethod.getResponseItem();
2536                     } else if (originalDateZulu.compareTo(itemOriginalStart) < 0) {
2537                         break; // past the target date
2538                     }
2539                 }
2540             } catch (IOException e) {
2541                 break; // beyond recurrence end
2542             }
2543         }
2544         return null;
2545     }
2546 
2547     /**
2548      * Get all contacts and distribution lists in the provided folder.
2549      *
2550      * @param folderPath Exchange folder path
2551      * @return list of contacts
2552      * @throws IOException on error
2553      */
2554     @Override
2555     public List<ExchangeSession.Contact> getAllContacts(String folderPath, boolean includeDistList) throws IOException {
2556         Condition condition;
2557         if (includeDistList) {
2558             condition = or(isEqualTo("outlookmessageclass", "IPM.Contact"), isEqualTo("outlookmessageclass", "IPM.DistList"));
2559         } else {
2560             condition = isEqualTo("outlookmessageclass", "IPM.Contact");
2561         }
2562         return searchContacts(folderPath, ExchangeSession.CONTACT_ATTRIBUTES, condition, 0);
2563     }
2564 
2565     @Override
2566     public List<ExchangeSession.Contact> searchContacts(String folderPath, Set<String> attributes, Condition condition, int maxCount) throws IOException {
2567         List<ExchangeSession.Contact> contacts = new ArrayList<>();
2568         List<EWSMethod.Item> responses = searchItems(folderPath, attributes, condition,
2569                 FolderQueryTraversal.SHALLOW, maxCount);
2570 
2571         for (EWSMethod.Item response : responses) {
2572             contacts.add(new Contact(response));
2573         }
2574         return contacts;
2575     }
2576 
2577     @Override
2578     protected Condition getCalendarItemCondition(Condition dateCondition) {
2579         // tasks in calendar not supported over EWS => do not look for instancetype null
2580         return or(
2581                 // Exchange 2010
2582                 or(isTrue("isrecurring"),
2583                         and(isFalse("isrecurring"), dateCondition)),
2584                 // Exchange 2007
2585                 or(isEqualTo("instancetype", 1),
2586                         and(isEqualTo("instancetype", 0), dateCondition))
2587         );
2588     }
2589 
2590     @Override
2591     public List<ExchangeSession.Event> getEventMessages(String folderPath) throws IOException {
2592         return searchEvents(folderPath, ITEM_PROPERTIES,
2593                 and(startsWith("outlookmessageclass", "IPM.Schedule.Meeting."),
2594                         or(isNull("processed"), isFalse("processed"))));
2595     }
2596 
2597     @Override
2598     public List<ExchangeSession.Event> searchEvents(String folderPath, Set<String> attributes, Condition condition) throws IOException {
2599         List<ExchangeSession.Event> events = new ArrayList<>();
2600         List<EWSMethod.Item> responses = searchItems(folderPath, attributes,
2601                 condition,
2602                 FolderQueryTraversal.SHALLOW, 0);
2603         for (EWSMethod.Item response : responses) {
2604             Event event = new Event(folderPath, response);
2605             if ("Message".equals(event.type)) {
2606                 // TODO: just exclude
2607                 // need to check body
2608                 try {
2609                     event.getEventContent();
2610                     events.add(event);
2611                 } catch (HttpNotFoundException e) {
2612                     LOGGER.warn("Ignore invalid event " + event.getHref());
2613                 }
2614                 // exclude exceptions
2615             } else if (event.isException) {
2616                 LOGGER.debug("Exclude recurrence exception " + event.getHref());
2617             } else {
2618                 events.add(event);
2619             }
2620 
2621         }
2622 
2623         return events;
2624     }
2625 
2626     /**
2627      * Common item properties
2628      */
2629     protected static final Set<String> ITEM_PROPERTIES = new HashSet<>();
2630 
2631     static {
2632         ITEM_PROPERTIES.add("etag");
2633         ITEM_PROPERTIES.add("displayname");
2634         // calendar CdoInstanceType
2635         ITEM_PROPERTIES.add("instancetype");
2636         ITEM_PROPERTIES.add("urlcompname");
2637         ITEM_PROPERTIES.add("subject");
2638     }
2639 
2640     protected static final HashSet<String> EVENT_REQUEST_PROPERTIES = new HashSet<>();
2641 
2642     static {
2643         EVENT_REQUEST_PROPERTIES.add("permanenturl");
2644         EVENT_REQUEST_PROPERTIES.add("etag");
2645         EVENT_REQUEST_PROPERTIES.add("displayname");
2646         EVENT_REQUEST_PROPERTIES.add("subject");
2647         EVENT_REQUEST_PROPERTIES.add("urlcompname");
2648         EVENT_REQUEST_PROPERTIES.add("displayto");
2649         EVENT_REQUEST_PROPERTIES.add("displaycc");
2650 
2651         EVENT_REQUEST_PROPERTIES.add("xmozlastack");
2652         EVENT_REQUEST_PROPERTIES.add("xmozsnoozetime");
2653     }
2654 
2655     protected static final HashSet<String> CALENDAR_ITEM_REQUEST_PROPERTIES = new HashSet<>();
2656 
2657     static {
2658         CALENDAR_ITEM_REQUEST_PROPERTIES.addAll(EVENT_REQUEST_PROPERTIES);
2659         CALENDAR_ITEM_REQUEST_PROPERTIES.add("ismeeting");
2660         CALENDAR_ITEM_REQUEST_PROPERTIES.add("myresponsetype");
2661     }
2662 
2663     @Override
2664     protected Set<String> getItemProperties() {
2665         return ITEM_PROPERTIES;
2666     }
2667 
2668     protected EWSMethod.Item getEwsItem(String folderPath, String itemName, Set<String> itemProperties) throws IOException {
2669         EWSMethod.Item item = null;
2670         String urlcompname = convertItemNameToEML(itemName);
2671         // workaround for missing urlcompname in Exchange 2010
2672         if (isItemId(urlcompname)) {
2673             ItemId itemId = new ItemId(StringUtil.urlToBase64(urlcompname.substring(0, urlcompname.indexOf('.'))));
2674             GetItemMethod getItemMethod = new GetItemMethod(BaseShape.ID_ONLY, itemId, false);
2675             for (String attribute : itemProperties) {
2676                 getItemMethod.addAdditionalProperty(Field.get(attribute));
2677             }
2678             executeMethod(getItemMethod);
2679             item = getItemMethod.getResponseItem();
2680         }
2681         // find item by urlcompname
2682         if (item == null) {
2683             List<EWSMethod.Item> responses = searchItems(folderPath, itemProperties, isEqualTo("urlcompname", urlcompname), FolderQueryTraversal.SHALLOW, 0);
2684             if (!responses.isEmpty()) {
2685                 item = responses.get(0);
2686             }
2687         }
2688         return item;
2689     }
2690 
2691 
2692     @Override
2693     public Item getItem(String folderPath, String itemName) throws IOException {
2694         EWSMethod.Item item = getEwsItem(folderPath, itemName, EVENT_REQUEST_PROPERTIES);
2695         if (item == null && isMainCalendar(folderPath)) {
2696             // look for item in task folder, replace extension first
2697             if (itemName.endsWith(".ics")) {
2698                 item = getEwsItem(TASKS, itemName.substring(0, itemName.length() - 3) + "EML", EVENT_REQUEST_PROPERTIES);
2699             } else {
2700                 item = getEwsItem(TASKS, itemName, EVENT_REQUEST_PROPERTIES);
2701             }
2702         }
2703 
2704         if (item == null) {
2705             throw new HttpNotFoundException(itemName + " not found in " + folderPath);
2706         }
2707 
2708         String itemType = item.type;
2709         if ("Contact".equals(itemType) || "DistributionList".equals(itemType)) {
2710             // retrieve Contact properties
2711             ItemId itemId = new ItemId(item);
2712             GetItemMethod getItemMethod = new GetItemMethod(BaseShape.ID_ONLY, itemId, false);
2713             Set<String> attributes = CONTACT_ATTRIBUTES;
2714             if ("DistributionList".equals(itemType)) {
2715                 attributes = DISTRIBUTION_LIST_ATTRIBUTES;
2716             }
2717             for (String attribute : attributes) {
2718                 getItemMethod.addAdditionalProperty(Field.get(attribute));
2719             }
2720             executeMethod(getItemMethod);
2721             item = getItemMethod.getResponseItem();
2722             if (item == null) {
2723                 throw new HttpNotFoundException(itemName + " not found in " + folderPath);
2724             }
2725             Contact contact = new Contact(item);
2726             contact.folderPath = folderPath;
2727             return contact;
2728         } else if ("CalendarItem".equals(itemType)
2729                 || "MeetingMessage".equals(itemType)
2730                 || "MeetingRequest".equals(itemType)
2731                 || "MeetingResponse".equals(itemType)
2732                 || "MeetingCancellation".equals(itemType)
2733                 || "Task".equals(itemType)
2734                 // VTODOs appear as Messages
2735                 || "Message".equals(itemType)) {
2736             Event event = new Event(folderPath, item);
2737             // force item name to client provided name (for tasks)
2738             event.setItemName(itemName);
2739             return event;
2740         } else {
2741             throw new HttpNotFoundException(itemName + " not found in " + folderPath);
2742         }
2743 
2744     }
2745 
2746     @Override
2747     public ContactPhoto getContactPhoto(ExchangeSession.Contact contact) throws IOException {
2748         ContactPhoto contactPhoto;
2749 
2750         GetItemMethod getItemMethod = new GetItemMethod(BaseShape.ID_ONLY, ((EwsExchangeSession.Contact) contact).itemId, false);
2751         getItemMethod.addAdditionalProperty(Field.get("attachments"));
2752         executeMethod(getItemMethod);
2753         EWSMethod.Item item = getItemMethod.getResponseItem();
2754         if (item == null) {
2755             return null;
2756         }
2757         FileAttachment attachment = item.getAttachmentByName("ContactPicture.jpg");
2758         if (attachment == null) {
2759             return null;
2760         }
2761         // get attachment content
2762         GetAttachmentMethod getAttachmentMethod = new GetAttachmentMethod(attachment.attachmentId);
2763         executeMethod(getAttachmentMethod);
2764 
2765         contactPhoto = new ContactPhoto();
2766         contactPhoto.content = getAttachmentMethod.getResponseItem().get("Content");
2767         if (attachment.contentType == null) {
2768             contactPhoto.contentType = "image/jpeg";
2769         } else {
2770             contactPhoto.contentType = attachment.contentType;
2771         }
2772 
2773         return contactPhoto;
2774     }
2775 
2776     @Override
2777     public ContactPhoto getADPhoto(String email) {
2778         ContactPhoto contactPhoto = null;
2779 
2780         if (email != null && !email.isEmpty()) {
2781             try {
2782                 GetUserPhotoMethod userPhotoMethod = new GetUserPhotoMethod(email, GetUserPhotoMethod.SizeRequested.HR240x240);
2783                 executeMethod(userPhotoMethod);
2784                 if (userPhotoMethod.getPictureData() != null) {
2785                     contactPhoto = new ContactPhoto();
2786                     contactPhoto.content = userPhotoMethod.getPictureData();
2787                     contactPhoto.contentType = userPhotoMethod.getContentType();
2788                     if (contactPhoto.contentType == null) {
2789                         contactPhoto.contentType = "image/jpeg";
2790                     }
2791                 }
2792             } catch (IOException e) {
2793                 LOGGER.debug("Error loading contact image from AD " + e + " " + e.getMessage());
2794             }
2795         }
2796 
2797         return contactPhoto;
2798     }
2799 
2800     @Override
2801     public void deleteItem(String folderPath, String itemName) throws IOException {
2802         EWSMethod.Item item = getEwsItem(folderPath, itemName, EVENT_REQUEST_PROPERTIES);
2803         if (item != null && "CalendarItem".equals(item.type)) {
2804             // reload with calendar property
2805             if (serverVersion.compareTo("Exchange2013") >= 0) {
2806                 CALENDAR_ITEM_REQUEST_PROPERTIES.add("isorganizer");
2807             }
2808             item = getEwsItem(folderPath, itemName, CALENDAR_ITEM_REQUEST_PROPERTIES);
2809         }
2810         if (item == null && isMainCalendar(folderPath)) {
2811             // look for item in task folder
2812             item = getEwsItem(TASKS, itemName, EVENT_REQUEST_PROPERTIES);
2813         }
2814         if (item != null) {
2815             boolean isMeeting = "true".equals(item.get(Field.get("ismeeting").getResponseName()));
2816             boolean isOrganizer;
2817             if (item.get(Field.get("isorganizer").getResponseName()) != null) {
2818                 // Exchange 2013 or later
2819                 isOrganizer = "true".equals(item.get(Field.get("isorganizer").getResponseName()));
2820             } else {
2821                 isOrganizer = "Organizer".equals(item.get(Field.get("myresponsetype").getResponseName()));
2822             }
2823             boolean hasAttendees = item.get(Field.get("displayto").getResponseName()) != null
2824                     || item.get(Field.get("displaycc").getResponseName()) != null;
2825 
2826             if (isMeeting && isOrganizer && hasAttendees
2827                     && !isSharedFolder(folderPath)
2828                     && Settings.getBooleanProperty("davmail.caldavAutoSchedule", true)) {
2829                 // cancel meeting
2830                 SendMeetingInvitations sendMeetingInvitations = SendMeetingInvitations.SendToAllAndSaveCopy;
2831                 MessageDisposition messageDisposition = MessageDisposition.SendAndSaveCopy;
2832                 String body = null;
2833                 // This is a meeting cancel, let user edit notification message
2834                 if (Settings.getBooleanProperty("davmail.caldavEditNotifications")) {
2835                     String vEventSubject = item.get(Field.get("subject").getResponseName());
2836                     if (vEventSubject == null) {
2837                         vEventSubject = "";
2838                     }
2839                     String notificationSubject = (BundleMessage.format("CANCELLED") + vEventSubject);
2840 
2841                     NotificationDialog notificationDialog = new NotificationDialog(notificationSubject, "");
2842                     if (!notificationDialog.getSendNotification()) {
2843                         LOGGER.debug("Notification canceled by user");
2844                         sendMeetingInvitations = SendMeetingInvitations.SendToNone;
2845                         messageDisposition = MessageDisposition.SaveOnly;
2846                     }
2847                     // get description from dialog
2848                     body = notificationDialog.getBody();
2849                 }
2850                 EWSMethod.Item cancelItem = new EWSMethod.Item();
2851                 cancelItem.type = "CancelCalendarItem";
2852                 cancelItem.referenceItemId = new ItemId("ReferenceItemId", item);
2853                 if (body != null && !body.isEmpty()) {
2854                     item.put("Body", body);
2855                 }
2856                 CreateItemMethod cancelItemMethod = new CreateItemMethod(messageDisposition,
2857                         sendMeetingInvitations,
2858                         getFolderId(SENT),
2859                         cancelItem
2860                 );
2861                 executeMethod(cancelItemMethod);
2862 
2863             } else {
2864                 DeleteType deleteType = DeleteType.MoveToDeletedItems;
2865                 if (isSharedFolder(folderPath)) {
2866                     // can't move event to trash in a shared mailbox
2867                     deleteType = DeleteType.HardDelete;
2868                 }
2869                 // delete item
2870                 DeleteItemMethod deleteItemMethod = new DeleteItemMethod(new ItemId(item), deleteType, SendMeetingCancellations.SendToAllAndSaveCopy);
2871                 executeMethod(deleteItemMethod);
2872             }
2873         }
2874     }
2875 
2876     @Override
2877     public void processItem(String folderPath, String itemName) throws IOException {
2878         EWSMethod.Item item = getEwsItem(folderPath, itemName, EVENT_REQUEST_PROPERTIES);
2879         if (item != null) {
2880             HashMap<String, String> localProperties = new HashMap<>();
2881             localProperties.put("processed", "1");
2882             localProperties.put("read", "1");
2883             UpdateItemMethod updateItemMethod = new UpdateItemMethod(MessageDisposition.SaveOnly,
2884                     ConflictResolution.AlwaysOverwrite,
2885                     SendMeetingInvitationsOrCancellations.SendToNone,
2886                     new ItemId(item), buildProperties(localProperties));
2887             executeMethod(updateItemMethod);
2888         }
2889     }
2890 
2891     @Override
2892     public int sendEvent(String icsBody) throws IOException {
2893         String itemName = UUID.randomUUID() + ".EML";
2894         byte[] mimeContent = new Event(DRAFTS, itemName, "urn:content-classes:calendarmessage", icsBody, null, null).createMimeContent();
2895         if (mimeContent == null) {
2896             // no recipients, cancel
2897             return HttpStatus.SC_NO_CONTENT;
2898         } else {
2899             sendMessage(null, mimeContent);
2900             return HttpStatus.SC_OK;
2901         }
2902     }
2903 
2904     @Override
2905     protected Contact buildContact(String folderPath, String itemName, Map<String, String> properties, String etag, String noneMatch) {
2906         return new Contact(folderPath, itemName, properties, StringUtil.removeQuotes(etag), noneMatch);
2907     }
2908 
2909     @Override
2910     protected ItemResult internalCreateOrUpdateEvent(String folderPath, String itemName, String contentClass, String icsBody, String etag, String noneMatch) throws IOException {
2911         return new Event(folderPath, itemName, contentClass, icsBody, StringUtil.removeQuotes(etag), noneMatch).createOrUpdate();
2912     }
2913 
2914     @Override
2915     public boolean isSharedFolder(String folderPath) {
2916         return folderPath.startsWith("/") && !folderPath.toLowerCase().startsWith(currentMailboxPath);
2917     }
2918 
2919     @Override
2920     public boolean isMainCalendar(String folderPath) throws IOException {
2921         FolderId currentFolderId = getFolderId(folderPath);
2922         FolderId calendarFolderId = getFolderId("calendar");
2923         return calendarFolderId.name.equals(currentFolderId.name) && calendarFolderId.value.equals(currentFolderId.value);
2924     }
2925 
2926     @Override
2927     protected String getCalendarEmail(String folderPath) throws IOException {
2928         // retrieve mailbox from folder
2929         String calendarEmail = getFolderId(folderPath).mailbox;
2930         if (calendarEmail == null) {
2931             // default is current mailbox
2932             calendarEmail = email;
2933         }
2934         return calendarEmail;
2935     }
2936 
2937     @Override
2938     protected String getFreeBusyData(String attendee, String start, String end, int interval) {
2939         String result = null;
2940         GetUserAvailabilityMethod getUserAvailabilityMethod = new GetUserAvailabilityMethod(attendee, start, end, interval);
2941         try {
2942             executeMethod(getUserAvailabilityMethod);
2943             result = getUserAvailabilityMethod.getMergedFreeBusy();
2944         } catch (IOException e) {
2945             // ignore
2946         }
2947         return result;
2948     }
2949 
2950     @Override
2951     protected void loadVtimezone() {
2952 
2953         try {
2954             String timezoneId;
2955             timezoneId = Settings.getProperty("davmail.timezoneId");
2956             if (timezoneId != null) {
2957                 // Check user provided a standard timezone, convert to Exchange
2958                 timezoneId = DateUtil.getExchangeTimeZone(timezoneId);
2959             } else {
2960                 if (!"Exchange2007_SP1".equals(serverVersion)) {
2961                     // On Exchange 2010, get user timezone from server
2962                     GetUserConfigurationMethod getUserConfigurationMethod = new GetUserConfigurationMethod();
2963                     executeMethod(getUserConfigurationMethod);
2964                     EWSMethod.Item item = getUserConfigurationMethod.getResponseItem();
2965                     if (item != null) {
2966                         timezoneId = item.get("timezone");
2967                     }
2968                 } else if (!directEws) {
2969                     timezoneId = getTimezoneidFromOptions();
2970                 }
2971             }
2972             // failover: use timezone id from settings file
2973             // last failover: use GMT
2974             if (timezoneId == null) {
2975                 LOGGER.warn("Unable to get user timezone, using GMT Standard Time. Set davmail.timezoneId setting to override this.");
2976                 timezoneId = "GMT Standard Time";
2977             }
2978 
2979             EWSMethod.Item item = new EWSMethod.Item();
2980             item.type = "CalendarItem";
2981             if (!"Exchange2007_SP1".equals(serverVersion)) {
2982                 SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH);
2983                 dateFormatter.setTimeZone(GMT_TIMEZONE);
2984                 Calendar cal = Calendar.getInstance();
2985                 item.put("Start", dateFormatter.format(cal.getTime()));
2986                 cal.add(Calendar.DAY_OF_MONTH, 1);
2987                 item.put("End", dateFormatter.format(cal.getTime()));
2988                 item.put("StartTimeZone", timezoneId);
2989             } else {
2990                 item.put("MeetingTimeZone", timezoneId);
2991             }
2992             CreateItemMethod createItemMethod = new CreateItemMethod(MessageDisposition.SaveOnly, SendMeetingInvitations.SendToNone, getFolderId("calendar"), item);
2993             executeMethod(createItemMethod);
2994             item = createItemMethod.getResponseItem();
2995             if (item == null) {
2996                 throw new IOException("Empty timezone item");
2997             }
2998             VCalendar vCalendar = new VCalendar(getContent(new ItemId(item)), email, null);
2999             this.vTimezone = vCalendar.getVTimezone();
3000             // delete temporary item
3001             DeleteItemMethod deleteItemMethod = new DeleteItemMethod(new ItemId(item), DeleteType.HardDelete, SendMeetingCancellations.SendToNone);
3002             executeMethod(deleteItemMethod);
3003         } catch (IOException e) {
3004             LOGGER.warn("Unable to get VTIMEZONE info: " + e, e);
3005         }
3006     }
3007 
3008     protected String getTimezoneidFromOptions() {
3009         String result = null;
3010         // get time zone setting from HTML body
3011         String optionsPath = "/owa/?ae=Options&t=Regional";
3012         GetRequest optionsMethod = new GetRequest(optionsPath);
3013         try (
3014                 CloseableHttpResponse response = httpClient.execute(optionsMethod);
3015                 BufferedReader optionsPageReader = new BufferedReader(new InputStreamReader(response.getEntity().getContent(), StandardCharsets.UTF_8))
3016         ) {
3017             String line;
3018             // find timezone
3019             //noinspection StatementWithEmptyBody
3020             while ((line = optionsPageReader.readLine()) != null
3021                     && (!line.contains("tblTmZn"))
3022                     && (!line.contains("selTmZn"))) {
3023             }
3024             if (line != null) {
3025                 if (line.contains("tblTmZn")) {
3026                     int start = line.indexOf("oV=\"") + 4;
3027                     int end = line.indexOf('\"', start);
3028                     result = line.substring(start, end);
3029                 } else {
3030                     int end = line.lastIndexOf("\" selected>");
3031                     int start = line.lastIndexOf('\"', end - 1);
3032                     result = line.substring(start + 1, end);
3033                 }
3034             }
3035         } catch (IOException e) {
3036             LOGGER.error("Error parsing options page at " + optionsPath);
3037         }
3038 
3039         return result;
3040     }
3041 
3042 
3043     protected FolderId getFolderId(String folderPath) throws IOException {
3044         FolderId folderId = getFolderIdIfExists(folderPath);
3045         if (folderId == null) {
3046             throw new HttpNotFoundException("Folder '" + folderPath + "' not found");
3047         }
3048         return folderId;
3049     }
3050 
3051     protected static final String USERS_ROOT = "/users/";
3052 
3053     protected FolderId getFolderIdIfExists(String folderPath) throws IOException {
3054         String lowerCaseFolderPath = folderPath.toLowerCase();
3055         if (lowerCaseFolderPath.equals(currentMailboxPath)) {
3056             return getSubFolderIdIfExists(null, "");
3057         } else if (lowerCaseFolderPath.startsWith(currentMailboxPath + '/')) {
3058             return getSubFolderIdIfExists(null, folderPath.substring(currentMailboxPath.length() + 1));
3059         } else if (folderPath.startsWith("/users/")) {
3060             int slashIndex = folderPath.indexOf('/', USERS_ROOT.length());
3061             String mailbox;
3062             String subFolderPath;
3063             if (slashIndex >= 0) {
3064                 mailbox = folderPath.substring(USERS_ROOT.length(), slashIndex);
3065                 subFolderPath = folderPath.substring(slashIndex + 1);
3066             } else {
3067                 mailbox = folderPath.substring(USERS_ROOT.length());
3068                 subFolderPath = "";
3069             }
3070             return getSubFolderIdIfExists(mailbox, subFolderPath);
3071         } else {
3072             return getSubFolderIdIfExists(null, folderPath);
3073         }
3074     }
3075 
3076     protected FolderId getSubFolderIdIfExists(String mailbox, String folderPath) throws IOException {
3077         String[] folderNames;
3078         FolderId currentFolderId;
3079 
3080         if ("/public".equals(folderPath)) {
3081             return DistinguishedFolderId.getInstance(mailbox, DistinguishedFolderId.Name.publicfoldersroot);
3082         } else if ("/archive".equals(folderPath)) {
3083             return DistinguishedFolderId.getInstance(mailbox, DistinguishedFolderId.Name.archivemsgfolderroot);
3084         } else if (isSubFolderOf(folderPath, PUBLIC_ROOT)) {
3085             currentFolderId = DistinguishedFolderId.getInstance(mailbox, DistinguishedFolderId.Name.publicfoldersroot);
3086             folderNames = folderPath.substring(PUBLIC_ROOT.length()).split("/");
3087         } else if (isSubFolderOf(folderPath, ARCHIVE_ROOT)) {
3088             currentFolderId = DistinguishedFolderId.getInstance(mailbox, DistinguishedFolderId.Name.archivemsgfolderroot);
3089             folderNames = folderPath.substring(ARCHIVE_ROOT.length()).split("/");
3090         } else if (isSubFolderOf(folderPath, INBOX) ||
3091                 isSubFolderOf(folderPath, LOWER_CASE_INBOX) ||
3092                 isSubFolderOf(folderPath, MIXED_CASE_INBOX)) {
3093             currentFolderId = DistinguishedFolderId.getInstance(mailbox, DistinguishedFolderId.Name.inbox);
3094             folderNames = folderPath.substring(INBOX.length()).split("/");
3095         } else if (isSubFolderOf(folderPath, CALENDAR)) {
3096             currentFolderId = DistinguishedFolderId.getInstance(mailbox, DistinguishedFolderId.Name.calendar);
3097             folderNames = folderPath.substring(CALENDAR.length()).split("/");
3098         } else if (isSubFolderOf(folderPath, TASKS)) {
3099             currentFolderId = DistinguishedFolderId.getInstance(mailbox, DistinguishedFolderId.Name.tasks);
3100             folderNames = folderPath.substring(TASKS.length()).split("/");
3101         } else if (isSubFolderOf(folderPath, CONTACTS)) {
3102             currentFolderId = DistinguishedFolderId.getInstance(mailbox, DistinguishedFolderId.Name.contacts);
3103             folderNames = folderPath.substring(CONTACTS.length()).split("/");
3104         } else if (isSubFolderOf(folderPath, SENT)) {
3105             currentFolderId = DistinguishedFolderId.getInstance(mailbox, DistinguishedFolderId.Name.sentitems);
3106             folderNames = folderPath.substring(SENT.length()).split("/");
3107         } else if (isSubFolderOf(folderPath, DRAFTS)) {
3108             currentFolderId = DistinguishedFolderId.getInstance(mailbox, DistinguishedFolderId.Name.drafts);
3109             folderNames = folderPath.substring(DRAFTS.length()).split("/");
3110         } else if (isSubFolderOf(folderPath, TRASH)) {
3111             currentFolderId = DistinguishedFolderId.getInstance(mailbox, DistinguishedFolderId.Name.deleteditems);
3112             folderNames = folderPath.substring(TRASH.length()).split("/");
3113         } else if (isSubFolderOf(folderPath, JUNK)) {
3114             currentFolderId = DistinguishedFolderId.getInstance(mailbox, DistinguishedFolderId.Name.junkemail);
3115             folderNames = folderPath.substring(JUNK.length()).split("/");
3116         } else if (isSubFolderOf(folderPath, UNSENT)) {
3117             currentFolderId = DistinguishedFolderId.getInstance(mailbox, DistinguishedFolderId.Name.outbox);
3118             folderNames = folderPath.substring(UNSENT.length()).split("/");
3119         } else {
3120             currentFolderId = DistinguishedFolderId.getInstance(mailbox, DistinguishedFolderId.Name.msgfolderroot);
3121             folderNames = folderPath.split("/");
3122         }
3123         for (String folderName : folderNames) {
3124             if (!folderName.isEmpty()) {
3125                 currentFolderId = getSubFolderByName(currentFolderId, folderName);
3126                 if (currentFolderId == null) {
3127                     break;
3128                 }
3129             }
3130         }
3131         return currentFolderId;
3132     }
3133 
3134     /**
3135      * Check if folderPath is the base folder or a sub folder path.
3136      *
3137      * @param folderPath folder path
3138      * @param baseFolder base folder
3139      * @return true if folderPath is under baseFolder
3140      */
3141     private boolean isSubFolderOf(String folderPath, String baseFolder) {
3142         if (PUBLIC_ROOT.equals(baseFolder) || ARCHIVE_ROOT.equals(baseFolder)) {
3143             return folderPath.startsWith(baseFolder);
3144         } else {
3145             return folderPath.startsWith(baseFolder)
3146                     && (folderPath.length() == baseFolder.length() || folderPath.charAt(baseFolder.length()) == '/');
3147         }
3148     }
3149 
3150     protected FolderId getSubFolderByName(FolderId parentFolderId, String folderName) throws IOException {
3151         FolderId folderId = null;
3152         FindFolderMethod findFolderMethod = new FindFolderMethod(
3153                 FolderQueryTraversal.SHALLOW,
3154                 BaseShape.ID_ONLY,
3155                 parentFolderId,
3156                 FOLDER_PROPERTIES,
3157                 new TwoOperandExpression(TwoOperandExpression.Operator.IsEqualTo,
3158                         Field.get("folderDisplayName"), decodeFolderName(folderName)),
3159                 0, 1
3160         );
3161         executeMethod(findFolderMethod);
3162         EWSMethod.Item item = findFolderMethod.getResponseItem();
3163         if (item != null) {
3164             folderId = new FolderId(item);
3165         }
3166         return folderId;
3167     }
3168 
3169     public static String decodeFolderName(String folderName) {
3170         if (folderName.contains("_xF8FF_")) {
3171             return folderName.replace("_xF8FF_", "/");
3172         }
3173         if (folderName.contains("_x003E_")) {
3174             return folderName.replace("_x003E_", ">");
3175         }
3176         return folderName;
3177     }
3178 
3179     public static String encodeFolderName(String folderName) {
3180         if (folderName.contains("/")) {
3181             folderName = folderName.replace("/", "_xF8FF_");
3182         }
3183         if (folderName.contains(">")) {
3184             folderName = folderName.replace(">", "_x003E_");
3185         }
3186         return folderName;
3187     }
3188 
3189     long throttlingTimestamp = 0;
3190 
3191     protected int executeMethod(EWSMethod ewsMethod) throws IOException {
3192         long throttlingDelay = throttlingTimestamp - System.currentTimeMillis();
3193         try {
3194             if (throttlingDelay > 0) {
3195                 LOGGER.warn("Throttling active on server, waiting " + (throttlingDelay / 1000) + " seconds");
3196                 try {
3197                     Thread.sleep(throttlingDelay);
3198                 } catch (InterruptedException e1) {
3199                     LOGGER.error("Throttling delay interrupted " + e1.getMessage());
3200                     Thread.currentThread().interrupt();
3201                 }
3202             }
3203             internalExecuteMethod(ewsMethod);
3204         } catch (EWSThrottlingException e) {
3205             // default throttling delay is one minute
3206             throttlingDelay = 60000;
3207             if (ewsMethod.backOffMilliseconds > 0) {
3208                 // server provided a throttling delay, add 10 seconds
3209                 throttlingDelay = ewsMethod.backOffMilliseconds + 10000;
3210             }
3211             throttlingTimestamp = System.currentTimeMillis() + throttlingDelay;
3212 
3213             LOGGER.warn("Throttling active on server, waiting " + (throttlingDelay / 1000) + " seconds");
3214             try {
3215                 Thread.sleep(throttlingDelay);
3216             } catch (InterruptedException e1) {
3217                 LOGGER.error("Throttling delay interrupted " + e1.getMessage());
3218                 Thread.currentThread().interrupt();
3219             }
3220             // retry once
3221             internalExecuteMethod(ewsMethod);
3222         }
3223         return ewsMethod.getStatusCode();
3224     }
3225 
3226     protected void internalExecuteMethod(EWSMethod ewsMethod) throws IOException {
3227         ewsMethod.setServerVersion(serverVersion);
3228         if (token != null) {
3229             ewsMethod.setHeader("Authorization", "Bearer " + token.getAccessToken());
3230         }
3231         try (CloseableHttpResponse response = httpClient.execute(ewsMethod)) {
3232             ewsMethod.handleResponse(response);
3233         }
3234         if (serverVersion == null) {
3235             serverVersion = ewsMethod.getServerVersion();
3236         }
3237         ewsMethod.checkSuccess();
3238     }
3239 
3240     protected static final HashMap<String, String> GALFIND_ATTRIBUTE_MAP = new HashMap<>();
3241 
3242     static {
3243         GALFIND_ATTRIBUTE_MAP.put("imapUid", "Name");
3244         GALFIND_ATTRIBUTE_MAP.put("cn", "DisplayName");
3245         GALFIND_ATTRIBUTE_MAP.put("givenName", "GivenName");
3246         GALFIND_ATTRIBUTE_MAP.put("sn", "Surname");
3247         GALFIND_ATTRIBUTE_MAP.put("smtpemail1", "EmailAddress");
3248 
3249         GALFIND_ATTRIBUTE_MAP.put("roomnumber", "OfficeLocation");
3250         GALFIND_ATTRIBUTE_MAP.put("street", "BusinessStreet");
3251         GALFIND_ATTRIBUTE_MAP.put("l", "BusinessCity");
3252         GALFIND_ATTRIBUTE_MAP.put("o", "CompanyName");
3253         GALFIND_ATTRIBUTE_MAP.put("postalcode", "BusinessPostalCode");
3254         GALFIND_ATTRIBUTE_MAP.put("st", "BusinessState");
3255         GALFIND_ATTRIBUTE_MAP.put("co", "BusinessCountryOrRegion");
3256 
3257         GALFIND_ATTRIBUTE_MAP.put("manager", "Manager");
3258         GALFIND_ATTRIBUTE_MAP.put("middlename", "Initials");
3259         GALFIND_ATTRIBUTE_MAP.put("title", "JobTitle");
3260         GALFIND_ATTRIBUTE_MAP.put("department", "Department");
3261 
3262         GALFIND_ATTRIBUTE_MAP.put("otherTelephone", "OtherTelephone");
3263         GALFIND_ATTRIBUTE_MAP.put("telephoneNumber", "BusinessPhone");
3264         GALFIND_ATTRIBUTE_MAP.put("mobile", "MobilePhone");
3265         GALFIND_ATTRIBUTE_MAP.put("facsimiletelephonenumber", "BusinessFax");
3266         GALFIND_ATTRIBUTE_MAP.put("secretarycn", "AssistantName");
3267 
3268         GALFIND_ATTRIBUTE_MAP.put("homePhone", "HomePhone");
3269         GALFIND_ATTRIBUTE_MAP.put("pager", "Pager");
3270         GALFIND_ATTRIBUTE_MAP.put("msexchangecertificate", "MSExchangeCertificate");
3271         GALFIND_ATTRIBUTE_MAP.put("usersmimecertificate", "UserSMIMECertificate");
3272     }
3273 
3274     protected static final HashSet<String> IGNORE_ATTRIBUTE_SET = new HashSet<>();
3275 
3276     static {
3277         IGNORE_ATTRIBUTE_SET.add("ContactSource");
3278         IGNORE_ATTRIBUTE_SET.add("Culture");
3279         IGNORE_ATTRIBUTE_SET.add("AssistantPhone");
3280     }
3281 
3282     protected Contact buildGalfindContact(EWSMethod.Item response) {
3283         Contact contact = new Contact();
3284         contact.setName(response.get("Name"));
3285         contact.put("imapUid", response.get("Name"));
3286         contact.put("uid", response.get("Name"));
3287         if (LOGGER.isDebugEnabled()) {
3288             for (Map.Entry<String, String> entry : response.entrySet()) {
3289                 String key = entry.getKey();
3290                 if (!IGNORE_ATTRIBUTE_SET.contains(key) && !GALFIND_ATTRIBUTE_MAP.containsValue(key)) {
3291                     LOGGER.debug("Unsupported ResolveNames " + contact.getName() + " response attribute: " + key + " value: " + entry.getValue());
3292                 }
3293             }
3294         }
3295         for (Map.Entry<String, String> entry : GALFIND_ATTRIBUTE_MAP.entrySet()) {
3296             String attributeValue = response.get(entry.getValue());
3297             if (attributeValue != null && !attributeValue.isEmpty()) {
3298                 contact.put(entry.getKey(), attributeValue);
3299             }
3300         }
3301         return contact;
3302     }
3303 
3304     @Override
3305     public Map<String, ExchangeSession.Contact> galFind(Condition condition, Set<String> returningAttributes, int sizeLimit) throws IOException {
3306         Map<String, ExchangeSession.Contact> contacts = new HashMap<>();
3307         if (condition instanceof MultiCondition) {
3308             List<Condition> conditions = ((ExchangeSession.MultiCondition) condition).getConditions();
3309             Operator operator = ((ExchangeSession.MultiCondition) condition).getOperator();
3310             if (operator == Operator.Or) {
3311                 for (Condition innerCondition : conditions) {
3312                     contacts.putAll(galFind(innerCondition, returningAttributes, sizeLimit));
3313                 }
3314             } else if (operator == Operator.And && !conditions.isEmpty()) {
3315                 Map<String, ExchangeSession.Contact> innerContacts = galFind(conditions.get(0), returningAttributes, sizeLimit);
3316                 for (ExchangeSession.Contact contact : innerContacts.values()) {
3317                     if (condition.isMatch(contact)) {
3318                         contacts.put(contact.getName().toLowerCase(), contact);
3319                     }
3320                 }
3321             }
3322         } else if (condition instanceof AttributeCondition) {
3323             String mappedAttributeName = GALFIND_ATTRIBUTE_MAP.get(((ExchangeSession.AttributeCondition) condition).getAttributeName());
3324             if (mappedAttributeName != null) {
3325                 String value = ((ExchangeSession.AttributeCondition) condition).getValue().toLowerCase();
3326                 Operator operator = ((AttributeCondition) condition).getOperator();
3327                 String searchValue = value;
3328                 if (mappedAttributeName.startsWith("EmailAddress")) {
3329                     searchValue = "smtp:" + searchValue;
3330                 }
3331                 if (operator == Operator.IsEqualTo) {
3332                     searchValue = '=' + searchValue;
3333                 }
3334                 ResolveNamesMethod resolveNamesMethod = new ResolveNamesMethod(searchValue);
3335                 executeMethod(resolveNamesMethod);
3336                 List<EWSMethod.Item> responses = resolveNamesMethod.getResponseItems();
3337                 if (LOGGER.isDebugEnabled()) {
3338                     LOGGER.debug("ResolveNames(" + searchValue + ") returned " + responses.size() + " results");
3339                 }
3340                 for (EWSMethod.Item response : responses) {
3341                     Contact contact = buildGalfindContact(response);
3342                     if (condition.isMatch(contact)) {
3343                         contacts.put(contact.getName().toLowerCase(), contact);
3344                     }
3345                 }
3346             }
3347         }
3348         return contacts;
3349     }
3350 
3351     protected Date parseDateFromExchange(String exchangeDateValue) throws DavMailException {
3352         Date dateValue = null;
3353         if (exchangeDateValue != null) {
3354             try {
3355                 dateValue = getExchangeZuluDateFormat().parse(exchangeDateValue);
3356             } catch (ParseException e) {
3357                 throw new DavMailException("EXCEPTION_INVALID_DATE", exchangeDateValue);
3358             }
3359         }
3360         return dateValue;
3361     }
3362 
3363     protected String convertDateFromExchange(String exchangeDateValue) throws DavMailException {
3364         // yyyy-MM-dd'T'HH:mm:ss'Z' to yyyyMMdd'T'HHmmss'Z'
3365         if (exchangeDateValue == null) {
3366             return null;
3367         } else {
3368             if (exchangeDateValue.length() != 20) {
3369                 throw new DavMailException("EXCEPTION_INVALID_DATE", exchangeDateValue);
3370             }
3371             StringBuilder buffer = new StringBuilder();
3372             for (int i = 0; i < exchangeDateValue.length(); i++) {
3373                 if (i == 4 || i == 7 || i == 13 || i == 16) {
3374                     i++;
3375                 }
3376                 buffer.append(exchangeDateValue.charAt(i));
3377             }
3378             return buffer.toString();
3379         }
3380     }
3381 
3382     protected String convertCalendarDateToExchange(String vcalendarDateValue) throws DavMailException {
3383         String zuluDateValue = null;
3384         if (vcalendarDateValue != null) {
3385             try {
3386                 SimpleDateFormat dateParser;
3387                 if (vcalendarDateValue.length() == 8) {
3388                     dateParser = new SimpleDateFormat("yyyyMMdd", Locale.ENGLISH);
3389                 } else {
3390                     dateParser = new SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.ENGLISH);
3391                 }
3392                 dateParser.setTimeZone(GMT_TIMEZONE);
3393                 SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH);
3394                 dateFormatter.setTimeZone(GMT_TIMEZONE);
3395                 zuluDateValue = dateFormatter.format(dateParser.parse(vcalendarDateValue));
3396             } catch (ParseException e) {
3397                 throw new DavMailException("EXCEPTION_INVALID_DATE", vcalendarDateValue);
3398             }
3399         }
3400         return zuluDateValue;
3401     }
3402 
3403     public static String convertDateFromExchangeToTaskDate(String exchangeDateValue) throws DavMailException {
3404         String zuluDateValue = null;
3405         if (exchangeDateValue != null) {
3406             try {
3407                 SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.ENGLISH);
3408                 dateFormat.setTimeZone(GMT_TIMEZONE);
3409                 zuluDateValue = dateFormat.format(getExchangeZuluDateFormat().parse(exchangeDateValue));
3410             } catch (ParseException e) {
3411                 throw new DavMailException("EXCEPTION_INVALID_DATE", exchangeDateValue);
3412             }
3413         }
3414         return zuluDateValue;
3415     }
3416 
3417     protected String convertTaskDateToZulu(String value) {
3418         String result = null;
3419         if (value != null && !value.isEmpty()) {
3420             try {
3421                 SimpleDateFormat parser = ExchangeSession.getExchangeDateFormat(value);
3422                 Calendar calendarValue = Calendar.getInstance(GMT_TIMEZONE);
3423                 calendarValue.setTime(parser.parse(value));
3424                 // zulu time: add 12 hours
3425                 if (value.length() == 16) {
3426                     calendarValue.add(Calendar.HOUR, 12);
3427                 }
3428                 calendarValue.set(Calendar.HOUR, 0);
3429                 calendarValue.set(Calendar.MINUTE, 0);
3430                 calendarValue.set(Calendar.SECOND, 0);
3431                 result = ExchangeSession.getExchangeZuluDateFormat().format(calendarValue.getTime());
3432             } catch (ParseException e) {
3433                 LOGGER.warn("Invalid date: " + value);
3434             }
3435         }
3436 
3437         return result;
3438     }
3439 
3440     /**
3441      * Format date to exchange defined search format.
3442      *
3443      * @param date date object
3444      * @return formatted search date
3445      */
3446     @Override
3447     public String formatSearchDate(Date date) {
3448         SimpleDateFormat dateFormatter = new SimpleDateFormat(YYYY_MM_DD_T_HHMMSS_Z, Locale.ENGLISH);
3449         dateFormatter.setTimeZone(GMT_TIMEZONE);
3450         return dateFormatter.format(date);
3451     }
3452 
3453     private static final Pattern BASE64_EML_PATTERN = Pattern.compile("^([A-Za-z0-9\\-_]{4})*([A-Za-z0-9\\-_]{4}|[A-Za-z0-9\\-_]{3}=|[A-Za-z0-9\\-_]{2}==)\\.EML$");
3454 
3455     /**
3456      * Check if the itemName is long and base64 encoded.
3457      * User generated item names are usually short
3458      *
3459      * @param itemName item name
3460      * @return true if itemName is an EWS item id
3461      */
3462     protected static boolean isItemId(String itemName) {
3463         return itemName.length() >= 140
3464                 // item name is base64url
3465                 && BASE64_EML_PATTERN.matcher(itemName).matches()
3466                 && itemName.indexOf(' ') < 0;
3467     }
3468 
3469     /**
3470      * Close session.
3471      * Shutdown http client connection manager
3472      */
3473     @Override
3474     public void close() {
3475         httpClient.close();
3476     }
3477 
3478 }
3479