From 046f847b3d68d8b41366a2281ddabf0c6e5cec94 Mon Sep 17 00:00:00 2001 From: John Cardinal Date: Fri, 9 Oct 2026 14:14:14 -0700 Subject: [PATCH 1/7] case 4670 initial changes --- Pages/Contact.cshtml.cs | 221 ++++++++++++----------------------- Program.cs | 19 ++- appsettings.Development.json | 3 +- contact.csproj | 23 +--- 4 files changed, 91 insertions(+), 175 deletions(-) diff --git a/Pages/Contact.cshtml.cs b/Pages/Contact.cshtml.cs index ba6af89..b776cce 100644 --- a/Pages/Contact.cshtml.cs +++ b/Pages/Contact.cshtml.cs @@ -1,13 +1,14 @@ -using System.Collections.Generic; -using Microsoft.AspNetCore.Mvc.RazorPages; +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 System.Net.Mail; -using System.Net; -//for captcha: -using System.Net.Http; -using Newtonsoft.Json.Linq; +using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using MimeKit; namespace contact.Pages { @@ -28,137 +29,31 @@ namespace contact.Pages 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; - public ContactModel(IConfiguration configuration) + private readonly ILogger _logger; + + public ContactModel(IConfiguration configuration, ILogger logger) { _configuration = configuration; - } - - public ActionResult OnPost() - { - if (!ModelState.IsValid) - { - return Page(); - } - - - //2025-11-01 Google dropping support for captcha, just nuking it for now, see how much spam I get - // if (!ReCaptchaPassed( - // Request.Form["g-recaptcha-response"], // that's how you get it from the Request object - // _configuration.GetSection("GoogleReCaptcha:secret").Value - // )) - // { - // //Return a fail code that will hopefully take us off the spammers list - // return StatusCode(500); - // } - - - - //go away spammers - //2022-05-06 changes to get rid of jumbo lead magnet spammer but still force him to pay someone to manually enter the stuff - //ideally it should read from a text file here but this is a quickie fix while Joyce is on important viha phone call - var blocks = new List(); - blocks.Add("google"); - blocks.Add("jumboleadmagnet");//<--go away this guy in particular, keeps spamming - blocks.Add("boostleadgeneration"); - //ericjonesmyemail@gmail.com - var companyname = Contact.Company.ToLowerInvariant(); - foreach (string s in blocks) - { - if (companyname.Contains(s)) - { - goto skipspam; - - } - } - - SendMail(); - - skipspam: - //TODO: go to a successful submit page on the ayanova site - //return Redirect("/confirm.html");//sample quickie page I made up - return Redirect("https://ayanova.com/confirmed.htm"); - } - - - private void SendMail() - { - var MessageBody = $"Name:\r\n{Contact.Name}\r\n\r\nCompany:\r\n{Contact.Company}\r\n\r\nDatabase Id:\r\n{Contact.DbId}\r\n\r\nMessage:\r\n{Contact.Message}"; - - //SEND TO US - using (var message = new MailMessage()) - { - message.To.Add(new MailAddress("support@ayanova.com")); - message.From = new MailAddress(Contact.Email); - message.Subject = $"Question about AyaNova from {Contact.Company}"; - message.Body = MessageBody; - using (var smtpClient = new SmtpClient("smtp.fastmail.com")) - { - - smtpClient.Host = "smtp.fastmail.com"; - smtpClient.Port = 587; - smtpClient.UseDefaultCredentials = false; - //testing - smtpClient.EnableSsl = true; - - // smtpClient.Port=465; - // smtpClient.DeliveryMethod= SmtpDeliveryMethod.Network; - - - - //NOTE: Do not use the noreply email address to send mail, it crashes the server somehow - - //****************************************************************************************************************** - //****************************************************************************************************************** - //TEST CRASH - // smtpClient.Credentials = new System.Net.NetworkCredential("noreply@ayanova.com", "91768700489f8edd28aa71e3e0f4073eba54ce83c4c1a6a910700fa94094ddfd"); - - - //FASTMAIL CHANGES - //Fastmail server names and ports help page: https://www.fastmail.help/hc/en-us/articles/1500000278342 - //APP Password created in fastmail for smtp use only labelled 'contact-form': cnea8agl6nlx5j7l - - //try using from as support@onayanova.com - //try domain version if this still won't work - smtpClient.Credentials = new System.Net.NetworkCredential("support@onayanova.com", "cnea8agl6nlx5j7l"); - //****************************************************************************************************************** - //****************************************************************************************************************** - - smtpClient.Send(message); - } - } - - //Removed 2022-10-06 causing backscatter and also not helpful - // //SEND COPY TO CLIENT - // using (var message = new MailMessage()) - // { - // message.To.Add(new MailAddress(Contact.Email)); - // message.From = new MailAddress("support@ayanova.com"); - // message.Subject = $"Confirmation that your question about AyaNova has been received from {Contact.Company}"; - // message.Body = MessageBody; - - // using (var smtpClient = new SmtpClient("smtp.fastmail.com")) - // { - - // smtpClient.Host = "smtp.fastmail.com"; - // smtpClient.Port = 587; - // smtpClient.UseDefaultCredentials = false; - // smtpClient.EnableSsl=true; - // //NOTE: Do not use the noreply email address to send mail, it crashes the server somehow - // smtpClient.Credentials = new System.Net.NetworkCredential("support@onayanova.com", "cnea8agl6nlx5j7l"); - // smtpClient.Send(message); - // } - // } - + _logger = logger; } public void OnGet() @@ -166,28 +61,64 @@ namespace contact.Pages Message = "Your contact page."; } + public async Task OnPostAsync() + { + if (!ModelState.IsValid) + { + return Page(); + } - //2025-11-01 Google dropping support for captcha, just nuking it for now, see how much spam I get - // public static bool ReCaptchaPassed(string gRecaptchaResponse, string secret) - // { - // HttpClient httpClient = new HttpClient(); - // var res = httpClient.GetAsync($"https://www.google.com/recaptcha/api/siteverify?secret={secret}&response={gRecaptchaResponse}").Result; - // if (res.StatusCode != HttpStatusCode.OK) - // { - // // logger.LogError("Error while sending request to ReCaptcha"); - // return false; - // } + //2025-11-01 reCAPTCHA removed (Google service change). Replacement deferred, see runbook Phase 6. - // string JSONres = res.Content.ReadAsStringAsync().Result; - // dynamic JSONdata = JObject.Parse(JSONres); - // if (JSONdata.success != "true") - // { - // return false; - // } + var company = Contact.Company.ToLowerInvariant(); + if (SpamBlocks.Any(s => company.Contains(s))) + { + return Redirect(ConfirmedUrl); + } - // return true; - // } + 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", " "); } } diff --git a/Program.cs b/Program.cs index a6518bb..b354103 100644 --- a/Program.cs +++ b/Program.cs @@ -1,19 +1,14 @@ -using Microsoft.AspNetCore; -using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; namespace contact { public class Program { - public static void Main(string[] args) - { - CreateWebHostBuilder(args).Build().Run(); - } - - public static IWebHostBuilder CreateWebHostBuilder(string[] args) => - WebHost.CreateDefaultBuilder(args) - .UseUrls("http://*:3002")//only this port works with nginx proxy - //.UseUrls("http://localhost:3002")//this doesn't work with nginx proxy - .UseStartup(); + public static void Main(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(web => web.UseStartup()) + .Build() + .Run(); } } diff --git a/appsettings.Development.json b/appsettings.Development.json index a2880cb..143621e 100644 --- a/appsettings.Development.json +++ b/appsettings.Development.json @@ -5,5 +5,6 @@ "System": "Information", "Microsoft": "Information" } - } + }, + "AllowedHosts": "localhost" } diff --git a/contact.csproj b/contact.csproj index 48a82e2..691b674 100644 --- a/contact.csproj +++ b/contact.csproj @@ -1,20 +1,9 @@ - - + - net6.0 + net10.0 + efc95c93-c628-4ad5-a697-cd1613181a44 - - - + + - - - + \ No newline at end of file From 38113310aab1b7ce01d2ba12636a2faf1e274ff5 Mon Sep 17 00:00:00 2001 From: John Cardinal Date: Fri, 9 Oct 2026 14:15:45 -0700 Subject: [PATCH 2/7] case 4670 config changes --- appsettings.json | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/appsettings.json b/appsettings.json index 848b568..7376aad 100644 --- a/appsettings.json +++ b/appsettings.json @@ -4,9 +4,5 @@ "Default": "Warning" } }, - "AllowedHosts": "*", - "GoogleReCaptcha": { - "key": "6LcH7GUUAAAAAJIDf_JDZolSv__xN6oqr9Dx79zs", - "secret": "6LcH7GUUAAAAAERHuA3fWDcMIlJ3QHE_WNZm4BSO" - } + "AllowedHosts": "*" } From 7feb501bc23f6b659972a351d7acbd0d356d9da1 Mon Sep 17 00:00:00 2001 From: John Cardinal Date: Fri, 9 Oct 2026 14:47:47 -0700 Subject: [PATCH 3/7] case 4670 multiple changes by Claude to break out message sending from the individual forms and support the trial server request --- Pages/Contact.cshtml | 41 ++-- Pages/Contact.cshtml.cs | 222 ++++++++----------- Pages/Request.cshtml | 28 ++- Pages/Request.cshtml.cs | 393 ++++++++++++++------------------- Pages/Shared/_Layout.cshtml | 34 +-- Program.cs | 31 +-- Properties/launchSettings.json | 39 +--- Startup.cs | 117 +++++----- appsettings.Development.json | 16 +- appsettings.json | 25 ++- contact.csproj | 21 +- 11 files changed, 426 insertions(+), 541 deletions(-) diff --git a/Pages/Contact.cshtml b/Pages/Contact.cshtml index c4f4500..ae8cf0b 100644 --- a/Pages/Contact.cshtml +++ b/Pages/Contact.cshtml @@ -1,12 +1,19 @@ -@page +@page @model ContactModel @{ ViewData["Title"] = "Contact"; } -
+ @if (Model.SendFailed) + { +
+ Sorry, your message could not be sent. Please try again in a few minutes, + or email support@ayanova.com directly. +
+ } +
@@ -63,9 +70,9 @@
- +
- +
@@ -97,29 +104,12 @@ - -
-
-
-
@@ -130,7 +120,7 @@

Why do we ask for your name, company name and email address?

-

We use the information you send us to respond to you, to provide sales support and technicial support to +

We use the information you send us to respond to you, to provide sales support and technical support to you and your company, and to make AyaNova recommendations and suggestions to you and your company. Also refer to our Privacy Policy for more clarity.

@@ -164,8 +154,7 @@

In order to be able to provide an affordable product for small business with fast technical support globally, we do not provide phone support.

Please contact us via the online form above, or send us a direct email to support@ayanova.com and our - North American support staff will respond promptly between 9:00AM and 5:00PM Pacific Standard Time (-8GMT/ - UTC) Monday to Friday.

+ North American support staff will respond promptly between 9:00AM and 5:00PM Pacific Time Monday to Friday.

@@ -185,4 +174,6 @@ Privacy Policy
- \ No newline at end of file + + +
\ No newline at end of file diff --git a/Pages/Contact.cshtml.cs b/Pages/Contact.cshtml.cs index b776cce..0925262 100644 --- a/Pages/Contact.cshtml.cs +++ b/Pages/Contact.cshtml.cs @@ -1,124 +1,98 @@ -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", " "); - } -} +using System; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using contact.Services; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; + +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; } + //Optional; AyaNova clients deep-link to this page with their database ID. + //Binding attributes left exactly as they were so that link keeps working. + [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 + public static readonly string[] SpamBlocks = { "google", "jumboleadmagnet", "boostleadgeneration" }; + + [BindProperty(SupportsGet = true)] + public ContactFormModel Contact { get; set; } + + //Set when the send fails, so the page can tell the visitor instead of claiming success + public bool SendFailed { get; set; } + + private readonly MailSender _mail; + private readonly ILogger _logger; + + public ContactModel(MailSender mail, ILogger logger) + { + _mail = mail; + _logger = logger; + } + + public void OnGet() + { + } + + public async Task OnPostAsync() + { + if (!ModelState.IsValid) + { + return Page(); + } + + //2025-11-01 reCAPTCHA removed (Google service change). Replacement deferred, see runbook Phase 6. + + if (IsBlocked(Contact.Company)) + { + return Redirect(ConfirmedUrl); + } + + //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}"; + + try + { + await _mail.SendToSupportAsync($"Question about AyaNova from {Contact.Company}", body, Contact.Name, Contact.Email); + } + catch (Exception ex) + { + //Log the whole submission so it can be recovered from the journal, not lost + _logger.LogError(ex, "Contact form send failed. Submission:\r\n{Body}", body); + SendFailed = true; + return Page(); + } + + return Redirect(ConfirmedUrl); + } + + public static bool IsBlocked(string company) + { + var c = company.ToLowerInvariant(); + return SpamBlocks.Any(s => c.Contains(s)); + } + } +} diff --git a/Pages/Request.cshtml b/Pages/Request.cshtml index 0bbe1b3..f26df87 100644 --- a/Pages/Request.cshtml +++ b/Pages/Request.cshtml @@ -1,4 +1,4 @@ -@page +@page @model RequestModel @{ ViewData["Title"] = "Request"; @@ -7,6 +7,14 @@
+ @if (Model.SendFailed) + { +
+ Sorry, your request could not be sent. Please try again in a few minutes, + or email support@ayanova.com directly. +
+ } +

Request AyaNova subscription 7 day trial account

@@ -58,7 +66,7 @@
- +
@@ -67,18 +75,6 @@ - -
@@ -102,4 +98,6 @@ Privacy Policy
- \ No newline at end of file + + + \ No newline at end of file diff --git a/Pages/Request.cshtml.cs b/Pages/Request.cshtml.cs index 73ce0c9..eb19f11 100644 --- a/Pages/Request.cshtml.cs +++ b/Pages/Request.cshtml.cs @@ -1,228 +1,165 @@ -using System.Collections.Generic; -using Microsoft.AspNetCore.Mvc.RazorPages; -using System.ComponentModel.DataAnnotations; -using Microsoft.AspNetCore.Mvc; -using System.Net.Mail; -using System.Net; -//for captcha: -using System.Net.Http; -using Newtonsoft.Json.Linq; -using Microsoft.Extensions.Configuration; -using Microsoft.AspNetCore.Mvc.Rendering; - -namespace contact.Pages -{ - [BindProperties(SupportsGet = true)] - public class RequestFormModel - { - public RequestFormModel() - { - DataCenter = "SF"; - } - [Required] - public string Name { get; set; } - [Required] - public string Company { get; set; } - - [Required, EmailAddress] - public string Email { get; set; } - [Required] - public string DataCenter { get; set; } - - - - - } - - public class RequestModel : PageModel - { - // public string DataCenter { get; set; } - [BindProperty(SupportsGet = true)] - public RequestFormModel SubRequest { get; set; } - - public List DataCenters { get; } = new List - { - new SelectListItem { Value = "SF", Text = "San Francisco" }, - new SelectListItem { Value = "NY", Text = "New York" }, - new SelectListItem { Value = "TOR", Text = "Toronto" }, - new SelectListItem { Value = "AMS", Text = "Amsterdam" }, - new SelectListItem { Value = "LON", Text = "London" }, - new SelectListItem { Value = "FFT", Text = "Frankfurt" }, - new SelectListItem { Value = "SNG", Text = "Singapore" }, - new SelectListItem { Value = "BNG", Text = "Bangalore" }, - new SelectListItem { Value = "SYD", Text = "Sydney Australia" } - - }; - - private readonly IConfiguration _configuration; - public RequestModel(IConfiguration configuration) - { - _configuration = configuration; - } - - public ActionResult OnPost() - { - if (!ModelState.IsValid) - { - return Page(); - } - - //2025-11-01 Google dropping support for captcha, just nuking it for now, see how much spam I get - // if (!ReCaptchaPassed( - // Request.Form["g-recaptcha-response"], // that's how you get it from the Request object - // _configuration.GetSection("GoogleReCaptcha:secret").Value - // )) - // { - // //Return a fail code that will hopefully take us off the spammers list - // return StatusCode(500); - // } - - - - //fuck those spammers - //2022-05-06 changes to get rid of jumbo lead magnet spammer but still force him to pay someone to manually enter the stuff - //ideally it should read from a text file here but this is a quickie fix while Joyce is on important viha phone call - var blocks = new List(); - blocks.Add("google"); - blocks.Add("jumboleadmagnet");//<--fuck this guy in particular, keeps spamming - blocks.Add("boostleadgeneration"); - //ericjonesmyemail@gmail.com - var companyname = SubRequest.Company.ToLowerInvariant(); - foreach (string s in blocks) - { - if (companyname.Contains(s)) - { - goto skipspam; - - } - } - - PostRequestToSockeye(); - SendMail(); - - skipspam: - return Redirect("https://ayanova.com/docs/ay-evaluate/"); - } - - private void PostRequestToSockeye() - { - - using var client = new HttpClient(); - //client.BaseAddress = new System.Uri("https://sockeye.ayanova.com");///api/v8.0/subscription-server/ - - var r = new - { - dataCenter = SubRequest.DataCenter, - email = SubRequest.Email, - company = SubRequest.Company, - name = SubRequest.Name, - rValue = "totallylegit" - }; - - - HttpContent body = new StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(r), - System.Text.Encoding.UTF8, - "application/json"); - var response = client.PostAsync("https://sockeye.ayanova.com/subscription-server-trial-request", body).Result; - //var response = client.PostAsync("http://localhost:7676/subscription-server-trial-request", body).Result; - - // if (response.IsSuccessStatusCode) - // { - // var content = await response.Content.ReadAsStringAsync(); - // if (!string.IsNullOrEmpty(content)) - // { - // var objDeserializeObject = JsonConvert.DeserializeObject(content); - - // Console.WriteLine("Data Saved Successfully."); - - // if (objDeserializeObject != null) - // { - // Console.WriteLine(objDeserializeObject.Name); - // } - // } - // } - - } - - - private void SendMail() - { - var MessageBody = $"Name:\r\n{SubRequest.Name}\r\n\r\nCompany:\r\n{SubRequest.Company}\r\n\r\nDataCenter:\r\n{SubRequest.DataCenter}"; - - //SEND TO US - using (var message = new MailMessage()) - { - message.To.Add(new MailAddress("support@ayanova.com")); - message.From = new MailAddress(SubRequest.Email); - message.Subject = $"Trial request from {SubRequest.Company}"; - message.Body = MessageBody; - using (var smtpClient = new SmtpClient("smtp.fastmail.com")) - { - - smtpClient.Host = "smtp.fastmail.com"; - smtpClient.Port = 587; - smtpClient.UseDefaultCredentials = false; - //testing - smtpClient.EnableSsl = true; - - // smtpClient.Port=465; - // smtpClient.DeliveryMethod= SmtpDeliveryMethod.Network; - - - - //NOTE: Do not use the noreply email address to send mail, it crashes the server somehow - - //****************************************************************************************************************** - //****************************************************************************************************************** - //TEST CRASH - // smtpClient.Credentials = new System.Net.NetworkCredential("noreply@ayanova.com", "91768700489f8edd28aa71e3e0f4073eba54ce83c4c1a6a910700fa94094ddfd"); - - - //FASTMAIL CHANGES - //Fastmail server names and ports help page: https://www.fastmail.help/hc/en-us/articles/1500000278342 - //APP Password created in fastmail for smtp use only labelled 'contact-form': cnea8agl6nlx5j7l - - //try using from as support@onayanova.com - //try domain version if this still won't work - smtpClient.Credentials = new System.Net.NetworkCredential("support@onayanova.com", "cnea8agl6nlx5j7l"); - //****************************************************************************************************************** - //****************************************************************************************************************** - - smtpClient.Send(message); - } - } - - - - } - - public void OnGet() - { - - } - - - //2025-11-01 Google dropping support for captcha, just nuking it for now, see how much spam I get - // public static bool ReCaptchaPassed(string gRecaptchaResponse, string secret) - // { - // HttpClient httpClient = new HttpClient(); - // var res = httpClient.GetAsync($"https://www.google.com/recaptcha/api/siteverify?secret={secret}&response={gRecaptchaResponse}").Result; - // if (res.StatusCode != HttpStatusCode.OK) - // { - // // logger.LogError("Error while sending request to ReCaptcha"); - // return false; - // } - - // string JSONres = res.Content.ReadAsStringAsync().Result; - // dynamic JSONdata = JObject.Parse(JSONres); - // if (JSONdata.success != "true") - // { - // return false; - // } - - // return true; - // } - - - } -} +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading.Tasks; +using contact.Services; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace contact.Pages +{ + [BindProperties(SupportsGet = true)] + public class RequestFormModel + { + public RequestFormModel() + { + DataCenter = "SF"; + } + [Required] + public string Name { get; set; } + [Required] + public string Company { get; set; } + [Required, EmailAddress] + public string Email { get; set; } + [Required] + public string DataCenter { get; set; } + } + + public class RequestModel : PageModel + { + private const string EvaluateUrl = "https://ayanova.com/docs/ay-evaluate/"; + + [BindProperty(SupportsGet = true)] + public RequestFormModel SubRequest { get; set; } + + //Set when neither Sockeye nor the email got the request, so the page can say so + public bool SendFailed { get; set; } + + public List DataCenters { get; } = new List + { + new SelectListItem { Value = "SF", Text = "San Francisco" }, + new SelectListItem { Value = "ATL", Text = "Atlanta" }, + new SelectListItem { Value = "NY", Text = "New York" }, + new SelectListItem { Value = "TOR", Text = "Toronto" }, + new SelectListItem { Value = "AMS", Text = "Amsterdam" }, + new SelectListItem { Value = "LON", Text = "London" }, + new SelectListItem { Value = "FFT", Text = "Frankfurt" }, + new SelectListItem { Value = "SNG", Text = "Singapore" }, + new SelectListItem { Value = "BNG", Text = "Bangalore" }, + new SelectListItem { Value = "SYD", Text = "Sydney Australia" } + }; + + private readonly IConfiguration _configuration; + private readonly IHttpClientFactory _httpClientFactory; + private readonly MailSender _mail; + private readonly ILogger _logger; + + public RequestModel(IConfiguration configuration, IHttpClientFactory httpClientFactory, MailSender mail, ILogger logger) + { + _configuration = configuration; + _httpClientFactory = httpClientFactory; + _mail = mail; + _logger = logger; + } + + public void OnGet() + { + } + + public async Task OnPostAsync() + { + //The select can be bypassed by a hand-crafted POST; only accept codes from the list + if (!DataCenters.Any(d => d.Value == SubRequest.DataCenter)) + { + ModelState.AddModelError("SubRequest.DataCenter", "Please choose a data center from the list."); + } + + if (!ModelState.IsValid) + { + return Page(); + } + + if (ContactModel.IsBlocked(SubRequest.Company)) + { + return Redirect(EvaluateUrl); + } + + //1. Record the request in Sockeye. A failure here is logged and reported in the email, + // so the trial can be set up by hand instead of the request disappearing. + string sockeyeResult; + bool sockeyeOk; + try + { + await PostRequestToSockeyeAsync(); + sockeyeOk = true; + sockeyeResult = "Recorded in Sockeye."; + } + catch (Exception ex) + { + sockeyeOk = false; + sockeyeResult = $"NOT RECORDED IN SOCKEYE ({ex.Message}). Set this trial up by hand."; + _logger.LogError(ex, "Trial request post to Sockeye failed. {Body}", Describe()); + } + + //2. Notify support. Email is in the body; From is the form account, so it was never there before either + var body = $"{Describe()}\r\n\r\nSockeye:\r\n{sockeyeResult}"; + var subject = sockeyeOk + ? $"Trial request from {SubRequest.Company}" + : $"Trial request NOT RECORDED from {SubRequest.Company}"; + bool mailOk; + try + { + await _mail.SendToSupportAsync(subject, body, SubRequest.Name, SubRequest.Email); + mailOk = true; + } + catch (Exception ex) + { + mailOk = false; + _logger.LogError(ex, "Trial request email failed. Submission:\r\n{Body}", body); + } + + //Either path reaching John means the request is not lost; only both failing is a failure for the visitor + if (!sockeyeOk && !mailOk) + { + SendFailed = true; + return Page(); + } + + return Redirect(EvaluateUrl); + } + + private string Describe() => + $"Name:\r\n{SubRequest.Name}\r\n\r\nCompany:\r\n{SubRequest.Company}\r\n\r\nEmail:\r\n{SubRequest.Email}\r\n\r\nDataCenter:\r\n{SubRequest.DataCenter}"; + + private async Task PostRequestToSockeyeAsync() + { + //Sockeye:TrialRequestUrl. On the server this is http://127.0.0.1:7575/subscription-server-trial-request, + //straight to Kestrel on the same host, so it depends on neither DNS nor the droplet reaching its own reserved IP. + var url = _configuration["Sockeye:TrialRequestUrl"]; + if (string.IsNullOrWhiteSpace(url)) + { + throw new InvalidOperationException("Sockeye:TrialRequestUrl is not configured"); + } + + var r = new + { + dataCenter = SubRequest.DataCenter, + email = SubRequest.Email, + company = SubRequest.Company, + name = SubRequest.Name, + rValue = "totallylegit" + }; + + var client = _httpClientFactory.CreateClient("sockeye"); + var response = await client.PostAsJsonAsync(url, r); + //Previously the response was never checked, so a rejected request failed silently + response.EnsureSuccessStatusCode(); + } + } +} diff --git a/Pages/Shared/_Layout.cshtml b/Pages/Shared/_Layout.cshtml index 57c9c38..00818d3 100644 --- a/Pages/Shared/_Layout.cshtml +++ b/Pages/Shared/_Layout.cshtml @@ -16,40 +16,16 @@ - + - - +
@RenderBody() - +
@@ -72,13 +48,13 @@ - + *@ @RenderSection("Scripts", required: false) diff --git a/Program.cs b/Program.cs index b354103..9e7921d 100644 --- a/Program.cs +++ b/Program.cs @@ -1,14 +1,17 @@ -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Hosting; - -namespace contact -{ - public class Program - { - public static void Main(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(web => web.UseStartup()) - .Build() - .Run(); - } -} +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace contact +{ + public class Program + { + //No UseUrls here. The listening address comes from ASPNETCORE_URLS in the systemd unit + //(http://127.0.0.1:3002) and from launchSettings.json for local runs. + //The old "only *:3002 works with nginx" comment was F1: nginx proxied to $server_addr, the public IP. + public static void Main(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(web => web.UseStartup()) + .Build() + .Run(); + } +} diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json index b5eb86e..d26bfc1 100644 --- a/Properties/launchSettings.json +++ b/Properties/launchSettings.json @@ -1,27 +1,12 @@ -{ - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:30176", - "sslPort": 44340 - } - }, - "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "contact": { - "commandName": "Project", - "launchBrowser": true, - "applicationUrl": "https://localhost:5001;http://localhost:5000", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } - } -} \ No newline at end of file +{ + "profiles": { + "contact": { + "commandName": "Project", + "launchBrowser": false, + "applicationUrl": "http://localhost:3002", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Startup.cs b/Startup.cs index ef2b38d..1bb9fed 100644 --- a/Startup.cs +++ b/Startup.cs @@ -1,52 +1,65 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -namespace contact -{ - public class Startup - { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - public IConfiguration Configuration { get; } - - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - services.AddControllers().AddNewtonsoftJson(); - services.AddRazorPages(); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - else - { - app.UseExceptionHandler("/Error"); - app.UseHsts(); - } - - // app.UseHttpsRedirection(); - app.UseStaticFiles(); - // app.UseCookiePolicy(); - app.UseRouting(); - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - endpoints.MapRazorPages(); - - - }); - // app.UseMvc(); - } - } -} +using System; +using System.IO; +using contact.Services; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace contact +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers(); + services.AddRazorPages(); + services.AddSingleton(); + + services.AddHttpClient("sockeye", client => + { + client.Timeout = TimeSpan.FromSeconds(15); + }); + + //Antiforgery tokens on the forms are encrypted with Data Protection keys. The service account has no + //home directory, so without this the keys live only in memory and every restart breaks forms that + //visitors already have open. Set DataProtection:KeyPath on the server (systemd StateDirectory=contact). + var keyPath = Configuration["DataProtection:KeyPath"]; + if (!string.IsNullOrWhiteSpace(keyPath)) + { + services.AddDataProtection() + .SetApplicationName("contact") + .PersistKeysToFileSystem(new DirectoryInfo(keyPath)); + } + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + else + { + app.UseExceptionHandler("/Error"); + } + + app.UseStaticFiles(); + app.UseRouting(); + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + endpoints.MapRazorPages(); + }); + } + } +} diff --git a/appsettings.Development.json b/appsettings.Development.json index 143621e..3b59b0d 100644 --- a/appsettings.Development.json +++ b/appsettings.Development.json @@ -1,10 +1,6 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Debug", - "System": "Information", - "Microsoft": "Information" - } - }, - "AllowedHosts": "localhost" -} +{ + "AllowedHosts": "localhost", + "Sockeye": { + "TrialRequestUrl": "http://localhost:7575/subscription-server-trial-request" + } +} diff --git a/appsettings.json b/appsettings.json index 7376aad..a91098d 100644 --- a/appsettings.json +++ b/appsettings.json @@ -1,8 +1,17 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Warning" - } - }, - "AllowedHosts": "*" -} +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "contact.ayanova.com", + "Smtp": { + "Host": "smtp.fastmail.com", + "Port": 587, + "User": "support@onayanova.com" + }, + "Sockeye": { + "TrialRequestUrl": "http://127.0.0.1:7575/subscription-server-trial-request" + } +} diff --git a/contact.csproj b/contact.csproj index 691b674..e8f8d01 100644 --- a/contact.csproj +++ b/contact.csproj @@ -1,9 +1,12 @@ - - - net10.0 - efc95c93-c628-4ad5-a697-cd1613181a44 - - - - - \ No newline at end of file + + + + net10.0 + + + + + + + + From b590d3e8593bde1f9115cee23498df2163cfbe59 Mon Sep 17 00:00:00 2001 From: John Cardinal Date: Fri, 9 Oct 2026 14:56:28 -0700 Subject: [PATCH 4/7] clean up conflicting privacy policy links to correct new page --- Pages/Contact.cshtml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Pages/Contact.cshtml b/Pages/Contact.cshtml index ae8cf0b..f431e1f 100644 --- a/Pages/Contact.cshtml +++ b/Pages/Contact.cshtml @@ -115,14 +115,14 @@
-

All of our responses are from real live people, so there could be a delay receiving a reply due to time +

We respond personally so there could be a delay receiving a reply due to time zone differences, time of day, weekend, or if a holiday long weekend.

Why do we ask for your name, company name and email address?

We use the information you send us to respond to you, to provide sales support and technical support to you and your company, and to make AyaNova recommendations and suggestions to you and your company. Also - refer to our Privacy Policy for more + refer to our Privacy Policy for more clarity.

@@ -154,7 +154,7 @@

In order to be able to provide an affordable product for small business with fast technical support globally, we do not provide phone support.

Please contact us via the online form above, or send us a direct email to support@ayanova.com and our - North American support staff will respond promptly between 9:00AM and 5:00PM Pacific Time Monday to Friday.

+ North American support staff will respond promptly between 9:00AM and 5:00PM Pacific Time (PCT) Monday to Friday.

@@ -171,7 +171,7 @@ Since 1999 AyaNova® is a registered trademark of Ground Zero Tech-Works Inc. in the United States and other countries.
- Privacy Policy + Privacy Policy From 909e8680013ac75bb1b5562e16b49c0391029a3d Mon Sep 17 00:00:00 2001 From: John Cardinal Date: Fri, 9 Oct 2026 14:56:49 -0700 Subject: [PATCH 5/7] Removed unneeded controllers reference --- Startup.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Startup.cs b/Startup.cs index 1bb9fed..c4e4353 100644 --- a/Startup.cs +++ b/Startup.cs @@ -20,8 +20,7 @@ namespace contact public IConfiguration Configuration { get; } public void ConfigureServices(IServiceCollection services) - { - services.AddControllers(); + { services.AddRazorPages(); services.AddSingleton(); From ad6aab94a91c90395a4fae0b121eff75b65f6234 Mon Sep 17 00:00:00 2001 From: John Cardinal Date: Fri, 9 Oct 2026 17:06:40 -0700 Subject: [PATCH 6/7] case 4670 tested, confirmed everything works as expected, ready to merge back to main now --- .vscode/launch.json | 4 ++-- appsettings.Development.json | 2 +- contact.csproj | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 4bc0806..83dfa9b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -2,7 +2,7 @@ // Use IntelliSense to find out which attributes exist for C# debugging // Use hover for the description of the existing attributes // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md - "version": "0.2.0", + "version": "1.0.0", "configurations": [ { "name": ".NET Core Launch (web)", @@ -10,7 +10,7 @@ "request": "launch", "preLaunchTask": "build", // If you have changed target frameworks, make sure to update the program path. - "program": "${workspaceFolder}/bin/Debug/net6.0/contact.dll", + "program": "${workspaceFolder}/bin/Debug/net10.0/contact.dll", "args": [], "cwd": "${workspaceFolder}", "stopAtEntry": false, diff --git a/appsettings.Development.json b/appsettings.Development.json index 3b59b0d..18b05aa 100644 --- a/appsettings.Development.json +++ b/appsettings.Development.json @@ -1,6 +1,6 @@ { "AllowedHosts": "localhost", "Sockeye": { - "TrialRequestUrl": "http://localhost:7575/subscription-server-trial-request" + "TrialRequestUrl": "http://localhost:7676/subscription-server-trial-request" } } diff --git a/contact.csproj b/contact.csproj index e8f8d01..7922c25 100644 --- a/contact.csproj +++ b/contact.csproj @@ -2,6 +2,7 @@ net10.0 + efc95c93-c628-4ad5-a697-cd1613181a44 From 4f6c18a1ac5e2b1c46fd43b1db100d4c3bcbec5b Mon Sep 17 00:00:00 2001 From: John Cardinal Date: Fri, 9 Oct 2026 17:08:23 -0700 Subject: [PATCH 7/7] case 4670 whups nearly missed an addition of services all ready to merge now for real --- Services/MailSender.cs | 47 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 Services/MailSender.cs diff --git a/Services/MailSender.cs b/Services/MailSender.cs new file mode 100644 index 0000000..b6e800d --- /dev/null +++ b/Services/MailSender.cs @@ -0,0 +1,47 @@ +using System.Threading.Tasks; +using MailKit.Net.Smtp; +using MailKit.Security; +using Microsoft.Extensions.Configuration; +using MimeKit; + +namespace contact.Services +{ + //Sends web form submissions to support. + //From is always the authenticated account; the visitor goes in Reply-To, so there is no sender spoofing. + //SMTP settings come from the "Smtp" config section; the password is never in source. + public class MailSender + { + private const string SupportAddress = "support@ayanova.com"; + private readonly IConfiguration _configuration; + + public MailSender(IConfiguration configuration) + { + _configuration = configuration; + } + + public async Task SendToSupportAsync(string subject, string body, string visitorName, string visitorEmail) + { + var smtp = _configuration.GetSection("Smtp"); + + var message = new MimeMessage(); + message.From.Add(new MailboxAddress("AyaNova web forms", smtp["User"])); + message.To.Add(MailboxAddress.Parse(SupportAddress)); + if (MailboxAddress.TryParse(visitorEmail, out var replyTo)) + { + replyTo.Name = visitorName; + message.ReplyTo.Add(replyTo); + } + message.Subject = OneLine(subject); + 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); + } + + //Subjects include visitor-supplied text; a hand-crafted POST could put line breaks in it + private static string OneLine(string s) => s.Replace("\r", " ").Replace("\n", " "); + } +}