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, SendMeetingInvitationsOrCancellations sendMeetingInvitationsOrCancellations) 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,
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 
1803             // by default no notifications
1804             SendMeetingInvitationsOrCancellations sendMeetingInvitationsOrCancellations = SendMeetingInvitationsOrCancellations.SendToNone;
1805 
1806             if (vCalendar.isTodo()) {
1807                 // create or update task method
1808                 EWSMethod.Item newItem = new EWSMethod.Item();
1809                 newItem.type = "Task";
1810                 List<FieldUpdate> updates = new ArrayList<>();
1811                 updates.add(Field.createFieldUpdate("importance", convertPriorityToExchange(vCalendar.getFirstVeventPropertyValue("PRIORITY"))));
1812                 updates.add(Field.createFieldUpdate("calendaruid", vCalendar.getFirstVeventPropertyValue("UID")));
1813                 // force urlcompname
1814                 updates.add(Field.createFieldUpdate("urlcompname", convertItemNameToEML(itemName)));
1815                 updates.add(Field.createFieldUpdate("subject", vCalendar.getFirstVeventPropertyValue("SUMMARY")));
1816                 updates.add(Field.createFieldUpdate("description", vCalendar.getFirstVeventPropertyValue("DESCRIPTION")));
1817 
1818                 // handle multiple keywords/categories
1819                 List<VProperty> categories = vCalendar.getFirstVeventProperties("CATEGORIES");
1820                 if (categories != null) {
1821                     HashSet<String> categoryValues = new HashSet<>();
1822                     for (VProperty category : categories) {
1823                         categoryValues.add(category.getValue());
1824                     }
1825                     updates.add(Field.createFieldUpdate("keywords", StringUtil.join(categoryValues, ",")));
1826                 }
1827 
1828                 updates.add(Field.createFieldUpdate("startdate", convertTaskDateToZulu(vCalendar.getFirstVeventPropertyValue("DTSTART"))));
1829                 updates.add(Field.createFieldUpdate("duedate", convertTaskDateToZulu(vCalendar.getFirstVeventPropertyValue("DUE"))));
1830                 updates.add(Field.createFieldUpdate("datecompleted", convertTaskDateToZulu(vCalendar.getFirstVeventPropertyValue("COMPLETED"))));
1831 
1832                 updates.add(Field.createFieldUpdate("commonstart", convertTaskDateToZulu(vCalendar.getFirstVeventPropertyValue("DTSTART"))));
1833                 updates.add(Field.createFieldUpdate("commonend", convertTaskDateToZulu(vCalendar.getFirstVeventPropertyValue("DUE"))));
1834 
1835                 String percentComplete = vCalendar.getFirstVeventPropertyValue("PERCENT-COMPLETE");
1836                 if (percentComplete == null) {
1837                     percentComplete = "0";
1838                 }
1839                 updates.add(Field.createFieldUpdate("percentcomplete", percentComplete));
1840                 String vTodoStatus = vCalendar.getFirstVeventPropertyValue("STATUS");
1841                 if (vTodoStatus == null) {
1842                     updates.add(Field.createFieldUpdate("taskstatus", "NotStarted"));
1843                 } else {
1844                     updates.add(Field.createFieldUpdate("taskstatus", vTodoToTaskStatusMap.get(vTodoStatus)));
1845                 }
1846 
1847                 //updates.add(Field.createFieldUpdate("iscomplete", "COMPLETED".equals(vTodoStatus)?"True":"False"));
1848 
1849                 if (currentItemId != null) {
1850                     // update
1851                     createOrUpdateItemMethod = new UpdateItemMethod(MessageDisposition.SaveOnly,
1852                             ConflictResolution.AutoResolve,
1853                             SendMeetingInvitationsOrCancellations.SendToNone,
1854                             currentItemId, updates);
1855                 } else {
1856                     newItem.setFieldUpdates(updates);
1857                     // create
1858                     createOrUpdateItemMethod = new CreateItemMethod(MessageDisposition.SaveOnly, SendMeetingInvitations.SendToNone, getFolderId(folderPath), newItem);
1859                 }
1860 
1861             } else {
1862 
1863                 // update existing item
1864                 if (currentItemId != null) {
1865                     if (isMeetingResponse && Settings.getBooleanProperty("davmail.caldavAutoSchedule", true)) {
1866                         // meeting response with server managed notifications
1867                         SendMeetingInvitations sendMeetingInvitations = SendMeetingInvitations.SendToAllAndSaveCopy;
1868                         MessageDisposition messageDisposition = MessageDisposition.SendAndSaveCopy;
1869                         String body = null;
1870                         // This is a meeting response, let user edit notification message
1871                         if (Settings.getBooleanProperty("davmail.caldavEditNotifications")) {
1872                             String vEventSubject = vCalendar.getFirstVeventPropertyValue("SUMMARY");
1873                             if (vEventSubject == null) {
1874                                 vEventSubject = BundleMessage.format("MEETING_REQUEST");
1875                             }
1876 
1877                             String status = vCalendar.getAttendeeStatus();
1878                             String notificationSubject = (status != null) ? (BundleMessage.format(status) + vEventSubject) : subject;
1879 
1880                             NotificationDialog notificationDialog = new NotificationDialog(notificationSubject, "");
1881                             if (!notificationDialog.getSendNotification()) {
1882                                 LOGGER.debug("Notification canceled by user");
1883                                 sendMeetingInvitations = SendMeetingInvitations.SendToNone;
1884                                 messageDisposition = MessageDisposition.SaveOnly;
1885                             }
1886                             // get description from dialog
1887                             body = notificationDialog.getBody();
1888                         }
1889                         EWSMethod.Item item = new EWSMethod.Item();
1890 
1891                         item.type = partstatToResponseMap.get(vCalendar.getAttendeeStatus());
1892                         item.referenceItemId = new ItemId("ReferenceItemId", currentItemId.id, currentItemId.changeKey);
1893                         if (body != null && !body.isEmpty()) {
1894                             item.put("Body", body);
1895                         }
1896                         createOrUpdateItemMethod = new CreateItemMethod(messageDisposition,
1897                                 sendMeetingInvitations,
1898                                 getFolderId(SENT),
1899                                 item
1900                         );
1901                     } else if (Settings.getBooleanProperty("davmail.caldavAutoSchedule", true)) {
1902                         // other changes with server side managed notifications
1903                         MessageDisposition messageDisposition = MessageDisposition.SaveOnly;
1904 
1905                         if (vCalendar.isMeeting() && vCalendar.isMeetingOrganizer() && isMozSendInvitations) {
1906                             messageDisposition = MessageDisposition.SendAndSaveCopy;
1907                             sendMeetingInvitationsOrCancellations = SendMeetingInvitationsOrCancellations.SendToAllAndSaveCopy;
1908                         }
1909                         createOrUpdateItemMethod = new UpdateItemMethod(messageDisposition,
1910                                 ConflictResolution.AutoResolve,
1911                                 sendMeetingInvitationsOrCancellations,
1912                                 currentItemId, buildFieldUpdates(vCalendar, vCalendar.getFirstVevent(), isMozDismiss));
1913                         // force context Timezone on Exchange 2010 and 2013
1914                         if (serverVersion != null && serverVersion.startsWith("Exchange201")) {
1915                             createOrUpdateItemMethod.setTimezoneContext(EwsExchangeSession.this.getVTimezone().getPropertyValue("TZID"));
1916                         }
1917                     } else {
1918                         // old hard/delete approach on update, used with client side notifications
1919                         DeleteItemMethod deleteItemMethod = new DeleteItemMethod(currentItemId, DeleteType.HardDelete, SendMeetingCancellations.SendToNone);
1920                         executeMethod(deleteItemMethod);
1921                     }
1922                 }
1923 
1924                 if (createOrUpdateItemMethod == null) {
1925                     // create
1926                     EWSMethod.Item newItem = new EWSMethod.Item();
1927                     newItem.type = "CalendarItem";
1928                     newItem.mimeContent = IOUtil.encodeBase64(vCalendar.toString());
1929                     ArrayList<FieldUpdate> updates = new ArrayList<>();
1930                     if (!vCalendar.hasVAlarm()) {
1931                         updates.add(Field.createFieldUpdate("reminderset", "false"));
1932                     }
1933                     //updates.add(Field.createFieldUpdate("outlookmessageclass", "IPM.Appointment"));
1934                     // force urlcompname
1935                     updates.add(Field.createFieldUpdate("urlcompname", convertItemNameToEML(itemName)));
1936                     if (vCalendar.isMeeting()) {
1937                         if (vCalendar.isMeetingOrganizer()) {
1938                             updates.add(Field.createFieldUpdate("apptstateflags", "1"));
1939                         } else {
1940                             updates.add(Field.createFieldUpdate("apptstateflags", "3"));
1941                         }
1942                     } else {
1943                         updates.add(Field.createFieldUpdate("apptstateflags", "0"));
1944                     }
1945                     // store mozilla invitations option
1946                     String xMozSendInvitations = vCalendar.getFirstVeventPropertyValue("X-MOZ-SEND-INVITATIONS");
1947                     if (xMozSendInvitations != null) {
1948                         updates.add(Field.createFieldUpdate("xmozsendinvitations", xMozSendInvitations));
1949                     }
1950                     // handle mozilla alarm
1951                     String xMozLastack = vCalendar.getFirstVeventPropertyValue("X-MOZ-LASTACK");
1952                     if (xMozLastack != null) {
1953                         updates.add(Field.createFieldUpdate("xmozlastack", xMozLastack));
1954                     }
1955                     String xMozSnoozeTime = vCalendar.getFirstVeventPropertyValue("X-MOZ-SNOOZE-TIME");
1956                     if (xMozSnoozeTime != null) {
1957                         updates.add(Field.createFieldUpdate("xmozsnoozetime", xMozSnoozeTime));
1958                     }
1959 
1960                     if (vCalendar.isMeeting() && "Exchange2007_SP1".equals(serverVersion)) {
1961                         Set<String> requiredAttendees = new HashSet<>();
1962                         Set<String> optionalAttendees = new HashSet<>();
1963                         List<VProperty> attendeeProperties = vCalendar.getFirstVeventProperties("ATTENDEE");
1964                         if (attendeeProperties != null) {
1965                             for (VProperty property : attendeeProperties) {
1966                                 String attendeeEmail = vCalendar.getEmailValue(property);
1967                                 if (attendeeEmail != null && attendeeEmail.indexOf('@') >= 0) {
1968                                     if (email.equals(attendeeEmail)) {
1969                                         String ownerPartStat = property.getParamValue("PARTSTAT");
1970                                         if ("ACCEPTED".equals(ownerPartStat)) {
1971                                             ownerResponseReply = "AcceptItem";
1972                                             // do not send DeclineItem to avoid deleting target event
1973                                         } else if ("DECLINED".equals(ownerPartStat) ||
1974                                                 "TENTATIVE".equals(ownerPartStat)) {
1975                                             ownerResponseReply = "TentativelyAcceptItem";
1976                                         }
1977                                     }
1978                                     InternetAddress internetAddress = new InternetAddress(attendeeEmail, property.getParamValue("CN"));
1979                                     String attendeeRole = property.getParamValue("ROLE");
1980                                     if ("REQ-PARTICIPANT".equals(attendeeRole)) {
1981                                         requiredAttendees.add(internetAddress.toString());
1982                                     } else {
1983                                         optionalAttendees.add(internetAddress.toString());
1984                                     }
1985                                 }
1986                             }
1987                         }
1988                         List<VProperty> organizerProperties = vCalendar.getFirstVeventProperties("ORGANIZER");
1989                         if (organizerProperties != null) {
1990                             VProperty property = organizerProperties.get(0);
1991                             String organizerEmail = vCalendar.getEmailValue(property);
1992                             if (organizerEmail != null && organizerEmail.indexOf('@') >= 0) {
1993                                 updates.add(Field.createFieldUpdate("from", organizerEmail));
1994                             }
1995                         }
1996 
1997                         if (!requiredAttendees.isEmpty()) {
1998                             updates.add(Field.createFieldUpdate("to", StringUtil.join(requiredAttendees, ", ")));
1999                         }
2000                         if (!optionalAttendees.isEmpty()) {
2001                             updates.add(Field.createFieldUpdate("cc", StringUtil.join(optionalAttendees, ", ")));
2002                         }
2003                     }
2004 
2005                     // patch allday date values, only on 2007
2006                     if ("Exchange2007_SP1".equals(serverVersion) && vCalendar.isCdoAllDay()) {
2007                         updates.add(Field.createFieldUpdate("dtstart", convertCalendarDateToExchange(vCalendar.getFirstVeventPropertyValue("DTSTART"))));
2008                         updates.add(Field.createFieldUpdate("dtend", convertCalendarDateToExchange(vCalendar.getFirstVeventPropertyValue("DTEND"))));
2009                     }
2010 
2011                     String status = vCalendar.getFirstVeventPropertyValue("STATUS");
2012                     if ("TENTATIVE".equals(status)) {
2013                         // this is a tentative event
2014                         updates.add(Field.createFieldUpdate("busystatus", "Tentative"));
2015                     } else {
2016                         // otherwise, we use the same value as before, as received from the server
2017                         // however, the case matters, so we still have to transform it "BUSY" -> "Busy"
2018                         updates.add(Field.createFieldUpdate("busystatus", "BUSY".equals(vCalendar.getFirstVeventPropertyValue("X-MICROSOFT-CDO-BUSYSTATUS")) ? "Busy" : "Free"));
2019                     }
2020 
2021                     if ("Exchange2007_SP1".equals(serverVersion) && vCalendar.isCdoAllDay()) {
2022                         updates.add(Field.createFieldUpdate("meetingtimezone", vCalendar.getVTimezone().getPropertyValue("TZID")));
2023                     }
2024 
2025                     newItem.setFieldUpdates(updates);
2026                     MessageDisposition messageDisposition = MessageDisposition.SaveOnly;
2027                     SendMeetingInvitations sendMeetingInvitations = SendMeetingInvitations.SendToNone;
2028                     if (vCalendar.isMeeting() && vCalendar.isMeetingOrganizer() && isMozSendInvitations
2029                             && Settings.getBooleanProperty("davmail.caldavAutoSchedule", true)) {
2030                         // meeting request creation with server managed notifications
2031                         messageDisposition = MessageDisposition.SendAndSaveCopy;
2032                         sendMeetingInvitations = SendMeetingInvitations.SendToAllAndSaveCopy;
2033                     }
2034                     createOrUpdateItemMethod = new CreateItemMethod(messageDisposition, sendMeetingInvitations, getFolderId(folderPath), newItem);
2035                     // force context Timezone on Exchange 2010 and 2013
2036                     if (serverVersion != null && serverVersion.startsWith("Exchange201")) {
2037                         createOrUpdateItemMethod.setTimezoneContext(EwsExchangeSession.this.getVTimezone().getPropertyValue("TZID"));
2038                     }
2039                 }
2040             }
2041 
2042             executeMethod(createOrUpdateItemMethod);
2043 
2044             itemResult.status = createOrUpdateItemMethod.getStatusCode();
2045             if (itemResult.status == HttpURLConnection.HTTP_OK) {
2046                 //noinspection VariableNotUsedInsideIf
2047                 if (currentItemId == null) {
2048                     itemResult.status = HttpStatus.SC_CREATED;
2049                     LOGGER.debug("Created event " + getHref());
2050                 } else {
2051                     LOGGER.warn("Overwritten event " + getHref());
2052                 }
2053             }
2054 
2055             // force responsetype on Exchange 2007
2056             if (ownerResponseReply != null) {
2057                 EWSMethod.Item responseTypeItem = new EWSMethod.Item();
2058                 responseTypeItem.referenceItemId = new ItemId("ReferenceItemId", createOrUpdateItemMethod.getResponseItem());
2059                 responseTypeItem.type = ownerResponseReply;
2060                 createOrUpdateItemMethod = new CreateItemMethod(MessageDisposition.SaveOnly, SendMeetingInvitations.SendToNone, null, responseTypeItem);
2061                 executeMethod(createOrUpdateItemMethod);
2062 
2063                 // force urlcompname again
2064                 ArrayList<FieldUpdate> updates = new ArrayList<>();
2065                 updates.add(Field.createFieldUpdate("urlcompname", convertItemNameToEML(itemName)));
2066                 createOrUpdateItemMethod = new UpdateItemMethod(MessageDisposition.SaveOnly,
2067                         ConflictResolution.AlwaysOverwrite,
2068                         SendMeetingInvitationsOrCancellations.SendToNone,
2069                         new ItemId(createOrUpdateItemMethod.getResponseItem()),
2070                         updates);
2071                 executeMethod(createOrUpdateItemMethod);
2072             }
2073 
2074             // handle deleted occurrences
2075             if (!vCalendar.isTodo() && currentItemId != null && !isMeetingResponse && !isMozDismiss) {
2076                 handleExcludedDates(currentItemId, vCalendar);
2077                 handleModifiedOccurrences(currentItemId, vCalendar, sendMeetingInvitationsOrCancellations);
2078             }
2079 
2080 
2081             // update etag
2082             if (createOrUpdateItemMethod.getResponseItem() != null) {
2083                 ItemId newItemId = new ItemId(createOrUpdateItemMethod.getResponseItem());
2084                 GetItemMethod getItemMethod = new GetItemMethod(BaseShape.ID_ONLY, newItemId, false);
2085                 getItemMethod.addAdditionalProperty(Field.get("etag"));
2086                 executeMethod(getItemMethod);
2087                 itemResult.etag = getItemMethod.getResponseItem().get(Field.get("etag").getResponseName());
2088                 itemResult.itemName = StringUtil.base64ToUrl(newItemId.id) + ".EML";
2089             }
2090 
2091             return itemResult;
2092 
2093         }
2094 
2095         @Override
2096         public byte[] getEventContent() throws IOException {
2097             byte[] content;
2098             if (LOGGER.isDebugEnabled()) {
2099                 LOGGER.debug("Get event: " + itemName);
2100             }
2101             try {
2102                 GetItemMethod getItemMethod;
2103                 if ("Task".equals(type)) {
2104                     getItemMethod = new GetItemMethod(BaseShape.ID_ONLY, itemId, false);
2105                     getItemMethod.addAdditionalProperty(Field.get("importance"));
2106                     getItemMethod.addAdditionalProperty(Field.get("subject"));
2107                     getItemMethod.addAdditionalProperty(Field.get("created"));
2108                     getItemMethod.addAdditionalProperty(Field.get("lastmodified"));
2109                     getItemMethod.addAdditionalProperty(Field.get("calendaruid"));
2110                     getItemMethod.addAdditionalProperty(Field.get("description"));
2111                     if (isExchange2013OrLater()) {
2112                         getItemMethod.addAdditionalProperty(Field.get("textbody"));
2113                     }
2114                     getItemMethod.addAdditionalProperty(Field.get("percentcomplete"));
2115                     getItemMethod.addAdditionalProperty(Field.get("taskstatus"));
2116                     getItemMethod.addAdditionalProperty(Field.get("startdate"));
2117                     getItemMethod.addAdditionalProperty(Field.get("duedate"));
2118                     getItemMethod.addAdditionalProperty(Field.get("datecompleted"));
2119                     getItemMethod.addAdditionalProperty(Field.get("keywords"));
2120 
2121                 } else if (!"Message".equals(type)
2122                         && !"MeetingCancellation".equals(type)
2123                         && !"MeetingResponse".equals(type)) {
2124                     getItemMethod = new GetItemMethod(BaseShape.ID_ONLY, itemId, true);
2125                     getItemMethod.addAdditionalProperty(Field.get("lastmodified"));
2126                     getItemMethod.addAdditionalProperty(Field.get("reminderset"));
2127                     getItemMethod.addAdditionalProperty(Field.get("calendaruid"));
2128                     getItemMethod.addAdditionalProperty(Field.get("myresponsetype"));
2129                     getItemMethod.addAdditionalProperty(Field.get("requiredattendees"));
2130                     getItemMethod.addAdditionalProperty(Field.get("optionalattendees"));
2131                     getItemMethod.addAdditionalProperty(Field.get("modifiedoccurrences"));
2132                     getItemMethod.addAdditionalProperty(Field.get("xmozlastack"));
2133                     getItemMethod.addAdditionalProperty(Field.get("xmozsnoozetime"));
2134                     getItemMethod.addAdditionalProperty(Field.get("xmozsendinvitations"));
2135                 } else {
2136                     getItemMethod = new GetItemMethod(BaseShape.ID_ONLY, itemId, true);
2137                 }
2138 
2139                 executeMethod(getItemMethod);
2140                 if ("Task".equals(type)) {
2141                     VCalendar localVCalendar = new VCalendar();
2142                     VObject vTodo = new VObject();
2143                     vTodo.type = "VTODO";
2144                     localVCalendar.setTimezone(getVTimezone());
2145                     vTodo.setPropertyValue("LAST-MODIFIED", convertDateFromExchange(getItemMethod.getResponseItem().get(Field.get("lastmodified").getResponseName())));
2146                     vTodo.setPropertyValue("CREATED", convertDateFromExchange(getItemMethod.getResponseItem().get(Field.get("created").getResponseName())));
2147                     String calendarUid = getItemMethod.getResponseItem().get(Field.get("calendaruid").getResponseName());
2148                     if (calendarUid == null) {
2149                         // use item id as uid for Exchange created tasks
2150                         calendarUid = itemId.id;
2151                     }
2152                     vTodo.setPropertyValue("UID", calendarUid);
2153                     vTodo.setPropertyValue("SUMMARY", getItemMethod.getResponseItem().get(Field.get("subject").getResponseName()));
2154                     String description = getItemMethod.getResponseItem().get(Field.get("description").getResponseName());
2155                     if (description == null) {
2156                         // Exchange 2013: try to get description from body
2157                         description = getItemMethod.getResponseItem().get(Field.get("textbody").getResponseName());
2158                     }
2159                     vTodo.setPropertyValue("DESCRIPTION", description);
2160                     vTodo.setPropertyValue("PRIORITY", convertPriorityFromExchange(getItemMethod.getResponseItem().get(Field.get("importance").getResponseName())));
2161                     vTodo.setPropertyValue("PERCENT-COMPLETE", getItemMethod.getResponseItem().get(Field.get("percentcomplete").getResponseName()));
2162                     vTodo.setPropertyValue("STATUS", taskTovTodoStatusMap.get(getItemMethod.getResponseItem().get(Field.get("taskstatus").getResponseName())));
2163 
2164                     vTodo.setPropertyValue("DUE;VALUE=DATE", convertDateFromExchangeToTaskDate(getItemMethod.getResponseItem().get(Field.get("duedate").getResponseName())));
2165                     vTodo.setPropertyValue("DTSTART;VALUE=DATE", convertDateFromExchangeToTaskDate(getItemMethod.getResponseItem().get(Field.get("startdate").getResponseName())));
2166                     vTodo.setPropertyValue("COMPLETED;VALUE=DATE", convertDateFromExchangeToTaskDate(getItemMethod.getResponseItem().get(Field.get("datecompleted").getResponseName())));
2167 
2168                     vTodo.setPropertyValue("CATEGORIES", getItemMethod.getResponseItem().get(Field.get("keywords").getResponseName()));
2169 
2170                     localVCalendar.addVObject(vTodo);
2171                     content = localVCalendar.toString().getBytes(StandardCharsets.UTF_8);
2172                 } else {
2173                     content = getItemMethod.getMimeContent();
2174                     if (content == null) {
2175                         throw new IOException("empty event body");
2176                     }
2177                     if (!"CalendarItem".equals(type)) {
2178                         content = getICS(new SharedByteArrayInputStream(content));
2179                     }
2180                     VCalendar localVCalendar = new VCalendar(content, email, getVTimezone());
2181 
2182                     String calendaruid = getItemMethod.getResponseItem().get(Field.get("calendaruid").getResponseName());
2183 
2184                     if ("Exchange2007_SP1".equals(serverVersion)) {
2185                         // remove additional reminder
2186                         if (!"true".equals(getItemMethod.getResponseItem().get(Field.get("reminderset").getResponseName()))) {
2187                             localVCalendar.removeVAlarm();
2188                         }
2189                         if (calendaruid != null) {
2190                             localVCalendar.setFirstVeventPropertyValue("UID", calendaruid);
2191                         }
2192                     }
2193                     fixAttendees(getItemMethod, localVCalendar.getFirstVevent());
2194                     // fix UID and RECURRENCE-ID, broken at least on Exchange 2007
2195                     List<EWSMethod.Occurrence> occurences = getItemMethod.getResponseItem().getOccurrences();
2196                     if (occurences != null) {
2197                         Iterator<VObject> modifiedOccurrencesIterator = localVCalendar.getModifiedOccurrences().iterator();
2198                         for (EWSMethod.Occurrence occurrence : occurences) {
2199                             if (modifiedOccurrencesIterator.hasNext()) {
2200                                 VObject modifiedOccurrence = modifiedOccurrencesIterator.next();
2201                                 // fix modified occurrences attendees
2202                                 GetItemMethod getOccurrenceMethod = new GetItemMethod(BaseShape.ID_ONLY, occurrence.itemId, false);
2203                                 getOccurrenceMethod.addAdditionalProperty(Field.get("requiredattendees"));
2204                                 getOccurrenceMethod.addAdditionalProperty(Field.get("optionalattendees"));
2205                                 getOccurrenceMethod.addAdditionalProperty(Field.get("modifiedoccurrences"));
2206                                 getOccurrenceMethod.addAdditionalProperty(Field.get("lastmodified"));
2207                                 executeMethod(getOccurrenceMethod);
2208                                 fixAttendees(getOccurrenceMethod, modifiedOccurrence);
2209                                 // LAST-MODIFIED is missing in event content
2210                                 modifiedOccurrence.setPropertyValue("LAST-MODIFIED", convertDateFromExchange(getOccurrenceMethod.getResponseItem().get(Field.get("lastmodified").getResponseName())));
2211 
2212                                 // fix uid, should be the same as main VEVENT
2213                                 if (calendaruid != null) {
2214                                     modifiedOccurrence.setPropertyValue("UID", calendaruid);
2215                                 }
2216 
2217                                 VProperty recurrenceId = modifiedOccurrence.getProperty("RECURRENCE-ID");
2218                                 if (recurrenceId != null) {
2219                                     recurrenceId.removeParam("TZID");
2220                                     recurrenceId.getValues().set(0, convertDateFromExchange(occurrence.originalStart));
2221                                 }
2222                             }
2223                         }
2224                     }
2225                     // LAST-MODIFIED is missing in event content
2226                     localVCalendar.setFirstVeventPropertyValue("LAST-MODIFIED", convertDateFromExchange(getItemMethod.getResponseItem().get(Field.get("lastmodified").getResponseName())));
2227 
2228                     // restore mozilla invitations option
2229                     localVCalendar.setFirstVeventPropertyValue("X-MOZ-SEND-INVITATIONS",
2230                             getItemMethod.getResponseItem().get(Field.get("xmozsendinvitations").getResponseName()));
2231                     // restore mozilla alarm status
2232                     localVCalendar.setFirstVeventPropertyValue("X-MOZ-LASTACK",
2233                             getItemMethod.getResponseItem().get(Field.get("xmozlastack").getResponseName()));
2234                     localVCalendar.setFirstVeventPropertyValue("X-MOZ-SNOOZE-TIME",
2235                             getItemMethod.getResponseItem().get(Field.get("xmozsnoozetime").getResponseName()));
2236                     // overwrite method
2237                     // localVCalendar.setPropertyValue("METHOD", "REQUEST");
2238                     content = localVCalendar.toString().getBytes(StandardCharsets.UTF_8);
2239                 }
2240             } catch (IOException | MessagingException e) {
2241                 throw buildHttpNotFoundException(e);
2242             }
2243             return content;
2244         }
2245 
2246         protected void fixAttendees(GetItemMethod getItemMethod, VObject vEvent) throws EWSException {
2247             if (getItemMethod.getResponseItem() != null) {
2248                 List<EWSMethod.Attendee> attendees = getItemMethod.getResponseItem().getAttendees();
2249                 if (attendees != null) {
2250                     for (EWSMethod.Attendee attendee : attendees) {
2251                         VProperty attendeeProperty = new VProperty("ATTENDEE", "mailto:" + attendee.email);
2252                         attendeeProperty.addParam("CN", attendee.name);
2253                         String myResponseType = getItemMethod.getResponseItem().get(Field.get("myresponsetype").getResponseName());
2254                         if (email.equalsIgnoreCase(attendee.email) && myResponseType != null) {
2255                             attendeeProperty.addParam("PARTSTAT", EWSMethod.responseTypeToPartstat(myResponseType));
2256                         } else {
2257                             attendeeProperty.addParam("PARTSTAT", attendee.partstat);
2258                         }
2259                         //attendeeProperty.addParam("RSVP", "TRUE");
2260                         attendeeProperty.addParam("ROLE", attendee.role);
2261                         vEvent.addProperty(attendeeProperty);
2262                     }
2263                 }
2264             }
2265         }
2266     }
2267 
2268     private boolean isExchange2013OrLater() {
2269         return "Exchange2013".compareTo(serverVersion) <= 0;
2270     }
2271 
2272     /**
2273      * Get all contacts and distribution lists in provided folder.
2274      *
2275      * @param folderPath Exchange folder path
2276      * @return list of contacts
2277      * @throws IOException on error
2278      */
2279     @Override
2280     public List<ExchangeSession.Contact> getAllContacts(String folderPath, boolean includeDistList) throws IOException {
2281         Condition condition;
2282         if (includeDistList) {
2283             condition = or(isEqualTo("outlookmessageclass", "IPM.Contact"), isEqualTo("outlookmessageclass", "IPM.DistList"));
2284         } else {
2285             condition = isEqualTo("outlookmessageclass", "IPM.Contact");
2286         }
2287         return searchContacts(folderPath, ExchangeSession.CONTACT_ATTRIBUTES, condition, 0);
2288     }
2289 
2290     @Override
2291     public List<ExchangeSession.Contact> searchContacts(String folderPath, Set<String> attributes, Condition condition, int maxCount) throws IOException {
2292         List<ExchangeSession.Contact> contacts = new ArrayList<>();
2293         List<EWSMethod.Item> responses = searchItems(folderPath, attributes, condition,
2294                 FolderQueryTraversal.SHALLOW, maxCount);
2295 
2296         for (EWSMethod.Item response : responses) {
2297             contacts.add(new Contact(response));
2298         }
2299         return contacts;
2300     }
2301 
2302     @Override
2303     protected Condition getCalendarItemCondition(Condition dateCondition) {
2304         // tasks in calendar not supported over EWS => do not look for instancetype null
2305         return or(
2306                 // Exchange 2010
2307                 or(isTrue("isrecurring"),
2308                         and(isFalse("isrecurring"), dateCondition)),
2309                 // Exchange 2007
2310                 or(isEqualTo("instancetype", 1),
2311                         and(isEqualTo("instancetype", 0), dateCondition))
2312         );
2313     }
2314 
2315     @Override
2316     public List<ExchangeSession.Event> getEventMessages(String folderPath) throws IOException {
2317         return searchEvents(folderPath, ITEM_PROPERTIES,
2318                 and(startsWith("outlookmessageclass", "IPM.Schedule.Meeting."),
2319                         or(isNull("processed"), isFalse("processed"))));
2320     }
2321 
2322     @Override
2323     public List<ExchangeSession.Event> searchEvents(String folderPath, Set<String> attributes, Condition condition) throws IOException {
2324         List<ExchangeSession.Event> events = new ArrayList<>();
2325         List<EWSMethod.Item> responses = searchItems(folderPath, attributes,
2326                 condition,
2327                 FolderQueryTraversal.SHALLOW, 0);
2328         for (EWSMethod.Item response : responses) {
2329             Event event = new Event(folderPath, response);
2330             if ("Message".equals(event.type)) {
2331                 // TODO: just exclude
2332                 // need to check body
2333                 try {
2334                     event.getEventContent();
2335                     events.add(event);
2336                 } catch (HttpNotFoundException e) {
2337                     LOGGER.warn("Ignore invalid event " + event.getHref());
2338                 }
2339                 // exclude exceptions
2340             } else if (event.isException) {
2341                 LOGGER.debug("Exclude recurrence exception " + event.getHref());
2342             } else {
2343                 events.add(event);
2344             }
2345 
2346         }
2347 
2348         return events;
2349     }
2350 
2351     /**
2352      * Common item properties
2353      */
2354     protected static final Set<String> ITEM_PROPERTIES = new HashSet<>();
2355 
2356     static {
2357         ITEM_PROPERTIES.add("etag");
2358         ITEM_PROPERTIES.add("displayname");
2359         // calendar CdoInstanceType
2360         ITEM_PROPERTIES.add("instancetype");
2361         ITEM_PROPERTIES.add("urlcompname");
2362         ITEM_PROPERTIES.add("subject");
2363     }
2364 
2365     protected static final HashSet<String> EVENT_REQUEST_PROPERTIES = new HashSet<>();
2366 
2367     static {
2368         EVENT_REQUEST_PROPERTIES.add("permanenturl");
2369         EVENT_REQUEST_PROPERTIES.add("etag");
2370         EVENT_REQUEST_PROPERTIES.add("displayname");
2371         EVENT_REQUEST_PROPERTIES.add("subject");
2372         EVENT_REQUEST_PROPERTIES.add("urlcompname");
2373         EVENT_REQUEST_PROPERTIES.add("displayto");
2374         EVENT_REQUEST_PROPERTIES.add("displaycc");
2375 
2376         EVENT_REQUEST_PROPERTIES.add("xmozlastack");
2377         EVENT_REQUEST_PROPERTIES.add("xmozsnoozetime");
2378     }
2379 
2380     protected static final HashSet<String> CALENDAR_ITEM_REQUEST_PROPERTIES = new HashSet<>();
2381 
2382     static {
2383         CALENDAR_ITEM_REQUEST_PROPERTIES.addAll(EVENT_REQUEST_PROPERTIES);
2384         CALENDAR_ITEM_REQUEST_PROPERTIES.add("ismeeting");
2385         CALENDAR_ITEM_REQUEST_PROPERTIES.add("myresponsetype");
2386     }
2387 
2388     @Override
2389     protected Set<String> getItemProperties() {
2390         return ITEM_PROPERTIES;
2391     }
2392 
2393     protected EWSMethod.Item getEwsItem(String folderPath, String itemName, Set<String> itemProperties) throws IOException {
2394         EWSMethod.Item item = null;
2395         String urlcompname = convertItemNameToEML(itemName);
2396         // workaround for missing urlcompname in Exchange 2010
2397         if (isItemId(urlcompname)) {
2398             ItemId itemId = new ItemId(StringUtil.urlToBase64(urlcompname.substring(0, urlcompname.indexOf('.'))));
2399             GetItemMethod getItemMethod = new GetItemMethod(BaseShape.ID_ONLY, itemId, false);
2400             for (String attribute : itemProperties) {
2401                 getItemMethod.addAdditionalProperty(Field.get(attribute));
2402             }
2403             executeMethod(getItemMethod);
2404             item = getItemMethod.getResponseItem();
2405         }
2406         // find item by urlcompname
2407         if (item == null) {
2408             List<EWSMethod.Item> responses = searchItems(folderPath, itemProperties, isEqualTo("urlcompname", urlcompname), FolderQueryTraversal.SHALLOW, 0);
2409             if (!responses.isEmpty()) {
2410                 item = responses.get(0);
2411             }
2412         }
2413         return item;
2414     }
2415 
2416 
2417     @Override
2418     public Item getItem(String folderPath, String itemName) throws IOException {
2419         EWSMethod.Item item = getEwsItem(folderPath, itemName, EVENT_REQUEST_PROPERTIES);
2420         if (item == null && isMainCalendar(folderPath)) {
2421             // look for item in task folder, replace extension first
2422             if (itemName.endsWith(".ics")) {
2423                 item = getEwsItem(TASKS, itemName.substring(0, itemName.length() - 3) + "EML", EVENT_REQUEST_PROPERTIES);
2424             } else {
2425                 item = getEwsItem(TASKS, itemName, EVENT_REQUEST_PROPERTIES);
2426             }
2427         }
2428 
2429         if (item == null) {
2430             throw new HttpNotFoundException(itemName + " not found in " + folderPath);
2431         }
2432 
2433         String itemType = item.type;
2434         if ("Contact".equals(itemType) || "DistributionList".equals(itemType)) {
2435             // retrieve Contact properties
2436             ItemId itemId = new ItemId(item);
2437             GetItemMethod getItemMethod = new GetItemMethod(BaseShape.ID_ONLY, itemId, false);
2438             Set<String> attributes = CONTACT_ATTRIBUTES;
2439             if ("DistributionList".equals(itemType)) {
2440                 attributes = DISTRIBUTION_LIST_ATTRIBUTES;
2441             }
2442             for (String attribute : attributes) {
2443                 getItemMethod.addAdditionalProperty(Field.get(attribute));
2444             }
2445             executeMethod(getItemMethod);
2446             item = getItemMethod.getResponseItem();
2447             if (item == null) {
2448                 throw new HttpNotFoundException(itemName + " not found in " + folderPath);
2449             }
2450             return new Contact(item);
2451         } else if ("CalendarItem".equals(itemType)
2452                 || "MeetingMessage".equals(itemType)
2453                 || "MeetingRequest".equals(itemType)
2454                 || "MeetingResponse".equals(itemType)
2455                 || "MeetingCancellation".equals(itemType)
2456                 || "Task".equals(itemType)
2457                 // VTODOs appear as Messages
2458                 || "Message".equals(itemType)) {
2459             Event event = new Event(folderPath, item);
2460             // force item name to client provided name (for tasks)
2461             event.setItemName(itemName);
2462             return event;
2463         } else {
2464             throw new HttpNotFoundException(itemName + " not found in " + folderPath);
2465         }
2466 
2467     }
2468 
2469     @Override
2470     public ContactPhoto getContactPhoto(ExchangeSession.Contact contact) throws IOException {
2471         ContactPhoto contactPhoto;
2472 
2473         GetItemMethod getItemMethod = new GetItemMethod(BaseShape.ID_ONLY, ((EwsExchangeSession.Contact) contact).itemId, false);
2474         getItemMethod.addAdditionalProperty(Field.get("attachments"));
2475         executeMethod(getItemMethod);
2476         EWSMethod.Item item = getItemMethod.getResponseItem();
2477         if (item == null) {
2478             throw new IOException("Missing contact picture");
2479         }
2480         FileAttachment attachment = item.getAttachmentByName("ContactPicture.jpg");
2481         if (attachment == null) {
2482             throw new IOException("Missing contact picture");
2483         }
2484         // get attachment content
2485         GetAttachmentMethod getAttachmentMethod = new GetAttachmentMethod(attachment.attachmentId);
2486         executeMethod(getAttachmentMethod);
2487 
2488         contactPhoto = new ContactPhoto();
2489         contactPhoto.content = getAttachmentMethod.getResponseItem().get("Content");
2490         if (attachment.contentType == null) {
2491             contactPhoto.contentType = "image/jpeg";
2492         } else {
2493             contactPhoto.contentType = attachment.contentType;
2494         }
2495 
2496         return contactPhoto;
2497     }
2498 
2499     @Override
2500     public ContactPhoto getADPhoto(String email) {
2501         ContactPhoto contactPhoto = null;
2502 
2503         if (email != null) {
2504             try {
2505                 GetUserPhotoMethod userPhotoMethod = new GetUserPhotoMethod(email, GetUserPhotoMethod.SizeRequested.HR240x240);
2506                 executeMethod(userPhotoMethod);
2507                 if (userPhotoMethod.getPictureData() != null) {
2508                     contactPhoto = new ContactPhoto();
2509                     contactPhoto.content = userPhotoMethod.getPictureData();
2510                     contactPhoto.contentType = userPhotoMethod.getContentType();
2511                     if (contactPhoto.contentType == null) {
2512                         contactPhoto.contentType = "image/jpeg";
2513                     }
2514                 }
2515             } catch (IOException e) {
2516                 LOGGER.debug("Error loading contact image from AD " + e + " " + e.getMessage());
2517             }
2518         }
2519 
2520         return contactPhoto;
2521     }
2522 
2523     @Override
2524     public void deleteItem(String folderPath, String itemName) throws IOException {
2525         EWSMethod.Item item = getEwsItem(folderPath, itemName, EVENT_REQUEST_PROPERTIES);
2526         if (item != null && "CalendarItem".equals(item.type)) {
2527             // reload with calendar property
2528             if (serverVersion.compareTo("Exchange2013") >= 0) {
2529                 CALENDAR_ITEM_REQUEST_PROPERTIES.add("isorganizer");
2530             }
2531             item = getEwsItem(folderPath, itemName, CALENDAR_ITEM_REQUEST_PROPERTIES);
2532         }
2533         if (item == null && isMainCalendar(folderPath)) {
2534             // look for item in task folder
2535             item = getEwsItem(TASKS, itemName, EVENT_REQUEST_PROPERTIES);
2536         }
2537         if (item != null) {
2538             boolean isMeeting = "true".equals(item.get(Field.get("ismeeting").getResponseName()));
2539             boolean isOrganizer;
2540             if (item.get(Field.get("isorganizer").getResponseName()) != null) {
2541                 // Exchange 2013 or later
2542                 isOrganizer = "true".equals(item.get(Field.get("isorganizer").getResponseName()));
2543             } else {
2544                 isOrganizer = "Organizer".equals(item.get(Field.get("myresponsetype").getResponseName()));
2545             }
2546             boolean hasAttendees = item.get(Field.get("displayto").getResponseName()) != null
2547                     || item.get(Field.get("displaycc").getResponseName()) != null;
2548 
2549             if (isMeeting && isOrganizer && hasAttendees
2550                     && !isSharedFolder(folderPath)
2551                     && Settings.getBooleanProperty("davmail.caldavAutoSchedule", true)) {
2552                 // cancel meeting
2553                 SendMeetingInvitations sendMeetingInvitations = SendMeetingInvitations.SendToAllAndSaveCopy;
2554                 MessageDisposition messageDisposition = MessageDisposition.SendAndSaveCopy;
2555                 String body = null;
2556                 // This is a meeting cancel, let user edit notification message
2557                 if (Settings.getBooleanProperty("davmail.caldavEditNotifications")) {
2558                     String vEventSubject = item.get(Field.get("subject").getResponseName());
2559                     if (vEventSubject == null) {
2560                         vEventSubject = "";
2561                     }
2562                     String notificationSubject = (BundleMessage.format("CANCELLED") + vEventSubject);
2563 
2564                     NotificationDialog notificationDialog = new NotificationDialog(notificationSubject, "");
2565                     if (!notificationDialog.getSendNotification()) {
2566                         LOGGER.debug("Notification canceled by user");
2567                         sendMeetingInvitations = SendMeetingInvitations.SendToNone;
2568                         messageDisposition = MessageDisposition.SaveOnly;
2569                     }
2570                     // get description from dialog
2571                     body = notificationDialog.getBody();
2572                 }
2573                 EWSMethod.Item cancelItem = new EWSMethod.Item();
2574                 cancelItem.type = "CancelCalendarItem";
2575                 cancelItem.referenceItemId = new ItemId("ReferenceItemId", item);
2576                 if (body != null && !body.isEmpty()) {
2577                     item.put("Body", body);
2578                 }
2579                 CreateItemMethod cancelItemMethod = new CreateItemMethod(messageDisposition,
2580                         sendMeetingInvitations,
2581                         getFolderId(SENT),
2582                         cancelItem
2583                 );
2584                 executeMethod(cancelItemMethod);
2585 
2586             } else {
2587                 DeleteType deleteType = DeleteType.MoveToDeletedItems;
2588                 if (isSharedFolder(folderPath)) {
2589                     // can't move event to trash in a shared mailbox
2590                     deleteType = DeleteType.HardDelete;
2591                 }
2592                 // delete item
2593                 DeleteItemMethod deleteItemMethod = new DeleteItemMethod(new ItemId(item), deleteType, SendMeetingCancellations.SendToAllAndSaveCopy);
2594                 executeMethod(deleteItemMethod);
2595             }
2596         }
2597     }
2598 
2599     @Override
2600     public void processItem(String folderPath, String itemName) throws IOException {
2601         EWSMethod.Item item = getEwsItem(folderPath, itemName, EVENT_REQUEST_PROPERTIES);
2602         if (item != null) {
2603             HashMap<String, String> localProperties = new HashMap<>();
2604             localProperties.put("processed", "1");
2605             localProperties.put("read", "1");
2606             UpdateItemMethod updateItemMethod = new UpdateItemMethod(MessageDisposition.SaveOnly,
2607                     ConflictResolution.AlwaysOverwrite,
2608                     SendMeetingInvitationsOrCancellations.SendToNone,
2609                     new ItemId(item), buildProperties(localProperties));
2610             executeMethod(updateItemMethod);
2611         }
2612     }
2613 
2614     @Override
2615     public int sendEvent(String icsBody) throws IOException {
2616         String itemName = UUID.randomUUID() + ".EML";
2617         byte[] mimeContent = new Event(DRAFTS, itemName, "urn:content-classes:calendarmessage", icsBody, null, null).createMimeContent();
2618         if (mimeContent == null) {
2619             // no recipients, cancel
2620             return HttpStatus.SC_NO_CONTENT;
2621         } else {
2622             sendMessage(null, mimeContent);
2623             return HttpStatus.SC_OK;
2624         }
2625     }
2626 
2627     @Override
2628     protected Contact buildContact(String folderPath, String itemName, Map<String, String> properties, String etag, String noneMatch) {
2629         return new Contact(folderPath, itemName, properties, StringUtil.removeQuotes(etag), noneMatch);
2630     }
2631 
2632     @Override
2633     protected ItemResult internalCreateOrUpdateEvent(String folderPath, String itemName, String contentClass, String icsBody, String etag, String noneMatch) throws IOException {
2634         return new Event(folderPath, itemName, contentClass, icsBody, StringUtil.removeQuotes(etag), noneMatch).createOrUpdate();
2635     }
2636 
2637     @Override
2638     public boolean isSharedFolder(String folderPath) {
2639         return folderPath.startsWith("/") && !folderPath.toLowerCase().startsWith(currentMailboxPath);
2640     }
2641 
2642     @Override
2643     public boolean isMainCalendar(String folderPath) throws IOException {
2644         FolderId currentFolderId = getFolderId(folderPath);
2645         FolderId calendarFolderId = getFolderId("calendar");
2646         return calendarFolderId.name.equals(currentFolderId.name) && calendarFolderId.value.equals(currentFolderId.value);
2647     }
2648 
2649     @Override
2650     protected String getFreeBusyData(String attendee, String start, String end, int interval) {
2651         String result = null;
2652         GetUserAvailabilityMethod getUserAvailabilityMethod = new GetUserAvailabilityMethod(attendee, start, end, interval);
2653         try {
2654             executeMethod(getUserAvailabilityMethod);
2655             result = getUserAvailabilityMethod.getMergedFreeBusy();
2656         } catch (IOException e) {
2657             // ignore
2658         }
2659         return result;
2660     }
2661 
2662     @Override
2663     protected void loadVtimezone() {
2664 
2665         try {
2666             String timezoneId;
2667             timezoneId = Settings.getProperty("davmail.timezoneId");
2668             if (timezoneId == null && !"Exchange2007_SP1".equals(serverVersion)) {
2669                 // On Exchange 2010, get user timezone from server
2670                 GetUserConfigurationMethod getUserConfigurationMethod = new GetUserConfigurationMethod();
2671                 executeMethod(getUserConfigurationMethod);
2672                 EWSMethod.Item item = getUserConfigurationMethod.getResponseItem();
2673                 if (item != null) {
2674                     timezoneId = item.get("timezone");
2675                 }
2676             } else if (!directEws) {
2677                 timezoneId = getTimezoneidFromOptions();
2678             }
2679             // failover: use timezone id from settings file
2680             // last failover: use GMT
2681             if (timezoneId == null) {
2682                 LOGGER.warn("Unable to get user timezone, using GMT Standard Time. Set davmail.timezoneId setting to override this.");
2683                 timezoneId = "GMT Standard Time";
2684             }
2685 
2686             // delete existing temp folder first to avoid errors
2687             deleteFolder("davmailtemp");
2688             createCalendarFolder("davmailtemp", null);
2689             EWSMethod.Item item = new EWSMethod.Item();
2690             item.type = "CalendarItem";
2691             if (!"Exchange2007_SP1".equals(serverVersion)) {
2692                 SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH);
2693                 dateFormatter.setTimeZone(GMT_TIMEZONE);
2694                 Calendar cal = Calendar.getInstance();
2695                 item.put("Start", dateFormatter.format(cal.getTime()));
2696                 cal.add(Calendar.DAY_OF_MONTH, 1);
2697                 item.put("End", dateFormatter.format(cal.getTime()));
2698                 item.put("StartTimeZone", timezoneId);
2699             } else {
2700                 item.put("MeetingTimeZone", timezoneId);
2701             }
2702             CreateItemMethod createItemMethod = new CreateItemMethod(MessageDisposition.SaveOnly, SendMeetingInvitations.SendToNone, getFolderId("davmailtemp"), item);
2703             executeMethod(createItemMethod);
2704             item = createItemMethod.getResponseItem();
2705             if (item == null) {
2706                 throw new IOException("Empty timezone item");
2707             }
2708             VCalendar vCalendar = new VCalendar(getContent(new ItemId(item)), email, null);
2709             this.vTimezone = vCalendar.getVTimezone();
2710             // delete temporary folder
2711             deleteFolder("davmailtemp");
2712         } catch (IOException e) {
2713             LOGGER.warn("Unable to get VTIMEZONE info: " + e, e);
2714         }
2715     }
2716 
2717     protected String getTimezoneidFromOptions() {
2718         String result = null;
2719         // get time zone setting from html body
2720         String optionsPath = "/owa/?ae=Options&t=Regional";
2721         GetRequest optionsMethod = new GetRequest(optionsPath);
2722         try (
2723                 CloseableHttpResponse response = httpClient.execute(optionsMethod);
2724                 BufferedReader optionsPageReader = new BufferedReader(new InputStreamReader(response.getEntity().getContent(), StandardCharsets.UTF_8))
2725         ) {
2726             String line;
2727             // find timezone
2728             //noinspection StatementWithEmptyBody
2729             while ((line = optionsPageReader.readLine()) != null
2730                     && (!line.contains("tblTmZn"))
2731                     && (!line.contains("selTmZn"))) {
2732             }
2733             if (line != null) {
2734                 if (line.contains("tblTmZn")) {
2735                     int start = line.indexOf("oV=\"") + 4;
2736                     int end = line.indexOf('\"', start);
2737                     result = line.substring(start, end);
2738                 } else {
2739                     int end = line.lastIndexOf("\" selected>");
2740                     int start = line.lastIndexOf('\"', end - 1);
2741                     result = line.substring(start + 1, end);
2742                 }
2743             }
2744         } catch (IOException e) {
2745             LOGGER.error("Error parsing options page at " + optionsPath);
2746         }
2747 
2748         return result;
2749     }
2750 
2751 
2752     protected FolderId getFolderId(String folderPath) throws IOException {
2753         FolderId folderId = getFolderIdIfExists(folderPath);
2754         if (folderId == null) {
2755             throw new HttpNotFoundException("Folder '" + folderPath + "' not found");
2756         }
2757         return folderId;
2758     }
2759 
2760     protected static final String USERS_ROOT = "/users/";
2761 
2762     protected FolderId getFolderIdIfExists(String folderPath) throws IOException {
2763         String lowerCaseFolderPath = folderPath.toLowerCase();
2764         if (lowerCaseFolderPath.equals(currentMailboxPath)) {
2765             return getSubFolderIdIfExists(null, "");
2766         } else if (lowerCaseFolderPath.startsWith(currentMailboxPath + '/')) {
2767             return getSubFolderIdIfExists(null, folderPath.substring(currentMailboxPath.length() + 1));
2768         } else if (folderPath.startsWith("/users/")) {
2769             int slashIndex = folderPath.indexOf('/', USERS_ROOT.length());
2770             String mailbox;
2771             String subFolderPath;
2772             if (slashIndex >= 0) {
2773                 mailbox = folderPath.substring(USERS_ROOT.length(), slashIndex);
2774                 subFolderPath = folderPath.substring(slashIndex + 1);
2775             } else {
2776                 mailbox = folderPath.substring(USERS_ROOT.length());
2777                 subFolderPath = "";
2778             }
2779             return getSubFolderIdIfExists(mailbox, subFolderPath);
2780         } else {
2781             return getSubFolderIdIfExists(null, folderPath);
2782         }
2783     }
2784 
2785     protected FolderId getSubFolderIdIfExists(String mailbox, String folderPath) throws IOException {
2786         String[] folderNames;
2787         FolderId currentFolderId;
2788 
2789         if ("/public".equals(folderPath)) {
2790             return DistinguishedFolderId.getInstance(mailbox, DistinguishedFolderId.Name.publicfoldersroot);
2791         } else if ("/archive".equals(folderPath)) {
2792             return DistinguishedFolderId.getInstance(mailbox, DistinguishedFolderId.Name.archivemsgfolderroot);
2793         } else if (isSubFolderOf(folderPath, PUBLIC_ROOT)) {
2794             currentFolderId = DistinguishedFolderId.getInstance(mailbox, DistinguishedFolderId.Name.publicfoldersroot);
2795             folderNames = folderPath.substring(PUBLIC_ROOT.length()).split("/");
2796         } else if (isSubFolderOf(folderPath, ARCHIVE_ROOT)) {
2797             currentFolderId = DistinguishedFolderId.getInstance(mailbox, DistinguishedFolderId.Name.archivemsgfolderroot);
2798             folderNames = folderPath.substring(ARCHIVE_ROOT.length()).split("/");
2799         } else if (isSubFolderOf(folderPath, INBOX) ||
2800                 isSubFolderOf(folderPath, LOWER_CASE_INBOX) ||
2801                 isSubFolderOf(folderPath, MIXED_CASE_INBOX)) {
2802             currentFolderId = DistinguishedFolderId.getInstance(mailbox, DistinguishedFolderId.Name.inbox);
2803             folderNames = folderPath.substring(INBOX.length()).split("/");
2804         } else if (isSubFolderOf(folderPath, CALENDAR)) {
2805             currentFolderId = DistinguishedFolderId.getInstance(mailbox, DistinguishedFolderId.Name.calendar);
2806             folderNames = folderPath.substring(CALENDAR.length()).split("/");
2807         } else if (isSubFolderOf(folderPath, TASKS)) {
2808             currentFolderId = DistinguishedFolderId.getInstance(mailbox, DistinguishedFolderId.Name.tasks);
2809             folderNames = folderPath.substring(TASKS.length()).split("/");
2810         } else if (isSubFolderOf(folderPath, CONTACTS)) {
2811             currentFolderId = DistinguishedFolderId.getInstance(mailbox, DistinguishedFolderId.Name.contacts);
2812             folderNames = folderPath.substring(CONTACTS.length()).split("/");
2813         } else if (isSubFolderOf(folderPath, SENT)) {
2814             currentFolderId = DistinguishedFolderId.getInstance(mailbox, DistinguishedFolderId.Name.sentitems);
2815             folderNames = folderPath.substring(SENT.length()).split("/");
2816         } else if (isSubFolderOf(folderPath, DRAFTS)) {
2817             currentFolderId = DistinguishedFolderId.getInstance(mailbox, DistinguishedFolderId.Name.drafts);
2818             folderNames = folderPath.substring(DRAFTS.length()).split("/");
2819         } else if (isSubFolderOf(folderPath, TRASH)) {
2820             currentFolderId = DistinguishedFolderId.getInstance(mailbox, DistinguishedFolderId.Name.deleteditems);
2821             folderNames = folderPath.substring(TRASH.length()).split("/");
2822         } else if (isSubFolderOf(folderPath, JUNK)) {
2823             currentFolderId = DistinguishedFolderId.getInstance(mailbox, DistinguishedFolderId.Name.junkemail);
2824             folderNames = folderPath.substring(JUNK.length()).split("/");
2825         } else if (isSubFolderOf(folderPath, UNSENT)) {
2826             currentFolderId = DistinguishedFolderId.getInstance(mailbox, DistinguishedFolderId.Name.outbox);
2827             folderNames = folderPath.substring(UNSENT.length()).split("/");
2828         } else {
2829             currentFolderId = DistinguishedFolderId.getInstance(mailbox, DistinguishedFolderId.Name.msgfolderroot);
2830             folderNames = folderPath.split("/");
2831         }
2832         for (String folderName : folderNames) {
2833             if (!folderName.isEmpty()) {
2834                 currentFolderId = getSubFolderByName(currentFolderId, folderName);
2835                 if (currentFolderId == null) {
2836                     break;
2837                 }
2838             }
2839         }
2840         return currentFolderId;
2841     }
2842 
2843     /**
2844      * Check if folderPath is base folder or a sub folder path.
2845      *
2846      * @param folderPath folder path
2847      * @param baseFolder base folder
2848      * @return true if folderPath is under baseFolder
2849      */
2850     private boolean isSubFolderOf(String folderPath, String baseFolder) {
2851         if (PUBLIC_ROOT.equals(baseFolder) || ARCHIVE_ROOT.equals(baseFolder)) {
2852             return folderPath.startsWith(baseFolder);
2853         } else {
2854             return folderPath.startsWith(baseFolder)
2855                     && (folderPath.length() == baseFolder.length() || folderPath.charAt(baseFolder.length()) == '/');
2856         }
2857     }
2858 
2859     protected FolderId getSubFolderByName(FolderId parentFolderId, String folderName) throws IOException {
2860         FolderId folderId = null;
2861         FindFolderMethod findFolderMethod = new FindFolderMethod(
2862                 FolderQueryTraversal.SHALLOW,
2863                 BaseShape.ID_ONLY,
2864                 parentFolderId,
2865                 FOLDER_PROPERTIES,
2866                 new TwoOperandExpression(TwoOperandExpression.Operator.IsEqualTo,
2867                         Field.get("folderDisplayName"), decodeFolderName(folderName)),
2868                 0, 1
2869         );
2870         executeMethod(findFolderMethod);
2871         EWSMethod.Item item = findFolderMethod.getResponseItem();
2872         if (item != null) {
2873             folderId = new FolderId(item);
2874         }
2875         return folderId;
2876     }
2877 
2878     public static String decodeFolderName(String folderName) {
2879         if (folderName.contains("_xF8FF_")) {
2880             return folderName.replaceAll("_xF8FF_", "/");
2881         }
2882         if (folderName.contains("_x003E_")) {
2883             return folderName.replaceAll("_x003E_", ">");
2884         }
2885         return folderName;
2886     }
2887 
2888     public static String encodeFolderName(String folderName) {
2889         if (folderName.contains("/")) {
2890             folderName = folderName.replaceAll("/", "_xF8FF_");
2891         }
2892         if (folderName.contains(">")) {
2893             folderName = folderName.replaceAll(">", "_x003E_");
2894         }
2895         return folderName;
2896     }
2897 
2898     long throttlingTimestamp = 0;
2899 
2900     protected int executeMethod(EWSMethod ewsMethod) throws IOException {
2901         long throttlingDelay = throttlingTimestamp - System.currentTimeMillis();
2902         try {
2903             if (throttlingDelay > 0) {
2904                 LOGGER.warn("Throttling active on server, waiting " + (throttlingDelay / 1000) + " seconds");
2905                 try {
2906                     Thread.sleep(throttlingDelay);
2907                 } catch (InterruptedException e1) {
2908                     LOGGER.error("Throttling delay interrupted " + e1.getMessage());
2909                     Thread.currentThread().interrupt();
2910                 }
2911             }
2912             internalExecuteMethod(ewsMethod);
2913         } catch (EWSThrottlingException e) {
2914             // default throttling delay is one minute
2915             throttlingDelay = 60000;
2916             if (ewsMethod.backOffMilliseconds > 0) {
2917                 // server provided a throttling delay, add 10 seconds
2918                 throttlingDelay = ewsMethod.backOffMilliseconds + 10000;
2919             }
2920             throttlingTimestamp = System.currentTimeMillis() + throttlingDelay;
2921 
2922             LOGGER.warn("Throttling active on server, waiting " + (throttlingDelay / 1000) + " seconds");
2923             try {
2924                 Thread.sleep(throttlingDelay);
2925             } catch (InterruptedException e1) {
2926                 LOGGER.error("Throttling delay interrupted " + e1.getMessage());
2927                 Thread.currentThread().interrupt();
2928             }
2929             // retry once
2930             internalExecuteMethod(ewsMethod);
2931         }
2932         return ewsMethod.getStatusCode();
2933     }
2934 
2935     protected void internalExecuteMethod(EWSMethod ewsMethod) throws IOException {
2936         ewsMethod.setServerVersion(serverVersion);
2937         if (token != null) {
2938             ewsMethod.setHeader("Authorization", "Bearer " + token.getAccessToken());
2939         }
2940         try (CloseableHttpResponse response = httpClient.execute(ewsMethod)) {
2941             ewsMethod.handleResponse(response);
2942         }
2943         if (serverVersion == null) {
2944             serverVersion = ewsMethod.getServerVersion();
2945         }
2946         ewsMethod.checkSuccess();
2947     }
2948 
2949     protected static final HashMap<String, String> GALFIND_ATTRIBUTE_MAP = new HashMap<>();
2950 
2951     static {
2952         GALFIND_ATTRIBUTE_MAP.put("imapUid", "Name");
2953         GALFIND_ATTRIBUTE_MAP.put("cn", "DisplayName");
2954         GALFIND_ATTRIBUTE_MAP.put("givenName", "GivenName");
2955         GALFIND_ATTRIBUTE_MAP.put("sn", "Surname");
2956         GALFIND_ATTRIBUTE_MAP.put("smtpemail1", "EmailAddress");
2957 
2958         GALFIND_ATTRIBUTE_MAP.put("roomnumber", "OfficeLocation");
2959         GALFIND_ATTRIBUTE_MAP.put("street", "BusinessStreet");
2960         GALFIND_ATTRIBUTE_MAP.put("l", "BusinessCity");
2961         GALFIND_ATTRIBUTE_MAP.put("o", "CompanyName");
2962         GALFIND_ATTRIBUTE_MAP.put("postalcode", "BusinessPostalCode");
2963         GALFIND_ATTRIBUTE_MAP.put("st", "BusinessState");
2964         GALFIND_ATTRIBUTE_MAP.put("co", "BusinessCountryOrRegion");
2965 
2966         GALFIND_ATTRIBUTE_MAP.put("manager", "Manager");
2967         GALFIND_ATTRIBUTE_MAP.put("middlename", "Initials");
2968         GALFIND_ATTRIBUTE_MAP.put("title", "JobTitle");
2969         GALFIND_ATTRIBUTE_MAP.put("department", "Department");
2970 
2971         GALFIND_ATTRIBUTE_MAP.put("otherTelephone", "OtherTelephone");
2972         GALFIND_ATTRIBUTE_MAP.put("telephoneNumber", "BusinessPhone");
2973         GALFIND_ATTRIBUTE_MAP.put("mobile", "MobilePhone");
2974         GALFIND_ATTRIBUTE_MAP.put("facsimiletelephonenumber", "BusinessFax");
2975         GALFIND_ATTRIBUTE_MAP.put("secretarycn", "AssistantName");
2976 
2977         GALFIND_ATTRIBUTE_MAP.put("homePhone", "HomePhone");
2978         GALFIND_ATTRIBUTE_MAP.put("pager", "Pager");
2979         GALFIND_ATTRIBUTE_MAP.put("msexchangecertificate", "MSExchangeCertificate");
2980         GALFIND_ATTRIBUTE_MAP.put("usersmimecertificate", "UserSMIMECertificate");
2981     }
2982 
2983     protected static final HashSet<String> IGNORE_ATTRIBUTE_SET = new HashSet<>();
2984 
2985     static {
2986         IGNORE_ATTRIBUTE_SET.add("ContactSource");
2987         IGNORE_ATTRIBUTE_SET.add("Culture");
2988         IGNORE_ATTRIBUTE_SET.add("AssistantPhone");
2989     }
2990 
2991     protected Contact buildGalfindContact(EWSMethod.Item response) {
2992         Contact contact = new Contact();
2993         contact.setName(response.get("Name"));
2994         contact.put("imapUid", response.get("Name"));
2995         contact.put("uid", response.get("Name"));
2996         if (LOGGER.isDebugEnabled()) {
2997             for (Map.Entry<String, String> entry : response.entrySet()) {
2998                 String key = entry.getKey();
2999                 if (!IGNORE_ATTRIBUTE_SET.contains(key) && !GALFIND_ATTRIBUTE_MAP.containsValue(key)) {
3000                     LOGGER.debug("Unsupported ResolveNames " + contact.getName() + " response attribute: " + key + " value: " + entry.getValue());
3001                 }
3002             }
3003         }
3004         for (Map.Entry<String, String> entry : GALFIND_ATTRIBUTE_MAP.entrySet()) {
3005             String attributeValue = response.get(entry.getValue());
3006             if (attributeValue != null && !attributeValue.isEmpty()) {
3007                 contact.put(entry.getKey(), attributeValue);
3008             }
3009         }
3010         return contact;
3011     }
3012 
3013     @Override
3014     public Map<String, ExchangeSession.Contact> galFind(Condition condition, Set<String> returningAttributes, int sizeLimit) throws IOException {
3015         Map<String, ExchangeSession.Contact> contacts = new HashMap<>();
3016         if (condition instanceof MultiCondition) {
3017             List<Condition> conditions = ((ExchangeSession.MultiCondition) condition).getConditions();
3018             Operator operator = ((ExchangeSession.MultiCondition) condition).getOperator();
3019             if (operator == Operator.Or) {
3020                 for (Condition innerCondition : conditions) {
3021                     contacts.putAll(galFind(innerCondition, returningAttributes, sizeLimit));
3022                 }
3023             } else if (operator == Operator.And && !conditions.isEmpty()) {
3024                 Map<String, ExchangeSession.Contact> innerContacts = galFind(conditions.get(0), returningAttributes, sizeLimit);
3025                 for (ExchangeSession.Contact contact : innerContacts.values()) {
3026                     if (condition.isMatch(contact)) {
3027                         contacts.put(contact.getName().toLowerCase(), contact);
3028                     }
3029                 }
3030             }
3031         } else if (condition instanceof AttributeCondition) {
3032             String mappedAttributeName = GALFIND_ATTRIBUTE_MAP.get(((ExchangeSession.AttributeCondition) condition).getAttributeName());
3033             if (mappedAttributeName != null) {
3034                 String value = ((ExchangeSession.AttributeCondition) condition).getValue().toLowerCase();
3035                 Operator operator = ((AttributeCondition) condition).getOperator();
3036                 String searchValue = value;
3037                 if (mappedAttributeName.startsWith("EmailAddress")) {
3038                     searchValue = "smtp:" + searchValue;
3039                 }
3040                 if (operator == Operator.IsEqualTo) {
3041                     searchValue = '=' + searchValue;
3042                 }
3043                 ResolveNamesMethod resolveNamesMethod = new ResolveNamesMethod(searchValue);
3044                 executeMethod(resolveNamesMethod);
3045                 List<EWSMethod.Item> responses = resolveNamesMethod.getResponseItems();
3046                 if (LOGGER.isDebugEnabled()) {
3047                     LOGGER.debug("ResolveNames(" + searchValue + ") returned " + responses.size() + " results");
3048                 }
3049                 for (EWSMethod.Item response : responses) {
3050                     Contact contact = buildGalfindContact(response);
3051                     if (condition.isMatch(contact)) {
3052                         contacts.put(contact.getName().toLowerCase(), contact);
3053                     }
3054                 }
3055             }
3056         }
3057         return contacts;
3058     }
3059 
3060     protected Date parseDateFromExchange(String exchangeDateValue) throws DavMailException {
3061         Date dateValue = null;
3062         if (exchangeDateValue != null) {
3063             try {
3064                 dateValue = getExchangeZuluDateFormat().parse(exchangeDateValue);
3065             } catch (ParseException e) {
3066                 throw new DavMailException("EXCEPTION_INVALID_DATE", exchangeDateValue);
3067             }
3068         }
3069         return dateValue;
3070     }
3071 
3072     protected String convertDateFromExchange(String exchangeDateValue) throws DavMailException {
3073         // yyyy-MM-dd'T'HH:mm:ss'Z' to yyyyMMdd'T'HHmmss'Z'
3074         if (exchangeDateValue == null) {
3075             return null;
3076         } else {
3077             if (exchangeDateValue.length() != 20) {
3078                 throw new DavMailException("EXCEPTION_INVALID_DATE", exchangeDateValue);
3079             }
3080             StringBuilder buffer = new StringBuilder();
3081             for (int i = 0; i < exchangeDateValue.length(); i++) {
3082                 if (i == 4 || i == 7 || i == 13 || i == 16) {
3083                     i++;
3084                 }
3085                 buffer.append(exchangeDateValue.charAt(i));
3086             }
3087             return buffer.toString();
3088         }
3089     }
3090 
3091     protected String convertCalendarDateToExchange(String vcalendarDateValue) throws DavMailException {
3092         String zuluDateValue = null;
3093         if (vcalendarDateValue != null) {
3094             try {
3095                 SimpleDateFormat dateParser;
3096                 if (vcalendarDateValue.length() == 8) {
3097                     dateParser = new SimpleDateFormat("yyyyMMdd", Locale.ENGLISH);
3098                 } else {
3099                     dateParser = new SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.ENGLISH);
3100                 }
3101                 dateParser.setTimeZone(GMT_TIMEZONE);
3102                 SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH);
3103                 dateFormatter.setTimeZone(GMT_TIMEZONE);
3104                 zuluDateValue = dateFormatter.format(dateParser.parse(vcalendarDateValue));
3105             } catch (ParseException e) {
3106                 throw new DavMailException("EXCEPTION_INVALID_DATE", vcalendarDateValue);
3107             }
3108         }
3109         return zuluDateValue;
3110     }
3111 
3112     public static String convertDateFromExchangeToTaskDate(String exchangeDateValue) throws DavMailException {
3113         String zuluDateValue = null;
3114         if (exchangeDateValue != null) {
3115             try {
3116                 SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.ENGLISH);
3117                 dateFormat.setTimeZone(GMT_TIMEZONE);
3118                 zuluDateValue = dateFormat.format(getExchangeZuluDateFormat().parse(exchangeDateValue));
3119             } catch (ParseException e) {
3120                 throw new DavMailException("EXCEPTION_INVALID_DATE", exchangeDateValue);
3121             }
3122         }
3123         return zuluDateValue;
3124     }
3125 
3126     protected String convertTaskDateToZulu(String value) {
3127         String result = null;
3128         if (value != null && !value.isEmpty()) {
3129             try {
3130                 SimpleDateFormat parser = ExchangeSession.getExchangeDateFormat(value);
3131                 Calendar calendarValue = Calendar.getInstance(GMT_TIMEZONE);
3132                 calendarValue.setTime(parser.parse(value));
3133                 // zulu time: add 12 hours
3134                 if (value.length() == 16) {
3135                     calendarValue.add(Calendar.HOUR, 12);
3136                 }
3137                 calendarValue.set(Calendar.HOUR, 0);
3138                 calendarValue.set(Calendar.MINUTE, 0);
3139                 calendarValue.set(Calendar.SECOND, 0);
3140                 result = ExchangeSession.getExchangeZuluDateFormat().format(calendarValue.getTime());
3141             } catch (ParseException e) {
3142                 LOGGER.warn("Invalid date: " + value);
3143             }
3144         }
3145 
3146         return result;
3147     }
3148 
3149     /**
3150      * Format date to exchange search format.
3151      *
3152      * @param date date object
3153      * @return formatted search date
3154      */
3155     @Override
3156     public String formatSearchDate(Date date) {
3157         SimpleDateFormat dateFormatter = new SimpleDateFormat(YYYY_MM_DD_T_HHMMSS_Z, Locale.ENGLISH);
3158         dateFormatter.setTimeZone(GMT_TIMEZONE);
3159         return dateFormatter.format(date);
3160     }
3161 
3162     /**
3163      * Check if itemName is long and base64 encoded.
3164      * User generated item names are usually short
3165      *
3166      * @param itemName item name
3167      * @return true if itemName is an EWS item id
3168      */
3169     protected static boolean isItemId(String itemName) {
3170         return itemName.length() >= 140
3171                 // item name is base64url
3172                 && itemName.matches("^([A-Za-z0-9-_]{4})*([A-Za-z0-9-_]{4}|[A-Za-z0-9-_]{3}=|[A-Za-z0-9-_]{2}==)\\.EML$")
3173                 && itemName.indexOf(' ') < 0;
3174     }
3175 
3176     /**
3177      * Close session.
3178      * Shutdown http client connection manager
3179      */
3180     @Override
3181     public void close() {
3182         httpClient.close();
3183     }
3184 
3185 }
3186