Move emails to platform, make sure to create phone and email in DB
This commit is contained in:
parent
dd33c6ab5e
commit
648e0ee567
17 changed files with 203 additions and 281 deletions
88
platform/email/initial.go
Normal file
88
platform/email/initial.go
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
package email
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/Gleipnir-Technology/nidus-sync/comms/email"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/config"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/db"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/db/enums"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/db/models"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type contentEmailInitial struct {
|
||||
Base contentEmailBase
|
||||
Destination string
|
||||
URLSubscribe string
|
||||
}
|
||||
|
||||
type jobInitial struct {
|
||||
base jobEmailBase
|
||||
}
|
||||
|
||||
func (job jobInitial) Destination() string {
|
||||
return job.base.destination
|
||||
}
|
||||
|
||||
func maybeSendInitialEmail(ctx context.Context, destination string) error {
|
||||
err := EnsureInDB(ctx, destination)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to add email recipient to database: %w", err)
|
||||
}
|
||||
rows, err := models.CommsEmailLogs.Query(
|
||||
models.SelectWhere.CommsEmailLogs.Destination.EQ(destination),
|
||||
models.SelectWhere.CommsEmailLogs.TemplateID.EQ(templateInitialID),
|
||||
).All(ctx, db.PGInstance.BobDB)
|
||||
|
||||
// We already sent an initial email
|
||||
if len(rows) > 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return sendEmailInitialContact(ctx, destination)
|
||||
}
|
||||
func urlEmailInBrowser(public_id string) string {
|
||||
return config.MakeURLReport("/email/render/%s", public_id)
|
||||
}
|
||||
func urlUnsubscribe(email string) string {
|
||||
return config.MakeURLReport("/email/unsubscribe?email=%s", email)
|
||||
}
|
||||
func sendEmailInitialContact(ctx context.Context, destination string) error {
|
||||
//data := pgtypes.HStore{}
|
||||
data := make(map[string]string, 0)
|
||||
source := config.ForwardEmailReportAddress
|
||||
data["Destination"] = destination
|
||||
data["Source"] = source
|
||||
data["URLLogo"] = config.MakeURLReport("/static/img/nidus-logo-no-lettering-64.png")
|
||||
data["URLSubscribe"] = config.MakeURLReport("/email/confirm?email=%s", destination)
|
||||
data["URLUnsubscribe"] = urlUnsubscribe(destination)
|
||||
|
||||
public_id := generatePublicId(enums.CommsMessagetypeemailInitialContact, data)
|
||||
data["URLBrowser"] = urlEmailInBrowser(public_id)
|
||||
|
||||
text, html, err := renderEmailTemplates(templateInitialID, data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to render email temlates: %w", err)
|
||||
}
|
||||
|
||||
subject := "Welcome"
|
||||
err = insertEmailLog(ctx, data, destination, public_id, source, subject, templateInitialID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to store email log: %w", err)
|
||||
}
|
||||
resp, err := email.Send(ctx, email.Request{
|
||||
From: source,
|
||||
HTML: html,
|
||||
Subject: subject,
|
||||
Text: text,
|
||||
To: destination,
|
||||
}, enums.CommsMessagetypeemailInitialContact)
|
||||
|
||||
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
|
||||
}
|
||||
41
platform/email/job.go
Normal file
41
platform/email/job.go
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
package email
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/Gleipnir-Technology/nidus-sync/db/enums"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type Job interface {
|
||||
destination() string
|
||||
messageType() enums.CommsMessagetypeemail
|
||||
renderHTML() (string, error)
|
||||
renderTXT() (string, error)
|
||||
subject() string
|
||||
}
|
||||
|
||||
type jobEmailBase struct {
|
||||
destination string
|
||||
source string
|
||||
}
|
||||
|
||||
func Handle(ctx context.Context, job Job) error {
|
||||
var err error
|
||||
log.Debug().Str("dest", job.destination()).Str("type", string(job.messageType())).Msg("Handling email job")
|
||||
switch job.messageType() {
|
||||
case enums.CommsMessagetypeemailReportSubscriptionConfirmation:
|
||||
return errors.New("ReportSubscription has been deprecated.")
|
||||
case enums.CommsMessagetypeemailReportNotificationConfirmation:
|
||||
err = sendEmailReportConfirmation(ctx, job)
|
||||
default:
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("dest", job.destination()).Str("type", string(job.messageType())).Msg("Error processing email")
|
||||
return fmt.Errorf("Failed to handle email: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
94
platform/email/report_notification_confirmation.go
Normal file
94
platform/email/report_notification_confirmation.go
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
package email
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/Gleipnir-Technology/nidus-sync/comms/email"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/config"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/db/enums"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type contentEmailReportConfirmation struct {
|
||||
Base contentEmailBase
|
||||
URLReportStatus string
|
||||
}
|
||||
|
||||
func NewJobReportNotificationConfirmation(destination, report_id string) Job {
|
||||
return jobEmailReportNotificationConfirmation{
|
||||
dest: destination,
|
||||
reportID: report_id,
|
||||
}
|
||||
}
|
||||
|
||||
type jobEmailReportNotificationConfirmation struct {
|
||||
dest string
|
||||
reportID string
|
||||
}
|
||||
|
||||
func (job jobEmailReportNotificationConfirmation) destination() string {
|
||||
return job.dest
|
||||
}
|
||||
func (job jobEmailReportNotificationConfirmation) messageType() enums.CommsMessagetypeemail {
|
||||
return enums.CommsMessagetypeemailReportNotificationConfirmation
|
||||
}
|
||||
func (job jobEmailReportNotificationConfirmation) renderHTML() (string, error) {
|
||||
_ = newContentEmailNotificationConfirmation(job)
|
||||
return "", nil
|
||||
}
|
||||
func (job jobEmailReportNotificationConfirmation) renderTXT() (string, error) {
|
||||
return "fake txt", nil
|
||||
}
|
||||
func (job jobEmailReportNotificationConfirmation) subject() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func sendEmailReportConfirmation(ctx context.Context, job Job) error {
|
||||
j, ok := job.(jobEmailReportNotificationConfirmation)
|
||||
if !ok {
|
||||
return fmt.Errorf("job is not for report subscription confirmation")
|
||||
}
|
||||
err := maybeSendInitialEmail(ctx, j.destination())
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to handle initial email: %w", err)
|
||||
}
|
||||
data := make(map[string]string, 0)
|
||||
data["report_id"] = j.reportID
|
||||
report_id_str := publicReportID(j.reportID)
|
||||
data["ReportIDStr"] = report_id_str
|
||||
data["URLLogo"] = config.MakeURLReport("/static/img/nidus-logo-no-lettering-64.png")
|
||||
data["URLReportStatus"] = config.MakeURLReport("/status/%s", j.reportID)
|
||||
data["URLReportUnsubscribe"] = config.MakeURLReport("/email/unsubscribe/report/%s", j.reportID)
|
||||
data["URLUnsubscribe"] = urlUnsubscribe(j.destination())
|
||||
|
||||
public_id := generatePublicId(enums.CommsMessagetypeemailReportNotificationConfirmation, data)
|
||||
data["URLViewInBrowser"] = urlEmailInBrowser(public_id)
|
||||
|
||||
text, html, err := renderEmailTemplates(templateReportNotificationConfirmationID, data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to render email report notification template: %w", err)
|
||||
}
|
||||
subject := fmt.Sprintf("Mosquito Report Submission - %s", report_id_str)
|
||||
err = insertEmailLog(ctx, data, j.destination(), public_id, config.ForwardEmailReportAddress, subject, templateReportNotificationConfirmationID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to store email log: %w", err)
|
||||
}
|
||||
resp, err := email.Send(ctx, email.Request{
|
||||
From: config.ForwardEmailReportAddress,
|
||||
HTML: html,
|
||||
Subject: subject,
|
||||
Text: text,
|
||||
To: j.destination(),
|
||||
}, enums.CommsMessagetypeemailReportNotificationConfirmation)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to send email report confirmation to %s for report %s: %w", j.dest, j.reportID, err)
|
||||
}
|
||||
log.Info().Str("id", resp.ID).Str("dest", j.dest).Str("report_id", j.reportID).Msg("Sent report confirmation email")
|
||||
return nil
|
||||
}
|
||||
|
||||
func newContentEmailNotificationConfirmation(job jobEmailReportNotificationConfirmation) (result contentEmailReportConfirmation) {
|
||||
result.URLReportStatus = config.MakeURLReport("/status/%s", job.reportID)
|
||||
return result
|
||||
}
|
||||
358
platform/email/template.go
Normal file
358
platform/email/template.go
Normal file
|
|
@ -0,0 +1,358 @@
|
|||
package email
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
templatehtml "html/template"
|
||||
"io"
|
||||
"io/fs"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
templatetxt "text/template"
|
||||
"time"
|
||||
|
||||
"github.com/Gleipnir-Technology/bob"
|
||||
"github.com/Gleipnir-Technology/bob/dialect/psql"
|
||||
"github.com/Gleipnir-Technology/bob/dialect/psql/um"
|
||||
"github.com/Gleipnir-Technology/bob/types/pgtypes"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/db"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/db/enums"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/db/models"
|
||||
"github.com/aarondl/opt/omit"
|
||||
"github.com/aarondl/opt/omitnull"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
//go:embed template/*
|
||||
var embeddedFiles embed.FS
|
||||
|
||||
var (
|
||||
templateByID map[int32]*builtTemplate
|
||||
templateInitialID int32
|
||||
templateReportNotificationConfirmationID int32
|
||||
)
|
||||
|
||||
type contentEmailBase struct {
|
||||
URLLogo string
|
||||
URLUnsubscribe string
|
||||
URLViewInBrowser string
|
||||
}
|
||||
|
||||
type ContentEmailRender struct {
|
||||
IsBrowser bool
|
||||
C any
|
||||
}
|
||||
|
||||
type templatePair struct {
|
||||
baseName string
|
||||
messageType enums.CommsMessagetypeemail
|
||||
htmlContent string
|
||||
txtContent string
|
||||
htmlHash string
|
||||
txtHash string
|
||||
}
|
||||
|
||||
func LoadTemplates() error {
|
||||
all_templates, err := readTemplates(embeddedFiles)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to read templates: %w", err)
|
||||
}
|
||||
ctx := context.TODO()
|
||||
tx, err := db.PGInstance.BobDB.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to start transaction: %w", err)
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
templateByID = make(map[int32]*builtTemplate, 0)
|
||||
for name, p := range all_templates {
|
||||
template_id, err := templateDBID(tx, name, p)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to add '%s' to DB: %w", name, err)
|
||||
}
|
||||
template_html, err := templatehtml.New(name).Parse(p.htmlContent)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to parse HTML portion of '%s': %w", name, err)
|
||||
}
|
||||
template_txt, err := templatetxt.New(name).Parse(p.txtContent)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to parse HTML portion of '%s': %w", name, err)
|
||||
}
|
||||
built := builtTemplate{
|
||||
name: name,
|
||||
templateHTML: template_html,
|
||||
templateTXT: template_txt,
|
||||
}
|
||||
templateByID[template_id] = &built
|
||||
log.Info().Int32("id", template_id).Str("name", name).Msg("Added template to cache")
|
||||
}
|
||||
templateInitialID, err = loadTemplateID(ctx, tx, enums.CommsMessagetypeemailInitialContact)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to load template ID: %s", err)
|
||||
}
|
||||
templateReportNotificationConfirmationID, err = loadTemplateID(ctx, tx, enums.CommsMessagetypeemailReportNotificationConfirmation)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to load report-notification-confirmation template ID: %s", err)
|
||||
}
|
||||
tx.Commit(ctx)
|
||||
return nil
|
||||
}
|
||||
|
||||
func RenderHTML(template_id int32, s pgtypes.HStore) (html []byte, err error) {
|
||||
data := convertFromPGData(s)
|
||||
t, ok := templateByID[template_id]
|
||||
if !ok {
|
||||
return []byte{}, fmt.Errorf("Failed to lookup template %d", template_id)
|
||||
}
|
||||
buf_html := &bytes.Buffer{}
|
||||
content := ContentEmailRender{
|
||||
C: data,
|
||||
IsBrowser: true,
|
||||
}
|
||||
err = t.executeTemplateHTML(buf_html, content)
|
||||
if err != nil {
|
||||
return []byte{}, fmt.Errorf("Failed to render HTML template: %w", err)
|
||||
}
|
||||
return buf_html.Bytes(), nil
|
||||
}
|
||||
|
||||
func loadTemplateID(ctx context.Context, tx bob.Tx, t enums.CommsMessagetypeemail) (int32, error) {
|
||||
templates, err := models.CommsEmailTemplates.Query(
|
||||
models.SelectWhere.CommsEmailTemplates.MessageType.EQ(t),
|
||||
models.SelectWhere.CommsEmailTemplates.Superceded.IsNull(),
|
||||
).All(ctx, tx)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("Failed to query template '%s': %w", t, err)
|
||||
}
|
||||
switch len(templates) {
|
||||
case 0:
|
||||
return 0, fmt.Errorf("No matching templates for '%s", t)
|
||||
case 1:
|
||||
return templates[0].ID, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("Found %d templates for '%s', should only have 1", len(templates), t)
|
||||
}
|
||||
}
|
||||
|
||||
func readTemplates(filesystem embed.FS) (results map[string]*templatePair, err error) {
|
||||
// First pass: read files and organize by base name
|
||||
results = make(map[string]*templatePair)
|
||||
|
||||
err = fs.WalkDir(filesystem, ".", func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Read file content
|
||||
content, err := filesystem.ReadFile(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading template %s: %w", path, err)
|
||||
}
|
||||
|
||||
// Calculate hash
|
||||
hash := fmt.Sprintf("%x", sha256.Sum256(content))
|
||||
|
||||
// Extract base name and extension
|
||||
ext := strings.ToLower(filepath.Ext(path))
|
||||
baseName := strings.TrimSuffix(filepath.Base(path), ext)
|
||||
|
||||
// Store in map by base name
|
||||
if _, exists := results[baseName]; !exists {
|
||||
t, err := messageTypeFromName(baseName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Cannot parse email templates: %w", err)
|
||||
}
|
||||
results[baseName] = &templatePair{
|
||||
baseName: baseName,
|
||||
messageType: *t,
|
||||
}
|
||||
}
|
||||
|
||||
// Add content based on extension
|
||||
switch ext {
|
||||
case ".html", ".htm":
|
||||
results[baseName].htmlContent = string(content)
|
||||
results[baseName].htmlHash = hash
|
||||
case ".txt":
|
||||
results[baseName].txtContent = string(content)
|
||||
results[baseName].txtHash = hash
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return results, fmt.Errorf("error walking template directory: %w", err)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func templateDBID(tx bob.Tx, name string, pair *templatePair) (int32, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Skip incomplete pairs
|
||||
if pair.htmlContent == "" {
|
||||
return 0, fmt.Errorf("Bad template pair '%s': no html content")
|
||||
}
|
||||
if pair.txtContent == "" {
|
||||
return 0, fmt.Errorf("Bad template pair '%s': no txt content")
|
||||
}
|
||||
|
||||
// Check if a template with these hashes already exists
|
||||
rows, err := models.CommsEmailTemplates.Query(
|
||||
models.SelectWhere.CommsEmailTemplates.ContentHashHTML.EQ(pair.htmlHash),
|
||||
models.SelectWhere.CommsEmailTemplates.ContentHashTXT.EQ(pair.txtHash),
|
||||
models.SelectWhere.CommsEmailTemplates.MessageType.EQ(pair.messageType),
|
||||
).All(ctx, tx)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("Failed to query for existing template: %w", err)
|
||||
}
|
||||
if len(rows) > 1 {
|
||||
return 0, fmt.Errorf("Got %d template rows, should only have 1", len(rows))
|
||||
} else if len(rows) == 1 {
|
||||
return rows[0].ID, nil
|
||||
}
|
||||
|
||||
// Supercede previous templates of this type
|
||||
_, err = psql.Update(
|
||||
um.Table(models.CommsEmailTemplates.Alias()),
|
||||
um.SetCol("superceded").ToArg(time.Now()),
|
||||
//um.Where(models.CommsEmailTemplates.Columns.MessageType.EQ(psql.Arg(pair.messageType))),
|
||||
um.Where(psql.Quote("message_type").EQ(psql.Arg(pair.messageType))),
|
||||
//um.Where(models.CommsEmailTemplates.Columns.Superceded.IsNull()),
|
||||
um.Where(psql.Quote("superceded").IsNull()),
|
||||
).Exec(ctx, tx)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("error superceding templates: %w", err)
|
||||
}
|
||||
|
||||
new_template, err := models.CommsEmailTemplates.Insert(&models.CommsEmailTemplateSetter{
|
||||
ContentHTML: omit.From(pair.htmlContent),
|
||||
ContentTXT: omit.From(pair.txtContent),
|
||||
ContentHashHTML: omit.From(pair.htmlHash),
|
||||
ContentHashTXT: omit.From(pair.txtHash),
|
||||
Created: omit.From(time.Now()),
|
||||
Superceded: omitnull.FromPtr[time.Time](nil),
|
||||
MessageType: omit.From(pair.messageType),
|
||||
}).One(ctx, tx)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("Failed to insert new template: %w", err)
|
||||
}
|
||||
log.Info().Int32("id", new_template.ID).Str("type", string(pair.messageType)).Msg("Added new email template")
|
||||
|
||||
return new_template.ID, nil
|
||||
}
|
||||
|
||||
type builtTemplate struct {
|
||||
name string
|
||||
templateHTML *templatehtml.Template
|
||||
templateTXT *templatetxt.Template
|
||||
}
|
||||
|
||||
func (bt *builtTemplate) executeTemplateHTML(w io.Writer, content any) error {
|
||||
if bt.templateHTML == nil {
|
||||
file := templateFileHTML(bt.name)
|
||||
templ, err := parseFromDiskHTML(file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to parse template file: %w", err)
|
||||
}
|
||||
if templ == nil {
|
||||
w.Write([]byte("Failed to read from disk: "))
|
||||
return errors.New("Template parsing failed")
|
||||
}
|
||||
//log.Debug().Str("name", templ.Name()).Msg("Parsed template")
|
||||
return templ.ExecuteTemplate(w, bt.name, content)
|
||||
} else {
|
||||
return bt.templateHTML.ExecuteTemplate(w, bt.name, content)
|
||||
}
|
||||
}
|
||||
func (bt *builtTemplate) executeTemplateTXT(w io.Writer, content any) error {
|
||||
if bt.templateTXT == nil {
|
||||
file := templateFileTXT(bt.name)
|
||||
templ, err := parseFromDiskTXT(file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to parse template file: %w", err)
|
||||
}
|
||||
if templ == nil {
|
||||
w.Write([]byte("Failed to read from disk: "))
|
||||
return errors.New("Template parsing failed")
|
||||
}
|
||||
//log.Debug().Str("name", templ.Name()).Msg("Parsed template")
|
||||
return templ.ExecuteTemplate(w, bt.name, content)
|
||||
} else {
|
||||
return bt.templateTXT.ExecuteTemplate(w, bt.name, content)
|
||||
}
|
||||
}
|
||||
func templateFileHTML(name string) string {
|
||||
return fmt.Sprintf("comms/template/%s.html", name)
|
||||
}
|
||||
func templateFileTXT(name string) string {
|
||||
return fmt.Sprintf("comms/template/%s.txt", name)
|
||||
}
|
||||
|
||||
func messageTypeFromName(n string) (*enums.CommsMessagetypeemail, error) {
|
||||
for _, t := range enums.AllCommsMessagetypeemail() {
|
||||
if n == string(t) {
|
||||
return &t, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("Unrecognized email type '%s'", n)
|
||||
}
|
||||
|
||||
func parseFromDiskHTML(file string) (*templatehtml.Template, error) {
|
||||
name := path.Base(file)
|
||||
//log.Debug().Str("name", name).Strs("files", files).Msg("parsing from disk")
|
||||
templ, err := templatehtml.New(name).ParseFiles(file)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to parse %s: %w", file, err)
|
||||
}
|
||||
return templ, nil
|
||||
}
|
||||
|
||||
func parseFromDiskTXT(file string) (*templatetxt.Template, error) {
|
||||
name := path.Base(file)
|
||||
//log.Debug().Str("name", name).Strs("files", files).Msg("parsing from disk")
|
||||
templ, err := templatetxt.New(name).ParseFiles(file)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to parse %s: %w", file, err)
|
||||
}
|
||||
return templ, nil
|
||||
}
|
||||
|
||||
func publicReportID(s string) string {
|
||||
if len(s) != 12 {
|
||||
return s
|
||||
}
|
||||
return s[0:4] + "-" + s[4:8] + "-" + s[8:12]
|
||||
}
|
||||
|
||||
func renderEmailTemplates(template_id int32, data map[string]string) (text string, html string, err error) {
|
||||
buf_txt := &bytes.Buffer{}
|
||||
t, ok := templateByID[template_id]
|
||||
if !ok {
|
||||
return "", "", fmt.Errorf("Failed to lookup template %d", template_id)
|
||||
}
|
||||
content := ContentEmailRender{
|
||||
C: data,
|
||||
IsBrowser: false,
|
||||
}
|
||||
err = t.executeTemplateTXT(buf_txt, content)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("Failed to render TXT template: %w", err)
|
||||
}
|
||||
buf_html := &bytes.Buffer{}
|
||||
err = t.executeTemplateHTML(buf_html, content)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("Failed to render HTML template: %w", err)
|
||||
}
|
||||
return buf_txt.String(), buf_html.String(), nil
|
||||
}
|
||||
124
platform/email/template/initial-contact.html
Normal file
124
platform/email/template/initial-contact.html
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
<!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">
|
||||
{{ if not .IsBrowser }}
|
||||
<div class="view-browser">
|
||||
Email not displaying correctly?
|
||||
<a href="{{ .C.URLBrowser }}">View it in your browser</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
|
||||
<div class="header">
|
||||
<!-- Logo Placeholder -->
|
||||
<img
|
||||
src="{{ .C.URLLogo }}"
|
||||
alt="Report Mosquitoes Online Logo"
|
||||
class="logo"
|
||||
/>
|
||||
</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 ({{ .C.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="{{ .C.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="{{ .C.URLUnsubscribe }}">click here to unsubscribe</a>.
|
||||
</p>
|
||||
<p>© 2026 Gleipnir LLC. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
7
platform/email/template/initial-contact.txt
Normal file
7
platform/email/template/initial-contact.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
We're sending you this email because it's the first time we've gotten this email address ({{.C.Destination}}).
|
||||
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.
|
||||
If instead you'd like to confirm that you're willing to receive emails at this address, you can do so by openining the following URL in a web browser: {{.C.URLSubscribe}}. You can also confirm your willingness by replying to this email with 'Confirm' in the subject on the body of the email.
|
||||
|
||||
Thank you,
|
||||
Report Mosquitoes Online
|
||||
|
||||
130
platform/email/template/report-notification-confirmation.html
Normal file
130
platform/email/template/report-notification-confirmation.html
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Thank You for Your Mosquito Report</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">
|
||||
{{ if not .IsBrowser }}
|
||||
<div class="view-browser">
|
||||
Email not displaying correctly?
|
||||
<a href="{{ .C.URLViewInBrowser }}">View it in your browser</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
|
||||
<div class="header">
|
||||
<!-- Logo Placeholder -->
|
||||
<img
|
||||
src="{{ .C.URLLogo }}"
|
||||
alt="Report Mosquitoes Online Logo"
|
||||
class="logo"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<h1>Thank You for Your Report</h1>
|
||||
|
||||
<p>
|
||||
We've received your mosquito report {{ .C.ReportIDStr }}. Thanks! We
|
||||
appreciate you taking the time to submit it.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
You can check the current status of your report at any time by
|
||||
clicking the button below:
|
||||
</p>
|
||||
|
||||
<div style="text-align: center;">
|
||||
<a href="{{ .C.URLReportStatus }}" class="button"
|
||||
>View Report Status</a
|
||||
>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
We'll send you additional updates as work is scheduled and completed.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If you have any questions or need further assistance, please don't
|
||||
hesitate to contact us by replying to this email.
|
||||
</p>
|
||||
<p>
|
||||
You can unsubscribe from notifications about this report by clicking
|
||||
<a hrep="{{ .C.URLReportUnsubscribe }}">here</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>
|
||||
This email was sent to you because you requested updates on your
|
||||
mosquito nuisance report.
|
||||
</p>
|
||||
<p>
|
||||
If you no longer wish to receive these updates,
|
||||
<a href="{{ .C.URLReportUnsubscribe }}">click here to unsubscribe</a>.
|
||||
</p>
|
||||
<p>© 2026 Gleipnir LLC. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
We've received your mosquito report. Thanks! We appreciate you taking the time to submit it.
|
||||
|
||||
You can check the current status of your report at any time at {{.C.URLReportStatus}}
|
||||
|
||||
We'll send you additional updates as work is scheduled and completed.
|
||||
|
||||
If you have any questions or need further assistance, please don't hesitate to contact us by replying to this email.
|
||||
|
||||
If you no longer wish to receive these updates, navigate your browser to {{.C.URLReportUnsubscribe}} to unsubscribe.
|
||||
Loading…
Add table
Add a link
Reference in a new issue