using System; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading.Tasks; using MailKit.Net.Smtp; using MailKit.Security; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using MimeKit; namespace contact.Pages { [BindProperties(SupportsGet = true)] public class ContactFormModel { public ContactFormModel() { DbId = "n/a v7"; } [Required] public string Name { get; set; } [Required] public string Company { get; set; } [BindProperty(SupportsGet = true)] public string DbId { get; set; } [Required, EmailAddress] public string Email { get; set; } [Required] public string Message { get; set; } } public class ContactModel : PageModel { private const string ConfirmedUrl = "https://ayanova.com/confirmed.htm"; //2022-05-06 Company names containing any of these are dropped silently but still get the //success redirect, so the spammer gets no signal and has to pay someone to enter them by hand private static readonly string[] SpamBlocks = { "google", "jumboleadmagnet", "boostleadgeneration" }; public string Message { get; set; } [BindProperty(SupportsGet = true)] public ContactFormModel Contact { get; set; } //Set when the send fails, so Contact.cshtml can tell the visitor instead of claiming success public bool SendFailed { get; set; } private readonly IConfiguration _configuration; private readonly ILogger _logger; public ContactModel(IConfiguration configuration, ILogger logger) { _configuration = configuration; _logger = logger; } public void OnGet() { Message = "Your contact page."; } public async Task OnPostAsync() { if (!ModelState.IsValid) { return Page(); } //2025-11-01 reCAPTCHA removed (Google service change). Replacement deferred, see runbook Phase 6. var company = Contact.Company.ToLowerInvariant(); if (SpamBlocks.Any(s => company.Contains(s))) { return Redirect(ConfirmedUrl); } try { await SendMailAsync(); } catch (Exception ex) { //Log the whole submission so it can be recovered from the journal, not lost _logger.LogError(ex, "Contact form send failed. Name: {Name} | Company: {Company} | Email: {Email} | DbId: {DbId} | Message: {Message}", Contact.Name, Contact.Company, Contact.Email, Contact.DbId, Contact.Message); SendFailed = true; return Page(); } return Redirect(ConfirmedUrl); } private async Task SendMailAsync() { var smtp = _configuration.GetSection("Smtp"); //Email is in the body as well as Reply-To, so it survives even if Reply-To can't be set var body = $"Name:\r\n{Contact.Name}\r\n\r\nCompany:\r\n{Contact.Company}\r\n\r\nEmail:\r\n{Contact.Email}\r\n\r\nDatabase Id:\r\n{Contact.DbId}\r\n\r\nMessage:\r\n{Contact.Message}"; var message = new MimeMessage(); //From is always the authenticated account; the visitor goes in Reply-To (no sender spoofing) message.From.Add(new MailboxAddress("AyaNova contact form", smtp["User"])); message.To.Add(MailboxAddress.Parse("support@ayanova.com")); if (MailboxAddress.TryParse(Contact.Email, out var replyTo)) { replyTo.Name = Contact.Name; message.ReplyTo.Add(replyTo); } message.Subject = $"Question about AyaNova from {OneLine(Contact.Company)}"; message.Body = new TextPart("plain") { Text = body }; using var client = new SmtpClient(); await client.ConnectAsync(smtp["Host"], smtp.GetValue("Port"), SecureSocketOptions.StartTls); await client.AuthenticateAsync(smtp["User"], smtp["Password"]); await client.SendAsync(message); await client.DisconnectAsync(true); } private static string OneLine(string s) => s.Replace("\r", " ").Replace("\n", " "); } }