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