Add initial onboard email

...and patterns for how to do email stuff in the future.
This commit is contained in:
Eli Ribble 2026-01-23 02:50:25 +00:00
parent aa7585563b
commit 44fdaa6c2b
No known key found for this signature in database
10 changed files with 205 additions and 31 deletions

View file

@ -97,8 +97,19 @@ func startWorkerText(ctx context.Context, channel chan jobText) {
}
func jobProcessEmail(job jobEmail) error {
log.Info().Str("dest", job.Destination).Str("type", string(job.Type)).Msg("Pretend doing email job")
return nil
switch job.Type {
case enums.CommsMessagetypeemailInitialContact:
return comms.SendEmailInitialContact(job.Destination)
default:
return errors.New("not implemented")
}
/*
case enums.CommsMessagetypeemailReportSubscriptionConfirmation:
case enums.CommsMessagetypeemailReportStatusScheduled:
case enums.CommsMessagetypeemailReportStatusComplete:
}
*/
}
func jobProcessText(job jobText) error {

View file

@ -13,13 +13,39 @@ import (
"github.com/rs/zerolog/log"
)
func RenderEmailInitial(w http.ResponseWriter, destination string) {
content := newContentEmailInitial(destination)
renderOrError(w, initialT, content)
}
func RenderEmailReportConfirmation(w http.ResponseWriter, report_id string) {
content := contentEmailSubscriptionConfirmation(report_id)
content := newContentEmailSubscriptionConfirmation(report_id)
renderOrError(w, reportConfirmationT, content)
}
func SendEmailInitialContact(destination string) error {
content := newContentEmailInitial(destination)
text, html, err := renderEmailTemplates(reportConfirmationT, content)
if err != nil {
return fmt.Errorf("Failed to render email temlate: %w", err)
}
resp, err := sendEmail(emailRequest{
From: config.ForwardEmailReportAddress,
HTML: html,
Subject: "Welcome",
Text: text,
To: destination,
})
if err != nil {
return fmt.Errorf("Failed to send email to %s: %w", err)
}
log.Info().Str("id", resp.ID).Str("to", destination).Msg("Sent initial contact email")
return nil
}
func SendEmailReportConfirmation(to string, report_id string) error {
report_id_str := publicReportID(report_id)
content := contentEmailSubscriptionConfirmation(report_id)
content := newContentEmailSubscriptionConfirmation(report_id)
text, html, err := renderEmailTemplates(reportConfirmationT, content)
if err != nil {
return fmt.Errorf("Failed to render template %s: %w", reportConfirmationT.name, err)
@ -39,6 +65,7 @@ func SendEmailReportConfirmation(to string, report_id string) error {
}
var (
initialT = buildTemplate("initial")
reportConfirmationT = buildTemplate("report-subscription-confirmation")
)
@ -50,11 +77,20 @@ type attachmentRequest struct {
Content string `json:"content"`
}
type contentEmailBase struct {
URLLogo string
URLUnsubscribe string
URLViewInBrowser string
}
type contentEmailReportConfirmation struct {
URLLogo string
URLReportStatus string
URLReportUnsubscribe string
URLViewInBrowser string
Base contentEmailBase
URLReportStatus string
}
type contentEmailInitial struct {
Base contentEmailBase
Destination string
URLSubscribe string
}
type emailRequest struct {
@ -104,13 +140,28 @@ type emailResponse struct {
Message string `json:"message"`
}
func contentEmailSubscriptionConfirmation(report_id string) contentEmailReportConfirmation {
return contentEmailReportConfirmation{
URLLogo: config.MakeURLReport("/static/img/nidus-logo-no-lettering-64.png"),
URLReportStatus: config.MakeURLReport("/status/%s", report_id),
URLReportUnsubscribe: config.MakeURLReport("/report/%s/unsubscribe", report_id),
URLViewInBrowser: config.MakeURLReport("/email/report/%s/subscription-confirmation", report_id),
}
func newContentBase(b *contentEmailBase, url string) {
b.URLLogo = config.MakeURLReport("/static/img/nidus-logo-no-lettering-64.png")
b.URLUnsubscribe = config.MakeURLReport("/email/unsubscribe")
b.URLViewInBrowser = url
}
func newContentEmailInitial(destination string) (result contentEmailInitial) {
newContentBase(
&result.Base,
config.MakeURLReport("/email/initial"),
)
result.Destination = destination
result.URLSubscribe = config.MakeURLReport("/email/subscribe?email=%s", destination)
return result
}
func newContentEmailSubscriptionConfirmation(report_id string) (result contentEmailReportConfirmation) {
newContentBase(
&result.Base,
config.MakeURLReport("/email/report/%s/subscription-confirmation", report_id),
)
result.URLReportStatus = config.MakeURLReport("/status/%s", report_id)
return result
}
func publicReportID(s string) string {

View file

@ -0,0 +1,97 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Welcome</title>
<style type="text/css">
body {
margin: 0;
padding: 0;
font-family: Arial, sans-serif;
font-size: 16px;
line-height: 1.5;
color: #333333;
}
.container {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.view-browser {
text-align: center;
font-size: 12px;
color: #777777;
padding-bottom: 15px;
}
.view-browser a {
color: #555555;
text-decoration: underline;
}
.header {
text-align: center;
padding: 20px 0;
}
.logo {
max-width: 150px;
height: auto;
}
.content {
background-color: #f9f9f9;
padding: 30px;
border-radius: 5px;
}
.button {
display: inline-block;
background-color: #0066cc;
color: #ffffff;
text-decoration: none;
padding: 12px 25px;
border-radius: 4px;
margin: 20px 0;
font-weight: bold;
}
.footer {
text-align: center;
font-size: 14px;
color: #777777;
padding: 20px 0;
}
.footer a {
color: #777777;
}
</style>
</head>
<body>
<div class="container">
<div class="view-browser">
Email not displaying correctly? <a href="{{.Base.URLViewInBrowser}}">View it in your browser</a>
</div>
<div class="header">
<!-- Logo Placeholder -->
<img src="{{.Base.URLLogo}}" alt="Report Mosquitoes Online Logo" class="logo"></img>
</div>
<div class="content">
<h1>Welcome</h1>
<p>We're sending you this email because it's the first time we've gotten this email address ({{.Destination}}).</p>
<p>If you'd rather not receive emails from us you can reply with "Unsubscribe" in the subject or body of the email. You can also use the "Unsubscribe" feature of your mail client, if it supports list unsubscribes.</p>
<p>If instead you'd like to confirm that you're willing to receive emails at this address, you can do so by clicking below:</p>
<div style="text-align: center;">
<a href="{{.Base.URLSubscribe}}" class="button">I want emails from Report Mosquitoes Online</a>
</div>
</div>
<div class="footer">
<p>This email was sent to you because you or someone else gave your email address to Report Mosquitoes Online.</p>
<p>If you no longer wish to receive these updates, <a href="{{.Base.URLUnsubscribe}}">click here to unsubscribe</a>.</p>
<p>&copy; 2026 Gleipnir LLC. All rights reserved.</p>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1 @@
Welcome to Report Mosquitoes Online.

View file

@ -2,6 +2,7 @@ package config
import (
"fmt"
"net/url"
"os"
"github.com/nyaruka/phonenumbers"
@ -35,18 +36,22 @@ func IsProductionEnvironment() bool {
return Environment == "PRODUCTION"
}
func makeURL(domain, path string, args ...interface{}) string {
func makeURL(domain, path string, args ...string) string {
to_add := make([]any, 0)
for _, a := range args {
to_add = append(to_add, url.QueryEscape(a))
}
pattern := "https://" + domain + path
return fmt.Sprintf(pattern, args...)
return fmt.Sprintf(pattern, to_add...)
}
func MakeURLNidus(path string, args ...interface{}) string {
func MakeURLNidus(path string, args ...string) string {
return makeURL(DomainNidus, path, args...)
}
func MakeURLReport(path string, args ...interface{}) string {
func MakeURLReport(path string, args ...string) string {
return makeURL(DomainRMO, path, args...)
}
func MakeURLTegola(path string, args ...interface{}) string {
func MakeURLTegola(path string, args ...string) string {
return makeURL(DomainTegola, path, args...)
}

17
public-report/email.go Normal file
View file

@ -0,0 +1,17 @@
package publicreport
import (
"net/http"
"github.com/Gleipnir-Technology/nidus-sync/comms"
"github.com/go-chi/chi/v5"
)
func getEmailInitial(w http.ResponseWriter, r *http.Request) {
email := chi.URLParam(r, "email")
comms.RenderEmailInitial(w, email)
}
func getEmailReportSubscriptionConfirmation(w http.ResponseWriter, r *http.Request) {
report_id := chi.URLParam(r, "report_id")
comms.RenderEmailReportConfirmation(w, report_id)
}

View file

@ -75,7 +75,7 @@ func getQuickSubmitComplete(w http.ResponseWriter, r *http.Request) {
log.Debug().Int32("org_id", org.ID).Int32("d_gid", d.Gid).Msg("Getting district")
if d != nil {
district = &District{
LogoURL: config.MakeURLNidus("/api/district/%d/logo", org_id),
LogoURL: config.MakeURLNidus("/api/district/%s/logo", strconv.Itoa(int(org_id))),
Name: d.Agency.GetOr("Unknown"),
}
}

View file

@ -4,11 +4,7 @@ import (
"crypto/rand"
"fmt"
"math/big"
"net/http"
"strings"
"github.com/Gleipnir-Technology/nidus-sync/comms"
"github.com/go-chi/chi/v5"
)
// GenerateReportID creates a 12-character random string using only unambiguous
@ -35,8 +31,3 @@ func GenerateReportID() (string, error) {
return builder.String(), nil
}
func getEmailReportSubscriptionConfirmation(w http.ResponseWriter, r *http.Request) {
report_id := chi.URLParam(r, "report_id")
comms.RenderEmailReportConfirmation(w, report_id)
}

View file

@ -10,6 +10,7 @@ func Router() chi.Router {
r.Get("/", getRoot)
r.Get("/privacy", getPrivacy)
r.Get("/robots.txt", getRobots)
r.Get("/email/initial", getEmailInitial)
r.Get("/email/report/{report_id}/subscription-confirmation", getEmailReportSubscriptionConfirmation)
r.Get("/image/{uuid}", getImageByUUID)
r.Get("/nuisance", getNuisance)

View file

@ -185,7 +185,7 @@ func contentFromQuick(ctx context.Context, report_id string) (result ContentStat
for _, image := range images {
result.Report.Images = append(result.Report.Images, Image{
Location: image.LocationJSON,
URL: config.MakeURLReport("/image/%s", image.StorageUUID),
URL: config.MakeURLReport("/image/%s", image.StorageUUID.String()),
})
}
type LocationGeoJSON struct {