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