001/* 002 * Copyright 2020 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.time.LocalDateTime; 019import java.time.ZoneId; 020import java.time.ZoneOffset; 021import java.time.ZonedDateTime; 022import java.time.format.DateTimeFormatter; 023import java.time.temporal.ChronoUnit; 024import java.util.ArrayList; 025import java.util.Collections; 026import java.util.Date; 027import java.util.List; 028import java.util.Map; 029import java.util.TreeMap; 030import java.util.stream.Collectors; 031 032import org.apache.avalon.framework.service.ServiceException; 033import org.apache.avalon.framework.service.ServiceManager; 034import org.apache.commons.lang3.StringUtils; 035 036import org.ametys.core.user.User; 037import org.ametys.core.user.UserIdentity; 038import org.ametys.core.user.UserManager; 039import org.ametys.core.util.DateUtils; 040import org.ametys.plugins.messagingconnector.AbstractMessagingConnector; 041import org.ametys.plugins.messagingconnector.CalendarEvent; 042import org.ametys.plugins.messagingconnector.EmailMessage; 043import org.ametys.plugins.messagingconnector.MessagingConnectorException; 044import org.ametys.runtime.config.Config; 045 046import com.microsoft.graph.auth.confidentialClient.ClientCredentialProvider; 047import com.microsoft.graph.auth.enums.NationalCloud; 048import com.microsoft.graph.models.extensions.Event; 049import com.microsoft.graph.models.extensions.IGraphServiceClient; 050import com.microsoft.graph.models.extensions.Message; 051import com.microsoft.graph.options.QueryOption; 052import com.microsoft.graph.requests.extensions.GraphServiceClient; 053import com.microsoft.graph.requests.extensions.IMessageCollectionPage; 054 055/** 056 * The connector used by the messaging connector plugin when connecting to Exchange Online.<br> 057 * Implemented through the Microsoft Graph API. 058 */ 059public class GraphConnector extends AbstractMessagingConnector 060{ 061 private static final String __SCOPE = "https://graph.microsoft.com/.default"; 062 063 private IGraphServiceClient _graphClient; 064 private UserManager _userManager; 065 066 @Override 067 public void service(ServiceManager manager) throws ServiceException 068 { 069 super.service(manager); 070 _userManager = (UserManager) manager.lookup(UserManager.ROLE); 071 } 072 073 @Override 074 public void initialize() 075 { 076 super.initialize(); 077 078 String appId = Config.getInstance().getValue("org.ametys.plugins.exchange.appid"); 079 String clientSecret = Config.getInstance().getValue("org.ametys.plugins.exchange.clientsecret"); 080 String tenant = Config.getInstance().getValue("org.ametys.plugins.exchange.tenant"); 081 082 ClientCredentialProvider authProvider = new ClientCredentialProvider(appId, List.of(__SCOPE), clientSecret, tenant, NationalCloud.Global); 083 084 _graphClient = GraphServiceClient.builder() 085 .authenticationProvider(authProvider) 086 .buildClient(); 087 } 088 089 private String _getUserPrincipalName(UserIdentity userIdentity) 090 { 091 String authMethod = Config.getInstance().getValue("org.ametys.plugins.exchange.authmethod"); 092 if ("email".equals(authMethod)) 093 { 094 User user = _userManager.getUser(userIdentity); 095 String email = user.getEmail(); 096 if (StringUtils.isBlank(email)) 097 { 098 if (getLogger().isWarnEnabled()) 099 { 100 getLogger().warn("The user '" + userIdentity.getLogin() + "' has no email address set, thus exchange cannot be contacted using 'email' authentication method"); 101 } 102 103 return null; 104 } 105 106 return email; 107 } 108 else 109 { 110 return userIdentity.getLogin(); 111 } 112 } 113 114 private List<Event> _getEvents(UserIdentity userIdentity, int maxDays) 115 { 116 String userPrincipal = _getUserPrincipalName(userIdentity); 117 if (userPrincipal == null) 118 { 119 return Collections.emptyList(); 120 } 121 122 ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC).truncatedTo(ChronoUnit.SECONDS); 123 124 String start = now.format(DateTimeFormatter.ISO_DATE_TIME); 125 String end = now.plusDays(maxDays).format(DateTimeFormatter.ISO_DATE_TIME); 126 127 return _graphClient.users(userPrincipal) 128 .calendar() 129 .calendarView() 130 .buildRequest(new QueryOption("startDateTime", start), 131 new QueryOption("endDateTime", end), 132 new QueryOption("$orderby", "start/datetime")) 133 .select("subject,start,end,location") 134 .get().getCurrentPage(); 135 } 136 137 @Override 138 protected List<CalendarEvent> internalGetEvents(UserIdentity userIdentity, int maxDays, int maxEvents) throws MessagingConnectorException 139 { 140 List<Event> events = _getEvents(userIdentity, maxDays); 141 142 Map<Long, CalendarEvent> calendarEvents = new TreeMap<>(); 143 for (Event event : events) 144 { 145 LocalDateTime ldt = LocalDateTime.parse(event.start.dateTime, DateTimeFormatter.ISO_DATE_TIME); 146 ZonedDateTime zdt = ldt.atZone(ZoneId.of(event.start.timeZone)); 147 zdt = zdt.withZoneSameInstant(ZoneId.systemDefault()); 148 149 long longStart = zdt.toEpochSecond(); 150 Date startDate = DateUtils.asDate(zdt); 151 152 ldt = LocalDateTime.parse(event.end.dateTime, DateTimeFormatter.ISO_DATE_TIME); 153 zdt = ldt.atZone(ZoneId.of(event.end.timeZone)); 154 zdt = zdt.withZoneSameInstant(ZoneId.systemDefault()); 155 156 Date endDate = DateUtils.asDate(zdt); 157 158 CalendarEvent newEvent = new CalendarEvent(); 159 newEvent.setStartDate(startDate); 160 newEvent.setEndDate(endDate); 161 newEvent.setSubject(event.subject); 162 newEvent.setLocation(event.location.displayName); 163 calendarEvents.put(longStart, newEvent); 164 } 165 166 return calendarEvents.entrySet().stream().limit(maxEvents).map(e -> e.getValue()).collect(Collectors.toList()); 167 } 168 169 @Override 170 protected int internalGetEventsCount(UserIdentity userIdentity, int maxDays) throws MessagingConnectorException 171 { 172 return _getEvents(userIdentity, maxDays).size(); 173 } 174 175 @Override 176 protected List<EmailMessage> internalGetEmails(UserIdentity userIdentity, int maxEmails) throws MessagingConnectorException 177 { 178 String userPrincipal = _getUserPrincipalName(userIdentity); 179 if (userPrincipal == null) 180 { 181 return Collections.emptyList(); 182 } 183 184 List<EmailMessage> result = new ArrayList<>(); 185 186 IMessageCollectionPage messageCollectionPage = _graphClient.users(userPrincipal) 187 .mailFolders("inbox").messages().buildRequest() 188 .top(maxEmails) 189 .select("sender,subject,bodyPreview") 190 .filter("isRead eq false") 191 .get(); 192 193 for (Message message : messageCollectionPage.getCurrentPage()) 194 { 195 result.add(new EmailMessage(message.subject, message.sender.emailAddress.address, message.bodyPreview)); 196 } 197 198 return result; 199 } 200 201 @Override 202 protected int internalGetEmailsCount(UserIdentity userIdentity) throws MessagingConnectorException 203 { 204 String userPrincipal = _getUserPrincipalName(userIdentity); 205 if (userPrincipal == null) 206 { 207 return 0; 208 } 209 210 return _graphClient.users(userPrincipal).mailFolders("inbox").buildRequest().get().unreadItemCount; 211 } 212}