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