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