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