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().getValue("zimbra.config.zimbra.baseUrl"), "/");
143        _domainPreauthSecretKey = Config.getInstance().getValue("zimbra.config.preauth.key");
144        
145        int maxSimultaneousConnections = (int) (long) Config.getInstance().getValue("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, (long) Config.getInstance().getValue("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&amp;end=p30d&amp;auth=qp&amp;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}