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