Extracting Orchard.Users email templates

The texts of the emails sent are defined in custom shapes.
It's using IMessageService to send an email directly.
This commit is contained in:
Sebastien Ros
2014-01-10 17:37:00 -08:00
parent 442cf93488
commit 546aa2fc9c
7 changed files with 139 additions and 133 deletions

View File

@@ -1,83 +0,0 @@
using System;
using System.Linq;
using Orchard.Localization;
using Orchard.Messaging.Events;
using Orchard.Messaging.Models;
using Orchard.ContentManagement;
using Orchard.Settings;
using Orchard.Users.Models;
namespace Orchard.Users.Handlers {
public class UserMessagesAlteration : IMessageEventHandler {
private readonly IContentManager _contentManager;
private readonly ISiteService _siteService;
public UserMessagesAlteration(IContentManager contentManager, ISiteService siteService) {
_contentManager = contentManager;
_siteService = siteService;
T = NullLocalizer.Instance;
}
public Localizer T { get; set; }
public void Sending(MessageContext context) {
if (context.MessagePrepared)
return;
switch (context.Type) {
case MessageTypes.Moderation:
context.MailMessage.Subject = T("New account").Text;
context.MailMessage.Body =
T("The user <b>{0}</b> with email <b>{1}</b> has requested a new account. This user won't be able to log while his account has not been approved.",
context.Properties["UserName"], context.Properties["Email"]).Text;
FormatEmailBody(context);
context.MessagePrepared = true;
break;
case MessageTypes.Validation:
var registeredWebsite = _siteService.GetSiteSettings().As<RegistrationSettingsPart>().ValidateEmailRegisteredWebsite;
var contactEmail = _siteService.GetSiteSettings().As<RegistrationSettingsPart>().ValidateEmailContactEMail;
context.MailMessage.Subject = T("Verification E-Mail").Text;
context.MailMessage.Body =
T("Thank you for registering with {0}.<br/><br/><br/><b>Final Step</b><br/>To verify that you own this e-mail address, please click the following link:<br/><a href=\"{1}\">{1}</a><br/><br/><b>Troubleshooting:</b><br/>If clicking on the link above does not work, try the following:<br/><br/>Select and copy the entire link.<br/>Open a browser window and paste the link in the address bar.<br/>Click <b>Go</b> or, on your keyboard, press <b>Enter</b> or <b>Return</b>.",
registeredWebsite, context.Properties["ChallengeUrl"]).Text;
if (!String.IsNullOrWhiteSpace(contactEmail)) {
context.MailMessage.Body +=
T("<br/><br/>If you continue to have access problems or want to report other issues, please <a href=\"mailto:{0}\">Contact Us</a>.",
contactEmail).Text;
}
FormatEmailBody(context);
context.MessagePrepared = true;
break;
case MessageTypes.LostPassword:
var recipient = GetRecipient(context);
context.MailMessage.Subject = T("Lost password").Text;
context.MailMessage.Body =
T("Dear {0}, please <a href=\"{1}\">click here</a> to change your password.", recipient.UserName,
context.Properties["LostPasswordUrl"]).Text;
FormatEmailBody(context);
context.MessagePrepared = true;
break;
}
}
private UserPart GetRecipient(MessageContext context) {
// we expect a single account to be created
var contentItem = _contentManager.Get(context.Recipients.Single().Id);
if (contentItem == null) {
return null;
}
return contentItem.As<UserPart>();
}
private static void FormatEmailBody(MessageContext context) {
context.MailMessage.Body = "<p style=\"font-family:Arial, Helvetica; font-size:10pt;\">" + context.MailMessage.Body;
context.MailMessage.Body += "</p>";
}
public void Sent(MessageContext context) {
}
}
}

View File

@@ -49,6 +49,10 @@
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.CSharp" />
<Reference Include="Newtonsoft.Json, Version=4.5.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\..\..\..\lib\newtonsoft.json\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.ComponentModel.DataAnnotations">
<RequiredTargetFramework>3.5</RequiredTargetFramework>
@@ -73,7 +77,6 @@
<Compile Include="Drivers\UserPartDriver.cs" />
<Compile Include="Migrations.cs" />
<Compile Include="Events\UserContext.cs" />
<Compile Include="Handlers\UserMessagesAlteration.cs" />
<Compile Include="Handlers\RegistrationSettingsPartHandler.cs" />
<Compile Include="Events\IUserEventHandler.cs" />
<Compile Include="Models\MessageTypes.cs" />
@@ -120,6 +123,10 @@
<Project>{9916839C-39FC-4CEB-A5AF-89CA7E87119F}</Project>
<Name>Orchard.Core</Name>
</ProjectReference>
<ProjectReference Include="..\Orchard.Messaging\Orchard.Messaging.csproj">
<Project>{085948FF-0E9B-4A9A-B564-F8B8B4BDDDBC}</Project>
<Name>Orchard.Messaging</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Content Include="Views\Items\Content-User.Edit.cshtml" />
@@ -151,6 +158,15 @@
<SubType>Designer</SubType>
</Content>
</ItemGroup>
<ItemGroup>
<Content Include="Views\Template.User.Moderated.cshtml" />
</ItemGroup>
<ItemGroup>
<Content Include="Views\Template.User.Validated.cshtml" />
</ItemGroup>
<ItemGroup>
<Content Include="Views\Template.User.LostPassword.cshtml" />
</ItemGroup>
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>

View File

@@ -4,6 +4,8 @@ using System.Security.Cryptography;
using System.Text;
using System.Web.Security;
using JetBrains.Annotations;
using Newtonsoft.Json;
using Orchard.DisplayManagement;
using Orchard.Localization;
using Orchard.Logging;
using Orchard.ContentManagement;
@@ -18,15 +20,26 @@ namespace Orchard.Users.Services {
[UsedImplicitly]
public class MembershipService : IMembershipService {
private readonly IOrchardServices _orchardServices;
private readonly IMessageManager _messageManager;
private readonly IMessageService _messageService;
private readonly IEnumerable<IUserEventHandler> _userEventHandlers;
private readonly IEncryptionService _encryptionService;
private readonly IShapeFactory _shapeFactory;
private readonly IShapeDisplay _shapeDisplay;
public MembershipService(IOrchardServices orchardServices, IMessageManager messageManager, IEnumerable<IUserEventHandler> userEventHandlers, IClock clock, IEncryptionService encryptionService) {
public MembershipService(
IOrchardServices orchardServices,
IMessageService messageService,
IEnumerable<IUserEventHandler> userEventHandlers,
IClock clock,
IEncryptionService encryptionService,
IShapeFactory shapeFactory,
IShapeDisplay shapeDisplay) {
_orchardServices = orchardServices;
_messageManager = messageManager;
_messageService = messageService;
_userEventHandlers = userEventHandlers;
_encryptionService = encryptionService;
_shapeFactory = shapeFactory;
_shapeDisplay = shapeDisplay;
Logger = NullLogger.Instance;
T = NullLocalizer.Instance;
}
@@ -51,7 +64,7 @@ namespace Orchard.Users.Services {
user.Record.Email = createUserParams.Email;
user.Record.NormalizedUserName = createUserParams.Username.ToLowerInvariant();
user.Record.HashAlgorithm = "SHA1";
SetPassword(user.Record, createUserParams.Password);
SetPassword(user, createUserParams.Password);
if ( registrationSettings != null ) {
user.Record.RegistrationStatus = registrationSettings.UsersAreModerated ? UserStatus.Pending : UserStatus.Approved;
@@ -81,7 +94,10 @@ namespace Orchard.Users.Services {
}
}
if ( registrationSettings != null && registrationSettings.UsersAreModerated && registrationSettings.NotifyModeration && !createUserParams.IsApproved ) {
if ( registrationSettings != null
&& registrationSettings.UsersAreModerated
&& registrationSettings.NotifyModeration
&& !createUserParams.IsApproved ) {
var usernames = String.IsNullOrWhiteSpace(registrationSettings.NotificationsRecipients)
? new string[0]
: registrationSettings.NotificationsRecipients.Split(new[] {',', ' '}, StringSplitOptions.RemoveEmptyEntries);
@@ -91,8 +107,15 @@ namespace Orchard.Users.Services {
continue;
}
var recipient = GetUser(userName);
if (recipient != null)
_messageManager.Send(recipient.ContentItem.Record, MessageTypes.Moderation, "email" , new Dictionary<string, string> { { "UserName", createUserParams.Username}, { "Email" , createUserParams.Email } });
if (recipient != null) {
var template = _shapeFactory.Create("Template_User_Moderated", Arguments.From(createUserParams));
var payload = new {
Subject = T("New account").Text,
Body = _shapeDisplay.Display(template),
Recipients = new [] { recipient.Email }
};
_messageService.Send("Email", JsonConvert.SerializeObject(payload));
}
}
}
@@ -113,7 +136,7 @@ namespace Orchard.Users.Services {
if (user == null)
user = _orchardServices.ContentManager.Query<UserPart, UserPartRecord>().Where(u => u.Email == lowerName).List().FirstOrDefault();
if ( user == null || ValidatePassword(user.As<UserPart>().Record, password) == false )
if ( user == null || ValidatePassword(user.As<UserPart>(), password) == false )
return null;
if ( user.EmailStatus != UserStatus.Approved )
@@ -129,54 +152,50 @@ namespace Orchard.Users.Services {
if (!user.Is<UserPart>())
throw new InvalidCastException();
var userRecord = user.As<UserPart>().Record;
SetPassword(userRecord, password);
}
var userPart = user.As<UserPart>();
void SetPassword(UserPartRecord partRecord, string password) {
switch (GetSettings().PasswordFormat) {
case MembershipPasswordFormat.Clear:
SetPasswordClear(partRecord, password);
SetPasswordClear(userPart, password);
break;
case MembershipPasswordFormat.Hashed:
SetPasswordHashed(partRecord, password);
SetPasswordHashed(userPart, password);
break;
case MembershipPasswordFormat.Encrypted:
SetPasswordEncrypted(partRecord, password);
SetPasswordEncrypted(userPart, password);
break;
default:
throw new ApplicationException(T("Unexpected password format value").ToString());
}
}
private bool ValidatePassword(UserPartRecord partRecord, string password) {
private bool ValidatePassword(UserPart userPart, string password) {
// Note - the password format stored with the record is used
// otherwise changing the password format on the site would invalidate
// all logins
switch (partRecord.PasswordFormat) {
switch (userPart.PasswordFormat) {
case MembershipPasswordFormat.Clear:
return ValidatePasswordClear(partRecord, password);
return ValidatePasswordClear(userPart, password);
case MembershipPasswordFormat.Hashed:
return ValidatePasswordHashed(partRecord, password);
return ValidatePasswordHashed(userPart, password);
case MembershipPasswordFormat.Encrypted:
return ValidatePasswordEncrypted(partRecord, password);
return ValidatePasswordEncrypted(userPart, password);
default:
throw new ApplicationException("Unexpected password format value");
}
}
private static void SetPasswordClear(UserPartRecord partRecord, string password) {
partRecord.PasswordFormat = MembershipPasswordFormat.Clear;
partRecord.Password = password;
partRecord.PasswordSalt = null;
private static void SetPasswordClear(UserPart userPart, string password) {
userPart.PasswordFormat = MembershipPasswordFormat.Clear;
userPart.Password = password;
userPart.PasswordSalt = null;
}
private static bool ValidatePasswordClear(UserPartRecord partRecord, string password) {
return partRecord.Password == password;
private static bool ValidatePasswordClear(UserPart userPart, string password) {
return userPart.Password == password;
}
private static void SetPasswordHashed(UserPartRecord partRecord, string password) {
private static void SetPasswordHashed(UserPart userPart, string password) {
var saltBytes = new byte[0x10];
using (var random = new RNGCryptoServiceProvider()) {
@@ -188,39 +207,39 @@ namespace Orchard.Users.Services {
var combinedBytes = saltBytes.Concat(passwordBytes).ToArray();
byte[] hashBytes;
using (var hashAlgorithm = HashAlgorithm.Create(partRecord.HashAlgorithm)) {
using (var hashAlgorithm = HashAlgorithm.Create(userPart.HashAlgorithm)) {
hashBytes = hashAlgorithm.ComputeHash(combinedBytes);
}
partRecord.PasswordFormat = MembershipPasswordFormat.Hashed;
partRecord.Password = Convert.ToBase64String(hashBytes);
partRecord.PasswordSalt = Convert.ToBase64String(saltBytes);
userPart.PasswordFormat = MembershipPasswordFormat.Hashed;
userPart.Password = Convert.ToBase64String(hashBytes);
userPart.PasswordSalt = Convert.ToBase64String(saltBytes);
}
private static bool ValidatePasswordHashed(UserPartRecord partRecord, string password) {
private static bool ValidatePasswordHashed(UserPart userPart, string password) {
var saltBytes = Convert.FromBase64String(partRecord.PasswordSalt);
var saltBytes = Convert.FromBase64String(userPart.PasswordSalt);
var passwordBytes = Encoding.Unicode.GetBytes(password);
var combinedBytes = saltBytes.Concat(passwordBytes).ToArray();
byte[] hashBytes;
using (var hashAlgorithm = HashAlgorithm.Create(partRecord.HashAlgorithm)) {
using (var hashAlgorithm = HashAlgorithm.Create(userPart.HashAlgorithm)) {
hashBytes = hashAlgorithm.ComputeHash(combinedBytes);
}
return partRecord.Password == Convert.ToBase64String(hashBytes);
return userPart.Password == Convert.ToBase64String(hashBytes);
}
private void SetPasswordEncrypted(UserPartRecord partRecord, string password) {
partRecord.Password = Convert.ToBase64String(_encryptionService.Encode(Encoding.UTF8.GetBytes(password)));
partRecord.PasswordSalt = null;
partRecord.PasswordFormat = MembershipPasswordFormat.Encrypted;
private void SetPasswordEncrypted(UserPart userPart, string password) {
userPart.Password = Convert.ToBase64String(_encryptionService.Encode(Encoding.UTF8.GetBytes(password)));
userPart.PasswordSalt = null;
userPart.PasswordFormat = MembershipPasswordFormat.Encrypted;
}
private bool ValidatePasswordEncrypted(UserPartRecord partRecord, string password) {
return String.Equals(password, Encoding.UTF8.GetString(_encryptionService.Decode(Convert.FromBase64String(partRecord.Password))), StringComparison.Ordinal);
private bool ValidatePasswordEncrypted(UserPart userPart, string password) {
return String.Equals(password, Encoding.UTF8.GetString(_encryptionService.Decode(Convert.FromBase64String(userPart.Password))), StringComparison.Ordinal);
}
}
}

View File

@@ -1,9 +1,12 @@
using System;
using System.Linq;
using System.Collections.Generic;
using JetBrains.Annotations;
using Newtonsoft.Json;
using Orchard.DisplayManagement;
using Orchard.Localization;
using Orchard.Logging;
using Orchard.ContentManagement;
using Orchard.Settings;
using Orchard.Users.Models;
using Orchard.Security;
using System.Xml.Linq;
@@ -22,19 +25,36 @@ namespace Orchard.Users.Services {
private readonly IContentManager _contentManager;
private readonly IMembershipService _membershipService;
private readonly IClock _clock;
private readonly IMessageManager _messageManager;
private readonly IMessageService _messageService;
private readonly IEncryptionService _encryptionService;
private readonly IShapeFactory _shapeFactory;
private readonly IShapeDisplay _shapeDisplay;
private readonly ISiteService _siteService;
public UserService(IContentManager contentManager, IMembershipService membershipService, IClock clock, IMessageManager messageManager, ShellSettings shellSettings, IEncryptionService encryptionService) {
public UserService(
IContentManager contentManager,
IMembershipService membershipService,
IClock clock,
IMessageService messageService,
ShellSettings shellSettings,
IEncryptionService encryptionService,
IShapeFactory shapeFactory,
IShapeDisplay shapeDisplay,
ISiteService siteService
) {
_contentManager = contentManager;
_membershipService = membershipService;
_clock = clock;
_messageManager = messageManager;
_messageService = messageService;
_encryptionService = encryptionService;
_shapeFactory = shapeFactory;
_shapeDisplay = shapeDisplay;
_siteService = siteService;
Logger = NullLogger.Instance;
}
public ILogger Logger { get; set; }
public Localizer T { get; set; }
public bool VerifyUserUnicity(string userName, string email) {
string normalizedUserName = userName.ToLowerInvariant();
@@ -111,7 +131,24 @@ namespace Orchard.Users.Services {
public void SendChallengeEmail(IUser user, Func<string, string> createUrl) {
string nonce = CreateNonce(user, DelayToValidate);
string url = createUrl(nonce);
_messageManager.Send(user.ContentItem.Record, MessageTypes.Validation, "email", new Dictionary<string, string> { { "ChallengeUrl", url } });
if (user != null) {
var site = _siteService.GetSiteSettings();
var template = _shapeFactory.Create("Template_User_Validated", Arguments.From(new {
RegisteredWebsite = site.As<RegistrationSettingsPart>().ValidateEmailRegisteredWebsite,
ContactEmail = site.As<RegistrationSettingsPart>().ValidateEmailContactEMail,
ChallengeUrl = url
}));
var payload = new {
Subject = T("Verification E-Mail").Text,
Body = _shapeDisplay.Display(template),
Recipients = new[] { user.Email }
};
_messageService.Send("Email", JsonConvert.SerializeObject(payload));
}
}
public bool SendLostPasswordEmail(string usernameOrEmail, Func<string, string> createUrl) {
@@ -122,7 +159,18 @@ namespace Orchard.Users.Services {
string nonce = CreateNonce(user, DelayToResetPassword);
string url = createUrl(nonce);
_messageManager.Send(user.ContentItem.Record, MessageTypes.LostPassword, "email", new Dictionary<string, string> { { "LostPasswordUrl", url } });
var template = _shapeFactory.Create("Template_User_LostPassword", Arguments.From(new {
User = user,
LostPasswordUrl = url
}));
var payload = new {
Subject = T("Lost password").Text,
Body = _shapeDisplay.Display(template),
Recipients = new[] { user.Email }
};
_messageService.Send("Email", JsonConvert.SerializeObject(payload));
return true;
}

View File

@@ -0,0 +1 @@
@T("Dear {0}, please <a href=\"{1}\">click here</a> to change your password.", Model.User.UserName, Model.LostPasswordUrl)

View File

@@ -0,0 +1 @@
@T("The user <b>{0}</b> with email <b>{1}</b> has requested a new account. This user won't be able to log while his account has not been approved.", Model.Username, Model.Email)

View File

@@ -0,0 +1,4 @@
@T("Thank you for registering with {0}.<br /><br /><br /><b>Final Step</b><br />To verify that you own this e-mail address, please click the following link:<br /><a href=\"{1}\">{1}</a><br /><br /><b>Troubleshooting:</b><br />If clicking on the link above does not work, try the following:<br /><br />Select and copy the entire link.<br />Open a browser window and paste the link in the address bar.<br />Click <b>Go</b> or, on your keyboard, press <b>Enter</b> or <b>Return</b>.", Model.RegisteredWebsite, Model.ChallengeUrl)
@if (!String.IsNullOrWhiteSpace(Model.ContactEmail)) {
@T("<br /><br />If you continue to have access problems or want to report other issues, please <a href=\"mailto:{0}\">Contact Us</a>.",Model.ContactEmail)
}