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}