.NET Core Razor Page Email Form using SendGrid and reCaptcha

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 can not 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.
  • The reCaptcha end point I will be calling and the secret key I will be getting from appsettings.json.
  • The SendGrid API key I will also be getting 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();
            }
            // Mail header
            var from = new EmailAddress(
                Contact.Email, Contact.Name);
            var to = new EmailAddress(
                _configuration.GetSection("ContactUsMailbox").Value);
            const string subject = "Website Contact Us Message";
            // Get SendGrid client ready
            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");
            }
            // On success just go to index page
            // (could refactor later to go to a thank you page instead)
            _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 relevent 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.


Posted

in

, ,

by