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