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}