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