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.exchange.auth.ExchangeAuthenticator;
27  import davmail.exchange.auth.ExchangeFormAuthenticator;
28  import davmail.exchange.dav.DavExchangeSession;
29  import davmail.exchange.ews.EwsExchangeSession;
30  import davmail.exchange.graph.GraphExchangeSession;
31  import davmail.http.HttpClientAdapter;
32  import davmail.http.request.GetRequest;
33  import org.apache.http.HttpStatus;
34  import org.apache.http.client.methods.CloseableHttpResponse;
35  
36  import java.awt.*;
37  import java.io.IOException;
38  import java.net.NetworkInterface;
39  import java.net.SocketException;
40  import java.net.UnknownHostException;
41  import java.util.Enumeration;
42  import java.util.HashMap;
43  import java.util.Map;
44  import java.util.Objects;
45  
46  /**
47   * Create ExchangeSession instances.
48   */
49  public final class ExchangeSessionFactory {
50      private static final Object LOCK = new Object();
51      private static final Map<PoolKey, ExchangeSession> POOL_MAP = new HashMap<>();
52      private static volatile boolean configChecked;
53      private static volatile boolean errorSent;
54  
55      static class PoolKey {
56          final String url;
57          final String userName;
58          final String password;
59  
60          PoolKey(String url, String userName, String password) {
61              this.url = url;
62              this.userName = convertUserName(userName);
63              this.password = password;
64          }
65  
66          @Override
67          public boolean equals(Object object) {
68              return object == this ||
69                      object instanceof PoolKey &&
70                              ((PoolKey) object).url.equals(this.url) &&
71                              ((PoolKey) object).userName.equals(this.userName) &&
72                              ((PoolKey) object).password.equals(this.password);
73          }
74  
75          @Override
76          public int hashCode() {
77              return Objects.hash(url, userName, password);
78          }
79      }
80  
81      private ExchangeSessionFactory() {
82      }
83  
84      /**
85       * Create authenticated Exchange session
86       *
87       * @param userName user login
88       * @param password user password
89       * @return authenticated session
90       * @throws IOException on error
91       */
92      public static ExchangeSession getInstance(String userName, String password) throws IOException {
93          String baseUrl = Settings.getProperty("davmail.url", Settings.getO365Url());
94          if (Settings.getBooleanProperty("davmail.server")) {
95              return getInstance(baseUrl, userName, password);
96          } else {
97              // serialize session creation in workstation mode to avoid multiple OTP requests
98              synchronized (LOCK) {
99                  return getInstance(baseUrl, userName, password);
100             }
101         }
102     }
103 
104     private static String convertUserName(String userName) {
105         String result = userName;
106         // prepend default windows domain prefix
107         String defaultDomain = Settings.getProperty("davmail.defaultDomain");
108         if (defaultDomain != null && userName.indexOf('\\') < 0 && userName.indexOf('@') < 0) {
109             result = defaultDomain + '\\' + userName;
110         }
111         return result;
112     }
113 
114     /**
115      * Create authenticated Exchange session
116      *
117      * @param baseUrl  OWA base URL
118      * @param userName user login
119      * @param password user password
120      * @return authenticated session
121      * @throws IOException on error
122      */
123     public static ExchangeSession getInstance(String baseUrl, String userName, String password) throws IOException {
124         ExchangeSession session = null;
125         try {
126             String mode = Settings.getProperty("davmail.mode");
127             if (Settings.O365.equals(mode)) {
128                 // force url with O365
129                 baseUrl = Settings.getO365Url();
130             }
131 
132             PoolKey poolKey = new PoolKey(baseUrl, userName, password);
133 
134             synchronized (LOCK) {
135                 session = POOL_MAP.get(poolKey);
136             }
137             if (session != null) {
138                 ExchangeSession.LOGGER.debug("Got session " + session + " from cache");
139             }
140 
141             if (session != null && session.isExpired()) {
142                 synchronized (LOCK) {
143                     session.close();
144                     ExchangeSession.LOGGER.debug("Session " + session + " for user " + session.userName + " expired");
145                     session = null;
146                     // expired session, remove from cache
147                     POOL_MAP.remove(poolKey);
148                 }
149             }
150 
151             if (session == null) {
152                 // convert old setting
153                 if (mode == null) {
154                     if ("false".equals(Settings.getProperty("davmail.enableEws"))) {
155                         mode = Settings.WEBDAV;
156                     } else {
157                         mode = Settings.EWS;
158                     }
159                 }
160                 // check for overridden authenticator
161                 String authenticatorClass = Settings.getProperty("davmail.authenticator");
162                 if (authenticatorClass == null && mode.startsWith("O365")) {
163                     // set authenticator class based on authentication mode
164                     String authentication = Settings.getProperty("davmail.authentication");
165                     authenticatorClass = getAuthenticatorClass(authentication);
166                     if (authenticatorClass == null) {
167                         // fallback to legacy modes
168                         authenticatorClass = getAuthenticatorClass(mode);
169                     }
170                 }
171 
172                 if (authenticatorClass != null) {
173                     ExchangeAuthenticator authenticator = (ExchangeAuthenticator) Class.forName(authenticatorClass)
174                             .getDeclaredConstructor().newInstance();
175                     authenticator.setUsername(poolKey.userName);
176                     authenticator.setPassword(poolKey.password);
177                     authenticator.authenticate();
178 
179                     if (Settings.isGraphEnabled()) {
180                         session = new GraphExchangeSession(authenticator.getHttpClientAdapter(), authenticator.getToken(), poolKey.userName);
181                     } else {
182                         session = new EwsExchangeSession(authenticator.getExchangeUri(), authenticator.getToken(), poolKey.userName);
183                     }
184 
185                 } else if (
186                     // new mode
187                         Settings.EXCHANGE_EWS.equals(mode)
188                                 // legacy
189                                 || Settings.EWS.equals(mode) || Settings.O365.equals(mode)
190                                 // direct EWS even if mode is different
191                                 || poolKey.url.toLowerCase().endsWith("/ews/exchange.asmx")
192                                 || poolKey.url.toLowerCase().endsWith("/ews/services.wsdl")) {
193                     if (poolKey.url.toLowerCase().endsWith("/ews/exchange.asmx")
194                             || poolKey.url.toLowerCase().endsWith("/ews/services.wsdl")) {
195                         ExchangeSession.LOGGER.debug("Direct EWS authentication");
196                         session = new EwsExchangeSession(poolKey.url, poolKey.userName, poolKey.password);
197                     } else {
198                         ExchangeSession.LOGGER.debug("OWA authentication in EWS mode");
199                         ExchangeFormAuthenticator exchangeFormAuthenticator = new ExchangeFormAuthenticator();
200                         exchangeFormAuthenticator.setUrl(poolKey.url);
201                         exchangeFormAuthenticator.setUsername(poolKey.userName);
202                         exchangeFormAuthenticator.setPassword(poolKey.password);
203                         exchangeFormAuthenticator.authenticate();
204                         session = new EwsExchangeSession(exchangeFormAuthenticator.getHttpClientAdapter(),
205                                 exchangeFormAuthenticator.getExchangeUri(), exchangeFormAuthenticator.getUsername());
206                     }
207                 } else {
208                     // Legacy Exchange 2003 WebDav mode
209                     ExchangeFormAuthenticator exchangeFormAuthenticator = new ExchangeFormAuthenticator();
210                     exchangeFormAuthenticator.setUrl(poolKey.url);
211                     exchangeFormAuthenticator.setUsername(poolKey.userName);
212                     exchangeFormAuthenticator.setPassword(poolKey.password);
213                     exchangeFormAuthenticator.authenticate();
214                     try {
215                         session = new DavExchangeSession(exchangeFormAuthenticator.getHttpClientAdapter(),
216                                 exchangeFormAuthenticator.getExchangeUri(),
217                                 exchangeFormAuthenticator.getUsername());
218                     } catch (WebdavNotAvailableException e) {
219                         if (Settings.AUTO.equals(mode)) {
220                             ExchangeSession.LOGGER.debug(e.getMessage() + ", retry with EWS");
221                             session = new EwsExchangeSession(poolKey.url, poolKey.userName, poolKey.password);
222                         } else {
223                             throw e;
224                         }
225                     }
226                 }
227                 try {
228                     checkWhiteList(session.getEmail());
229                 } catch (DavMailAuthenticationException e) {
230                     session.close();
231                     session = null;
232                     throw e;
233                 }
234                 ExchangeSession.LOGGER.debug("Created new session " + session + " for user " + poolKey.userName);
235             }
236             // successful login, put session in cache
237             synchronized (LOCK) {
238                 POOL_MAP.put(poolKey, session);
239             }
240             // session opened; future failure will mean network down
241             configChecked = true;
242             // Reset so next time a problem occurs message is sent once
243             errorSent = false;
244         } catch (DavMailException | IllegalStateException | NullPointerException exc) {
245             throw exc;
246         } catch (Exception exc) {
247             handleNetworkDown(exc);
248         }
249         return session;
250     }
251 
252     private static String getAuthenticatorClass(String authentication) throws DavMailException {
253         String authenticatorClass = null;
254         if (authentication != null) {
255             switch (authentication) {
256                 case Settings.O365_INTERACTIVE:
257                     authenticatorClass = "davmail.exchange.auth.O365InteractiveAuthenticator";
258                     if (GraphicsEnvironment.isHeadless()) {
259                         throw new DavMailException("EXCEPTION_DAVMAIL_CONFIGURATION", "O365Interactive not supported in headless mode");
260                     }
261                     break;
262                 case Settings.O365_MANUAL:
263                     authenticatorClass = "davmail.exchange.auth.O365ManualAuthenticator";
264                     break;
265                 case Settings.O365_DEVICECODE:
266                     authenticatorClass = "davmail.exchange.auth.O365DeviceCodeAuthenticator";
267                     break;
268                 case Settings.O365_TRANSPARENT:
269                 case Settings.O365_MODERN:
270                     authenticatorClass = "davmail.exchange.auth.O365Authenticator";
271                     break;
272             }
273         }
274         return authenticatorClass;
275     }
276 
277     /**
278      * Check if whitelist is empty or email is allowed.
279      * userWhiteList is a comma-separated list of values.
280      * \@company.com means all domain users are allowed
281      *
282      * @param email user email
283      */
284     private static void checkWhiteList(String email) throws DavMailAuthenticationException {
285         String whiteListString = Settings.getProperty("davmail.userWhiteList");
286         if (whiteListString != null && !whiteListString.isEmpty()) {
287             for (String whiteListValue : whiteListString.split(",")) {
288                 if (whiteListValue.startsWith("@") && email.endsWith(whiteListValue)) {
289                     return;
290                 } else if (email.equalsIgnoreCase(whiteListValue)) {
291                     return;
292                 }
293             }
294             ExchangeSession.LOGGER.warn(email + " not allowed by whitelist");
295             throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED");
296         }
297     }
298 
299     /**
300      * Get a non-expired session.
301      * If the current session is not expired, return current session, else try to create a new session
302      *
303      * @param currentSession current session
304      * @param userName       user login
305      * @param password       user password
306      * @return authenticated session
307      * @throws IOException on error
308      */
309     public static ExchangeSession getInstance(ExchangeSession currentSession, String userName, String password)
310             throws IOException {
311         ExchangeSession session = currentSession;
312         try {
313             if (session.isExpired()) {
314                 ExchangeSession.LOGGER.debug("Session " + session + " expired, trying to open a new one");
315                 session = null;
316                 String baseUrl = Settings.getProperty("davmail.url", Settings.getO365Url());
317                 PoolKey poolKey = new PoolKey(baseUrl, userName, password);
318                 // expired session, remove from cache
319                 synchronized (LOCK) {
320                     POOL_MAP.remove(poolKey);
321                 }
322                 session = getInstance(userName, password);
323             }
324         } catch (DavMailAuthenticationException exc) {
325             ExchangeSession.LOGGER.debug("Unable to reopen session", exc);
326             throw exc;
327         } catch (Exception exc) {
328             ExchangeSession.LOGGER.debug("Unable to reopen session", exc);
329             handleNetworkDown(exc);
330         }
331         return session;
332     }
333 
334     /**
335      * Send a request to Exchange server to check current settings.
336      *
337      * @throws IOException if unable to access Exchange server
338      */
339     public static void checkConfig() throws IOException {
340         String url;
341         if (Settings.isGraphEnabled()) {
342             // In Graph mode, check the Graph endpoint instead of the legacy EWS endpoint
343             url = Settings.getGraphUrl();
344         } else {
345             url = Settings.getProperty("davmail.url", Settings.getO365Url());
346         }
347         if (url == null || (!url.startsWith("http://") && !url.startsWith("https://"))) {
348             throw new DavMailException("LOG_INVALID_URL", url);
349         }
350         try (
351                 HttpClientAdapter httpClientAdapter = new HttpClientAdapter(url);
352                 CloseableHttpResponse response = httpClientAdapter.execute(new GetRequest(url))
353         ) {
354             // get webMail root url (will not follow redirects)
355             int status = response.getStatusLine().getStatusCode();
356             ExchangeSession.LOGGER.debug("Test configuration status: " + status);
357             if (status != HttpStatus.SC_OK && status != HttpStatus.SC_UNAUTHORIZED
358                     && !HttpClientAdapter.isRedirect(status)) {
359                 throw new DavMailException("EXCEPTION_CONNECTION_FAILED", url, status);
360             }
361             // session opened; future failure will mean network down
362             configChecked = true;
363             // Reset so next time a problem occurs message is sent once
364             errorSent = false;
365         } catch (Exception exc) {
366             handleNetworkDown(exc);
367         }
368 
369     }
370 
371     private static void handleNetworkDown(Exception exc) throws DavMailException {
372         if (!checkNetwork() || configChecked) {
373             ExchangeSession.LOGGER.warn(BundleMessage.formatLog("EXCEPTION_NETWORK_DOWN"));
374             // log full stack trace for unknown errors
375             if (!((exc instanceof UnknownHostException) || (exc instanceof NetworkDownException))) {
376                 ExchangeSession.LOGGER.debug(exc, exc);
377             }
378             throw new NetworkDownException("EXCEPTION_NETWORK_DOWN");
379         } else {
380             BundleMessage message = new BundleMessage("EXCEPTION_CONNECT", exc.getClass().getName(), exc.getMessage());
381             if (errorSent) {
382                 ExchangeSession.LOGGER.warn(message);
383                 throw new NetworkDownException("EXCEPTION_DAVMAIL_CONFIGURATION", message);
384             } else {
385                 // Mark that an error has been sent so you only get one
386                 // error in a row (not a repeating string of errors).
387                 errorSent = true;
388                 ExchangeSession.LOGGER.error(message);
389                 throw new DavMailException("EXCEPTION_DAVMAIL_CONFIGURATION", message);
390             }
391         }
392     }
393 
394     /**
395      * Get user password from session pool for SASL authentication
396      *
397      * @param userName Exchange username
398      * @return user password
399      */
400     public static String getUserPassword(String userName) {
401         String fullUserName = convertUserName(userName);
402         synchronized (LOCK) {
403             for (PoolKey poolKey : POOL_MAP.keySet()) {
404                 if (poolKey.userName.equals(fullUserName)) {
405                     return poolKey.password;
406                 }
407             }
408         }
409         return null;
410     }
411 
412     /**
413      * Check if at least one network interface is up and active (i.e., has an address)
414      *
415      * @return true if network available
416      */
417     static boolean checkNetwork() {
418         boolean up = false;
419         Enumeration<NetworkInterface> enumeration;
420         try {
421             enumeration = NetworkInterface.getNetworkInterfaces();
422             while (!up && enumeration.hasMoreElements()) {
423                 NetworkInterface networkInterface = enumeration.nextElement();
424                 up = networkInterface.isUp() && !networkInterface.isLoopback()
425                         && networkInterface.getInetAddresses().hasMoreElements();
426                 if (up) {
427                     ExchangeSession.LOGGER.debug(networkInterface.getDisplayName() + " is up");
428                 }
429             }
430         } catch (NoSuchMethodError error) {
431             ExchangeSession.LOGGER.debug("Unable to test network interfaces (not available under Java 1.5)");
432             up = true;
433         } catch (SocketException exc) {
434             ExchangeSession.LOGGER.error("DavMail configuration exception: \n Error listing network interfaces " + exc.getMessage(), exc);
435         }
436         return up;
437     }
438 
439     /**
440      * Reset config check status and clear session pool.
441      */
442     public static void shutdown() {
443         configChecked = false;
444         errorSent = false;
445         synchronized (LOCK) {
446             for (ExchangeSession session : POOL_MAP.values()) {
447                 session.close();
448             }
449             POOL_MAP.clear();
450         }
451     }
452 }