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