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