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