No there’s no SMTP provider built into ServiceStack or any high-level functionality that depends on it.
You could build a verify Email feature by registering a OnRegistered Session or Auth Event to immediately lock the user, add a record in a custom VerifyEmail
table with a GUID or random token string and send an email with the token to call a Service that unlocks the User Account.
We do something similar for forgot password where we add a record in a ForgotPassword
table with a random token and send a user email containing the token which then calls a Service to update the Users password. We’ve developed this feature in servicestack.net but the source code for the website is private, here are the Service implementations for the feature which may be able to help implement it:
We have a HTML form for users to provide the Email they want to request a password request for:
<form method="POST" action="@(new RequestPasswordReset().ToUrl())">
<input type="text" name="Email" placeholder="email address"/>
<button type="submit">Next</button>
</form>
This calls this Service to cancel all previous ForgotPassword
requests and add a new record with a new token from a GUID:
public object Any(RequestPasswordReset request)
{
var user = UserAuthRepository.GetUserAuthByUserName(request.Email);
if (user == null)
throw HttpError.NotFound("No Account with that Email Exists");
Db.UpdateOnly(new ForgotPassword { CancelledDate = DateTime.UtcNow },
where: x => x.UserAuthId == user.Id,
onlyFields: x => new { x.CancelledDate });
var dto = new ForgotPassword
{
UserAuthId = user.Id,
DisplayName = user.DisplayName,
Email = request.Email,
ResetToken = Guid.NewGuid().ToString("N"),
CreatedDate = DateTime.UtcNow,
};
Db.Save(dto);
using (var service = base.ResolveService<EmailService>())
{
service.SendForgotPasswordEmail(dto);
}
return request;
}
We have a custom Email Service that uses the Request DTO and custom Email templates to generate a plain-text and HTML version of emails that gets sent with AWS SES.
Part of the code used to generate the ForgotPasswordEmail Template looks like:
private IEmailTemplate GetForgotPasswordEmail(ForgotPassword request)
{
var dto = new EmailForgotPassword
{
To = request.Email,
ToName = request.DisplayName,
PaswordResetUrl = new ResetUserPassword { ResetToken = request.ResetToken }.ToAbsoluteUri(),
};
return new ForgotPasswordEmail(dto);
}
Where new ResetUserPassword { ResetToken = request.ResetToken }.ToAbsoluteUri()
is the URL embedded in the Email.
Which calls this Service when the User clicks on it:
public object Get(ResetUserPassword request)
{
var resetRequest = Db.Single<ForgotPassword>(q =>
q.ResetToken == request.ResetToken
&& q.CancelledDate == null && q.CompletedDate == null
&& q.CreatedDate >= DateTime.UtcNow.AddMinutes(-5));
if (resetRequest == null)
throw new Exception("Password request has expired or does not exist");
return new ResetUserPassword
{
Email = resetRequest.Email,
ResetToken = resetRequest.ResetToken,
};
}
Which returns a View Model that renders a Razor view which embeds the RestToken in a FORM POST Request that gets sent along with the new User Password:
<div>
<h2>New Password for @Model.Email</h2>
<form action="@(new ResetUserPassword { ResetToken = Model.ResetToken }.ToPostUrl())" method="POST" class="validation">
<div class="error-summary alert alert-danger"></div>
<div>
<div class="form-group">
<label for="NewPassword">New Password</label>
<input type="password" id="NewPassword" name="NewPassword" class="form-control input-lg" />
<span class="help-block error"></span>
</div>
<div class="form-group">
<label for="Confirm">Confirm</label>
<input type="password" id="Confirm" name="Confirm" class="form-control input-lg" />
<span class="help-block error"></span>
</div>
<button type="submit">Change Password</button>
</div>
</form>
</div>
<script type="text/javascript">
$("FORM").bindForm();
</script>
Note we use Form data binding in ss-utils to automatically bind Validation Errors to the above form.
Which then gets POST’ed to this Service to update the Users password:
public object Post(ResetUserPassword request)
{
var resetRequest = Db.Single<ForgotPassword>(q =>
q.ResetToken == request.ResetToken
&& q.CancelledDate == null && q.CompletedDate == null
&& q.CreatedDate >= DateTime.UtcNow.AddMinutes(-5));
var userAuth = AuthRepository.GetUserAuthByUserName(resetRequest.Email);
if (userAuth == null)
throw HttpError.NotFound("User not found");
userAuth.InvalidLoginAttempts = 0;
userAuth.LockedDate = null;
AuthRepository.UpdateUserAuth(userAuth, userAuth, request.NewPassword);
resetRequest.CompletedDate = DateTime.UtcNow;
Db.Update(resetRequest);
using (var auth = base.ResolveService<AuthenticateService>())
{
var dto = new Authenticate
{
provider = CredentialsAuthProvider.Name,
UserName = resetRequest.Email,
Password = request.NewPassword,
RememberMe = true,
};
var response = auth.Post(dto);
if (response is IHttpError)
return response;
return HttpResult.SoftRedirect(new Subscriptions().ToAbsoluteUri());
}
}
Which fetches the ForgotPassword
entry if it was created within the last 5 minutes and uses the AuthRepository
to update the Users password and automatically Authenticates them using the new password.
Hope this helps outline how you would be able to build this feature, but I agree having an integrated solution for this higher-level functionality is a good idea, but it would have to be maintained outside of the core libs, either in combination of a separate package, Live Demo or Custom VS.NET Template. You can add a feature request for this so we can measure interest it.