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}