001/*
002 *  Copyright 2026 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.IOException;
019import java.util.Arrays;
020import java.util.HashMap;
021import java.util.List;
022import java.util.Map;
023import java.util.Objects;
024import java.util.Set;
025import java.util.stream.Collectors;
026
027import org.apache.avalon.framework.service.ServiceException;
028import org.apache.avalon.framework.service.ServiceManager;
029import org.apache.avalon.framework.service.Serviceable;
030import org.apache.commons.lang3.StringUtils;
031import org.apache.commons.lang3.tuple.Pair;
032
033import org.ametys.core.observation.AsyncObserver;
034import org.ametys.core.observation.Event;
035import org.ametys.core.ui.mail.StandardMailBodyHelper;
036import org.ametys.core.ui.mail.StandardMailBodyHelper.MailBodyBuilder;
037import org.ametys.core.user.CurrentUserProvider;
038import org.ametys.core.user.User;
039import org.ametys.core.user.UserIdentity;
040import org.ametys.core.user.UserManager;
041import org.ametys.core.util.I18nUtils;
042import org.ametys.core.util.language.UserLanguagesManager;
043import org.ametys.core.util.mail.SendMailHelper;
044import org.ametys.plugins.repository.AmetysObjectResolver;
045import org.ametys.plugins.workspaces.ObservationConstants;
046import org.ametys.plugins.workspaces.WorkspacesHelper;
047import org.ametys.plugins.workspaces.project.notification.preferences.NotificationPreferencesHelper;
048import org.ametys.plugins.workspaces.project.objects.Project;
049import org.ametys.runtime.i18n.I18nizableText;
050import org.ametys.runtime.i18n.I18nizableTextParameter;
051import org.ametys.runtime.plugin.component.AbstractLogEnabled;
052
053import jakarta.mail.MessagingException;
054
055/**
056 * Observer to send mail notifications to manager of project when a new member join the project
057 * @implNote the class doesn't extends AbstractMemberMailNotifierObserver because it relies
058 * directly on the {@link StandardMailBodyHelper} instead of the early custom implementation
059 */
060public class NewMemberMailManagersNotifierObserver extends AbstractLogEnabled implements AsyncObserver, Serviceable
061{
062    /** The current user provider */
063    protected CurrentUserProvider _currentUserProvider;
064    /** The I18n utils */
065    protected I18nUtils _i18nUtils;
066    /** The notification preference helper */
067    protected NotificationPreferencesHelper _notificationPrefHelper;
068    /** The ametys object resolver */
069    protected AmetysObjectResolver _resolver;
070    /** The user languages manager */
071    protected UserLanguagesManager _userLanguagesManager;
072    /** The user manager */
073    protected UserManager _userManager;
074    /** The workspaces helper */
075    protected WorkspacesHelper _workspacesHelper;
076
077    public void service(ServiceManager manager) throws ServiceException
078    {
079        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
080        _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE);
081        _notificationPrefHelper = (NotificationPreferencesHelper) manager.lookup(NotificationPreferencesHelper.ROLE);
082        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
083        _userLanguagesManager = (UserLanguagesManager) manager.lookup(UserLanguagesManager.ROLE);
084        _userManager = (UserManager) manager.lookup(UserManager.ROLE);
085        _workspacesHelper = (WorkspacesHelper) manager.lookup(WorkspacesHelper.ROLE);
086    }
087    
088    @Override
089    public int getPriority()
090    {
091        return MIN_PRIORITY;
092    }
093    
094    public boolean supports(Event event)
095    {
096        return event.getId().equals(ObservationConstants.EVENT_MEMBER_JOINED);
097    }
098    
099    @Override
100    public void observe(Event event, Map<String, Object> transientVars) throws Exception
101    {
102        Map<String, Object> args = event.getArguments();
103        UserIdentity currentUser = _currentUserProvider.getUser();
104        
105        String projectId = (String) args.get(ObservationConstants.ARGS_PROJECT_ID);
106        Project project = _resolver.resolveById(projectId);
107        String url = project.getSite().getUrl();
108        
109        // Compute subject and body
110        Map<String, I18nizableTextParameter> params = new HashMap<>();
111        User current = _userManager.getUser(currentUser);
112        params.put("user", new I18nizableText(current != null ? current.getFullName() : currentUser.getLogin()));
113        params.put("project", new I18nizableText(project.getTitle()));
114        params.put("url", new I18nizableText(url != null ? url : ""));
115
116        I18nizableText i18nSubject = new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_CATALOGUE_JOINPROJECT_MAIL_TITLE", params);
117        I18nizableText i18nTextBody = new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_CATALOGUE_JOINPROJECT_MAIL_BODY_TEXT", params);
118        
119        MailBodyBuilder htmlBodyBuilder = StandardMailBodyHelper.newHTMLBody()
120                .withTitle(i18nSubject)
121                .withMessage(new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_CATALOGUE_JOINPROJECT_MAIL_BODY_HTML", params))
122                .withLink(url, new I18nizableText("plugin.workspaces", "PROJECT_MAIL_NOTIFICATION_BODY_DEFAULT_BUTTON_TEXT"));
123        
124        Map<String, List<String>> recipientsByLanguage = getUserToNotifyByLanguage(event, project);
125        
126        for (String language : recipientsByLanguage.keySet())
127        {
128            String subject = _i18nUtils.translate(i18nSubject, language);
129            String txtBody = _i18nUtils.translate(i18nTextBody, language);
130            
131            String htmlBody = htmlBodyBuilder.withLanguage(language).build();
132
133            try
134            {
135                SendMailHelper.newMail()
136                    .withSubject(subject)
137                    .withTextBody(txtBody)
138                    .withHTMLBody(htmlBody)
139                    .withRecipients(recipientsByLanguage.get(language))
140                    .withAsync(true)
141                    .withInlineCSS(false)
142                    .sendMail();
143            }
144            catch (MessagingException | IOException e)
145            {
146                getLogger().warn("Could not send a notification e-mail to " + recipientsByLanguage + " following his removal from the project " + project.getTitle(), e);
147            }
148            
149        }
150    }
151    
152    /**
153     * Get the user to notify
154     * @param event the event to notify
155     * @param project the project related to the event
156     * @return the list of email to notify by language of notification
157     */
158    protected Map<String, List<String>> getUserToNotifyByLanguage(Event event, Project project)
159    {
160        String defaultLang = _workspacesHelper.getLang(project, _userLanguagesManager.getDefaultLanguage());
161        
162        // Recipients are project managers
163        return Arrays.stream(project.getManagers())
164                .map(_userManager::getUser)
165                .filter(Objects::nonNull)
166                .map(user -> Pair.of(user, user.getEmail()))
167                .filter(p -> StringUtils.isNotEmpty(p.getRight()))
168                .filter(p -> {
169                    Set<String> pausedProjects = _notificationPrefHelper.getPausedProjects(p.getLeft().getIdentity());
170                    return pausedProjects != null   // all notification paused
171                        && !pausedProjects.contains(project.getName());
172                })
173                .collect(Collectors.groupingBy(
174                        p -> {
175                            return StringUtils.defaultIfEmpty(p.getLeft().getLanguage(), defaultLang);
176                        },
177                        Collectors.mapping(
178                                Pair::getRight,
179                                Collectors.toList()
180                        )
181                    )
182                );
183    }
184}