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