001/*
002 *  Copyright 2018 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.members;
017
018import java.io.IOException;
019import java.io.UnsupportedEncodingException;
020import java.net.URLEncoder;
021import java.util.ArrayList;
022import java.util.Arrays;
023import java.util.Date;
024import java.util.HashMap;
025import java.util.List;
026import java.util.Map;
027import java.util.Set;
028
029import javax.jcr.RepositoryException;
030
031import org.apache.avalon.framework.component.Component;
032import org.apache.avalon.framework.configuration.Configurable;
033import org.apache.avalon.framework.configuration.Configuration;
034import org.apache.avalon.framework.configuration.ConfigurationException;
035import org.apache.avalon.framework.context.Context;
036import org.apache.avalon.framework.context.ContextException;
037import org.apache.avalon.framework.context.Contextualizable;
038import org.apache.avalon.framework.service.ServiceException;
039import org.apache.avalon.framework.service.ServiceManager;
040import org.apache.avalon.framework.service.Serviceable;
041import org.apache.cocoon.components.ContextHelper;
042import org.apache.cocoon.environment.Request;
043import org.apache.commons.lang3.StringUtils;
044
045import org.ametys.cms.languages.Language;
046import org.ametys.cms.languages.LanguagesManager;
047import org.ametys.cms.transformation.xslt.ResolveURIComponent;
048import org.ametys.core.ui.Callable;
049import org.ametys.core.ui.mail.StandardMailBodyHelper;
050import org.ametys.core.ui.mail.StandardMailBodyHelper.MailBodyBuilder;
051import org.ametys.core.user.CurrentUserProvider;
052import org.ametys.core.user.User;
053import org.ametys.core.user.UserIdentity;
054import org.ametys.core.user.UserManager;
055import org.ametys.core.user.directory.NotUniqueUserException;
056import org.ametys.core.user.directory.UserDirectory;
057import org.ametys.core.user.population.PopulationContextHelper;
058import org.ametys.core.util.I18nUtils;
059import org.ametys.core.util.language.UserLanguagesManager;
060import org.ametys.core.util.mail.SendMailHelper;
061import org.ametys.plugins.core.user.UserHelper;
062import org.ametys.plugins.repository.AmetysObjectIterable;
063import org.ametys.plugins.repository.provider.RequestAttributeWorkspaceSelector;
064import org.ametys.plugins.workspaces.WorkspacesHelper;
065import org.ametys.plugins.workspaces.members.MembersWorkspaceModule.Invitation;
066import org.ametys.plugins.workspaces.project.ProjectManager;
067import org.ametys.plugins.workspaces.project.modules.WorkspaceModuleExtensionPoint;
068import org.ametys.plugins.workspaces.project.objects.Project;
069import org.ametys.plugins.workspaces.project.rights.ProjectRightHelper;
070import org.ametys.runtime.authentication.AccessDeniedException;
071import org.ametys.runtime.i18n.I18nizableText;
072import org.ametys.runtime.i18n.I18nizableTextParameter;
073import org.ametys.runtime.plugin.component.AbstractLogEnabled;
074import org.ametys.runtime.plugin.component.PluginAware;
075import org.ametys.web.WebConstants;
076import org.ametys.web.WebHelper;
077import org.ametys.web.repository.page.Page;
078import org.ametys.web.repository.page.Zone;
079import org.ametys.web.repository.page.ZoneItem;
080import org.ametys.web.repository.site.Site;
081import org.ametys.web.repository.site.SiteManager;
082import org.ametys.web.usermanagement.UserManagementException;
083import org.ametys.web.usermanagement.UserManagementException.StatusError;
084import org.ametys.web.usermanagement.UserSignUpConfiguration;
085import org.ametys.web.usermanagement.UserSignupManager;
086
087import jakarta.mail.MessagingException;
088
089/**
090 * Helper for invitations by email
091 *
092 */
093public class ProjectInvitationHelper extends AbstractLogEnabled implements Serviceable, Component, Configurable, PluginAware, Contextualizable
094{
095    /** The role */
096    public static final String ROLE = ProjectInvitationHelper.class.getName();
097    
098    private static final String __MAIL_PROJECT_EMAIL_PATTERN = "${email}";
099    private static final String __MAIL_PROJECT_TOKEN_PATTERN = "${token}";
100    
101    private ProjectManager _projectManager;
102    private UserSignUpConfiguration _signupConfig;
103    private UserSignupManager _signupManager;
104    private SiteManager _siteManager;
105    private WorkspaceModuleExtensionPoint _moduleEP;
106    private CurrentUserProvider _currentUserProvider;
107    private UserManager _userManager;
108    private UserHelper _userHelper;
109    private ProjectRightHelper _projectRightsHelper;
110    private ProjectMemberManager _projectMemberManager;
111    private PopulationContextHelper _populationContextHelper;
112    private LanguagesManager _languagesManager;
113    private UserLanguagesManager _userLanguagesManager;
114    private WorkspacesHelper _workspacesHelper;
115    
116    private String _subjectKeyForInvitation;
117    private String _textBodyKeyForInvitation;
118    private String _htmlBodyKeyForInvitation;
119    private String _subjectKeyForInvitationAccepted;
120    private String _textBodyKeyForInvitationAccepted;
121    private String _htmlBodyKeyForInvitationAccepted;
122
123    private I18nUtils _i18nUtils;
124
125    private String _pluginName;
126
127    private Context _context;
128
129
130    public void setPluginInfo(String pluginName, String featureName, String id)
131    {
132        _pluginName = pluginName;
133    }
134    
135    public void contextualize(Context context) throws ContextException
136    {
137        _context = context;
138    }
139    
140    @Override
141    public void service(ServiceManager serviceManager) throws ServiceException
142    {
143        _projectManager = (ProjectManager) serviceManager.lookup(ProjectManager.ROLE);
144        _projectRightsHelper = (ProjectRightHelper) serviceManager.lookup(ProjectRightHelper.ROLE);
145        _projectMemberManager = (ProjectMemberManager) serviceManager.lookup(ProjectMemberManager.ROLE);
146        _signupConfig = (UserSignUpConfiguration) serviceManager.lookup(UserSignUpConfiguration.ROLE);
147        _signupManager = (UserSignupManager) serviceManager.lookup(UserSignupManager.ROLE);
148        _siteManager = (SiteManager) serviceManager.lookup(SiteManager.ROLE);
149        _moduleEP = (WorkspaceModuleExtensionPoint) serviceManager.lookup(WorkspaceModuleExtensionPoint.ROLE);
150        _currentUserProvider = (CurrentUserProvider) serviceManager.lookup(CurrentUserProvider.ROLE);
151        _userManager = (UserManager) serviceManager.lookup(UserManager.ROLE);
152        _userHelper = (UserHelper) serviceManager.lookup(UserHelper.ROLE);
153        _i18nUtils = (I18nUtils) serviceManager.lookup(I18nUtils.ROLE);
154        _populationContextHelper = (PopulationContextHelper) serviceManager.lookup(PopulationContextHelper.ROLE);
155        _languagesManager = (LanguagesManager) serviceManager.lookup(LanguagesManager.ROLE);
156        _userLanguagesManager = (UserLanguagesManager) serviceManager.lookup(UserLanguagesManager.ROLE);
157        _workspacesHelper = (WorkspacesHelper) serviceManager.lookup(WorkspacesHelper.ROLE);
158    }
159    
160    @Override
161    public void configure(Configuration configuration) throws ConfigurationException
162    {
163        _subjectKeyForInvitation = configuration.getChild("invitation-email-subject").getValue(null);
164        _textBodyKeyForInvitation = configuration.getChild("invitation-email-text-body").getValue(null);
165        _htmlBodyKeyForInvitation = configuration.getChild("invitation-email-html-body").getValue(null);
166        
167        _subjectKeyForInvitationAccepted = configuration.getChild("invitation-accepted-email-subject").getValue(null);
168        _textBodyKeyForInvitationAccepted = configuration.getChild("invitation-accepted-email-text-body").getValue(null);
169        _htmlBodyKeyForInvitationAccepted = configuration.getChild("invitation-accepted-email-html-body").getValue(null);
170    }
171    
172    /**
173     * Invite emails to be member of a project
174     * @param projectName The project name
175     * @param emails The emails
176     * @param allowedProfileByModule the allowed profiles by module
177     * @return The result
178     * @throws UserManagementException if failed to invit user
179     * @throws NotUniqueUserException if many users match the given email
180     */
181    public Map<String, Object> inviteEmails(String projectName, List<String> emails, Map<String, String> allowedProfileByModule) throws UserManagementException, NotUniqueUserException
182    {
183        Request request = ContextHelper.getRequest(_context);
184        String siteName = WebHelper.getSiteName(request);
185        String sitemapLanguage = (String) request.getAttribute(WebConstants.REQUEST_ATTR_SITEMAP_NAME);
186        
187        return inviteEmails(projectName, siteName, sitemapLanguage, emails, allowedProfileByModule);
188    }
189    /**
190     * Invite emails to be member of a project
191     * @param projectName The project name
192     * @param siteName The site name
193     * @param lang the current language
194     * @param emails The emails
195     * @param allowedProfileByModule the allowed profiles by module
196     * @return The result
197     * @throws UserManagementException if failed to invit user
198     * @throws NotUniqueUserException if many users match the given email
199     */
200    public Map<String, Object> inviteEmails(String projectName, String siteName, String lang, List<String> emails, Map<String, String> allowedProfileByModule) throws UserManagementException, NotUniqueUserException
201    {
202        Map<String, Object> result = new HashMap<>();
203        
204        result.put("existing-users", new ArrayList<>());
205        result.put("email-success", new ArrayList<>());
206        result.put("email-error", new ArrayList<>());
207        
208        Project project = _projectManager.getProject(projectName);
209        if (project != null)
210        {
211            MembersWorkspaceModule module = _moduleEP.getModule(MembersWorkspaceModule.MEMBERS_MODULE_ID);
212            if (module != null && _projectManager.isModuleActivated(project, module.getId()))
213            {
214                String language = _workspacesHelper.getLang(project, lang);
215                
216                if (!_projectRightsHelper.canAddMember(project))
217                {
218                    throw new AccessDeniedException("User '" + _currentUserProvider.getUser() + "' tried to send invitations without sufficient right.");
219                }
220                
221                String catalogSiteName = _projectManager.getCatalogSiteName();
222                
223                UserDirectory userDirectory = _getUserDirectoryForSignup(result, catalogSiteName, language);
224                if (userDirectory != null)
225                {
226                    String populationId = userDirectory.getPopulationId();
227                    String userDirectoryId = userDirectory.getId();
228                    
229                    // Prepare mail
230                    Map<String, String> emailConfiguration = _getInvitationEmailConfiguration(project, language);
231                    String mailSubject = emailConfiguration.get("subject");
232                    String mailBodyText = emailConfiguration.get("bodyText");
233                    String mailBodyHtml = emailConfiguration.get("bodyHtml");
234                    
235                    for (String email : emails)
236                    {
237                        try
238                        {
239                            _signupManager.inviteToSignup(catalogSiteName, language, email, populationId, userDirectoryId, null, null, false, false, false);
240                            
241                            if (_addOrUpdateInvitation(project, catalogSiteName, module, email, allowedProfileByModule, populationId, userDirectoryId, mailSubject, mailBodyText, mailBodyHtml))
242                            {
243                                @SuppressWarnings("unchecked")
244                                List<String> emailSuccess = (List<String>) result.get("email-success");
245                                emailSuccess.add(email);
246                                
247                            }
248                        }
249                        catch (UserManagementException e)
250                        {
251                            switch (e.getStatusError())
252                            {
253                                case USER_ALREADY_EXISTS:
254                                    User user = _getUser(catalogSiteName, email);
255                                    
256                                    Map<String, Object> user2json = _userHelper.user2json(user, true);
257                                    
258                                    @SuppressWarnings("unchecked")
259                                    List<Map<String, Object>> existingUsers = (List<Map<String, Object>>) result.get("existing-users");
260                                    existingUsers.add(user2json);
261                                    break;
262                                    
263                                case TEMP_USER_ALREADY_EXISTS:
264                                    // if mail has already been invited, re-created the invitation
265                                    if (_addOrUpdateInvitation(project, catalogSiteName, module, email, allowedProfileByModule, populationId, userDirectoryId, mailSubject, mailBodyText, mailBodyHtml))
266                                    {
267                                        @SuppressWarnings("unchecked")
268                                        List<String> emailSuccess = (List<String>) result.get("email-success");
269                                        emailSuccess.add(email);
270                                        
271                                    }
272                                    break;
273                                    
274                                default:
275                                    getLogger().error("Cannot invite " + email, e);
276                                    @SuppressWarnings("unchecked")
277                                    List<String> emailErrors = (List<String>) result.get("email-error");
278                                    emailErrors.add(email);
279                                    break;
280                            }
281                        }
282                    }
283                    
284                    @SuppressWarnings("unchecked")
285                    List<String> emailSuccess = (List<String>) result.get("email-success");
286                    if (emailSuccess.size() == emails.size())
287                    {
288                        result.put("success", true);
289                    }
290                }
291            }
292        }
293        else
294        {
295            result.put("success", false);
296            result.put("unknown-project", projectName);
297        }
298        
299        return result;
300    }
301    
302    private User _getUser(String siteName, String email) throws NotUniqueUserException
303    {
304        Set<String> populations = _populationContextHelper.getUserPopulationsOnContexts(Arrays.asList("/sites/" + siteName, "/sites-fo/" + siteName), false);
305        for (String population : populations)
306        {
307            User user = _userManager.getUser(population, email);
308            if (user == null)
309            {
310                user = _userManager.getUserByEmail(population, email);
311            }
312            
313            if (user != null)
314            {
315                return user;
316            }
317        }
318        
319        return null;
320    }
321    
322    /**
323     * Get the configuration to invite users by emails
324     * @param projectName The current project
325     * @param lang the current language
326     * @return the configuration for email invitations
327     */
328    @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION)
329    public Map<String, Object> getInvitationConfiguration(String projectName, String lang)
330    {
331        Project project = _projectManager.getProject(projectName);
332        
333        Map<String, Object> config = new HashMap<>();
334
335        // Check the current user has right to invite users
336        if (!_projectRightsHelper.canAddMember(project))
337        {
338            config.put("allowed", false);
339            config.put("error", "no-right");
340            return config;
341        }
342        
343        // Check the configuration is valid for invitations
344        String catalogSiteName = _projectManager.getCatalogSiteName();
345        if (_getUserDirectoryForSignup(config, catalogSiteName, lang) == null)
346        {
347            config.remove("success");
348            config.put("allowed", false);
349            return config;
350        }
351        
352        config.put("allowed", true);
353        config.put("email", _getInvitationEmailConfiguration(project, lang));
354        config.put("rights", _projectRightsHelper.getProjectRightsData(projectName));
355        
356        return config;
357    }
358    
359    private Map<String, String> _getInvitationEmailConfiguration(Project project, String lang)
360    {
361        Map<String, String> emailConfig = new HashMap<>();
362        
363        Map<String, I18nizableTextParameter> i18nparams = new HashMap<>();
364        i18nparams.put("projectTitle", new I18nizableText(project.getTitle()));
365        i18nparams.put("projectUrl", new I18nizableText(_projectManager.getProjectUrl(project, StringUtils.EMPTY)));
366        i18nparams.put("nbDays", new I18nizableText(String.valueOf(_signupConfig.getTokenValidity())));
367        i18nparams.put("nbDays", new I18nizableText(String.valueOf(_signupConfig.getTokenValidity())));
368        
369        String catalogSiteName = _projectManager.getCatalogSiteName();
370        Site catalogSite = _siteManager.getSite(catalogSiteName);
371        i18nparams.put("catalogSiteTitle", new I18nizableText(catalogSite.getTitle()));
372        i18nparams.put("catalogSiteUrl", new I18nizableText(catalogSite.getUrl()));
373        
374        Page signupPage = _getSignupPage(_projectManager.getCatalogSiteName(), lang);
375        if (signupPage != null)
376        {
377            String signupUri = ResolveURIComponent.resolve("page", signupPage.getId(), false, true) + "?email=${email}&token=${token}";
378            i18nparams.put("signupUri", new I18nizableText(signupUri));
379        }
380        
381        String subject = getSubjectForInvitationEmail(i18nparams, lang);
382        if (subject != null)
383        {
384            emailConfig.put("subject", subject);
385        }
386        String bodyTxt = getTextBodyForInvitationEmail(i18nparams, lang);
387        if (bodyTxt != null)
388        {
389            emailConfig.put("bodyText", bodyTxt);
390        }
391        String bodyHtml = getHtmlBodyForInvitationEmail(i18nparams, lang);
392        if (bodyHtml != null)
393        {
394            emailConfig.put("bodyHtml", bodyHtml);
395        }
396        
397        return emailConfig;
398    }
399
400    private Page _getSignupPage(String catalogSiteName, String lang)
401    {
402        Page signupPage = _signupManager.getSignupPage(catalogSiteName, lang);
403
404        if (signupPage == null)
405        {
406            signupPage = _signupManager.getSignupPage(catalogSiteName, "en");
407        }
408        
409        if (signupPage == null)
410        {
411            Map<String, Language> availableLanguages = _languagesManager.getAvailableLanguages();
412            for (Language availableLanguage : availableLanguages.values())
413            {
414                if (signupPage == null)
415                {
416                    signupPage = _signupManager.getSignupPage(catalogSiteName, availableLanguage.getCode());
417                }
418            }
419        }
420        return signupPage;
421    }
422    
423    private void _sendInvitationMail (String catalogSiteName, String email, String populationId, String userDirectoryId, String mailSubject, String mailBodyText, String mailBodyHtml) throws UserManagementException
424    {
425        String encodedEmail;
426        try
427        {
428            encodedEmail = URLEncoder.encode(email, "UTF-8");
429        }
430        catch (UnsupportedEncodingException e)
431        {
432            // Should never happen.
433            throw new UserManagementException("Encoding error while sending a sign-up confirmation e-mail.", StatusError.MAIL_ERROR, e);
434        }
435
436        String token = _signupManager.getToken(catalogSiteName, email, populationId, userDirectoryId);
437        
438        String bodyTxt = StringUtils.replace(mailBodyText, __MAIL_PROJECT_TOKEN_PATTERN, token);
439        bodyTxt = StringUtils.replace(bodyTxt, __MAIL_PROJECT_EMAIL_PATTERN, encodedEmail);
440        
441        String bodyHtml = StringUtils.replace(mailBodyHtml, __MAIL_PROJECT_TOKEN_PATTERN, token);
442        bodyHtml = StringUtils.replace(bodyHtml, __MAIL_PROJECT_EMAIL_PATTERN, encodedEmail);
443        
444        getLogger().debug("Sending signup invitation e-mail to {}", email);
445        
446        Site site = _siteManager.getSite(catalogSiteName);
447        String from = site.getValue("site-mail-from");
448
449        try
450        {
451            List<String> errorReport = new ArrayList<>();
452            
453            // Send the e-mail.
454            SendMailHelper.newMail()
455                          .withSubject(mailSubject)
456                          .withTextBody(bodyTxt)
457                          .withHTMLBody(bodyHtml)
458                          .withSender(from)
459                          .withRecipient(email)
460                          .withErrorReport(errorReport)
461                          .sendMail();
462            
463            if (errorReport.contains(email))
464            {
465                throw new UserManagementException("Error sending the sign-up confirmation mail.", StatusError.MAIL_ERROR);
466            }
467        }
468        catch (MessagingException | IOException e)
469        {
470            throw new UserManagementException("Error sending the sign-up confirmation mail.", StatusError.MAIL_ERROR, e);
471        }
472    }
473    
474    private boolean _addOrUpdateInvitation(Project project, String catalogSiteName, MembersWorkspaceModule module, String email, Map<String, String> allowedProfileByModules, String populationId, String userDirectoryId, String mailSubject, String mailBodyText, String mailBodyHtml) throws UserManagementException
475    {
476        try
477        {
478            // First remove invitation if exists
479            module.removeInvitation(project, email);
480            
481            // Add invitations with temporary rights
482            module.addInvitation(project, new Date(), email, _currentUserProvider.getUser(), allowedProfileByModules);
483            
484            project.saveChanges();
485
486            _sendInvitationMail(catalogSiteName, email, populationId, userDirectoryId, mailSubject, mailBodyText, mailBodyHtml);
487            
488            return true;
489        }
490        catch (RepositoryException e)
491        {
492            getLogger().error("Fail to store invitation for email " + email, e);
493            return false;
494        }
495    }
496    
497    private UserDirectory _getUserDirectoryForSignup(Map<String, Object> result, String catalogSiteName, String lang)
498    {
499        if (catalogSiteName == null)
500        {
501            getLogger().error("The catalog's site name is not configured. User invitations can not be activated.");
502            result.put("success", false);
503            result.put("error", "invalid-configuration");
504            return null;
505        }
506        
507        if (!_signupManager.isSignupAllowed(catalogSiteName))
508        {
509            getLogger().warn("Signup is disabled for the catalog's site.");
510            result.put("success", false);
511            return null;
512        }
513        
514        Page signupPage = _getSignupPage(catalogSiteName, lang);
515        if (signupPage == null)
516        {
517            getLogger().error("The catalog's site does not contain the signup service for language " + lang + ". User invitations can not be activated.");
518            result.put("success", false);
519            result.put("error", "invalid-configuration");
520            return null;
521        }
522        
523        UserDirectory userDirectory = _getUserDirectoryForSignup(signupPage);
524        if (userDirectory == null)
525        {
526            getLogger().error("There is no user directory configured for users signup. Please check the sign up service of catalog's site.");
527            result.put("success", false);
528            result.put("error", "invalid-configuration");
529            return null;
530        }
531        
532        return userDirectory;
533            
534    }
535    
536    private UserDirectory _getUserDirectoryForSignup(Page signupPage)
537    {
538        for (Zone zone : signupPage.getZones())
539        {
540            try (AmetysObjectIterable<? extends ZoneItem> zoneItems = zone.getZoneItems())
541            {
542                for (ZoneItem zoneItem : zoneItems)
543                {
544                    UserDirectory userDirectory = _signupManager.getUserDirectory(zoneItem);
545                    if (userDirectory != null)
546                    {
547                        return userDirectory;
548                    }
549                }
550            }
551        }
552        
553        return null;
554        
555    }
556    
557    /**
558     * Add user as member of all project where it has been invited
559     * @param user the new user
560     */
561    public void createMemberFromInvitations(User user)
562    {
563        Request request = ContextHelper.getRequest(_context);
564        String currentWorkspace = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
565        
566        try
567        {
568            // Force default workspace
569            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, "default");
570            
571            MembersWorkspaceModule memberModule = (MembersWorkspaceModule) _moduleEP.getExtension(MembersWorkspaceModule.MEMBERS_MODULE_ID);
572            
573            List<Invitation> invitations = memberModule.getInvitations(user.getEmail());
574            
575            for (Invitation invitation : invitations)
576            {
577                Map<String, String> allowedProfileByModules = invitation.getAllowedProfileByModules();
578                String projectName = invitation.getProjectName();
579                
580                Project project = _projectManager.getProject(projectName);
581                
582                _projectMemberManager.addOrUpdateProjectMember(project, user.getIdentity(), allowedProfileByModules, invitation.getAuthor());
583                
584                // Notify author that invitation was accepted
585                UserIdentity author = invitation.getAuthor();
586                sendInvitationAcceptedMail(project, _userManager.getUser(author), user);
587                
588                try
589                {
590                    // Remove invitation
591                    memberModule.removeInvitation(project, invitation.getEmail());
592                }
593                catch (RepositoryException e)
594                {
595                    getLogger().error("Failed to remove invitation " + invitation, e);
596                }
597            }
598        }
599        finally
600        {
601            // Restore current workspace
602            RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWorkspace);
603        }
604    
605    }
606    
607    /**
608     * Send email to the user who initiated the invitation
609     * @param project The project
610     * @param invitAuthor The author of invitation
611     * @param newMember The new member
612     */
613    protected void sendInvitationAcceptedMail(Project project, User invitAuthor, User newMember)
614    {
615        if (invitAuthor != null)
616        {
617            Site site = project.getSite();
618            String from = site.getValue("site-mail-from");
619            String email = invitAuthor.getEmail();
620            String language = StringUtils.defaultIfBlank(invitAuthor.getLanguage(), _workspacesHelper.getLang(project, _userLanguagesManager.getDefaultLanguage()));
621            
622            if (StringUtils.isNotEmpty(email))
623            {
624                // Prepare mail.
625                Map<String, I18nizableTextParameter> i18nParams = new HashMap<>();
626                i18nParams.put("projectTitle", new I18nizableText(project.getTitle()));
627                i18nParams.put("projectUrl", new I18nizableText(_projectManager.getProjectUrl(project, StringUtils.EMPTY)));
628                i18nParams.put("newMember", new I18nizableText(newMember.getFullName()));
629                i18nParams.put("newMemberMail", new I18nizableText(newMember.getEmail()));
630                
631                Set<Page> memberPages = _projectManager.getModulePages(project, MembersWorkspaceModule.MEMBERS_MODULE_ID);
632                if (!memberPages.isEmpty())
633                {
634                    Page page = memberPages.iterator().next();
635                    i18nParams.put("membersPageUri", new I18nizableText(ResolveURIComponent.resolve("page", page.getId(), false, true)));
636                }
637
638                String subject = getSubjectForInvitationAcceptedEmail(i18nParams, language);
639                String textBody = getTextBodyForInvitationAcceptedEmail(i18nParams, language);
640                String htmlBody = getHtmlBodyForInvitationAcceptedEmail(project, i18nParams, language);
641
642                try
643                {
644                    // Send the e-mail.
645                    SendMailHelper.newMail()
646                                  .withSubject(subject)
647                                  .withHTMLBody(htmlBody)
648                                  .withTextBody(textBody)
649                                  .withSender(from)
650                                  .withRecipient(email)
651                                  .sendMail();
652                }
653                catch (MessagingException | IOException e)
654                {
655                    getLogger().error("Error sending the invitation accepted email.", e);
656                }
657            }
658        }
659    }
660    
661    /**
662     * The email subject for invitation by email
663     * @param defaultI18nParams The default i18n parameters
664     * @param language the language
665     * @return the email subject for invitation by email
666     */
667    public String getSubjectForInvitationEmail (Map<String, I18nizableTextParameter> defaultI18nParams , String language)
668    {
669        if (StringUtils.isNotBlank(_subjectKeyForInvitation))
670        {
671            return _i18nUtils.translate(_getI18nizableText(_subjectKeyForInvitation, defaultI18nParams), language);
672        }
673        
674        return null;
675    }
676
677    /**
678     * The email text body for invitation by email
679     * @param defaultI18nParams The default i18n parameters with :
680     * siteName the site name
681     * email the mail
682     * token the token
683     * confirmUri the confirmation uri
684     * siteTitle the site title
685     * siteUrl the site url
686     * @param language the language
687     * @return the email text for invitation by email
688     */
689    public String getTextBodyForInvitationEmail(Map<String, I18nizableTextParameter> defaultI18nParams, String language)
690    {
691        if (StringUtils.isNotBlank(_textBodyKeyForInvitation))
692        {
693            return _i18nUtils.translate(_getI18nizableText(_textBodyKeyForInvitation, defaultI18nParams), language);
694        }
695        
696        return null;
697    }
698
699    /**
700     * The email html body for invitation by email
701     * @param defaultI18nParams The default i18n parameters with :
702     * siteName the site name
703     * email the mail
704     * token the token
705     * confirmUri the confirmation uri
706     * siteTitle the site title
707     * siteUrl the site url
708     * @param language the language
709     * @return the email html for invitation by email
710     */
711    public String getHtmlBodyForInvitationEmail(Map<String, I18nizableTextParameter> defaultI18nParams, String language)
712    {
713        if (StringUtils.isNotBlank(_htmlBodyKeyForInvitation))
714        {
715            try
716            {
717                MailBodyBuilder bodyBuilder = StandardMailBodyHelper.newHTMLBody()
718                        .withLanguage(language)
719                        .withMessage(_getI18nizableText(_htmlBodyKeyForInvitation, defaultI18nParams));
720                    
721                Page signupPage = _getSignupPage(_projectManager.getCatalogSiteName(), language);
722                if (signupPage != null)
723                {
724                    String signupUri = ResolveURIComponent.resolve("page", signupPage.getId(), false, true) + "?email=${email}&token=${token}";
725                    bodyBuilder.withLink(signupUri, new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_USER_INVITATION_MAIL_BODY_HTML_LINK_TITLE"));
726                }
727                
728                return bodyBuilder.build();
729            }
730            catch (IOException e)
731            {
732                getLogger().warn("Faild to build HTML body for invitation email", e);
733            }
734        }
735        
736        return null;
737    }
738    
739    /**
740     * The email subject for invitation by email
741     * @param defaultI18nParams The default i18n parameters
742     * @param language the language
743     * @return the email subject for invitation by email
744     */
745    public String getSubjectForInvitationAcceptedEmail (Map<String, I18nizableTextParameter> defaultI18nParams , String language)
746    {
747        if (StringUtils.isNotBlank(_subjectKeyForInvitationAccepted))
748        {
749            return _i18nUtils.translate(_getI18nizableText(_subjectKeyForInvitationAccepted, defaultI18nParams), language);
750        }
751        
752        return null;
753    }
754    
755    /**
756     * The email text body for invitation by email
757     * @param defaultI18nParams The default i18n parameters with :
758     * @param language the language
759     * @return the email text for invitation by email
760     */
761    public String getTextBodyForInvitationAcceptedEmail(Map<String, I18nizableTextParameter> defaultI18nParams, String language)
762    {
763        if (StringUtils.isNotBlank(_textBodyKeyForInvitationAccepted))
764        {
765            return _i18nUtils.translate(_getI18nizableText(_textBodyKeyForInvitationAccepted, defaultI18nParams), language);
766        }
767        
768        return null;
769    }
770    
771    /**
772     * The email html body for invitation by email
773     * @param project The project
774     * @param defaultI18nParams The default i18n parameters with :
775     * @param language the language
776     * @return the email html for invitation by email
777     */
778    public String getHtmlBodyForInvitationAcceptedEmail(Project project, Map<String, I18nizableTextParameter> defaultI18nParams, String language)
779    {
780        if (StringUtils.isNotBlank(_htmlBodyKeyForInvitationAccepted))
781        {
782            try
783            {
784                MailBodyBuilder bodyBuilder = StandardMailBodyHelper.newHTMLBody()
785                        .withLanguage(language)
786                        .withMessage(_getI18nizableText(_htmlBodyKeyForInvitationAccepted, defaultI18nParams));
787                    
788                Set<Page> memberPages = _projectManager.getModulePages(project, MembersWorkspaceModule.MEMBERS_MODULE_ID);
789                if (!memberPages.isEmpty())
790                {
791                    Page page = memberPages.iterator().next();
792                    String memberPageUri = ResolveURIComponent.resolve("page", page.getId(), false, true);
793                    bodyBuilder.withLink(memberPageUri, new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_USER_INVITATION_ACCEPTED_MAIL_BODY_HTML_LINK_TITLE"));
794                }
795                
796                return bodyBuilder.build();
797            }
798            catch (IOException e)
799            {
800                getLogger().warn("Faild to build HTML body for invitation accepted email", e);
801            }
802        }
803        
804        return null;
805    }
806    
807    /**
808     * Get the {@link I18nizableText} from the configured key and i18n parameters
809     * @param fullI18nKey the configured i18n key
810     * @param i18nParams the i18n parameters
811     * @return the i18nizable text
812     */
813    protected I18nizableText _getI18nizableText(String fullI18nKey, Map<String, I18nizableTextParameter> i18nParams)
814    {
815        String catalogue = StringUtils.contains(fullI18nKey, ":") ? StringUtils.substringBefore(fullI18nKey, ":") : "plugin." + _pluginName;
816        String i18nKey = StringUtils.contains(fullI18nKey, ":") ? StringUtils.substringAfter(fullI18nKey, ":") : fullI18nKey;
817        
818        return new I18nizableText(catalogue, i18nKey, i18nParams);
819    }
820    
821}