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
|
|
@ -1,129 +0,0 @@
|
|||
package email
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
func convertToPGData(data map[string]string) pgtypes.HStore {
|
||||
result := pgtypes.HStore{}
|
||||
for k, v := range data {
|
||||
result[k] = sql.Null[string]{V: v, Valid: true}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func convertFromPGData(d pgtypes.HStore) map[string]string {
|
||||
result := make(map[string]string, 0)
|
||||
for k, v := range d {
|
||||
value, err := v.Value()
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("key", k).Msg("Failed to convert from HSTORE")
|
||||
continue
|
||||
}
|
||||
value_str, ok := value.(string)
|
||||
if !ok {
|
||||
log.Warn().Msg("Failed to convert to string")
|
||||
}
|
||||
result[k] = value_str
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func ensureInDB(ctx context.Context, destination string) (err error) {
|
||||
_, err = models.FindCommsEmailContact(ctx, db.PGInstance.BobDB, destination)
|
||||
if err != nil {
|
||||
// doesn't exist
|
||||
if err.Error() == "sql: no rows in result set" {
|
||||
public_id := fmt.Sprintf("%x", sha256.Sum256([]byte(destination)))
|
||||
_, err = models.CommsEmailContacts.Insert(&models.CommsEmailContactSetter{
|
||||
Address: omit.From(destination),
|
||||
Confirmed: omit.From(false),
|
||||
IsSubscribed: omit.From(false),
|
||||
PublicID: omit.From(public_id),
|
||||
}).One(ctx, db.PGInstance.BobDB)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to insert new email: %w", err)
|
||||
}
|
||||
log.Info().Str("email", destination).Msg("Added email to the comms database")
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("Unexpected error searching for contact: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func insertEmailLog(ctx context.Context, data map[string]string, destination string, public_id string, source string, subject string, template_id int32) (err error) {
|
||||
data_for_insert := convertToPGData(data)
|
||||
var type_ enums.CommsMessagetypeemail
|
||||
switch template_id {
|
||||
case templateReportNotificationConfirmationID:
|
||||
type_ = enums.CommsMessagetypeemailReportNotificationConfirmation
|
||||
case templateInitialID:
|
||||
type_ = enums.CommsMessagetypeemailInitialContact
|
||||
default:
|
||||
return fmt.Errorf("Unrecognized template ID %d", template_id)
|
||||
}
|
||||
_, err = models.CommsEmailLogs.Insert(&models.CommsEmailLogSetter{
|
||||
//ID:
|
||||
Created: omit.From(time.Now()),
|
||||
DeliveryStatus: omit.From("initial"),
|
||||
Destination: omit.From(destination),
|
||||
PublicID: omit.From(public_id),
|
||||
SentAt: omitnull.FromPtr[time.Time](nil),
|
||||
Source: omit.From(source),
|
||||
Subject: omit.From(subject),
|
||||
TemplateID: omit.From(template_id),
|
||||
TemplateData: omit.From(data_for_insert),
|
||||
Type: omit.From(type_),
|
||||
}).One(ctx, db.PGInstance.BobDB)
|
||||
|
||||
return err
|
||||
}
|
||||
func generatePublicId(t enums.CommsMessagetypeemail, m map[string]string) string {
|
||||
if m == nil || len(m) == 0 {
|
||||
// Return hash of empty string for empty maps
|
||||
emptyHash := sha256.Sum256([]byte(""))
|
||||
return hex.EncodeToString(emptyHash[:])
|
||||
}
|
||||
|
||||
// Get and sort keys for deterministic ordering
|
||||
keys := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
// Build a string with all key-value pairs
|
||||
var sb strings.Builder
|
||||
// Add type first
|
||||
sb.WriteString(fmt.Sprintf("type:%s,", t))
|
||||
for _, k := range keys {
|
||||
sb.WriteString(k)
|
||||
sb.WriteString(":") // Separator between key and value
|
||||
sb.WriteString(m[k])
|
||||
sb.WriteString(",") // Separator between pairs
|
||||
}
|
||||
|
||||
// Compute SHA-256 hash
|
||||
hasher := sha256.New()
|
||||
hasher.Write([]byte(sb.String()))
|
||||
hashBytes := hasher.Sum(nil)
|
||||
|
||||
// Convert to hex string and return
|
||||
return hex.EncodeToString(hashBytes)
|
||||
}
|
||||
|
|
@ -18,23 +18,7 @@ type attachmentRequest struct {
|
|||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type contentEmailBase struct {
|
||||
URLLogo string
|
||||
URLUnsubscribe string
|
||||
URLViewInBrowser string
|
||||
}
|
||||
|
||||
type contentEmailReportConfirmation struct {
|
||||
Base contentEmailBase
|
||||
URLReportStatus string
|
||||
}
|
||||
type contentEmailInitial struct {
|
||||
Base contentEmailBase
|
||||
Destination string
|
||||
URLSubscribe string
|
||||
}
|
||||
|
||||
type emailRequest struct {
|
||||
type Request struct {
|
||||
From string `json:"from"`
|
||||
To string `json:"to"`
|
||||
CC []string `json:"cc,omitempty"`
|
||||
|
|
@ -83,7 +67,7 @@ type emailResponse struct {
|
|||
|
||||
var FORWARDEMAIL_API = "https://api.forwardemail.net/v1/emails"
|
||||
|
||||
func sendEmail(ctx context.Context, email emailRequest, t enums.CommsMessagetypeemail) (response emailResponse, err error) {
|
||||
func Send(ctx context.Context, email Request, t enums.CommsMessagetypeemail) (response emailResponse, err error) {
|
||||
payload, err := json.Marshal(email)
|
||||
if err != nil {
|
||||
return response, fmt.Errorf("Failed to marshal email request: %w", err)
|
||||
|
|
|
|||
|
|
@ -1,81 +0,0 @@
|
|||
package email
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"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 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 := sendEmail(ctx, emailRequest{
|
||||
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
|
||||
}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
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
|
||||
}
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
package email
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/Gleipnir-Technology/nidus-sync/config"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/db/enums"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
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 := sendEmail(ctx, emailRequest{
|
||||
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
|
||||
}
|
||||
|
|
@ -1,352 +0,0 @@
|
|||
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 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
|
||||
}
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
<!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>
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
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
|
||||
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
<!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>
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
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