001/* 002 * Copyright 2019 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.workspaces.project.notification; 017 018import java.io.ByteArrayOutputStream; 019import java.io.IOException; 020import java.io.InputStream; 021import java.util.List; 022import java.util.Map; 023import java.util.stream.Collectors; 024 025import org.apache.avalon.framework.context.Context; 026import org.apache.avalon.framework.context.ContextException; 027import org.apache.avalon.framework.context.Contextualizable; 028import org.apache.avalon.framework.service.ServiceException; 029import org.apache.avalon.framework.service.ServiceManager; 030import org.apache.avalon.framework.service.Serviceable; 031import org.apache.cocoon.components.ContextHelper; 032import org.apache.cocoon.environment.Request; 033import org.apache.commons.lang.StringUtils; 034import org.apache.excalibur.source.Source; 035import org.apache.excalibur.source.SourceResolver; 036import org.apache.excalibur.source.SourceUtil; 037 038import org.ametys.core.group.GroupManager; 039import org.ametys.core.observation.AsyncObserver; 040import org.ametys.core.observation.Event; 041import org.ametys.core.user.User; 042import org.ametys.core.user.UserManager; 043import org.ametys.core.util.I18nUtils; 044import org.ametys.core.util.mail.SendMailHelper; 045import org.ametys.plugins.repository.AmetysObjectResolver; 046import org.ametys.plugins.workspaces.ObservationConstants; 047import org.ametys.plugins.workspaces.members.ProjectMemberManager; 048import org.ametys.plugins.workspaces.project.ProjectManager; 049import org.ametys.plugins.workspaces.project.objects.Project; 050import org.ametys.runtime.i18n.I18nizableText; 051import org.ametys.runtime.plugin.component.AbstractLogEnabled; 052import org.ametys.runtime.plugin.component.PluginAware; 053import org.ametys.web.WebConstants; 054import org.ametys.web.population.PopulationContextHelper; 055import org.ametys.web.renderingcontext.RenderingContext; 056import org.ametys.web.renderingcontext.RenderingContextHandler; 057import org.ametys.web.repository.site.Site; 058import org.ametys.web.repository.site.SiteManager; 059 060import jakarta.mail.MessagingException; 061 062/** 063 * Abstract observer for sending mail to members 064 */ 065public abstract class AbstractMemberMailNotifierObserver extends AbstractLogEnabled implements AsyncObserver, PluginAware, Serviceable, Contextualizable 066{ 067 /** The avalon context */ 068 protected Context _context; 069 /** The name of current plugin */ 070 protected String _pluginName; 071 /** The Ametys Object resolver */ 072 protected AmetysObjectResolver _resolver; 073 /** The i18n utils */ 074 protected I18nUtils _i18nUtils; 075 /** The project member manager */ 076 protected ProjectMemberManager _projectMemberManager; 077 /** Project manager */ 078 protected ProjectManager _projectManager; 079 /** Site manager */ 080 protected SiteManager _siteManager; 081 /** Population context helper */ 082 protected PopulationContextHelper _populationContextHelper; 083 /** Source Resolver */ 084 protected SourceResolver _srcResolver; 085 /** User manager */ 086 protected UserManager _userManager; 087 /** Group manager */ 088 protected GroupManager _groupManager; 089 /** The rendering context */ 090 protected RenderingContextHandler _renderingContextHandler; 091 092 093 @Override 094 public void setPluginInfo(String pluginName, String featureName, String id) 095 { 096 _pluginName = pluginName; 097 } 098 099 @Override 100 public void contextualize(Context context) throws ContextException 101 { 102 _context = context; 103 } 104 105 public void service(ServiceManager manager) throws ServiceException 106 { 107 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 108 _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE); 109 _projectMemberManager = (ProjectMemberManager) manager.lookup(ProjectMemberManager.ROLE); 110 _projectManager = (ProjectManager) manager.lookup(ProjectManager.ROLE); 111 _siteManager = (SiteManager) manager.lookup(SiteManager.ROLE); 112 _populationContextHelper = (PopulationContextHelper) manager.lookup(org.ametys.core.user.population.PopulationContextHelper.ROLE); 113 _srcResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE); 114 _userManager = (UserManager) manager.lookup(UserManager.ROLE); 115 _groupManager = (GroupManager) manager.lookup(GroupManager.ROLE); 116 _renderingContextHandler = (RenderingContextHandler) manager.lookup(RenderingContextHandler.ROLE); 117 } 118 119 @Override 120 public int getPriority(Event event) 121 { 122 return MIN_PRIORITY; 123 } 124 125 @Override 126 public void observe(Event event, Map<String, Object> transientVars) throws Exception 127 { 128 Map<String, Object> args = event.getArguments(); 129 130 String projectId = (String) args.get(ObservationConstants.ARGS_PROJECT_ID); 131 Project project = _resolver.resolveById(projectId); 132 Site site = project.getSite(); 133 String lang = site.getSitemaps().iterator().next().getName(); 134 // Compute subject and body 135 I18nizableText i18nSubject = getI18nSubject(event, project); 136 String subject = _i18nUtils.translate(i18nSubject, lang); 137 138 String mailBody; 139 Source source = null; 140 RenderingContext currentContext = _renderingContextHandler.getRenderingContext(); 141 try 142 { 143 // Force rendering context.FRONT to resolve URI 144 _renderingContextHandler.setRenderingContext(RenderingContext.FRONT); 145 Request request = ContextHelper.getRequest(_context); 146 request.setAttribute("forceAbsoluteUrl", true); 147 request.setAttribute("forceBase64Encoding", true); 148 request.setAttribute("lang", lang); 149 request.setAttribute(WebConstants.REQUEST_ATTR_SITE, site); 150 request.setAttribute(WebConstants.REQUEST_ATTR_SITE_NAME, site.getName()); 151 request.setAttribute(WebConstants.REQUEST_ATTR_SKIN_ID, site.getSkinId()); 152 source = _srcResolver.resolveURI(getMailBodyURI(event, project), null, Map.of("event", event, "project", project)); 153 try (InputStream is = source.getInputStream()) 154 { 155 ByteArrayOutputStream bos = new ByteArrayOutputStream(); 156 SourceUtil.copy(is, bos); 157 158 mailBody = bos.toString("UTF-8"); 159 } 160 } 161 catch (IOException e) 162 { 163 throw new RuntimeException("Failed to create mail body", e); 164 } 165 finally 166 { 167 _renderingContextHandler.setRenderingContext(currentContext); 168 169 if (source != null) 170 { 171 _srcResolver.release(source); 172 } 173 } 174 // Send mail to removed members 175 List<String> recipients = getUserToNotify(event, project); 176 try 177 { 178 SendMailHelper.newMail() 179 .withSubject(subject) 180 .withHTMLBody(mailBody) 181 .withRecipients(recipients) 182 .withAsync(true) 183 .withInlineCSS(false) 184 .sendMail(); 185 } 186 catch (MessagingException | IOException e) 187 { 188 getLogger().warn("Could not send a notification e-mail to " + recipients + " following his removal from the project " + project.getTitle(), e); 189 } 190 } 191 192 /** 193 * Get email's recipients 194 * @param event the event 195 * @param project the project 196 * @return the recipients 197 */ 198 protected abstract List<String> getUserToNotify(Event event, Project project); 199 200 /** 201 * Get the list of emails from users, filtering out users with no emails 202 * @param users The list of users 203 * @return The list of emails 204 */ 205 protected List<String> getUsersEmail(List<User> users) 206 { 207 return users.stream() 208 .map(User::getEmail) 209 .filter(StringUtils::isNotEmpty) 210 .collect(Collectors.toList()); 211 } 212 213 /** 214 * Returns the URI for HTML mail body 215 * @param event the event 216 * @param project the project 217 * @return The URI for HTML mail body 218 */ 219 protected String getMailBodyURI(Event event, Project project) 220 { 221 return "cocoon://_plugins/workspaces/notification-mail-member"; 222 } 223 224 /** 225 * Get the {@link I18nizableText} for mail subject 226 * @param event the event 227 * @param project the project 228 * @return the {@link I18nizableText} for subject 229 */ 230 protected abstract I18nizableText getI18nSubject(Event event, Project project); 231}