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