View Javadoc
1   /*
2    * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
3    * Copyright (C) 2010  Mickael Guessant
4    *
5    * This program is free software; you can redistribute it and/or
6    * modify it under the terms of the GNU General Public License
7    * as published by the Free Software Foundation; either version 2
8    * of the License, or (at your option) any later version.
9    *
10   * This program is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU General Public License for more details.
14   *
15   * You should have received a copy of the GNU General Public License
16   * along with this program; if not, write to the Free Software
17   * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
18   */
19  package davmail.exchange.dav;
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.exception.HttpPreconditionFailedException;
27  import davmail.exception.InsufficientStorageException;
28  import davmail.exception.LoginTimeoutException;
29  import davmail.exception.WebdavNotAvailableException;
30  import davmail.exchange.ExchangeSession;
31  import davmail.exchange.VCalendar;
32  import davmail.exchange.VObject;
33  import davmail.exchange.VProperty;
34  import davmail.exchange.XMLStreamUtil;
35  import davmail.http.HttpClientAdapter;
36  import davmail.http.URIUtil;
37  import davmail.http.request.ExchangePropPatchRequest;
38  import davmail.ui.tray.DavGatewayTray;
39  import davmail.util.IOUtil;
40  import davmail.util.StringUtil;
41  import org.apache.http.Consts;
42  import org.apache.http.HttpResponse;
43  import org.apache.http.HttpStatus;
44  import org.apache.http.NameValuePair;
45  import org.apache.http.client.HttpResponseException;
46  import org.apache.http.client.entity.UrlEncodedFormEntity;
47  import org.apache.http.client.methods.CloseableHttpResponse;
48  import org.apache.http.client.methods.HttpDelete;
49  import org.apache.http.client.methods.HttpGet;
50  import org.apache.http.client.methods.HttpHead;
51  import org.apache.http.client.methods.HttpPost;
52  import org.apache.http.client.methods.HttpPut;
53  import org.apache.http.client.protocol.HttpClientContext;
54  import org.apache.http.client.utils.URIUtils;
55  import org.apache.http.entity.ByteArrayEntity;
56  import org.apache.http.entity.ContentType;
57  import org.apache.http.impl.client.BasicCookieStore;
58  import org.apache.http.impl.client.BasicResponseHandler;
59  import org.apache.http.message.BasicNameValuePair;
60  import org.apache.jackrabbit.webdav.DavException;
61  import org.apache.jackrabbit.webdav.MultiStatus;
62  import org.apache.jackrabbit.webdav.MultiStatusResponse;
63  import org.apache.jackrabbit.webdav.client.methods.HttpCopy;
64  import org.apache.jackrabbit.webdav.client.methods.HttpMove;
65  import org.apache.jackrabbit.webdav.client.methods.HttpPropfind;
66  import org.apache.jackrabbit.webdav.client.methods.HttpProppatch;
67  import org.apache.jackrabbit.webdav.property.DavProperty;
68  import org.apache.jackrabbit.webdav.property.DavPropertyNameSet;
69  import org.apache.jackrabbit.webdav.property.DavPropertySet;
70  import org.apache.jackrabbit.webdav.property.PropEntry;
71  import org.w3c.dom.Node;
72  
73  import javax.mail.MessagingException;
74  import javax.mail.Session;
75  import javax.mail.internet.InternetAddress;
76  import javax.mail.internet.MimeMessage;
77  import javax.mail.internet.MimeMultipart;
78  import javax.mail.internet.MimePart;
79  import javax.mail.util.SharedByteArrayInputStream;
80  import javax.xml.stream.XMLStreamException;
81  import javax.xml.stream.XMLStreamReader;
82  import java.io.BufferedReader;
83  import java.io.ByteArrayInputStream;
84  import java.io.ByteArrayOutputStream;
85  import java.io.FilterInputStream;
86  import java.io.IOException;
87  import java.io.InputStream;
88  import java.io.InputStreamReader;
89  import java.net.NoRouteToHostException;
90  import java.net.SocketException;
91  import java.net.URI;
92  import java.net.URISyntaxException;
93  import java.net.URL;
94  import java.net.URLStreamHandler;
95  import java.net.UnknownHostException;
96  import java.nio.charset.StandardCharsets;
97  import java.text.ParseException;
98  import java.text.SimpleDateFormat;
99  import java.util.*;
100 import java.util.zip.GZIPInputStream;
101 
102 /**
103  * Webdav Exchange adapter.
104  * Compatible with Exchange 2003 and 2007 with webdav available.
105  */
106 @SuppressWarnings("rawtypes")
107 public class DavExchangeSession extends ExchangeSession {
108     protected enum FolderQueryTraversal {
109         Shallow, Deep
110     }
111 
112     protected static final DavPropertyNameSet WELL_KNOWN_FOLDERS = new DavPropertyNameSet();
113 
114     static {
115         WELL_KNOWN_FOLDERS.add(Field.getPropertyName("inbox"));
116         WELL_KNOWN_FOLDERS.add(Field.getPropertyName("deleteditems"));
117         WELL_KNOWN_FOLDERS.add(Field.getPropertyName("sentitems"));
118         WELL_KNOWN_FOLDERS.add(Field.getPropertyName("sendmsg"));
119         WELL_KNOWN_FOLDERS.add(Field.getPropertyName("drafts"));
120         WELL_KNOWN_FOLDERS.add(Field.getPropertyName("calendar"));
121         WELL_KNOWN_FOLDERS.add(Field.getPropertyName("tasks"));
122         WELL_KNOWN_FOLDERS.add(Field.getPropertyName("contacts"));
123         WELL_KNOWN_FOLDERS.add(Field.getPropertyName("outbox"));
124     }
125 
126     static final Map<String, String> vTodoToTaskStatusMap = new HashMap<>();
127     static final Map<String, String> taskTovTodoStatusMap = new HashMap<>();
128 
129     static {
130         //taskTovTodoStatusMap.put("0", null);
131         taskTovTodoStatusMap.put("1", "IN-PROCESS");
132         taskTovTodoStatusMap.put("2", "COMPLETED");
133         taskTovTodoStatusMap.put("3", "NEEDS-ACTION");
134         taskTovTodoStatusMap.put("4", "CANCELLED");
135 
136         //vTodoToTaskStatusMap.put(null, "0");
137         vTodoToTaskStatusMap.put("IN-PROCESS", "1");
138         vTodoToTaskStatusMap.put("COMPLETED", "2");
139         vTodoToTaskStatusMap.put("NEEDS-ACTION", "3");
140         vTodoToTaskStatusMap.put("CANCELLED", "4");
141     }
142 
143     /**
144      * HttpClient 4 adapter to replace httpClient
145      */
146     private final HttpClientAdapter httpClientAdapter;
147 
148     /**
149      * Various standard mail boxes Urls
150      */
151     protected String inboxUrl;
152     protected String deleteditemsUrl;
153     protected String sentitemsUrl;
154     protected String sendmsgUrl;
155     protected String draftsUrl;
156     protected String calendarUrl;
157     protected String tasksUrl;
158     protected String contactsUrl;
159     protected String outboxUrl;
160 
161     protected String inboxName;
162     protected String deleteditemsName;
163     protected String sentitemsName;
164     protected String sendmsgName;
165     protected String draftsName;
166     protected String calendarName;
167     protected String tasksName;
168     protected String contactsName;
169     protected String outboxName;
170 
171     protected static final String USERS = "/users/";
172 
173     /**
174      * HttpClient4 conversion.
175      * TODO: move up to ExchangeSession
176      */
177     protected void getEmailAndAliasFromOptions() {
178         // get user mail URL from html body
179         HttpGet optionsMethod = new HttpGet("/owa/?ae=Options&t=About");
180         try (
181                 CloseableHttpResponse response = httpClientAdapter.execute(optionsMethod, cloneContext());
182                 InputStream inputStream = response.getEntity().getContent();
183                 BufferedReader optionsPageReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))
184         ) {
185             String line;
186 
187             // find email and alias
188             //noinspection StatementWithEmptyBody
189             while ((line = optionsPageReader.readLine()) != null
190                     && (line.indexOf('[') == -1
191                     || line.indexOf('@') == -1
192                     || line.indexOf(']') == -1
193                     || !line.toLowerCase().contains(MAILBOX_BASE))) {
194             }
195             if (line != null) {
196                 int start = line.toLowerCase().lastIndexOf(MAILBOX_BASE) + MAILBOX_BASE.length();
197                 int end = line.indexOf('<', start);
198                 alias = line.substring(start, end);
199                 end = line.lastIndexOf(']');
200                 start = line.lastIndexOf('[', end) + 1;
201                 email = line.substring(start, end);
202             }
203         } catch (IOException e) {
204             LOGGER.error("Error parsing options page at " + optionsMethod.getURI());
205         }
206     }
207 
208     /**
209      * Create a separate Http context to protect session cookies.
210      *
211      * @return HttpClientContext instance with cookies
212      */
213     private HttpClientContext cloneContext() {
214         // Create a local context to avoid cookie reset on error
215         BasicCookieStore cookieStore = new BasicCookieStore();
216         cookieStore.addCookies(httpClientAdapter.getCookies().toArray(new org.apache.http.cookie.Cookie[0]));
217         HttpClientContext context = HttpClientContext.create();
218         context.setCookieStore(cookieStore);
219         return context;
220     }
221 
222     @Override
223     public boolean isExpired() throws NoRouteToHostException, UnknownHostException {
224         // experimental: try to reset session timeout
225         if ("Exchange2007".equals(serverVersion)) {
226             HttpGet getMethod = new HttpGet("/owa/");
227             try (CloseableHttpResponse response = httpClientAdapter.execute(getMethod)) {
228                 LOGGER.debug(response.getStatusLine().getStatusCode() + " at /owa/");
229             } catch (IOException e) {
230                 LOGGER.warn(e.getMessage());
231             }
232         }
233 
234         return super.isExpired();
235     }
236 
237 
238     /**
239      * Convert logical or relative folder path to exchange folder path.
240      *
241      * @param folderPath folder name
242      * @return folder path
243      */
244     public String getFolderPath(String folderPath) {
245         String exchangeFolderPath;
246         // IMAP path
247         if (folderPath.startsWith(INBOX)) {
248             exchangeFolderPath = mailPath + inboxName + folderPath.substring(INBOX.length());
249         } else if (folderPath.startsWith(TRASH)) {
250             exchangeFolderPath = mailPath + deleteditemsName + folderPath.substring(TRASH.length());
251         } else if (folderPath.startsWith(DRAFTS)) {
252             exchangeFolderPath = mailPath + draftsName + folderPath.substring(DRAFTS.length());
253         } else if (folderPath.startsWith(SENT)) {
254             exchangeFolderPath = mailPath + sentitemsName + folderPath.substring(SENT.length());
255         } else if (folderPath.startsWith(SENDMSG)) {
256             exchangeFolderPath = mailPath + sendmsgName + folderPath.substring(SENDMSG.length());
257         } else if (folderPath.startsWith(CONTACTS)) {
258             exchangeFolderPath = mailPath + contactsName + folderPath.substring(CONTACTS.length());
259         } else if (folderPath.startsWith(CALENDAR)) {
260             exchangeFolderPath = mailPath + calendarName + folderPath.substring(CALENDAR.length());
261         } else if (folderPath.startsWith(TASKS)) {
262             exchangeFolderPath = mailPath + tasksName + folderPath.substring(TASKS.length());
263         } else if (folderPath.startsWith("public")) {
264             exchangeFolderPath = publicFolderUrl + folderPath.substring("public".length());
265 
266             // caldav path
267         } else if (folderPath.startsWith(USERS)) {
268             // get requested principal
269             String principal;
270             String localPath;
271             int principalIndex = folderPath.indexOf('/', USERS.length());
272             if (principalIndex >= 0) {
273                 principal = folderPath.substring(USERS.length(), principalIndex);
274                 localPath = folderPath.substring(USERS.length() + principal.length() + 1);
275                 if (localPath.startsWith(LOWER_CASE_INBOX) || localPath.startsWith(INBOX) || localPath.startsWith(MIXED_CASE_INBOX)) {
276                     localPath = inboxName + localPath.substring(LOWER_CASE_INBOX.length());
277                 } else if (localPath.startsWith(CALENDAR)) {
278                     localPath = calendarName + localPath.substring(CALENDAR.length());
279                 } else if (localPath.startsWith(TASKS)) {
280                     localPath = tasksName + localPath.substring(TASKS.length());
281                 } else if (localPath.startsWith(CONTACTS)) {
282                     localPath = contactsName + localPath.substring(CONTACTS.length());
283                 } else if (localPath.startsWith(ADDRESSBOOK)) {
284                     localPath = contactsName + localPath.substring(ADDRESSBOOK.length());
285                 }
286             } else {
287                 principal = folderPath.substring(USERS.length());
288                 localPath = "";
289             }
290             if (principal.isEmpty()) {
291                 exchangeFolderPath = rootPath;
292             } else if (alias.equalsIgnoreCase(principal) || (email != null && email.equalsIgnoreCase(principal))) {
293                 exchangeFolderPath = mailPath + localPath;
294             } else {
295                 LOGGER.debug("Detected shared path for principal " + principal + ", user principal is " + email);
296                 exchangeFolderPath = rootPath + principal + '/' + localPath;
297             }
298 
299             // absolute folder path
300         } else if (folderPath.startsWith("/")) {
301             exchangeFolderPath = folderPath;
302         } else {
303             exchangeFolderPath = mailPath + folderPath;
304         }
305         return exchangeFolderPath;
306     }
307 
308     /**
309      * Test if folderPath is inside user mailbox.
310      *
311      * @param folderPath absolute folder path
312      * @return true if folderPath is a public or shared folder
313      */
314     @Override
315     public boolean isSharedFolder(String folderPath) {
316         return !getFolderPath(folderPath).toLowerCase().startsWith(mailPath.toLowerCase());
317     }
318 
319     /**
320      * Test if folderPath is main calendar.
321      *
322      * @param folderPath absolute folder path
323      * @return true if folderPath is a public or shared folder
324      */
325     @Override
326     public boolean isMainCalendar(String folderPath) {
327         return getFolderPath(folderPath).equalsIgnoreCase(getFolderPath("calendar"));
328     }
329 
330     @Override
331     protected String getCalendarEmail(String folderPath) throws IOException {
332         String calendarPath = getFolderPath(folderPath);
333         String[] folderNames = calendarPath.split("/");
334         if (folderNames.length > 1) {
335             // assume folder email is second path element
336             return folderNames[1];
337         } else {
338             // failover to email
339             return email;
340         }
341     }
342 
343     /**
344      * Build base path for cmd commands (galfind, gallookup).
345      *
346      * @return cmd base path
347      */
348     public String getCmdBasePath() {
349         if (("Exchange2003".equals(serverVersion) || PUBLIC_ROOT.equals(publicFolderUrl)) && mailPath != null) {
350             // public folder is not available => try to use mailbox path
351             // Note: This does not work with freebusy, which requires /public/
352             return mailPath;
353         } else {
354             // use public folder url
355             return publicFolderUrl;
356         }
357     }
358 
359     /**
360      * LDAP to Exchange Criteria Map
361      */
362     static final HashMap<String, String> GALFIND_CRITERIA_MAP = new HashMap<>();
363 
364     static {
365         GALFIND_CRITERIA_MAP.put("imapUid", "AN");
366         GALFIND_CRITERIA_MAP.put("smtpemail1", "EM");
367         GALFIND_CRITERIA_MAP.put("cn", "DN");
368         GALFIND_CRITERIA_MAP.put("givenName", "FN");
369         GALFIND_CRITERIA_MAP.put("sn", "LN");
370         GALFIND_CRITERIA_MAP.put("title", "TL");
371         GALFIND_CRITERIA_MAP.put("o", "CP");
372         GALFIND_CRITERIA_MAP.put("l", "OF");
373         GALFIND_CRITERIA_MAP.put("department", "DP");
374     }
375 
376     static final HashSet<String> GALLOOKUP_ATTRIBUTES = new HashSet<>();
377 
378     static {
379         GALLOOKUP_ATTRIBUTES.add("givenName");
380         GALLOOKUP_ATTRIBUTES.add("initials");
381         GALLOOKUP_ATTRIBUTES.add("sn");
382         GALLOOKUP_ATTRIBUTES.add("street");
383         GALLOOKUP_ATTRIBUTES.add("st");
384         GALLOOKUP_ATTRIBUTES.add("postalcode");
385         GALLOOKUP_ATTRIBUTES.add("co");
386         GALLOOKUP_ATTRIBUTES.add("departement");
387         GALLOOKUP_ATTRIBUTES.add("mobile");
388     }
389 
390     /**
391      * Exchange to LDAP attribute map
392      */
393     static final HashMap<String, String> GALFIND_ATTRIBUTE_MAP = new HashMap<>();
394 
395     static {
396         GALFIND_ATTRIBUTE_MAP.put("uid", "AN");
397         GALFIND_ATTRIBUTE_MAP.put("smtpemail1", "EM");
398         GALFIND_ATTRIBUTE_MAP.put("cn", "DN");
399         GALFIND_ATTRIBUTE_MAP.put("displayName", "DN");
400         GALFIND_ATTRIBUTE_MAP.put("telephoneNumber", "PH");
401         GALFIND_ATTRIBUTE_MAP.put("l", "OFFICE");
402         GALFIND_ATTRIBUTE_MAP.put("o", "CP");
403         GALFIND_ATTRIBUTE_MAP.put("title", "TL");
404 
405         GALFIND_ATTRIBUTE_MAP.put("givenName", "first");
406         GALFIND_ATTRIBUTE_MAP.put("initials", "initials");
407         GALFIND_ATTRIBUTE_MAP.put("sn", "last");
408         GALFIND_ATTRIBUTE_MAP.put("street", "street");
409         GALFIND_ATTRIBUTE_MAP.put("st", "state");
410         GALFIND_ATTRIBUTE_MAP.put("postalcode", "zip");
411         GALFIND_ATTRIBUTE_MAP.put("co", "country");
412         GALFIND_ATTRIBUTE_MAP.put("department", "department");
413         GALFIND_ATTRIBUTE_MAP.put("mobile", "mobile");
414         GALFIND_ATTRIBUTE_MAP.put("roomnumber", "office");
415     }
416 
417     boolean disableGalFind;
418 
419     protected Map<String, Map<String, String>> galFind(String query) throws IOException {
420         Map<String, Map<String, String>> results;
421         String path = getCmdBasePath() + "?Cmd=galfind" + query;
422         HttpGet httpGet = new HttpGet(path);
423         try (CloseableHttpResponse response = httpClientAdapter.execute(httpGet)) {
424             results = XMLStreamUtil.getElementContentsAsMap(response.getEntity().getContent(), "item", "AN");
425             if (LOGGER.isDebugEnabled()) {
426                 LOGGER.debug(path + ": " + results.size() + " result(s)");
427             }
428         } catch (IOException e) {
429             LOGGER.debug("GET " + path + " failed: " + e + ' ' + e.getMessage());
430             disableGalFind = true;
431             throw e;
432         }
433         return results;
434     }
435 
436 
437     @Override
438     public Map<String, ExchangeSession.Contact> galFind(Condition condition, Set<String> returningAttributes, int sizeLimit) throws IOException {
439         Map<String, ExchangeSession.Contact> contacts = new HashMap<>();
440         //noinspection StatementWithEmptyBody
441         if (disableGalFind) {
442             // do nothing
443         } else if (condition instanceof MultiCondition) {
444             List<Condition> conditions = ((ExchangeSession.MultiCondition) condition).getConditions();
445             Operator operator = ((ExchangeSession.MultiCondition) condition).getOperator();
446             if (operator == Operator.Or) {
447                 for (Condition innerCondition : conditions) {
448                     contacts.putAll(galFind(innerCondition, returningAttributes, sizeLimit));
449                 }
450             } else if (operator == Operator.And && !conditions.isEmpty()) {
451                 Map<String, ExchangeSession.Contact> innerContacts = galFind(conditions.get(0), returningAttributes, sizeLimit);
452                 for (ExchangeSession.Contact contact : innerContacts.values()) {
453                     if (condition.isMatch(contact)) {
454                         contacts.put(contact.getName().toLowerCase(), contact);
455                     }
456                 }
457             }
458         } else if (condition instanceof AttributeCondition) {
459             String searchAttributeName = ((ExchangeSession.AttributeCondition) condition).getAttributeName();
460             String searchAttribute = GALFIND_CRITERIA_MAP.get(searchAttributeName);
461             if (searchAttribute != null) {
462                 String searchValue = ((ExchangeSession.AttributeCondition) condition).getValue();
463                 StringBuilder query = new StringBuilder();
464                 if ("EM".equals(searchAttribute)) {
465                     // mail search, split
466                     int atIndex = searchValue.indexOf('@');
467                     // remove suffix
468                     if (atIndex >= 0) {
469                         searchValue = searchValue.substring(0, atIndex);
470                     }
471                     // split firstname.lastname
472                     int dotIndex = searchValue.indexOf('.');
473                     if (dotIndex >= 0) {
474                         // assume mail starts with firstname
475                         query.append("&FN=").append(URIUtil.encodeWithinQuery(searchValue.substring(0, dotIndex)));
476                         query.append("&LN=").append(URIUtil.encodeWithinQuery(searchValue.substring(dotIndex + 1)));
477                     } else {
478                         query.append("&FN=").append(URIUtil.encodeWithinQuery(searchValue));
479                     }
480                 } else {
481                     query.append('&').append(searchAttribute).append('=').append(URIUtil.encodeWithinQuery(searchValue));
482                 }
483                 Map<String, Map<String, String>> results = galFind(query.toString());
484                 for (Map<String, String> result : results.values()) {
485                     Contact contact = new Contact();
486                     contact.setName(result.get("AN"));
487                     contact.put("imapUid", result.get("AN"));
488                     buildGalfindContact(contact, result);
489                     if (needGalLookup(searchAttributeName, returningAttributes)) {
490                         galLookup(contact);
491                         // iCal fix to suit both iCal 3 and 4:  move cn to sn, remove cn
492                     } else if (returningAttributes.contains("apple-serviceslocator")) {
493                         if (contact.get("cn") != null && returningAttributes.contains("sn")) {
494                             contact.put("sn", contact.get("cn"));
495                             contact.remove("cn");
496                         }
497                     }
498                     if (condition.isMatch(contact)) {
499                         contacts.put(contact.getName().toLowerCase(), contact);
500                     }
501                 }
502             }
503 
504         }
505         return contacts;
506     }
507 
508     protected boolean needGalLookup(String searchAttributeName, Set<String> returningAttributes) {
509         // return all attributes => call gallookup
510         if (returningAttributes == null || returningAttributes.isEmpty()) {
511             return true;
512             // iCal search, do not call gallookup
513         } else if (returningAttributes.contains("apple-serviceslocator")) {
514             return false;
515             // Lightning search, no need to gallookup
516         } else if ("sn".equals(searchAttributeName)) {
517             return returningAttributes.contains("sn");
518             // search attribute is gallookup attribute, need to fetch value for isMatch
519         } else if (GALLOOKUP_ATTRIBUTES.contains(searchAttributeName)) {
520             return true;
521         }
522 
523         for (String attributeName : GALLOOKUP_ATTRIBUTES) {
524             if (returningAttributes.contains(attributeName)) {
525                 return true;
526             }
527         }
528         return false;
529     }
530 
531     private boolean disableGalLookup;
532 
533     /**
534      * Get extended address book information for person with gallookup.
535      * Does not work with Exchange 2007
536      *
537      * @param contact galfind contact
538      */
539     public void galLookup(Contact contact) {
540         if (!disableGalLookup) {
541             LOGGER.debug("galLookup(" + contact.get("smtpemail1") + ')');
542             HttpGet httpGet = new HttpGet(URIUtil.encodePathQuery(getCmdBasePath() + "?Cmd=gallookup&ADDR=" + contact.get("smtpemail1")));
543             try (CloseableHttpResponse response = httpClientAdapter.execute(httpGet)) {
544                 Map<String, Map<String, String>> results = XMLStreamUtil.getElementContentsAsMap(response.getEntity().getContent(), "person", "alias");
545                 // add detailed information
546                 if (!results.isEmpty()) {
547                     Map<String, String> personGalLookupDetails = results.get(contact.get("uid").toLowerCase());
548                     if (personGalLookupDetails != null) {
549                         buildGalfindContact(contact, personGalLookupDetails);
550                     }
551                 }
552             } catch (IOException e) {
553                 LOGGER.warn("Unable to gallookup person: " + contact + ", disable GalLookup");
554                 disableGalLookup = true;
555             }
556         }
557     }
558 
559     protected void buildGalfindContact(Contact contact, Map<String, String> response) {
560         for (Map.Entry<String, String> entry : GALFIND_ATTRIBUTE_MAP.entrySet()) {
561             String attributeValue = response.get(entry.getValue());
562             if (attributeValue != null) {
563                 contact.put(entry.getKey(), attributeValue);
564             }
565         }
566     }
567 
568     @Override
569     protected String getFreeBusyData(String attendee, String start, String end, int interval) throws IOException {
570         String freebusyUrl = publicFolderUrl + "/?cmd=freebusy" +
571                 "&start=" + start +
572                 "&end=" + end +
573                 "&interval=" + interval +
574                 "&u=SMTP:" + attendee;
575         HttpGet httpGet = new HttpGet(freebusyUrl);
576         httpGet.setHeader("Content-Type", "text/xml");
577         String fbdata;
578         try (CloseableHttpResponse response = httpClientAdapter.execute(httpGet)) {
579             fbdata = StringUtil.getLastToken(new BasicResponseHandler().handleResponse(response), "<a:fbdata>", "</a:fbdata>");
580         }
581         return fbdata;
582     }
583 
584     public DavExchangeSession(HttpClientAdapter httpClientAdapter, java.net.URI uri, String userName) throws IOException {
585         this.httpClientAdapter = httpClientAdapter;
586         this.userName = userName;
587         buildSessionInfo(uri);
588     }
589 
590 
591     @Override
592     public void buildSessionInfo(java.net.URI uri) throws DavMailException {
593         buildMailPath(uri);
594 
595         // get base http mailbox http urls
596         getWellKnownFolders();
597     }
598 
599     static final String BASE_HREF = "<base href=\"";
600 
601     /**
602      * Exchange 2003: get mailPath from welcome page
603      *
604      * @param uri current uri
605      * @return mail path from body
606      */
607     protected String getMailpathFromWelcomePage(java.net.URI uri) {
608         String welcomePageMailPath = null;
609         // get user mail URL from html body (multi frame)
610         HttpGet method = new HttpGet(uri.toString());
611 
612         try (
613                 CloseableHttpResponse response = httpClientAdapter.execute(method);
614                 InputStream inputStream = response.getEntity().getContent();
615                 BufferedReader mainPageReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))
616         ) {
617             String line;
618             //noinspection StatementWithEmptyBody
619             while ((line = mainPageReader.readLine()) != null && !line.toLowerCase().contains(BASE_HREF)) {
620             }
621             if (line != null) {
622                 // Exchange 2003
623                 int start = line.toLowerCase().indexOf(BASE_HREF) + BASE_HREF.length();
624                 int end = line.indexOf('\"', start);
625                 String mailBoxBaseHref = line.substring(start, end);
626                 URL baseURL = URI.create(mailBoxBaseHref).toURL();
627                 welcomePageMailPath = URIUtil.decode(baseURL.getPath());
628                 LOGGER.debug("Base href found in body, mailPath is " + welcomePageMailPath);
629             }
630         } catch (IOException e) {
631             LOGGER.error("Error parsing main page at " + method.getURI(), e);
632         }
633         return welcomePageMailPath;
634     }
635 
636     protected void buildMailPath(java.net.URI uri) throws DavMailAuthenticationException {
637         // get mailPath from welcome page on Exchange 2003
638         mailPath = getMailpathFromWelcomePage(uri);
639 
640         //noinspection VariableNotUsedInsideIf
641         if (mailPath != null) {
642             // Exchange 2003
643             serverVersion = "Exchange2003";
644             fixClientHost(uri);
645             checkPublicFolder();
646             buildEmail(uri.getHost());
647         } else {
648             // Exchange 2007 : get alias and email from options page
649             serverVersion = "Exchange2007";
650 
651             // Gallookup is an Exchange 2003 only feature
652             disableGalLookup = true;
653             fixClientHost(uri);
654             getEmailAndAliasFromOptions();
655 
656             checkPublicFolder();
657 
658             // failover: try to get email through Webdav and Galfind
659             if (alias == null || email == null) {
660                 buildEmail(uri.getHost());
661             }
662 
663             // build standard mailbox link with email
664             mailPath = "/exchange/" + email + '/';
665         }
666 
667         if (mailPath == null || email == null) {
668             throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED_PASSWORD_EXPIRED");
669         }
670         LOGGER.debug("Current user email is " + email + ", alias is " + alias + ", mailPath is " + mailPath + " on " + serverVersion);
671         rootPath = mailPath.substring(0, mailPath.lastIndexOf('/', mailPath.length() - 2) + 1);
672     }
673 
674     /**
675      * Determine user email through various means.
676      *
677      * @param hostName Exchange server host name for last failover
678      */
679     public void buildEmail(String hostName) {
680         String mailBoxPath = getMailboxPath();
681         // mailPath contains either alias or email
682         if (mailBoxPath != null && mailBoxPath.indexOf('@') >= 0) {
683             email = mailBoxPath;
684             alias = getAliasFromMailboxDisplayName();
685             if (alias == null) {
686                 alias = getAliasFromLogin();
687             }
688         } else {
689             // use mailbox name as alias
690             alias = mailBoxPath;
691             email = getEmail(alias);
692             if (email == null) {
693                 // failover: try to get email from login name
694                 alias = getAliasFromLogin();
695                 email = getEmail(alias);
696             }
697             // another failover : get alias from mailPath display name
698             if (email == null) {
699                 alias = getAliasFromMailboxDisplayName();
700                 email = getEmail(alias);
701             }
702             if (email == null) {
703                 LOGGER.debug("Unable to get user email with alias " + mailBoxPath
704                         + " or " + getAliasFromLogin()
705                         + " or " + alias
706                 );
707                 // last failover: build email from domain name and mailbox display name
708                 StringBuilder buffer = new StringBuilder();
709                 // most reliable alias
710                 if (mailBoxPath != null) {
711                     alias = mailBoxPath;
712                 } else {
713                     alias = getAliasFromLogin();
714                 }
715                 if (alias == null) {
716                     alias = "unknown";
717                 }
718                 buffer.append(alias);
719                 if (alias.indexOf('@') < 0) {
720                     buffer.append('@');
721                     if (hostName == null) {
722                         hostName = "mail.unknown.com";
723                     }
724                     int dotIndex = hostName.indexOf('.');
725                     if (dotIndex >= 0) {
726                         buffer.append(hostName.substring(dotIndex + 1));
727                     }
728                 }
729                 email = buffer.toString();
730             }
731         }
732     }
733 
734     /**
735      * Get user alias from mailbox display name over Webdav.
736      *
737      * @return user alias
738      */
739     public String getAliasFromMailboxDisplayName() {
740         if (mailPath == null) {
741             return null;
742         }
743         String displayName = null;
744         try {
745             Folder rootFolder = getFolder("");
746             if (rootFolder == null) {
747                 LOGGER.warn(new BundleMessage("EXCEPTION_UNABLE_TO_GET_MAIL_FOLDER", mailPath));
748             } else {
749                 displayName = rootFolder.displayName;
750             }
751         } catch (IOException e) {
752             LOGGER.warn(new BundleMessage("EXCEPTION_UNABLE_TO_GET_MAIL_FOLDER", mailPath));
753         }
754         return displayName;
755     }
756 
757     /**
758      * Get current Exchange alias name from mailbox name
759      *
760      * @return user name
761      */
762     protected String getMailboxPath() {
763         if (mailPath == null) {
764             return null;
765         }
766         int index = mailPath.lastIndexOf('/', mailPath.length() - 2);
767         if (index >= 0 && mailPath.endsWith("/")) {
768             return mailPath.substring(index + 1, mailPath.length() - 1);
769         } else {
770             LOGGER.warn(new BundleMessage("EXCEPTION_INVALID_MAIL_PATH", mailPath));
771             return null;
772         }
773     }
774 
775     /**
776      * Get user email from global address list (galfind).
777      *
778      * @param alias user alias
779      * @return user email
780      */
781     public String getEmail(String alias) {
782         String emailResult = null;
783         if (alias != null && !disableGalFind) {
784             try {
785                 Map<String, Map<String, String>> results = galFind("&AN=" + URIUtil.encodeWithinQuery(alias));
786                 Map<String, String> result = results.get(alias.toLowerCase());
787                 if (result != null) {
788                     emailResult = result.get("EM");
789                 }
790             } catch (IOException e) {
791                 // galfind not available
792                 disableGalFind = true;
793                 LOGGER.debug("getEmail(" + alias + ") failed");
794             }
795         }
796         return emailResult;
797     }
798 
799     protected String getURIPropertyIfExists(DavPropertySet properties, String alias) throws IOException {
800         DavProperty property = properties.get(Field.getPropertyName(alias));
801         if (property == null) {
802             return null;
803         } else {
804             return URIUtil.decode((String) property.getValue());
805         }
806     }
807 
808     // return last folder name from url
809 
810     protected String getFolderName(String url) {
811         if (url != null) {
812             if (url.endsWith("/")) {
813                 return url.substring(url.lastIndexOf('/', url.length() - 2) + 1, url.length() - 1);
814             } else if (url.indexOf('/') > 0) {
815                 return url.substring(url.lastIndexOf('/') + 1);
816             } else {
817                 return null;
818             }
819         } else {
820             return null;
821         }
822     }
823 
824     protected void fixClientHost(java.net.URI currentUri) {
825         // update client host, workaround for Exchange 2003 mailbox with an Exchange 2007 frontend
826         if (currentUri != null && currentUri.getHost() != null && currentUri.getScheme() != null) {
827             httpClientAdapter.setUri(currentUri);
828         }
829     }
830 
831     protected void checkPublicFolder() {
832         // check public folder access
833         try {
834             publicFolderUrl = URIUtils.resolve(httpClientAdapter.getUri(), PUBLIC_ROOT).toString();
835             DavPropertyNameSet davPropertyNameSet = new DavPropertyNameSet();
836             davPropertyNameSet.add(Field.getPropertyName("displayname"));
837 
838             HttpPropfind httpPropfind = new HttpPropfind(publicFolderUrl, davPropertyNameSet, 0);
839             httpClientAdapter.executeDavRequest(httpPropfind);
840             // update public folder URI
841             publicFolderUrl = httpPropfind.getURI().toString();
842 
843         } catch (IOException e) {
844             LOGGER.warn("Public folders not available: " + (e.getMessage() == null ? e : e.getMessage()));
845             // default public folder path
846             publicFolderUrl = PUBLIC_ROOT;
847         }
848     }
849 
850 
851     protected void getWellKnownFolders() throws DavMailException {
852         // Retrieve well known URLs
853         try {
854             HttpPropfind httpPropfind = new HttpPropfind(mailPath, WELL_KNOWN_FOLDERS, 0);
855             MultiStatus multiStatus;
856             try (CloseableHttpResponse response = httpClientAdapter.execute(httpPropfind)) {
857                 multiStatus = httpPropfind.getResponseBodyAsMultiStatus(response);
858             }
859             MultiStatusResponse[] responses = multiStatus.getResponses();
860             if (responses.length == 0) {
861                 throw new WebdavNotAvailableException("EXCEPTION_UNABLE_TO_GET_MAIL_FOLDER", mailPath);
862             }
863             DavPropertySet properties = responses[0].getProperties(org.apache.http.HttpStatus.SC_OK);
864             inboxUrl = getURIPropertyIfExists(properties, "inbox");
865             inboxName = getFolderName(inboxUrl);
866             deleteditemsUrl = getURIPropertyIfExists(properties, "deleteditems");
867             deleteditemsName = getFolderName(deleteditemsUrl);
868             sentitemsUrl = getURIPropertyIfExists(properties, "sentitems");
869             sentitemsName = getFolderName(sentitemsUrl);
870             sendmsgUrl = getURIPropertyIfExists(properties, "sendmsg");
871             sendmsgName = getFolderName(sendmsgUrl);
872             draftsUrl = getURIPropertyIfExists(properties, "drafts");
873             draftsName = getFolderName(draftsUrl);
874             calendarUrl = getURIPropertyIfExists(properties, "calendar");
875             calendarName = getFolderName(calendarUrl);
876             tasksUrl = getURIPropertyIfExists(properties, "tasks");
877             tasksName = getFolderName(tasksUrl);
878             contactsUrl = getURIPropertyIfExists(properties, "contacts");
879             contactsName = getFolderName(contactsUrl);
880             outboxUrl = getURIPropertyIfExists(properties, "outbox");
881             outboxName = getFolderName(outboxUrl);
882             // junk folder not available over webdav
883 
884             LOGGER.debug("Inbox URL: " + inboxUrl +
885                     " Trash URL: " + deleteditemsUrl +
886                     " Sent URL: " + sentitemsUrl +
887                     " Send URL: " + sendmsgUrl +
888                     " Drafts URL: " + draftsUrl +
889                     " Calendar URL: " + calendarUrl +
890                     " Tasks URL: " + tasksUrl +
891                     " Contacts URL: " + contactsUrl +
892                     " Outbox URL: " + outboxUrl +
893                     " Public folder URL: " + publicFolderUrl
894             );
895         } catch (IOException | DavException e) {
896             LOGGER.error(e.getMessage());
897             throw new WebdavNotAvailableException("EXCEPTION_UNABLE_TO_GET_MAIL_FOLDER", mailPath);
898         }
899     }
900 
901     protected static class MultiCondition extends ExchangeSession.MultiCondition {
902         protected MultiCondition(Operator operator, Condition... condition) {
903             super(operator, condition);
904         }
905 
906         public void appendTo(StringBuilder buffer) {
907             boolean first = true;
908 
909             for (Condition condition : conditions) {
910                 if (condition != null && !condition.isEmpty()) {
911                     if (first) {
912                         buffer.append('(');
913                         first = false;
914                     } else {
915                         buffer.append(' ').append(operator).append(' ');
916                     }
917                     condition.appendTo(buffer);
918                 }
919             }
920             // at least one non empty condition
921             if (!first) {
922                 buffer.append(')');
923             }
924         }
925     }
926 
927     protected static class NotCondition extends ExchangeSession.NotCondition {
928         protected NotCondition(Condition condition) {
929             super(condition);
930         }
931 
932         public void appendTo(StringBuilder buffer) {
933             buffer.append("(Not ");
934             condition.appendTo(buffer);
935             buffer.append(')');
936         }
937     }
938 
939     static final Map<Operator, String> OPERATOR_MAP = new HashMap<>();
940 
941     static {
942         OPERATOR_MAP.put(Operator.IsEqualTo, " = ");
943         OPERATOR_MAP.put(Operator.IsGreaterThanOrEqualTo, " >= ");
944         OPERATOR_MAP.put(Operator.IsGreaterThan, " > ");
945         OPERATOR_MAP.put(Operator.IsLessThanOrEqualTo, " <= ");
946         OPERATOR_MAP.put(Operator.IsLessThan, " < ");
947         OPERATOR_MAP.put(Operator.Like, " like ");
948         OPERATOR_MAP.put(Operator.IsNull, " is null");
949         OPERATOR_MAP.put(Operator.IsFalse, " = false");
950         OPERATOR_MAP.put(Operator.IsTrue, " = true");
951         OPERATOR_MAP.put(Operator.StartsWith, " = ");
952         OPERATOR_MAP.put(Operator.Contains, " = ");
953     }
954 
955     protected static class AttributeCondition extends ExchangeSession.AttributeCondition {
956         protected boolean isIntValue;
957 
958         protected AttributeCondition(String attributeName, Operator operator, String value) {
959             super(attributeName, operator, value);
960         }
961 
962         protected AttributeCondition(String attributeName, Operator operator, int value) {
963             super(attributeName, operator, String.valueOf(value));
964             isIntValue = true;
965         }
966 
967         public void appendTo(StringBuilder buffer) {
968             Field field = Field.get(attributeName);
969             buffer.append('"').append(field.getUri()).append('"');
970             buffer.append(OPERATOR_MAP.get(operator));
971             //noinspection VariableNotUsedInsideIf
972             if (field.cast != null) {
973                 buffer.append("CAST (\"");
974             } else if (!isIntValue && !field.isIntValue()) {
975                 buffer.append('\'');
976             }
977             if (Operator.Like == operator) {
978                 buffer.append('%');
979             }
980             if ("urlcompname".equals(field.alias)) {
981                 buffer.append(StringUtil.encodeUrlcompname(StringUtil.davSearchEncode(value)));
982             } else if (field.isIntValue()) {
983                 // check value
984                 try {
985                     Integer.parseInt(value);
986                     buffer.append(value);
987                 } catch (NumberFormatException e) {
988                     // invalid value, replace with 0
989                     buffer.append('0');
990                 }
991             } else {
992                 buffer.append(StringUtil.davSearchEncode(value));
993             }
994             if (Operator.Like == operator || Operator.StartsWith == operator) {
995                 buffer.append('%');
996             }
997             if (field.cast != null) {
998                 buffer.append("\" as '").append(field.cast).append("')");
999             } else if (!isIntValue && !field.isIntValue()) {
1000                 buffer.append('\'');
1001             }
1002         }
1003 
1004         public boolean isMatch(ExchangeSession.Contact contact) {
1005             String lowerCaseValue = value.toLowerCase();
1006             String actualValue = contact.get(attributeName);
1007             Operator actualOperator = operator;
1008             // patch for iCal or Lightning search without galLookup
1009             if (actualValue == null && ("givenName".equals(attributeName) || "sn".equals(attributeName))) {
1010                 actualValue = contact.get("cn");
1011                 actualOperator = Operator.Like;
1012             }
1013             if (actualValue == null) {
1014                 return false;
1015             }
1016             actualValue = actualValue.toLowerCase();
1017             return (actualOperator == Operator.IsEqualTo && actualValue.equals(lowerCaseValue)) ||
1018                     (actualOperator == Operator.Like && actualValue.contains(lowerCaseValue)) ||
1019                     (actualOperator == Operator.StartsWith && actualValue.startsWith(lowerCaseValue));
1020         }
1021     }
1022 
1023     protected static class HeaderCondition extends AttributeCondition {
1024 
1025         protected HeaderCondition(String attributeName, Operator operator, String value) {
1026             super(attributeName, operator, value);
1027         }
1028 
1029         @Override
1030         public void appendTo(StringBuilder buffer) {
1031             buffer.append('"').append(Field.getHeader(attributeName).getUri()).append('"');
1032             buffer.append(OPERATOR_MAP.get(operator));
1033             buffer.append('\'');
1034             if (Operator.Like == operator) {
1035                 buffer.append('%');
1036             }
1037             buffer.append(value);
1038             if (Operator.Like == operator) {
1039                 buffer.append('%');
1040             }
1041             buffer.append('\'');
1042         }
1043     }
1044 
1045     protected static class MonoCondition extends ExchangeSession.MonoCondition {
1046         protected MonoCondition(String attributeName, Operator operator) {
1047             super(attributeName, operator);
1048         }
1049 
1050         public void appendTo(StringBuilder buffer) {
1051             buffer.append('"').append(Field.get(attributeName).getUri()).append('"');
1052             buffer.append(OPERATOR_MAP.get(operator));
1053         }
1054     }
1055 
1056     @Override
1057     public ExchangeSession.MultiCondition and(Condition... condition) {
1058         return new MultiCondition(Operator.And, condition);
1059     }
1060 
1061     @Override
1062     public ExchangeSession.MultiCondition or(Condition... condition) {
1063         return new MultiCondition(Operator.Or, condition);
1064     }
1065 
1066     @Override
1067     public Condition not(Condition condition) {
1068         if (condition == null) {
1069             return null;
1070         } else {
1071             return new NotCondition(condition);
1072         }
1073     }
1074 
1075     @Override
1076     public Condition isEqualTo(String attributeName, String value) {
1077         return new AttributeCondition(attributeName, Operator.IsEqualTo, value);
1078     }
1079 
1080     @Override
1081     public Condition isEqualTo(String attributeName, int value) {
1082         return new AttributeCondition(attributeName, Operator.IsEqualTo, value);
1083     }
1084 
1085     @Override
1086     public Condition headerIsEqualTo(String headerName, String value) {
1087         return new HeaderCondition(headerName, Operator.IsEqualTo, value);
1088     }
1089 
1090     @Override
1091     public Condition gte(String attributeName, String value) {
1092         return new AttributeCondition(attributeName, Operator.IsGreaterThanOrEqualTo, value);
1093     }
1094 
1095     @Override
1096     public Condition lte(String attributeName, String value) {
1097         return new AttributeCondition(attributeName, Operator.IsLessThanOrEqualTo, value);
1098     }
1099 
1100     @Override
1101     public Condition lt(String attributeName, String value) {
1102         return new AttributeCondition(attributeName, Operator.IsLessThan, value);
1103     }
1104 
1105     @Override
1106     public Condition gt(String attributeName, String value) {
1107         return new AttributeCondition(attributeName, Operator.IsGreaterThan, value);
1108     }
1109 
1110     @Override
1111     public Condition contains(String attributeName, String value) {
1112         return new AttributeCondition(attributeName, Operator.Like, value);
1113     }
1114 
1115     @Override
1116     public Condition startsWith(String attributeName, String value) {
1117         return new AttributeCondition(attributeName, Operator.StartsWith, value);
1118     }
1119 
1120     @Override
1121     public Condition isNull(String attributeName) {
1122         return new MonoCondition(attributeName, Operator.IsNull);
1123     }
1124 
1125     @Override
1126     public Condition exists(String attributeName) {
1127         return not(new MonoCondition(attributeName, Operator.IsNull));
1128     }
1129 
1130     @Override
1131     public Condition isTrue(String attributeName) {
1132         if ("Exchange2003".equals(this.serverVersion) && "deleted".equals(attributeName)) {
1133             return isEqualTo(attributeName, "1");
1134         } else {
1135             return new MonoCondition(attributeName, Operator.IsTrue);
1136         }
1137     }
1138 
1139     @Override
1140     public Condition isFalse(String attributeName) {
1141         if ("Exchange2003".equals(this.serverVersion) && "deleted".equals(attributeName)) {
1142             return or(isEqualTo(attributeName, "0"), isNull(attributeName));
1143         } else {
1144             return new MonoCondition(attributeName, Operator.IsFalse);
1145         }
1146     }
1147 
1148     /**
1149      * @inheritDoc
1150      */
1151     public class Message extends ExchangeSession.Message {
1152 
1153         @Override
1154         public String getPermanentId() {
1155             return permanentUrl;
1156         }
1157 
1158         @Override
1159         protected InputStream getMimeHeaders() {
1160             InputStream result = null;
1161             try {
1162                 String messageHeaders = getItemProperty(permanentUrl, "messageheaders");
1163                 if (messageHeaders != null) {
1164                     final String MS_HEADER = "Microsoft Mail Internet Headers Version 2.0";
1165                     if (messageHeaders.startsWith(MS_HEADER)) {
1166                         messageHeaders = messageHeaders.substring(MS_HEADER.length());
1167                         if (!messageHeaders.isEmpty() && messageHeaders.charAt(0) == '\r') {
1168                             messageHeaders = messageHeaders.substring(1);
1169                         }
1170                         if (!messageHeaders.isEmpty() && messageHeaders.charAt(0) == '\n') {
1171                             messageHeaders = messageHeaders.substring(1);
1172                         }
1173                     }
1174                     // workaround for messages in Sent folder
1175                     if (!messageHeaders.contains("From:")) {
1176                         String from = getItemProperty(permanentUrl, "from");
1177                         messageHeaders = "From: " + from + '\n' + messageHeaders;
1178                     }
1179                     result = new ByteArrayInputStream(messageHeaders.getBytes(StandardCharsets.UTF_8));
1180                 }
1181             } catch (Exception e) {
1182                 LOGGER.warn(e.getMessage());
1183             }
1184 
1185             return result;
1186         }
1187     }
1188 
1189 
1190     /**
1191      * @inheritDoc
1192      */
1193     public class Contact extends ExchangeSession.Contact {
1194         /**
1195          * Build Contact instance from multistatusResponse info
1196          *
1197          * @param multiStatusResponse response
1198          * @throws IOException      on error
1199          * @throws DavMailException on error
1200          */
1201         public Contact(MultiStatusResponse multiStatusResponse) throws IOException, DavMailException {
1202             setHref(URIUtil.decode(multiStatusResponse.getHref()));
1203             DavPropertySet properties = multiStatusResponse.getProperties(HttpStatus.SC_OK);
1204             permanentUrl = getURLPropertyIfExists(properties, "permanenturl");
1205             etag = getPropertyIfExists(properties, "etag");
1206             displayName = getPropertyIfExists(properties, "displayname");
1207             for (String attributeName : CONTACT_ATTRIBUTES) {
1208                 String value = getPropertyIfExists(properties, attributeName);
1209                 if (value != null) {
1210                     if ("bday".equals(attributeName) || "anniversary".equals(attributeName)
1211                             || "lastmodified".equals(attributeName) || "datereceived".equals(attributeName)) {
1212                         value = convertDateFromExchange(value);
1213                     } else if ("haspicture".equals(attributeName) || "private".equals(attributeName)) {
1214                         value = "1".equals(value) ? "true" : "false";
1215                     }
1216                     put(attributeName, value);
1217                 }
1218             }
1219         }
1220 
1221         public Contact(String folderPath, String itemName, Map<String, String> properties, String etag, String noneMatch) {
1222             super(folderPath, itemName, properties, etag, noneMatch);
1223         }
1224 
1225         /**
1226          * Default constructor for galFind
1227          */
1228         public Contact() {
1229         }
1230 
1231         protected Set<PropertyValue> buildProperties() {
1232             Set<PropertyValue> propertyValues = new HashSet<>();
1233             for (Map.Entry<String, String> entry : entrySet()) {
1234                 String key = entry.getKey();
1235                 if (!"photo".equals(key)) {
1236                     propertyValues.add(Field.createPropertyValue(key, entry.getValue()));
1237                     if (key.startsWith("email")) {
1238                         propertyValues.add(Field.createPropertyValue(key + "type", "SMTP"));
1239                     }
1240                 }
1241             }
1242 
1243             return propertyValues;
1244         }
1245 
1246         protected ExchangePropPatchRequest internalCreateOrUpdate(String encodedHref) throws IOException {
1247             ExchangePropPatchRequest propPatchRequest = new ExchangePropPatchRequest(encodedHref, buildProperties());
1248             propPatchRequest.setHeader("Translate", "f");
1249             if (etag != null) {
1250                 propPatchRequest.setHeader("If-Match", etag);
1251             }
1252             if (noneMatch != null) {
1253                 propPatchRequest.setHeader("If-None-Match", noneMatch);
1254             }
1255             try (CloseableHttpResponse response = httpClientAdapter.execute(propPatchRequest)) {
1256                 LOGGER.debug("internalCreateOrUpdate returned " + response.getStatusLine().getStatusCode() + " " + response.getStatusLine().getReasonPhrase());
1257             }
1258             return propPatchRequest;
1259         }
1260 
1261         /**
1262          * Create or update contact
1263          *
1264          * @return action result
1265          * @throws IOException on error
1266          */
1267         @Override
1268         public ItemResult createOrUpdate() throws IOException {
1269             String encodedHref = URIUtil.encodePath(getHref());
1270             ExchangePropPatchRequest propPatchRequest = internalCreateOrUpdate(encodedHref);
1271             int status = propPatchRequest.getStatusLine().getStatusCode();
1272             if (status == HttpStatus.SC_MULTI_STATUS) {
1273                 try {
1274                     status = propPatchRequest.getResponseStatusCode();
1275                 } catch (HttpResponseException e) {
1276                     throw new IOException(e.getMessage(), e);
1277                 }
1278                 //noinspection VariableNotUsedInsideIf
1279                 if (status == HttpStatus.SC_CREATED) {
1280                     LOGGER.debug("Created contact " + encodedHref);
1281                 } else {
1282                     LOGGER.debug("Updated contact " + encodedHref);
1283                 }
1284             } else if (status == HttpStatus.SC_NOT_FOUND) {
1285                 LOGGER.debug("Contact not found at " + encodedHref + ", searching permanenturl by urlcompname");
1286                 // failover, search item by urlcompname
1287                 MultiStatusResponse[] responses = searchItems(folderPath, EVENT_REQUEST_PROPERTIES, DavExchangeSession.this.isEqualTo("urlcompname", convertItemNameToEML(itemName)), FolderQueryTraversal.Shallow, 1);
1288                 if (responses.length == 1) {
1289                     encodedHref = getPropertyIfExists(responses[0].getProperties(HttpStatus.SC_OK), "permanenturl");
1290                     LOGGER.warn("Contact found, permanenturl is " + encodedHref);
1291                     propPatchRequest = internalCreateOrUpdate(encodedHref);
1292                     status = propPatchRequest.getStatusLine().getStatusCode();
1293                     if (status == HttpStatus.SC_MULTI_STATUS) {
1294                         try {
1295                             status = propPatchRequest.getResponseStatusCode();
1296                         } catch (HttpResponseException e) {
1297                             throw new IOException(e.getMessage(), e);
1298                         }
1299                         LOGGER.debug("Updated contact " + encodedHref);
1300                     } else {
1301                         LOGGER.warn("Unable to create or update contact " + status + ' ' + propPatchRequest.getStatusLine());
1302                     }
1303                 }
1304 
1305             } else {
1306                 LOGGER.warn("Unable to create or update contact " + status + ' ' + propPatchRequest.getStatusLine().getReasonPhrase());
1307             }
1308             ItemResult itemResult = new ItemResult();
1309             // 440 means forbidden on Exchange
1310             if (status == 440) {
1311                 status = HttpStatus.SC_FORBIDDEN;
1312             }
1313             itemResult.status = status;
1314 
1315             if (status == HttpStatus.SC_OK || status == HttpStatus.SC_CREATED) {
1316                 String contactPictureUrl = URIUtil.encodePath(getHref() + "/ContactPicture.jpg");
1317                 String photo = get("photo");
1318                 if (photo != null) {
1319                     try {
1320                         final HttpPut httpPut = new HttpPut(contactPictureUrl);
1321                         // need to update photo
1322                         byte[] resizedImageBytes = IOUtil.resizeImage(IOUtil.decodeBase64(photo), 90);
1323 
1324                         httpPut.setHeader("Overwrite", "t");
1325                         // TODO: required ?
1326                         httpPut.setHeader("Content-Type", "image/jpeg");
1327                         httpPut.setEntity(new ByteArrayEntity(resizedImageBytes, ContentType.IMAGE_JPEG));
1328 
1329                         try (CloseableHttpResponse response = httpClientAdapter.execute(httpPut)) {
1330                             status = response.getStatusLine().getStatusCode();
1331                             if (status != HttpStatus.SC_OK && status != HttpStatus.SC_CREATED) {
1332                                 throw new IOException("Unable to update contact picture: " + status + ' ' + response.getStatusLine().getReasonPhrase());
1333                             }
1334                         }
1335                     } catch (IOException e) {
1336                         LOGGER.error("Error in contact photo create or update", e);
1337                         throw e;
1338                     }
1339 
1340                     Set<PropertyValue> picturePropertyValues = new HashSet<>();
1341                     picturePropertyValues.add(Field.createPropertyValue("attachmentContactPhoto", "true"));
1342                     // picturePropertyValues.add(Field.createPropertyValue("renderingPosition", "-1"));
1343                     picturePropertyValues.add(Field.createPropertyValue("attachExtension", ".jpg"));
1344 
1345                     final ExchangePropPatchRequest attachmentPropPatchRequest = new ExchangePropPatchRequest(contactPictureUrl, picturePropertyValues);
1346                     try (CloseableHttpResponse response = httpClientAdapter.execute(attachmentPropPatchRequest)) {
1347                         attachmentPropPatchRequest.handleResponse(response);
1348                         status = response.getStatusLine().getStatusCode();
1349                         if (status != HttpStatus.SC_MULTI_STATUS) {
1350                             LOGGER.error("Error in contact photo create or update: " + response.getStatusLine().getStatusCode());
1351                             throw new IOException("Unable to update contact picture");
1352                         }
1353                     }
1354 
1355                 } else {
1356                     // try to delete picture
1357                     HttpDelete httpDelete = new HttpDelete(contactPictureUrl);
1358                     try (CloseableHttpResponse response = httpClientAdapter.execute(httpDelete)) {
1359                         status = response.getStatusLine().getStatusCode();
1360                         if (status != HttpStatus.SC_OK && status != HttpStatus.SC_NOT_FOUND) {
1361                             LOGGER.error("Error in contact photo delete: " + status);
1362                             throw new IOException("Unable to delete contact picture");
1363                         }
1364                     }
1365                 }
1366                 // need to retrieve new etag
1367                 HttpHead headMethod = new HttpHead(URIUtil.encodePath(getHref()));
1368                 try (CloseableHttpResponse response = httpClientAdapter.execute(headMethod)) {
1369                     if (response.getFirstHeader("ETag") != null) {
1370                         itemResult.etag = response.getFirstHeader("ETag").getValue();
1371                     }
1372                 }
1373             }
1374             return itemResult;
1375 
1376         }
1377 
1378     }
1379 
1380     /**
1381      * @inheritDoc
1382      */
1383     public class Event extends ExchangeSession.Event {
1384         protected String instancetype;
1385 
1386         /**
1387          * Build Event instance from response info.
1388          *
1389          * @param multiStatusResponse response
1390          * @throws IOException on error
1391          */
1392         public Event(MultiStatusResponse multiStatusResponse) throws IOException {
1393             setHref(URIUtil.decode(multiStatusResponse.getHref()));
1394             DavPropertySet properties = multiStatusResponse.getProperties(HttpStatus.SC_OK);
1395             permanentUrl = getURLPropertyIfExists(properties, "permanenturl");
1396             etag = getPropertyIfExists(properties, "etag");
1397             displayName = getPropertyIfExists(properties, "displayname");
1398             subject = getPropertyIfExists(properties, "subject");
1399             instancetype = getPropertyIfExists(properties, "instancetype");
1400             contentClass = getPropertyIfExists(properties, "contentclass");
1401         }
1402 
1403         protected String getPermanentUrl() {
1404             return permanentUrl;
1405         }
1406 
1407         public Event(String folderPath, String itemName, String contentClass, String itemBody, String etag, String noneMatch) throws IOException {
1408             super(folderPath, itemName, contentClass, itemBody, etag, noneMatch);
1409         }
1410 
1411         protected byte[] getICSFromInternetContentProperty() throws IOException, DavException, MessagingException {
1412             byte[] result = null;
1413             // PropFind PR_INTERNET_CONTENT
1414             String propertyValue = getItemProperty(permanentUrl, "internetContent");
1415             if (propertyValue != null) {
1416                 result = getICS(new ByteArrayInputStream(IOUtil.decodeBase64(propertyValue)));
1417             }
1418             return result;
1419         }
1420 
1421         /**
1422          * Load ICS content from Exchange server.
1423          * User Translate: f header to get MIME event content and get ICS attachment from it
1424          *
1425          * @return ICS (iCalendar) event
1426          * @throws IOException on error
1427          */
1428         @Override
1429         public byte[] getEventContent() throws IOException {
1430             byte[] result = null;
1431             LOGGER.debug("Get event subject: " + subject + " contentclass: " + contentClass + " href: " + getHref() + " permanentUrl: " + permanentUrl);
1432             // do not try to load tasks MIME body
1433             if (!"urn:content-classes:task".equals(contentClass)) {
1434                 // try to get PR_INTERNET_CONTENT
1435                 try {
1436                     result = getICSFromInternetContentProperty();
1437                     if (result == null) {
1438                         HttpGet httpGet = new HttpGet(encodeAndFixUrl(permanentUrl));
1439                         httpGet.setHeader("Content-Type", "text/xml; charset=utf-8");
1440                         httpGet.setHeader("Translate", "f");
1441                         try (CloseableHttpResponse response = httpClientAdapter.execute(httpGet)) {
1442                             result = getICS(response.getEntity().getContent());
1443                         }
1444                     }
1445                 } catch (DavException | IOException | MessagingException e) {
1446                     LOGGER.warn(e.getMessage());
1447                 }
1448             }
1449 
1450             // failover: rebuild event from MAPI properties
1451             if (result == null) {
1452                 try {
1453                     result = getICSFromItemProperties();
1454                 } catch (IOException e) {
1455                     deleteBroken();
1456                     throw e;
1457                 }
1458             }
1459             // debug code
1460             /*if (new String(result).indexOf("VTODO") < 0) {
1461                 LOGGER.debug("Original body: " + new String(result));
1462                 result = getICSFromItemProperties();
1463                 LOGGER.debug("Rebuilt body: " + new String(result));
1464             }*/
1465 
1466             return result;
1467         }
1468 
1469         private byte[] getICSFromItemProperties() throws HttpNotFoundException {
1470             byte[] result;
1471 
1472             // experimental: build VCALENDAR from properties
1473 
1474             try {
1475                 //MultiStatusResponse[] responses = DavGatewayHttpClientFacade.executeMethod(httpClient, propFindMethod);
1476                 Set<String> eventProperties = new HashSet<>();
1477                 eventProperties.add("method");
1478 
1479                 eventProperties.add("created");
1480                 eventProperties.add("calendarlastmodified");
1481                 eventProperties.add("dtstamp");
1482                 eventProperties.add("calendaruid");
1483                 eventProperties.add("subject");
1484                 eventProperties.add("dtstart");
1485                 eventProperties.add("dtend");
1486                 eventProperties.add("transparent");
1487                 eventProperties.add("organizer");
1488                 eventProperties.add("to");
1489                 eventProperties.add("description");
1490                 eventProperties.add("rrule");
1491                 eventProperties.add("exdate");
1492                 eventProperties.add("sensitivity");
1493                 eventProperties.add("alldayevent");
1494                 eventProperties.add("busystatus");
1495                 eventProperties.add("reminderset");
1496                 eventProperties.add("reminderdelta");
1497                 // task
1498                 eventProperties.add("importance");
1499                 eventProperties.add("uid");
1500                 eventProperties.add("taskstatus");
1501                 eventProperties.add("percentcomplete");
1502                 eventProperties.add("keywords");
1503                 eventProperties.add("startdate");
1504                 eventProperties.add("duedate");
1505                 eventProperties.add("datecompleted");
1506 
1507                 MultiStatusResponse[] responses = searchItems(folderPath, eventProperties, DavExchangeSession.this.isEqualTo("urlcompname", convertItemNameToEML(itemName)), FolderQueryTraversal.Shallow, 1);
1508                 if (responses.length == 0) {
1509                     throw new HttpNotFoundException(permanentUrl + " not found");
1510                 }
1511                 DavPropertySet davPropertySet = responses[0].getProperties(HttpStatus.SC_OK);
1512                 VCalendar localVCalendar = new VCalendar();
1513                 localVCalendar.setPropertyValue("PRODID", "-//davmail.sf.net/NONSGML DavMail Calendar V1.1//EN");
1514                 localVCalendar.setPropertyValue("VERSION", "2.0");
1515                 localVCalendar.setPropertyValue("METHOD", getPropertyIfExists(davPropertySet, "method"));
1516                 VObject vEvent = new VObject();
1517                 vEvent.setPropertyValue("CREATED", convertDateFromExchange(getPropertyIfExists(davPropertySet, "created")));
1518                 vEvent.setPropertyValue("LAST-MODIFIED", convertDateFromExchange(getPropertyIfExists(davPropertySet, "calendarlastmodified")));
1519                 vEvent.setPropertyValue("DTSTAMP", convertDateFromExchange(getPropertyIfExists(davPropertySet, "dtstamp")));
1520 
1521                 String uid = getPropertyIfExists(davPropertySet, "calendaruid");
1522                 if (uid == null) {
1523                     uid = getPropertyIfExists(davPropertySet, "uid");
1524                 }
1525                 vEvent.setPropertyValue("UID", uid);
1526                 vEvent.setPropertyValue("SUMMARY", getPropertyIfExists(davPropertySet, "subject"));
1527                 vEvent.setPropertyValue("DESCRIPTION", getPropertyIfExists(davPropertySet, "description"));
1528                 vEvent.setPropertyValue("PRIORITY", convertPriorityFromExchange(getPropertyIfExists(davPropertySet, "importance")));
1529                 vEvent.setPropertyValue("CATEGORIES", getPropertyIfExists(davPropertySet, "keywords"));
1530                 String sensitivity = getPropertyIfExists(davPropertySet, "sensitivity");
1531                 if ("2".equals(sensitivity)) {
1532                     vEvent.setPropertyValue("CLASS", "PRIVATE");
1533                 } else if ("3".equals(sensitivity)) {
1534                     vEvent.setPropertyValue("CLASS", "CONFIDENTIAL");
1535                 } else if ("0".equals(sensitivity)) {
1536                     vEvent.setPropertyValue("CLASS", "PUBLIC");
1537                 }
1538 
1539                 if (instancetype == null) {
1540                     vEvent.type = "VTODO";
1541                     double percentComplete = getDoublePropertyIfExists(davPropertySet, "percentcomplete");
1542                     if (percentComplete > 0) {
1543                         vEvent.setPropertyValue("PERCENT-COMPLETE", String.valueOf((int) (percentComplete * 100)));
1544                     }
1545                     vEvent.setPropertyValue("STATUS", taskTovTodoStatusMap.get(getPropertyIfExists(davPropertySet, "taskstatus")));
1546                     vEvent.setPropertyValue("DUE;VALUE=DATE", convertDateFromExchangeToTaskDate(getPropertyIfExists(davPropertySet, "duedate")));
1547                     vEvent.setPropertyValue("DTSTART;VALUE=DATE", convertDateFromExchangeToTaskDate(getPropertyIfExists(davPropertySet, "startdate")));
1548                     vEvent.setPropertyValue("COMPLETED;VALUE=DATE", convertDateFromExchangeToTaskDate(getPropertyIfExists(davPropertySet, "datecompleted")));
1549 
1550                 } else {
1551                     vEvent.type = "VEVENT";
1552                     // check mandatory dtstart value
1553                     String dtstart = getPropertyIfExists(davPropertySet, "dtstart");
1554                     if (dtstart != null) {
1555                         vEvent.setPropertyValue("DTSTART", convertDateFromExchange(dtstart));
1556                     } else {
1557                         LOGGER.warn("missing dtstart on item, using fake value. Set davmail.deleteBroken=true to delete broken events");
1558                         vEvent.setPropertyValue("DTSTART", "20000101T000000Z");
1559                         deleteBroken();
1560                     }
1561                     // same on DTEND
1562                     String dtend = getPropertyIfExists(davPropertySet, "dtend");
1563                     if (dtend != null) {
1564                         vEvent.setPropertyValue("DTEND", convertDateFromExchange(dtend));
1565                     } else {
1566                         LOGGER.warn("missing dtend on item, using fake value. Set davmail.deleteBroken=true to delete broken events");
1567                         vEvent.setPropertyValue("DTEND", "20000101T010000Z");
1568                         deleteBroken();
1569                     }
1570                     vEvent.setPropertyValue("TRANSP", getPropertyIfExists(davPropertySet, "transparent"));
1571                     vEvent.setPropertyValue("RRULE", getPropertyIfExists(davPropertySet, "rrule"));
1572                     String exdates = getPropertyIfExists(davPropertySet, "exdate");
1573                     if (exdates != null) {
1574                         String[] exdatearray = exdates.split(",");
1575                         for (String exdate : exdatearray) {
1576                             vEvent.addPropertyValue("EXDATE",
1577                                     StringUtil.convertZuluDateTimeToAllDay(convertDateFromExchange(exdate)));
1578                         }
1579                     }
1580                     String organizer = getPropertyIfExists(davPropertySet, "organizer");
1581                     String organizerEmail = null;
1582                     if (organizer != null) {
1583                         InternetAddress organizerAddress = new InternetAddress(organizer);
1584                         organizerEmail = organizerAddress.getAddress();
1585                         vEvent.setPropertyValue("ORGANIZER", "MAILTO:" + organizerEmail);
1586                     }
1587 
1588                     // Parse attendee list
1589                     String toHeader = getPropertyIfExists(davPropertySet, "to");
1590                     if (toHeader != null && !toHeader.equals(organizerEmail)) {
1591                         InternetAddress[] attendees = InternetAddress.parseHeader(toHeader, false);
1592                         for (InternetAddress attendee : attendees) {
1593                             if (!attendee.getAddress().equalsIgnoreCase(organizerEmail)) {
1594                                 VProperty vProperty = new VProperty("ATTENDEE", attendee.getAddress());
1595                                 if (attendee.getPersonal() != null) {
1596                                     vProperty.addParam("CN", attendee.getPersonal());
1597                                 }
1598                                 vEvent.addProperty(vProperty);
1599                             }
1600                         }
1601 
1602                     }
1603                     vEvent.setPropertyValue("X-MICROSOFT-CDO-ALLDAYEVENT",
1604                             "1".equals(getPropertyIfExists(davPropertySet, "alldayevent")) ? "TRUE" : "FALSE");
1605                     vEvent.setPropertyValue("X-MICROSOFT-CDO-BUSYSTATUS", getPropertyIfExists(davPropertySet, "busystatus"));
1606 
1607                     if ("1".equals(getPropertyIfExists(davPropertySet, "reminderset"))) {
1608                         VObject vAlarm = new VObject();
1609                         vAlarm.type = "VALARM";
1610                         vAlarm.setPropertyValue("ACTION", "DISPLAY");
1611                         vAlarm.setPropertyValue("DISPLAY", "Reminder");
1612                         String reminderdelta = getPropertyIfExists(davPropertySet, "reminderdelta");
1613                         VProperty vProperty = new VProperty("TRIGGER", "-PT" + reminderdelta + 'M');
1614                         vProperty.addParam("VALUE", "DURATION");
1615                         vAlarm.addProperty(vProperty);
1616                         vEvent.addVObject(vAlarm);
1617                     }
1618                 }
1619 
1620                 localVCalendar.addVObject(vEvent);
1621                 result = localVCalendar.toString().getBytes(StandardCharsets.UTF_8);
1622             } catch (MessagingException | IOException e) {
1623                 LOGGER.warn("Unable to rebuild event content: " + e.getMessage(), e);
1624                 throw new HttpNotFoundException("Unable to get event " + getName() + " subject: " + subject + " at " + permanentUrl + ": " + e.getMessage());
1625             }
1626 
1627             return result;
1628         }
1629 
1630         protected void deleteBroken() {
1631             // try to delete broken event
1632             if (Settings.getBooleanProperty("davmail.deleteBroken")) {
1633                 LOGGER.warn("Deleting broken event at: " + permanentUrl);
1634                 try {
1635                     HttpDelete httpDelete = new HttpDelete(encodeAndFixUrl(permanentUrl));
1636                     try (CloseableHttpResponse response = httpClientAdapter.execute(httpDelete)) {
1637                         LOGGER.warn("deleteBroken returned " + response.getStatusLine().getStatusCode());
1638                     }
1639                 } catch (IOException e) {
1640                     LOGGER.warn("Unable to delete broken event at: " + permanentUrl);
1641                 }
1642             }
1643         }
1644 
1645         protected CloseableHttpResponse internalCreateOrUpdate(String encodedHref, byte[] mimeContent) throws IOException {
1646             HttpPut httpPut = new HttpPut(encodedHref);
1647             httpPut.setHeader("Translate", "f");
1648             httpPut.setHeader("Overwrite", "f");
1649             if (etag != null) {
1650                 httpPut.setHeader("If-Match", etag);
1651             }
1652             if (noneMatch != null) {
1653                 httpPut.setHeader("If-None-Match", noneMatch);
1654             }
1655             httpPut.setHeader("Content-Type", "message/rfc822");
1656             httpPut.setEntity(new ByteArrayEntity(mimeContent, ContentType.getByMimeType("message/rfc822")));
1657             try (CloseableHttpResponse response = httpClientAdapter.execute(httpPut)) {
1658                 return response;
1659             }
1660         }
1661 
1662         /**
1663          * @inheritDoc
1664          */
1665         @Override
1666         public ItemResult createOrUpdate() throws IOException {
1667             ItemResult itemResult = new ItemResult();
1668             if (vCalendar.isTodo()) {
1669                 if ((mailPath + calendarName).equals(folderPath)) {
1670                     folderPath = mailPath + tasksName;
1671                 }
1672                 String encodedHref = URIUtil.encodePath(getHref());
1673                 Set<PropertyValue> propertyValues = new HashSet<>();
1674                 // set contentclass on create
1675                 if (noneMatch != null) {
1676                     propertyValues.add(Field.createPropertyValue("contentclass", "urn:content-classes:task"));
1677                     propertyValues.add(Field.createPropertyValue("outlookmessageclass", "IPM.Task"));
1678                     propertyValues.add(Field.createPropertyValue("calendaruid", vCalendar.getFirstVeventPropertyValue("UID")));
1679                 }
1680                 propertyValues.add(Field.createPropertyValue("subject", vCalendar.getFirstVeventPropertyValue("SUMMARY")));
1681                 propertyValues.add(Field.createPropertyValue("description", vCalendar.getFirstVeventPropertyValue("DESCRIPTION")));
1682                 propertyValues.add(Field.createPropertyValue("importance", convertPriorityToExchange(vCalendar.getFirstVeventPropertyValue("PRIORITY"))));
1683                 String percentComplete = vCalendar.getFirstVeventPropertyValue("PERCENT-COMPLETE");
1684                 if (percentComplete == null) {
1685                     percentComplete = "0";
1686                 }
1687                 propertyValues.add(Field.createPropertyValue("percentcomplete", String.valueOf(Double.parseDouble(percentComplete) / 100)));
1688                 String taskStatus = vTodoToTaskStatusMap.get(vCalendar.getFirstVeventPropertyValue("STATUS"));
1689                 propertyValues.add(Field.createPropertyValue("taskstatus", taskStatus));
1690                 propertyValues.add(Field.createPropertyValue("keywords", vCalendar.getFirstVeventPropertyValue("CATEGORIES")));
1691                 propertyValues.add(Field.createPropertyValue("startdate", convertTaskDateToZulu(vCalendar.getFirstVeventPropertyValue("DTSTART"))));
1692                 propertyValues.add(Field.createPropertyValue("duedate", convertTaskDateToZulu(vCalendar.getFirstVeventPropertyValue("DUE"))));
1693                 propertyValues.add(Field.createPropertyValue("datecompleted", convertTaskDateToZulu(vCalendar.getFirstVeventPropertyValue("COMPLETED"))));
1694 
1695                 propertyValues.add(Field.createPropertyValue("iscomplete", "2".equals(taskStatus) ? "true" : "false"));
1696                 propertyValues.add(Field.createPropertyValue("commonstart", convertTaskDateToZulu(vCalendar.getFirstVeventPropertyValue("DTSTART"))));
1697                 propertyValues.add(Field.createPropertyValue("commonend", convertTaskDateToZulu(vCalendar.getFirstVeventPropertyValue("DUE"))));
1698 
1699                 ExchangePropPatchRequest propPatchMethod = new ExchangePropPatchRequest(encodedHref, propertyValues);
1700                 propPatchMethod.setHeader("Translate", "f");
1701                 if (etag != null) {
1702                     propPatchMethod.setHeader("If-Match", etag);
1703                 }
1704                 if (noneMatch != null) {
1705                     propPatchMethod.setHeader("If-None-Match", noneMatch);
1706                 }
1707                 try (CloseableHttpResponse response = httpClientAdapter.execute(propPatchMethod)) {
1708                     int status = response.getStatusLine().getStatusCode();
1709 
1710                     if (status == HttpStatus.SC_MULTI_STATUS) {
1711                         Item newItem = getItem(folderPath, itemName);
1712                         try {
1713                             itemResult.status = propPatchMethod.getResponseStatusCode();
1714                         } catch (HttpResponseException e) {
1715                             throw new IOException(e.getMessage(), e);
1716                         }
1717                         itemResult.etag = newItem.etag;
1718                     } else {
1719                         itemResult.status = status;
1720                     }
1721                 }
1722 
1723             } else {
1724                 String encodedHref = URIUtil.encodePath(getHref());
1725                 byte[] mimeContent = createMimeContent();
1726                 HttpResponse httpResponse = internalCreateOrUpdate(encodedHref, mimeContent);
1727                 int status = httpResponse.getStatusLine().getStatusCode();
1728 
1729                 if (status == HttpStatus.SC_OK) {
1730                     LOGGER.debug("Updated event " + encodedHref);
1731                 } else if (status == HttpStatus.SC_CREATED) {
1732                     LOGGER.debug("Created event " + encodedHref);
1733                 } else if (status == HttpStatus.SC_NOT_FOUND) {
1734                     LOGGER.debug("Event not found at " + encodedHref + ", searching permanenturl by urlcompname");
1735                     // failover, search item by urlcompname
1736                     MultiStatusResponse[] responses = searchItems(folderPath, EVENT_REQUEST_PROPERTIES, DavExchangeSession.this.isEqualTo("urlcompname", convertItemNameToEML(itemName)), FolderQueryTraversal.Shallow, 1);
1737                     if (responses.length == 1) {
1738                         encodedHref = getPropertyIfExists(responses[0].getProperties(HttpStatus.SC_OK), "permanenturl");
1739                         LOGGER.warn("Event found, permanenturl is " + encodedHref);
1740                         httpResponse = internalCreateOrUpdate(encodedHref, mimeContent);
1741                         status = httpResponse.getStatusLine().getStatusCode();
1742                         if (status == HttpStatus.SC_OK) {
1743                             LOGGER.debug("Updated event " + encodedHref);
1744                         } else {
1745                             LOGGER.warn("Unable to create or update event " + status + ' ' + httpResponse.getStatusLine().getReasonPhrase());
1746                         }
1747                     }
1748                 } else {
1749                     LOGGER.warn("Unable to create or update event " + status + ' ' + httpResponse.getStatusLine().getReasonPhrase());
1750                 }
1751 
1752                 // 440 means forbidden on Exchange
1753                 if (status == 440) {
1754                     status = HttpStatus.SC_FORBIDDEN;
1755                 } else if (status == HttpStatus.SC_UNAUTHORIZED && getHref().startsWith("/public")) {
1756                     LOGGER.warn("Ignore 401 unauthorized on public event");
1757                     status = HttpStatus.SC_OK;
1758                 }
1759                 itemResult.status = status;
1760                 if (httpResponse.getFirstHeader("GetETag") != null) {
1761                     itemResult.etag = httpResponse.getFirstHeader("GetETag").getValue();
1762                 }
1763 
1764                 // trigger activeSync push event, only if davmail.forceActiveSyncUpdate setting is true
1765                 if ((status == HttpStatus.SC_OK || status == HttpStatus.SC_CREATED) &&
1766                         (Settings.getBooleanProperty("davmail.forceActiveSyncUpdate"))) {
1767                     ArrayList<PropEntry> propertyList = new ArrayList<>();
1768                     // Set contentclass to make ActiveSync happy
1769                     propertyList.add(Field.createDavProperty("contentclass", contentClass));
1770                     // ... but also set PR_INTERNET_CONTENT to preserve custom properties
1771                     propertyList.add(Field.createDavProperty("internetContent", IOUtil.encodeBase64AsString(mimeContent)));
1772                     HttpProppatch propPatchMethod = new HttpProppatch(encodedHref, propertyList);
1773                     try (CloseableHttpResponse response = httpClientAdapter.execute(propPatchMethod)) {
1774                         int patchStatus = response.getStatusLine().getStatusCode();
1775                         if (patchStatus != HttpStatus.SC_MULTI_STATUS) {
1776                             LOGGER.warn("Unable to patch event to trigger activeSync push");
1777                         } else {
1778                             // need to retrieve new etag
1779                             Item newItem = getItem(folderPath, itemName);
1780                             itemResult.etag = newItem.etag;
1781                         }
1782                     }
1783                 }
1784             }
1785             return itemResult;
1786         }
1787 
1788 
1789     }
1790 
1791     protected Folder buildFolder(MultiStatusResponse entity) throws IOException {
1792         String href = URIUtil.decode(entity.getHref());
1793         Folder folder = new Folder();
1794         DavPropertySet properties = entity.getProperties(HttpStatus.SC_OK);
1795         folder.displayName = getPropertyIfExists(properties, "displayname");
1796         folder.folderClass = getPropertyIfExists(properties, "folderclass");
1797         folder.hasChildren = "1".equals(getPropertyIfExists(properties, "hassubs"));
1798         folder.noInferiors = "1".equals(getPropertyIfExists(properties, "nosubs"));
1799         folder.messageCount = getIntPropertyIfExists(properties, "count");
1800         folder.unreadCount = getIntPropertyIfExists(properties, "unreadcount");
1801         // fake recent value
1802         folder.recent = folder.unreadCount;
1803         folder.ctag = getPropertyIfExists(properties, "contenttag");
1804         folder.etag = getPropertyIfExists(properties, "lastmodified");
1805 
1806         folder.uidNext = getIntPropertyIfExists(properties, "uidNext");
1807 
1808         // replace well known folder names
1809         if (inboxUrl != null && href.startsWith(inboxUrl)) {
1810             folder.folderPath = href.replaceFirst(inboxUrl, INBOX);
1811         } else if (sentitemsUrl != null && href.startsWith(sentitemsUrl)) {
1812             folder.folderPath = href.replaceFirst(sentitemsUrl, SENT);
1813         } else if (draftsUrl != null && href.startsWith(draftsUrl)) {
1814             folder.folderPath = href.replaceFirst(draftsUrl, DRAFTS);
1815         } else if (deleteditemsUrl != null && href.startsWith(deleteditemsUrl)) {
1816             folder.folderPath = href.replaceFirst(deleteditemsUrl, TRASH);
1817         } else if (calendarUrl != null && href.startsWith(calendarUrl)) {
1818             folder.folderPath = href.replaceFirst(calendarUrl, CALENDAR);
1819         } else if (contactsUrl != null && href.startsWith(contactsUrl)) {
1820             folder.folderPath = href.replaceFirst(contactsUrl, CONTACTS);
1821         } else {
1822             int index = href.indexOf(mailPath.substring(0, mailPath.length() - 1));
1823             if (index >= 0) {
1824                 if (index + mailPath.length() > href.length()) {
1825                     folder.folderPath = "";
1826                 } else {
1827                     folder.folderPath = href.substring(index + mailPath.length());
1828                 }
1829             } else {
1830                 try {
1831                     java.net.URI folderURI = new java.net.URI(href);
1832                     folder.folderPath = folderURI.getPath();
1833                     if (folder.folderPath == null) {
1834                         throw new DavMailException("EXCEPTION_INVALID_FOLDER_URL", href);
1835                     }
1836                 } catch (URISyntaxException e) {
1837                     throw new DavMailException("EXCEPTION_INVALID_FOLDER_URL", href);
1838                 }
1839             }
1840         }
1841         if (folder.folderPath.endsWith("/")) {
1842             folder.folderPath = folder.folderPath.substring(0, folder.folderPath.length() - 1);
1843         }
1844         return folder;
1845     }
1846 
1847     protected static final Set<String> FOLDER_PROPERTIES = new HashSet<>();
1848 
1849     static {
1850         FOLDER_PROPERTIES.add("displayname");
1851         FOLDER_PROPERTIES.add("folderclass");
1852         FOLDER_PROPERTIES.add("hassubs");
1853         FOLDER_PROPERTIES.add("nosubs");
1854         FOLDER_PROPERTIES.add("count");
1855         FOLDER_PROPERTIES.add("unreadcount");
1856         FOLDER_PROPERTIES.add("contenttag");
1857         FOLDER_PROPERTIES.add("lastmodified");
1858         FOLDER_PROPERTIES.add("uidNext");
1859     }
1860 
1861     protected static final DavPropertyNameSet FOLDER_PROPERTIES_NAME_SET = new DavPropertyNameSet();
1862 
1863     static {
1864         for (String attribute : FOLDER_PROPERTIES) {
1865             FOLDER_PROPERTIES_NAME_SET.add(Field.getPropertyName(attribute));
1866         }
1867     }
1868 
1869     /**
1870      * @inheritDoc
1871      */
1872     @Override
1873     protected Folder internalGetFolder(String folderPath) throws IOException {
1874         MultiStatus multiStatus = httpClientAdapter.executeDavRequest(new HttpPropfind(
1875                 URIUtil.encodePath(getFolderPath(folderPath)),
1876                 FOLDER_PROPERTIES_NAME_SET, 0));
1877         MultiStatusResponse[] responses = multiStatus.getResponses();
1878 
1879         Folder folder = null;
1880         if (responses.length > 0) {
1881             folder = buildFolder(responses[0]);
1882             folder.folderPath = folderPath;
1883         }
1884         return folder;
1885     }
1886 
1887     /**
1888      * @inheritDoc
1889      */
1890     @Override
1891     public List<Folder> getSubFolders(String folderPath, Condition condition, boolean recursive) throws IOException {
1892         boolean isPublic = folderPath.startsWith("/public");
1893         FolderQueryTraversal mode = (!isPublic && recursive) ? FolderQueryTraversal.Deep : FolderQueryTraversal.Shallow;
1894         List<Folder> folders = new ArrayList<>();
1895 
1896         MultiStatusResponse[] responses = searchItems(folderPath, FOLDER_PROPERTIES, and(isTrue("isfolder"), isFalse("ishidden"), condition), mode, 0);
1897 
1898         for (MultiStatusResponse response : responses) {
1899             Folder folder = buildFolder(response);
1900             folders.add(buildFolder(response));
1901             if (isPublic && recursive) {
1902                 getSubFolders(folder.folderPath, condition, recursive);
1903             }
1904         }
1905         return folders;
1906     }
1907 
1908     /**
1909      * @inheritDoc
1910      */
1911     @Override
1912     public int createFolder(String folderPath, String folderClass, Map<String, String> properties) throws IOException {
1913         Set<PropertyValue> propertyValues = new HashSet<>();
1914         if (properties != null) {
1915             for (Map.Entry<String, String> entry : properties.entrySet()) {
1916                 propertyValues.add(Field.createPropertyValue(entry.getKey(), entry.getValue()));
1917             }
1918         }
1919         propertyValues.add(Field.createPropertyValue("folderclass", folderClass));
1920 
1921         // standard MkColMethod does not take properties, override ExchangePropPatchRequest instead
1922         ExchangePropPatchRequest propPatchRequest = new ExchangePropPatchRequest(URIUtil.encodePath(getFolderPath(folderPath)), propertyValues) {
1923             @Override
1924             public String getMethod() {
1925                 return "MKCOL";
1926             }
1927         };
1928         int status;
1929         try (CloseableHttpResponse response = httpClientAdapter.execute(propPatchRequest)) {
1930             propPatchRequest.handleResponse(response);
1931             status = response.getStatusLine().getStatusCode();
1932             if (status == HttpStatus.SC_MULTI_STATUS) {
1933                 status = propPatchRequest.getResponseStatusCode();
1934             } else if (status == HttpStatus.SC_METHOD_NOT_ALLOWED) {
1935                 LOGGER.info("Folder " + folderPath + " already exists");
1936             }
1937         } catch (HttpResponseException e) {
1938             throw new IOException(e.getMessage(), e);
1939         }
1940         LOGGER.debug("Create folder " + folderPath + " returned " + status);
1941         return status;
1942     }
1943 
1944     /**
1945      * @inheritDoc
1946      */
1947     @Override
1948     public int updateFolder(String folderPath, Map<String, String> properties) throws IOException {
1949         Set<PropertyValue> propertyValues = new HashSet<>();
1950         if (properties != null) {
1951             for (Map.Entry<String, String> entry : properties.entrySet()) {
1952                 propertyValues.add(Field.createPropertyValue(entry.getKey(), entry.getValue()));
1953             }
1954         }
1955 
1956         ExchangePropPatchRequest propPatchRequest = new ExchangePropPatchRequest(URIUtil.encodePath(getFolderPath(folderPath)), propertyValues);
1957         try (CloseableHttpResponse response = httpClientAdapter.execute(propPatchRequest)) {
1958             propPatchRequest.handleResponse(response);
1959             int status = response.getStatusLine().getStatusCode();
1960             if (status == HttpStatus.SC_MULTI_STATUS) {
1961                 try {
1962                     status = propPatchRequest.getResponseStatusCode();
1963                 } catch (HttpResponseException e) {
1964                     throw new IOException(e.getMessage(), e);
1965                 }
1966             }
1967 
1968             return status;
1969         }
1970     }
1971 
1972     /**
1973      * @inheritDoc
1974      */
1975     @Override
1976     public void deleteFolder(String folderPath) throws IOException {
1977         HttpDelete httpDelete = new HttpDelete(URIUtil.encodePath(getFolderPath(folderPath)));
1978         try (CloseableHttpResponse response = httpClientAdapter.execute(httpDelete)) {
1979             int status = response.getStatusLine().getStatusCode();
1980             if (status != HttpStatus.SC_OK && status != HttpStatus.SC_NOT_FOUND) {
1981                 throw HttpClientAdapter.buildHttpResponseException(httpDelete, response);
1982             }
1983         }
1984     }
1985 
1986     /**
1987      * @inheritDoc
1988      */
1989     @Override
1990     public void moveFolder(String folderPath, String targetPath) throws IOException {
1991         HttpMove httpMove = new HttpMove(URIUtil.encodePath(getFolderPath(folderPath)),
1992                 URIUtil.encodePath(getFolderPath(targetPath)), false);
1993         try (CloseableHttpResponse response = httpClientAdapter.execute(httpMove)) {
1994             int statusCode = response.getStatusLine().getStatusCode();
1995             if (statusCode == HttpStatus.SC_PRECONDITION_FAILED) {
1996                 throw new HttpPreconditionFailedException(BundleMessage.format("EXCEPTION_UNABLE_TO_MOVE_FOLDER"));
1997             } else if (statusCode != HttpStatus.SC_CREATED) {
1998                 throw HttpClientAdapter.buildHttpResponseException(httpMove, response);
1999             } else if (folderPath.equalsIgnoreCase("/users/" + getEmail() + "/calendar")) {
2000                 // calendar renamed, need to reload well known folders
2001                 getWellKnownFolders();
2002             }
2003         }
2004     }
2005 
2006     /**
2007      * @inheritDoc
2008      */
2009     @Override
2010     public void moveItem(String sourcePath, String targetPath) throws IOException {
2011         HttpMove httpMove = new HttpMove(URIUtil.encodePath(getFolderPath(sourcePath)),
2012                 URIUtil.encodePath(getFolderPath(targetPath)), false);
2013         moveItem(httpMove);
2014     }
2015 
2016     protected void moveItem(HttpMove httpMove) throws IOException {
2017         try (CloseableHttpResponse response = httpClientAdapter.execute(httpMove)) {
2018             int statusCode = response.getStatusLine().getStatusCode();
2019             if (statusCode == HttpStatus.SC_PRECONDITION_FAILED) {
2020                 throw new DavMailException("EXCEPTION_UNABLE_TO_MOVE_ITEM");
2021             } else if (statusCode != HttpStatus.SC_CREATED && statusCode != HttpStatus.SC_OK) {
2022                 throw HttpClientAdapter.buildHttpResponseException(httpMove, response);
2023             }
2024         }
2025     }
2026 
2027     protected String getPropertyIfExists(DavPropertySet properties, String alias) {
2028         DavProperty property = properties.get(Field.getResponsePropertyName(alias));
2029         if (property == null) {
2030             return null;
2031         } else {
2032             Object value = property.getValue();
2033             if (value instanceof Node) {
2034                 return ((Node) value).getTextContent();
2035             } else if (value instanceof List) {
2036                 StringBuilder buffer = new StringBuilder();
2037                 for (Object node : (List) value) {
2038                     if (buffer.length() > 0) {
2039                         buffer.append(',');
2040                     }
2041                     if (node instanceof Node) {
2042                         // jackrabbit
2043                         buffer.append(((Node) node).getTextContent());
2044                     } else {
2045                         // ExchangeDavMethod
2046                         buffer.append(node);
2047                     }
2048                 }
2049                 return buffer.toString();
2050             } else {
2051                 return (String) value;
2052             }
2053         }
2054     }
2055 
2056     protected String getURLPropertyIfExists(DavPropertySet properties, @SuppressWarnings("SameParameterValue") String alias) throws IOException {
2057         String result = getPropertyIfExists(properties, alias);
2058         if (result != null) {
2059             result = URIUtil.decode(result);
2060         }
2061         return result;
2062     }
2063 
2064     protected int getIntPropertyIfExists(DavPropertySet properties, String alias) {
2065         DavProperty property = properties.get(Field.getPropertyName(alias));
2066         if (property == null) {
2067             return 0;
2068         } else {
2069             return Integer.parseInt((String) property.getValue());
2070         }
2071     }
2072 
2073     protected long getLongPropertyIfExists(DavPropertySet properties, @SuppressWarnings("SameParameterValue") String alias) {
2074         DavProperty property = properties.get(Field.getPropertyName(alias));
2075         if (property == null) {
2076             return 0;
2077         } else {
2078             return Long.parseLong((String) property.getValue());
2079         }
2080     }
2081 
2082     protected double getDoublePropertyIfExists(DavPropertySet properties, @SuppressWarnings("SameParameterValue") String alias) {
2083         DavProperty property = properties.get(Field.getResponsePropertyName(alias));
2084         if (property == null) {
2085             return 0;
2086         } else {
2087             return Double.parseDouble((String) property.getValue());
2088         }
2089     }
2090 
2091     protected byte[] getBinaryPropertyIfExists(DavPropertySet properties, @SuppressWarnings("SameParameterValue") String alias) {
2092         byte[] property = null;
2093         String base64Property = getPropertyIfExists(properties, alias);
2094         if (base64Property != null) {
2095             property = IOUtil.decodeBase64(base64Property);
2096         }
2097         return property;
2098     }
2099 
2100 
2101     protected Message buildMessage(MultiStatusResponse responseEntity) throws IOException {
2102         Message message = new Message();
2103         message.messageUrl = URIUtil.decode(responseEntity.getHref());
2104         DavPropertySet properties = responseEntity.getProperties(HttpStatus.SC_OK);
2105 
2106         message.permanentUrl = getURLPropertyIfExists(properties, "permanenturl");
2107         message.size = getIntPropertyIfExists(properties, "messageSize");
2108         message.uid = getPropertyIfExists(properties, "uid");
2109         message.contentClass = getPropertyIfExists(properties, "contentclass");
2110         message.imapUid = getLongPropertyIfExists(properties, "imapUid");
2111         message.read = "1".equals(getPropertyIfExists(properties, "read"));
2112         message.junk = "1".equals(getPropertyIfExists(properties, "junk"));
2113         message.flagged = "2".equals(getPropertyIfExists(properties, "flagStatus"));
2114         message.draft = (getIntPropertyIfExists(properties, "messageFlags") & 8) != 0;
2115         String lastVerbExecuted = getPropertyIfExists(properties, "lastVerbExecuted");
2116         message.answered = "102".equals(lastVerbExecuted) || "103".equals(lastVerbExecuted);
2117         message.forwarded = "104".equals(lastVerbExecuted);
2118         message.date = convertDateFromExchange(getPropertyIfExists(properties, "date"));
2119         message.deleted = "1".equals(getPropertyIfExists(properties, "deleted"));
2120 
2121         String lastmodified = convertDateFromExchange(getPropertyIfExists(properties, "lastmodified"));
2122         message.recent = !message.read && lastmodified != null && lastmodified.equals(message.date);
2123 
2124         message.keywords = getPropertyIfExists(properties, "keywords");
2125 
2126         if (LOGGER.isDebugEnabled()) {
2127             StringBuilder buffer = new StringBuilder();
2128             buffer.append("Message");
2129             if (message.imapUid != 0) {
2130                 buffer.append(" IMAP uid: ").append(message.imapUid);
2131             }
2132             if (message.uid != null) {
2133                 buffer.append(" uid: ").append(message.uid);
2134             }
2135             buffer.append(" href: ").append(responseEntity.getHref()).append(" permanenturl:").append(message.permanentUrl);
2136             LOGGER.debug(buffer.toString());
2137         }
2138         return message;
2139     }
2140 
2141     @Override
2142     public MessageList searchMessages(String folderPath, Set<String> attributes, Condition condition) throws IOException {
2143         MessageList messages = new MessageList();
2144         int maxCount = Settings.getIntProperty("davmail.folderSizeLimit", 0);
2145         MultiStatusResponse[] responses = searchItems(folderPath, attributes, and(isFalse("isfolder"), isFalse("ishidden"), condition), FolderQueryTraversal.Shallow, maxCount);
2146 
2147         for (MultiStatusResponse response : responses) {
2148             Message message = buildMessage(response);
2149             message.messageList = messages;
2150             messages.add(message);
2151         }
2152         Collections.sort(messages);
2153         return messages;
2154     }
2155 
2156     /**
2157      * @inheritDoc
2158      */
2159     @Override
2160     public List<ExchangeSession.Contact> searchContacts(String folderPath, Set<String> attributes, Condition condition, int maxCount) throws IOException {
2161         List<ExchangeSession.Contact> contacts = new ArrayList<>();
2162         MultiStatusResponse[] responses = searchItems(folderPath, attributes,
2163                 and(isEqualTo("outlookmessageclass", "IPM.Contact"), isFalse("isfolder"), isFalse("ishidden"), condition),
2164                 FolderQueryTraversal.Shallow, maxCount);
2165         for (MultiStatusResponse response : responses) {
2166             contacts.add(new Contact(response));
2167         }
2168         return contacts;
2169     }
2170 
2171     /**
2172      * Common item properties
2173      */
2174     protected static final Set<String> ITEM_PROPERTIES = new HashSet<>();
2175 
2176     static {
2177         ITEM_PROPERTIES.add("etag");
2178         ITEM_PROPERTIES.add("displayname");
2179         // calendar CdoInstanceType
2180         ITEM_PROPERTIES.add("instancetype");
2181         ITEM_PROPERTIES.add("urlcompname");
2182         ITEM_PROPERTIES.add("subject");
2183         ITEM_PROPERTIES.add("contentclass");
2184     }
2185 
2186     @Override
2187     protected Set<String> getItemProperties() {
2188         return ITEM_PROPERTIES;
2189     }
2190 
2191 
2192     /**
2193      * @inheritDoc
2194      */
2195     @Override
2196     public List<ExchangeSession.Event> getEventMessages(String folderPath) throws IOException {
2197         return searchEvents(folderPath, ITEM_PROPERTIES,
2198                 and(isEqualTo("contentclass", "urn:content-classes:calendarmessage"),
2199                         or(isNull("processed"), isFalse("processed"))));
2200     }
2201 
2202 
2203     @Override
2204     public List<ExchangeSession.Event> searchEvents(String folderPath, Set<String> attributes, Condition condition) throws IOException {
2205         List<ExchangeSession.Event> events = new ArrayList<>();
2206         MultiStatusResponse[] responses = searchItems(folderPath, attributes, and(isFalse("isfolder"), isFalse("ishidden"), condition), FolderQueryTraversal.Shallow, 0);
2207         for (MultiStatusResponse response : responses) {
2208             String instancetype = getPropertyIfExists(response.getProperties(HttpStatus.SC_OK), "instancetype");
2209             Event event = new Event(response);
2210             //noinspection VariableNotUsedInsideIf
2211             if (instancetype == null) {
2212                 // check ics content
2213                 try {
2214                     event.getBody();
2215                     // getBody success => add event or task
2216                     events.add(event);
2217                 } catch (IOException e) {
2218                     // invalid event: exclude from list
2219                     LOGGER.warn("Invalid event " + event.displayName + " found at " + response.getHref(), e);
2220                 }
2221             } else {
2222                 events.add(event);
2223             }
2224         }
2225         return events;
2226     }
2227 
2228     @Override
2229     protected Condition getCalendarItemCondition(Condition dateCondition) {
2230         boolean caldavEnableLegacyTasks = Settings.getBooleanProperty("davmail.caldavEnableLegacyTasks", false);
2231         if (caldavEnableLegacyTasks) {
2232             // return tasks created in calendar folder
2233             return or(isNull("instancetype"),
2234                     isEqualTo("instancetype", 1),
2235                     and(isEqualTo("instancetype", 0), dateCondition));
2236         } else {
2237             // instancetype 0 single appointment / 1 master recurring appointment
2238             return and(or(isEqualTo("outlookmessageclass", "IPM.Appointment"), isEqualTo("outlookmessageclass", "IPM.Appointment.MeetingEvent")),
2239                     or(isEqualTo("instancetype", 1),
2240                             and(isEqualTo("instancetype", 0), dateCondition)));
2241         }
2242     }
2243 
2244     protected MultiStatusResponse[] searchItems(String folderPath, Set<String> attributes, Condition condition,
2245                                                 FolderQueryTraversal folderQueryTraversal, int maxCount) throws IOException {
2246         String folderUrl;
2247         if (folderPath.startsWith("http")) {
2248             folderUrl = folderPath;
2249         } else {
2250             folderUrl = getFolderPath(folderPath);
2251         }
2252         StringBuilder searchRequest = new StringBuilder();
2253         searchRequest.append("SELECT ")
2254                 .append(Field.getRequestPropertyString("permanenturl"));
2255         if (attributes != null) {
2256             for (String attribute : attributes) {
2257                 searchRequest.append(',').append(Field.getRequestPropertyString(attribute));
2258             }
2259         }
2260         searchRequest.append(" FROM SCOPE('").append(folderQueryTraversal).append(" TRAVERSAL OF \"").append(folderUrl).append("\"')");
2261         if (condition != null) {
2262             searchRequest.append(" WHERE ");
2263             condition.appendTo(searchRequest);
2264         }
2265         searchRequest.append(" ORDER BY ").append(Field.getRequestPropertyString("imapUid")).append(" DESC");
2266         DavGatewayTray.debug(new BundleMessage("LOG_SEARCH_QUERY", searchRequest));
2267         MultiStatusResponse[] responses = httpClientAdapter.executeSearchRequest(
2268                 encodeAndFixUrl(folderUrl), searchRequest.toString(), maxCount);
2269         DavGatewayTray.debug(new BundleMessage("LOG_SEARCH_RESULT", responses.length));
2270         return responses;
2271     }
2272 
2273     protected static final Set<String> EVENT_REQUEST_PROPERTIES = new HashSet<>();
2274 
2275     static {
2276         EVENT_REQUEST_PROPERTIES.add("permanenturl");
2277         EVENT_REQUEST_PROPERTIES.add("urlcompname");
2278         EVENT_REQUEST_PROPERTIES.add("etag");
2279         EVENT_REQUEST_PROPERTIES.add("contentclass");
2280         EVENT_REQUEST_PROPERTIES.add("displayname");
2281         EVENT_REQUEST_PROPERTIES.add("subject");
2282     }
2283 
2284     protected static final DavPropertyNameSet EVENT_REQUEST_PROPERTIES_NAME_SET = new DavPropertyNameSet();
2285 
2286     static {
2287         for (String attribute : EVENT_REQUEST_PROPERTIES) {
2288             EVENT_REQUEST_PROPERTIES_NAME_SET.add(Field.getPropertyName(attribute));
2289         }
2290 
2291     }
2292 
2293     @Override
2294     public Item getItem(String folderPath, String itemName) throws IOException {
2295         String emlItemName = convertItemNameToEML(itemName);
2296         String itemPath = getFolderPath(folderPath) + '/' + emlItemName;
2297         MultiStatusResponse[] responses = null;
2298         try {
2299             HttpPropfind httpPropfind = new HttpPropfind(URIUtil.encodePath(itemPath), EVENT_REQUEST_PROPERTIES_NAME_SET, 0);
2300             try (CloseableHttpResponse response = httpClientAdapter.execute(httpPropfind)) {
2301                 responses = httpPropfind.getResponseBodyAsMultiStatus(response).getResponses();
2302             } catch (HttpNotFoundException | DavException e) {
2303                 // ignore
2304             }
2305             if (responses == null || responses.length == 0 && isMainCalendar(folderPath)) {
2306                 if (itemName.endsWith(".ics")) {
2307                     itemName = itemName.substring(0, itemName.length() - 3) + "EML";
2308                 }
2309                 // look for item in tasks folder
2310                 HttpPropfind taskHttpPropfind = new HttpPropfind(URIUtil.encodePath(getFolderPath(TASKS) + '/' + emlItemName), EVENT_REQUEST_PROPERTIES_NAME_SET, 0);
2311                 try (CloseableHttpResponse response = httpClientAdapter.execute(taskHttpPropfind)) {
2312                     responses = taskHttpPropfind.getResponseBodyAsMultiStatus(response).getResponses();
2313                 } catch (HttpNotFoundException | DavException e) {
2314                     // ignore
2315                 }
2316             }
2317             if (responses == null || responses.length == 0) {
2318                 throw new HttpNotFoundException(itemPath + " not found");
2319             }
2320         } catch (HttpNotFoundException e) {
2321             try {
2322                 LOGGER.debug(itemPath + " not found, searching by urlcompname");
2323                 // failover: try to get event by displayname
2324                 responses = searchItems(folderPath, EVENT_REQUEST_PROPERTIES, isEqualTo("urlcompname", emlItemName), FolderQueryTraversal.Shallow, 1);
2325                 if (responses.length == 0 && isMainCalendar(folderPath)) {
2326                     responses = searchItems(TASKS, EVENT_REQUEST_PROPERTIES, isEqualTo("urlcompname", emlItemName), FolderQueryTraversal.Shallow, 1);
2327                 }
2328                 if (responses.length == 0) {
2329                     throw new HttpNotFoundException(itemPath + " not found");
2330                 }
2331             } catch (HttpNotFoundException e2) {
2332                 LOGGER.debug("last failover: search all items");
2333                 List<ExchangeSession.Event> events = getAllEvents(folderPath);
2334                 for (ExchangeSession.Event event : events) {
2335                     if (itemName.equals(event.getName())) {
2336                         HttpPropfind permanentHttpPropfind = new HttpPropfind(encodeAndFixUrl(((DavExchangeSession.Event) event).getPermanentUrl()), EVENT_REQUEST_PROPERTIES_NAME_SET, 0);
2337                         try (CloseableHttpResponse response = httpClientAdapter.execute(permanentHttpPropfind)) {
2338                             responses = permanentHttpPropfind.getResponseBodyAsMultiStatus(response).getResponses();
2339                         } catch (DavException e3) {
2340                             // ignore
2341                         }
2342                         break;
2343                     }
2344                 }
2345                 if (responses == null || responses.length == 0) {
2346                     throw new HttpNotFoundException(itemPath + " not found");
2347                 }
2348                 LOGGER.warn("search by urlcompname failed, actual value is " + getPropertyIfExists(responses[0].getProperties(HttpStatus.SC_OK), "urlcompname"));
2349             }
2350         }
2351         // build item
2352         String contentClass = getPropertyIfExists(responses[0].getProperties(HttpStatus.SC_OK), "contentclass");
2353         String urlcompname = getPropertyIfExists(responses[0].getProperties(HttpStatus.SC_OK), "urlcompname");
2354         if ("urn:content-classes:person".equals(contentClass)) {
2355             // retrieve Contact properties
2356             List<ExchangeSession.Contact> contacts = searchContacts(folderPath, CONTACT_ATTRIBUTES,
2357                     isEqualTo("urlcompname", StringUtil.decodeUrlcompname(urlcompname)), 1);
2358             if (contacts.isEmpty()) {
2359                 LOGGER.warn("Item found, but unable to build contact");
2360                 throw new HttpNotFoundException(itemPath + " not found");
2361             }
2362             return contacts.get(0);
2363         } else if ("urn:content-classes:appointment".equals(contentClass)
2364                 || "urn:content-classes:calendarmessage".equals(contentClass)
2365                 || "urn:content-classes:task".equals(contentClass)) {
2366             return new Event(responses[0]);
2367         } else {
2368             LOGGER.warn("wrong contentclass on item " + itemPath + ": " + contentClass);
2369             // return item anyway
2370             return new Event(responses[0]);
2371         }
2372 
2373     }
2374 
2375     @Override
2376     public ExchangeSession.ContactPhoto getContactPhoto(ExchangeSession.Contact contact) throws IOException {
2377         ContactPhoto contactPhoto;
2378         final HttpGet httpGet = new HttpGet(URIUtil.encodePath(contact.getHref()) + "/ContactPicture.jpg");
2379         httpGet.setHeader("Translate", "f");
2380         httpGet.setHeader("Accept-Encoding", "gzip");
2381 
2382         InputStream inputStream = null;
2383         try (CloseableHttpResponse response = httpClientAdapter.execute(httpGet)) {
2384             if (HttpClientAdapter.isGzipEncoded(response)) {
2385                 inputStream = (new GZIPInputStream(response.getEntity().getContent()));
2386             } else {
2387                 inputStream = response.getEntity().getContent();
2388             }
2389 
2390             contactPhoto = new ContactPhoto();
2391             contactPhoto.contentType = "image/jpeg";
2392 
2393             ByteArrayOutputStream baos = new ByteArrayOutputStream();
2394             InputStream partInputStream = inputStream;
2395             IOUtil.write(partInputStream, baos);
2396             contactPhoto.content = IOUtil.encodeBase64AsString(baos.toByteArray());
2397         } finally {
2398             if (inputStream != null) {
2399                 try {
2400                     inputStream.close();
2401                 } catch (IOException e) {
2402                     LOGGER.debug(e);
2403                 }
2404             }
2405         }
2406         return contactPhoto;
2407     }
2408 
2409     @Override
2410     public int sendEvent(String icsBody) throws IOException {
2411         String itemName = UUID.randomUUID() + ".EML";
2412         byte[] mimeContent = (new Event(getFolderPath(DRAFTS), itemName, "urn:content-classes:calendarmessage", icsBody, null, null)).createMimeContent();
2413         if (mimeContent == null) {
2414             // no recipients, cancel
2415             return HttpStatus.SC_NO_CONTENT;
2416         } else {
2417             sendMessage(mimeContent);
2418             return HttpStatus.SC_OK;
2419         }
2420     }
2421 
2422     @Override
2423     public void deleteItem(String folderPath, String itemName) throws IOException {
2424         String eventPath = URIUtil.encodePath(getFolderPath(folderPath) + '/' + convertItemNameToEML(itemName));
2425         HttpDelete httpDelete = new HttpDelete(eventPath);
2426         int status;
2427         try (CloseableHttpResponse response = httpClientAdapter.execute(httpDelete)) {
2428             status = response.getStatusLine().getStatusCode();
2429         }
2430         if (status == HttpStatus.SC_NOT_FOUND && isMainCalendar(folderPath)) {
2431             // retry in tasks folder
2432             eventPath = URIUtil.encodePath(getFolderPath(TASKS) + '/' + convertItemNameToEML(itemName));
2433             httpDelete = new HttpDelete(eventPath);
2434             try (CloseableHttpResponse response = httpClientAdapter.execute(httpDelete)) {
2435                 status = response.getStatusLine().getStatusCode();
2436             }
2437         }
2438         if (status == HttpStatus.SC_NOT_FOUND) {
2439             LOGGER.debug("Unable to delete " + itemName + ": item not found");
2440         }
2441     }
2442 
2443     @Override
2444     public void processItem(String folderPath, String itemName) throws IOException {
2445         String eventPath = URIUtil.encodePath(getFolderPath(folderPath) + '/' + convertItemNameToEML(itemName));
2446         // do not delete calendar messages, mark read and processed
2447         ArrayList<PropEntry> list = new ArrayList<>();
2448         list.add(Field.createDavProperty("processed", "true"));
2449         list.add(Field.createDavProperty("read", "1"));
2450         HttpProppatch patchMethod = new HttpProppatch(eventPath, list);
2451         try (CloseableHttpResponse response = httpClientAdapter.execute(patchMethod)) {
2452             LOGGER.debug("Processed " + itemName + " " + response.getStatusLine().getStatusCode());
2453         }
2454     }
2455 
2456     @Override
2457     public ItemResult internalCreateOrUpdateEvent(String folderPath, String itemName, String contentClass, String icsBody, String etag, String noneMatch) throws IOException {
2458         return new Event(getFolderPath(folderPath), itemName, contentClass, icsBody, etag, noneMatch).createOrUpdate();
2459     }
2460 
2461     /**
2462      * create a fake event to get VTIMEZONE body
2463      */
2464     @Override
2465     protected void loadVtimezone() {
2466         try {
2467             // create temporary folder
2468             String folderPath = getFolderPath("davmailtemp");
2469             createCalendarFolder(folderPath, null);
2470 
2471             String fakeEventUrl = null;
2472             if ("Exchange2003".equals(serverVersion)) {
2473                 HttpPost httpPost = new HttpPost(URIUtil.encodePath(folderPath));
2474                 ArrayList<NameValuePair> parameters = new ArrayList<>();
2475                 parameters.add(new BasicNameValuePair("Cmd", "saveappt"));
2476                 parameters.add(new BasicNameValuePair("FORMTYPE", "appointment"));
2477                 httpPost.setEntity(new UrlEncodedFormEntity(parameters, Consts.UTF_8));
2478 
2479                 try (CloseableHttpResponse response = httpClientAdapter.execute(httpPost)) {
2480                     // create fake event
2481                     int statusCode = response.getStatusLine().getStatusCode();
2482                     if (statusCode == HttpStatus.SC_OK) {
2483                         fakeEventUrl = StringUtil.getToken(new BasicResponseHandler().handleResponse(response), "<span id=\"itemHREF\">", "</span>");
2484                         if (fakeEventUrl != null) {
2485                             fakeEventUrl = URIUtil.decode(fakeEventUrl);
2486                         }
2487                     }
2488                 }
2489             }
2490             // failover for Exchange 2007, use PROPPATCH with forced timezone
2491             if (fakeEventUrl == null) {
2492                 ArrayList<PropEntry> propertyList = new ArrayList<>();
2493                 propertyList.add(Field.createDavProperty("contentclass", "urn:content-classes:appointment"));
2494                 propertyList.add(Field.createDavProperty("outlookmessageclass", "IPM.Appointment"));
2495                 propertyList.add(Field.createDavProperty("instancetype", "0"));
2496 
2497                 // get forced timezone id from settings
2498                 String timezoneId = Settings.getProperty("davmail.timezoneId");
2499                 if (timezoneId == null) {
2500                     // get timezoneid from OWA settings
2501                     timezoneId = getTimezoneIdFromExchange();
2502                 }
2503                 // without a timezoneId, use Exchange timezone
2504                 if (timezoneId != null) {
2505                     propertyList.add(Field.createDavProperty("timezoneid", timezoneId));
2506                 }
2507                 String patchMethodUrl = folderPath + '/' + UUID.randomUUID() + ".EML";
2508                 HttpProppatch patchMethod = new HttpProppatch(URIUtil.encodePath(patchMethodUrl), propertyList);
2509                 try (CloseableHttpResponse response = httpClientAdapter.execute(patchMethod)) {
2510                     int statusCode = response.getStatusLine().getStatusCode();
2511                     if (statusCode == HttpStatus.SC_MULTI_STATUS) {
2512                         fakeEventUrl = patchMethodUrl;
2513                     }
2514                 }
2515             }
2516             if (fakeEventUrl != null) {
2517                 // get fake event body
2518                 HttpGet httpGet = new HttpGet(URIUtil.encodePath(fakeEventUrl));
2519                 httpGet.setHeader("Translate", "f");
2520                 try (CloseableHttpResponse response = httpClientAdapter.execute(httpGet)) {
2521                     this.vTimezone = new VObject("BEGIN:VTIMEZONE" +
2522                             StringUtil.getToken(new BasicResponseHandler().handleResponse(response), "BEGIN:VTIMEZONE", "END:VTIMEZONE") +
2523                             "END:VTIMEZONE\r\n");
2524                 }
2525             }
2526 
2527             // delete temporary folder
2528             deleteFolder("davmailtemp");
2529         } catch (IOException e) {
2530             LOGGER.warn("Unable to get VTIMEZONE info: " + e, e);
2531         }
2532     }
2533 
2534     protected String getTimezoneIdFromExchange() {
2535         String timezoneId = null;
2536         String timezoneName = null;
2537         try {
2538             Set<String> attributes = new HashSet<>();
2539             attributes.add("roamingdictionary");
2540 
2541             MultiStatusResponse[] responses = searchItems("/users/" + getEmail() + "/NON_IPM_SUBTREE", attributes, isEqualTo("messageclass", "IPM.Configuration.OWA.UserOptions"), DavExchangeSession.FolderQueryTraversal.Deep, 1);
2542             if (responses.length == 1) {
2543                 byte[] roamingdictionary = getBinaryPropertyIfExists(responses[0].getProperties(HttpStatus.SC_OK), "roamingdictionary");
2544                 if (roamingdictionary != null) {
2545                     timezoneName = getTimezoneNameFromRoamingDictionary(roamingdictionary);
2546                     if (timezoneName != null) {
2547                         timezoneId = ResourceBundle.getBundle("timezoneids").getString(timezoneName);
2548                     }
2549                 }
2550             }
2551         } catch (MissingResourceException e) {
2552             LOGGER.warn("Unable to retrieve Exchange timezone id for name " + timezoneName);
2553         } catch (IOException e) {
2554             LOGGER.warn("Unable to retrieve Exchange timezone id: " + e.getMessage(), e);
2555         }
2556         return timezoneId;
2557     }
2558 
2559     protected String getTimezoneNameFromRoamingDictionary(byte[] roamingdictionary) {
2560         String timezoneName = null;
2561         XMLStreamReader reader;
2562         try {
2563             reader = XMLStreamUtil.createXMLStreamReader(roamingdictionary);
2564             while (reader.hasNext()) {
2565                 reader.next();
2566                 if (XMLStreamUtil.isStartTag(reader, "e")
2567                         && "18-timezone".equals(reader.getAttributeValue(null, "k"))) {
2568                     String value = reader.getAttributeValue(null, "v");
2569                     if (value != null && value.startsWith("18-")) {
2570                         timezoneName = value.substring(3);
2571                     }
2572                 }
2573             }
2574 
2575         } catch (XMLStreamException e) {
2576             LOGGER.error("Error while parsing RoamingDictionary: " + e, e);
2577         }
2578         return timezoneName;
2579     }
2580 
2581     @Override
2582     protected Contact buildContact(String folderPath, String itemName, Map<String, String> properties, String etag, String noneMatch) {
2583         return new Contact(getFolderPath(folderPath), itemName, properties, etag, noneMatch);
2584     }
2585 
2586     protected List<PropEntry> buildProperties(Map<String, String> properties) {
2587         ArrayList<PropEntry> list = new ArrayList<>();
2588         if (properties != null) {
2589             for (Map.Entry<String, String> entry : properties.entrySet()) {
2590                 if ("read".equals(entry.getKey())) {
2591                     list.add(Field.createDavProperty("read", entry.getValue()));
2592                 } else if ("junk".equals(entry.getKey())) {
2593                     list.add(Field.createDavProperty("junk", entry.getValue()));
2594                 } else if ("flagged".equals(entry.getKey())) {
2595                     list.add(Field.createDavProperty("flagStatus", entry.getValue()));
2596                 } else if ("answered".equals(entry.getKey())) {
2597                     list.add(Field.createDavProperty("lastVerbExecuted", entry.getValue()));
2598                     if ("102".equals(entry.getValue())) {
2599                         list.add(Field.createDavProperty("iconIndex", "261"));
2600                     }
2601                 } else if ("forwarded".equals(entry.getKey())) {
2602                     list.add(Field.createDavProperty("lastVerbExecuted", entry.getValue()));
2603                     if ("104".equals(entry.getValue())) {
2604                         list.add(Field.createDavProperty("iconIndex", "262"));
2605                     }
2606                 } else if ("bcc".equals(entry.getKey())) {
2607                     list.add(Field.createDavProperty("bcc", entry.getValue()));
2608                 } else if ("deleted".equals(entry.getKey())) {
2609                     list.add(Field.createDavProperty("deleted", entry.getValue()));
2610                 } else if ("datereceived".equals(entry.getKey())) {
2611                     list.add(Field.createDavProperty("datereceived", entry.getValue()));
2612                 } else if ("keywords".equals(entry.getKey())) {
2613                     list.add(Field.createDavProperty("keywords", entry.getValue()));
2614                 }
2615             }
2616         }
2617         return list;
2618     }
2619 
2620     /**
2621      * Create message in specified folder.
2622      * Will overwrite an existing message with same messageName in the same folder
2623      *
2624      * @param folderPath  Exchange folder path
2625      * @param messageName message name
2626      * @param properties  message properties (flags)
2627      * @param mimeMessage MIME message
2628      * @throws IOException when unable to create message
2629      */
2630     @Override
2631     public Message createMessage(String folderPath, String messageName, HashMap<String, String> properties, MimeMessage mimeMessage) throws IOException {
2632         String messageUrl = URIUtil.encodePathQuery(getFolderPath(folderPath) + '/' + messageName);
2633 
2634         List<PropEntry> davProperties = buildProperties(properties);
2635 
2636         if (properties != null && properties.containsKey("draft")) {
2637             // note: draft is readonly after create, create the message first with requested messageFlags
2638             davProperties.add(Field.createDavProperty("messageFlags", properties.get("draft")));
2639         }
2640         if (properties != null && properties.containsKey("mailOverrideFormat")) {
2641             davProperties.add(Field.createDavProperty("mailOverrideFormat", properties.get("mailOverrideFormat")));
2642         }
2643         if (properties != null && properties.containsKey("messageFormat")) {
2644             davProperties.add(Field.createDavProperty("messageFormat", properties.get("messageFormat")));
2645         }
2646         if (!davProperties.isEmpty()) {
2647             HttpProppatch httpProppatch = new HttpProppatch(messageUrl, davProperties);
2648             try (CloseableHttpResponse response = httpClientAdapter.execute(httpProppatch)) {
2649                 // update message with blind carbon copy and other flags
2650                 int statusCode = response.getStatusLine().getStatusCode();
2651                 if (statusCode != HttpStatus.SC_MULTI_STATUS) {
2652                     throw new DavMailException("EXCEPTION_UNABLE_TO_CREATE_MESSAGE", messageUrl, statusCode, ' ', response.getStatusLine().getReasonPhrase());
2653                 }
2654 
2655             }
2656         }
2657 
2658         // update message body
2659         HttpPut putmethod = new HttpPut(messageUrl);
2660         putmethod.setHeader("Translate", "f");
2661         putmethod.setHeader("Content-Type", "message/rfc822");
2662 
2663         try {
2664             // use same encoding as client socket reader
2665             ByteArrayOutputStream baos = new ByteArrayOutputStream();
2666             mimeMessage.writeTo(baos);
2667             baos.close();
2668             putmethod.setEntity(new ByteArrayEntity(baos.toByteArray()));
2669 
2670             int code;
2671             String reasonPhrase;
2672             try (CloseableHttpResponse response = httpClientAdapter.execute(putmethod)) {
2673                 code = response.getStatusLine().getStatusCode();
2674                 reasonPhrase = response.getStatusLine().getReasonPhrase();
2675             }
2676 
2677             // workaround for misconfigured Exchange server
2678             if (code == HttpStatus.SC_NOT_ACCEPTABLE) {
2679                 LOGGER.warn("Draft message creation failed, failover to property update. Note: attachments are lost");
2680 
2681                 ArrayList<PropEntry> propertyList = new ArrayList<>();
2682                 propertyList.add(Field.createDavProperty("to", mimeMessage.getHeader("to", ",")));
2683                 propertyList.add(Field.createDavProperty("cc", mimeMessage.getHeader("cc", ",")));
2684                 propertyList.add(Field.createDavProperty("message-id", mimeMessage.getHeader("message-id", ",")));
2685 
2686                 MimePart mimePart = mimeMessage;
2687                 if (mimeMessage.getContent() instanceof MimeMultipart) {
2688                     MimeMultipart multiPart = (MimeMultipart) mimeMessage.getContent();
2689                     for (int i = 0; i < multiPart.getCount(); i++) {
2690                         String contentType = multiPart.getBodyPart(i).getContentType();
2691                         if (contentType.startsWith("text/")) {
2692                             mimePart = (MimePart) multiPart.getBodyPart(i);
2693                             break;
2694                         }
2695                     }
2696                 }
2697 
2698                 String contentType = mimePart.getContentType();
2699 
2700                 if (contentType.startsWith("text/plain")) {
2701                     propertyList.add(Field.createDavProperty("description", (String) mimePart.getContent()));
2702                 } else if (contentType.startsWith("text/html")) {
2703                     propertyList.add(Field.createDavProperty("htmldescription", (String) mimePart.getContent()));
2704                 } else {
2705                     LOGGER.warn("Unsupported content type: " + contentType.replaceAll("[\n\r\t]", "_") + " message body will be empty");
2706                 }
2707 
2708                 propertyList.add(Field.createDavProperty("subject", mimeMessage.getHeader("subject", ",")));
2709                 HttpProppatch propPatchMethod = new HttpProppatch(messageUrl, propertyList);
2710                 try (CloseableHttpResponse response = httpClientAdapter.execute(propPatchMethod)) {
2711                     int patchStatus = response.getStatusLine().getStatusCode();
2712                     if (patchStatus == HttpStatus.SC_MULTI_STATUS) {
2713                         code = HttpStatus.SC_OK;
2714                     }
2715                 }
2716             }
2717 
2718 
2719             if (code != HttpStatus.SC_OK && code != HttpStatus.SC_CREATED) {
2720 
2721                 // first delete draft message
2722                 if (!davProperties.isEmpty()) {
2723                     HttpDelete httpDelete = new HttpDelete(messageUrl);
2724                     try (CloseableHttpResponse response = httpClientAdapter.execute(httpDelete)) {
2725                         int status = response.getStatusLine().getStatusCode();
2726                         if (status != HttpStatus.SC_OK && status != HttpStatus.SC_NOT_FOUND) {
2727                             throw HttpClientAdapter.buildHttpResponseException(httpDelete, response);
2728                         }
2729                     } catch (IOException e) {
2730                         LOGGER.warn("Unable to delete draft message");
2731                     }
2732                 }
2733                 if (code == HttpStatus.SC_INSUFFICIENT_STORAGE) {
2734                     throw new InsufficientStorageException(reasonPhrase);
2735                 } else {
2736                     throw new DavMailException("EXCEPTION_UNABLE_TO_CREATE_MESSAGE", messageUrl, code, ' ', reasonPhrase);
2737                 }
2738             }
2739         } catch (MessagingException e) {
2740             throw new IOException(e.getMessage());
2741         } finally {
2742             putmethod.releaseConnection();
2743         }
2744 
2745         try {
2746             // need to update bcc after put
2747             if (mimeMessage.getHeader("Bcc") != null) {
2748                 davProperties = new ArrayList<>();
2749                 davProperties.add(Field.createDavProperty("bcc", mimeMessage.getHeader("Bcc", ",")));
2750                 HttpProppatch httpProppatch = new HttpProppatch(messageUrl, davProperties);
2751                 // update message with blind carbon copy
2752                 try (CloseableHttpResponse response = httpClientAdapter.execute(httpProppatch)) {
2753                     int statusCode = response.getStatusLine().getStatusCode();
2754                     if (statusCode != HttpStatus.SC_MULTI_STATUS) {
2755                         throw new DavMailException("EXCEPTION_UNABLE_TO_CREATE_MESSAGE", messageUrl, statusCode, ' ', response.getStatusLine().getReasonPhrase());
2756                     }
2757                 }
2758             }
2759         } catch (MessagingException e) {
2760             throw new IOException(e.getMessage());
2761         }
2762         // fetch after create not implemented
2763         return null;
2764     }
2765 
2766     /**
2767      * @inheritDoc
2768      */
2769     @Override
2770     public void updateMessage(ExchangeSession.Message message, Map<String, String> properties) throws IOException {
2771         HttpProppatch patchMethod = new HttpProppatch(encodeAndFixUrl(message.permanentUrl), buildProperties(properties)) {
2772             @Override
2773             public MultiStatus getResponseBodyAsMultiStatus(HttpResponse response) {
2774                 // ignore response body, sometimes invalid with exchange mapi properties
2775                 throw new UnsupportedOperationException();
2776             }
2777         };
2778         try (CloseableHttpResponse response = httpClientAdapter.execute(patchMethod)) {
2779             int statusCode = response.getStatusLine().getStatusCode();
2780             if (statusCode != HttpStatus.SC_MULTI_STATUS) {
2781                 throw new DavMailException("EXCEPTION_UNABLE_TO_UPDATE_MESSAGE");
2782             }
2783         }
2784     }
2785 
2786     /**
2787      * @inheritDoc
2788      */
2789     @Override
2790     public void deleteMessage(ExchangeSession.Message message) throws IOException {
2791         LOGGER.debug("Delete " + message.permanentUrl + " (" + message.messageUrl + ')');
2792         HttpDelete httpDelete = new HttpDelete(encodeAndFixUrl(message.permanentUrl));
2793         try (CloseableHttpResponse response = httpClientAdapter.execute(httpDelete)) {
2794             int status = response.getStatusLine().getStatusCode();
2795             if (status != HttpStatus.SC_OK && status != HttpStatus.SC_NOT_FOUND) {
2796                 throw HttpClientAdapter.buildHttpResponseException(httpDelete, response);
2797             }
2798         }
2799     }
2800 
2801     /**
2802      * Send message.
2803      *
2804      * @param messageBody MIME message body
2805      * @throws IOException on error
2806      */
2807     public void sendMessage(byte[] messageBody) throws IOException {
2808         try {
2809             sendMessage(new MimeMessage(null, new SharedByteArrayInputStream(messageBody)));
2810         } catch (MessagingException e) {
2811             throw new IOException(e.getMessage());
2812         }
2813     }
2814 
2815     //protected static final long MAPI_SEND_NO_RICH_INFO = 0x00010000L;
2816     protected static final long ENCODING_PREFERENCE = 0x00020000L;
2817     protected static final long ENCODING_MIME = 0x00040000L;
2818     //protected static final long BODY_ENCODING_HTML = 0x00080000L;
2819     protected static final long BODY_ENCODING_TEXT_AND_HTML = 0x00100000L;
2820     //protected static final long MAC_ATTACH_ENCODING_UUENCODE = 0x00200000L;
2821     //protected static final long MAC_ATTACH_ENCODING_APPLESINGLE = 0x00400000L;
2822     //protected static final long MAC_ATTACH_ENCODING_APPLEDOUBLE = 0x00600000L;
2823     //protected static final long OOP_DONT_LOOKUP = 0x10000000L;
2824 
2825     @Override
2826     public void sendMessage(MimeMessage mimeMessage) throws IOException {
2827         try {
2828             // need to create draft first
2829             String itemName = UUID.randomUUID() + ".EML";
2830             HashMap<String, String> properties = new HashMap<>();
2831             properties.put("draft", "9");
2832             String contentType = mimeMessage.getContentType();
2833             if (contentType != null && contentType.startsWith("text/plain")) {
2834                 properties.put("messageFormat", "1");
2835             } else {
2836                 properties.put("mailOverrideFormat", String.valueOf(ENCODING_PREFERENCE | ENCODING_MIME | BODY_ENCODING_TEXT_AND_HTML));
2837                 properties.put("messageFormat", "2");
2838             }
2839             createMessage(DRAFTS, itemName, properties, mimeMessage);
2840             HttpMove httpMove = new HttpMove(URIUtil.encodePath(getFolderPath(DRAFTS + '/' + itemName)),
2841                     URIUtil.encodePath(getFolderPath(SENDMSG)), false);
2842             // set header if saveInSent is disabled
2843             if (!Settings.getBooleanProperty("davmail.smtpSaveInSent", true)) {
2844                 httpMove.setHeader("Saveinsent", "f");
2845             }
2846             moveItem(httpMove);
2847         } catch (MessagingException e) {
2848             throw new IOException(e.getMessage());
2849         }
2850     }
2851 
2852     // wrong hostname fix flag
2853     protected boolean restoreHostName;
2854 
2855     /**
2856      * @inheritDoc
2857      */
2858     @Override
2859     protected byte[] getContent(ExchangeSession.Message message) throws IOException {
2860         ByteArrayOutputStream baos = new ByteArrayOutputStream();
2861         InputStream contentInputStream;
2862         try {
2863             try {
2864                 try {
2865                     contentInputStream = getContentInputStream(message.messageUrl);
2866                 } catch (UnknownHostException e) {
2867                     // failover for misconfigured Exchange server, replace host name in url
2868                     restoreHostName = true;
2869                     contentInputStream = getContentInputStream(message.messageUrl);
2870                 }
2871             } catch (HttpNotFoundException e) {
2872                 LOGGER.debug("Message not found at: " + message.messageUrl + ", retrying with permanenturl");
2873                 contentInputStream = getContentInputStream(message.permanentUrl);
2874             }
2875 
2876             try {
2877                 IOUtil.write(contentInputStream, baos);
2878             } finally {
2879                 contentInputStream.close();
2880             }
2881 
2882         } catch (LoginTimeoutException | SocketException e) {
2883             // throw error on expired session
2884             LOGGER.warn(e.getMessage());
2885             throw e;
2886         } // throw error on broken connection
2887         catch (IOException e) {
2888             LOGGER.warn("Broken message at: " + message.messageUrl + " permanentUrl: " + message.permanentUrl + ", trying to rebuild from properties");
2889 
2890             try {
2891                 DavPropertyNameSet messageProperties = new DavPropertyNameSet();
2892                 messageProperties.add(Field.getPropertyName("contentclass"));
2893                 messageProperties.add(Field.getPropertyName("message-id"));
2894                 messageProperties.add(Field.getPropertyName("from"));
2895                 messageProperties.add(Field.getPropertyName("to"));
2896                 messageProperties.add(Field.getPropertyName("cc"));
2897                 messageProperties.add(Field.getPropertyName("subject"));
2898                 messageProperties.add(Field.getPropertyName("date"));
2899                 messageProperties.add(Field.getPropertyName("htmldescription"));
2900                 messageProperties.add(Field.getPropertyName("body"));
2901                 HttpPropfind httpPropfind = new HttpPropfind(encodeAndFixUrl(message.permanentUrl), messageProperties, 0);
2902                 try (CloseableHttpResponse response = httpClientAdapter.execute(httpPropfind)) {
2903                     MultiStatus responses = httpPropfind.getResponseBodyAsMultiStatus(response);
2904                     if (responses.getResponses().length > 0) {
2905                         MimeMessage mimeMessage = new MimeMessage((Session) null);
2906 
2907                         DavPropertySet properties = responses.getResponses()[0].getProperties(HttpStatus.SC_OK);
2908                         String propertyValue = getPropertyIfExists(properties, "contentclass");
2909                         if (propertyValue != null) {
2910                             mimeMessage.addHeader("Content-class", propertyValue);
2911                         }
2912                         propertyValue = getPropertyIfExists(properties, "date");
2913                         if (propertyValue != null) {
2914                             mimeMessage.setSentDate(parseDateFromExchange(propertyValue));
2915                         }
2916                         propertyValue = getPropertyIfExists(properties, "from");
2917                         if (propertyValue != null) {
2918                             mimeMessage.addHeader("From", propertyValue);
2919                         }
2920                         propertyValue = getPropertyIfExists(properties, "to");
2921                         if (propertyValue != null) {
2922                             mimeMessage.addHeader("To", propertyValue);
2923                         }
2924                         propertyValue = getPropertyIfExists(properties, "cc");
2925                         if (propertyValue != null) {
2926                             mimeMessage.addHeader("Cc", propertyValue);
2927                         }
2928                         propertyValue = getPropertyIfExists(properties, "subject");
2929                         if (propertyValue != null) {
2930                             mimeMessage.setSubject(propertyValue);
2931                         }
2932                         propertyValue = getPropertyIfExists(properties, "htmldescription");
2933                         if (propertyValue != null) {
2934                             mimeMessage.setContent(propertyValue, "text/html; charset=UTF-8");
2935                         } else {
2936                             propertyValue = getPropertyIfExists(properties, "body");
2937                             if (propertyValue != null) {
2938                                 mimeMessage.setText(propertyValue);
2939                             }
2940                         }
2941                         mimeMessage.writeTo(baos);
2942                     }
2943                 }
2944                 if (LOGGER.isDebugEnabled()) {
2945                     LOGGER.debug("Rebuilt message content: " + new String(baos.toByteArray(), StandardCharsets.UTF_8));
2946                 }
2947             } catch (IOException | DavException | MessagingException e2) {
2948                 LOGGER.warn(e2);
2949             }
2950             // other exception
2951             if (baos.size() == 0 && Settings.getBooleanProperty("davmail.deleteBroken")) {
2952                 LOGGER.warn("Deleting broken message at: " + message.messageUrl + " permanentUrl: " + message.permanentUrl);
2953                 try {
2954                     message.delete();
2955                 } catch (IOException ioe) {
2956                     LOGGER.warn("Unable to delete broken message at: " + message.permanentUrl);
2957                 }
2958                 throw e;
2959             }
2960         }
2961 
2962         return baos.toByteArray();
2963     }
2964 
2965     /**
2966      * sometimes permanenturis inside items are wrong after an Exchange version migration
2967      * need to restore base uri to actual public Exchange uri
2968      *
2969      * @param url input uri
2970      * @return fixed uri
2971      * @throws IOException on error
2972      */
2973     protected String encodeAndFixUrl(String url) throws IOException {
2974         String fixedurl = URIUtil.encodePath(url);
2975         // sometimes permanenturis inside items are wrong after an Exchange version migration
2976         // need to restore base uri to actual public Exchange uri
2977         if (restoreHostName && fixedurl.startsWith("http")) {
2978             try {
2979                 return URIUtils.rewriteURI(new java.net.URI(fixedurl), URIUtils.extractHost(httpClientAdapter.getUri())).toString();
2980             } catch (URISyntaxException e) {
2981                 throw new IOException(e.getMessage(), e);
2982             }
2983         }
2984         return fixedurl;
2985     }
2986 
2987     protected InputStream getContentInputStream(String url) throws IOException {
2988         String encodedUrl = encodeAndFixUrl(url);
2989 
2990         final HttpGet httpGet = new HttpGet(encodedUrl);
2991         httpGet.setHeader("Content-Type", "text/xml; charset=utf-8");
2992         httpGet.setHeader("Translate", "f");
2993         httpGet.setHeader("Accept-Encoding", "gzip");
2994 
2995         InputStream inputStream;
2996         try (CloseableHttpResponse response = httpClientAdapter.execute(httpGet)) {
2997             if (HttpClientAdapter.isGzipEncoded(response)) {
2998                 inputStream = new GZIPInputStream(response.getEntity().getContent());
2999             } else {
3000                 inputStream = response.getEntity().getContent();
3001             }
3002             inputStream = new FilterInputStream(inputStream) {
3003                 int totalCount;
3004                 int lastLogCount;
3005 
3006                 @Override
3007                 public int read(byte[] buffer, int offset, int length) throws IOException {
3008                     int count = super.read(buffer, offset, length);
3009                     totalCount += count;
3010                     if (totalCount - lastLogCount > 1024 * 128) {
3011                         DavGatewayTray.debug(new BundleMessage("LOG_DOWNLOAD_PROGRESS", String.valueOf(totalCount / 1024), httpGet.getURI()));
3012                         DavGatewayTray.switchIcon();
3013                         lastLogCount = totalCount;
3014                     }
3015                     return count;
3016                 }
3017 
3018                 @Override
3019                 public void close() throws IOException {
3020                     try {
3021                         super.close();
3022                     } finally {
3023                         httpGet.releaseConnection();
3024                     }
3025                 }
3026             };
3027 
3028         } catch (IOException e) {
3029             LOGGER.warn("Unable to retrieve message at: " + url);
3030             throw e;
3031         }
3032         return inputStream;
3033     }
3034 
3035     /**
3036      * @inheritDoc
3037      */
3038     @Override
3039     public void moveMessage(ExchangeSession.Message message, String targetFolder) throws IOException {
3040         try {
3041             moveMessage(message.permanentUrl, targetFolder);
3042         } catch (HttpNotFoundException e) {
3043             LOGGER.debug("404 not found at permanenturl: " + message.permanentUrl + ", retry with messageurl");
3044             moveMessage(message.messageUrl, targetFolder);
3045         }
3046     }
3047 
3048     protected void moveMessage(String sourceUrl, String targetFolder) throws IOException {
3049         String targetPath = URIUtil.encodePath(getFolderPath(targetFolder)) + '/' + UUID.randomUUID();
3050         HttpMove method = new HttpMove(URIUtil.encodePath(sourceUrl), targetPath, false);
3051         // allow rename if a message with the same name exists
3052         method.setHeader("Allow-Rename", "t");
3053         try (CloseableHttpResponse response = httpClientAdapter.execute(method)) {
3054             int statusCode = response.getStatusLine().getStatusCode();
3055             if (statusCode == HttpStatus.SC_PRECONDITION_FAILED ||
3056                     statusCode == HttpStatus.SC_CONFLICT) {
3057                 throw new DavMailException("EXCEPTION_UNABLE_TO_MOVE_MESSAGE");
3058             } else if (statusCode != HttpStatus.SC_CREATED) {
3059                 throw HttpClientAdapter.buildHttpResponseException(method, response);
3060             }
3061         } finally {
3062             method.releaseConnection();
3063         }
3064     }
3065 
3066     /**
3067      * @inheritDoc
3068      */
3069     @Override
3070     public void copyMessage(ExchangeSession.Message message, String targetFolder) throws IOException {
3071         try {
3072             copyMessage(message.permanentUrl, targetFolder);
3073         } catch (HttpNotFoundException e) {
3074             LOGGER.debug("404 not found at permanenturl: " + message.permanentUrl + ", retry with messageurl");
3075             copyMessage(message.messageUrl, targetFolder);
3076         }
3077     }
3078 
3079     protected void copyMessage(String sourceUrl, String targetFolder) throws IOException {
3080         String targetPath = URIUtil.encodePath(getFolderPath(targetFolder)) + '/' + UUID.randomUUID();
3081         HttpCopy httpCopy = new HttpCopy(URIUtil.encodePath(sourceUrl), targetPath, false, false);
3082         // allow rename if a message with the same name exists
3083         httpCopy.addHeader("Allow-Rename", "t");
3084         try (CloseableHttpResponse response = httpClientAdapter.execute(httpCopy)) {
3085             int statusCode = response.getStatusLine().getStatusCode();
3086             if (statusCode == HttpStatus.SC_PRECONDITION_FAILED) {
3087                 throw new DavMailException("EXCEPTION_UNABLE_TO_COPY_MESSAGE");
3088             } else if (statusCode != HttpStatus.SC_CREATED) {
3089                 throw HttpClientAdapter.buildHttpResponseException(httpCopy, response);
3090             }
3091         }
3092     }
3093 
3094     @Override
3095     protected void moveToTrash(ExchangeSession.Message message) throws IOException {
3096         String destination = URIUtil.encodePath(deleteditemsUrl) + '/' + UUID.randomUUID();
3097         LOGGER.debug("Deleting : " + message.permanentUrl + " to " + destination);
3098         HttpMove method = new HttpMove(encodeAndFixUrl(message.permanentUrl), destination, false);
3099         method.addHeader("Allow-rename", "t");
3100 
3101         try (CloseableHttpResponse response = httpClientAdapter.execute(method)) {
3102             int status = response.getStatusLine().getStatusCode();
3103             // do not throw error if already deleted
3104             if (status != HttpStatus.SC_CREATED && status != HttpStatus.SC_NOT_FOUND) {
3105                 throw HttpClientAdapter.buildHttpResponseException(method, response);
3106             }
3107             if (response.getFirstHeader("Location") != null) {
3108                 destination = method.getFirstHeader("Location").getValue();
3109             }
3110         }
3111 
3112         LOGGER.debug("Deleted to :" + destination);
3113     }
3114 
3115     protected String getItemProperty(String permanentUrl, String propertyName) throws IOException, DavException {
3116         String result = null;
3117         DavPropertyNameSet davPropertyNameSet = new DavPropertyNameSet();
3118         davPropertyNameSet.add(Field.getPropertyName(propertyName));
3119         HttpPropfind propFindMethod = new HttpPropfind(encodeAndFixUrl(permanentUrl), davPropertyNameSet, 0);
3120         MultiStatus responses;
3121         try (CloseableHttpResponse response = httpClientAdapter.execute(propFindMethod)) {
3122             responses = propFindMethod.getResponseBodyAsMultiStatus(response);
3123         } catch (UnknownHostException e) {
3124             // failover for misconfigured Exchange server, replace host name in url
3125             restoreHostName = true;
3126             propFindMethod = new HttpPropfind(encodeAndFixUrl(permanentUrl), davPropertyNameSet, 0);
3127             try (CloseableHttpResponse response = httpClientAdapter.execute(propFindMethod)) {
3128                 responses = propFindMethod.getResponseBodyAsMultiStatus(response);
3129             }
3130         }
3131 
3132         if (responses.getResponses().length > 0) {
3133             DavPropertySet properties = responses.getResponses()[0].getProperties(HttpStatus.SC_OK);
3134             result = getPropertyIfExists(properties, propertyName);
3135         }
3136 
3137         return result;
3138     }
3139 
3140     protected String convertDateFromExchange(String exchangeDateValue) throws DavMailException {
3141         String zuluDateValue = null;
3142         if (exchangeDateValue != null) {
3143             try {
3144                 zuluDateValue = getZuluDateFormat().format(getExchangeZuluDateFormatMillisecond().parse(exchangeDateValue));
3145             } catch (ParseException e) {
3146                 throw new DavMailException("EXCEPTION_INVALID_DATE", exchangeDateValue);
3147             }
3148         }
3149         return zuluDateValue;
3150     }
3151 
3152     protected static final Map<String, String> importanceToPriorityMap = new HashMap<>();
3153 
3154     static {
3155         importanceToPriorityMap.put("high", "1");
3156         importanceToPriorityMap.put("normal", "5");
3157         importanceToPriorityMap.put("low", "9");
3158     }
3159 
3160     protected static final Map<String, String> priorityToImportanceMap = new HashMap<>();
3161 
3162     static {
3163         priorityToImportanceMap.put("1", "high");
3164         priorityToImportanceMap.put("5", "normal");
3165         priorityToImportanceMap.put("9", "low");
3166     }
3167 
3168     protected String convertPriorityFromExchange(String exchangeImportanceValue) {
3169         String value = null;
3170         if (exchangeImportanceValue != null) {
3171             value = importanceToPriorityMap.get(exchangeImportanceValue);
3172         }
3173         return value;
3174     }
3175 
3176     protected String convertPriorityToExchange(String vTodoPriorityValue) {
3177         String value = null;
3178         if (vTodoPriorityValue != null) {
3179             value = priorityToImportanceMap.get(vTodoPriorityValue);
3180         }
3181         return value;
3182     }
3183 
3184 
3185     @Override
3186     public void close() {
3187         httpClientAdapter.close();
3188     }
3189 
3190     /**
3191      * Format date to exchange search format.
3192      *
3193      * @param date date object
3194      * @return formatted search date
3195      */
3196     @Override
3197     public String formatSearchDate(Date date) {
3198         SimpleDateFormat dateFormatter = new SimpleDateFormat(YYYY_MM_DD_HH_MM_SS, Locale.ENGLISH);
3199         dateFormatter.setTimeZone(GMT_TIMEZONE);
3200         return dateFormatter.format(date);
3201     }
3202 
3203     protected String convertTaskDateToZulu(String value) {
3204         String result = null;
3205         if (value != null && !value.isEmpty()) {
3206             try {
3207                 SimpleDateFormat parser = ExchangeSession.getExchangeDateFormat(value);
3208 
3209                 Calendar calendarValue = Calendar.getInstance(GMT_TIMEZONE);
3210                 calendarValue.setTime(parser.parse(value));
3211                 // zulu time: add 12 hours
3212                 if (value.length() == 16) {
3213                     calendarValue.add(Calendar.HOUR, 12);
3214                 }
3215                 calendarValue.set(Calendar.HOUR, 0);
3216                 calendarValue.set(Calendar.MINUTE, 0);
3217                 calendarValue.set(Calendar.SECOND, 0);
3218                 result = ExchangeSession.getExchangeZuluDateFormatMillisecond().format(calendarValue.getTime());
3219             } catch (ParseException e) {
3220                 LOGGER.warn("Invalid date: " + value);
3221             }
3222         }
3223 
3224         return result;
3225     }
3226 
3227     protected String convertDateFromExchangeToTaskDate(String exchangeDateValue) throws DavMailException {
3228         String result = null;
3229         if (exchangeDateValue != null) {
3230             try {
3231                 SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.ENGLISH);
3232                 dateFormat.setTimeZone(GMT_TIMEZONE);
3233                 result = dateFormat.format(getExchangeZuluDateFormatMillisecond().parse(exchangeDateValue));
3234             } catch (ParseException e) {
3235                 throw new DavMailException("EXCEPTION_INVALID_DATE", exchangeDateValue);
3236             }
3237         }
3238         return result;
3239     }
3240 
3241     protected Date parseDateFromExchange(String exchangeDateValue) throws DavMailException {
3242         Date result = null;
3243         if (exchangeDateValue != null) {
3244             try {
3245                 result = getExchangeZuluDateFormatMillisecond().parse(exchangeDateValue);
3246             } catch (ParseException e) {
3247                 throw new DavMailException("EXCEPTION_INVALID_DATE", exchangeDateValue);
3248             }
3249         }
3250         return result;
3251     }
3252 }