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