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