diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..e84ffe3 --- /dev/null +++ b/.env.sample @@ -0,0 +1,2 @@ +# Copy this file to .env and fill in the real values. .env is never committed. +SMTP_PASSWORD=your-mailbox-password diff --git a/.gitignore b/.gitignore index dc5d4b8..6b8995f 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ *.log *.user *.suo + +# Secrets +/.env diff --git a/README.md b/README.md index e29cf51..2e20392 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ A clean Classic ASP application scaffold based on the RouteKit MVC framework. 1. Install IIS Express with Classic ASP enabled. 2. Run `run_site.cmd`. -3. Browse to `http://localhost:8080/`. +3. Browse to `http://localhost:8082/`. The landing page is served by `HomeController.Index` through `app/views/Home/landing.asp`. The earlier logo-reference page remains at diff --git a/app/controllers/HomeController.asp b/app/controllers/HomeController.asp index ebf00d4..e36cfd2 100644 --- a/app/controllers/HomeController.asp +++ b/app/controllers/HomeController.asp @@ -3,9 +3,21 @@ Class HomeController_Class Public Title Public useLayout + ' Contact form state shared with the landing view + Private contactErrors + Private contactName + Private contactEmail + Private contactPhone + Private contactBusiness + Private contactMessage + Private contactSent + Private Sub Class_Initialize() Title = "PostcardMojo Logo Files" useLayout = False + contactErrors = Array() + ReDim contactErrors(-1) + contactSent = (Request.QueryString("contact") = "sent") End Sub Public Sub Index() @@ -13,6 +25,42 @@ Class HomeController_Class <% End Sub + + ' GetAntiCSRFToken returns an empty value the first time it creates a token, so read it again. + Private Function ContactCsrfToken() + ContactCsrfToken = HTMLSecurity().GetAntiCSRFToken("home.contact") + If Len(ContactCsrfToken) = 0 Then ContactCsrfToken = HTMLSecurity().GetAntiCSRFToken("home.contact") + End Function + + ' POST /contact - validate the inquiry and email the PostcardMojo team. + Public Sub Contact() + contactName = Trim(Request.Form("name")) + contactEmail = Trim(Request.Form("email")) + contactPhone = Trim(Request.Form("phone")) + contactBusiness = Trim(Request.Form("business")) + contactMessage = Trim(Request.Form("message")) + + ' Honeypot: real visitors never see or fill this field + If Len(Request.Form("zq_trap_field")) > 0 Then + Response.Redirect "/?contact=sent#contact" + End If + + If Not HTMLSecurity().IsValidAntiCSRFToken("home.contact", Request.Form("csrf_token")) Then + contactErrors = Array("Your session expired. Please try again.") + Else + contactErrors = ContactInquiryService().ValidationErrors(contactName, contactEmail, contactBusiness, contactMessage) + End If + + If UBound(contactErrors) < 0 Then + If ContactInquiryService().Notify(contactName, contactEmail, contactPhone, contactBusiness, contactMessage) Then + HTMLSecurity().ClearAntiCSRFToken "home.contact" + Response.Redirect "/?contact=sent#contact" + End If + contactErrors = Array("We could not send your message. Please email danielcovington@postcardmojo.com directly.") + End If + + Index + End Sub End Class Dim HomeController_Class__Singleton diff --git a/app/controllers/autoload_controllers.asp b/app/controllers/autoload_controllers.asp index ebc1d93..51bd037 100644 --- a/app/controllers/autoload_controllers.asp +++ b/app/controllers/autoload_controllers.asp @@ -1 +1,3 @@ + + diff --git a/app/services/ContactInquiryService.asp b/app/services/ContactInquiryService.asp new file mode 100644 index 0000000..b02967f --- /dev/null +++ b/app/services/ContactInquiryService.asp @@ -0,0 +1,81 @@ +<% +' Validates a visitor inquiry and notifies the PostcardMojo team by email. +Class ContactInquiryService_Class + Public Function ValidationErrors(name, email, business, message) + Dim errs : errs = Array() + ReDim errs(-1) + + If Len(name) = 0 Then AddMessage errs, "Please enter your name." + If Len(name) > 100 Then AddMessage errs, "Name must be 100 characters or fewer." + + If Len(email) = 0 Then + AddMessage errs, "Please enter your email address." + ElseIf Len(email) > 150 Or Not IsValidEmail(email) Then + AddMessage errs, "Please enter a valid email address." + End If + + If Len(business) > 150 Then AddMessage errs, "Business name must be 150 characters or fewer." + If Len(message) > 2000 Then AddMessage errs, "Message must be 2000 characters or fewer." + + ValidationErrors = errs + End Function + + Public Function Notify(name, email, phone, business, message) + Dim recipient, smtpServer, smtpPort, smtpUser, body + recipient = Setting("ContactRecipient", "covington.daniel@gmail.com") + smtpServer = Setting("SmtpServer", "localhost") + smtpPort = Setting("SmtpPort", "25") + smtpUser = Setting("SmtpUsername", "") + + body = "New PostcardMojo website inquiry" & vbCrLf & vbCrLf & _ + "Name: " & name & vbCrLf & _ + "Email: " & email & vbCrLf & _ + "Phone: " & phone & vbCrLf & _ + "Business: " & business & vbCrLf & vbCrLf & _ + "Message:" & vbCrLf & message & vbCrLf & vbCrLf & _ + "Received: " & Now() & vbCrLf & _ + "IP: " & Request.ServerVariables("REMOTE_ADDR") + + CDOEmail.From = Setting("SmtpFrom", Choice(Len(smtpUser) > 0, smtpUser, recipient)) + CDOEmail.Subject = "PostcardMojo inquiry from " & StripLineBreaks(name) + CDOEmail.Body = body + CDOEmail.IsBodyHTML = False + CDOEmail.SMTPServer = smtpServer + CDOEmail.SMTPPort = CLng(smtpPort) + CDOEmail.SMTPUsername = smtpUser + CDOEmail.SMTPPassword = EnvService().Value("SMTP_PASSWORD", "") + CDOEmail.SMTPUseSSL = (LCase(Setting("SmtpUseSsl", "false")) = "true") + CDOEmail.AddRecipient "TO", recipient + Notify = CDOEmail.Send() + End Function + + Public Function StripLineBreaks(value) + StripLineBreaks = Replace(Replace(value, vbCr, " "), vbLf, " ") + End Function + + Private Function Setting(key, fallback) + Dim value : value = GetAppSetting(key) + If value = "nothing" Or Len(value) = 0 Then value = fallback + Setting = value + End Function + + Private Function IsValidEmail(value) + Dim re : Set re = New RegExp + re.Pattern = "^[^\s@<>,;""]+@[^\s@<>,;""]+\.[A-Za-z]{2,}$" + IsValidEmail = re.Test(value) + End Function + + Private Sub AddMessage(ByRef errs, msg) + ReDim Preserve errs(UBound(errs) + 1) + errs(UBound(errs)) = msg + End Sub +End Class + +Dim ContactInquiryService_Class__Singleton +Function ContactInquiryService() + If IsEmpty(ContactInquiryService_Class__Singleton) Then + Set ContactInquiryService_Class__Singleton = New ContactInquiryService_Class + End If + Set ContactInquiryService = ContactInquiryService_Class__Singleton +End Function +%> diff --git a/app/services/EnvService.asp b/app/services/EnvService.asp new file mode 100644 index 0000000..3eb29a5 --- /dev/null +++ b/app/services/EnvService.asp @@ -0,0 +1,56 @@ +<% +' Reads KEY=VALUE secrets from the .env file in the project root (outside the +' public web root). Real environment variables take precedence over the file. +Class EnvService_Class + Private values + + Private Sub Class_Initialize() + Set values = Server.CreateObject("Scripting.Dictionary") + values.CompareMode = 1 + LoadFile + End Sub + + Public Function Value(key, fallback) + Dim shell : Set shell = Server.CreateObject("WScript.Shell") + Value = shell.Environment("Process")(key) + Set shell = Nothing + If Len(Value) > 0 Then Exit Function + If values.Exists(key) Then + Value = values(key) + Else + Value = fallback + End If + End Function + + Private Sub LoadFile() + Dim fso, path, stream, line, pos, key, val + Set fso = Server.CreateObject("Scripting.FileSystemObject") + path = Server.MapPath("..\.env") + If Not fso.FileExists(path) Then Exit Sub + Set stream = fso.OpenTextFile(path, 1, False) + Do Until stream.AtEndOfStream + line = Trim(stream.ReadLine()) + If Len(line) > 0 And Left(line, 1) <> "#" Then + pos = InStr(line, "=") + If pos > 1 Then + key = Trim(Left(line, pos - 1)) + val = Trim(Mid(line, pos + 1)) + If Len(val) > 1 Then + If (Left(val, 1) = """" And Right(val, 1) = """") Or (Left(val, 1) = "'" And Right(val, 1) = "'") Then val = Mid(val, 2, Len(val) - 2) + End If + values(key) = val + End If + End If + Loop + stream.Close + End Sub +End Class + +Dim EnvService_Class__Singleton +Function EnvService() + If IsEmpty(EnvService_Class__Singleton) Then + Set EnvService_Class__Singleton = New EnvService_Class + End If + Set EnvService = EnvService_Class__Singleton +End Function +%> diff --git a/app/views/Home/landing.asp b/app/views/Home/landing.asp index e85b868..83e48c5 100644 --- a/app/views/Home/landing.asp +++ b/app/views/Home/landing.asp @@ -30,7 +30,7 @@ Response.CodePage = 65001 Who it is for FAQ - Find your spot + Find your spot @@ -42,9 +42,9 @@ Response.CodePage = 65001
Shared direct mail for local businesses
PostcardMojo puts a curated lineup of local businesses on one oversized 9×12 postcard. Your offer gets a bold, tangible place in nearby homes while participating businesses share the cost of printing and postage.
+PostcardMojo puts a curated lineup of local businesses on one oversized 9×12 postcard and is delivered to 5,000 mailboxes. Your offer gets a bold, tangible place in nearby homes while participating businesses share the cost of printing and postage.
Simple by design
-PostcardMojo turns a group of complementary local businesses into one well-organized, high-visibility mail piece.
+Pick your size, give us your ad, and we handle the printing and mailing.
Start with the local area and timing that fit your business goals.
+Pick a 3×4 or a 6×8 space on the shared 9×12 postcard.
Reserve a space for your type of business, subject to campaign availability.
-Bring your logo, message, and promotion together in a clear, focused ad.
+Already have an ad? Send it over. Need one? We will design it for you.
The finished postcard is printed and mailed as one coordinated campaign.
+We print the postcard and deliver it to mailboxes in your area.
Tell us what you do and where you want to be seen. We will help you explore the right PostcardMojo campaign.
@@ -251,7 +283,7 @@ Response.CodePage = 65001 © <%= Year(Date()) %> PostcardMojo. All rights reserved. diff --git a/applicationhost.config b/applicationhost.config index 8d6b56d..e2214ff 100644 --- a/applicationhost.config +++ b/applicationhost.config @@ -159,7 +159,7 @@