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