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) {
162                     switch (mode) {
163                         case Settings.O365_MODERN:
164                             authenticatorClass = "davmail.exchange.auth.O365Authenticator";
165                             break;
166                         case Settings.O365_INTERACTIVE:
167                             authenticatorClass = "davmail.exchange.auth.O365InteractiveAuthenticator";
168                             if (GraphicsEnvironment.isHeadless()) {
169                                 throw new DavMailException("EXCEPTION_DAVMAIL_CONFIGURATION", "O365Interactive not supported in headless mode");
170                             }
171                             break;
172                         case Settings.O365_MANUAL:
173                             authenticatorClass = "davmail.exchange.auth.O365ManualAuthenticator";
174                             break;
175                     }
176                 }
177 
178                 if (authenticatorClass != null) {
179                     ExchangeAuthenticator authenticator = (ExchangeAuthenticator) Class.forName(authenticatorClass)
180                             .getDeclaredConstructor().newInstance();
181                     authenticator.setUsername(poolKey.userName);
182                     authenticator.setPassword(poolKey.password);
183                     authenticator.authenticate();
184 
185                     if (Settings.getBooleanProperty("davmail.enableGraph", false)) {
186                         session = new GraphExchangeSession(authenticator.getHttpClientAdapter(), authenticator.getToken(), poolKey.userName);
187                     } else {
188                         session = new EwsExchangeSession(authenticator.getExchangeUri(), authenticator.getToken(), poolKey.userName);
189                     }
190 
191                 } else if (Settings.EWS.equals(mode) || Settings.O365.equals(mode)
192                         // direct EWS even if mode is different
193                         || poolKey.url.toLowerCase().endsWith("/ews/exchange.asmx")
194                         || poolKey.url.toLowerCase().endsWith("/ews/services.wsdl")) {
195                     if (poolKey.url.toLowerCase().endsWith("/ews/exchange.asmx")
196                             || poolKey.url.toLowerCase().endsWith("/ews/services.wsdl")) {
197                         ExchangeSession.LOGGER.debug("Direct EWS authentication");
198                         session = new EwsExchangeSession(poolKey.url, poolKey.userName, poolKey.password);
199                     } else {
200                         ExchangeSession.LOGGER.debug("OWA authentication in EWS mode");
201                         ExchangeFormAuthenticator exchangeFormAuthenticator = new ExchangeFormAuthenticator();
202                         exchangeFormAuthenticator.setUrl(poolKey.url);
203                         exchangeFormAuthenticator.setUsername(poolKey.userName);
204                         exchangeFormAuthenticator.setPassword(poolKey.password);
205                         exchangeFormAuthenticator.authenticate();
206                         session = new EwsExchangeSession(exchangeFormAuthenticator.getHttpClientAdapter(),
207                                 exchangeFormAuthenticator.getExchangeUri(), exchangeFormAuthenticator.getUsername());
208                     }
209                 } else {
210                     ExchangeFormAuthenticator exchangeFormAuthenticator = new ExchangeFormAuthenticator();
211                     exchangeFormAuthenticator.setUrl(poolKey.url);
212                     exchangeFormAuthenticator.setUsername(poolKey.userName);
213                     exchangeFormAuthenticator.setPassword(poolKey.password);
214                     exchangeFormAuthenticator.authenticate();
215                     try {
216                         session = new DavExchangeSession(exchangeFormAuthenticator.getHttpClientAdapter(),
217                                 exchangeFormAuthenticator.getExchangeUri(),
218                                 exchangeFormAuthenticator.getUsername());
219                     } catch (WebdavNotAvailableException e) {
220                         if (Settings.AUTO.equals(mode)) {
221                             ExchangeSession.LOGGER.debug(e.getMessage() + ", retry with EWS");
222                             session = new EwsExchangeSession(poolKey.url, poolKey.userName, poolKey.password);
223                         } else {
224                             throw e;
225                         }
226                     }
227                 }
228                 checkWhiteList(session.getEmail());
229                 ExchangeSession.LOGGER.debug("Created new session " + session + " for user " + poolKey.userName);
230             }
231             // successful login, put session in cache
232             synchronized (LOCK) {
233                 POOL_MAP.put(poolKey, session);
234             }
235             // session opened, future failure will mean network down
236             configChecked = true;
237             // Reset so next time an problem occurs message will be sent once
238             errorSent = false;
239         } catch (DavMailException | IllegalStateException | NullPointerException exc) {
240             throw exc;
241         } catch (Exception exc) {
242             handleNetworkDown(exc);
243         }
244         return session;
245     }
246 
247     /**
248      * Check if whitelist is empty or email is allowed.
249      * userWhiteList is a comma separated list of values.
250      * \@company.com means all domain users are allowed
251      *
252      * @param email user email
253      */
254     private static void checkWhiteList(String email) throws DavMailAuthenticationException {
255         String whiteListString = Settings.getProperty("davmail.userWhiteList");
256         if (whiteListString != null && !whiteListString.isEmpty()) {
257             for (String whiteListvalue : whiteListString.split(",")) {
258                 if (whiteListvalue.startsWith("@") && email.endsWith(whiteListvalue)) {
259                     return;
260                 } else if (email.equalsIgnoreCase(whiteListvalue)) {
261                     return;
262                 }
263             }
264             ExchangeSession.LOGGER.warn(email + " not allowed by whitelist");
265             throw new DavMailAuthenticationException("EXCEPTION_AUTHENTICATION_FAILED");
266         }
267     }
268 
269     /**
270      * Get a non expired session.
271      * If the current session is not expired, return current session, else try to create a new session
272      *
273      * @param currentSession current session
274      * @param userName       user login
275      * @param password       user password
276      * @return authenticated session
277      * @throws IOException on error
278      */
279     public static ExchangeSession getInstance(ExchangeSession currentSession, String userName, String password)
280             throws IOException {
281         ExchangeSession session = currentSession;
282         try {
283             if (session.isExpired()) {
284                 ExchangeSession.LOGGER.debug("Session " + session + " expired, trying to open a new one");
285                 session = null;
286                 String baseUrl = Settings.getProperty("davmail.url", Settings.getO365Url());
287                 PoolKey poolKey = new PoolKey(baseUrl, userName, password);
288                 // expired session, remove from cache
289                 synchronized (LOCK) {
290                     POOL_MAP.remove(poolKey);
291                 }
292                 session = getInstance(userName, password);
293             }
294         } catch (DavMailAuthenticationException exc) {
295             ExchangeSession.LOGGER.debug("Unable to reopen session", exc);
296             throw exc;
297         } catch (Exception exc) {
298             ExchangeSession.LOGGER.debug("Unable to reopen session", exc);
299             handleNetworkDown(exc);
300         }
301         return session;
302     }
303 
304     /**
305      * Send a request to Exchange server to check current settings.
306      *
307      * @throws IOException if unable to access Exchange server
308      */
309     public static void checkConfig() throws IOException {
310         String url = Settings.getProperty("davmail.url", Settings.getO365Url());
311         if (url == null || (!url.startsWith("http://") && !url.startsWith("https://"))) {
312             throw new DavMailException("LOG_INVALID_URL", url);
313         }
314         try (
315                 HttpClientAdapter httpClientAdapter = new HttpClientAdapter(url);
316                 CloseableHttpResponse response = httpClientAdapter.execute(new GetRequest(url))
317         ) {
318             // get webMail root url (will not follow redirects)
319             int status = response.getStatusLine().getStatusCode();
320             ExchangeSession.LOGGER.debug("Test configuration status: " + status);
321             if (status != HttpStatus.SC_OK && status != HttpStatus.SC_UNAUTHORIZED
322                     && !HttpClientAdapter.isRedirect(status)) {
323                 throw new DavMailException("EXCEPTION_CONNECTION_FAILED", url, status);
324             }
325             // session opened, future failure will mean network down
326             configChecked = true;
327             // Reset so next time an problem occurs message will be sent once
328             errorSent = false;
329         } catch (Exception exc) {
330             handleNetworkDown(exc);
331         }
332 
333     }
334 
335     private static void handleNetworkDown(Exception exc) throws DavMailException {
336         if (!checkNetwork() || configChecked) {
337             ExchangeSession.LOGGER.warn(BundleMessage.formatLog("EXCEPTION_NETWORK_DOWN"));
338             // log full stack trace for unknown errors
339             if (!((exc instanceof UnknownHostException) || (exc instanceof NetworkDownException))) {
340                 ExchangeSession.LOGGER.debug(exc, exc);
341             }
342             throw new NetworkDownException("EXCEPTION_NETWORK_DOWN");
343         } else {
344             BundleMessage message = new BundleMessage("EXCEPTION_CONNECT", exc.getClass().getName(), exc.getMessage());
345             if (errorSent) {
346                 ExchangeSession.LOGGER.warn(message);
347                 throw new NetworkDownException("EXCEPTION_DAVMAIL_CONFIGURATION", message);
348             } else {
349                 // Mark that an error has been sent so you only get one
350                 // error in a row (not a repeating string of errors).
351                 errorSent = true;
352                 ExchangeSession.LOGGER.error(message);
353                 throw new DavMailException("EXCEPTION_DAVMAIL_CONFIGURATION", message);
354             }
355         }
356     }
357 
358     /**
359      * Get user password from session pool for SASL authentication
360      *
361      * @param userName Exchange user name
362      * @return user password
363      */
364     public static String getUserPassword(String userName) {
365         String fullUserName = convertUserName(userName);
366         for (PoolKey poolKey : POOL_MAP.keySet()) {
367             if (poolKey.userName.equals(fullUserName)) {
368                 return poolKey.password;
369             }
370         }
371         return null;
372     }
373 
374     /**
375      * Check if at least one network interface is up and active (i.e. has an address)
376      *
377      * @return true if network available
378      */
379     static boolean checkNetwork() {
380         boolean up = false;
381         Enumeration<NetworkInterface> enumeration;
382         try {
383             enumeration = NetworkInterface.getNetworkInterfaces();
384             if (enumeration != null) {
385                 while (!up && enumeration.hasMoreElements()) {
386                     NetworkInterface networkInterface = enumeration.nextElement();
387                     up = networkInterface.isUp() && !networkInterface.isLoopback()
388                             && networkInterface.getInetAddresses().hasMoreElements();
389                 }
390             }
391         } catch (NoSuchMethodError error) {
392             ExchangeSession.LOGGER.debug("Unable to test network interfaces (not available under Java 1.5)");
393             up = true;
394         } catch (SocketException exc) {
395             ExchangeSession.LOGGER.error("DavMail configuration exception: \n Error listing network interfaces " + exc.getMessage(), exc);
396         }
397         return up;
398     }
399 
400     /**
401      * Reset config check status and clear session pool.
402      */
403     public static void shutdown() {
404         configChecked = false;
405         errorSent = false;
406         synchronized (LOCK) {
407             for (ExchangeSession session:POOL_MAP.values()) {
408                 session.close();
409             }
410             POOL_MAP.clear();
411         }
412     }
413 }