001/* 002 * Copyright 2017 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.plugins.zimbra; 017 018import java.io.IOException; 019import java.io.StringReader; 020import java.io.UnsupportedEncodingException; 021import java.net.SocketTimeoutException; 022import java.net.UnknownHostException; 023import java.nio.charset.StandardCharsets; 024import java.security.InvalidKeyException; 025import java.security.NoSuchAlgorithmException; 026import java.time.Duration; 027import java.time.Instant; 028import java.time.ZonedDateTime; 029import java.util.ArrayList; 030import java.util.Collections; 031import java.util.Date; 032import java.util.List; 033import java.util.Map; 034import java.util.TreeMap; 035import java.util.stream.Collectors; 036 037import javax.crypto.Mac; 038import javax.crypto.spec.SecretKeySpec; 039 040import org.apache.avalon.framework.activity.Disposable; 041import org.apache.avalon.framework.service.ServiceException; 042import org.apache.avalon.framework.service.ServiceManager; 043import org.apache.cocoon.ProcessingException; 044import org.apache.cocoon.environment.Redirector; 045import org.apache.commons.codec.binary.Hex; 046import org.apache.commons.lang3.StringUtils; 047import org.apache.excalibur.xml.dom.DOMParser; 048import org.apache.excalibur.xml.xpath.PrefixResolver; 049import org.apache.excalibur.xml.xpath.XPathProcessor; 050import org.apache.http.HeaderElement; 051import org.apache.http.HeaderElementIterator; 052import org.apache.http.HttpResponse; 053import org.apache.http.NameValuePair; 054import org.apache.http.client.config.RequestConfig; 055import org.apache.http.client.methods.CloseableHttpResponse; 056import org.apache.http.client.methods.HttpGet; 057import org.apache.http.client.methods.HttpPost; 058import org.apache.http.client.protocol.HttpClientContext; 059import org.apache.http.client.utils.URLEncodedUtils; 060import org.apache.http.conn.ConnectionKeepAliveStrategy; 061import org.apache.http.conn.ConnectionPoolTimeoutException; 062import org.apache.http.cookie.Cookie; 063import org.apache.http.entity.StringEntity; 064import org.apache.http.impl.client.CloseableHttpClient; 065import org.apache.http.impl.client.HttpClients; 066import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; 067import org.apache.http.message.BasicHeaderElementIterator; 068import org.apache.http.message.BasicNameValuePair; 069import org.apache.http.protocol.HTTP; 070import org.apache.http.protocol.HttpContext; 071import org.apache.http.util.EntityUtils; 072import org.w3c.dom.Document; 073import org.w3c.dom.Element; 074import org.w3c.dom.Node; 075import org.w3c.dom.NodeList; 076import org.xml.sax.InputSource; 077import org.xml.sax.SAXException; 078 079import org.ametys.core.cache.AbstractCacheManager; 080import org.ametys.core.cache.Cache; 081import org.ametys.core.user.User; 082import org.ametys.core.user.UserIdentity; 083import org.ametys.core.user.UserManager; 084import org.ametys.core.util.JSONUtils; 085import org.ametys.plugins.messagingconnector.AbstractMessagingConnector; 086import org.ametys.plugins.messagingconnector.CalendarEvent; 087import org.ametys.plugins.messagingconnector.EmailMessage; 088import org.ametys.plugins.messagingconnector.MessagingConnectorException; 089import org.ametys.runtime.config.Config; 090import org.ametys.runtime.i18n.I18nizableText; 091 092/** 093 * The connector used by the messaging connector plugin when the zimbra mail 094 * server is used. Implements the methods of the MessagingConnector interface in 095 * order to get the informations from the mail server 096 */ 097public class ZimbraConnector extends AbstractMessagingConnector implements Disposable 098{ 099 /** token cache id */ 100 public static final String TOKEN_CACHE = ZimbraConnector.class.getName() + "$token"; 101 102 /** The number of seconds after what kept alive connections are dropt */ 103 protected static final int _DROP_KEPTALIVE_CONNECTION_AFTER = 5; 104 105 private static final ZimbraPrefixResolver __PREFIX_RESOLVER = new ZimbraPrefixResolver(); 106 107 /** The user manager */ 108 protected UserManager _usersManager; 109 110 /** The JSON Utils */ 111 protected JSONUtils _jsonUtils; 112 113 /** Url to zimbra */ 114 protected String _zimbraUrl; 115 116 /** Preauth secret key */ 117 protected String _domainPreauthSecretKey; 118 119 /** Request to the remote app will be ppoled for perfs purposes */ 120 protected PoolingHttpClientConnectionManager _connectionManager; 121 122 /** The keep-alive stragegy to optimize http clients */ 123 protected ConnectionKeepAliveStrategy _connectionKeepAliveStrategy; 124 125 /** The shared configuration for request (for timeout purposes) */ 126 protected RequestConfig _connectionConfig; 127 128 private XPathProcessor _xPathProcessor; 129 private DOMParser _domParser; 130 131 @Override 132 public void service(ServiceManager smanager) throws ServiceException 133 { 134 super.service(smanager); 135 _usersManager = (UserManager) smanager.lookup(UserManager.ROLE); 136 _jsonUtils = (JSONUtils) smanager.lookup(JSONUtils.ROLE); 137 _cacheManager = (AbstractCacheManager) smanager.lookup(AbstractCacheManager.ROLE); 138 _xPathProcessor = (XPathProcessor) smanager.lookup(XPathProcessor.ROLE); 139 _domParser = (DOMParser) smanager.lookup(DOMParser.ROLE); 140 } 141 142 @Override 143 public void initialize() 144 { 145 super.initialize(); 146 _zimbraUrl = StringUtils.removeEnd(Config.getInstance().getValue("zimbra.config.zimbra.baseUrl"), "/"); 147 _domainPreauthSecretKey = Config.getInstance().getValue("zimbra.config.preauth.key"); 148 149 int maxSimultaneousConnections = (int) (long) Config.getInstance().getValue("zimbra.config.maxconnections"); 150 // Same value for 3 timeouts (socket, connection and response) so one connection can last 3 times this value 151 int connectionTimeout = (int) Math.max(0, (long) Config.getInstance().getValue("zimbra.config.timeout")); 152 153 // A pooling connection manager to avoid flooding remote server by reusing existing connections AND by limiting their number 154 _connectionManager = new PoolingHttpClientConnectionManager(); 155 _connectionManager.setMaxTotal(maxSimultaneousConnections > 0 ? maxSimultaneousConnections : Integer.MAX_VALUE); 156 _connectionManager.setDefaultMaxPerRoute(maxSimultaneousConnections > 0 ? maxSimultaneousConnections : Integer.MAX_VALUE); 157 158 // inspired from http://www.baeldung.com/httpclient-connection-management 159 // to keep a connection alive for a few seconds if the remote server did not send back this information 160 _connectionKeepAliveStrategy = new ConnectionKeepAliveStrategy() 161 { 162 @Override 163 public long getKeepAliveDuration(HttpResponse response, HttpContext context) 164 { 165 HeaderElementIterator it = new BasicHeaderElementIterator(response.headerIterator(HTTP.CONN_KEEP_ALIVE)); 166 while (it.hasNext()) 167 { 168 HeaderElement he = it.nextElement(); 169 String param = he.getName(); 170 String value = he.getValue(); 171 if (value != null && param.equalsIgnoreCase("timeout")) 172 { 173 try 174 { 175 return Long.parseLong(value) * 1000; 176 } 177 catch (NumberFormatException ignore) 178 { 179 // Ignore 180 } 181 } 182 } 183 return _DROP_KEPTALIVE_CONNECTION_AFTER * 1000; 184 } 185 }; 186 187 _connectionConfig = RequestConfig.custom() 188 .setConnectTimeout(connectionTimeout * 1000) 189 .setConnectionRequestTimeout(connectionTimeout * 1000) // Time to get an object from the pool 190 .setSocketTimeout(connectionTimeout * 1000).build(); 191 192 long tokenDuration = Math.max(0, (long) Config.getInstance().getValue("zimbra.config.tokenDuration")); 193 194 _cacheManager.createMemoryCache(TOKEN_CACHE, 195 new I18nizableText("plugin.zimbra", "PLUGINS_ZIMBRA_CACHE_TOKEN_LABEL"), 196 new I18nizableText("plugin.zimbra", "PLUGINS_ZIMBRA_CACHE_TOKEN_DESCRIPTION"), 197 false, 198 Duration.ofMinutes(tokenDuration)); 199 } 200 201 public void dispose() 202 { 203 _connectionManager.close(); 204 } 205 206 /** 207 * Get a new pooled http client. Do not forget to close it. 208 * @return The client 209 */ 210 protected CloseableHttpClient _getHttpClient() 211 { 212 return HttpClients.custom() 213 .setConnectionManager(_connectionManager) 214 .setConnectionManagerShared(true) // avoid automatic pool closing 215 .setKeepAliveStrategy(_connectionKeepAliveStrategy) 216 .setDefaultRequestConfig(_connectionConfig) 217 .build(); 218 } 219 220 /** 221 * Preauth user and redirect to the zimbra application 222 * @param redirector The redirector 223 * @param targetApp The zimbra application (ex: mail) 224 * @throws ProcessingException if failed to redirect 225 * @throws IOException if failed to redirect 226 */ 227 public void redirect(Redirector redirector, String targetApp) throws ProcessingException, IOException 228 { 229 UserIdentity identity = _currentUserProvider.getUser(); 230 User user = _usersManager.getUser(identity); 231 232 if (user != null) 233 { 234 String qs = _computeQueryString(user, targetApp); 235 if (qs != null) 236 { 237 redirector.redirect(false, _zimbraUrl + "/service/preauth?" + qs); 238 } 239 } 240 } 241 242 243 /** 244 * Zimbra preauth request to log the current user into zimbra and retrieve the ZM_AUTH_TOKEN 245 * @param userIdentity The user for which the preauth request will be done. 246 * @return The Zimbra ZM_AUTH_TOKEN which can be used in future request made through the Zimbra REST API or <code>null</code> if user is null or has no email. 247 * @throws MessagingConnectorException if failed to get zimbra token for user 248 */ 249 protected String _doPreauthRequest(UserIdentity userIdentity) 250 { 251 User user = _usersManager.getUser(userIdentity); 252 // Computing query string 253 String qs = _computeQueryString(user, null); 254 if (qs == null) 255 { 256 return null; 257 } 258 259 HttpGet get = new HttpGet(_zimbraUrl + "/service/preauth?" + qs); 260 261 if (getLogger().isDebugEnabled()) 262 { 263 getLogger().debug("ZimbraConnector: performing preauth request for user {}", user.getIdentity()); 264 } 265 266 try (CloseableHttpClient httpClient = _getHttpClient()) 267 { 268 HttpClientContext context = HttpClientContext.create(); 269 try (CloseableHttpResponse response = httpClient.execute(get, context)) 270 { 271 List<Cookie> cookies = context.getCookieStore().getCookies(); 272 273 for (Cookie cookie : cookies) 274 { 275 if (StringUtils.equals(cookie.getName(), "ZM_AUTH_TOKEN")) 276 { 277 return cookie.getValue(); 278 } 279 } 280 281 throw new MessagingConnectorException("Zimbra authentification failed for user " + user.getEmail(), MessagingConnectorException.ExceptionType.CONFIGURATION_EXCEPTION); 282 } 283 } 284 catch (UnknownHostException e) 285 { 286 throw new MessagingConnectorException("Unknown host for zimbra server. Giving up to proceed to the Zimbra preauth action for user " + user, MessagingConnectorException.ExceptionType.CONFIGURATION_EXCEPTION, e); 287 } 288 catch (ConnectionPoolTimeoutException | SocketTimeoutException e) 289 { 290 throw new MessagingConnectorException("There are already too many connections to zimbra server. Giving up to proceed to the Zimbra preauth action for user " + user, MessagingConnectorException.ExceptionType.TIMEOUT, e); 291 } 292 catch (IOException e) 293 { 294 throw new MessagingConnectorException("Unable to proceed to the Zimbra preauth action for user : " + user.getEmail(), MessagingConnectorException.ExceptionType.UNKNOWN, e); 295 } 296 } 297 298 private String _computeQueryString(User user, String targetApp) 299 { 300 if (user == null) 301 { 302 return null; 303 } 304 305 String zimbraUser = user.getEmail(); 306 307 if (StringUtils.isEmpty(zimbraUser)) 308 { 309 if (getLogger().isDebugEnabled()) 310 { 311 getLogger().debug("Cannot retreive zimbra information with empty email for user " + user); 312 } 313 return null; 314 } 315 316 String timestamp = String.valueOf(System.currentTimeMillis()); 317 String computedPreauth = null; 318 319 try 320 { 321 computedPreauth = _getComputedPreauth(zimbraUser, timestamp, _domainPreauthSecretKey); 322 } 323 catch (Exception e) 324 { 325 throw new MessagingConnectorException("Unable to compute the preauth key during the Zimbra preauth action for user : " + zimbraUser, MessagingConnectorException.ExceptionType.UNKNOWN, e); 326 } 327 328 // Preauth request parameters 329 List<NameValuePair> params = new ArrayList<>(); 330 params.add(new BasicNameValuePair("account", zimbraUser)); 331 params.add(new BasicNameValuePair("timestamp", timestamp)); 332 params.add(new BasicNameValuePair("expires", "0")); 333 params.add(new BasicNameValuePair("preauth", computedPreauth)); 334 if (targetApp != null) 335 { 336 params.add(new BasicNameValuePair("redirectURL", "/?app=" + targetApp)); 337 } 338 339 // Computing query string 340 return URLEncodedUtils.format(params, StandardCharsets.UTF_8); 341 } 342 343 /** 344 * Compute the preauth key. 345 * @param zimbraUser The Zimbra User 346 * @param timestamp The timestamp 347 * @param secretKey The secret key 348 * @return The computed preauth key 349 * @throws NoSuchAlgorithmException if no Provider supports a MacSpi 350 * implementation for the specified algorithm (HmacSHA1). 351 * @throws InvalidKeyException if the given key is inappropriate for 352 * initializing the MAC 353 * @throws UnsupportedEncodingException If the named charset (UTF-8) is not 354 * supported 355 */ 356 protected String _getComputedPreauth(String zimbraUser, String timestamp, String secretKey) throws NoSuchAlgorithmException, InvalidKeyException, UnsupportedEncodingException 357 { 358 SecretKeySpec signingKey = new SecretKeySpec(secretKey.getBytes(), "HmacSHA1"); 359 Mac mac = Mac.getInstance("HmacSHA1"); 360 mac.init(signingKey); 361 362 String data = StringUtils.join(new String[] {zimbraUser, "name", "0", timestamp}, '|'); 363 byte[] rawHmac = mac.doFinal(data.getBytes()); 364 byte[] hexBytes = new Hex().encode(rawHmac); 365 return new String(hexBytes, "UTF-8"); 366 } 367 368 @Override 369 protected List<CalendarEvent> internalGetEvents(UserIdentity userIdentity, int maxDays, int maxEvents) throws MessagingConnectorException 370 { 371 Document response = _getSoapEvents(userIdentity, maxDays); 372 373 if (response == null) 374 { 375 return Collections.EMPTY_LIST; 376 } 377 378 NodeList events = _xPathProcessor.selectNodeList(response, "/soap:Envelope/soap:Body/mail:SearchResponse/mail:appt/mail:inst", __PREFIX_RESOLVER); 379 int count = 0; 380 Map<Long, CalendarEvent> calendarEvents = new TreeMap<>(); 381 while (count < events.getLength()) 382 { 383 Element item = (Element) events.item(count); 384 385 String rawStart = _getAttribute(item, "s"); 386 long longStart = Long.parseLong(rawStart); 387 Instant startInst = Instant.ofEpochMilli(longStart); 388 Date start = Date.from(startInst); 389 390 String duration = _getAttribute(item, "dur"); 391 Instant endInst = startInst.plusMillis(Long.parseLong(duration)); 392 Date end = Date.from(endInst); 393 394 String name = _getAttribute(item, "name"); 395 String location = _getAttribute(item, "loc"); 396 397 CalendarEvent newEvent = new CalendarEvent(); 398 newEvent.setStartDate(start); 399 newEvent.setEndDate(end); 400 newEvent.setSubject(name); 401 newEvent.setLocation(location); 402 calendarEvents.put(longStart, newEvent); 403 404 count++; 405 } 406 407 return calendarEvents.entrySet().stream().limit(maxEvents).map(e -> e.getValue()).collect(Collectors.toList()); 408 } 409 410 private String _getAttribute(Element item, String name) 411 { 412 String value = item.getAttribute(name); 413 if (StringUtils.isEmpty(value)) 414 { 415 value = ((Element) item.getParentNode()).getAttribute(name); 416 } 417 418 return value; 419 } 420 421 @Override 422 protected int internalGetEventsCount(UserIdentity userIdentity, int maxDays) throws MessagingConnectorException 423 { 424 Document response = _getSoapEvents(userIdentity, maxDays); 425 426 if (response == null) 427 { 428 return 0; 429 } 430 431 return _xPathProcessor.evaluateAsNumber(response, "count(/soap:Envelope/soap:Body/mail:SearchResponse/mail:appt/mail:inst)", __PREFIX_RESOLVER).intValue(); 432 } 433 434 private Document _getSoapEvents(UserIdentity userIdentity, int maxDays) 435 { 436 // Connection to the zimbra mail server 437 String authToken = _getToken(userIdentity); 438 439 if (StringUtils.isEmpty(authToken)) 440 { 441 return null; 442 } 443 444 ZonedDateTime now = ZonedDateTime.now(); 445 String soapRequest = "<soap:Envelope xmlns:soap=\"http://www.w3.org/2003/05/soap-envelope\">" 446 + " <soap:Header>" 447 + " <context xmlns=\"urn:zimbra\">" 448 + " <authToken>" + authToken + "</authToken>" 449 + " </context>" 450 + " </soap:Header>" 451 + " <soap:Body>" 452 + " <SearchRequest xmlns=\"urn:zimbraMail\" types=\"appointment\" calExpandInstStart=\"" + now.toInstant().toEpochMilli() + "\" calExpandInstEnd =\"" + now.plusDays(maxDays).toInstant().toEpochMilli() + "\" >" 453 + " <query>in:calendar</query>" 454 + " </SearchRequest>" 455 + " </soap:Body>" 456 + "</soap:Envelope>"; 457 458 HttpPost req = new HttpPost(_zimbraUrl + "/service/soap"); 459 req.setEntity(new StringEntity(soapRequest, StandardCharsets.UTF_8)); 460 461 try (CloseableHttpClient httpClient = _getHttpClient(); CloseableHttpResponse response = httpClient.execute(req, HttpClientContext.create())) 462 { 463 String content = EntityUtils.toString(response.getEntity()); 464 465 Document document = _domParser.parseDocument(new InputSource(new StringReader(content))); 466 467 String failure = _xPathProcessor.evaluateAsString(document, "/soap:Envelope/soap:Body/soap:Fault/soap:Reason/soap:Text", __PREFIX_RESOLVER); 468 469 if (StringUtils.isEmpty(failure)) 470 { 471 return document; 472 } 473 else 474 { 475 getLogger().warn("Zimbra failed to return next events for user {}: {}", userIdentity, failure); 476 return null; 477 } 478 } 479 catch (IOException | SAXException e) 480 { 481 throw new MessagingConnectorException("Failed to get Zimbra events for user " + userIdentity, MessagingConnectorException.ExceptionType.UNKNOWN, e); 482 } 483 } 484 485 @Override 486 protected List<EmailMessage> internalGetEmails(UserIdentity userIdentity, int maxEmails) throws MessagingConnectorException 487 { 488 // Connection to the zimbra mail server 489 String authToken = _getToken(userIdentity); 490 491 if (StringUtils.isEmpty(authToken)) 492 { 493 return Collections.EMPTY_LIST; 494 } 495 496 String soapRequest = "<soap:Envelope xmlns:soap=\"http://www.w3.org/2003/05/soap-envelope\">" 497 + " <soap:Header>" 498 + " <context xmlns=\"urn:zimbra\">" 499 + " <authToken>" + authToken + "</authToken>" 500 + " </context>" 501 + " </soap:Header>" 502 + " <soap:Body>" 503 + " <SearchRequest xmlns=\"urn:zimbraMail\" types=\"message\" limit=\"" + maxEmails + "\">" 504 + " <query>is:unread in:inbox</query>" 505 + " </SearchRequest>" 506 + " </soap:Body>" 507 + "</soap:Envelope>"; 508 509 HttpPost req = new HttpPost(_zimbraUrl + "/service/soap"); 510 req.setEntity(new StringEntity(soapRequest, StandardCharsets.UTF_8)); 511 512 try (CloseableHttpClient httpClient = _getHttpClient(); CloseableHttpResponse response = httpClient.execute(req, HttpClientContext.create())) 513 { 514 String content = EntityUtils.toString(response.getEntity()); 515 516 Document document = _domParser.parseDocument(new InputSource(new StringReader(content))); 517 518 String failure = _xPathProcessor.evaluateAsString(document, "/soap:Envelope/soap:Body/soap:Fault/soap:Reason/soap:Text", __PREFIX_RESOLVER); 519 520 if (StringUtils.isEmpty(failure)) 521 { 522 NodeList emails = _xPathProcessor.selectNodeList(document, "/soap:Envelope/soap:Body/mail:SearchResponse/mail:m", __PREFIX_RESOLVER); 523 524 List<EmailMessage> result = new ArrayList<>(); 525 int count = 0; 526 while (count < emails.getLength()) 527 { 528 Node item = emails.item(count); 529 530 EmailMessage email = new EmailMessage(); 531 email.setSender(_xPathProcessor.evaluateAsString(item, "mail:e/@a", __PREFIX_RESOLVER)); 532 email.setSubject(_xPathProcessor.evaluateAsString(item, "mail:su", __PREFIX_RESOLVER)); 533 email.setSummary(_xPathProcessor.evaluateAsString(item, "mail:fr", __PREFIX_RESOLVER)); 534 535 result.add(email); 536 537 count++; 538 } 539 540 return result; 541 } 542 else 543 { 544 getLogger().warn("Zimbra failed to return unread email for user {}: {}", userIdentity, failure); 545 return Collections.emptyList(); 546 } 547 } 548 catch (IOException | SAXException e) 549 { 550 throw new MessagingConnectorException("Failed to get Zimbra email count for user " + userIdentity, MessagingConnectorException.ExceptionType.UNKNOWN, e); 551 } 552 } 553 554 @Override 555 protected int internalGetEmailsCount(UserIdentity userIdentity) throws MessagingConnectorException 556 { 557 // Connection to the zimbra mail server 558 String authToken = _getToken(userIdentity); 559 560 if (StringUtils.isEmpty(authToken)) 561 { 562 return 0; 563 } 564 565 String soapRequest = "<soap:Envelope xmlns:soap=\"http://www.w3.org/2003/05/soap-envelope\">" 566 + " <soap:Header>" 567 + " <context xmlns=\"urn:zimbra\">" 568 + " <authToken>" + authToken + "</authToken>" 569 + " </context>" 570 + " </soap:Header>" 571 + " <soap:Body>" 572 + " <GetFolderRequest xmlns=\"urn:zimbraMail\" depth=\"0\">" 573 + " <folder path=\"inbox\" />" 574 + " </GetFolderRequest>" 575 + " </soap:Body>" 576 + "</soap:Envelope>"; 577 578 HttpPost req = new HttpPost(_zimbraUrl + "/service/soap"); 579 req.setEntity(new StringEntity(soapRequest, StandardCharsets.UTF_8)); 580 581 try (CloseableHttpClient httpClient = _getHttpClient(); CloseableHttpResponse response = httpClient.execute(req, HttpClientContext.create())) 582 { 583 String content = EntityUtils.toString(response.getEntity()); 584 585 Document document = _domParser.parseDocument(new InputSource(new StringReader(content))); 586 587 String failure = _xPathProcessor.evaluateAsString(document, "/soap:Envelope/soap:Body/soap:Fault/soap:Reason/soap:Text", __PREFIX_RESOLVER); 588 589 if (StringUtils.isEmpty(failure)) 590 { 591 return _xPathProcessor.evaluateAsNumber(document, "/soap:Envelope/soap:Body/mail:GetFolderResponse/mail:folder/@u", __PREFIX_RESOLVER).intValue(); 592 } 593 else 594 { 595 getLogger().warn("Zimbra failed to return unread email count for user {}: {}", userIdentity, failure); 596 return 0; 597 } 598 } 599 catch (IOException | SAXException e) 600 { 601 throw new MessagingConnectorException("Failed to get Zimbra email count for user " + userIdentity, MessagingConnectorException.ExceptionType.UNKNOWN, e); 602 } 603 } 604 605 private static class ZimbraPrefixResolver implements PrefixResolver 606 { 607 private Map<String, String> _namespaces = Map.of("soap", "http://www.w3.org/2003/05/soap-envelope", 608 "mail", "urn:zimbraMail"); 609 610 public String prefixToNamespace(String prefix) 611 { 612 return _namespaces.get(prefix); 613 } 614 } 615 616 private String _getToken (UserIdentity user) 617 { 618 if (user == null) 619 { 620 return null; 621 } 622 623 Cache<UserIdentity, String> cache = _getCache(); 624 625 return cache.get(user, key -> _doPreauthRequest(key)); 626 } 627 628 private Cache<UserIdentity, String> _getCache() 629 { 630 return this._cacheManager.get(TOKEN_CACHE); 631 } 632}