View Javadoc

1   /*
2    * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
3    * Copyright (C) 2009  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;
20  
21  import davmail.BundleMessage;
22  import davmail.Settings;
23  import davmail.exception.DavMailAuthenticationException;
24  import davmail.exception.DavMailException;
25  import davmail.exception.WebdavNotAvailableException;
26  import davmail.http.DavGatewayHttpClientFacade;
27  import davmail.http.DavGatewayOTPPrompt;
28  import davmail.ui.NotificationDialog;
29  import davmail.util.StringUtil;
30  import org.apache.commons.httpclient.*;
31  import org.apache.commons.httpclient.methods.GetMethod;
32  import org.apache.commons.httpclient.methods.PostMethod;
33  import org.apache.commons.httpclient.params.HttpClientParams;
34  import org.apache.commons.httpclient.util.URIUtil;
35  import org.apache.log4j.Logger;
36  import org.htmlcleaner.CommentNode;
37  import org.htmlcleaner.ContentNode;
38  import org.htmlcleaner.HtmlCleaner;
39  import org.htmlcleaner.TagNode;
40  
41  import javax.imageio.ImageIO;
42  import javax.mail.MessagingException;
43  import javax.mail.internet.*;
44  import javax.mail.util.SharedByteArrayInputStream;
45  import java.awt.image.BufferedImage;
46  import java.io.*;
47  import java.net.ConnectException;
48  import java.net.NoRouteToHostException;
49  import java.net.UnknownHostException;
50  import java.text.ParseException;
51  import java.text.SimpleDateFormat;
52  import java.util.*;
53  
54  /**
55   * Exchange session through Outlook Web Access (DAV)
56   */
57  public abstract class ExchangeSession {
58  
59      protected static final Logger LOGGER = Logger.getLogger("davmail.exchange.ExchangeSession");
60  
61      /**
62       * Reference GMT timezone to format dates
63       */
64      public static final SimpleTimeZone GMT_TIMEZONE = new SimpleTimeZone(0, "GMT");
65  
66      protected static final Set<String> USER_NAME_FIELDS = new HashSet<String>();
67  
68      static {
69          USER_NAME_FIELDS.add("username");
70          USER_NAME_FIELDS.add("txtUserName");
71          USER_NAME_FIELDS.add("userid");
72          USER_NAME_FIELDS.add("SafeWordUser");
73          USER_NAME_FIELDS.add("user_name");
74      }
75  
76      protected static final Set<String> PASSWORD_FIELDS = new HashSet<String>();
77  
78      static {
79          PASSWORD_FIELDS.add("password");
80          PASSWORD_FIELDS.add("txtUserPass");
81          PASSWORD_FIELDS.add("pw");
82          PASSWORD_FIELDS.add("basicPassword");
83      }
84  
85      protected static final Set<String> TOKEN_FIELDS = new HashSet<String>();
86  
87      static {
88          TOKEN_FIELDS.add("SafeWordPassword");
89          TOKEN_FIELDS.add("passcode");
90      }
91  
92      protected static final int FREE_BUSY_INTERVAL = 15;
93  
94      protected static final String PUBLIC_ROOT = "/public/";
95      protected static final String CALENDAR = "calendar";
96      protected static final String TASKS = "tasks";
97      /**
98       * Contacts folder logical name
99       */
100     public static final String CONTACTS = "contacts";
101     protected static final String ADDRESSBOOK = "addressbook";
102     protected static final String INBOX = "INBOX";
103     protected static final String LOWER_CASE_INBOX = "inbox";
104     protected static final String SENT = "Sent";
105     protected static final String SENDMSG = "##DavMailSubmissionURI##";
106     protected static final String DRAFTS = "Drafts";
107     protected static final String TRASH = "Trash";
108     protected static final String JUNK = "Junk";
109     protected static final String UNSENT = "Unsent Messages";
110 
111     static {
112         // Adjust Mime decoder settings
113         System.setProperty("mail.mime.ignoreunknownencoding", "true");
114         System.setProperty("mail.mime.decodetext.strict", "false");
115     }
116 
117     protected String publicFolderUrl;
118 
119     /**
120      * Base user mailboxes path (used to select folder)
121      */
122     protected String mailPath;
123     protected String rootPath;
124     protected String email;
125     protected String alias;
126     /**
127      * Lower case Caldav path to current user mailbox.
128      * /users/<i>email</i>
129      */
130     protected String currentMailboxPath;
131     protected final HttpClient httpClient;
132 
133     protected String userName;
134     /**
135      * A OTP pre-auth page may require a different username.
136      */
137     private String preAuthUsername;
138 
139     protected String serverVersion;
140 
141     protected static final String YYYY_MM_DD_HH_MM_SS = "yyyy/MM/dd HH:mm:ss";
142     private static final String YYYYMMDD_T_HHMMSS_Z = "yyyyMMdd'T'HHmmss'Z'";
143     protected static final String YYYY_MM_DD_T_HHMMSS_Z = "yyyy-MM-dd'T'HH:mm:ss'Z'";
144     private static final String YYYY_MM_DD = "yyyy-MM-dd";
145     private static final String YYYY_MM_DD_T_HHMMSS_SSS_Z = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
146 
147     /**
148      * Logon form user name fields.
149      */
150     private final List<String> userNameInputs = new ArrayList<String>();
151     /**
152      * Logon form password field, default is password.
153      */
154     private String passwordInput = null;
155     /**
156      * Tells if, during the login navigation, an OTP pre-auth page has been found.
157      */
158     private boolean otpPreAuthFound = false;
159     /**
160      * Lets the user try again a couple of times to enter the OTP pre-auth key before giving up.
161      */
162     private int otpPreAuthRetries = 0;
163     /**
164      * Maximum number of times the user can try to input again the OTP pre-auth key before giving up.
165      */
166     private static final int MAX_OTP_RETRIES = 3;
167 
168     /**
169      * Create an exchange session for the given URL.
170      * The session is established for given userName and password
171      *
172      * @param url      Exchange url
173      * @param userName user login name
174      * @param password user password
175      * @throws IOException on error
176      */
177     public ExchangeSession(String url, String userName, String password) throws IOException {
178         this.userName = userName;
179         try {
180             httpClient = DavGatewayHttpClientFacade.getInstance(url);
181             // set private connection pool
182             DavGatewayHttpClientFacade.createMultiThreadedHttpConnectionManager(httpClient);
183             boolean isBasicAuthentication = isBasicAuthentication(httpClient, url);
184             // clear cookies created by authentication test
185             httpClient.getState().clearCookies();
186 
187             // The user may have configured an OTP pre-auth username. It is processed
188             // so early because OTP pre-auth may disappear in the Exchange LAN and this
189             // helps the user to not change is account settings in mail client at each network change.
190             if (preAuthUsername == null) {
191                 // Searches for the delimiter in configured username for the pre-auth user. 
192                 // The double-quote is not allowed inside email addresses anyway.
193                 int doubleQuoteIndex = this.userName.indexOf('"');
194                 if (doubleQuoteIndex > 0) {
195                     preAuthUsername = this.userName.substring(0, doubleQuoteIndex);
196                     this.userName = this.userName.substring(doubleQuoteIndex + 1);
197                 } else {
198                     // No doublequote: the pre-auth user is the full username, or it is not used at all.
199                     preAuthUsername = this.userName;
200                 }
201             }
202 
203             DavGatewayHttpClientFacade.setCredentials(httpClient, userName, password);
204 
205             // get webmail root url
206             // providing credentials
207             // manually follow redirect
208             HttpMethod method = DavGatewayHttpClientFacade.executeFollowRedirects(httpClient, url);
209 
210             if (!this.isAuthenticated(method)) {
211                 if (isBasicAuthentication) {
212                     int status = method.getStatusCode();
213 
214                     if (status == HttpStatus.SC_UNAUTHORIZED) {
215                         method.releaseConnection();
216                         throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED");
217                     } else if (status != HttpStatus.SC_OK) {
218                         method.releaseConnection();
219                         throw DavGatewayHttpClientFacade.buildHttpException(method);
220                     }
221                     // workaround for basic authentication on /exchange and form based authentication at /owa
222                     if ("/owa/auth/logon.aspx".equals(method.getPath())) {
223                         method = formLogin(httpClient, method, userName, password);
224                     }
225                 } else {
226                     method = formLogin(httpClient, method, userName, password);
227                 }
228             }
229 
230             // avoid 401 roundtrips, only if NTLM is disabled and basic authentication enabled
231             if (isBasicAuthentication && !DavGatewayHttpClientFacade.hasNTLMorNegotiate(httpClient)) {
232                 httpClient.getParams().setParameter(HttpClientParams.PREEMPTIVE_AUTHENTICATION, true);
233             }
234 
235             buildSessionInfo(method);
236 
237         } catch (DavMailAuthenticationException exc) {
238             LOGGER.error(exc.getMessage());
239             throw exc;
240         } catch (ConnectException exc) {
241             BundleMessage message = new BundleMessage("EXCEPTION_CONNECT", exc.getClass().getName(), exc.getMessage());
242             ExchangeSession.LOGGER.error(message);
243             throw new DavMailException("EXCEPTION_DAVMAIL_CONFIGURATION", message);
244         } catch (UnknownHostException exc) {
245             BundleMessage message = new BundleMessage("EXCEPTION_CONNECT", exc.getClass().getName(), exc.getMessage());
246             ExchangeSession.LOGGER.error(message);
247             throw new DavMailException("EXCEPTION_DAVMAIL_CONFIGURATION", message);
248         } catch (WebdavNotAvailableException exc) {
249             throw exc;
250         } catch (IOException exc) {
251             LOGGER.error(BundleMessage.formatLog("EXCEPTION_EXCHANGE_LOGIN_FAILED", exc));
252             throw new DavMailException("EXCEPTION_EXCHANGE_LOGIN_FAILED", exc);
253         }
254         LOGGER.debug("Session " + this + " created");
255     }
256 
257     /**
258      * Format date to exchange search format.
259      *
260      * @param date date object
261      * @return formatted search date
262      */
263     public abstract String formatSearchDate(Date date);
264 
265     /**
266      * Return standard zulu date formatter.
267      *
268      * @return zulu date formatter
269      */
270     public static SimpleDateFormat getZuluDateFormat() {
271         SimpleDateFormat dateFormat = new SimpleDateFormat(YYYYMMDD_T_HHMMSS_Z, Locale.ENGLISH);
272         dateFormat.setTimeZone(GMT_TIMEZONE);
273         return dateFormat;
274     }
275 
276     protected static SimpleDateFormat getVcardBdayFormat() {
277         SimpleDateFormat dateFormat = new SimpleDateFormat(YYYY_MM_DD, Locale.ENGLISH);
278         dateFormat.setTimeZone(GMT_TIMEZONE);
279         return dateFormat;
280     }
281 
282     protected static SimpleDateFormat getExchangeZuluDateFormat() {
283         SimpleDateFormat dateFormat = new SimpleDateFormat(YYYY_MM_DD_T_HHMMSS_Z, Locale.ENGLISH);
284         dateFormat.setTimeZone(GMT_TIMEZONE);
285         return dateFormat;
286     }
287 
288     protected static SimpleDateFormat getExchangeZuluDateFormatMillisecond() {
289         SimpleDateFormat dateFormat = new SimpleDateFormat(YYYY_MM_DD_T_HHMMSS_SSS_Z, Locale.ENGLISH);
290         dateFormat.setTimeZone(GMT_TIMEZONE);
291         return dateFormat;
292     }
293 
294     protected static Date parseDate(String dateString) throws ParseException {
295         SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd");
296         dateFormat.setTimeZone(GMT_TIMEZONE);
297         return dateFormat.parse(dateString);
298     }
299 
300 
301     /**
302      * Test if the session expired.
303      *
304      * @return true this session expired
305      * @throws NoRouteToHostException on error
306      * @throws UnknownHostException   on error
307      */
308     public boolean isExpired() throws NoRouteToHostException, UnknownHostException {
309         boolean isExpired = false;
310         try {
311             getFolder("");
312         } catch (UnknownHostException exc) {
313             throw exc;
314         } catch (NoRouteToHostException exc) {
315             throw exc;
316         } catch (IOException e) {
317             isExpired = true;
318         }
319 
320         return isExpired;
321     }
322 
323     /**
324      * Test authentication mode : form based or basic.
325      *
326      * @param url        exchange base URL
327      * @param httpClient httpClient instance
328      * @return true if basic authentication detected
329      */
330     protected boolean isBasicAuthentication(HttpClient httpClient, String url) {
331         return DavGatewayHttpClientFacade.getHttpStatus(httpClient, url) == HttpStatus.SC_UNAUTHORIZED;
332     }
333 
334     protected String getAbsoluteUri(HttpMethod method, String path) throws URIException {
335         URI uri = method.getURI();
336         if (path != null) {
337             // reset query string
338             uri.setQuery(null);
339             if (path.startsWith("/")) {
340                 // path is absolute, replace method path
341                 uri.setPath(path);
342             } else if (path.startsWith("http://") || path.startsWith("https://")) {
343                 return path;
344             } else {
345                 // relative path, build new path
346                 String currentPath = method.getPath();
347                 int end = currentPath.lastIndexOf('/');
348                 if (end >= 0) {
349                     uri.setPath(currentPath.substring(0, end + 1) + path);
350                 } else {
351                     throw new URIException(uri.getURI());
352                 }
353             }
354         }
355         return uri.getURI();
356     }
357 
358     protected String getScriptBasedFormURL(HttpMethod initmethod, String pathQuery) throws URIException {
359         URI initmethodURI = initmethod.getURI();
360         int queryIndex = pathQuery.indexOf('?');
361         if (queryIndex >= 0) {
362             if (queryIndex > 0) {
363                 // update path
364                 String newPath = pathQuery.substring(0, queryIndex);
365                 if (newPath.startsWith("/")) {
366                     // absolute path
367                     initmethodURI.setPath(newPath);
368                 } else {
369                     String currentPath = initmethodURI.getPath();
370                     int folderIndex = currentPath.lastIndexOf('/');
371                     if (folderIndex >= 0) {
372                         // replace relative path
373                         initmethodURI.setPath(currentPath.substring(0, folderIndex + 1) + newPath);
374                     } else {
375                         // should not happen
376                         initmethodURI.setPath('/' + newPath);
377                     }
378                 }
379             }
380             initmethodURI.setQuery(pathQuery.substring(queryIndex + 1));
381         }
382         return initmethodURI.getURI();
383     }
384 
385     /**
386      * Try to find logon method path from logon form body.
387      *
388      * @param httpClient httpClient instance
389      * @param initmethod form body http method
390      * @return logon method
391      * @throws IOException on error
392      */
393     protected HttpMethod buildLogonMethod(HttpClient httpClient, HttpMethod initmethod) throws IOException {
394 
395         HttpMethod logonMethod = null;
396 
397         // create an instance of HtmlCleaner
398         HtmlCleaner cleaner = new HtmlCleaner();
399 
400         // A OTP token authentication form in a previous page could have username fields with different names
401         userNameInputs.clear();
402 
403         try {
404             TagNode node = cleaner.clean(initmethod.getResponseBodyAsStream());
405             List forms = node.getElementListByName("form", true);
406             TagNode logonForm = null;
407             // select form
408             if (forms.size() == 1) {
409                 logonForm = (TagNode) forms.get(0);
410             } else if (forms.size() > 1) {
411                 for (Object form : forms) {
412                     if ("logonForm".equals(((TagNode) form).getAttributeByName("name"))) {
413                         logonForm = ((TagNode) form);
414                     }
415                 }
416             }
417             if (logonForm != null) {
418                 String logonMethodPath = logonForm.getAttributeByName("action");
419 
420                 // workaround for broken form with empty action
421                 if (logonMethodPath != null && logonMethodPath.length() == 0) {
422                     logonMethodPath = "/owa/auth.owa";
423                 }
424 
425                 logonMethod = new PostMethod(getAbsoluteUri(initmethod, logonMethodPath));
426 
427                 // retrieve lost inputs attached to body
428                 List inputList = node.getElementListByName("input", true);
429 
430                 for (Object input : inputList) {
431                     String type = ((TagNode) input).getAttributeByName("type");
432                     String name = ((TagNode) input).getAttributeByName("name");
433                     String value = ((TagNode) input).getAttributeByName("value");
434                     if ("hidden".equalsIgnoreCase(type) && name != null && value != null) {
435                         ((PostMethod) logonMethod).addParameter(name, value);
436                     }
437                     // custom login form
438                     if (USER_NAME_FIELDS.contains(name)) {
439                         userNameInputs.add(name);
440                     } else if (PASSWORD_FIELDS.contains(name)) {
441                         passwordInput = name;
442                     } else if ("addr".equals(name)) {
443                         // this is not a logon form but a redirect form
444                         HttpMethod newInitMethod = DavGatewayHttpClientFacade.executeFollowRedirects(httpClient, logonMethod);
445                         logonMethod = buildLogonMethod(httpClient, newInitMethod);
446                     } else if (TOKEN_FIELDS.contains(name)) {
447                         // one time password, ask it to the user
448                         ((PostMethod) logonMethod).addParameter(name, DavGatewayOTPPrompt.getOneTimePassword());
449                     } else if ("otc".equals(name)) {
450                         // captcha image, get image and ask user
451                         String pinsafeUser = getAliasFromLogin();
452                         if (pinsafeUser == null) {
453                             pinsafeUser = userName;
454                         }
455                         GetMethod getMethod = new GetMethod("/PINsafeISAFilter.dll?username=" + pinsafeUser);
456                         try {
457                             int status = httpClient.executeMethod(getMethod);
458                             if (status != HttpStatus.SC_OK) {
459                                 throw DavGatewayHttpClientFacade.buildHttpException(getMethod);
460                             }
461                             BufferedImage captchaImage = ImageIO.read(getMethod.getResponseBodyAsStream());
462                             ((PostMethod) logonMethod).addParameter(name, DavGatewayOTPPrompt.getCaptchaValue(captchaImage));
463 
464                         } finally {
465                             getMethod.releaseConnection();
466                         }
467                     }
468                 }
469             } else {
470                 List frameList = node.getElementListByName("frame", true);
471                 if (frameList.size() == 1) {
472                     String src = ((TagNode) frameList.get(0)).getAttributeByName("src");
473                     if (src != null) {
474                         LOGGER.debug("Frames detected in form page, try frame content");
475                         initmethod.releaseConnection();
476                         HttpMethod newInitMethod = DavGatewayHttpClientFacade.executeFollowRedirects(httpClient, src);
477                         logonMethod = buildLogonMethod(httpClient, newInitMethod);
478                     }
479                 } else {
480                     // another failover for script based logon forms (Exchange 2007)
481                     List scriptList = node.getElementListByName("script", true);
482                     for (Object script : scriptList) {
483                         List contents = ((TagNode) script).getChildren();
484                         for (Object content : contents) {
485                             if (content instanceof CommentNode) {
486                                 String scriptValue = ((CommentNode) content).getCommentedContent();
487                                 String sUrl = StringUtil.getToken(scriptValue, "var a_sUrl = \"", "\"");
488                                 String sLgn = StringUtil.getToken(scriptValue, "var a_sLgnQS = \"", "\"");
489                                 if (sLgn == null) {
490                                     sLgn = StringUtil.getToken(scriptValue, "var a_sLgn = \"", "\"");
491                                 }
492                                 if (sUrl != null && sLgn != null) {
493                                     String src = getScriptBasedFormURL(initmethod, sLgn + sUrl);
494                                     LOGGER.debug("Detected script based logon, redirect to form at " + src);
495                                     HttpMethod newInitMethod = DavGatewayHttpClientFacade.executeFollowRedirects(httpClient, src);
496                                     logonMethod = buildLogonMethod(httpClient, newInitMethod);
497                                 }
498 
499                             } else if (content instanceof ContentNode) {
500                                 // Microsoft Forefront Unified Access Gateway redirect
501                                 String scriptValue = ((ContentNode) content).getContent().toString();
502                                 String location = StringUtil.getToken(scriptValue, "window.location.replace(\"", "\"");
503                                 if (location != null) {
504                                     LOGGER.debug("Post logon redirect to: " + location);
505                                     logonMethod = DavGatewayHttpClientFacade.executeFollowRedirects(httpClient, location);
506                                 }
507                             }
508                         }
509                     }
510                 }
511             }
512         } catch (IOException e) {
513             LOGGER.error("Error parsing login form at " + initmethod.getURI());
514         } finally {
515             initmethod.releaseConnection();
516         }
517 
518         return logonMethod;
519     }
520 
521     protected HttpMethod postLogonMethod(HttpClient httpClient, HttpMethod logonMethod, String userName, String password) throws IOException {
522 
523         setAuthFormFields(logonMethod, httpClient, password);
524 
525         // add exchange 2010 PBack cookie in compatibility mode
526         httpClient.getState().addCookie(new Cookie(httpClient.getHostConfiguration().getHost(), "PBack", "0", "/", null, false));
527 
528         logonMethod = DavGatewayHttpClientFacade.executeFollowRedirects(httpClient, logonMethod);
529 
530         // test form based authentication
531         checkFormLoginQueryString(logonMethod);
532 
533         // workaround for post logon script redirect
534         if (!isAuthenticated(logonMethod)) {
535             // try to get new method from script based redirection
536             logonMethod = buildLogonMethod(httpClient, logonMethod);
537 
538             if (logonMethod != null) {
539                 if (otpPreAuthFound && otpPreAuthRetries < MAX_OTP_RETRIES) {
540                     // A OTP pre-auth page has been found, it is needed to restart the login process.
541                     // This applies to both the case the user entered a good OTP code (the usual login process
542                     // takes place) and the case the user entered a wrong OTP code (another code will be asked to him).
543                     // The user has up to MAX_OTP_RETRIES chances to input a valid OTP key.
544                     return postLogonMethod(httpClient, logonMethod, userName, password);
545                 }
546 
547                 // if logonMethod is not null, try to follow redirection
548                 logonMethod = DavGatewayHttpClientFacade.executeFollowRedirects(httpClient, logonMethod);
549                 checkFormLoginQueryString(logonMethod);
550                 // also check cookies
551                 if (!isAuthenticated(logonMethod)) {
552                     throwAuthenticationFailed();
553                 }
554             } else {
555                 // authentication failed
556                 throwAuthenticationFailed();
557             }
558         }
559 
560         // check for language selection form
561         if (logonMethod != null && "/owa/languageselection.aspx".equals(logonMethod.getPath())) {
562             // need to submit form
563             logonMethod = submitLanguageSelectionForm(logonMethod);
564         }
565         return logonMethod;
566     }
567 
568     protected void setAuthFormFields(HttpMethod logonMethod, HttpClient httpClient, String password) throws IllegalArgumentException {
569         String userNameInput;
570         if (userNameInputs.size() == 2) {
571             String userid;
572             // multiple username fields, split userid|username on |
573             int pipeIndex = userName.indexOf('|');
574             if (pipeIndex < 0) {
575                 LOGGER.debug("Multiple user fields detected, please use userid|username as user name in client, except when userid is username");
576                 userid = userName;
577             } else {
578                 userid = userName.substring(0, pipeIndex);
579                 userName = userName.substring(pipeIndex + 1);
580                 // adjust credentials
581                 DavGatewayHttpClientFacade.setCredentials(httpClient, userName, password);
582             }
583             ((PostMethod) logonMethod).removeParameter("userid");
584             ((PostMethod) logonMethod).addParameter("userid", userid);
585 
586             userNameInput = "username";
587         } else if (userNameInputs.size() == 1) {
588             // simple username field
589             userNameInput = userNameInputs.get(0);
590         } else {
591             // should not happen
592             userNameInput = "username";
593         }
594         // make sure username and password fields are empty
595         ((PostMethod) logonMethod).removeParameter(userNameInput);
596         if (passwordInput != null) {
597             ((PostMethod) logonMethod).removeParameter(passwordInput);
598         }
599         ((PostMethod) logonMethod).removeParameter("trusted");
600         ((PostMethod) logonMethod).removeParameter("flags");
601 
602         if (passwordInput == null) {
603             // This is a OTP pre-auth page. A different username may be required.
604             otpPreAuthFound = true;
605             otpPreAuthRetries++;
606             ((PostMethod) logonMethod).addParameter(userNameInput, preAuthUsername);
607         } else {
608             otpPreAuthFound = false;
609             otpPreAuthRetries = 0;
610             // This is a regular Exchange login page
611             ((PostMethod) logonMethod).addParameter(userNameInput, userName);
612             ((PostMethod) logonMethod).addParameter(passwordInput, password);
613             ((PostMethod) logonMethod).addParameter("trusted", "4");
614             ((PostMethod) logonMethod).addParameter("flags", "4");
615         }
616     }
617 
618     protected HttpMethod formLogin(HttpClient httpClient, HttpMethod initmethod, String userName, String password) throws IOException {
619         LOGGER.debug("Form based authentication detected");
620 
621         HttpMethod logonMethod = buildLogonMethod(httpClient, initmethod);
622         if (logonMethod == null) {
623             LOGGER.debug("Authentication form not found at " + initmethod.getURI() + ", trying default url");
624             logonMethod = new PostMethod("/owa/auth/owaauth.dll");
625         }
626         logonMethod = postLogonMethod(httpClient, logonMethod, userName, password);
627 
628         return logonMethod;
629     }
630 
631     protected HttpMethod submitLanguageSelectionForm(HttpMethod logonMethod) throws IOException {
632         PostMethod postLanguageFormMethod;
633         // create an instance of HtmlCleaner
634         HtmlCleaner cleaner = new HtmlCleaner();
635 
636         try {
637             TagNode node = cleaner.clean(logonMethod.getResponseBodyAsStream());
638             List forms = node.getElementListByName("form", true);
639             TagNode languageForm;
640             // select form
641             if (forms.size() == 1) {
642                 languageForm = (TagNode) forms.get(0);
643             } else {
644                 throw new IOException("Form not found");
645             }
646             String languageMethodPath = languageForm.getAttributeByName("action");
647 
648             postLanguageFormMethod = new PostMethod(getAbsoluteUri(logonMethod, languageMethodPath));
649 
650             List inputList = languageForm.getElementListByName("input", true);
651             for (Object input : inputList) {
652                 String name = ((TagNode) input).getAttributeByName("name");
653                 String value = ((TagNode) input).getAttributeByName("value");
654                 if (name != null && value != null) {
655                     postLanguageFormMethod.addParameter(name, value);
656                 }
657             }
658             List selectList = languageForm.getElementListByName("select", true);
659             for (Object select : selectList) {
660                 String name = ((TagNode) select).getAttributeByName("name");
661                 List optionList = ((TagNode) select).getElementListByName("option", true);
662                 String value = null;
663                 for (Object option : optionList) {
664                     if (((TagNode) option).getAttributeByName("selected") != null) {
665                         value = ((TagNode) option).getAttributeByName("value");
666                         break;
667                     }
668                 }
669                 if (name != null && value != null) {
670                     postLanguageFormMethod.addParameter(name, value);
671                 }
672             }
673         } catch (IOException e) {
674             String errorMessage = "Error parsing language selection form at " + logonMethod.getURI();
675             LOGGER.error(errorMessage);
676             throw new IOException(errorMessage);
677         } finally {
678             logonMethod.releaseConnection();
679         }
680 
681         return DavGatewayHttpClientFacade.executeFollowRedirects(httpClient, postLanguageFormMethod);
682     }
683 
684     /**
685      * Look for session cookies.
686      *
687      * @return true if session cookies are available
688      */
689     protected boolean isAuthenticated(HttpMethod method) {
690         boolean authenticated = false;
691         if (method.getStatusCode() == HttpStatus.SC_OK
692                 && "/ews/services.wsdl".equalsIgnoreCase(method.getPath())) {
693             // direct EWS access returned wsdl
694             authenticated = true;
695         } else {
696             // check cookies
697             for (Cookie cookie : httpClient.getState().getCookies()) {
698                 // Exchange 2003 cookies
699                 if (cookie.getName().startsWith("cadata") || "sessionid".equals(cookie.getName())
700                         // Exchange 2007 cookie
701                         || "UserContext".equals(cookie.getName())
702                         // Direct EWS access
703                         || "exchangecookie".equals(cookie.getName())
704                         ) {
705                     authenticated = true;
706                     break;
707                 }
708             }
709         }
710         return authenticated;
711     }
712 
713     protected void checkFormLoginQueryString(HttpMethod logonMethod) throws DavMailAuthenticationException {
714         String queryString = logonMethod.getQueryString();
715         if (queryString != null && (queryString.contains("reason=2") || queryString.contains("reason=4"))) {
716             logonMethod.releaseConnection();
717             throwAuthenticationFailed();
718         }
719     }
720 
721     protected void throwAuthenticationFailed() throws DavMailAuthenticationException {
722         if (this.userName != null && this.userName.contains("\\")) {
723             throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED");
724         } else {
725             throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED_RETRY");
726         }
727     }
728 
729     protected abstract void buildSessionInfo(HttpMethod method) throws DavMailException;
730 
731     /**
732      * Create message in specified folder.
733      * Will overwrite an existing message with same subject in the same folder
734      *
735      * @param folderPath  Exchange folder path
736      * @param messageName message name
737      * @param properties  message properties (flags)
738      * @param mimeMessage MIME message
739      * @throws IOException when unable to create message
740      */
741     public abstract void createMessage(String folderPath, String messageName, HashMap<String, String> properties, MimeMessage mimeMessage) throws IOException;
742 
743     /**
744      * Update given properties on message.
745      *
746      * @param message    Exchange message
747      * @param properties Webdav properties map
748      * @throws IOException on error
749      */
750     public abstract void updateMessage(Message message, Map<String, String> properties) throws IOException;
751 
752 
753     /**
754      * Delete Exchange message.
755      *
756      * @param message Exchange message
757      * @throws IOException on error
758      */
759     public abstract void deleteMessage(Message message) throws IOException;
760 
761     /**
762      * Get raw MIME message content
763      *
764      * @param message Exchange message
765      * @return message body
766      * @throws IOException on error
767      */
768     protected abstract byte[] getContent(Message message) throws IOException;
769 
770     protected static final Set<String> POP_MESSAGE_ATTRIBUTES = new HashSet<String>();
771 
772     static {
773         POP_MESSAGE_ATTRIBUTES.add("uid");
774         POP_MESSAGE_ATTRIBUTES.add("imapUid");
775         POP_MESSAGE_ATTRIBUTES.add("messageSize");
776     }
777 
778     /**
779      * Return folder message list with id and size only (for POP3 listener).
780      *
781      * @param folderName Exchange folder name
782      * @return folder message list
783      * @throws IOException on error
784      */
785     public MessageList getAllMessageUidAndSize(String folderName) throws IOException {
786         return searchMessages(folderName, POP_MESSAGE_ATTRIBUTES, null);
787     }
788 
789     protected static final Set<String> IMAP_MESSAGE_ATTRIBUTES = new HashSet<String>();
790 
791     static {
792         IMAP_MESSAGE_ATTRIBUTES.add("permanenturl");
793         IMAP_MESSAGE_ATTRIBUTES.add("urlcompname");
794         IMAP_MESSAGE_ATTRIBUTES.add("uid");
795         IMAP_MESSAGE_ATTRIBUTES.add("messageSize");
796         IMAP_MESSAGE_ATTRIBUTES.add("imapUid");
797         IMAP_MESSAGE_ATTRIBUTES.add("junk");
798         IMAP_MESSAGE_ATTRIBUTES.add("flagStatus");
799         IMAP_MESSAGE_ATTRIBUTES.add("messageFlags");
800         IMAP_MESSAGE_ATTRIBUTES.add("lastVerbExecuted");
801         IMAP_MESSAGE_ATTRIBUTES.add("read");
802         IMAP_MESSAGE_ATTRIBUTES.add("deleted");
803         IMAP_MESSAGE_ATTRIBUTES.add("date");
804         IMAP_MESSAGE_ATTRIBUTES.add("lastmodified");
805         // OSX IMAP requests content-class
806         IMAP_MESSAGE_ATTRIBUTES.add("contentclass");
807         IMAP_MESSAGE_ATTRIBUTES.add("keywords");
808     }
809 
810     protected static final Set<String> UID_MESSAGE_ATTRIBUTES = new HashSet<String>();
811 
812     static {
813         UID_MESSAGE_ATTRIBUTES.add("uid");
814     }
815 
816     /**
817      * Get all folder messages.
818      *
819      * @param folderPath Exchange folder name
820      * @return message list
821      * @throws IOException on error
822      */
823     public MessageList searchMessages(String folderPath) throws IOException {
824         return searchMessages(folderPath, IMAP_MESSAGE_ATTRIBUTES, null);
825     }
826 
827     /**
828      * Search folder for messages matching conditions, with attributes needed by IMAP listener.
829      *
830      * @param folderName Exchange folder name
831      * @param condition  search filter
832      * @return message list
833      * @throws IOException on error
834      */
835     public MessageList searchMessages(String folderName, Condition condition) throws IOException {
836         return searchMessages(folderName, IMAP_MESSAGE_ATTRIBUTES, condition);
837     }
838 
839     /**
840      * Search folder for messages matching conditions, with given attributes.
841      *
842      * @param folderName Exchange folder name
843      * @param attributes requested Webdav attributes
844      * @param condition  search filter
845      * @return message list
846      * @throws IOException on error
847      */
848     public abstract MessageList searchMessages(String folderName, Set<String> attributes, Condition condition) throws IOException;
849 
850     /**
851      * Get server version (Exchange2003, Exchange2007 or Exchange2010)
852      *
853      * @return server version
854      */
855     public String getServerVersion() {
856         return serverVersion;
857     }
858 
859     @SuppressWarnings({"JavaDoc"})
860     public enum Operator {
861         Or, And, Not, IsEqualTo,
862         IsGreaterThan, IsGreaterThanOrEqualTo,
863         IsLessThan, IsLessThanOrEqualTo,
864         IsNull, IsTrue, IsFalse,
865         Like, StartsWith, Contains
866     }
867 
868     /**
869      * Exchange search filter.
870      */
871     public static interface Condition {
872         /**
873          * Append condition to buffer.
874          *
875          * @param buffer search filter buffer
876          */
877         void appendTo(StringBuilder buffer);
878 
879         /**
880          * True if condition is empty.
881          *
882          * @return true if condition is empty
883          */
884         boolean isEmpty();
885 
886         /**
887          * Test if the contact matches current condition.
888          *
889          * @param contact Exchange Contact
890          * @return true if contact matches condition
891          */
892         boolean isMatch(ExchangeSession.Contact contact);
893     }
894 
895     /**
896      * Attribute condition.
897      */
898     public abstract static class AttributeCondition implements Condition {
899         protected final String attributeName;
900         protected final Operator operator;
901         protected final String value;
902 
903         protected AttributeCondition(String attributeName, Operator operator, String value) {
904             this.attributeName = attributeName;
905             this.operator = operator;
906             this.value = value;
907         }
908 
909         public boolean isEmpty() {
910             return false;
911         }
912 
913         /**
914          * Get attribute name.
915          *
916          * @return attribute name
917          */
918         public String getAttributeName() {
919             return attributeName;
920         }
921 
922         /**
923          * Condition value.
924          *
925          * @return value
926          */
927         public String getValue() {
928             return value;
929         }
930 
931     }
932 
933     /**
934      * Multiple condition.
935      */
936     public abstract static class MultiCondition implements Condition {
937         protected final Operator operator;
938         protected final List<Condition> conditions;
939 
940         protected MultiCondition(Operator operator, Condition... conditions) {
941             this.operator = operator;
942             this.conditions = new ArrayList<Condition>();
943             for (Condition condition : conditions) {
944                 if (condition != null) {
945                     this.conditions.add(condition);
946                 }
947             }
948         }
949 
950         /**
951          * Conditions list.
952          *
953          * @return conditions
954          */
955         public List<Condition> getConditions() {
956             return conditions;
957         }
958 
959         /**
960          * Condition operator.
961          *
962          * @return operator
963          */
964         public Operator getOperator() {
965             return operator;
966         }
967 
968         /**
969          * Add a new condition.
970          *
971          * @param condition single condition
972          */
973         public void add(Condition condition) {
974             if (condition != null) {
975                 conditions.add(condition);
976             }
977         }
978 
979         public boolean isEmpty() {
980             boolean isEmpty = true;
981             for (Condition condition : conditions) {
982                 if (!condition.isEmpty()) {
983                     isEmpty = false;
984                     break;
985                 }
986             }
987             return isEmpty;
988         }
989 
990         public boolean isMatch(ExchangeSession.Contact contact) {
991             if (operator == Operator.And) {
992                 for (Condition condition : conditions) {
993                     if (!condition.isMatch(contact)) {
994                         return false;
995                     }
996                 }
997                 return true;
998             } else if (operator == Operator.Or) {
999                 for (Condition condition : conditions) {
1000                     if (condition.isMatch(contact)) {
1001                         return true;
1002                     }
1003                 }
1004                 return false;
1005             } else {
1006                 return false;
1007             }
1008         }
1009 
1010     }
1011 
1012     /**
1013      * Not condition.
1014      */
1015     public abstract static class NotCondition implements Condition {
1016         protected final Condition condition;
1017 
1018         protected NotCondition(Condition condition) {
1019             this.condition = condition;
1020         }
1021 
1022         public boolean isEmpty() {
1023             return condition.isEmpty();
1024         }
1025 
1026         public boolean isMatch(ExchangeSession.Contact contact) {
1027             return !condition.isMatch(contact);
1028         }
1029     }
1030 
1031     /**
1032      * Single search filter condition.
1033      */
1034     public abstract static class MonoCondition implements Condition {
1035         protected final String attributeName;
1036         protected final Operator operator;
1037 
1038         protected MonoCondition(String attributeName, Operator operator) {
1039             this.attributeName = attributeName;
1040             this.operator = operator;
1041         }
1042 
1043         public boolean isEmpty() {
1044             return false;
1045         }
1046 
1047         public boolean isMatch(ExchangeSession.Contact contact) {
1048             String actualValue = contact.get(attributeName);
1049             return (operator == Operator.IsNull && actualValue == null) ||
1050                     (operator == Operator.IsFalse && "false".equals(actualValue)) ||
1051                     (operator == Operator.IsTrue && "true".equals(actualValue));
1052         }
1053     }
1054 
1055     /**
1056      * And search filter.
1057      *
1058      * @param condition search conditions
1059      * @return condition
1060      */
1061     public abstract MultiCondition and(Condition... condition);
1062 
1063     /**
1064      * Or search filter.
1065      *
1066      * @param condition search conditions
1067      * @return condition
1068      */
1069     public abstract MultiCondition or(Condition... condition);
1070 
1071     /**
1072      * Not search filter.
1073      *
1074      * @param condition search condition
1075      * @return condition
1076      */
1077     public abstract Condition not(Condition condition);
1078 
1079     /**
1080      * Equals condition.
1081      *
1082      * @param attributeName logical Exchange attribute name
1083      * @param value         attribute value
1084      * @return condition
1085      */
1086     public abstract Condition isEqualTo(String attributeName, String value);
1087 
1088     /**
1089      * Equals condition.
1090      *
1091      * @param attributeName logical Exchange attribute name
1092      * @param value         attribute value
1093      * @return condition
1094      */
1095     public abstract Condition isEqualTo(String attributeName, int value);
1096 
1097     /**
1098      * MIME header equals condition.
1099      *
1100      * @param headerName MIME header name
1101      * @param value      attribute value
1102      * @return condition
1103      */
1104     public abstract Condition headerIsEqualTo(String headerName, String value);
1105 
1106     /**
1107      * Greater than or equals condition.
1108      *
1109      * @param attributeName logical Exchange attribute name
1110      * @param value         attribute value
1111      * @return condition
1112      */
1113     public abstract Condition gte(String attributeName, String value);
1114 
1115     /**
1116      * Greater than condition.
1117      *
1118      * @param attributeName logical Exchange attribute name
1119      * @param value         attribute value
1120      * @return condition
1121      */
1122     public abstract Condition gt(String attributeName, String value);
1123 
1124     /**
1125      * Lower than condition.
1126      *
1127      * @param attributeName logical Exchange attribute name
1128      * @param value         attribute value
1129      * @return condition
1130      */
1131     public abstract Condition lt(String attributeName, String value);
1132 
1133     /**
1134      * Lower than or equals condition.
1135      *
1136      * @param attributeName logical Exchange attribute name
1137      * @param value         attribute value
1138      * @return condition
1139      */
1140     @SuppressWarnings({"UnusedDeclaration"})
1141     public abstract Condition lte(String attributeName, String value);
1142 
1143     /**
1144      * Contains condition.
1145      *
1146      * @param attributeName logical Exchange attribute name
1147      * @param value         attribute value
1148      * @return condition
1149      */
1150     public abstract Condition contains(String attributeName, String value);
1151 
1152     /**
1153      * Starts with condition.
1154      *
1155      * @param attributeName logical Exchange attribute name
1156      * @param value         attribute value
1157      * @return condition
1158      */
1159     public abstract Condition startsWith(String attributeName, String value);
1160 
1161     /**
1162      * Is null condition.
1163      *
1164      * @param attributeName logical Exchange attribute name
1165      * @return condition
1166      */
1167     public abstract Condition isNull(String attributeName);
1168 
1169     /**
1170      * Is true condition.
1171      *
1172      * @param attributeName logical Exchange attribute name
1173      * @return condition
1174      */
1175     public abstract Condition isTrue(String attributeName);
1176 
1177     /**
1178      * Is false condition.
1179      *
1180      * @param attributeName logical Exchange attribute name
1181      * @return condition
1182      */
1183     public abstract Condition isFalse(String attributeName);
1184 
1185     /**
1186      * Search mail and generic folders under given folder.
1187      * Exclude calendar and contacts folders
1188      *
1189      * @param folderName Exchange folder name
1190      * @param recursive  deep search if true
1191      * @return list of folders
1192      * @throws IOException on error
1193      */
1194     public List<Folder> getSubFolders(String folderName, boolean recursive) throws IOException {
1195         Condition folderCondition = null;
1196         if (!Settings.getBooleanProperty("davmail.imapIncludeSpecialFolders", false)) {
1197             folderCondition = or(isEqualTo("folderclass", "IPF.Note"), isNull("folderclass"));
1198         }
1199         List<Folder> results = getSubFolders(folderName, folderCondition,
1200                 recursive);
1201         // need to include base folder in recursive search, except on root
1202         if (recursive && folderName.length() > 0) {
1203             results.add(getFolder(folderName));
1204         }
1205 
1206         return results;
1207     }
1208 
1209     /**
1210      * Search calendar folders under given folder.
1211      *
1212      * @param folderName Exchange folder name
1213      * @param recursive  deep search if true
1214      * @return list of folders
1215      * @throws IOException on error
1216      */
1217     public List<Folder> getSubCalendarFolders(String folderName, boolean recursive) throws IOException {
1218         return getSubFolders(folderName, isEqualTo("folderclass", "IPF.Appointment"), recursive);
1219     }
1220 
1221     /**
1222      * Search folders under given folder matching filter.
1223      *
1224      * @param folderName Exchange folder name
1225      * @param condition  search filter
1226      * @param recursive  deep search if true
1227      * @return list of folders
1228      * @throws IOException on error
1229      */
1230     public abstract List<Folder> getSubFolders(String folderName, Condition condition, boolean recursive) throws IOException;
1231 
1232     /**
1233      * Delete oldest messages in trash.
1234      * keepDelay is the number of days to keep messages in trash before delete
1235      *
1236      * @throws IOException when unable to purge messages
1237      */
1238     public void purgeOldestTrashAndSentMessages() throws IOException {
1239         int keepDelay = Settings.getIntProperty("davmail.keepDelay");
1240         if (keepDelay != 0) {
1241             purgeOldestFolderMessages(TRASH, keepDelay);
1242         }
1243         // this is a new feature, default is : do nothing
1244         int sentKeepDelay = Settings.getIntProperty("davmail.sentKeepDelay");
1245         if (sentKeepDelay != 0) {
1246             purgeOldestFolderMessages(SENT, sentKeepDelay);
1247         }
1248     }
1249 
1250     protected void purgeOldestFolderMessages(String folderPath, int keepDelay) throws IOException {
1251         Calendar cal = Calendar.getInstance();
1252         cal.add(Calendar.DAY_OF_MONTH, -keepDelay);
1253         LOGGER.debug("Delete messages in " + folderPath + " not modified since " + cal.getTime());
1254 
1255         MessageList messages = searchMessages(folderPath, UID_MESSAGE_ATTRIBUTES,
1256                 lt("lastmodified", formatSearchDate(cal.getTime())));
1257 
1258         for (Message message : messages) {
1259             message.delete();
1260         }
1261     }
1262 
1263     protected void convertResentHeader(MimeMessage mimeMessage, String headerName) throws MessagingException {
1264         String[] resentHeader = mimeMessage.getHeader("Resent-" + headerName);
1265         if (resentHeader != null) {
1266             mimeMessage.removeHeader("Resent-" + headerName);
1267             mimeMessage.removeHeader(headerName);
1268             for (String value : resentHeader) {
1269                 mimeMessage.addHeader(headerName, value);
1270             }
1271         }
1272     }
1273 
1274     protected String lastSentMessageId;
1275 
1276     /**
1277      * Send message in reader to recipients.
1278      * Detect visible recipients in message body to determine bcc recipients
1279      *
1280      * @param rcptToRecipients recipients list
1281      * @param mimeMessage      mime message
1282      * @throws IOException        on error
1283      * @throws MessagingException on error
1284      */
1285     public void sendMessage(List<String> rcptToRecipients, MimeMessage mimeMessage) throws IOException, MessagingException {
1286         // detect duplicate send command
1287         String messageId = mimeMessage.getMessageID();
1288         if (lastSentMessageId != null && lastSentMessageId.equals(messageId)) {
1289             LOGGER.debug("Dropping message id " + messageId + ": already sent");
1290             return;
1291         }
1292         lastSentMessageId = messageId;
1293 
1294         convertResentHeader(mimeMessage, "From");
1295         convertResentHeader(mimeMessage, "To");
1296         convertResentHeader(mimeMessage, "Cc");
1297         convertResentHeader(mimeMessage, "Bcc");
1298         convertResentHeader(mimeMessage, "Message-Id");
1299 
1300         // do not allow send as another user on Exchange 2003
1301         if ("Exchange2003".equals(serverVersion) || Settings.getBooleanProperty("davmail.smtpStripFrom", false)) {
1302             mimeMessage.removeHeader("From");
1303         }
1304 
1305         // remove visible recipients from list
1306         Set<String> visibleRecipients = new HashSet<String>();
1307         List<InternetAddress> recipients = getAllRecipients(mimeMessage);
1308         for (InternetAddress address : recipients) {
1309             visibleRecipients.add((address.getAddress().toLowerCase()));
1310         }
1311         for (String recipient : rcptToRecipients) {
1312             if (!visibleRecipients.contains(recipient.toLowerCase())) {
1313                 mimeMessage.addRecipient(javax.mail.Message.RecipientType.BCC, new InternetAddress(recipient));
1314             }
1315         }
1316         sendMessage(mimeMessage);
1317 
1318     }
1319 
1320     static final String[] RECIPIENT_HEADERS = {"to", "cc", "bcc"};
1321 
1322     protected List<InternetAddress> getAllRecipients(MimeMessage mimeMessage) throws MessagingException {
1323         List<InternetAddress> recipientList = new ArrayList<InternetAddress>();
1324         for (String recipientHeader : RECIPIENT_HEADERS) {
1325             final String recipientHeaderValue = mimeMessage.getHeader(recipientHeader, ",");
1326             if (recipientHeaderValue != null) {
1327                 // parse headers in non strict mode
1328                 recipientList.addAll(Arrays.asList(InternetAddress.parseHeader(recipientHeaderValue, false)));
1329             }
1330 
1331         }
1332         return recipientList;
1333     }
1334 
1335     /**
1336      * Send Mime message.
1337      *
1338      * @param mimeMessage MIME message
1339      * @throws IOException        on error
1340      * @throws MessagingException on error
1341      */
1342     public abstract void sendMessage(MimeMessage mimeMessage) throws IOException, MessagingException;
1343 
1344     /**
1345      * Get folder object.
1346      * Folder name can be logical names INBOX, Drafts, Trash or calendar,
1347      * or a path relative to user base folder or absolute path.
1348      *
1349      * @param folderPath folder path
1350      * @return Folder object
1351      * @throws IOException on error
1352      */
1353     public ExchangeSession.Folder getFolder(String folderPath) throws IOException {
1354         Folder folder = internalGetFolder(folderPath);
1355         if (isMainCalendar(folderPath)) {
1356             Folder taskFolder = internalGetFolder(TASKS);
1357             folder.ctag += taskFolder.ctag;
1358         }
1359         return folder;
1360     }
1361 
1362     protected abstract Folder internalGetFolder(String folderName) throws IOException;
1363 
1364     /**
1365      * Check folder ctag and reload messages as needed.
1366      *
1367      * @param currentFolder current folder
1368      * @return true if folder changed
1369      * @throws IOException on error
1370      */
1371     public boolean refreshFolder(Folder currentFolder) throws IOException {
1372         Folder newFolder = getFolder(currentFolder.folderPath);
1373         if (currentFolder.ctag == null || !currentFolder.ctag.equals(newFolder.ctag)) {
1374             if (LOGGER.isDebugEnabled()) {
1375                 LOGGER.debug("Contenttag changed on " + currentFolder.folderPath + ' '
1376                         + currentFolder.ctag + " => " + newFolder.ctag + ", reloading messages");
1377             }
1378             currentFolder.hasChildren = newFolder.hasChildren;
1379             currentFolder.noInferiors = newFolder.noInferiors;
1380             currentFolder.unreadCount = newFolder.unreadCount;
1381             currentFolder.ctag = newFolder.ctag;
1382             currentFolder.etag = newFolder.etag;
1383             if (newFolder.uidNext > currentFolder.uidNext) {
1384                 currentFolder.uidNext = newFolder.uidNext;
1385             }
1386             currentFolder.loadMessages();
1387             return true;
1388         } else {
1389             return false;
1390         }
1391     }
1392 
1393     /**
1394      * Create Exchange message folder.
1395      *
1396      * @param folderName logical folder name
1397      * @return status
1398      * @throws IOException on error
1399      */
1400     public int createMessageFolder(String folderName) throws IOException {
1401         return createFolder(folderName, "IPF.Note", null);
1402     }
1403 
1404     /**
1405      * Create Exchange calendar folder.
1406      *
1407      * @param folderName logical folder name
1408      * @param properties folder properties
1409      * @return status
1410      * @throws IOException on error
1411      */
1412     public int createCalendarFolder(String folderName, Map<String, String> properties) throws IOException {
1413         return createFolder(folderName, "IPF.Appointment", properties);
1414     }
1415 
1416     /**
1417      * Create Exchange contact folder.
1418      *
1419      * @param folderName logical folder name
1420      * @param properties folder properties
1421      * @return status
1422      * @throws IOException on error
1423      */
1424     public int createContactFolder(String folderName, Map<String, String> properties) throws IOException {
1425         return createFolder(folderName, "IPF.Contact", properties);
1426     }
1427 
1428     /**
1429      * Create Exchange folder with given folder class.
1430      *
1431      * @param folderName  logical folder name
1432      * @param folderClass folder class
1433      * @param properties  folder properties
1434      * @return status
1435      * @throws IOException on error
1436      */
1437     public abstract int createFolder(String folderName, String folderClass, Map<String, String> properties) throws IOException;
1438 
1439     /**
1440      * Update Exchange folder properties.
1441      *
1442      * @param folderName logical folder name
1443      * @param properties folder properties
1444      * @return status
1445      * @throws IOException on error
1446      */
1447     public abstract int updateFolder(String folderName, Map<String, String> properties) throws IOException;
1448 
1449     /**
1450      * Delete Exchange folder.
1451      *
1452      * @param folderName logical folder name
1453      * @throws IOException on error
1454      */
1455     public abstract void deleteFolder(String folderName) throws IOException;
1456 
1457     /**
1458      * Copy message to target folder
1459      *
1460      * @param message      Exchange message
1461      * @param targetFolder target folder
1462      * @throws IOException on error
1463      */
1464     public abstract void copyMessage(Message message, String targetFolder) throws IOException;
1465 
1466     public void copyMessages(List<Message> messages, String targetFolder) throws IOException {
1467         for (Message message: messages) {
1468             copyMessage(message, targetFolder);
1469         }
1470     }
1471 
1472 
1473     /**
1474      * Move message to target folder
1475      *
1476      * @param message      Exchange message
1477      * @param targetFolder target folder
1478      * @throws IOException on error
1479      */
1480     public abstract void moveMessage(Message message, String targetFolder) throws IOException;
1481 
1482     public void moveMessages(List<Message> messages, String targetFolder) throws IOException {
1483         for (Message message: messages) {
1484             moveMessage(message, targetFolder);
1485         }
1486     }
1487 
1488     /**
1489      * Move folder to target name.
1490      *
1491      * @param folderName current folder name/path
1492      * @param targetName target folder name/path
1493      * @throws IOException on error
1494      */
1495     public abstract void moveFolder(String folderName, String targetName) throws IOException;
1496 
1497     /**
1498      * Move item from source path to target path.
1499      *
1500      * @param sourcePath item source path
1501      * @param targetPath item target path
1502      * @throws IOException on error
1503      */
1504     public abstract void moveItem(String sourcePath, String targetPath) throws IOException;
1505 
1506     protected abstract void moveToTrash(Message message) throws IOException;
1507 
1508     /**
1509      * Convert keyword value to IMAP flag.
1510      *
1511      * @param value keyword value
1512      * @return IMAP flag
1513      */
1514     public String convertKeywordToFlag(String value) {
1515         // first test for keyword in settings
1516         Properties flagSettings = Settings.getSubProperties("davmail.imapFlags");
1517         Enumeration flagSettingsEnum = flagSettings.propertyNames();
1518         while (flagSettingsEnum.hasMoreElements()) {
1519             String key = (String) flagSettingsEnum.nextElement();
1520             if (value.equalsIgnoreCase(flagSettings.getProperty(key))) {
1521                 return key;
1522             }
1523         }
1524 
1525         ResourceBundle flagBundle = ResourceBundle.getBundle("imapflags");
1526         Enumeration<String> flagBundleEnum = flagBundle.getKeys();
1527         while (flagBundleEnum.hasMoreElements()) {
1528             String key = flagBundleEnum.nextElement();
1529             if (value.equalsIgnoreCase(flagBundle.getString(key))) {
1530                 return key;
1531             }
1532         }
1533 
1534         // fall back to raw value
1535         return value;
1536     }
1537 
1538     /**
1539      * Convert IMAP flag to keyword value.
1540      *
1541      * @param value IMAP flag
1542      * @return keyword value
1543      */
1544     public String convertFlagToKeyword(String value) {
1545         // first test for flag in settings
1546         Properties flagSettings = Settings.getSubProperties("davmail.imapFlags");
1547         String flagValue = flagSettings.getProperty(value);
1548         if (flagValue != null) {
1549             return flagValue;
1550         }
1551 
1552         // fall back to predefined flags
1553         ResourceBundle flagBundle = ResourceBundle.getBundle("imapflags");
1554         try {
1555             return flagBundle.getString(value);
1556         } catch (MissingResourceException e) {
1557             // ignore
1558         }
1559 
1560         // fall back to raw value
1561         return value;
1562     }
1563 
1564     /**
1565      * Exchange folder with IMAP properties
1566      */
1567     public class Folder {
1568         /**
1569          * Logical (IMAP) folder path.
1570          */
1571         public String folderPath;
1572 
1573         /**
1574          * Display Name.
1575          */
1576         public String displayName;
1577         /**
1578          * Folder class (PR_CONTAINER_CLASS).
1579          */
1580         public String folderClass;
1581         /**
1582          * Folder message count.
1583          */
1584         public int count;
1585         /**
1586          * Folder unread message count.
1587          */
1588         public int unreadCount;
1589         /**
1590          * true if folder has subfolders (DAV:hassubs).
1591          */
1592         public boolean hasChildren;
1593         /**
1594          * true if folder has no subfolders (DAV:nosubs).
1595          */
1596         public boolean noInferiors;
1597         /**
1598          * Folder content tag (to detect folder content changes).
1599          */
1600         public String ctag;
1601         /**
1602          * Folder etag (to detect folder object changes).
1603          */
1604         public String etag;
1605         /**
1606          * Next IMAP uid
1607          */
1608         public long uidNext;
1609         /**
1610          * recent count
1611          */
1612         public int recent;
1613 
1614         /**
1615          * Folder message list, empty before loadMessages call.
1616          */
1617         public ExchangeSession.MessageList messages;
1618         /**
1619          * Permanent uid (PR_SEARCH_KEY) to IMAP UID map.
1620          */
1621         private final HashMap<String, Long> permanentUrlToImapUidMap = new HashMap<String, Long>();
1622 
1623         /**
1624          * Get IMAP folder flags.
1625          *
1626          * @return folder flags in IMAP format
1627          */
1628         public String getFlags() {
1629             if (noInferiors) {
1630                 return "\\NoInferiors";
1631             } else if (hasChildren) {
1632                 return "\\HasChildren";
1633             } else {
1634                 return "\\HasNoChildren";
1635             }
1636         }
1637 
1638         /**
1639          * Load folder messages.
1640          *
1641          * @throws IOException on error
1642          */
1643         public void loadMessages() throws IOException {
1644             messages = ExchangeSession.this.searchMessages(folderPath, null);
1645             fixUids(messages);
1646             recent = 0;
1647             for (Message message : messages) {
1648                 if (message.recent) {
1649                     recent++;
1650                 }
1651             }
1652             long computedUidNext = 1;
1653             if (!messages.isEmpty()) {
1654                 computedUidNext = messages.get(messages.size() - 1).getImapUid() + 1;
1655             }
1656             if (computedUidNext > uidNext) {
1657                 uidNext = computedUidNext;
1658             }
1659         }
1660 
1661         /**
1662          * Search messages in folder matching query.
1663          *
1664          * @param condition search query
1665          * @return message list
1666          * @throws IOException on error
1667          */
1668         public MessageList searchMessages(Condition condition) throws IOException {
1669             MessageList localMessages = ExchangeSession.this.searchMessages(folderPath, condition);
1670             fixUids(localMessages);
1671             return localMessages;
1672         }
1673 
1674         /**
1675          * Restore previous uids changed by a PROPPATCH (flag change).
1676          *
1677          * @param messages message list
1678          */
1679         protected void fixUids(MessageList messages) {
1680             boolean sortNeeded = false;
1681             for (Message message : messages) {
1682                 if (permanentUrlToImapUidMap.containsKey(message.getPermanentId())) {
1683                     long previousUid = permanentUrlToImapUidMap.get(message.getPermanentId());
1684                     if (message.getImapUid() != previousUid) {
1685                         LOGGER.debug("Restoring IMAP uid " + message.getImapUid() + " -> " + previousUid + " for message " + message.getPermanentId());
1686                         message.setImapUid(previousUid);
1687                         sortNeeded = true;
1688                     }
1689                 } else {
1690                     // add message to uid map
1691                     permanentUrlToImapUidMap.put(message.getPermanentId(), message.getImapUid());
1692                 }
1693             }
1694             if (sortNeeded) {
1695                 Collections.sort(messages);
1696             }
1697         }
1698 
1699         /**
1700          * Folder message count.
1701          *
1702          * @return message count
1703          */
1704         public int count() {
1705             if (messages == null) {
1706                 return count;
1707             } else {
1708                 return messages.size();
1709             }
1710         }
1711 
1712         /**
1713          * Compute IMAP uidnext.
1714          *
1715          * @return max(messageuids)+1
1716          */
1717         public long getUidNext() {
1718             return uidNext;
1719         }
1720 
1721         /**
1722          * Get message at index.
1723          *
1724          * @param index message index
1725          * @return message
1726          */
1727         public Message get(int index) {
1728             return messages.get(index);
1729         }
1730 
1731         /**
1732          * Get current folder messages imap uids and flags
1733          *
1734          * @return imap uid list
1735          */
1736         public TreeMap<Long, String> getImapFlagMap() {
1737             TreeMap<Long, String> imapFlagMap = new TreeMap<Long, String>();
1738             for (ExchangeSession.Message message : messages) {
1739                 imapFlagMap.put(message.getImapUid(), message.getImapFlags());
1740             }
1741             return imapFlagMap;
1742         }
1743 
1744         /**
1745          * Calendar folder flag.
1746          *
1747          * @return true if this is a calendar folder
1748          */
1749         public boolean isCalendar() {
1750             return "IPF.Appointment".equals(folderClass);
1751         }
1752 
1753         /**
1754          * Contact folder flag.
1755          *
1756          * @return true if this is a calendar folder
1757          */
1758         public boolean isContact() {
1759             return "IPF.Contact".equals(folderClass);
1760         }
1761 
1762         /**
1763          * Task folder flag.
1764          *
1765          * @return true if this is a task folder
1766          */
1767         public boolean isTask() {
1768             return "IPF.Task".equals(folderClass);
1769         }
1770 
1771         /**
1772          * drop cached message
1773          */
1774         public void clearCache() {
1775             messages.cachedMimeBody = null;
1776             messages.cachedMimeMessage = null;
1777             messages.cachedMessageImapUid = 0;
1778         }
1779     }
1780 
1781     /**
1782      * Exchange message.
1783      */
1784     public abstract class Message implements Comparable<Message> {
1785         /**
1786          * enclosing message list
1787          */
1788         public MessageList messageList;
1789         /**
1790          * Message url.
1791          */
1792         public String messageUrl;
1793         /**
1794          * Message permanent url (does not change on message move).
1795          */
1796         public String permanentUrl;
1797         /**
1798          * Message uid.
1799          */
1800         public String uid;
1801         /**
1802          * Message content class.
1803          */
1804         public String contentClass;
1805         /**
1806          * Message keywords (categories).
1807          */
1808         public String keywords;
1809         /**
1810          * Message IMAP uid, unique in folder (x0e230003).
1811          */
1812         public long imapUid;
1813         /**
1814          * MAPI message size.
1815          */
1816         public int size;
1817         /**
1818          * Message date (urn:schemas:mailheader:date).
1819          */
1820         public String date;
1821 
1822         /**
1823          * Message flag: read.
1824          */
1825         public boolean read;
1826         /**
1827          * Message flag: deleted.
1828          */
1829         public boolean deleted;
1830         /**
1831          * Message flag: junk.
1832          */
1833         public boolean junk;
1834         /**
1835          * Message flag: flagged.
1836          */
1837         public boolean flagged;
1838         /**
1839          * Message flag: recent.
1840          */
1841         public boolean recent;
1842         /**
1843          * Message flag: draft.
1844          */
1845         public boolean draft;
1846         /**
1847          * Message flag: answered.
1848          */
1849         public boolean answered;
1850         /**
1851          * Message flag: fowarded.
1852          */
1853         public boolean forwarded;
1854 
1855         /**
1856          * Unparsed message content.
1857          */
1858         protected SharedByteArrayInputStream mimeBody;
1859 
1860         /**
1861          * Message content parsed in a MIME message.
1862          */
1863         protected MimeMessage mimeMessage;
1864 
1865         /**
1866          * Get permanent message id.
1867          * permanentUrl over WebDav or IitemId over EWS
1868          *
1869          * @return permanent id
1870          */
1871         public abstract String getPermanentId();
1872 
1873         /**
1874          * IMAP uid , unique in folder (x0e230003)
1875          *
1876          * @return IMAP uid
1877          */
1878         public long getImapUid() {
1879             return imapUid;
1880         }
1881 
1882         /**
1883          * Set IMAP uid.
1884          *
1885          * @param imapUid new uid
1886          */
1887         public void setImapUid(long imapUid) {
1888             this.imapUid = imapUid;
1889         }
1890 
1891         /**
1892          * Exchange uid.
1893          *
1894          * @return uid
1895          */
1896         public String getUid() {
1897             return uid;
1898         }
1899 
1900         /**
1901          * Return message flags in IMAP format.
1902          *
1903          * @return IMAP flags
1904          */
1905         public String getImapFlags() {
1906             StringBuilder buffer = new StringBuilder();
1907             if (read) {
1908                 buffer.append("\\Seen ");
1909             }
1910             if (deleted) {
1911                 buffer.append("\\Deleted ");
1912             }
1913             if (recent) {
1914                 buffer.append("\\Recent ");
1915             }
1916             if (flagged) {
1917                 buffer.append("\\Flagged ");
1918             }
1919             if (junk) {
1920                 buffer.append("Junk ");
1921             }
1922             if (draft) {
1923                 buffer.append("\\Draft ");
1924             }
1925             if (answered) {
1926                 buffer.append("\\Answered ");
1927             }
1928             if (forwarded) {
1929                 buffer.append("$Forwarded ");
1930             }
1931             if (keywords != null) {
1932                 for (String keyword : keywords.split(",")) {
1933                     buffer.append(convertKeywordToFlag(keyword)).append(" ");
1934                 }
1935             }
1936             return buffer.toString().trim();
1937         }
1938 
1939         /**
1940          * Load message content in a Mime message
1941          *
1942          * @throws IOException        on error
1943          * @throws MessagingException on error
1944          */
1945         public void loadMimeMessage() throws IOException, MessagingException {
1946             if (mimeMessage == null) {
1947                 // try to get message content from cache
1948                 if (this.imapUid == messageList.cachedMessageImapUid) {
1949                     mimeBody = messageList.cachedMimeBody;
1950                     mimeMessage = messageList.cachedMimeMessage;
1951                     LOGGER.debug("Got message content for " + imapUid + " from cache");
1952                 } else {
1953                     // load and parse message
1954                     mimeBody = new SharedByteArrayInputStream(getContent(this));
1955                     mimeMessage = new MimeMessage(null, mimeBody);
1956                     mimeBody.reset();
1957                     // workaround for Exchange 2003 ActiveSync bug
1958                     if (mimeMessage.getHeader("MAIL FROM") != null) {
1959                         mimeBody = (SharedByteArrayInputStream) mimeMessage.getRawInputStream();
1960                         mimeMessage = new MimeMessage(null, mimeBody);
1961                         mimeBody.reset();
1962                     }
1963                     LOGGER.debug("Downloaded full message content for IMAP UID " + imapUid + " (" + mimeBody.available() + " bytes)");
1964                 }
1965             }
1966         }
1967 
1968         /**
1969          * Get message content as a Mime message.
1970          *
1971          * @return mime message
1972          * @throws IOException        on error
1973          * @throws MessagingException on error
1974          */
1975         public MimeMessage getMimeMessage() throws IOException, MessagingException {
1976             loadMimeMessage();
1977             return mimeMessage;
1978         }
1979 
1980         public Enumeration getMatchingHeaderLinesFromHeaders(String[] headerNames) throws MessagingException, IOException {
1981             Enumeration result = null;
1982             if (mimeMessage == null) {
1983                 // message not loaded, try to get headers only
1984                 InputStream headers = getMimeHeaders();
1985                 if (headers != null) {
1986                     InternetHeaders internetHeaders = new InternetHeaders(headers);
1987                     if (internetHeaders.getHeader("Subject") == null) {
1988                         // invalid header content
1989                         return null;
1990                     }
1991                     if (headerNames == null) {
1992                         result = internetHeaders.getAllHeaderLines();
1993                     } else {
1994                         result = internetHeaders.getMatchingHeaderLines(headerNames);
1995                     }
1996                 }
1997             }
1998             return result;
1999         }
2000 
2001         public Enumeration getMatchingHeaderLines(String[] headerNames) throws MessagingException, IOException {
2002             Enumeration result = getMatchingHeaderLinesFromHeaders(headerNames);
2003             if (result == null) {
2004                 if (headerNames == null) {
2005                     result = getMimeMessage().getAllHeaderLines();
2006                 } else {
2007                     result = getMimeMessage().getMatchingHeaderLines(headerNames);
2008                 }
2009 
2010             }
2011             return result;
2012         }
2013 
2014         protected abstract InputStream getMimeHeaders();
2015 
2016         /**
2017          * Get message body size.
2018          *
2019          * @return mime message size
2020          * @throws IOException        on error
2021          * @throws MessagingException on error
2022          */
2023         public int getMimeMessageSize() throws IOException, MessagingException {
2024             loadMimeMessage();
2025             mimeBody.reset();
2026             return mimeBody.available();
2027         }
2028 
2029         /**
2030          * Get message body input stream.
2031          *
2032          * @return mime message InputStream
2033          * @throws IOException        on error
2034          * @throws MessagingException on error
2035          */
2036         public InputStream getRawInputStream() throws IOException, MessagingException {
2037             loadMimeMessage();
2038             mimeBody.reset();
2039             return mimeBody;
2040         }
2041 
2042 
2043         /**
2044          * Drop mime message to avoid keeping message content in memory,
2045          * keep a single message in MessageList cache to handle chunked fetch.
2046          */
2047         public void dropMimeMessage() {
2048             // update single message cache
2049             if (mimeMessage != null) {
2050                 messageList.cachedMessageImapUid = imapUid;
2051                 messageList.cachedMimeBody = mimeBody;
2052                 messageList.cachedMimeMessage = mimeMessage;
2053             }
2054             // drop curent message body to save memory
2055             mimeMessage = null;
2056             mimeBody = null;
2057         }
2058 
2059         public boolean isLoaded() {
2060             return mimeMessage != null;
2061         }
2062 
2063         /**
2064          * Delete message.
2065          *
2066          * @throws IOException on error
2067          */
2068         public void delete() throws IOException {
2069             deleteMessage(this);
2070         }
2071 
2072         /**
2073          * Move message to trash, mark message read.
2074          *
2075          * @throws IOException on error
2076          */
2077         public void moveToTrash() throws IOException {
2078             markRead();
2079 
2080             ExchangeSession.this.moveToTrash(this);
2081         }
2082 
2083         /**
2084          * Mark message as read.
2085          *
2086          * @throws IOException on error
2087          */
2088         public void markRead() throws IOException {
2089             HashMap<String, String> properties = new HashMap<String, String>();
2090             properties.put("read", "1");
2091             updateMessage(this, properties);
2092         }
2093 
2094         /**
2095          * Comparator to sort messages by IMAP uid
2096          *
2097          * @param message other message
2098          * @return imapUid comparison result
2099          */
2100         public int compareTo(Message message) {
2101             long compareValue = (imapUid - message.imapUid);
2102             if (compareValue > 0) {
2103                 return 1;
2104             } else if (compareValue < 0) {
2105                 return -1;
2106             } else {
2107                 return 0;
2108             }
2109         }
2110 
2111         /**
2112          * Override equals, compare IMAP uids
2113          *
2114          * @param message other message
2115          * @return true if IMAP uids are equal
2116          */
2117         @Override
2118         public boolean equals(Object message) {
2119             return message instanceof Message && imapUid == ((Message) message).imapUid;
2120         }
2121 
2122         /**
2123          * Override hashCode, return imapUid hashcode.
2124          *
2125          * @return imapUid hashcode
2126          */
2127         @Override
2128         public int hashCode() {
2129             return (int) (imapUid ^ (imapUid >>> 32));
2130         }
2131 
2132         public String removeFlag(String flag) {
2133             if (keywords != null) {
2134                 final String exchangeFlag = convertFlagToKeyword(flag);
2135                 Set<String> keywordSet = new HashSet<String>();
2136                 String[] keywordArray = keywords.split(",");
2137                 for (String value : keywordArray) {
2138                     if (!value.equalsIgnoreCase(exchangeFlag)) {
2139                         keywordSet.add(value);
2140                     }
2141                 }
2142                 keywords = StringUtil.join(keywordSet, ",");
2143             }
2144             return keywords;
2145         }
2146 
2147         public String addFlag(String flag) {
2148             final String exchangeFlag = convertFlagToKeyword(flag);
2149             HashSet<String> keywordSet = new HashSet<String>();
2150             boolean hasFlag = false;
2151             if (keywords != null) {
2152                 String[] keywordArray = keywords.split(",");
2153                 for (String value : keywordArray) {
2154                     keywordSet.add(value);
2155                     if (value.equalsIgnoreCase(exchangeFlag)) {
2156                         hasFlag = true;
2157                     }
2158                 }
2159             }
2160             if (!hasFlag) {
2161                 keywordSet.add(exchangeFlag);
2162             }
2163             keywords = StringUtil.join(keywordSet, ",");
2164             return keywords;
2165         }
2166 
2167         public String setFlags(HashSet<String> flags) {
2168             HashSet<String> keywordSet = new HashSet<String>();
2169             for (String flag : flags) {
2170                 keywordSet.add(convertFlagToKeyword(flag));
2171             }
2172             keywords = StringUtil.join(keywordSet, ",");
2173             return keywords;
2174         }
2175 
2176     }
2177 
2178     /**
2179      * Message list, includes a single messsage cache
2180      */
2181     public static class MessageList extends ArrayList<Message> {
2182         /**
2183          * Cached message content parsed in a MIME message.
2184          */
2185         protected transient MimeMessage cachedMimeMessage;
2186         /**
2187          * Cached message uid.
2188          */
2189         protected transient long cachedMessageImapUid;
2190         /**
2191          * Cached unparsed message
2192          */
2193         protected transient SharedByteArrayInputStream cachedMimeBody;
2194 
2195     }
2196 
2197     /**
2198      * Generic folder item.
2199      */
2200     public abstract static class Item extends HashMap<String, String> {
2201         protected String folderPath;
2202         protected String itemName;
2203         protected String permanentUrl;
2204         /**
2205          * Display name.
2206          */
2207         public String displayName;
2208         /**
2209          * item etag
2210          */
2211         public String etag;
2212         protected String noneMatch;
2213 
2214         /**
2215          * Build item instance.
2216          *
2217          * @param folderPath folder path
2218          * @param itemName   item name class
2219          * @param etag       item etag
2220          * @param noneMatch  none match flag
2221          */
2222         public Item(String folderPath, String itemName, String etag, String noneMatch) {
2223             this.folderPath = folderPath;
2224             this.itemName = itemName;
2225             this.etag = etag;
2226             this.noneMatch = noneMatch;
2227         }
2228 
2229         /**
2230          * Default constructor.
2231          */
2232         protected Item() {
2233         }
2234 
2235         /**
2236          * Return item content type
2237          *
2238          * @return content type
2239          */
2240         public abstract String getContentType();
2241 
2242         /**
2243          * Retrieve item body from Exchange
2244          *
2245          * @return item body
2246          * @throws HttpException on error
2247          */
2248         public abstract String getBody() throws IOException;
2249 
2250         /**
2251          * Get event name (file name part in URL).
2252          *
2253          * @return event name
2254          */
2255         public String getName() {
2256             return itemName;
2257         }
2258 
2259         /**
2260          * Get event etag (last change tag).
2261          *
2262          * @return event etag
2263          */
2264         public String getEtag() {
2265             return etag;
2266         }
2267 
2268         /**
2269          * Set item href.
2270          *
2271          * @param href item href
2272          */
2273         public void setHref(String href) {
2274             int index = href.lastIndexOf('/');
2275             if (index >= 0) {
2276                 folderPath = href.substring(0, index);
2277                 itemName = href.substring(index + 1);
2278             } else {
2279                 throw new IllegalArgumentException(href);
2280             }
2281         }
2282 
2283         /**
2284          * Return item href.
2285          *
2286          * @return item href
2287          */
2288         public String getHref() {
2289             return folderPath + '/' + itemName;
2290         }
2291     }
2292 
2293     /**
2294      * Contact object
2295      */
2296     public abstract class Contact extends Item {
2297 
2298         /**
2299          * @inheritDoc
2300          */
2301         public Contact(String folderPath, String itemName, Map<String, String> properties, String etag, String noneMatch) {
2302             super(folderPath, itemName.endsWith(".vcf") ? itemName.substring(0, itemName.length() - 3) + "EML" : itemName, etag, noneMatch);
2303             this.putAll(properties);
2304         }
2305 
2306         /**
2307          * @inheritDoc
2308          */
2309         protected Contact() {
2310         }
2311 
2312         /**
2313          * Convert EML extension to vcf.
2314          *
2315          * @return item name
2316          */
2317         @Override
2318         public String getName() {
2319             String name = super.getName();
2320             if (name.endsWith(".EML")) {
2321                 name = name.substring(0, name.length() - 3) + "vcf";
2322             }
2323             return name;
2324         }
2325 
2326         /**
2327          * Set contact name
2328          *
2329          * @param name contact name
2330          */
2331         public void setName(String name) {
2332             this.itemName = name;
2333         }
2334 
2335         /**
2336          * Compute vcard uid from name.
2337          *
2338          * @return uid
2339          * @throws URIException on error
2340          */
2341         protected String getUid() throws URIException {
2342             String uid = getName();
2343             int dotIndex = uid.lastIndexOf('.');
2344             if (dotIndex > 0) {
2345                 uid = uid.substring(0, dotIndex);
2346             }
2347             return URIUtil.encodePath(uid);
2348         }
2349 
2350         @Override
2351         public String getContentType() {
2352             return "text/vcard";
2353         }
2354 
2355 
2356         @Override
2357         public String getBody() throws HttpException {
2358             // build RFC 2426 VCard from contact information
2359             VCardWriter writer = new VCardWriter();
2360             writer.startCard();
2361             writer.appendProperty("UID", getUid());
2362             // common name
2363             writer.appendProperty("FN", get("cn"));
2364             // RFC 2426: Family Name, Given Name, Additional Names, Honorific Prefixes, and Honorific Suffixes
2365             writer.appendProperty("N", get("sn"), get("givenName"), get("middlename"), get("personaltitle"), get("namesuffix"));
2366 
2367             writer.appendProperty("TEL;TYPE=cell", get("mobile"));
2368             writer.appendProperty("TEL;TYPE=work", get("telephoneNumber"));
2369             writer.appendProperty("TEL;TYPE=home", get("homePhone"));
2370             writer.appendProperty("TEL;TYPE=fax", get("facsimiletelephonenumber"));
2371             writer.appendProperty("TEL;TYPE=pager", get("pager"));
2372             writer.appendProperty("TEL;TYPE=car", get("othermobile"));
2373             writer.appendProperty("TEL;TYPE=home,fax", get("homefax"));
2374             writer.appendProperty("TEL;TYPE=isdn", get("internationalisdnnumber"));
2375             writer.appendProperty("TEL;TYPE=msg", get("otherTelephone"));
2376 
2377             // The structured type value corresponds, in sequence, to the post office box; the extended address;
2378             // the street address; the locality (e.g., city); the region (e.g., state or province);
2379             // the postal code; the country name
2380             writer.appendProperty("ADR;TYPE=home",
2381                     get("homepostofficebox"), null, get("homeStreet"), get("homeCity"), get("homeState"), get("homePostalCode"), get("homeCountry"));
2382             writer.appendProperty("ADR;TYPE=work",
2383                     get("postofficebox"), get("roomnumber"), get("street"), get("l"), get("st"), get("postalcode"), get("co"));
2384             writer.appendProperty("ADR;TYPE=other",
2385                     get("otherpostofficebox"), null, get("otherstreet"), get("othercity"), get("otherstate"), get("otherpostalcode"), get("othercountry"));
2386 
2387             writer.appendProperty("EMAIL;TYPE=work", get("smtpemail1"));
2388             writer.appendProperty("EMAIL;TYPE=home", get("smtpemail2"));
2389             writer.appendProperty("EMAIL;TYPE=other", get("smtpemail3"));
2390 
2391             writer.appendProperty("ORG", get("o"), get("department"));
2392             writer.appendProperty("URL;TYPE=work", get("businesshomepage"));
2393             writer.appendProperty("URL;TYPE=home", get("personalHomePage"));
2394             writer.appendProperty("TITLE", get("title"));
2395             writer.appendProperty("NOTE", get("description"));
2396 
2397             writer.appendProperty("CUSTOM1", get("extensionattribute1"));
2398             writer.appendProperty("CUSTOM2", get("extensionattribute2"));
2399             writer.appendProperty("CUSTOM3", get("extensionattribute3"));
2400             writer.appendProperty("CUSTOM4", get("extensionattribute4"));
2401 
2402             writer.appendProperty("ROLE", get("profession"));
2403             writer.appendProperty("NICKNAME", get("nickname"));
2404             writer.appendProperty("X-AIM", get("im"));
2405 
2406             writer.appendProperty("BDAY", convertZuluDateToBday(get("bday")));
2407             writer.appendProperty("ANNIVERSARY", convertZuluDateToBday(get("anniversary")));
2408 
2409             String gender = get("gender");
2410             if ("1".equals(gender)) {
2411                 writer.appendProperty("SEX", "2");
2412             } else if ("2".equals(gender)) {
2413                 writer.appendProperty("SEX", "1");
2414             }
2415 
2416             writer.appendProperty("CATEGORIES", get("keywords"));
2417 
2418             writer.appendProperty("FBURL", get("fburl"));
2419 
2420             if ("1".equals(get("private"))) {
2421                 writer.appendProperty("CLASS", "PRIVATE");
2422             }
2423 
2424             writer.appendProperty("X-ASSISTANT", get("secretarycn"));
2425             writer.appendProperty("X-MANAGER", get("manager"));
2426             writer.appendProperty("X-SPOUSE", get("spousecn"));
2427 
2428             writer.appendProperty("REV", get("lastmodified"));
2429 
2430             if ("true".equals(get("haspicture"))) {
2431                 try {
2432                     ContactPhoto contactPhoto = getContactPhoto(this);
2433                     writer.writeLine("PHOTO;BASE64;TYPE=\"" + contactPhoto.contentType + "\";ENCODING=\"b\":");
2434                     writer.writeLine(contactPhoto.content, true);
2435                 } catch (IOException e) {
2436                     LOGGER.warn("Unable to get photo from contact " + this.get("cn"));
2437                 }
2438             }
2439 
2440             writer.endCard();
2441             return writer.toString();
2442         }
2443 
2444     }
2445 
2446     /**
2447      * Calendar event object.
2448      */
2449     public abstract class Event extends Item {
2450         protected String contentClass;
2451         protected String subject;
2452         protected VCalendar vCalendar;
2453 
2454         /**
2455          * @inheritDoc
2456          */
2457         public Event(String folderPath, String itemName, String contentClass, String itemBody, String etag, String noneMatch) throws IOException {
2458             super(folderPath, itemName, etag, noneMatch);
2459             this.contentClass = contentClass;
2460             fixICS(itemBody.getBytes("UTF-8"), false);
2461             // fix task item name
2462             if (vCalendar.isTodo() && this.itemName.endsWith(".ics")) {
2463                 this.itemName = itemName.substring(0, itemName.length() - 3) + "EML";
2464             }
2465         }
2466 
2467         /**
2468          * @inheritDoc
2469          */
2470         protected Event() {
2471         }
2472 
2473         @Override
2474         public String getContentType() {
2475             return "text/calendar;charset=UTF-8";
2476         }
2477 
2478         @Override
2479         public String getBody() throws IOException {
2480             if (vCalendar == null) {
2481                 fixICS(getEventContent(), true);
2482             }
2483             return vCalendar.toString();
2484         }
2485 
2486         protected HttpException buildHttpException(Exception e) {
2487             String message = "Unable to get event " + getName() + " subject: " + subject + " at " + permanentUrl + ": " + e.getMessage();
2488             LOGGER.warn(message);
2489             return new HttpException(message);
2490         }
2491 
2492         /**
2493          * Retrieve item body from Exchange
2494          *
2495          * @return item content
2496          * @throws HttpException on error
2497          */
2498         public abstract byte[] getEventContent() throws IOException;
2499 
2500         protected static final String TEXT_CALENDAR = "text/calendar";
2501         protected static final String APPLICATION_ICS = "application/ics";
2502 
2503         protected boolean isCalendarContentType(String contentType) {
2504             return TEXT_CALENDAR.regionMatches(true, 0, contentType, 0, TEXT_CALENDAR.length()) ||
2505                     APPLICATION_ICS.regionMatches(true, 0, contentType, 0, APPLICATION_ICS.length());
2506         }
2507 
2508         protected MimePart getCalendarMimePart(MimeMultipart multiPart) throws IOException, MessagingException {
2509             MimePart bodyPart = null;
2510             for (int i = 0; i < multiPart.getCount(); i++) {
2511                 String contentType = multiPart.getBodyPart(i).getContentType();
2512                 if (isCalendarContentType(contentType)) {
2513                     bodyPart = (MimePart) multiPart.getBodyPart(i);
2514                     break;
2515                 } else if (contentType.startsWith("multipart")) {
2516                     Object content = multiPart.getBodyPart(i).getContent();
2517                     if (content instanceof MimeMultipart) {
2518                         bodyPart = getCalendarMimePart((MimeMultipart) content);
2519                     }
2520                 }
2521             }
2522 
2523             return bodyPart;
2524         }
2525 
2526         /**
2527          * Load ICS content from MIME message input stream
2528          *
2529          * @param mimeInputStream mime message input stream
2530          * @return mime message ics attachment body
2531          * @throws IOException        on error
2532          * @throws MessagingException on error
2533          */
2534         protected byte[] getICS(InputStream mimeInputStream) throws IOException, MessagingException {
2535             byte[] result;
2536             MimeMessage mimeMessage = new MimeMessage(null, mimeInputStream);
2537             String[] contentClassHeader = mimeMessage.getHeader("Content-class");
2538             // task item, return null
2539             if (contentClassHeader != null && contentClassHeader.length > 0 && "urn:content-classes:task".equals(contentClassHeader[0])) {
2540                 return null;
2541             }
2542             Object mimeBody = mimeMessage.getContent();
2543             MimePart bodyPart = null;
2544             if (mimeBody instanceof MimeMultipart) {
2545                 bodyPart = getCalendarMimePart((MimeMultipart) mimeBody);
2546             } else if (isCalendarContentType(mimeMessage.getContentType())) {
2547                 // no multipart, single body
2548                 bodyPart = mimeMessage;
2549             }
2550 
2551             if (bodyPart != null) {
2552                 ByteArrayOutputStream baos = new ByteArrayOutputStream();
2553                 bodyPart.getDataHandler().writeTo(baos);
2554                 baos.close();
2555                 result = baos.toByteArray();
2556             } else {
2557                 ByteArrayOutputStream baos = new ByteArrayOutputStream();
2558                 mimeMessage.writeTo(baos);
2559                 baos.close();
2560                 throw new DavMailException("EXCEPTION_INVALID_MESSAGE_CONTENT", new String(baos.toByteArray(), "UTF-8"));
2561             }
2562             return result;
2563         }
2564 
2565         protected void fixICS(byte[] icsContent, boolean fromServer) throws IOException {
2566             if (LOGGER.isDebugEnabled() && fromServer) {
2567                 dumpIndex++;
2568                 String icsBody = new String(icsContent, "UTF-8");
2569                 dumpICS(icsBody, fromServer, false);
2570                 LOGGER.debug("Vcalendar body received from server:\n" + icsBody);
2571             }
2572             vCalendar = new VCalendar(icsContent, getEmail(), getVTimezone());
2573             vCalendar.fixVCalendar(fromServer);
2574             if (LOGGER.isDebugEnabled() && !fromServer) {
2575                 String resultString = vCalendar.toString();
2576                 LOGGER.debug("Fixed Vcalendar body to server:\n" + resultString);
2577                 dumpICS(resultString, fromServer, true);
2578             }
2579         }
2580 
2581         protected void dumpICS(String icsBody, boolean fromServer, boolean after) {
2582             String logFileDirectory = Settings.getLogFileDirectory();
2583 
2584             // additional setting to activate ICS dump (not available in GUI)
2585             int dumpMax = Settings.getIntProperty("davmail.dumpICS");
2586             if (dumpMax > 0) {
2587                 if (dumpIndex > dumpMax) {
2588                     // Delete the oldest dump file
2589                     final int oldest = dumpIndex - dumpMax;
2590                     try {
2591                         File[] oldestFiles = (new File(logFileDirectory)).listFiles(new FilenameFilter() {
2592                             public boolean accept(File dir, String name) {
2593                                 if (name.endsWith(".ics")) {
2594                                     int dashIndex = name.indexOf('-');
2595                                     if (dashIndex > 0) {
2596                                         try {
2597                                             int fileIndex = Integer.parseInt(name.substring(0, dashIndex));
2598                                             return fileIndex < oldest;
2599                                         } catch (NumberFormatException nfe) {
2600                                             // ignore
2601                                         }
2602                                     }
2603                                 }
2604                                 return false;
2605                             }
2606                         });
2607                         if (oldestFiles != null) {
2608                             for (File file : oldestFiles) {
2609                                 if (!file.delete()) {
2610                                     LOGGER.warn("Unable to delete " + file.getAbsolutePath());
2611                                 }
2612                             }
2613                         }
2614                     } catch (Exception ex) {
2615                         LOGGER.warn("Error deleting ics dump: " + ex.getMessage());
2616                     }
2617                 }
2618 
2619                 StringBuilder filePath = new StringBuilder();
2620                 filePath.append(logFileDirectory).append('/')
2621                         .append(dumpIndex)
2622                         .append(after ? "-to" : "-from")
2623                         .append((after ^ fromServer) ? "-server" : "-client")
2624                         .append(".ics");
2625                 if ((icsBody != null) && (icsBody.length() > 0)) {
2626                     OutputStreamWriter writer = null;
2627                     try {
2628                         writer = new OutputStreamWriter(new FileOutputStream(filePath.toString()), "UTF-8");
2629                         writer.write(icsBody);
2630                     } catch (IOException e) {
2631                         LOGGER.error(e);
2632                     } finally {
2633                         if (writer != null) {
2634                             try {
2635                                 writer.close();
2636                             } catch (IOException e) {
2637                                 LOGGER.error(e);
2638                             }
2639                         }
2640                     }
2641 
2642 
2643                 }
2644             }
2645 
2646         }
2647 
2648         /**
2649          * Build Mime body for event or event message.
2650          *
2651          * @return mimeContent as byte array or null
2652          * @throws IOException on error
2653          */
2654         public byte[] createMimeContent() throws IOException {
2655             String boundary = UUID.randomUUID().toString();
2656             ByteArrayOutputStream baos = new ByteArrayOutputStream();
2657             MimeOutputStreamWriter writer = new MimeOutputStreamWriter(baos);
2658 
2659             writer.writeHeader("Content-Transfer-Encoding", "7bit");
2660             writer.writeHeader("Content-class", contentClass);
2661             // append date
2662             writer.writeHeader("Date", new Date());
2663 
2664             // Make sure invites have a proper subject line
2665             String vEventSubject = vCalendar.getFirstVeventPropertyValue("SUMMARY");
2666             if (vEventSubject == null) {
2667                 vEventSubject = BundleMessage.format("MEETING_REQUEST");
2668             }
2669 
2670             // Write a part of the message that contains the
2671             // ICS description so that invites contain the description text
2672             String description = vCalendar.getFirstVeventPropertyValue("DESCRIPTION");
2673 
2674             // handle notifications
2675             if ("urn:content-classes:calendarmessage".equals(contentClass)) {
2676                 // need to parse attendees and organizer to build recipients
2677                 VCalendar.Recipients recipients = vCalendar.getRecipients(true);
2678                 String to;
2679                 String cc;
2680                 String notificationSubject;
2681                 if (email.equalsIgnoreCase(recipients.organizer)) {
2682                     // current user is organizer => notify all
2683                     to = recipients.attendees;
2684                     cc = recipients.optionalAttendees;
2685                     notificationSubject = subject;
2686                 } else {
2687                     String status = vCalendar.getAttendeeStatus();
2688                     // notify only organizer
2689                     to = recipients.organizer;
2690                     cc = null;
2691                     notificationSubject = (status != null) ? (BundleMessage.format(status) + vEventSubject) : subject;
2692                     description = "";
2693                 }
2694 
2695                 // Allow end user notification edit
2696                 if (Settings.getBooleanProperty("davmail.caldavEditNotifications")) {
2697                     // create notification edit dialog
2698                     NotificationDialog notificationDialog = new NotificationDialog(to,
2699                             cc, notificationSubject, description);
2700                     if (!notificationDialog.getSendNotification()) {
2701                         LOGGER.debug("Notification canceled by user");
2702                         return null;
2703                     }
2704                     // get description from dialog
2705                     to = notificationDialog.getTo();
2706                     cc = notificationDialog.getCc();
2707                     notificationSubject = notificationDialog.getSubject();
2708                     description = notificationDialog.getBody();
2709                 }
2710 
2711                 // do not send notification if no recipients found
2712                 if ((to == null || to.length() == 0) && (cc == null || cc.length() == 0)) {
2713                     return null;
2714                 }
2715 
2716                 writer.writeHeader("To", to);
2717                 writer.writeHeader("Cc", cc);
2718                 writer.writeHeader("Subject", notificationSubject);
2719 
2720 
2721                 if (LOGGER.isDebugEnabled()) {
2722                     StringBuilder logBuffer = new StringBuilder("Sending notification ");
2723                     if (to != null) {
2724                         logBuffer.append("to: ").append(to);
2725                     }
2726                     if (cc != null) {
2727                         logBuffer.append("cc: ").append(cc);
2728                     }
2729                     LOGGER.debug(logBuffer.toString());
2730                 }
2731             } else {
2732                 // need to parse attendees and organizer to build recipients
2733                 VCalendar.Recipients recipients = vCalendar.getRecipients(false);
2734                 // storing appointment, full recipients header
2735                 if (recipients.attendees != null) {
2736                     writer.writeHeader("To", recipients.attendees);
2737                 } else {
2738                     // use current user as attendee
2739                     writer.writeHeader("To", email);
2740                 }
2741                 writer.writeHeader("Cc", recipients.optionalAttendees);
2742 
2743                 if (recipients.organizer != null) {
2744                     writer.writeHeader("From", recipients.organizer);
2745                 } else {
2746                     writer.writeHeader("From", email);
2747                 }
2748             }
2749             if (vCalendar.getMethod() == null) {
2750                 vCalendar.setPropertyValue("METHOD", "REQUEST");
2751             }
2752             writer.writeHeader("MIME-Version", "1.0");
2753             writer.writeHeader("Content-Type", "multipart/alternative;\r\n" +
2754                     "\tboundary=\"----=_NextPart_" + boundary + '\"');
2755             writer.writeLn();
2756             writer.writeLn("This is a multi-part message in MIME format.");
2757             writer.writeLn();
2758             writer.writeLn("------=_NextPart_" + boundary);
2759 
2760             if (description != null && description.length() > 0) {
2761                 writer.writeHeader("Content-Type", "text/plain;\r\n" +
2762                         "\tcharset=\"utf-8\"");
2763                 writer.writeHeader("content-transfer-encoding", "8bit");
2764                 writer.writeLn();
2765                 writer.flush();
2766                 baos.write(description.getBytes("UTF-8"));
2767                 writer.writeLn();
2768                 writer.writeLn("------=_NextPart_" + boundary);
2769             }
2770             writer.writeHeader("Content-class", contentClass);
2771             writer.writeHeader("Content-Type", "text/calendar;\r\n" +
2772                     "\tmethod=" + vCalendar.getMethod() + ";\r\n" +
2773                     "\tcharset=\"utf-8\""
2774             );
2775             writer.writeHeader("Content-Transfer-Encoding", "8bit");
2776             writer.writeLn();
2777             writer.flush();
2778             baos.write(vCalendar.toString().getBytes("UTF-8"));
2779             writer.writeLn();
2780             writer.writeLn("------=_NextPart_" + boundary + "--");
2781             writer.close();
2782             return baos.toByteArray();
2783         }
2784 
2785         /**
2786          * Create or update item
2787          *
2788          * @return action result
2789          * @throws IOException on error
2790          */
2791         public abstract ItemResult createOrUpdate() throws IOException;
2792 
2793     }
2794 
2795     protected abstract Set<String> getItemProperties();
2796 
2797     /**
2798      * Search contacts in provided folder.
2799      *
2800      * @param folderPath Exchange folder path
2801      * @return list of contacts
2802      * @throws IOException on error
2803      */
2804     public List<ExchangeSession.Contact> getAllContacts(String folderPath) throws IOException {
2805         return searchContacts(folderPath, ExchangeSession.CONTACT_ATTRIBUTES, isEqualTo("outlookmessageclass", "IPM.Contact"), 0);
2806     }
2807 
2808 
2809     /**
2810      * Search contacts in provided folder matching the search query.
2811      *
2812      * @param folderPath Exchange folder path
2813      * @param attributes requested attributes
2814      * @param condition  Exchange search query
2815      * @param maxCount   maximum item count
2816      * @return list of contacts
2817      * @throws IOException on error
2818      */
2819     public abstract List<Contact> searchContacts(String folderPath, Set<String> attributes, Condition condition, int maxCount) throws IOException;
2820 
2821     /**
2822      * Search calendar messages in provided folder.
2823      *
2824      * @param folderPath Exchange folder path
2825      * @return list of calendar messages as Event objects
2826      * @throws IOException on error
2827      */
2828     public abstract List<Event> getEventMessages(String folderPath) throws IOException;
2829 
2830     /**
2831      * Search calendar events in provided folder.
2832      *
2833      * @param folderPath Exchange folder path
2834      * @return list of calendar events
2835      * @throws IOException on error
2836      */
2837     public List<Event> getAllEvents(String folderPath) throws IOException {
2838         List<Event> results = searchEvents(folderPath, getCalendarItemCondition(getPastDelayCondition("dtstart")));
2839 
2840         if (!Settings.getBooleanProperty("davmail.caldavDisableTasks", false) && isMainCalendar(folderPath)) {
2841             // retrieve tasks from main tasks folder
2842             results.addAll(searchTasksOnly(TASKS));
2843         }
2844 
2845         return results;
2846     }
2847 
2848     protected abstract Condition getCalendarItemCondition(Condition dateCondition);
2849 
2850     protected Condition getPastDelayCondition(String attribute) {
2851         int caldavPastDelay = Settings.getIntProperty("davmail.caldavPastDelay");
2852         Condition dateCondition = null;
2853         if (caldavPastDelay != 0) {
2854             Calendar cal = Calendar.getInstance();
2855             cal.add(Calendar.DAY_OF_MONTH, -caldavPastDelay);
2856             dateCondition = gt(attribute, formatSearchDate(cal.getTime()));
2857         }
2858         return dateCondition;
2859     }
2860 
2861     protected Condition getRangeCondition(String timeRangeStart, String timeRangeEnd) throws IOException {
2862         try {
2863             SimpleDateFormat parser = getZuluDateFormat();
2864             ExchangeSession.MultiCondition andCondition = and();
2865             if (timeRangeStart != null) {
2866                 andCondition.add(gt("dtend", formatSearchDate(parser.parse(timeRangeStart))));
2867             }
2868             if (timeRangeEnd != null) {
2869                 andCondition.add(lt("dtstart", formatSearchDate(parser.parse(timeRangeEnd))));
2870             }
2871             return andCondition;
2872         } catch (ParseException e) {
2873             throw new IOException(e + " " + e.getMessage());
2874         }
2875     }
2876 
2877     /**
2878      * Search events between start and end.
2879      *
2880      * @param folderPath     Exchange folder path
2881      * @param timeRangeStart date range start in zulu format
2882      * @param timeRangeEnd   date range start in zulu format
2883      * @return list of calendar events
2884      * @throws IOException on error
2885      */
2886     public List<Event> searchEvents(String folderPath, String timeRangeStart, String timeRangeEnd) throws IOException {
2887         Condition dateCondition = getRangeCondition(timeRangeStart, timeRangeEnd);
2888         Condition condition = getCalendarItemCondition(dateCondition);
2889 
2890         return searchEvents(folderPath, condition);
2891     }
2892 
2893     /**
2894      * Search events between start and end, exclude tasks.
2895      *
2896      * @param folderPath     Exchange folder path
2897      * @param timeRangeStart date range start in zulu format
2898      * @param timeRangeEnd   date range start in zulu format
2899      * @return list of calendar events
2900      * @throws IOException on error
2901      */
2902     public List<Event> searchEventsOnly(String folderPath, String timeRangeStart, String timeRangeEnd) throws IOException {
2903         Condition dateCondition = getRangeCondition(timeRangeStart, timeRangeEnd);
2904         return searchEvents(folderPath, getCalendarItemCondition(dateCondition));
2905     }
2906 
2907     /**
2908      * Search tasks only (VTODO).
2909      *
2910      * @param folderPath Exchange folder path
2911      * @return list of tasks
2912      * @throws IOException on error
2913      */
2914     public List<Event> searchTasksOnly(String folderPath) throws IOException {
2915         return searchEvents(folderPath, and(isEqualTo("outlookmessageclass", "IPM.Task"),
2916                 or(isNull("datecompleted"), getPastDelayCondition("datecompleted"))));
2917     }
2918 
2919     /**
2920      * Search calendar events in provided folder.
2921      *
2922      * @param folderPath Exchange folder path
2923      * @param filter     search filter
2924      * @return list of calendar events
2925      * @throws IOException on error
2926      */
2927     public List<Event> searchEvents(String folderPath, Condition filter) throws IOException {
2928 
2929         Condition privateCondition = null;
2930         if (isSharedFolder(folderPath) && Settings.getBooleanProperty("davmail.excludePrivateEvents", true)) {
2931             LOGGER.debug("Shared or public calendar: exclude private events");
2932             privateCondition = isEqualTo("sensitivity", 0);
2933         }
2934 
2935         return searchEvents(folderPath, getItemProperties(),
2936                 and(filter, privateCondition));
2937     }
2938 
2939     /**
2940      * Search calendar events or messages in provided folder matching the search query.
2941      *
2942      * @param folderPath Exchange folder path
2943      * @param attributes requested attributes
2944      * @param condition  Exchange search query
2945      * @return list of calendar messages as Event objects
2946      * @throws IOException on error
2947      */
2948     public abstract List<Event> searchEvents(String folderPath, Set<String> attributes, Condition condition) throws IOException;
2949 
2950     /**
2951      * convert vcf extension to EML.
2952      *
2953      * @param itemName item name
2954      * @return EML item name
2955      */
2956     protected String convertItemNameToEML(String itemName) {
2957         if (itemName.endsWith(".vcf")) {
2958             return itemName.substring(0, itemName.length() - 3) + "EML";
2959         } else {
2960             return itemName;
2961         }
2962     }
2963 
2964     /**
2965      * Get item named eventName in folder
2966      *
2967      * @param folderPath Exchange folder path
2968      * @param itemName   event name
2969      * @return event object
2970      * @throws IOException on error
2971      */
2972     public abstract Item getItem(String folderPath, String itemName) throws IOException;
2973 
2974     /**
2975      * Contact picture
2976      */
2977     public static class ContactPhoto {
2978         /**
2979          * Contact picture content type (always image/jpeg on read)
2980          */
2981         public String contentType;
2982         /**
2983          * Base64 encoded picture content
2984          */
2985         public String content;
2986     }
2987 
2988     /**
2989      * Retrieve contact photo attached to contact
2990      *
2991      * @param contact address book contact
2992      * @return contact photo
2993      * @throws IOException on error
2994      */
2995     public abstract ContactPhoto getContactPhoto(Contact contact) throws IOException;
2996 
2997 
2998     /**
2999      * Delete event named itemName in folder
3000      *
3001      * @param folderPath Exchange folder path
3002      * @param itemName   item name
3003      * @throws IOException on error
3004      */
3005     public abstract void deleteItem(String folderPath, String itemName) throws IOException;
3006 
3007     /**
3008      * Mark event processed named eventName in folder
3009      *
3010      * @param folderPath Exchange folder path
3011      * @param itemName   item name
3012      * @throws IOException on error
3013      */
3014     public abstract void processItem(String folderPath, String itemName) throws IOException;
3015 
3016 
3017     private static int dumpIndex;
3018 
3019     /**
3020      * Replace iCal4 (Snow Leopard) principal paths with mailto expression
3021      *
3022      * @param value attendee value or ics line
3023      * @return fixed value
3024      */
3025     protected String replaceIcal4Principal(String value) {
3026         if (value != null && value.contains("/principals/__uuids__/")) {
3027             return value.replaceAll("/principals/__uuids__/([^/]*)__AT__([^/]*)/", "mailto:$1@$2");
3028         } else {
3029             return value;
3030         }
3031     }
3032 
3033     /**
3034      * Event result object to hold HTTP status and event etag from an event creation/update.
3035      */
3036     public static class ItemResult {
3037         /**
3038          * HTTP status
3039          */
3040         public int status;
3041         /**
3042          * Event etag from response HTTP header
3043          */
3044         public String etag;
3045     }
3046 
3047     /**
3048      * Build and send the MIME message for the provided ICS event.
3049      *
3050      * @param icsBody event in iCalendar format
3051      * @return HTTP status
3052      * @throws IOException on error
3053      */
3054     public abstract int sendEvent(String icsBody) throws IOException;
3055 
3056     /**
3057      * Create or update item (event or contact) on the Exchange server
3058      *
3059      * @param folderPath Exchange folder path
3060      * @param itemName   event name
3061      * @param itemBody   event body in iCalendar format
3062      * @param etag       previous event etag to detect concurrent updates
3063      * @param noneMatch  if-none-match header value
3064      * @return HTTP response event result (status and etag)
3065      * @throws IOException on error
3066      */
3067     public ItemResult createOrUpdateItem(String folderPath, String itemName, String itemBody, String etag, String noneMatch) throws IOException {
3068         if (itemBody.startsWith("BEGIN:VCALENDAR")) {
3069             return internalCreateOrUpdateEvent(folderPath, itemName, "urn:content-classes:appointment", itemBody, etag, noneMatch);
3070         } else if (itemBody.startsWith("BEGIN:VCARD")) {
3071             return createOrUpdateContact(folderPath, itemName, itemBody, etag, noneMatch);
3072         } else {
3073             throw new IOException(BundleMessage.format("EXCEPTION_INVALID_MESSAGE_CONTENT", itemBody));
3074         }
3075     }
3076 
3077     static final String[] VCARD_N_PROPERTIES = {"sn", "givenName", "middlename", "personaltitle", "namesuffix"};
3078     static final String[] VCARD_ADR_HOME_PROPERTIES = {"homepostofficebox", null, "homeStreet", "homeCity", "homeState", "homePostalCode", "homeCountry"};
3079     static final String[] VCARD_ADR_WORK_PROPERTIES = {"postofficebox", "roomnumber", "street", "l", "st", "postalcode", "co"};
3080     static final String[] VCARD_ADR_OTHER_PROPERTIES = {"otherpostofficebox", null, "otherstreet", "othercity", "otherstate", "otherpostalcode", "othercountry"};
3081     static final String[] VCARD_ORG_PROPERTIES = {"o", "department"};
3082 
3083     protected void convertContactProperties(Map<String, String> properties, String[] contactProperties, List<String> values) {
3084         for (int i = 0; i < values.size() && i < contactProperties.length; i++) {
3085             if (contactProperties[i] != null) {
3086                 properties.put(contactProperties[i], values.get(i));
3087             }
3088         }
3089     }
3090 
3091     protected ItemResult createOrUpdateContact(String folderPath, String itemName, String itemBody, String etag, String noneMatch) throws IOException {
3092         // parse VCARD body to build contact property map
3093         Map<String, String> properties = new HashMap<String, String>();
3094         properties.put("outlookmessageclass", "IPM.Contact");
3095 
3096         VObject vcard = new VObject(new ICSBufferedReader(new StringReader(itemBody)));
3097         for (VProperty property : vcard.getProperties()) {
3098             if ("FN".equals(property.getKey())) {
3099                 properties.put("cn", property.getValue());
3100                 properties.put("subject", property.getValue());
3101                 properties.put("fileas", property.getValue());
3102 
3103             } else if ("N".equals(property.getKey())) {
3104                 convertContactProperties(properties, VCARD_N_PROPERTIES, property.getValues());
3105             } else if ("NICKNAME".equals(property.getKey())) {
3106                 properties.put("nickname", property.getValue());
3107             } else if ("TEL".equals(property.getKey())) {
3108                 if (property.hasParam("TYPE", "cell") || property.hasParam("X-GROUP", "cell")) {
3109                     properties.put("mobile", property.getValue());
3110                 } else if (property.hasParam("TYPE", "work") || property.hasParam("X-GROUP", "work")) {
3111                     properties.put("telephoneNumber", property.getValue());
3112                 } else if (property.hasParam("TYPE", "home") || property.hasParam("X-GROUP", "home")) {
3113                     properties.put("homePhone", property.getValue());
3114                 } else if (property.hasParam("TYPE", "fax")) {
3115                     if (property.hasParam("TYPE", "home")) {
3116                         properties.put("homefax", property.getValue());
3117                     } else {
3118                         properties.put("facsimiletelephonenumber", property.getValue());
3119                     }
3120                 } else if (property.hasParam("TYPE", "pager")) {
3121                     properties.put("pager", property.getValue());
3122                 } else if (property.hasParam("TYPE", "car")) {
3123                     properties.put("othermobile", property.getValue());
3124                 } else {
3125                     properties.put("otherTelephone", property.getValue());
3126                 }
3127             } else if ("ADR".equals(property.getKey())) {
3128                 // address
3129                 if (property.hasParam("TYPE", "home")) {
3130                     convertContactProperties(properties, VCARD_ADR_HOME_PROPERTIES, property.getValues());
3131                 } else if (property.hasParam("TYPE", "work")) {
3132                     convertContactProperties(properties, VCARD_ADR_WORK_PROPERTIES, property.getValues());
3133                     // any other type goes to other address
3134                 } else {
3135                     convertContactProperties(properties, VCARD_ADR_OTHER_PROPERTIES, property.getValues());
3136                 }
3137             } else if ("EMAIL".equals(property.getKey())) {
3138                 if (property.hasParam("TYPE", "home")) {
3139                     properties.put("email2", property.getValue());
3140                     properties.put("smtpemail2", property.getValue());
3141                 } else if (property.hasParam("TYPE", "other")) {
3142                     properties.put("email3", property.getValue());
3143                     properties.put("smtpemail3", property.getValue());
3144                 } else {
3145                     properties.put("email1", property.getValue());
3146                     properties.put("smtpemail1", property.getValue());
3147                 }
3148             } else if ("ORG".equals(property.getKey())) {
3149                 convertContactProperties(properties, VCARD_ORG_PROPERTIES, property.getValues());
3150             } else if ("URL".equals(property.getKey())) {
3151                 if (property.hasParam("TYPE", "work")) {
3152                     properties.put("businesshomepage", property.getValue());
3153                 } else if (property.hasParam("TYPE", "home")) {
3154                     properties.put("personalHomePage", property.getValue());
3155                 } else {
3156                     // default: set personal home page
3157                     properties.put("personalHomePage", property.getValue());
3158                 }
3159             } else if ("TITLE".equals(property.getKey())) {
3160                 properties.put("title", property.getValue());
3161             } else if ("NOTE".equals(property.getKey())) {
3162                 properties.put("description", property.getValue());
3163             } else if ("CUSTOM1".equals(property.getKey())) {
3164                 properties.put("extensionattribute1", property.getValue());
3165             } else if ("CUSTOM2".equals(property.getKey())) {
3166                 properties.put("extensionattribute2", property.getValue());
3167             } else if ("CUSTOM3".equals(property.getKey())) {
3168                 properties.put("extensionattribute3", property.getValue());
3169             } else if ("CUSTOM4".equals(property.getKey())) {
3170                 properties.put("extensionattribute4", property.getValue());
3171             } else if ("ROLE".equals(property.getKey())) {
3172                 properties.put("profession", property.getValue());
3173             } else if ("X-AIM".equals(property.getKey())) {
3174                 properties.put("im", property.getValue());
3175             } else if ("BDAY".equals(property.getKey())) {
3176                 properties.put("bday", convertBDayToZulu(property.getValue()));
3177             } else if ("ANNIVERSARY".equals(property.getKey()) || "X-ANNIVERSARY".equals(property.getKey())) {
3178                 properties.put("anniversary", convertBDayToZulu(property.getValue()));
3179             } else if ("CATEGORIES".equals(property.getKey())) {
3180                 properties.put("keywords", property.getValue());
3181             } else if ("CLASS".equals(property.getKey())) {
3182                 if ("PUBLIC".equals(property.getValue())) {
3183                     properties.put("sensitivity", "0");
3184                     properties.put("private", "false");
3185                 } else {
3186                     properties.put("sensitivity", "2");
3187                     properties.put("private", "true");
3188                 }
3189             } else if ("SEX".equals(property.getKey())) {
3190                 String propertyValue = property.getValue();
3191                 if ("1".equals(propertyValue)) {
3192                     properties.put("gender", "2");
3193                 } else if ("2".equals(propertyValue)) {
3194                     properties.put("gender", "1");
3195                 }
3196             } else if ("FBURL".equals(property.getKey())) {
3197                 properties.put("fburl", property.getValue());
3198             } else if ("X-ASSISTANT".equals(property.getKey())) {
3199                 properties.put("secretarycn", property.getValue());
3200             } else if ("X-MANAGER".equals(property.getKey())) {
3201                 properties.put("manager", property.getValue());
3202             } else if ("X-SPOUSE".equals(property.getKey())) {
3203                 properties.put("spousecn", property.getValue());
3204             } else if ("PHOTO".equals(property.getKey())) {
3205                 properties.put("photo", property.getValue());
3206                 properties.put("haspicture", "true");
3207             }
3208         }
3209         LOGGER.debug("Create or update contact " + itemName + ": " + properties);
3210         // reset missing properties to null
3211         for (String key : CONTACT_ATTRIBUTES) {
3212             if (!"imapUid".equals(key) && !"etag".equals(key) && !"urlcompname".equals(key)
3213                     && !"lastmodified".equals(key) && !"sensitivity".equals(key) &&
3214                     !properties.containsKey(key)) {
3215                 properties.put(key, null);
3216             }
3217         }
3218         return internalCreateOrUpdateContact(folderPath, itemName, properties, etag, noneMatch);
3219     }
3220 
3221     protected String convertZuluDateToBday(String value) {
3222         String result = null;
3223         if (value != null && value.length() > 0) {
3224             try {
3225                 SimpleDateFormat parser = ExchangeSession.getZuluDateFormat();
3226                 Calendar cal = Calendar.getInstance();
3227                 cal.setTime(parser.parse(value));
3228                 cal.add(Calendar.HOUR_OF_DAY, 12);
3229                 result = ExchangeSession.getVcardBdayFormat().format(cal.getTime());
3230             } catch (ParseException e) {
3231                 LOGGER.warn("Invalid date: " + value);
3232             }
3233         }
3234         return result;
3235     }
3236 
3237     protected String convertBDayToZulu(String value) {
3238         String result = null;
3239         if (value != null && value.length() > 0) {
3240             try {
3241                 SimpleDateFormat parser;
3242                 if (value.length() == 10) {
3243                     parser = ExchangeSession.getVcardBdayFormat();
3244                 } else if (value.length() == 15) {
3245                     parser = new SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.ENGLISH);
3246                     parser.setTimeZone(GMT_TIMEZONE);
3247                 } else {
3248                     parser = ExchangeSession.getExchangeZuluDateFormat();
3249                 }
3250                 result = ExchangeSession.getExchangeZuluDateFormatMillisecond().format(parser.parse(value));
3251             } catch (ParseException e) {
3252                 LOGGER.warn("Invalid date: " + value);
3253             }
3254         }
3255 
3256         return result;
3257     }
3258 
3259 
3260     protected abstract ItemResult internalCreateOrUpdateContact(String folderPath, String itemName, Map<String, String> properties, String etag, String noneMatch) throws IOException;
3261 
3262     protected abstract ItemResult internalCreateOrUpdateEvent(String folderPath, String itemName, String contentClass, String icsBody, String etag, String noneMatch) throws IOException;
3263 
3264     /**
3265      * Get current Exchange alias name from login name
3266      *
3267      * @return user name
3268      */
3269     public String getAliasFromLogin() {
3270         // login is email, not alias
3271         if (this.userName.indexOf('@') >= 0) {
3272             return null;
3273         }
3274         String result = this.userName;
3275         // remove domain name
3276         int index = Math.max(result.indexOf('\\'), result.indexOf('/'));
3277         if (index >= 0) {
3278             result = result.substring(index + 1);
3279         }
3280         return result;
3281     }
3282 
3283     protected String getEmailSuffixFromHostname() {
3284         String domain = httpClient.getHostConfiguration().getHost();
3285         int start = domain.lastIndexOf('.', domain.lastIndexOf('.') - 1);
3286         if (start >= 0) {
3287             return '@' + domain.substring(start + 1);
3288         } else {
3289             return '@' + domain;
3290         }
3291     }
3292 
3293     /**
3294      * Test if folderPath is inside user mailbox.
3295      *
3296      * @param folderPath absolute folder path
3297      * @return true if folderPath is a public or shared folder
3298      */
3299     public abstract boolean isSharedFolder(String folderPath);
3300 
3301     /**
3302      * Test if folderPath is main calendar.
3303      *
3304      * @param folderPath absolute folder path
3305      * @return true if folderPath is a public or shared folder
3306      */
3307     public abstract boolean isMainCalendar(String folderPath) throws IOException;
3308 
3309     static final String MAILBOX_BASE = "/cn=";
3310 
3311     protected void getEmailAndAliasFromOptions() {
3312         synchronized (httpClient.getState()) {
3313             Cookie[] currentCookies = httpClient.getState().getCookies();
3314             // get user mail URL from html body
3315             BufferedReader optionsPageReader = null;
3316             GetMethod optionsMethod = new GetMethod("/owa/?ae=Options&t=About");
3317             try {
3318                 DavGatewayHttpClientFacade.executeGetMethod(httpClient, optionsMethod, false);
3319                 optionsPageReader = new BufferedReader(new InputStreamReader(optionsMethod.getResponseBodyAsStream(), "UTF-8"));
3320                 String line;
3321 
3322                 // find email and alias
3323                 //noinspection StatementWithEmptyBody
3324                 while ((line = optionsPageReader.readLine()) != null
3325                         && (line.indexOf('[') == -1
3326                         || line.indexOf('@') == -1
3327                         || line.indexOf(']') == -1
3328                         || !line.toLowerCase().contains(MAILBOX_BASE))) {
3329                 }
3330                 if (line != null) {
3331                     int start = line.toLowerCase().lastIndexOf(MAILBOX_BASE) + MAILBOX_BASE.length();
3332                     int end = line.indexOf('<', start);
3333                     alias = line.substring(start, end);
3334                     end = line.lastIndexOf(']');
3335                     start = line.lastIndexOf('[', end) + 1;
3336                     email = line.substring(start, end);
3337                 }
3338             } catch (IOException e) {
3339                 // restore cookies on error
3340                 httpClient.getState().addCookies(currentCookies);
3341                 LOGGER.error("Error parsing options page at " + optionsMethod.getPath());
3342             } finally {
3343                 if (optionsPageReader != null) {
3344                     try {
3345                         optionsPageReader.close();
3346                     } catch (IOException e) {
3347                         LOGGER.error("Error parsing options page at " + optionsMethod.getPath());
3348                     }
3349                 }
3350                 optionsMethod.releaseConnection();
3351             }
3352         }
3353     }
3354 
3355     /**
3356      * Get current user email
3357      *
3358      * @return user email
3359      */
3360     public String getEmail() {
3361         return email;
3362     }
3363 
3364     /**
3365      * Get current user alias
3366      *
3367      * @return user email
3368      */
3369     public String getAlias() {
3370         return alias;
3371     }
3372 
3373     /**
3374      * Search global address list
3375      *
3376      * @param condition           search filter
3377      * @param returningAttributes returning attributes
3378      * @param sizeLimit           size limit
3379      * @return matching contacts from gal
3380      * @throws IOException on error
3381      */
3382     public abstract Map<String, Contact> galFind(Condition condition, Set<String> returningAttributes, int sizeLimit) throws IOException;
3383 
3384     /**
3385      * Full Contact attribute list
3386      */
3387     public static final Set<String> CONTACT_ATTRIBUTES = new HashSet<String>();
3388 
3389     static {
3390         CONTACT_ATTRIBUTES.add("imapUid");
3391         CONTACT_ATTRIBUTES.add("etag");
3392         CONTACT_ATTRIBUTES.add("urlcompname");
3393 
3394         CONTACT_ATTRIBUTES.add("extensionattribute1");
3395         CONTACT_ATTRIBUTES.add("extensionattribute2");
3396         CONTACT_ATTRIBUTES.add("extensionattribute3");
3397         CONTACT_ATTRIBUTES.add("extensionattribute4");
3398         CONTACT_ATTRIBUTES.add("bday");
3399         CONTACT_ATTRIBUTES.add("anniversary");
3400         CONTACT_ATTRIBUTES.add("businesshomepage");
3401         CONTACT_ATTRIBUTES.add("personalHomePage");
3402         CONTACT_ATTRIBUTES.add("cn");
3403         CONTACT_ATTRIBUTES.add("co");
3404         CONTACT_ATTRIBUTES.add("department");
3405         CONTACT_ATTRIBUTES.add("smtpemail1");
3406         CONTACT_ATTRIBUTES.add("smtpemail2");
3407         CONTACT_ATTRIBUTES.add("smtpemail3");
3408         CONTACT_ATTRIBUTES.add("facsimiletelephonenumber");
3409         CONTACT_ATTRIBUTES.add("givenName");
3410         CONTACT_ATTRIBUTES.add("homeCity");
3411         CONTACT_ATTRIBUTES.add("homeCountry");
3412         CONTACT_ATTRIBUTES.add("homePhone");
3413         CONTACT_ATTRIBUTES.add("homePostalCode");
3414         CONTACT_ATTRIBUTES.add("homeState");
3415         CONTACT_ATTRIBUTES.add("homeStreet");
3416         CONTACT_ATTRIBUTES.add("homepostofficebox");
3417         CONTACT_ATTRIBUTES.add("l");
3418         CONTACT_ATTRIBUTES.add("manager");
3419         CONTACT_ATTRIBUTES.add("mobile");
3420         CONTACT_ATTRIBUTES.add("namesuffix");
3421         CONTACT_ATTRIBUTES.add("nickname");
3422         CONTACT_ATTRIBUTES.add("o");
3423         CONTACT_ATTRIBUTES.add("pager");
3424         CONTACT_ATTRIBUTES.add("personaltitle");
3425         CONTACT_ATTRIBUTES.add("postalcode");
3426         CONTACT_ATTRIBUTES.add("postofficebox");
3427         CONTACT_ATTRIBUTES.add("profession");
3428         CONTACT_ATTRIBUTES.add("roomnumber");
3429         CONTACT_ATTRIBUTES.add("secretarycn");
3430         CONTACT_ATTRIBUTES.add("sn");
3431         CONTACT_ATTRIBUTES.add("spousecn");
3432         CONTACT_ATTRIBUTES.add("st");
3433         CONTACT_ATTRIBUTES.add("street");
3434         CONTACT_ATTRIBUTES.add("telephoneNumber");
3435         CONTACT_ATTRIBUTES.add("title");
3436         CONTACT_ATTRIBUTES.add("description");
3437         CONTACT_ATTRIBUTES.add("im");
3438         CONTACT_ATTRIBUTES.add("middlename");
3439         CONTACT_ATTRIBUTES.add("lastmodified");
3440         CONTACT_ATTRIBUTES.add("otherstreet");
3441         CONTACT_ATTRIBUTES.add("otherstate");
3442         CONTACT_ATTRIBUTES.add("otherpostofficebox");
3443         CONTACT_ATTRIBUTES.add("otherpostalcode");
3444         CONTACT_ATTRIBUTES.add("othercountry");
3445         CONTACT_ATTRIBUTES.add("othercity");
3446         CONTACT_ATTRIBUTES.add("haspicture");
3447         CONTACT_ATTRIBUTES.add("keywords");
3448         CONTACT_ATTRIBUTES.add("othermobile");
3449         CONTACT_ATTRIBUTES.add("otherTelephone");
3450         CONTACT_ATTRIBUTES.add("gender");
3451         CONTACT_ATTRIBUTES.add("private");
3452         CONTACT_ATTRIBUTES.add("sensitivity");
3453         CONTACT_ATTRIBUTES.add("fburl");
3454     }
3455 
3456     /**
3457      * Get freebusy data string from Exchange.
3458      *
3459      * @param attendee attendee email address
3460      * @param start    start date in Exchange zulu format
3461      * @param end      end date in Exchange zulu format
3462      * @param interval freebusy interval in minutes
3463      * @return freebusy data or null
3464      * @throws IOException on error
3465      */
3466     protected abstract String getFreeBusyData(String attendee, String start, String end, int interval) throws IOException;
3467 
3468     /**
3469      * Get freebusy info for attendee between start and end date.
3470      *
3471      * @param attendee       attendee email
3472      * @param startDateValue start date
3473      * @param endDateValue   end date
3474      * @return FreeBusy info
3475      * @throws IOException on error
3476      */
3477     public FreeBusy getFreebusy(String attendee, String startDateValue, String endDateValue) throws IOException {
3478         // replace ical encoded attendee name
3479         attendee = replaceIcal4Principal(attendee);
3480 
3481         // then check that email address is valid to avoid InvalidSmtpAddress error
3482         if (attendee == null || attendee.indexOf('@') < 0 || attendee.charAt(attendee.length() - 1) == '@') {
3483             return null;
3484         }
3485 
3486         if (attendee.startsWith("mailto:") || attendee.startsWith("MAILTO:")) {
3487             attendee = attendee.substring("mailto:".length());
3488         }
3489 
3490         SimpleDateFormat exchangeZuluDateFormat = getExchangeZuluDateFormat();
3491         SimpleDateFormat icalDateFormat = getZuluDateFormat();
3492 
3493         Date startDate;
3494         Date endDate;
3495         try {
3496             if (startDateValue.length() == 8) {
3497                 startDate = parseDate(startDateValue);
3498             } else {
3499                 startDate = icalDateFormat.parse(startDateValue);
3500             }
3501             if (endDateValue.length() == 8) {
3502                 endDate = parseDate(endDateValue);
3503             } else {
3504                 endDate = icalDateFormat.parse(endDateValue);
3505             }
3506         } catch (ParseException e) {
3507             throw new DavMailException("EXCEPTION_INVALID_DATES", e.getMessage());
3508         }
3509 
3510         FreeBusy freeBusy = null;
3511         String fbdata = getFreeBusyData(attendee, exchangeZuluDateFormat.format(startDate), exchangeZuluDateFormat.format(endDate), FREE_BUSY_INTERVAL);
3512         if (fbdata != null) {
3513             freeBusy = new FreeBusy(icalDateFormat, startDate, fbdata);
3514         }
3515 
3516         if (freeBusy != null && freeBusy.knownAttendee) {
3517             return freeBusy;
3518         } else {
3519             return null;
3520         }
3521     }
3522 
3523     /**
3524      * Exchange to iCalendar Free/Busy parser.
3525      * Free time returns 0, Tentative returns 1, Busy returns 2, and Out of Office (OOF) returns 3
3526      */
3527     public static final class FreeBusy {
3528         final SimpleDateFormat icalParser;
3529         boolean knownAttendee = true;
3530         static final HashMap<Character, String> FBTYPES = new HashMap<Character, String>();
3531 
3532         static {
3533             FBTYPES.put('1', "BUSY-TENTATIVE");
3534             FBTYPES.put('2', "BUSY");
3535             FBTYPES.put('3', "BUSY-UNAVAILABLE");
3536         }
3537 
3538         final HashMap<String, StringBuilder> busyMap = new HashMap<String, StringBuilder>();
3539 
3540         StringBuilder getBusyBuffer(char type) {
3541             String fbType = FBTYPES.get(Character.valueOf(type));
3542             StringBuilder buffer = busyMap.get(fbType);
3543             if (buffer == null) {
3544                 buffer = new StringBuilder();
3545                 busyMap.put(fbType, buffer);
3546             }
3547             return buffer;
3548         }
3549 
3550         void startBusy(char type, Calendar currentCal) {
3551             if (type == '4') {
3552                 knownAttendee = false;
3553             } else if (type != '0') {
3554                 StringBuilder busyBuffer = getBusyBuffer(type);
3555                 if (busyBuffer.length() > 0) {
3556                     busyBuffer.append(',');
3557                 }
3558                 busyBuffer.append(icalParser.format(currentCal.getTime()));
3559             }
3560         }
3561 
3562         void endBusy(char type, Calendar currentCal) {
3563             if (type != '0' && type != '4') {
3564                 getBusyBuffer(type).append('/').append(icalParser.format(currentCal.getTime()));
3565             }
3566         }
3567 
3568         FreeBusy(SimpleDateFormat icalParser, Date startDate, String fbdata) {
3569             this.icalParser = icalParser;
3570             if (fbdata.length() > 0) {
3571                 Calendar currentCal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
3572                 currentCal.setTime(startDate);
3573 
3574                 startBusy(fbdata.charAt(0), currentCal);
3575                 for (int i = 1; i < fbdata.length() && knownAttendee; i++) {
3576                     currentCal.add(Calendar.MINUTE, FREE_BUSY_INTERVAL);
3577                     char previousState = fbdata.charAt(i - 1);
3578                     char currentState = fbdata.charAt(i);
3579                     if (previousState != currentState) {
3580                         endBusy(previousState, currentCal);
3581                         startBusy(currentState, currentCal);
3582                     }
3583                 }
3584                 currentCal.add(Calendar.MINUTE, FREE_BUSY_INTERVAL);
3585                 endBusy(fbdata.charAt(fbdata.length() - 1), currentCal);
3586             }
3587         }
3588 
3589         /**
3590          * Append freebusy information to buffer.
3591          *
3592          * @param buffer String buffer
3593          */
3594         public void appendTo(StringBuilder buffer) {
3595             for (Map.Entry<String, StringBuilder> entry : busyMap.entrySet()) {
3596                 buffer.append("FREEBUSY;FBTYPE=").append(entry.getKey())
3597                         .append(':').append(entry.getValue()).append((char) 13).append((char) 10);
3598             }
3599         }
3600     }
3601 
3602     protected VObject vTimezone;
3603 
3604     /**
3605      * Load and return current user OWA timezone.
3606      *
3607      * @return current timezone
3608      */
3609     public VObject getVTimezone() {
3610         if (vTimezone == null) {
3611             // need to load Timezone info from OWA
3612             loadVtimezone();
3613         }
3614         return vTimezone;
3615     }
3616 
3617     protected abstract void loadVtimezone();
3618 
3619     /**
3620      * Return internal HttpClient instance
3621      *
3622      * @return http client
3623      */
3624     public HttpClient getHttpClient() {
3625         return httpClient;
3626     }
3627 }