Introduction
I wanted to put a “Contact Us” email form on my .NET Core App driven web site deployed on Azure. I wanted to protect it somewhat by using reCaptcha to reduce the spam emails, and I also wanted to use SendGrid
rather than my own smtp server.
Preparing for SendGrid
SendGrid
has a free plan that will allow you to send up to 100 emails per day, more than enough for my small web site, and I always have the option of moving up to one of their paid plans or switching back out to my SMTP server. Simply go to https://sendgrid.com and create an account, you can then obtain a SendGrid
API key that you can use to validate the emails you will be sending through them using their API.
Preparing for Google reCaptcha
To setup Google reCaptcha, you will need a Google account. I setup a v2 reCaptcha, adding localhost (so I can test locally on my development PC), my domain name, and my app domain on azurewebsites.net to the list of valid domains. Google will give you two snippets to include on the client side, one a reference to their JavaScipt library, and the other a div
element to include where you want the reCaptcha to appear. They will also give you a server side secret that will be included in your calls to the reCaptcha verification.
Razor Page Code
The key points to note are:
- I have created a class called
ContactFormModel
that will be used to move the email details entered by the user to the server, along with attributes that will help display and validate the email details via the public
bound property Contact
. - I am injecting
IConfiguration
so I can read values from the appsettings.json. - I am injecting
IHttpClientFactory
so I cannot be too concerned about the lifetime or number of http clients I will be creating. - I am injecting
ILogger
so I can log some debug messages to help me debug the code. - I will be calling the reCaptcha end point and I will be getting the secret key from appsettings.json.
- I will also be getting the
SendGrid
API key from appsettings.json.
using System.ComponentModel.DataAnnotations;
using System.Net;
using System.Net.Http;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
using SendGrid;
using SendGrid.Helpers.Mail;
namespace MainWebsite.Pages
{
public class ContactFormModel
{
[Required]
[StringLength(50)]
public string Name { get; set; }
[Required]
[StringLength(255)]
[EmailAddress]
public string Email { get; set; }
[Required]
[StringLength(1000)]
public string Message { get; set; }
}
public class ContactModel : PageModel
{
[BindProperty]
public ContactFormModel Contact { get; set; }
private readonly IConfiguration _configuration;
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<ContactModel> _logger;
public ContactModel(IConfiguration configuration,
IHttpClientFactory httpClientFactory,
ILogger<ContactModel> logger)
{
_configuration = configuration;
_httpClientFactory = httpClientFactory;
_logger = logger;
}
private bool RecaptchaPassed(string recaptchaResponse)
{
_logger.LogDebug("Contact.RecaptchaPassed entered");
var secret =
_configuration.GetSection("RecaptchaKey").Value;
var endPoint =
_configuration.GetSection("RecaptchaEndPoint").Value;
var googleCheckUrl =
$"{endPoint}?secret={secret}&response={recaptchaResponse}";
_logger.LogDebug("Checking reCaptcha");
var httpClient = _httpClientFactory.CreateClient();
var response = httpClient.GetAsync(googleCheckUrl).Result;
if (!response.IsSuccessStatusCode)
{
_logger.LogDebug($"reCaptcha bad response {response.StatusCode}");
return false;
}
dynamic jsonData =
JObject.Parse(response.Content.ReadAsStringAsync().Result);
_logger.LogDebug("reCaptcha returned successfully");
return (jsonData.success == "true");
}
public async Task<IActionResult> OnPostAsync()
{
_logger.LogDebug("Contact.OnPostSync entered");
if (!ModelState.IsValid)
{
_logger.LogDebug("Model state not valid");
return Page();
}
var gRecaptchaResponse = Request.Form["g-recaptcha-response"];
if (string.IsNullOrEmpty(gRecaptchaResponse)
|| !RecaptchaPassed(gRecaptchaResponse))
{
_logger.LogDebug("Recaptcha empty or failed");
ModelState.AddModelError(string.Empty, "You failed the CAPTCHA");
return Page();
}
var from = new EmailAddress(
Contact.Email, Contact.Name);
var to = new EmailAddress(
_configuration.GetSection("ContactUsMailbox").Value);
const string subject = "Website Contact Us Message";
var apiKey = _configuration.GetSection("SENDGRID_API_KEY").Value;
var client = new SendGridClient(apiKey);
var msg = MailHelper.CreateSingleEmail(from, to, subject,
Contact.Message, WebUtility.HtmlEncode(Contact.Message));
_logger.LogDebug("Sending email via SendGrid");
var response = await client.SendEmailAsync(msg);
if (response.StatusCode != HttpStatusCode.Accepted)
{
_logger.LogDebug($"Sendgrid problem {response.StatusCode}");
throw new ExternalException("Error sending message");
}
_logger.LogDebug("Email sent via SendGrid");
return RedirectToPage("Index");
}
}
}
Razor Page Markup
The key points to note are:
- The model set at the top is
ContactModel
(not ContactFormModel
, sorry if you find the names confusing). - I am using the “
@section Scripts
” to include the client Google reCaptcha JavaScript library (putting it in _Layout
would increase the load on every page, not just this one). - The
div
elemnt for the reCaptcha is included just before the submit button (you will need to change data-sitekey
attribute).
@page
@model MainWebsite.Pages.ContactModel
@{
ViewData["Title"] = "Contact Us";
}
@section Scripts
{
<script src='https://www.google.com/recaptcha/api.js'></script>
}
<h1>Contact Us</h1>
<div class="row">
<div class="col-md-6">
<h3>Send message:</h3>
<form method="post">
<div asp-validation-summary="All"></div>
<div class="form-group row">
<label class="col-form-label col-md-3 text-md-right"
asp-for="Contact.Name">Name:</label>
<div class="col-md-9">
<input class="form-control" asp-for="Contact.Name" />
<span asp-validation-for="Contact.Name"></span>
</div>
</div>
<div class="form-group row">
<label class="col-form-label col-md-3 text-md-right"
asp-for="Contact.Email">Email:</label>
<div class="col-md-9">
<input class="form-control" asp-for="Contact.Email" />
<span asp-validation-for="Contact.Email"></span>
</div>
</div>
<div class="form-group row">
<label class="col-form-label col-md-3 text-md-right"
asp-for="Contact.Message">Message:</label>
<div class="col-md-9">
<textarea class="form-control" rows="5"
asp-for="Contact.Message"></textarea>
<span asp-validation-for="Contact.Message"></span>
</div>
</div>
<div class="form-group row">
<div class="offset-md-3 col-md-9">
<div class="g-recaptcha"
data-sitekey="enter recaptcha client key here"></div>
</div>
</div>
<div class="form-group row">
<div class="offset-md-3 col-md-9">
<button type="submit" class="btn btn-primary">
<span class="fa fa-envelope"></span> Send
</button>
</div>
</div>
</form>
</div>
</div>
appsettings.json
Here’s a snip of the appsettings.json, I have not included my keys, you will need to insert your keys in the relevant places.
"SENDGRID_API_KEY": "enter your sendgrid api key here",
"ContactUsMailbox": "enter email address you want mail sent to here",
"RecaptchaKey": "enter your recaptcha key here",
"RecaptchaEndPoint": "https://www.google.com/recaptcha/api/siteverify"
Startup.cs
You will need to include the following line in the ConfigureServices
method of startup.cs to ensure IHttpClientFactory
is available for injection.
services.AddHttpClient();
Program.cs
If you want to use ILogger
for logging debug messages, you will need to configure it here. I leave that entirely up to you.