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.exchange;
017
018import java.net.SocketTimeoutException;
019import java.net.URI;
020import java.net.URISyntaxException;
021import java.net.UnknownHostException;
022import java.time.LocalDateTime;
023import java.time.ZoneId;
024import java.time.ZonedDateTime;
025import java.util.ArrayList;
026import java.util.Collection;
027import java.util.Collections;
028import java.util.Date;
029import java.util.HashMap;
030import java.util.List;
031import java.util.Map;
032import java.util.Set;
033import java.util.TimeZone;
034
035import org.apache.avalon.framework.service.ServiceException;
036import org.apache.avalon.framework.service.ServiceManager;
037import org.apache.commons.lang3.EnumUtils;
038import org.apache.commons.lang3.StringUtils;
039import org.jsoup.Jsoup;
040
041import org.ametys.core.user.User;
042import org.ametys.core.user.UserIdentity;
043import org.ametys.core.user.UserManager;
044import org.ametys.core.userpref.UserPreferencesException;
045import org.ametys.core.util.DateUtils;
046import org.ametys.plugins.explorer.calendars.EventRecurrenceTypeEnum;
047import org.ametys.plugins.messagingconnector.AbstractMessagingConnector;
048import org.ametys.plugins.messagingconnector.CalendarEvent;
049import org.ametys.plugins.messagingconnector.EmailMessage;
050import org.ametys.plugins.messagingconnector.MessagingConnectorException;
051import org.ametys.plugins.messagingconnector.MessagingConnectorException.ExceptionType;
052import org.ametys.runtime.config.Config;
053
054import microsoft.exchange.webservices.data.core.ExchangeService;
055import microsoft.exchange.webservices.data.core.enumeration.availability.AvailabilityData;
056import microsoft.exchange.webservices.data.core.enumeration.misc.ConnectingIdType;
057import microsoft.exchange.webservices.data.core.enumeration.misc.ExchangeVersion;
058import microsoft.exchange.webservices.data.core.enumeration.property.LegacyFreeBusyStatus;
059import microsoft.exchange.webservices.data.core.enumeration.property.MeetingResponseType;
060import microsoft.exchange.webservices.data.core.enumeration.property.WellKnownFolderName;
061import microsoft.exchange.webservices.data.core.enumeration.property.time.DayOfTheWeek;
062import microsoft.exchange.webservices.data.core.enumeration.search.LogicalOperator;
063import microsoft.exchange.webservices.data.core.enumeration.service.ConflictResolutionMode;
064import microsoft.exchange.webservices.data.core.enumeration.service.DeleteMode;
065import microsoft.exchange.webservices.data.core.enumeration.service.SendInvitationsMode;
066import microsoft.exchange.webservices.data.core.enumeration.service.SendInvitationsOrCancellationsMode;
067import microsoft.exchange.webservices.data.core.enumeration.service.ServiceResult;
068import microsoft.exchange.webservices.data.core.exception.http.HttpErrorException;
069import microsoft.exchange.webservices.data.core.exception.service.remote.ServiceRequestException;
070import microsoft.exchange.webservices.data.core.exception.service.remote.ServiceResponseException;
071import microsoft.exchange.webservices.data.core.response.AttendeeAvailability;
072import microsoft.exchange.webservices.data.core.service.folder.CalendarFolder;
073import microsoft.exchange.webservices.data.core.service.folder.Folder;
074import microsoft.exchange.webservices.data.core.service.item.Appointment;
075import microsoft.exchange.webservices.data.core.service.item.Item;
076import microsoft.exchange.webservices.data.core.service.schema.EmailMessageSchema;
077import microsoft.exchange.webservices.data.credential.ExchangeCredentials;
078import microsoft.exchange.webservices.data.credential.WebCredentials;
079import microsoft.exchange.webservices.data.misc.ImpersonatedUserId;
080import microsoft.exchange.webservices.data.misc.availability.AttendeeInfo;
081import microsoft.exchange.webservices.data.misc.availability.GetUserAvailabilityResults;
082import microsoft.exchange.webservices.data.misc.availability.TimeWindow;
083import microsoft.exchange.webservices.data.property.complex.Attendee;
084import microsoft.exchange.webservices.data.property.complex.AttendeeCollection;
085import microsoft.exchange.webservices.data.property.complex.FolderId;
086import microsoft.exchange.webservices.data.property.complex.ItemId;
087import microsoft.exchange.webservices.data.property.complex.Mailbox;
088import microsoft.exchange.webservices.data.property.complex.MessageBody;
089import microsoft.exchange.webservices.data.property.complex.recurrence.pattern.Recurrence;
090import microsoft.exchange.webservices.data.property.complex.time.TimeZoneDefinition;
091import microsoft.exchange.webservices.data.search.CalendarView;
092import microsoft.exchange.webservices.data.search.FindItemsResults;
093import microsoft.exchange.webservices.data.search.ItemView;
094import microsoft.exchange.webservices.data.search.filter.SearchFilter;
095import microsoft.exchange.webservices.data.util.TimeZoneUtils;
096
097/**
098 * 
099 * The connector used by the messaging connector plugin when the exchange mail
100 * server is used. Implements the methods of the MessagingConnector interface in
101 * order to get the informations from the mail server
102 *
103 */
104public class ExchangeConnector extends AbstractMessagingConnector
105{
106    /** The user manager */
107    private UserManager _userManager;
108    
109    @Override
110    public void service(ServiceManager manager) throws ServiceException
111    {
112        super.service(manager);
113        _userManager = (UserManager) manager.lookup(UserManager.ROLE);
114    }
115
116    /**
117     * Get the service of connexion to the server exchange
118     * @param userIdentity The user identity
119     * @return the service
120     * @throws URISyntaxException if an error occurred
121     */
122    protected ExchangeService getService(UserIdentity userIdentity) throws URISyntaxException
123    {
124        if (userIdentity == null)
125        {
126            return null;
127        }
128        String url = Config.getInstance().getValue("org.ametys.plugins.exchange.url");
129
130        ExchangeService service = supportUserCredential() ? _getSimpleService(userIdentity) : _getImpersonatedService(userIdentity);
131
132        if (service != null)
133        {
134            service.setUrl(new URI(url));
135        }
136        return service;
137    }
138
139    @Override
140    public boolean supportUserCredential()
141    {
142        boolean impersonation = Config.getInstance().getValue("org.ametys.plugins.exchange.impersonation");
143        return !impersonation;
144    }
145
146    private ExchangeService _getSimpleService(UserIdentity userIdentity)
147    {
148        String userName = null;
149        String password = null;
150        
151        String authMethod = Config.getInstance().getValue("org.ametys.plugins.exchange.authmethod");
152        if ("email".equals(authMethod))
153        {
154            User user = _userManager.getUser(userIdentity);
155            String email = user.getEmail();
156            if (StringUtils.isBlank(email))
157            {
158                if (getLogger().isWarnEnabled())
159                {
160                    getLogger().warn("The user '" + userIdentity.getLogin() + "' has no email address set, thus exchange cannot be contacted using 'email' authentication method");
161                }
162                return null;
163            }
164            userName = email;
165        }
166        else
167        {
168            userName = userIdentity.getLogin();
169        }
170
171        try
172        {
173            password = getUserPassword(userIdentity);
174            
175            if (userName != null && password != null)
176            {
177                ExchangeService service = _initService(userName, password);
178                return service;
179            }
180            else if (password == null)
181            {
182                throw new MessagingConnectorException("Missing exchange password for user " + userIdentity, ExceptionType.UNAUTHORIZED);
183            }
184            return null;
185        }
186        catch (UserPreferencesException e)
187        {
188            getLogger().error("Unable to get exchange user password for user'" + userIdentity.getLogin() + "'", e);
189            return null;
190        }
191    }
192
193    private ExchangeService _getImpersonatedService(UserIdentity userIdentity)
194    {
195        String userName = Config.getInstance().getValue("org.ametys.plugins.exchange.username");
196        String password = Config.getInstance().getValue("org.ametys.plugins.exchange.password");
197        ExchangeService service = _initService(userName, password);
198
199        String authMethod = Config.getInstance().getValue("org.ametys.plugins.exchange.authmethod");
200
201        if ("email".equals(authMethod))
202        {
203            User user = _userManager.getUser(userIdentity);
204            String email = user.getEmail();
205            if (StringUtils.isBlank(email))
206            {
207                if (getLogger().isWarnEnabled())
208                {
209                    getLogger().warn("The user '" + userIdentity.getLogin() + "' has no email address set, thus exchange cannot be contacted using 'email' authentication method");
210                }
211                return null;
212            }
213            service.setImpersonatedUserId(new ImpersonatedUserId(ConnectingIdType.SmtpAddress, email));
214        }
215        else
216        {
217            service.setImpersonatedUserId(new ImpersonatedUserId(ConnectingIdType.PrincipalName, userIdentity.getLogin()));
218        }
219        return service;
220    }
221
222    private ExchangeService _initService(String userName, String password)
223    {
224        ExchangeService service = new ExchangeService(ExchangeVersion.Exchange2010_SP2);
225        ExchangeCredentials credentials = new WebCredentials(userName, password);
226        service.setCredentials(credentials);
227        
228        return service;
229    }
230
231    @Override
232    protected List<CalendarEvent> internalGetEvents(UserIdentity userIdentity, int maxDays, int maxEvents) throws MessagingConnectorException
233    {
234        try
235        {        
236            List<CalendarEvent> calendar = new ArrayList<>();
237            ExchangeService service = getService(userIdentity);
238
239            if (service != null)
240            {
241                // The search filter to get futur or not terminated events
242                CalendarFolder cf = CalendarFolder.bind(service, WellKnownFolderName.Calendar);
243    
244                LocalDateTime nowLdt = LocalDateTime.now();
245                Date fromDate = DateUtils.asDate(nowLdt.withSecond(0));
246                Date untilDate = DateUtils.asDate(nowLdt.withHour(0).withMinute(0).withSecond(0).plusDays(maxDays));
247                
248                CalendarView calendarView = new CalendarView(fromDate, untilDate);
249                FindItemsResults<Appointment> findResultsEvent = cf.findAppointments(calendarView);
250    
251                calendarView.setMaxItemsReturned(maxEvents > 0 ? maxEvents : null);
252                findResultsEvent = cf.findAppointments(calendarView);
253    
254                for (Appointment event : findResultsEvent.getItems())
255                {
256                    CalendarEvent newEvent = new CalendarEvent();
257                    newEvent.setStartDate(event.getStart());
258                    newEvent.setEndDate(event.getEnd());
259                    newEvent.setSubject(event.getSubject());
260                    newEvent.setLocation(event.getLocation());
261                    calendar.add(newEvent);
262                }
263                
264            }
265            return calendar;
266        }
267        catch (ServiceRequestException e)
268        {
269            Throwable cause = e.getCause();
270            ExceptionType type = _getExceptionType(cause);
271            throw new MessagingConnectorException("Failed to get the events for user " + userIdentity.toString(), type, e);
272        }
273        catch (Exception e)
274        {
275            throw new MessagingConnectorException("Failed to get the events for user " + userIdentity.toString(), e);
276        }
277    }
278    
279    @Override
280    protected int internalGetEventsCount(UserIdentity userIdentity, int maxDays) throws MessagingConnectorException
281    {
282        try
283        {
284            int nextEventsCount = 0;
285            ExchangeService service = getService(userIdentity);
286            if (service != null)
287            {
288                // The search filter to get futur or not terminated events
289                CalendarFolder cf = CalendarFolder.bind(service, WellKnownFolderName.Calendar);
290    
291                LocalDateTime nowLdt = LocalDateTime.now();
292                Date fromDate = DateUtils.asDate(nowLdt.withSecond(0));
293                Date untilDate = DateUtils.asDate(nowLdt.withHour(0).withMinute(0).withSecond(0).plusDays(maxDays));
294                
295                CalendarView calendarView = new CalendarView(fromDate, untilDate);
296                FindItemsResults<Appointment> findResultsEvent = cf.findAppointments(calendarView);
297                nextEventsCount = findResultsEvent.getTotalCount();
298            }
299            return nextEventsCount;
300        }
301        catch (ServiceRequestException e)
302        {
303            Throwable cause = e.getCause();
304            ExceptionType type = _getExceptionType(cause);
305            throw new MessagingConnectorException("Failed to get the events count for user " + userIdentity.toString(), type, e);
306        }
307        catch (MessagingConnectorException e)
308        {
309            throw e;
310        }
311        catch (Exception e)
312        {
313            throw new MessagingConnectorException("Failed to get the events count for user " + userIdentity.toString(), e);
314        }
315    }
316    
317    @Override
318    protected List<EmailMessage> internalGetEmails(UserIdentity userIdentity, int maxEmails) throws MessagingConnectorException
319    {
320        try
321        {
322            List<EmailMessage> mailMessage = new ArrayList<>();
323            
324            ExchangeService service = getService(userIdentity);
325
326            if (service != null)
327            {
328                // The search filter to get unread email
329                SearchFilter sf = new SearchFilter.SearchFilterCollection(LogicalOperator.And, new SearchFilter.IsEqualTo(EmailMessageSchema.IsRead, false));
330                ItemView view = new ItemView(maxEmails);
331                FindItemsResults<Item> findResultsMail = service.findItems(WellKnownFolderName.Inbox, sf, view);
332    
333                List<Item> messagesReceived = findResultsMail.getItems();
334                for (Item message : messagesReceived)
335                {
336                    message.load();
337    
338                    EmailMessage newMessage = new EmailMessage();
339                    newMessage.setSender(((microsoft.exchange.webservices.data.core.service.item.EmailMessage) message).getSender().getAddress());
340                    if (message.getSubject() != null)
341                    {
342                        newMessage.setSubject(message.getSubject());
343                    }
344                    if (message.getBody() != null)
345                    {
346                        newMessage.setSummary(html2text(message.getBody().toString()));
347                    }
348                    mailMessage.add(newMessage);
349                }
350            }
351            return mailMessage;
352        }
353        catch (ServiceRequestException e)
354        {
355            Throwable cause = e.getCause();
356            ExceptionType type = _getExceptionType(cause);
357            throw new MessagingConnectorException("Failed to get the emails for user " + userIdentity.toString(), type, e);
358        }
359        catch (MessagingConnectorException e)
360        {
361            throw e;
362        }
363        catch (Exception e)
364        {
365            throw new MessagingConnectorException("Failed to get the emails for user " + userIdentity.toString(), e);
366        }
367        
368    }
369    
370    @Override
371    protected int internalGetEmailsCount(UserIdentity userIdentity) throws MessagingConnectorException
372    {
373        try
374        {
375            int emailsCount = 0;
376            
377            ExchangeService service = getService(userIdentity);
378
379            if (service != null)
380            {
381                // The search filter to get unread email
382                SearchFilter sf = new SearchFilter.SearchFilterCollection(LogicalOperator.And, new SearchFilter.IsEqualTo(EmailMessageSchema.IsRead, false));
383                ItemView view = new ItemView(20);
384                FindItemsResults<Item> findResultsMail = service.findItems(WellKnownFolderName.Inbox, sf, view);
385                
386                emailsCount = findResultsMail.getTotalCount();
387            }
388            return emailsCount;
389        }
390        catch (ServiceRequestException e)
391        {
392            Throwable cause = e.getCause();
393            ExceptionType type = _getExceptionType(cause);
394            throw new MessagingConnectorException("Failed to get the emails for user " + userIdentity.toString(), type, e);
395        }
396        catch (MessagingConnectorException e)
397        {
398            throw e;
399        }
400        catch (Exception e)
401        {
402            throw new MessagingConnectorException("Failed to get the emails for user " + userIdentity.toString(), e);
403        }
404    }
405
406    @Override
407    public boolean supportInvitation() throws MessagingConnectorException
408    {
409        return true;
410    }
411
412    @Override
413    public boolean internalIsEventExist(String eventId, UserIdentity organiser) throws MessagingConnectorException
414    {
415        try
416        {
417            ExchangeService service = getService(organiser);
418            if (service != null)
419            {
420                ItemId itemId = new ItemId(eventId);
421                Appointment appointment = Appointment.bind(service, itemId);
422                
423                return appointment != null;
424            }
425        }
426        catch (ServiceResponseException e)
427        {
428            Throwable cause = e.getCause();
429            ExceptionType type = _getExceptionType(cause);
430            if (type == ExceptionType.UNKNOWN)
431            {
432                return false; //Exchange doesn't find the event with id 'event Id'
433            }
434            else
435            {
436                //Throw an exception if this is a known error
437                throw new MessagingConnectorException("Failed to get event " + eventId + " from organiser " + organiser.toString(), type, e);
438            }
439        }
440        catch (MessagingConnectorException e)
441        {
442            throw e;
443        }
444        catch (Exception e) 
445        {
446            throw new MessagingConnectorException("Failed to get event " + eventId + " from organiser " + organiser.toString(), e);
447        }
448        
449        return false;
450    }
451    
452    @Override
453    public String internalCreateEvent(String title, String description, String place, boolean isAllDay, Date startDate, Date endDate, EventRecurrenceTypeEnum recurrenceType, Date untilDate, Map<String, Boolean> attendees, UserIdentity organiser) throws MessagingConnectorException
454    {
455        try
456        {
457            ExchangeService service = getService(organiser);
458            if (service != null)
459            {
460                Appointment appointment = new Appointment(service);
461                
462                _setDataEvent(service, appointment, title, description, place, isAllDay, startDate, endDate, recurrenceType, untilDate, attendees);
463                
464                User organiserUser = _userManager.getUser(organiser);
465                Mailbox mailBox = new Mailbox(organiserUser.getEmail());
466                appointment.save(new FolderId(WellKnownFolderName.Calendar, mailBox), SendInvitationsMode.SendOnlyToAll);
467                
468                return appointment.getId().getUniqueId();
469            }
470        }
471        catch (ServiceRequestException e)
472        {
473            Throwable cause = e.getCause();
474            ExceptionType type = _getExceptionType(cause);
475            throw new MessagingConnectorException("Failed to create event from organiser " + organiser.toString(), type, e);
476        }
477        catch (MessagingConnectorException e)
478        {
479            throw e;
480        }
481        catch (Exception e)
482        {
483            throw new MessagingConnectorException("Failed to create event from organiser " + organiser.toString(), e);
484        }
485            
486        return null;
487    }
488    
489    @Override
490    public void internalUpdateEvent(String eventId, String title, String description, String place, boolean isAllDay, Date startDate, Date endDate, EventRecurrenceTypeEnum recurrenceType, Date untilDate, Map<String, Boolean> attendees, UserIdentity organiser) throws MessagingConnectorException
491    {
492        try
493        {
494            ExchangeService service = getService(organiser);
495            if (service != null)
496            {
497                ItemId itemId = new ItemId(eventId);
498                Appointment appointment = Appointment.bind(service, itemId);
499
500                _setDataEvent(service, appointment, title, description, place, isAllDay, startDate, endDate, recurrenceType, untilDate, attendees);
501                
502                appointment.update(ConflictResolutionMode.AlwaysOverwrite, SendInvitationsOrCancellationsMode.SendOnlyToAll);
503            }
504        }
505        catch (ServiceRequestException e)
506        {
507            Throwable cause = e.getCause();
508            ExceptionType type = _getExceptionType(cause);
509            throw new MessagingConnectorException("Failed to update event from organiser " + organiser.toString(), type, e);
510        }
511        catch (MessagingConnectorException e)
512        {
513            throw e;
514        }
515        catch (Exception e)
516        {
517            throw new MessagingConnectorException("Failed to update event from organiser " + organiser.toString(), e);
518        }
519    }
520    
521    private void _setDataEvent(ExchangeService service, Appointment appointment, String title, String description, String place, boolean isAllDay, Date startDate, Date endDate, EventRecurrenceTypeEnum recurrenceType, Date untilDate, Map<String, Boolean> attendees) throws Exception
522    {
523        TimeZone defaultTimeZone = TimeZone.getDefault();
524        Map<String, String> olsonTimeZoneToMsMap = TimeZoneUtils.createOlsonTimeZoneToMsMap();
525        String msTimeZoneId = olsonTimeZoneToMsMap.get(defaultTimeZone.getID());
526
527        Collection<TimeZoneDefinition> serverTimeZones = service.getServerTimeZones(Collections.singletonList(msTimeZoneId));
528        TimeZoneDefinition timeZone = serverTimeZones.iterator().next();
529        
530        appointment.setSubject(title);
531        appointment.setBody(new MessageBody(description));
532        appointment.setStart(startDate);
533        if (isAllDay)
534        {
535            Date date = Date.from(endDate.toInstant().atZone(ZoneId.systemDefault()).plusDays(1).toInstant());
536            appointment.setEnd(date);
537        }
538        else
539        {
540            appointment.setEnd(endDate);
541        }
542        appointment.setIsAllDayEvent(isAllDay);
543        appointment.setLocation(place);
544        appointment.setStartTimeZone(timeZone);
545        appointment.setEndTimeZone(timeZone);
546        
547        _setRecurrence(appointment, startDate, recurrenceType, untilDate);
548        
549        _setAttendees(appointment, attendees);
550    }
551    
552    private void _setRecurrence(Appointment appointment, Date startDate, EventRecurrenceTypeEnum recurrenceType, Date untilDate) throws Exception
553    {
554        Recurrence recurrence = null;
555        switch (recurrenceType)
556        {
557            case ALL_DAY:
558                recurrence = new Recurrence.DailyPattern(startDate, 1);
559                break;
560            case ALL_WORKING_DAY:
561                String workingDayAsString = Config.getInstance().getValue("org.ametys.plugins.explorer.calendar.event.working.day");
562                
563                List<DayOfTheWeek> days = new ArrayList<>();
564                for (String idDay : StringUtils.split(workingDayAsString, ","))
565                {
566                    days.add(EnumUtils.getEnumList(DayOfTheWeek.class).get(Integer.parseInt(idDay) - 1));
567                }
568                
569                recurrence = new Recurrence.WeeklyPattern(startDate, 1, days.toArray(new DayOfTheWeek[days.size()]));
570                break;
571            case WEEKLY:
572                ZonedDateTime startWeeklyDateTime = startDate.toInstant().atZone(ZoneId.systemDefault());
573                int dayOfWeekForWeekly = startWeeklyDateTime.getDayOfWeek().getValue();
574                
575                recurrence = new Recurrence.WeeklyPattern(startDate, 1, EnumUtils.getEnumList(DayOfTheWeek.class).get(dayOfWeekForWeekly % 7));
576                break;
577            case BIWEEKLY:
578                ZonedDateTime startBiWeeklyDateTime = startDate.toInstant().atZone(ZoneId.systemDefault());
579                int dayOfWeekForBiWeekly = startBiWeeklyDateTime.getDayOfWeek().getValue();
580                
581                recurrence = new Recurrence.WeeklyPattern(startDate, 2, EnumUtils.getEnumList(DayOfTheWeek.class).get(dayOfWeekForBiWeekly % 7));
582                break;
583            case MONTHLY:
584                ZonedDateTime startMonthlyDateTime = startDate.toInstant().atZone(ZoneId.systemDefault());
585                int dayOfMonth = startMonthlyDateTime.getDayOfMonth();
586                
587                recurrence = new Recurrence.MonthlyPattern(startDate, 1, dayOfMonth);
588                break;
589            case NEVER:
590            default:
591                //Still null
592                break;
593        }
594
595        if (untilDate != null && recurrence != null)
596        {
597            recurrence.setEndDate(untilDate);
598            appointment.setRecurrence(recurrence);
599        }
600    }
601
602    @Override
603    public void internalDeleteEvent(String eventId, UserIdentity organiser) throws MessagingConnectorException
604    {
605        try
606        {
607            ExchangeService service = getService(organiser);
608            if (service != null)
609            {
610                ItemId itemId = new ItemId(eventId);
611                Appointment appointment = Appointment.bind(service, itemId);
612                appointment.delete(DeleteMode.MoveToDeletedItems);
613            }
614        }
615        catch (ServiceRequestException e)
616        {
617            Throwable cause = e.getCause();
618            ExceptionType type = _getExceptionType(cause);
619            throw new MessagingConnectorException("Failed to delete event " + eventId + " with organiser " + organiser.toString(), type, e);
620        }
621        catch (Exception e)
622        {
623            throw new MessagingConnectorException("Failed to delete event " + eventId + " with organiser " + organiser.toString(), e);
624        }
625            
626    }
627
628    @Override
629    public Map<String, AttendeeInformation> internalGetAttendees(String eventId, UserIdentity organiser) throws MessagingConnectorException
630    {
631        Map<String, AttendeeInformation> attendees = new HashMap<>();
632        try
633        {
634            ExchangeService service = getService(organiser);
635            if (service != null)
636            {
637                ItemId itemId = new ItemId(eventId);
638                Appointment appointment = Appointment.bind(service, itemId);
639                
640                for (Attendee attendee : appointment.getRequiredAttendees())
641                {
642                    ResponseType responseStatus = _getResponseStatus(attendee.getResponseType());
643                    AttendeeInformation attendeeInformation = new AttendeeInformation(true, responseStatus);
644                    attendees.put(attendee.getAddress(), attendeeInformation);
645                }
646                
647                for (Attendee attendee : appointment.getOptionalAttendees())
648                {
649                    ResponseType responseStatus = _getResponseStatus(attendee.getResponseType());
650                    AttendeeInformation attendeeInformation = new AttendeeInformation(false, responseStatus);
651                    attendees.put(attendee.getAddress(), attendeeInformation);
652                }
653            }
654        }
655        catch (ServiceResponseException e)
656        {
657            Throwable cause = e.getCause();
658            ExceptionType type = _getExceptionType(cause);
659            if (type == ExceptionType.UNKNOWN)
660            {
661                return attendees; //Exchange doesn't find the event with id 'event Id'
662            }
663            else
664            {
665                throw new MessagingConnectorException("Failed to get attendees from event " + eventId + " with organiser " + organiser.toString(), type, e);
666            }
667        }
668        catch (Exception e)
669        {
670            throw new MessagingConnectorException("Failed to get attendees from event " + eventId + " with organiser " + organiser.toString(), e);
671        }
672        
673        return attendees;
674    }
675
676    @Override
677    public void internalSetAttendees(String eventId, Map<String, Boolean> attendees, UserIdentity organiser) throws MessagingConnectorException
678    {
679        try
680        {
681            ExchangeService service = getService(organiser);
682            if (service != null)
683            {
684                ItemId itemId = new ItemId(eventId);
685                Appointment appointment = Appointment.bind(service, itemId);
686                
687                _setAttendees(appointment, attendees);
688                
689                appointment.update(ConflictResolutionMode.AlwaysOverwrite, SendInvitationsOrCancellationsMode.SendOnlyToChanged);
690            }
691        }
692        catch (ServiceRequestException e)
693        {
694            Throwable cause = e.getCause();
695            ExceptionType type = _getExceptionType(cause);
696            throw new MessagingConnectorException("Failed to get attendees from event " + eventId + " with organiser " + organiser.toString(), type, e);
697        }
698        catch (Exception e)
699        {
700            throw new MessagingConnectorException("Failed to get attendees from event " + eventId + " with organiser " + organiser.toString(), e);
701        }
702    }
703
704    @Override
705    public Map<String, FreeBusyStatus> internalGetFreeBusy(Date startDate, Date endDate, boolean isAllDay, Set<String> attendees, UserIdentity organiser) throws MessagingConnectorException
706    {
707        Map<String, FreeBusyStatus> attendeesMap = new HashMap<>();
708        if (attendees.isEmpty())
709        {
710            return attendeesMap;
711        }
712        
713        try
714        {
715            ExchangeService service = getService(organiser);
716            if (service != null)
717            {
718                TimeWindow timeWindow = null;
719                if (isAllDay)
720                {
721                    Date endDatePlus1 = Date.from(endDate.toInstant().atZone(ZoneId.systemDefault()).plusDays(1).toInstant());
722                    timeWindow = new TimeWindow(startDate, endDatePlus1);
723                }
724                else
725                {
726                    Date startDateMinus1 = Date.from(startDate.toInstant().atZone(ZoneId.systemDefault()).minusDays(1).toInstant());
727                    Date endDatePlus1 = Date.from(endDate.toInstant().atZone(ZoneId.systemDefault()).plusDays(1).toInstant());
728                    timeWindow = new TimeWindow(startDateMinus1, endDatePlus1);
729                }
730
731                List<AttendeeInfo> attendeesInfo = new ArrayList<>();
732                for (String email : attendees)
733                {
734                    attendeesInfo.add(new AttendeeInfo(email));
735                }
736                
737                GetUserAvailabilityResults userAvailability = service.getUserAvailability(attendeesInfo, timeWindow, AvailabilityData.FreeBusy);
738                int index = 0;
739                for (AttendeeAvailability availability : userAvailability.getAttendeesAvailability())
740                {
741                    AttendeeInfo attendeeInfo = attendeesInfo.get(index);
742                    String email = attendeeInfo.getSmtpAddress();
743                    
744                    FreeBusyStatus freeBusyStatus = FreeBusyStatus.Unknown;
745                    if (!ServiceResult.Error.equals(availability.getResult()))
746                    {
747                        freeBusyStatus = FreeBusyStatus.Free;
748                        for (microsoft.exchange.webservices.data.property.complex.availability.CalendarEvent calEvent : availability.getCalendarEvents())
749                        {
750                            if (isAllDay)
751                            {
752                                if (calEvent.getFreeBusyStatus().equals(LegacyFreeBusyStatus.Busy))
753                                {
754                                    freeBusyStatus = FreeBusyStatus.Busy;
755                                }
756                            }
757                            else
758                            {
759                                if (calEvent.getFreeBusyStatus().equals(LegacyFreeBusyStatus.Busy) && startDate.before(calEvent.getEndTime()) && endDate.after(calEvent.getStartTime()))
760                                {
761                                    freeBusyStatus = FreeBusyStatus.Busy;
762                                }
763                            }
764                        }
765                    }
766                    
767                    attendeesMap.put(email, freeBusyStatus);
768                    index++;
769                }
770            }
771        }
772        catch (ServiceRequestException e)
773        {
774            Throwable cause = e.getCause();
775            ExceptionType type = _getExceptionType(cause);
776            throw new MessagingConnectorException("Failed to get free/busy with organiser " + organiser.toString(), type, e);
777        }
778        catch (Exception e)
779        {
780            throw new MessagingConnectorException("Failed to get free/busy with organiser " + organiser.toString(), e);
781        }
782        
783        return attendeesMap;
784    }
785    
786    @Override
787    public boolean isUserExist(UserIdentity userIdentity) throws MessagingConnectorException
788    {
789        try
790        {
791            ExchangeService service = getService(userIdentity);
792            if (service != null)
793            {
794                Folder.bind(service, WellKnownFolderName.Inbox);
795                return true;
796            }
797            
798            return false;
799        }
800        catch (ServiceRequestException e)
801        {
802            Throwable cause = e.getCause();
803            ExceptionType type = _getExceptionType(cause);
804            if (type == ExceptionType.UNKNOWN)
805            {
806                return false;
807            }
808            else
809            {
810                throw new MessagingConnectorException("Failed to know if user " + userIdentity.getLogin() + " exist in exchange", type, e);
811            }
812        }
813        catch (Exception e)
814        {
815            throw new MessagingConnectorException("Failed to know if user " + userIdentity.getLogin() + " exist in exchange", e);
816        }
817    }
818    
819    private ResponseType _getResponseStatus(MeetingResponseType meetingResponseType)
820    {
821        switch (meetingResponseType)
822        {
823            case Accept:
824                return ResponseType.Accept;
825            case Decline:
826                return ResponseType.Decline;
827            case Tentative:
828                return ResponseType.Maybe;
829            default:
830                return ResponseType.Unknown;
831        }
832    }
833    
834    private void _setAttendees(Appointment appointment, Map<String, Boolean> attendees) throws Exception
835    {
836        if (attendees != null)
837        {
838            AttendeeCollection requiredAttendees = appointment.getRequiredAttendees();
839            AttendeeCollection optionalAttendees = appointment.getOptionalAttendees();
840            
841            requiredAttendees.clear();
842            optionalAttendees.clear();
843            for (String email : attendees.keySet())
844            {
845                boolean isMandatory = attendees.get(email);
846                if (isMandatory)
847                {
848                    requiredAttendees.add(new Attendee(email));
849                }
850                else
851                {
852                    optionalAttendees.add(new Attendee(email));
853                }
854            }
855        }
856    }
857    
858    /**
859     * Converts a given html String into a plain text String
860     * @param html the html String that will be converted
861     * @return a String plain text of the given html
862     */
863    protected static String html2text(String html)
864    {
865        return Jsoup.parse(html).text();
866    }
867
868    /**
869     * get the type of exception from the Throwable
870     * @param exception exception thrown by Exchange API
871     * @return {@link ExceptionType}
872     */
873    private ExceptionType _getExceptionType(Throwable exception)
874    {
875        ExceptionType type = ExceptionType.UNKNOWN;
876        if (exception == null)
877        {
878            return ExceptionType.UNKNOWN;
879        }
880
881        HttpErrorException httpException = null;
882        if (exception instanceof HttpErrorException)
883        {
884            httpException = (HttpErrorException) exception;
885        }
886        if (exception.getCause() instanceof HttpErrorException)
887        {
888            httpException = (HttpErrorException) exception.getCause();
889        }
890
891        if (httpException != null)
892        {
893            int httpErrorCode = httpException.getHttpErrorCode();
894            if (httpErrorCode == 401)
895            {
896                if (!supportUserCredential())
897                {
898                    // Impersonation, so this is not a problem about the user but a configuration exception
899                    type = ExceptionType.CONFIGURATION_EXCEPTION;
900                }
901                else
902                {
903                    type = ExceptionType.UNAUTHORIZED;
904                }
905            }
906            else if (httpErrorCode == 404)
907            {
908                type = ExceptionType.CONFIGURATION_EXCEPTION;
909            }
910        }
911        else if (exception.getCause() instanceof UnknownHostException)
912        {
913            type = ExceptionType.CONFIGURATION_EXCEPTION;
914        }
915        else if (exception.getCause() instanceof SocketTimeoutException)
916        {
917            type = ExceptionType.TIMEOUT;
918        }
919        return type;
920    }
921}