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.InputStream; 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.LocalDateTime; 027import java.util.ArrayList; 028import java.util.Collection; 029import java.util.Collections; 030import java.util.Comparator; 031import java.util.Date; 032import java.util.HashMap; 033import java.util.Iterator; 034import java.util.LinkedHashMap; 035import java.util.List; 036import java.util.Map; 037import java.util.Optional; 038import java.util.SortedMap; 039import java.util.TreeMap; 040import java.util.stream.Collectors; 041 042import javax.crypto.Mac; 043import javax.crypto.spec.SecretKeySpec; 044 045import org.apache.avalon.framework.activity.Disposable; 046import org.apache.avalon.framework.service.ServiceException; 047import org.apache.avalon.framework.service.ServiceManager; 048import org.apache.cocoon.ProcessingException; 049import org.apache.cocoon.environment.Redirector; 050import org.apache.commons.codec.binary.Hex; 051import org.apache.commons.collections.ListUtils; 052import org.apache.commons.lang.StringUtils; 053import org.apache.http.HeaderElement; 054import org.apache.http.HeaderElementIterator; 055import org.apache.http.HttpResponse; 056import org.apache.http.NameValuePair; 057import org.apache.http.client.config.RequestConfig; 058import org.apache.http.client.methods.CloseableHttpResponse; 059import org.apache.http.client.methods.HttpGet; 060import org.apache.http.client.protocol.HttpClientContext; 061import org.apache.http.client.utils.URLEncodedUtils; 062import org.apache.http.conn.ConnectionKeepAliveStrategy; 063import org.apache.http.conn.ConnectionPoolTimeoutException; 064import org.apache.http.cookie.Cookie; 065import org.apache.http.impl.client.CloseableHttpClient; 066import org.apache.http.impl.client.HttpClients; 067import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; 068import org.apache.http.message.BasicHeaderElementIterator; 069import org.apache.http.message.BasicNameValuePair; 070import org.apache.http.protocol.HTTP; 071import org.apache.http.protocol.HttpContext; 072import org.apache.http.util.EntityUtils; 073 074import org.ametys.core.user.User; 075import org.ametys.core.user.UserIdentity; 076import org.ametys.core.user.UserManager; 077import org.ametys.core.util.DateUtils; 078import org.ametys.core.util.JSONUtils; 079import org.ametys.plugins.messagingconnector.AbstractMessagingConnector; 080import org.ametys.plugins.messagingconnector.CalendarEvent; 081import org.ametys.plugins.messagingconnector.EmailMessage; 082import org.ametys.plugins.messagingconnector.MessagingConnectorException; 083import org.ametys.runtime.config.Config; 084 085import net.fortuna.ical4j.data.CalendarBuilder; 086import net.fortuna.ical4j.data.ParserException; 087import net.fortuna.ical4j.model.Calendar; 088import net.fortuna.ical4j.model.Component; 089import net.fortuna.ical4j.model.ComponentList; 090import net.fortuna.ical4j.model.DateTime; 091import net.fortuna.ical4j.model.Period; 092import net.fortuna.ical4j.model.PeriodList; 093import net.fortuna.ical4j.model.component.VEvent; 094import net.fortuna.ical4j.model.property.DtEnd; 095import net.fortuna.ical4j.model.property.DtStart; 096import net.fortuna.ical4j.model.property.Location; 097import net.fortuna.ical4j.model.property.Summary; 098 099/** 100 * 101 * The connector used by the messaging connector plugin when the zimbra mail 102 * server is used. Implements the methods of the MessagingConnector interface in 103 * order to get the informations from the mail server 104 * 105 */ 106public class ZimbraConnector extends AbstractMessagingConnector implements Disposable 107{ 108 /** The number of seconds after what kept alive connections are dropt */ 109 protected static final int _DROP_KEPTALIVE_CONNECTION_AFTER = 5; 110 111 /** The user manager */ 112 protected UserManager _usersManager; 113 114 /** The JSON Utils */ 115 protected JSONUtils _jsonUtils; 116 117 /** Url to zimbra */ 118 protected String _zimbraUrl; 119 120 /** Preauth secret key */ 121 protected String _domainPreauthSecretKey; 122 123 /** Request to the remote app will be ppoled for perfs purposes */ 124 protected PoolingHttpClientConnectionManager _connectionManager; 125 /** The keep-alive stragegy to optimize http clients */ 126 protected ConnectionKeepAliveStrategy _connectionKeepAliveStrategy; 127 /** The shared configuration for request (for timeout purposes) */ 128 protected RequestConfig _connectionConfig; 129 130 @Override 131 public void service(ServiceManager smanager) throws ServiceException 132 { 133 super.service(smanager); 134 _usersManager = (UserManager) smanager.lookup(UserManager.ROLE); 135 _jsonUtils = (JSONUtils) smanager.lookup(JSONUtils.ROLE); 136 } 137 138 @Override 139 public void initialize() 140 { 141 super.initialize(); 142 _zimbraUrl = StringUtils.removeEnd(Config.getInstance().getValueAsString("zimbra.config.zimbra.baseUrl"), "/"); 143 _domainPreauthSecretKey = Config.getInstance().getValueAsString("zimbra.config.preauth.key"); 144 145 int maxSimultaneousConnections = (int) (long) Config.getInstance().getValueAsLong("zimbra.config.maxconnections"); 146 // Same value for 3 timeouts (socket, connection and response) so one connection can last 3 times this value 147 int connectionTimeout = (int) Math.max(0, Config.getInstance().getValueAsLong("zimbra.config.timeout")); 148 149 // A pooling connection manager to avoid flooding remote server by reusing existing connections AND by limiting their number 150 _connectionManager = new PoolingHttpClientConnectionManager(); 151 _connectionManager.setMaxTotal(maxSimultaneousConnections > 0 ? maxSimultaneousConnections : Integer.MAX_VALUE); 152 _connectionManager.setDefaultMaxPerRoute(maxSimultaneousConnections > 0 ? maxSimultaneousConnections : Integer.MAX_VALUE); 153 154 // inspired from http://www.baeldung.com/httpclient-connection-management 155 // to keep a connection alive for a few seconds if the remote server did not send back this information 156 _connectionKeepAliveStrategy = new ConnectionKeepAliveStrategy() 157 { 158 @Override 159 public long getKeepAliveDuration(HttpResponse response, HttpContext context) 160 { 161 HeaderElementIterator it = new BasicHeaderElementIterator(response.headerIterator(HTTP.CONN_KEEP_ALIVE)); 162 while (it.hasNext()) 163 { 164 HeaderElement he = it.nextElement(); 165 String param = he.getName(); 166 String value = he.getValue(); 167 if (value != null && param.equalsIgnoreCase("timeout")) 168 { 169 try 170 { 171 return Long.parseLong(value) * 1000; 172 } 173 catch (NumberFormatException ignore) 174 { 175 // Ignore 176 } 177 } 178 } 179 return _DROP_KEPTALIVE_CONNECTION_AFTER * 1000; 180 } 181 }; 182 183 _connectionConfig = RequestConfig.custom() 184 .setConnectTimeout(connectionTimeout * 1000) 185 .setConnectionRequestTimeout(connectionTimeout * 1000) // Time to get an object from the pool 186 .setSocketTimeout(connectionTimeout * 1000).build(); 187 } 188 189 public void dispose() 190 { 191 _connectionManager.close(); 192 } 193 194 /** 195 * Get a new pooled http client. Do not forget to close it. 196 * @return The client 197 */ 198 protected CloseableHttpClient _getHttpClient() 199 { 200 return HttpClients.custom() 201 .setConnectionManager(_connectionManager) 202 .setConnectionManagerShared(true) // avoid automatic pool closing 203 .setKeepAliveStrategy(_connectionKeepAliveStrategy) 204 .setDefaultRequestConfig(_connectionConfig) 205 .build(); 206 } 207 208 /** 209 * Preauth user and redirect to the zimbra application 210 * @param redirector The redirector 211 * @param targetApp The zimbra application (ex: mail) 212 * @throws ProcessingException if failed to redirect 213 * @throws IOException if failed to redirect 214 */ 215 public void redirect(Redirector redirector, String targetApp) throws ProcessingException, IOException 216 { 217 UserIdentity identity = _currentUserProvider.getUser(); 218 User user = _usersManager.getUser(identity); 219 220 if (user != null) 221 { 222 String qs = _computeQueryString(user, targetApp); 223 if (qs != null) 224 { 225 redirector.redirect(false, _zimbraUrl + "/service/preauth?" + qs); 226 } 227 } 228 } 229 230 /** 231 * Retrieves some mail info through a query via the Zimbra REST API. Query 232 * used is : 233 * home/~/inbox?query=is:unread&fmt=json&auth=qp&zauthtoken=##TOKEN## 234 * @param user The user 235 * @param zmAuthToken The auth token corresponding to a currenlty logged 236 * user in zimbra. 237 * @return Map of info returned by the REST request 238 */ 239 protected Map<String, Object> _getEmailInfo(User user, String zmAuthToken) 240 { 241 // Preauth request parameters 242 List<NameValuePair> params = new ArrayList<>(); 243 params.add(new BasicNameValuePair("fmt", "json")); // Request json format Restrict to unread messages from the inbox folder. 244 params.add(new BasicNameValuePair("query", "is:unread")); // Restrict to unread messages from the inbox folder. 245 params.add(new BasicNameValuePair("auth", "qp")); // Requires authentication token method 246 params.add(new BasicNameValuePair("zauthtoken", zmAuthToken)); // Authentication token 247 248 // Computing query string 249 String qs = URLEncodedUtils.format(params, StandardCharsets.UTF_8); 250 251 // Rest API URL. '~' stands for the current active user, which is 252 // retrieved through the auth token b/c auth token method is used (see 253 // query string params) 254 HttpGet get = new HttpGet(_zimbraUrl + "/home/~/inbox?" + qs); 255 256 try (CloseableHttpClient httpClient = _getHttpClient(); CloseableHttpResponse response = httpClient.execute(get, HttpClientContext.create())) 257 { 258 String content = EntityUtils.toString(response.getEntity()); 259 return _jsonUtils.convertJsonToMap(content); 260 } 261 catch (UnknownHostException e) 262 { 263 throw new MessagingConnectorException("Unknown host for zimbra server. Giving up to get Zimbra emails for user " + user, MessagingConnectorException.ExceptionType.CONFIGURATION_EXCEPTION, e); 264 } 265 catch (ConnectionPoolTimeoutException | SocketTimeoutException e) 266 { 267 throw new MessagingConnectorException("There are already too many connections to Zimbra server. Giving up to get Zimbra emails for user " + user, MessagingConnectorException.ExceptionType.TIMEOUT, e); 268 } 269 catch (IOException e) 270 { 271 throw new MessagingConnectorException("Failed to get Zimbra emails for user " + user, MessagingConnectorException.ExceptionType.UNKNOWN, e); 272 } 273 } 274 275 /** 276 * Retrieves a calendar through a query via the Zimbra REST API. 277 * Query used is : 278 * home/~/calendar?fmt=ics&end=p30d&auth=qp&zauthtoken=##TOKEN## 279 * @param user The user 280 * @param zmAuthToken The auth token corresponding to a currenlty logged 281 * user in zimbra. 282 * @param dayInterval Interval of day to search for the next event 283 * @return The Calendar 284 */ 285 protected Calendar _getCalendar(User user, String zmAuthToken, String dayInterval) 286 { 287 // Preauth request parameters 288 String relativeEnd = "p" + dayInterval + "d"; 289 List<NameValuePair> params = new ArrayList<>(); 290 params.add(new BasicNameValuePair("fmt", "ics")); // Request iCAL format 291 params.add(new BasicNameValuePair("start", "p0mi")); // Interval starts from now 292 params.add(new BasicNameValuePair("end", relativeEnd)); // and ends after the configured number of days. 293 params.add(new BasicNameValuePair("auth", "qp")); // Requires authentication token method 294 params.add(new BasicNameValuePair("zauthtoken", zmAuthToken)); // Authentication token 295 296 // Computing query string 297 String qs = URLEncodedUtils.format(params, StandardCharsets.UTF_8); 298 299 // Rest API URL. '~' stands for the current active user, which is 300 // retrieved through the auth token b/c auth token method is used 301 // (see query string params) 302 HttpGet get = new HttpGet(_zimbraUrl + "/home/~/calendar?" + qs); 303 304 try (CloseableHttpClient httpClient = _getHttpClient(); CloseableHttpResponse response = httpClient.execute(get, HttpClientContext.create()); InputStream responseIs = response.getEntity().getContent()) 305 { 306 // Construct the iCAL calendar from the response input stream. 307 CalendarBuilder builder = new CalendarBuilder(); 308 Calendar calendar = builder.build(responseIs); 309 return calendar; 310 } 311 catch (UnknownHostException e) 312 { 313 throw new MessagingConnectorException("Unknown host for zimbra server. Giving up to get Zimbra calendar events for user " + user, MessagingConnectorException.ExceptionType.CONFIGURATION_EXCEPTION, e); 314 } 315 catch (ConnectionPoolTimeoutException | SocketTimeoutException e) 316 { 317 throw new MessagingConnectorException("There are already too many connections to zimbra server. Giving up to get Zimbra calendar events for user " + user, MessagingConnectorException.ExceptionType.TIMEOUT, e); 318 } 319 catch (ParserException | IOException e) 320 { 321 throw new MessagingConnectorException("Failed to get Zimbra calendar events for user " + user, MessagingConnectorException.ExceptionType.UNKNOWN, e); 322 } 323 } 324 325 /** 326 * Extract interesting properties for the calendar. Retrieves the next 327 * events and return some info. 328 * @param calendar The calendar 329 * @param dayInterval Interval of day to search for the next event 330 * @param maxEvents The maximum number of events 331 * @return map of info about the next events. 332 */ 333 protected List<Map<String, Object>> _extractCalendarProperties(Calendar calendar, int dayInterval, int maxEvents) 334 { 335 // Retrieving events. 336 ComponentList components = calendar.getComponents(Component.VEVENT); 337 List<VEvent> events = ListUtils.typedList(components, VEvent.class); 338 339 // Iterates over all events to find the closest date from now (taking 340 // recurring events into account). Constructs a sorted map where keys 341 // are next closest dates for each event, and values are the 342 // corresponding events. 343 SortedMap<Date, ZimbraEvent> eventMap = new TreeMap<>(new Comparator<Date>() 344 { 345 @Override 346 public int compare(Date d1, Date d2) 347 { 348 return d1.compareTo(d2); 349 } 350 }); 351 352 LocalDateTime nowLdt = LocalDateTime.now(); 353 Date fromDate = DateUtils.asDate(nowLdt.withSecond(0)); 354 Date untilDate = DateUtils.asDate(nowLdt.withHour(0).withMinute(0).withSecond(0).plusDays(dayInterval)); 355 356 DateTime start = new DateTime(fromDate); 357 DateTime end = new DateTime(untilDate); 358 359 for (VEvent event : events) 360 { 361 PeriodList consumedTime = event.getConsumedTime(start, end); 362 Iterator periodIterator = consumedTime.iterator(); 363 364 while (periodIterator.hasNext()) 365 { 366 Period period = (Period) periodIterator.next(); 367 Date pStart = period.getStart(); 368 Date pEnd = period.getEnd(); 369 370 if (pStart != null) 371 { 372 eventMap.put(pStart, new ZimbraEvent(event, pStart, pEnd)); 373 } 374 } 375 } 376 377 return eventMap.entrySet().stream().limit(maxEvents).map(e -> _extractCalendarProperties(e.getValue())).collect(Collectors.toList()); 378 } 379 380 /** 381 * Extract interesting properties for the event. 382 * @param zimbraEvent The event 383 * @return map of interesting info about this event. 384 */ 385 protected Map<String, Object> _extractCalendarProperties(ZimbraEvent zimbraEvent) 386 { 387 Map<String, Object> properties = new HashMap<>(); 388 389 VEvent event = zimbraEvent.getEvent(); 390 Date startDate = zimbraEvent.getStartDate(); 391 Date endDate = zimbraEvent.getEndDate(); 392 393 if (event != null && startDate != null && endDate != null) 394 { 395 properties.put("eventStartDateRaw", startDate); 396 397 properties.put("eventEndDate", endDate); 398 399 // Location 400 Location location = event.getLocation(); 401 if (location != null) 402 { 403 String eventLocation = _sanitizeEventLocation(location); 404 properties.put("eventLocation", eventLocation); 405 } 406 407 // Summary 408 Summary summary = event.getSummary(); 409 if (summary != null) 410 { 411 properties.put("eventSubject", summary.getValue()); 412 } 413 } 414 else 415 { 416 properties.put("eventNothing", true); 417 } 418 419 return properties; 420 } 421 422 /** 423 * Extract the interesting part of the raw event location. 424 * @param location The iCal4j location object. 425 * @return A String representing the sanitized location. 426 */ 427 protected String _sanitizeEventLocation(Location location) 428 { 429 String extractedValue = location.getValue(); 430 431 if (StringUtils.contains(extractedValue, ';')) 432 { 433 extractedValue = StringUtils.substringBefore(extractedValue, ";"); 434 } 435 436 String candidateValue = StringUtils.substringBetween(extractedValue, "\""); 437 if (StringUtils.isNotBlank(candidateValue)) 438 { 439 extractedValue = candidateValue; 440 } 441 442 return extractedValue; 443 } 444 445 /** 446 * Zimbra preauth request to log the current user into zimbra and retrieve the ZM_AUTH_TOKEN 447 * @param user The user for which the preauth request will be done. 448 * @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. 449 * @throws MessagingConnectorException if failed to get zimbra token for user 450 */ 451 protected String _doPreauthRequest(User user) 452 { 453 // Computing query string 454 String qs = _computeQueryString(user, null); 455 if (qs == null) 456 { 457 return null; 458 } 459 460 HttpGet get = new HttpGet(_zimbraUrl + "/service/preauth?" + qs); 461 462 try (CloseableHttpClient httpClient = _getHttpClient()) 463 { 464 HttpClientContext context = HttpClientContext.create(); 465 try (CloseableHttpResponse response = httpClient.execute(get, context)) 466 { 467 List<Cookie> cookies = context.getCookieStore().getCookies(); 468 469 for (Cookie cookie : cookies) 470 { 471 if (StringUtils.equals(cookie.getName(), "ZM_AUTH_TOKEN")) 472 { 473 return cookie.getValue(); 474 } 475 } 476 477 throw new MessagingConnectorException("Zimbra authentification failed for user " + user.getEmail(), MessagingConnectorException.ExceptionType.CONFIGURATION_EXCEPTION); 478 } 479 } 480 catch (UnknownHostException e) 481 { 482 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); 483 } 484 catch (ConnectionPoolTimeoutException | SocketTimeoutException e) 485 { 486 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); 487 } 488 catch (IOException e) 489 { 490 throw new MessagingConnectorException("Unable to proceed to the Zimbra preauth action for user : " + user.getEmail(), MessagingConnectorException.ExceptionType.UNKNOWN, e); 491 } 492 } 493 494 private String _computeQueryString(User user, String targetApp) 495 { 496 if (user == null) 497 { 498 return null; 499 } 500 501 String zimbraUser = user.getEmail(); 502 503 if (StringUtils.isEmpty(zimbraUser)) 504 { 505 if (getLogger().isDebugEnabled()) 506 { 507 getLogger().debug("Cannot retreive zimbra information with empty email for user " + user); 508 } 509 return null; 510 } 511 512 String timestamp = String.valueOf(System.currentTimeMillis()); 513 String computedPreauth = null; 514 515 try 516 { 517 computedPreauth = _getComputedPreauth(zimbraUser, timestamp, _domainPreauthSecretKey); 518 } 519 catch (Exception e) 520 { 521 throw new MessagingConnectorException("Unable to compute the preauth key during the Zimbra preauth action for user : " + zimbraUser, MessagingConnectorException.ExceptionType.UNKNOWN, e); 522 } 523 524 // Preauth request parameters 525 List<NameValuePair> params = new ArrayList<>(); 526 params.add(new BasicNameValuePair("account", zimbraUser)); 527 params.add(new BasicNameValuePair("timestamp", timestamp)); 528 params.add(new BasicNameValuePair("expires", "0")); 529 params.add(new BasicNameValuePair("preauth", computedPreauth)); 530 if (targetApp != null) 531 { 532 params.add(new BasicNameValuePair("redirectURL", "/?app=" + targetApp)); 533 } 534 535 // Computing query string 536 return URLEncodedUtils.format(params, StandardCharsets.UTF_8); 537 } 538 539 /** 540 * Compute the preauth key. 541 * @param zimbraUser The Zimbra User 542 * @param timestamp The timestamp 543 * @param secretKey The secret key 544 * @return The computed preauth key 545 * @throws NoSuchAlgorithmException if no Provider supports a MacSpi 546 * implementation for the specified algorithm (HmacSHA1). 547 * @throws InvalidKeyException if the given key is inappropriate for 548 * initializing the MAC 549 * @throws UnsupportedEncodingException If the named charset (UTF-8) is not 550 * supported 551 */ 552 protected String _getComputedPreauth(String zimbraUser, String timestamp, String secretKey) throws NoSuchAlgorithmException, InvalidKeyException, UnsupportedEncodingException 553 { 554 SecretKeySpec signingKey = new SecretKeySpec(secretKey.getBytes(), "HmacSHA1"); 555 Mac mac = Mac.getInstance("HmacSHA1"); 556 mac.init(signingKey); 557 558 String data = StringUtils.join(new String[] {zimbraUser, "name", "0", timestamp}, '|'); 559 byte[] rawHmac = mac.doFinal(data.getBytes()); 560 byte[] hexBytes = new Hex().encode(rawHmac); 561 return new String(hexBytes, "UTF-8"); 562 } 563 564 @Override 565 protected List<CalendarEvent> internalGetEvents(UserIdentity userIdentity, int maxDays, int maxEvents) throws MessagingConnectorException 566 { 567 // Connection to the zimbra mail server 568 User user = _usersManager.getUser(userIdentity); 569 String authToken = _doPreauthRequest(user); 570 571 if (StringUtils.isEmpty(authToken)) 572 { 573 return Collections.EMPTY_LIST; 574 } 575 576 List<CalendarEvent> calendarEvent = new ArrayList<>(); 577 // Collect the list of events 578 Calendar calendar = _getCalendar(user, authToken, String.valueOf(maxDays)); 579 List<Map<String, Object>> vevents = _extractCalendarProperties(calendar, maxDays, maxEvents); 580 for (Map<String, Object> vevent : vevents) 581 { 582 // Creating the CalendarEvent 583 CalendarEvent newEvent = new CalendarEvent(); 584 newEvent.setStartDate((Date) vevent.get("eventStartDateRaw")); 585 newEvent.setEndDate((Date) vevent.get("eventEndDate")); 586 newEvent.setSubject((String) vevent.get("eventSubject")); 587 newEvent.setLocation((String) vevent.get("eventLocation")); 588 calendarEvent.add(newEvent); 589 } 590 return calendarEvent; 591 } 592 593 @Override 594 protected int internalGetEventsCount(UserIdentity userIdentity, int maxDays) throws MessagingConnectorException 595 { 596 // Connection to the zimbra mail server 597 User user = _usersManager.getUser(userIdentity); 598 String authToken = _doPreauthRequest(user); 599 600 if (StringUtils.isEmpty(authToken)) 601 { 602 return 0; 603 } 604 605 return _getCalendar(user, authToken, String.valueOf(maxDays)).getComponents(Component.VEVENT).size(); 606 } 607 608 @SuppressWarnings("unchecked") 609 @Override 610 protected List<EmailMessage> internalGetEmails(UserIdentity userIdentity, int maxEmails) throws MessagingConnectorException 611 { 612 // Connection to the zimbra mail server 613 User user = _usersManager.getUser(userIdentity); 614 String authToken = _doPreauthRequest(user); 615 616 if (StringUtils.isEmpty(authToken)) 617 { 618 return Collections.EMPTY_LIST; 619 } 620 621 Map<String, Object> mailInfo = _getEmailInfo(user, authToken); 622 623 List<EmailMessage> listMail = new ArrayList<>(); 624 625 if (mailInfo != null && !mailInfo.isEmpty()) 626 { 627 if (mailInfo.values().iterator().next() instanceof Collection) 628 { 629 Collection<LinkedHashMap<String, Object>> mailObject = (Collection<LinkedHashMap<String, Object>>) mailInfo.values().iterator().next(); 630 for (LinkedHashMap<String, Object> mailObjectMap : mailObject) 631 { 632 if (listMail.size() < maxEmails) 633 { 634 EmailMessage newMail = new EmailMessage(); 635 if (mailObjectMap.get("e") instanceof List) 636 { 637 List<LinkedHashMap<String, Object>> mailObjectPerson = (List<LinkedHashMap<String, Object>>) mailObjectMap.get("e"); 638 newMail.setSender(mailObjectPerson.get(1).get("a").toString()); // get the sender 639 } 640 newMail.setSubject(mailObjectMap.get("su").toString()); // get the subject 641 newMail.setSummary(mailObjectMap.get("fr").toString()); // get the summary 642 listMail.add(newMail); 643 } 644 } 645 } 646 } 647 else if (getLogger().isWarnEnabled()) 648 { 649 getLogger().warn("Zimbra returned empty information for user " + userIdentity); 650 } 651 return listMail; 652 } 653 654 @SuppressWarnings("unchecked") 655 @Override 656 protected int internalGetEmailsCount(UserIdentity userIdentity) throws MessagingConnectorException 657 { 658 // Connection to the zimbra mail server 659 User user = _usersManager.getUser(userIdentity); 660 String authToken = _doPreauthRequest(user); 661 662 if (StringUtils.isEmpty(authToken)) 663 { 664 return 0; 665 } 666 667 Map<String, Object> mailInfo = _getEmailInfo(user, authToken); 668 if (mailInfo != null && !mailInfo.isEmpty()) 669 { 670 return ((Collection<LinkedHashMap<String, Object>>) mailInfo.values().iterator().next()).size(); 671 } 672 673 if (getLogger().isWarnEnabled()) 674 { 675 getLogger().warn("Zimbra returned empty information for user " + userIdentity); 676 } 677 678 return 0; 679 } 680 681 private class ZimbraEvent 682 { 683 private VEvent _event; 684 private Date _startDate; 685 private Date _endDate; 686 687 /** 688 * Default constructor 689 * @param event The event 690 * @param startDate The start of the event 691 * @param endDate The end of the event 692 */ 693 public ZimbraEvent(VEvent event, Date startDate, Date endDate) 694 { 695 _event = event; 696 _startDate = startDate; 697 _endDate = endDate; 698 } 699 700 /** 701 * Get the event 702 * @return the event 703 */ 704 public VEvent getEvent() 705 { 706 return _event; 707 } 708 709 /** 710 * Get the start date 711 * @return the startDate 712 */ 713 public Date getStartDate() 714 { 715 return _startDate != null ? _startDate : Optional.of(_event) 716 .map(VEvent::getStartDate) 717 .map(DtStart::getDate) 718 .orElse(null); 719 } 720 721 /** 722 * Get the end date 723 * @return the endDate 724 */ 725 public Date getEndDate() 726 { 727 return _endDate != null ? _startDate : Optional.of(_event) 728 .map(VEvent::getEndDate) 729 .map(DtEnd::getDate) 730 .orElse(null); 731 } 732 733 734 } 735}