Overhaul email sending system
Add logging and saving templates to the database for historical accuracy.
This commit is contained in:
parent
3fed489258
commit
196792810b
44 changed files with 4846 additions and 2361 deletions
245
comms/email.go
245
comms/email.go
|
|
@ -1,245 +0,0 @@
|
|||
package comms
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"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/Gleipnir-Technology/nidus-sync/htmlpage"
|
||||
"github.com/aarondl/opt/omit"
|
||||
"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 := newContentEmailSubscriptionConfirmation(report_id)
|
||||
renderOrError(w, reportConfirmationT, content)
|
||||
}
|
||||
|
||||
func SendEmailInitialContact(ctx context.Context, 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(ctx, emailRequest{
|
||||
From: config.ForwardEmailReportAddress,
|
||||
HTML: html,
|
||||
Subject: "Welcome",
|
||||
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
|
||||
}
|
||||
|
||||
func SendEmailReportConfirmation(ctx context.Context, to string, report_id string) error {
|
||||
report_id_str := publicReportID(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)
|
||||
}
|
||||
resp, err := sendEmail(ctx, emailRequest{
|
||||
From: config.ForwardEmailReportAddress,
|
||||
HTML: html,
|
||||
Subject: fmt.Sprintf("Mosquito Report Submission - %s", report_id_str),
|
||||
Text: text,
|
||||
To: to,
|
||||
}, enums.CommsMessagetypeemailReportSubscriptionConfirmation)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to send email report confirmation to %s for report %s: %w", to, report_id, err)
|
||||
}
|
||||
log.Info().Str("id", resp.ID).Str("to", to).Str("report_id", report_id).Msg("Sent report confirmation email")
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
initialT = buildTemplate("initial")
|
||||
reportConfirmationT = buildTemplate("report-subscription-confirmation")
|
||||
)
|
||||
|
||||
//go:embed template/*
|
||||
var embeddedFiles embed.FS
|
||||
|
||||
type attachmentRequest struct {
|
||||
Filename string `json:"filename"`
|
||||
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 {
|
||||
From string `json:"from"`
|
||||
To string `json:"to"`
|
||||
CC []string `json:"cc,omitempty"`
|
||||
BCC []string `json:"bcc,omitempty"`
|
||||
Subject string `json:"subject"`
|
||||
Text string `json:"text"`
|
||||
HTML string `json:"html,omitempty"`
|
||||
Attachments []attachmentRequest `json:"attachments,omitempty"`
|
||||
Sender string `json:"sender"`
|
||||
ReplyTo string `json:"replyTo,omitempty"`
|
||||
InReplyTo string `json:"inReplyTo,omitempty"`
|
||||
References []string `json:"references,omitempty"`
|
||||
}
|
||||
|
||||
type emailEnvelope struct {
|
||||
From string `json:"from"`
|
||||
To []string `json:"to"`
|
||||
}
|
||||
|
||||
type emailResponse struct {
|
||||
IsRedacted bool `json:"is_redacted"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
HardBounces []string `json:"hard_bounces"`
|
||||
SoftBounces []string `json:"soft_bounces"`
|
||||
IsBounce bool `json:"is_bounce"`
|
||||
Alias string `json:"alias"`
|
||||
Domain string `json:"domain"`
|
||||
User string `json:"user"`
|
||||
Status string `json:"status"`
|
||||
IsLocked bool `json:"is_locked"`
|
||||
Envelope emailEnvelope `json:"envelope"`
|
||||
RequireTLS bool `json:"requireTLS"`
|
||||
MessageID string `json:"messageId"`
|
||||
Headers map[string]string `json:"headers"`
|
||||
Date string `json:"date"`
|
||||
Subject string `json:"subject"`
|
||||
Accepted []string `json:"accepted"`
|
||||
Deliveries []string `json:"deliveries"`
|
||||
RejectedErrors []string `json:"rejectedErrors"`
|
||||
ID string `json:"id"`
|
||||
Object string `json:"object"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
Link string `json:"link"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
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 {
|
||||
if len(s) != 12 {
|
||||
return s
|
||||
}
|
||||
return s[0:4] + "-" + s[4:8] + "-" + s[8:12]
|
||||
}
|
||||
|
||||
func renderOrError(w http.ResponseWriter, template *builtTemplate, context interface{}) {
|
||||
buf := &bytes.Buffer{}
|
||||
err := template.executeTemplateHTML(buf, context)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("name", template.name).Msg("Failed to render template")
|
||||
htmlpage.RespondError(w, "Failed to render template", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
buf.WriteTo(w)
|
||||
}
|
||||
|
||||
var FORWARDEMAIL_API = "https://api.forwardemail.net/v1/emails"
|
||||
|
||||
func ensureInDB(ctx context.Context, destination string) (err error) {
|
||||
_, err = models.FindCommsEmail(ctx, db.PGInstance.BobDB, destination)
|
||||
if err != nil {
|
||||
// assume it exists
|
||||
log.Warn().Err(err).Msg("ElI, check what this error should look like")
|
||||
return nil
|
||||
}
|
||||
_, err = models.CommsEmails.Insert(&models.CommsEmailSetter{
|
||||
Address: omit.From(destination),
|
||||
Confirmed: omit.From(false),
|
||||
IsSubscribed: omit.From(false),
|
||||
}).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
|
||||
}
|
||||
|
||||
func insertEmailLog(ctx context.Context, email emailRequest, t enums.CommsMessagetypeemail) (err error) {
|
||||
_, err = models.CommsEmailLogs.Insert(&models.CommsEmailLogSetter{
|
||||
Created: omit.From(time.Now()),
|
||||
Destination: omit.From(email.To),
|
||||
Source: omit.From(email.From),
|
||||
Type: omit.From(t),
|
||||
}).One(ctx, db.PGInstance.BobDB)
|
||||
return err
|
||||
}
|
||||
func sendEmail(ctx context.Context, email emailRequest, t enums.CommsMessagetypeemail) (response emailResponse, err error) {
|
||||
ensureInDB(ctx, email.To)
|
||||
payload, err := json.Marshal(email)
|
||||
if err != nil {
|
||||
return response, fmt.Errorf("Failed to marshal email request: %w", err)
|
||||
}
|
||||
|
||||
insertEmailLog(ctx, email, t)
|
||||
req, _ := http.NewRequest("POST", FORWARDEMAIL_API, bytes.NewReader(payload))
|
||||
req.SetBasicAuth(config.ForwardEmailAPIToken, "")
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
|
||||
res, _ := http.DefaultClient.Do(req)
|
||||
|
||||
defer res.Body.Close()
|
||||
body, _ := io.ReadAll(res.Body)
|
||||
|
||||
// Parse the JSON response
|
||||
err = json.Unmarshal(body, &response)
|
||||
if err != nil {
|
||||
log.Warn().Str("status", res.Status).Str("response_body", string(body)).Msg("Attempted to send email but couldn't parse the resulting JSON")
|
||||
return response, fmt.Errorf("Failed to unmarshal JSON response: %w", err)
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
103
comms/email/db.go
Normal file
103
comms/email/db.go
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
package email
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
"github.com/stephenafamo/bob/types/pgtypes"
|
||||
)
|
||||
|
||||
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 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)
|
||||
_, 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: omitnull.From(templateInitialID),
|
||||
TemplateData: omit.From(data_for_insert),
|
||||
Type: omit.From(enums.CommsMessagetypeemailInitialContact),
|
||||
}).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)
|
||||
}
|
||||
108
comms/email/email.go
Normal file
108
comms/email/email.go
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
package email
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/Gleipnir-Technology/nidus-sync/config"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/db/enums"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type attachmentRequest struct {
|
||||
Filename string `json:"filename"`
|
||||
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 {
|
||||
From string `json:"from"`
|
||||
To string `json:"to"`
|
||||
CC []string `json:"cc,omitempty"`
|
||||
BCC []string `json:"bcc,omitempty"`
|
||||
Subject string `json:"subject"`
|
||||
Text string `json:"text"`
|
||||
HTML string `json:"html,omitempty"`
|
||||
Attachments []attachmentRequest `json:"attachments,omitempty"`
|
||||
Sender string `json:"sender"`
|
||||
ReplyTo string `json:"replyTo,omitempty"`
|
||||
InReplyTo string `json:"inReplyTo,omitempty"`
|
||||
References []string `json:"references,omitempty"`
|
||||
}
|
||||
|
||||
type emailEnvelope struct {
|
||||
From string `json:"from"`
|
||||
To []string `json:"to"`
|
||||
}
|
||||
|
||||
type emailResponse struct {
|
||||
IsRedacted bool `json:"is_redacted"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
HardBounces []string `json:"hard_bounces"`
|
||||
SoftBounces []string `json:"soft_bounces"`
|
||||
IsBounce bool `json:"is_bounce"`
|
||||
Alias string `json:"alias"`
|
||||
Domain string `json:"domain"`
|
||||
User string `json:"user"`
|
||||
Status string `json:"status"`
|
||||
IsLocked bool `json:"is_locked"`
|
||||
Envelope emailEnvelope `json:"envelope"`
|
||||
RequireTLS bool `json:"requireTLS"`
|
||||
MessageID string `json:"messageId"`
|
||||
Headers map[string]string `json:"headers"`
|
||||
Date string `json:"date"`
|
||||
Subject string `json:"subject"`
|
||||
Accepted []string `json:"accepted"`
|
||||
Deliveries []string `json:"deliveries"`
|
||||
RejectedErrors []string `json:"rejectedErrors"`
|
||||
ID string `json:"id"`
|
||||
Object string `json:"object"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
Link string `json:"link"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
var FORWARDEMAIL_API = "https://api.forwardemail.net/v1/emails"
|
||||
|
||||
func sendEmail(ctx context.Context, email emailRequest, 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)
|
||||
}
|
||||
|
||||
req, _ := http.NewRequest("POST", FORWARDEMAIL_API, bytes.NewReader(payload))
|
||||
req.SetBasicAuth(config.ForwardEmailAPIToken, "")
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
|
||||
res, _ := http.DefaultClient.Do(req)
|
||||
|
||||
defer res.Body.Close()
|
||||
body, _ := io.ReadAll(res.Body)
|
||||
|
||||
// Parse the JSON response
|
||||
err = json.Unmarshal(body, &response)
|
||||
if err != nil {
|
||||
log.Warn().Str("status", res.Status).Str("response_body", string(body)).Msg("Attempted to send email but couldn't parse the resulting JSON")
|
||||
return response, fmt.Errorf("Failed to unmarshal JSON response: %w", err)
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
74
comms/email/initial.go
Normal file
74
comms/email/initial.go
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
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 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["url_logo"] = config.MakeURLReport("/static/img/nidus-logo-no-lettering-64.png")
|
||||
data["url_subscribe"] = config.MakeURLReport("/email/subscribe?email=%s", destination)
|
||||
data["url_unsubscribe"] = config.MakeURLReport("/email/unsubscribe")
|
||||
public_id := generatePublicId(enums.CommsMessagetypeemailInitialContact, data)
|
||||
data["url_browser"] = config.MakeURLReport("/email?id=%s", 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
|
||||
}
|
||||
44
comms/email/job.go
Normal file
44
comms/email/job.go
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
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
|
||||
switch job.messageType() {
|
||||
case enums.CommsMessagetypeemailReportSubscriptionConfirmation:
|
||||
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
|
||||
/*
|
||||
case enums.CommsMessagetypeemailReportStatusScheduled:
|
||||
case enums.CommsMessagetypeemailReportStatusComplete:
|
||||
|
||||
}
|
||||
*/
|
||||
}
|
||||
80
comms/email/report_subscription_confirmation.go
Normal file
80
comms/email/report_subscription_confirmation.go
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
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 NewJobReportSubscriptionConfirmation(destination, report_id string) Job {
|
||||
return jobEmailReportSubscriptionConfirmation{
|
||||
dest: destination,
|
||||
reportID: report_id,
|
||||
}
|
||||
}
|
||||
|
||||
type jobEmailReportSubscriptionConfirmation struct {
|
||||
dest string
|
||||
reportID string
|
||||
}
|
||||
|
||||
func (job jobEmailReportSubscriptionConfirmation) destination() string {
|
||||
return job.dest
|
||||
}
|
||||
func (job jobEmailReportSubscriptionConfirmation) messageType() enums.CommsMessagetypeemail {
|
||||
return enums.CommsMessagetypeemailReportSubscriptionConfirmation
|
||||
}
|
||||
func (job jobEmailReportSubscriptionConfirmation) renderHTML() (string, error) {
|
||||
_ = newContentEmailSubscriptionConfirmation(job)
|
||||
return "", nil
|
||||
}
|
||||
func (job jobEmailReportSubscriptionConfirmation) renderTXT() (string, error) {
|
||||
return "fake txt", nil
|
||||
}
|
||||
func (job jobEmailReportSubscriptionConfirmation) subject() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func sendEmailReportConfirmation(ctx context.Context, job Job) error {
|
||||
j, ok := job.(jobEmailReportSubscriptionConfirmation)
|
||||
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)
|
||||
}
|
||||
return nil
|
||||
/*
|
||||
report_id_str := publicReportID(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)
|
||||
}
|
||||
resp, err := sendEmail(ctx, emailRequest{
|
||||
From: config.ForwardEmailReportAddress,
|
||||
HTML: html,
|
||||
Subject: fmt.Sprintf("Mosquito Report Submission - %s", report_id_str),
|
||||
Text: text,
|
||||
To: to,
|
||||
}, enums.CommsMessagetypeemailReportSubscriptionConfirmation)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to send email report confirmation to %s for report %s: %w", to, report_id, err)
|
||||
}
|
||||
log.Info().Str("id", resp.ID).Str("to", to).Str("report_id", report_id).Msg("Sent report confirmation email")
|
||||
return nil
|
||||
*/
|
||||
}
|
||||
|
||||
func newContentEmailSubscriptionConfirmation(job jobEmailReportSubscriptionConfirmation) (result contentEmailReportConfirmation) {
|
||||
/*newContentBase(
|
||||
&result.Base,
|
||||
config.MakeURLReport("/email/report/%s/subscription-confirmation", job.reportID),
|
||||
)*/
|
||||
result.URLReportStatus = config.MakeURLReport("/status/%s", job.reportID)
|
||||
return result
|
||||
}
|
||||
319
comms/email/template.go
Normal file
319
comms/email/template.go
Normal file
|
|
@ -0,0 +1,319 @@
|
|||
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/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"
|
||||
"github.com/stephenafamo/bob"
|
||||
"github.com/stephenafamo/bob/dialect/psql"
|
||||
"github.com/stephenafamo/bob/dialect/psql/um"
|
||||
)
|
||||
|
||||
//go:embed template/*
|
||||
var embeddedFiles embed.FS
|
||||
|
||||
var (
|
||||
templateByID map[int32]*builtTemplate
|
||||
templateInitialID int32
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
tx.Commit(ctx)
|
||||
return 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, content 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)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
|
@ -65,31 +65,31 @@
|
|||
<body>
|
||||
<div class="container">
|
||||
<div class="view-browser">
|
||||
Email not displaying correctly? <a href="{{.Base.URLViewInBrowser}}">View it in your browser</a>
|
||||
Email not displaying correctly? <a href="{{.url_browser}}">View it in your browser</a>
|
||||
</div>
|
||||
|
||||
<div class="header">
|
||||
<!-- Logo Placeholder -->
|
||||
<img src="{{.Base.URLLogo}}" alt="Report Mosquitoes Online Logo" class="logo"></img>
|
||||
<img src="{{.url_logo}}" 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>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="{{.URLSubscribe}}" class="button">I want emails from Report Mosquitoes Online</a>
|
||||
<a href="{{.url_subscribe}}" 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>If you no longer wish to receive these updates, <a href="{{.url_unsubscribe}}">click here to unsubscribe</a>.</p>
|
||||
<p>© 2026 Gleipnir LLC. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
7
comms/email/template/initial-contact.txt
Normal file
7
comms/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 ({{.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: {{.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,148 +0,0 @@
|
|||
package comms
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
templatehtml "html/template"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
templatetxt "text/template"
|
||||
|
||||
"github.com/Gleipnir-Technology/nidus-sync/config"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
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+".html", content)
|
||||
} else {
|
||||
return bt.templateHTML.ExecuteTemplate(w, bt.name+".html", 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+".txt", content)
|
||||
} else {
|
||||
return bt.templateTXT.ExecuteTemplate(w, bt.name+".txt", 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 buildTemplate(name string) *builtTemplate {
|
||||
files_on_disk := true
|
||||
file_html := templateFileHTML(name)
|
||||
file_txt := templateFileTXT(name)
|
||||
for _, f := range []string{file_html, file_txt} {
|
||||
_, err := os.Stat(f)
|
||||
if err != nil {
|
||||
files_on_disk = false
|
||||
if !config.IsProductionEnvironment() {
|
||||
log.Warn().Str("file", f).Msg("email template file is not on disk")
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
var result builtTemplate
|
||||
if files_on_disk {
|
||||
result = builtTemplate{
|
||||
name: name,
|
||||
templateHTML: nil,
|
||||
templateTXT: nil,
|
||||
}
|
||||
} else {
|
||||
result = builtTemplate{
|
||||
name: name,
|
||||
templateHTML: parseEmbeddedHTML(embeddedFiles, "comms", file_html),
|
||||
templateTXT: parseEmbeddedTXT(embeddedFiles, "comms", file_txt),
|
||||
}
|
||||
}
|
||||
return &result
|
||||
}
|
||||
|
||||
func parseEmbeddedHTML(embeddedFiles embed.FS, subdir string, file string) *templatehtml.Template {
|
||||
// Remap the file names to embedded paths
|
||||
to_trim := subdir + "/"
|
||||
embeddedFilePaths := []string{strings.TrimPrefix(file, to_trim)}
|
||||
name := path.Base(embeddedFilePaths[0])
|
||||
log.Debug().Str("name", name).Strs("paths", embeddedFilePaths).Msg("Parsing embedded template")
|
||||
return templatehtml.Must(
|
||||
templatehtml.New(name).ParseFS(embeddedFiles, embeddedFilePaths...))
|
||||
}
|
||||
func parseEmbeddedTXT(embeddedFiles embed.FS, subdir string, file string) *templatetxt.Template {
|
||||
// Remap the file names to embedded paths
|
||||
to_trim := subdir + "/"
|
||||
embeddedFilePaths := []string{strings.TrimPrefix(file, to_trim)}
|
||||
name := path.Base(embeddedFilePaths[0])
|
||||
log.Debug().Str("name", name).Strs("paths", embeddedFilePaths).Msg("Parsing embedded template")
|
||||
return templatetxt.Must(
|
||||
templatetxt.New(name).ParseFS(embeddedFiles, embeddedFilePaths...))
|
||||
}
|
||||
|
||||
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 renderEmailTemplates(t *builtTemplate, content interface{}) (text string, html string, err error) {
|
||||
buf_txt := &bytes.Buffer{}
|
||||
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 +0,0 @@
|
|||
Welcome to Report Mosquitoes Online.
|
||||
Loading…
Add table
Add a link
Reference in a new issue