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