View Javadoc
1   /*
2    * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
3    * Copyright (C) 2010  Mickael Guessant
4    *
5    * This program is free software; you can redistribute it and/or
6    * modify it under the terms of the GNU General Public License
7    * as published by the Free Software Foundation; either version 2
8    * of the License, or (at your option) any later version.
9    *
10   * This program is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU General Public License for more details.
14   *
15   * You should have received a copy of the GNU General Public License
16   * along with this program; if not, write to the Free Software
17   * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
18   */
19  
20  package davmail.exchange.auth;
21  
22  import davmail.BundleMessage;
23  import davmail.exception.DavMailAuthenticationException;
24  import davmail.exception.DavMailException;
25  import davmail.exception.WebdavNotAvailableException;
26  import davmail.http.DavGatewayOTPPrompt;
27  import davmail.http.HttpClientAdapter;
28  import davmail.http.URIUtil;
29  import davmail.http.request.GetRequest;
30  import davmail.http.request.PostRequest;
31  import davmail.http.request.ResponseWrapper;
32  import davmail.util.StringUtil;
33  import org.apache.http.HttpStatus;
34  import org.apache.http.client.methods.CloseableHttpResponse;
35  import org.apache.http.client.methods.HttpGet;
36  import org.apache.http.client.methods.HttpRequestBase;
37  import org.apache.http.client.protocol.HttpClientContext;
38  import org.apache.http.client.utils.URIBuilder;
39  import org.apache.http.cookie.Cookie;
40  import org.apache.http.impl.client.BasicCookieStore;
41  import org.apache.http.impl.cookie.BasicClientCookie;
42  import org.apache.log4j.Logger;
43  import org.htmlcleaner.BaseToken;
44  import org.htmlcleaner.CommentNode;
45  import org.htmlcleaner.ContentNode;
46  import org.htmlcleaner.HtmlCleaner;
47  import org.htmlcleaner.TagNode;
48  
49  import javax.imageio.ImageIO;
50  import java.awt.image.BufferedImage;
51  import java.io.ByteArrayInputStream;
52  import java.io.IOException;
53  import java.net.ConnectException;
54  import java.net.URI;
55  import java.net.URISyntaxException;
56  import java.net.UnknownHostException;
57  import java.nio.charset.StandardCharsets;
58  import java.util.ArrayList;
59  import java.util.HashSet;
60  import java.util.List;
61  import java.util.Set;
62  
63  /**
64   * New Exchange form authenticator based on HttpClient 4.
65   */
66  public class ExchangeFormAuthenticator implements ExchangeAuthenticator {
67      protected static final Logger LOGGER = Logger.getLogger("davmail.exchange.ExchangeSession");
68  
69      /**
70       * Various username fields found on custom Exchange authentication forms
71       */
72      protected static final Set<String> USER_NAME_FIELDS = new HashSet<>();
73  
74      static {
75          USER_NAME_FIELDS.add("username");
76          USER_NAME_FIELDS.add("txtusername");
77          USER_NAME_FIELDS.add("userid");
78          USER_NAME_FIELDS.add("SafeWordUser");
79          USER_NAME_FIELDS.add("user_name");
80          USER_NAME_FIELDS.add("login");
81          USER_NAME_FIELDS.add("UserName");
82      }
83  
84      /**
85       * Various password fields found on custom Exchange authentication forms
86       */
87      protected static final Set<String> PASSWORD_FIELDS = new HashSet<>();
88  
89      static {
90          PASSWORD_FIELDS.add("password");
91          PASSWORD_FIELDS.add("txtUserPass");
92          PASSWORD_FIELDS.add("pw");
93          PASSWORD_FIELDS.add("basicPassword");
94          PASSWORD_FIELDS.add("passwd");
95          PASSWORD_FIELDS.add("Password");
96      }
97  
98      /**
99       * Various OTP (one time password) fields found on custom Exchange authentication forms.
100      * Used to open OTP dialog
101      */
102     protected static final Set<String> TOKEN_FIELDS = new HashSet<>();
103 
104     static {
105         TOKEN_FIELDS.add("SafeWordPassword");
106         TOKEN_FIELDS.add("passcode");
107     }
108 
109 
110     /**
111      * User provided username.
112      * Old preauth syntax: preauthusername"username
113      * Windows authentication with domain: domain\\username
114      * Note that OSX Mail.app does not support backslash in username, set default domain in DavMail settings instead
115      */
116     private String username;
117     /**
118      * User provided password
119      */
120     private String password;
121     /**
122      * OWA or EWS url
123      */
124     private String url;
125     /**
126      * HttpClient 4 adapter
127      */
128     private HttpClientAdapter httpClientAdapter;
129     /**
130      * A OTP pre-auth page may require a different username.
131      */
132     private String preAuthusername;
133 
134     /**
135      * Logon form user name fields.
136      */
137     private final List<String> usernameInputs = new ArrayList<>();
138     /**
139      * Logon form password field, default is password.
140      */
141     private String passwordInput = null;
142     /**
143      * Tells if, during the login navigation, an OTP pre-auth page has been found.
144      */
145     private boolean otpPreAuthFound = false;
146     /**
147      * Lets the user try again a couple of times to enter the OTP pre-auth key before giving up.
148      */
149     private int otpPreAuthRetries = 0;
150     /**
151      * Maximum number of times the user can try to input again the OTP pre-auth key before giving up.
152      */
153     private static final int MAX_OTP_RETRIES = 3;
154 
155     /**
156      * base Exchange URI after authentication
157      */
158     private java.net.URI exchangeUri;
159 
160     @Override
161     public void setUsername(String username) {
162         this.username = username;
163     }
164 
165     @Override
166     public void setPassword(String password) {
167         this.password = password;
168     }
169 
170     public void setUrl(String url) {
171         this.url = url;
172     }
173 
174     @Override
175     public void authenticate() throws DavMailException {
176         try {
177             // create HttpClient adapter, enable pooling as this instance will be passed to ExchangeSession
178             httpClientAdapter = new HttpClientAdapter(url, true);
179             boolean isHttpAuthentication = isHttpAuthentication(httpClientAdapter, url);
180 
181             // The user may have configured an OTP pre-auth username. It is processed
182             // so early because OTP pre-auth may disappear in the Exchange LAN and this
183             // helps the user to not change is account settings in mail client at each network change.
184             if (preAuthusername == null) {
185                 // Searches for the delimiter in configured username for the pre-auth user.
186                 // The double-quote is not allowed inside email addresses anyway.
187                 int doubleQuoteIndex = this.username.indexOf('"');
188                 if (doubleQuoteIndex > 0) {
189                     preAuthusername = this.username.substring(0, doubleQuoteIndex);
190                     this.username = this.username.substring(doubleQuoteIndex + 1);
191                 } else {
192                     // No doublequote: the pre-auth user is the full username, or it is not used at all.
193                     preAuthusername = this.username;
194                 }
195             }
196 
197             // set real credentials on http client
198             httpClientAdapter.setCredentials(username, password);
199 
200             // get webmail root url
201             // providing credentials
202             // manually follow redirect
203             GetRequest getRequest = httpClientAdapter.executeFollowRedirect(new GetRequest(url));
204 
205             if (!this.isAuthenticated(getRequest)) {
206                 if (isHttpAuthentication) {
207                     int status = getRequest.getStatusCode();
208 
209                     if (status == HttpStatus.SC_UNAUTHORIZED) {
210                         throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED");
211                     } else if (status != HttpStatus.SC_OK) {
212                         throw HttpClientAdapter.buildHttpResponseException(getRequest, getRequest.getHttpResponse());
213                     }
214                     // workaround for basic authentication on /exchange and form based authentication at /owa
215                     if ("/owa/auth/logon.aspx".equals(getRequest.getURI().getPath())) {
216                         formLogin(httpClientAdapter, getRequest, password);
217                     }
218                 } else {
219                     formLogin(httpClientAdapter, getRequest, password);
220                 }
221             }
222 
223         } catch (DavMailAuthenticationException exc) {
224             close();
225             LOGGER.error(exc.getMessage());
226             throw exc;
227         } catch (ConnectException | UnknownHostException exc) {
228             close();
229             BundleMessage message = new BundleMessage("EXCEPTION_CONNECT", exc.getClass().getName(), exc.getMessage());
230             LOGGER.error(message);
231             throw new DavMailException("EXCEPTION_DAVMAIL_CONFIGURATION", message);
232         } catch (WebdavNotAvailableException exc) {
233             close();
234             throw exc;
235         } catch (IOException exc) {
236             close();
237             LOGGER.error(BundleMessage.formatLog("EXCEPTION_EXCHANGE_LOGIN_FAILED", exc));
238             throw new DavMailException("EXCEPTION_EXCHANGE_LOGIN_FAILED", exc);
239         }
240         LOGGER.debug("Successfully authenticated to " + exchangeUri);
241     }
242 
243     /**
244      * Test authentication mode : form based or basic.
245      *
246      * @param url        exchange base URL
247      * @param httpClient httpClientAdapter instance
248      * @return true if basic authentication detected
249      */
250     protected boolean isHttpAuthentication(HttpClientAdapter httpClient, String url) {
251         boolean isHttpAuthentication = false;
252         HttpGet httpGet = new HttpGet(url);
253         // Create a local context to avoid cookies in main httpClient
254         HttpClientContext context = HttpClientContext.create();
255         context.setCookieStore(new BasicCookieStore());
256         try (CloseableHttpResponse response = httpClient.execute(httpGet, context)) {
257             isHttpAuthentication = response.getStatusLine().getStatusCode() == HttpStatus.SC_UNAUTHORIZED;
258         } catch (IOException e) {
259             // ignore
260         }
261         return isHttpAuthentication;
262     }
263 
264     /**
265      * Look for session cookies.
266      *
267      * @return true if session cookies are available
268      */
269     protected boolean isAuthenticated(ResponseWrapper getRequest) {
270         boolean authenticated = false;
271         if (getRequest.getStatusCode() == HttpStatus.SC_OK
272                 && "/ews/services.wsdl".equalsIgnoreCase(getRequest.getURI().getPath())) {
273             // direct EWS access returned wsdl
274             authenticated = true;
275         } else {
276             // check cookies
277             for (Cookie cookie : httpClientAdapter.getCookies()) {
278                 // Exchange 2003 cookies
279                 if (cookie.getName().startsWith("cadata") || "sessionid".equals(cookie.getName())
280                         // Exchange 2007 cookie
281                         || "UserContext".equals(cookie.getName())
282                         // Federated Authentication
283                         || "TimeWindowSig".equals(cookie.getName())
284                 ) {
285                     authenticated = true;
286                     break;
287                 }
288             }
289         }
290         return authenticated;
291     }
292 
293     protected void formLogin(HttpClientAdapter httpClient, ResponseWrapper initRequest, String password) throws IOException {
294         LOGGER.debug("Form based authentication detected");
295 
296         PostRequest postRequest = buildLogonMethod(httpClient, initRequest);
297         if (postRequest == null) {
298             LOGGER.debug("Authentication form not found at " + initRequest.getURI() + ", trying default url");
299             postRequest = new PostRequest("/owa/auth/owaauth.dll");
300         }
301 
302         exchangeUri = postLogonMethod(httpClient, postRequest, password).getURI();
303     }
304 
305     /**
306      * Try to find logon method path from logon form body.
307      *
308      * @param httpClient      httpClientAdapter instance
309      * @param responseWrapper init request response wrapper
310      * @return logon method
311      */
312     protected PostRequest buildLogonMethod(HttpClientAdapter httpClient, ResponseWrapper responseWrapper) {
313         PostRequest logonMethod = null;
314 
315         // create an instance of HtmlCleaner
316         HtmlCleaner cleaner = new HtmlCleaner();
317         // In the federated auth flow, an input field may contain a saml xml assertion with > characters
318         cleaner.getProperties().setAllowHtmlInsideAttributes(true);
319 
320         // A OTP token authentication form in a previous page could have username fields with different names
321         usernameInputs.clear();
322 
323         try {
324             URI uri = responseWrapper.getURI();
325             String responseBody = responseWrapper.getResponseBodyAsString();
326             TagNode node = cleaner.clean(new ByteArrayInputStream(responseBody.getBytes(StandardCharsets.UTF_8)));
327             List<? extends TagNode> forms = node.getElementListByName("form", true);
328             TagNode logonForm = null;
329             // select form
330             if (forms.size() == 1) {
331                 logonForm = forms.get(0);
332             } else if (forms.size() > 1) {
333                 for (TagNode form : forms) {
334                     if ("logonForm".equals(form.getAttributeByName("name"))) {
335                         logonForm = form;
336                     } else if ("loginForm".equals(form.getAttributeByName("id"))) {
337                         logonForm = form;
338                     }
339 
340                 }
341             }
342             if (logonForm != null) {
343                 String logonMethodPath = logonForm.getAttributeByName("action");
344 
345                 // workaround for broken form with empty action
346                 if (logonMethodPath != null && logonMethodPath.isEmpty()) {
347                     logonMethodPath = "/owa/auth.owa";
348                 }
349 
350                 logonMethod = new PostRequest(getAbsoluteUri(uri, logonMethodPath));
351 
352                 // retrieve lost inputs attached to body
353                 List<? extends TagNode> inputList = node.getElementListByName("input", true);
354 
355                 for (TagNode input : inputList) {
356                     String type = input.getAttributeByName("type");
357                     String name = input.getAttributeByName("name");
358                     String value = input.getAttributeByName("value");
359                     if ("hidden".equalsIgnoreCase(type) && name != null && value != null) {
360                         // decode XML SAML assertion correctly from hidden field value
361                         if ("wresult".equals(name)) {
362                             String decoded = value.replaceAll("&quot;","\"").replaceAll("&lt;","<");
363                             logonMethod.setParameter(name, decoded);
364                             // The OWA accepting this assertion needs the Referer set, but it can be anything
365                             logonMethod.setRequestHeader("Referer", url);
366                         } else if ("wctx".equals(name)) {
367                             String decoded = value.replaceAll("&amp;","&");
368                             logonMethod.setParameter(name, decoded);
369                         } else {
370                             logonMethod.setParameter(name, value);
371                         }
372                     }
373                     // custom login form
374                     if (USER_NAME_FIELDS.contains(name) && !usernameInputs.contains(name)) {
375                         usernameInputs.add(name);
376                     } else if (PASSWORD_FIELDS.contains(name)) {
377                         passwordInput = name;
378                     } else if ("addr".equals(name)) {
379                         // this is not a logon form but a redirect form
380                         logonMethod = buildLogonMethod(httpClient, httpClient.executeFollowRedirect(logonMethod));
381                     } else if (TOKEN_FIELDS.contains(name)) {
382                         // one time password, ask it to the user
383                         logonMethod.setParameter(name, DavGatewayOTPPrompt.getOneTimePassword());
384                     } else if ("otc".equals(name)) {
385                         // captcha image, get image and ask user
386                         String pinsafeUser = getAliasFromLogin();
387                         if (pinsafeUser == null) {
388                             pinsafeUser = username;
389                         }
390                         HttpGet pinRequest = new HttpGet("/PINsafeISAFilter.dll?username=" + pinsafeUser);
391                         try (CloseableHttpResponse pinResponse = httpClient.execute(pinRequest)) {
392                             int status = pinResponse.getStatusLine().getStatusCode();
393                             if (status != HttpStatus.SC_OK) {
394                                 throw HttpClientAdapter.buildHttpResponseException(pinRequest, pinResponse.getStatusLine());
395                             }
396                             BufferedImage captchaImage = ImageIO.read(pinResponse.getEntity().getContent());
397                             logonMethod.setParameter(name, DavGatewayOTPPrompt.getCaptchaValue(captchaImage));
398                         }
399                     }
400                 }
401             } else {
402                 List<? extends TagNode> frameList = node.getElementListByName("frame", true);
403                 if (frameList.size() == 1) {
404                     String src = frameList.get(0).getAttributeByName("src");
405                     if (src != null) {
406                         LOGGER.debug("Frames detected in form page, try frame content");
407                         logonMethod = buildLogonMethod(httpClient, httpClient.executeFollowRedirect(new GetRequest(src)));
408                     }
409                 } else {
410                     // another failover for script based logon forms (Exchange 2007)
411                     List<? extends TagNode> scriptList = node.getElementListByName("script", true);
412                     for (TagNode script : scriptList) {
413                         List<? extends BaseToken> contents = script.getAllChildren();
414                         for (Object content : contents) {
415                             if (content instanceof CommentNode) {
416                                 String scriptValue = ((CommentNode) content).getCommentedContent();
417                                 String sUrl = StringUtil.getToken(scriptValue, "var a_sUrl = \"", "\"");
418                                 String sLgn = StringUtil.getToken(scriptValue, "var a_sLgnQS = \"", "\"");
419                                 if (sLgn == null) {
420                                     sLgn = StringUtil.getToken(scriptValue, "var a_sLgn = \"", "\"");
421                                 }
422                                 if (sUrl != null && sLgn != null) {
423                                     URI src = getScriptBasedFormURL(uri, sLgn + sUrl);
424                                     LOGGER.debug("Detected script based logon, redirect to form at " + src);
425                                     logonMethod = buildLogonMethod(httpClient, httpClient.executeFollowRedirect(new GetRequest(src)));
426                                 }
427 
428                             } else if (content instanceof ContentNode) {
429                                 // Microsoft Forefront Unified Access Gateway redirect
430                                 String scriptValue = ((ContentNode) content).getContent();
431                                 String location = StringUtil.getToken(scriptValue, "window.location.replace(\"", "\"");
432                                 if (location != null) {
433                                     LOGGER.debug("Post logon redirect to: " + location);
434                                     logonMethod = buildLogonMethod(httpClient, httpClient.executeFollowRedirect(new GetRequest(location)));
435                                 }
436                             }
437                         }
438                     }
439                 }
440             }
441         } catch (IOException | URISyntaxException e) {
442             LOGGER.error("Error parsing login form at " + responseWrapper.getURI());
443         }
444 
445         return logonMethod;
446     }
447 
448 
449     protected ResponseWrapper postLogonMethod(HttpClientAdapter httpClient, PostRequest logonMethod, String password) throws IOException {
450 
451         setAuthFormFields(logonMethod, httpClient, password);
452 
453         // add exchange 2010 PBack cookie in compatibility mode
454         BasicClientCookie pBackCookie = new BasicClientCookie("PBack", "0");
455         pBackCookie.setPath("/");
456         pBackCookie.setDomain(httpClientAdapter.getHost());
457         httpClient.addCookie(pBackCookie);
458 
459         ResponseWrapper resultRequest = httpClient.executeFollowRedirect(logonMethod);
460 
461         // test form based authentication
462         checkFormLoginQueryString(resultRequest);
463 
464         // workaround for post logon script redirect
465         if (!isAuthenticated(resultRequest)) {
466             // try to get new method from script based redirection
467             logonMethod = buildLogonMethod(httpClient, resultRequest);
468 
469             if (logonMethod != null) {
470                 if (otpPreAuthFound && otpPreAuthRetries < MAX_OTP_RETRIES) {
471                     // A OTP pre-auth page has been found, it is needed to restart the login process.
472                     // This applies to both the case the user entered a good OTP code (the usual login process
473                     // takes place) and the case the user entered a wrong OTP code (another code will be asked to him).
474                     // The user has up to MAX_OTP_RETRIES chances to input a valid OTP key.
475                     return postLogonMethod(httpClient, logonMethod, password);
476                 }
477 
478                 // if logonMethod is not null, try to follow redirection
479                 resultRequest = httpClient.executeFollowRedirect(logonMethod);
480 
481                 checkFormLoginQueryString(resultRequest);
482                 // also check cookies
483                 if (!isAuthenticated(resultRequest)) {
484                     throwAuthenticationFailed();
485                 }
486             } else {
487                 // authentication failed
488                 throwAuthenticationFailed();
489             }
490         }
491 
492         // check for language selection form
493         if ("/owa/languageselection.aspx".equals(resultRequest.getURI().getPath())) {
494             // need to submit form
495             resultRequest = submitLanguageSelectionForm(resultRequest.getURI(), resultRequest.getResponseBodyAsString());
496         }
497         return resultRequest;
498     }
499 
500     protected ResponseWrapper submitLanguageSelectionForm(URI uri, String responseBodyAsString) throws IOException {
501         PostRequest postLanguageFormMethod;
502         // create an instance of HtmlCleaner
503         HtmlCleaner cleaner = new HtmlCleaner();
504 
505         try {
506             TagNode node = cleaner.clean(responseBodyAsString);
507             List<? extends TagNode> forms = node.getElementListByName("form", true);
508             TagNode languageForm;
509             // select form
510             if (forms.size() == 1) {
511                 languageForm = forms.get(0);
512             } else {
513                 throw new IOException("Form not found");
514             }
515             String languageMethodPath = languageForm.getAttributeByName("action");
516 
517             postLanguageFormMethod = new PostRequest(getAbsoluteUri(uri, languageMethodPath));
518 
519             List<? extends TagNode> inputList = languageForm.getElementListByName("input", true);
520             for (TagNode input : inputList) {
521                 String name = input.getAttributeByName("name");
522                 String value = input.getAttributeByName("value");
523                 if (name != null && value != null) {
524                     postLanguageFormMethod.setParameter(name, value);
525                 }
526             }
527             List<? extends TagNode> selectList = languageForm.getElementListByName("select", true);
528             for (TagNode select : selectList) {
529                 String name = select.getAttributeByName("name");
530                 List<? extends TagNode> optionList = select.getElementListByName("option", true);
531                 String value = null;
532                 for (TagNode option : optionList) {
533                     if (option.getAttributeByName("selected") != null) {
534                         value = option.getAttributeByName("value");
535                         break;
536                     }
537                 }
538                 if (name != null && value != null) {
539                     postLanguageFormMethod.setParameter(name, value);
540                 }
541             }
542         } catch (IOException | URISyntaxException e) {
543             String errorMessage = "Error parsing language selection form at " + uri;
544             LOGGER.error(errorMessage);
545             throw new IOException(errorMessage);
546         }
547 
548         return httpClientAdapter.executeFollowRedirect(postLanguageFormMethod);
549     }
550 
551     protected void setAuthFormFields(HttpRequestBase logonMethod, HttpClientAdapter httpClient, String password) throws IllegalArgumentException {
552         String usernameInput;
553         if (usernameInputs.size() == 2) {
554             String userid;
555             // multiple username fields, split userid|username on |
556             int pipeIndex = username.indexOf('|');
557             if (pipeIndex < 0) {
558                 LOGGER.debug("Multiple user fields detected, please use userid|username as user name in client, except when userid is username");
559                 userid = username;
560             } else {
561                 userid = username.substring(0, pipeIndex);
562                 username = username.substring(pipeIndex + 1);
563                 // adjust credentials
564                 httpClient.setCredentials(username, password);
565             }
566             ((PostRequest) logonMethod).removeParameter("userid");
567             ((PostRequest) logonMethod).setParameter("userid", userid);
568 
569             usernameInput = "username";
570         } else if (usernameInputs.size() == 1) {
571             // simple username field
572             usernameInput = usernameInputs.get(0);
573         } else {
574             // should not happen
575             usernameInput = "username";
576         }
577         // make sure username and password fields are empty
578         ((PostRequest) logonMethod).removeParameter(usernameInput);
579         if (passwordInput != null) {
580             ((PostRequest) logonMethod).removeParameter(passwordInput);
581         }
582         ((PostRequest) logonMethod).removeParameter("trusted");
583         ((PostRequest) logonMethod).removeParameter("flags");
584 
585         if (passwordInput == null) {
586             // This is a OTP pre-auth page. A different username may be required.
587             otpPreAuthFound = true;
588             otpPreAuthRetries++;
589             ((PostRequest) logonMethod).setParameter(usernameInput, preAuthusername);
590         } else {
591             otpPreAuthFound = false;
592             otpPreAuthRetries = 0;
593             // This is a regular Exchange login page
594             ((PostRequest) logonMethod).setParameter(usernameInput, username);
595             ((PostRequest) logonMethod).setParameter(passwordInput, password);
596             ((PostRequest) logonMethod).setParameter("trusted", "4");
597             ((PostRequest) logonMethod).setParameter("flags", "4");
598         }
599     }
600 
601     protected URI getAbsoluteUri(URI uri, String path) throws URISyntaxException {
602         URIBuilder uriBuilder = new URIBuilder(uri);
603         if (path != null) {
604             // reset query string
605             uriBuilder.clearParameters();
606             if (path.startsWith("/")) {
607                 // path is absolute, replace method path
608                 uriBuilder.setPath(path);
609             } else if (path.startsWith("http://") || path.startsWith("https://")) {
610                 return URI.create(path);
611             } else {
612                 // relative path, build new path
613                 String currentPath = uri.getPath();
614                 int end = currentPath.lastIndexOf('/');
615                 if (end >= 0) {
616                     uriBuilder.setPath(currentPath.substring(0, end + 1) + path);
617                 } else {
618                     throw new URISyntaxException(uriBuilder.build().toString(), "Invalid path");
619                 }
620             }
621         }
622         return uriBuilder.build();
623     }
624 
625     protected URI getScriptBasedFormURL(URI uri, String pathQuery) throws URISyntaxException, IOException {
626         URIBuilder uriBuilder = new URIBuilder(uri);
627         int queryIndex = pathQuery.indexOf('?');
628         if (queryIndex >= 0) {
629             if (queryIndex > 0) {
630                 // update path
631                 String newPath = pathQuery.substring(0, queryIndex);
632                 if (newPath.startsWith("/")) {
633                     // absolute path
634                     uriBuilder.setPath(newPath);
635                 } else {
636                     String currentPath = uriBuilder.getPath();
637                     int folderIndex = currentPath.lastIndexOf('/');
638                     if (folderIndex >= 0) {
639                         // replace relative path
640                         uriBuilder.setPath(currentPath.substring(0, folderIndex + 1) + newPath);
641                     } else {
642                         // should not happen
643                         uriBuilder.setPath('/' + newPath);
644                     }
645                 }
646             }
647             uriBuilder.setCustomQuery(URIUtil.decode(pathQuery.substring(queryIndex + 1)));
648         }
649         return uriBuilder.build();
650     }
651 
652     protected void checkFormLoginQueryString(ResponseWrapper logonMethod) throws DavMailAuthenticationException {
653         String queryString = logonMethod.getURI().getRawQuery();
654         if (queryString != null && (queryString.contains("reason=2") || queryString.contains("reason=4"))) {
655             throwAuthenticationFailed();
656         }
657     }
658 
659     protected void throwAuthenticationFailed() throws DavMailAuthenticationException {
660         if (this.username != null && this.username.contains("\\")) {
661             throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED");
662         } else {
663             throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED_RETRY");
664         }
665     }
666 
667     /**
668      * Get current Exchange alias name from login name
669      *
670      * @return user name
671      */
672     public String getAliasFromLogin() {
673         // login is email, not alias
674         if (this.username.indexOf('@') >= 0) {
675             return null;
676         }
677         String result = this.username;
678         // remove domain name
679         int index = Math.max(result.indexOf('\\'), result.indexOf('/'));
680         if (index >= 0) {
681             result = result.substring(index + 1);
682         }
683         return result;
684     }
685 
686     /**
687      * Close session.
688      * Shutdown http client connection manager
689      */
690     public void close() {
691         httpClientAdapter.close();
692     }
693 
694     /**
695      * Oauth token.
696      * Only for Office 365 authenticators
697      *
698      * @return unsupported
699      */
700     @Override
701     public O365Token getToken() {
702         throw new UnsupportedOperationException();
703     }
704 
705     /**
706      * Base Exchange URL.
707      * Welcome page for Exchange 2003, EWS url for Exchange 2007 and later
708      *
709      * @return Exchange url
710      */
711     @Override
712     public java.net.URI getExchangeUri() {
713         return exchangeUri;
714     }
715 
716     /**
717      * Return authenticated HttpClient 4 HttpClientAdapter
718      *
719      * @return HttpClientAdapter instance
720      */
721     public HttpClientAdapter getHttpClientAdapter() {
722         return httpClientAdapter;
723     }
724 
725     /**
726      * Actual username.
727      * may be different from input username with preauth
728      *
729      * @return username
730      */
731     public String getUsername() {
732         return username;
733     }
734 }
735