2026-01-17 01:13:27 +00:00
|
|
|
package comms
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
2026-01-23 03:32:06 +00:00
|
|
|
"context"
|
2026-01-19 17:58:30 +00:00
|
|
|
"embed"
|
2026-01-17 01:13:27 +00:00
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
|
|
|
|
"net/http"
|
2026-01-23 03:32:06 +00:00
|
|
|
"time"
|
2026-01-17 01:13:27 +00:00
|
|
|
|
|
|
|
|
"github.com/Gleipnir-Technology/nidus-sync/config"
|
2026-01-23 03:32:06 +00:00
|
|
|
"github.com/Gleipnir-Technology/nidus-sync/db"
|
|
|
|
|
"github.com/Gleipnir-Technology/nidus-sync/db/enums"
|
|
|
|
|
"github.com/Gleipnir-Technology/nidus-sync/db/models"
|
2026-01-19 21:21:02 +00:00
|
|
|
"github.com/Gleipnir-Technology/nidus-sync/htmlpage"
|
2026-01-23 03:32:06 +00:00
|
|
|
"github.com/aarondl/opt/omit"
|
2026-01-17 01:13:27 +00:00
|
|
|
"github.com/rs/zerolog/log"
|
|
|
|
|
)
|
|
|
|
|
|
2026-01-23 02:50:25 +00:00
|
|
|
func RenderEmailInitial(w http.ResponseWriter, destination string) {
|
|
|
|
|
content := newContentEmailInitial(destination)
|
|
|
|
|
renderOrError(w, initialT, content)
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-19 21:21:02 +00:00
|
|
|
func RenderEmailReportConfirmation(w http.ResponseWriter, report_id string) {
|
2026-01-23 02:50:25 +00:00
|
|
|
content := newContentEmailSubscriptionConfirmation(report_id)
|
2026-01-19 21:21:02 +00:00
|
|
|
renderOrError(w, reportConfirmationT, content)
|
|
|
|
|
}
|
2026-01-23 02:50:25 +00:00
|
|
|
|
2026-01-23 03:32:06 +00:00
|
|
|
func SendEmailInitialContact(ctx context.Context, destination string) error {
|
2026-01-23 02:50:25 +00:00
|
|
|
content := newContentEmailInitial(destination)
|
|
|
|
|
text, html, err := renderEmailTemplates(reportConfirmationT, content)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("Failed to render email temlate: %w", err)
|
|
|
|
|
}
|
2026-01-23 03:32:06 +00:00
|
|
|
resp, err := sendEmail(ctx, emailRequest{
|
2026-01-23 02:50:25 +00:00
|
|
|
From: config.ForwardEmailReportAddress,
|
|
|
|
|
HTML: html,
|
|
|
|
|
Subject: "Welcome",
|
|
|
|
|
Text: text,
|
|
|
|
|
To: destination,
|
2026-01-23 03:32:06 +00:00
|
|
|
}, enums.CommsMessagetypeemailInitialContact)
|
2026-01-23 02:50:25 +00:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-23 03:32:06 +00:00
|
|
|
func SendEmailReportConfirmation(ctx context.Context, to string, report_id string) error {
|
2026-01-19 18:19:02 +00:00
|
|
|
report_id_str := publicReportID(report_id)
|
2026-01-23 02:50:25 +00:00
|
|
|
content := newContentEmailSubscriptionConfirmation(report_id)
|
2026-01-19 17:58:30 +00:00
|
|
|
text, html, err := renderEmailTemplates(reportConfirmationT, content)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("Failed to render template %s: %w", reportConfirmationT.name, err)
|
|
|
|
|
}
|
2026-01-23 03:32:06 +00:00
|
|
|
resp, err := sendEmail(ctx, emailRequest{
|
2026-01-19 17:58:30 +00:00
|
|
|
From: config.ForwardEmailReportAddress,
|
|
|
|
|
HTML: html,
|
2026-01-19 18:19:02 +00:00
|
|
|
Subject: fmt.Sprintf("Mosquito Report Submission - %s", report_id_str),
|
2026-01-19 17:58:30 +00:00
|
|
|
Text: text,
|
|
|
|
|
To: to,
|
2026-01-23 03:32:06 +00:00
|
|
|
}, enums.CommsMessagetypeemailReportSubscriptionConfirmation)
|
2026-01-19 18:10:17 +00:00
|
|
|
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
|
2026-01-19 17:58:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var (
|
2026-01-23 02:50:25 +00:00
|
|
|
initialT = buildTemplate("initial")
|
2026-01-19 17:58:30 +00:00
|
|
|
reportConfirmationT = buildTemplate("report-subscription-confirmation")
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
//go:embed template/*
|
|
|
|
|
var embeddedFiles embed.FS
|
|
|
|
|
|
|
|
|
|
type attachmentRequest struct {
|
2026-01-17 01:13:27 +00:00
|
|
|
Filename string `json:"filename"`
|
2026-01-18 03:00:48 +00:00
|
|
|
Content string `json:"content"`
|
2026-01-17 01:13:27 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-23 02:50:25 +00:00
|
|
|
type contentEmailBase struct {
|
|
|
|
|
URLLogo string
|
|
|
|
|
URLUnsubscribe string
|
|
|
|
|
URLViewInBrowser string
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-19 17:58:30 +00:00
|
|
|
type contentEmailReportConfirmation struct {
|
2026-01-23 02:50:25 +00:00
|
|
|
Base contentEmailBase
|
|
|
|
|
URLReportStatus string
|
|
|
|
|
}
|
|
|
|
|
type contentEmailInitial struct {
|
|
|
|
|
Base contentEmailBase
|
|
|
|
|
Destination string
|
|
|
|
|
URLSubscribe string
|
2026-01-19 17:58:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type emailRequest struct {
|
2026-01-18 03:00:48 +00:00
|
|
|
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"`
|
2026-01-19 17:58:30 +00:00
|
|
|
Attachments []attachmentRequest `json:"attachments,omitempty"`
|
2026-01-18 03:00:48 +00:00
|
|
|
Sender string `json:"sender"`
|
|
|
|
|
ReplyTo string `json:"replyTo,omitempty"`
|
|
|
|
|
InReplyTo string `json:"inReplyTo,omitempty"`
|
|
|
|
|
References []string `json:"references,omitempty"`
|
2026-01-17 01:13:27 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-19 18:10:17 +00:00
|
|
|
type emailEnvelope struct {
|
|
|
|
|
From string `json:"from"`
|
|
|
|
|
To []string `json:"to"`
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-19 17:58:30 +00:00
|
|
|
type emailResponse struct {
|
2026-01-19 18:10:17 +00:00
|
|
|
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"`
|
2026-01-17 01:13:27 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-23 02:50:25 +00:00
|
|
|
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
|
2026-01-19 21:21:02 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-19 18:19:02 +00:00
|
|
|
func publicReportID(s string) string {
|
|
|
|
|
if len(s) != 12 {
|
|
|
|
|
return s
|
|
|
|
|
}
|
|
|
|
|
return s[0:4] + "-" + s[4:8] + "-" + s[8:12]
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-19 21:21:02 +00:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-20 17:10:22 +00:00
|
|
|
var FORWARDEMAIL_API = "https://api.forwardemail.net/v1/emails"
|
2026-01-17 01:13:27 +00:00
|
|
|
|
2026-01-23 03:32:06 +00:00
|
|
|
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)
|
2026-01-17 01:13:27 +00:00
|
|
|
payload, err := json.Marshal(email)
|
|
|
|
|
if err != nil {
|
2026-01-19 18:10:17 +00:00
|
|
|
return response, fmt.Errorf("Failed to marshal email request: %w", err)
|
2026-01-17 01:13:27 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-23 03:32:06 +00:00
|
|
|
insertEmailLog(ctx, email, t)
|
2026-01-20 17:10:22 +00:00
|
|
|
req, _ := http.NewRequest("POST", FORWARDEMAIL_API, bytes.NewReader(payload))
|
2026-01-17 01:13:27 +00:00
|
|
|
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)
|
|
|
|
|
|
2026-01-19 18:10:17 +00:00
|
|
|
// 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
|
2026-01-17 01:13:27 +00:00
|
|
|
}
|