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