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