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