Files
nuntius/internal/email/acknowledgement.go
T

121 lines
4.7 KiB
Go
Raw Normal View History

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
//go:build linux || freebsd
package email
import (
"bytes"
"fmt"
"html/template"
"net/smtp"
"time"
"sourcedock.dev/petrbalvin/nuntius/internal/config"
)
// emailTemplateAcknowledgement is the HTML body of the automated receipt
// a form with auto_reply enabled sends to the submitter.
var emailTemplateAcknowledgement = template.Must(template.New("acknowledgement").Parse(`<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"></head>
<body style="margin:0;padding:0;background:#f3f4f6;font-family:Inter,ui-sans-serif,system-ui,sans-serif">
<table width="100%" cellpadding="0" cellspacing="0" style="background:#f3f4f6;padding:32px 0">
<tr><td align="center">
<table width="460" cellpadding="0" cellspacing="0" style="background:#fff;border-radius:12px;overflow:hidden;box-shadow:0 2px 16px rgba(0,0,0,.06)">
<tr><td style="background:#1e40af;padding:20px 24px">
<p style="margin:0;font-size:18px;font-weight:700;color:#fff">Message received</p>
</td></tr>
<tr><td style="padding:24px">
<p style="font-size:14px;line-height:1.6;color:#1f2937;margin:0 0 12px">Hello,</p>
<p style="font-size:14px;line-height:1.6;color:#1f2937;margin:0 0 12px">your message to <strong>{{.FormName}}</strong> was received. A reply will follow as soon as possible.</p>
<p style="font-size:14px;line-height:1.6;color:#1f2937;margin:0">Please do not respond to this automated receipt.</p>
{{if .Brand}}
<p style="font-size:11px;color:#9ca3af;margin:16px 0 0;border-top:1px solid #e5e7eb;padding-top:16px">Delivered by <strong style="color:#6b7280">{{.Brand}}</strong></p>
{{end}}
</td></tr>
</table>
</td></tr>
</table>
</body>
</html>`))
// SendAcknowledgement mails the submitter a short receipt. It uses the
// form's SMTP identity, and the Reply-To points at the owner, so a reply
// to the receipt lands in the human's inbox and not into the void.
func (s *FormSender) SendAcknowledgement(to string) error {
auth := smtp.PlainAuth("", s.form.SMTP.User, s.form.SMTP.Password, s.form.SMTP.Host)
msg, err := composeAcknowledgement(s.form, to)
if err != nil {
return fmt.Errorf("compose acknowledgement: %w", err)
}
return sendMail(s.form.SMTP, auth, s.form.From, []string{to}, msg, s.form.SMTP.Timeout(), s.TLSConfig)
}
// composeAcknowledgement builds the multipart receipt. The submitted
// values are deliberately not echoed back: the receipt confirms arrival,
// it does not mirror a message's content into the submitter's inbox,
// where any third party who could fill the form would read it.
func composeAcknowledgement(form *config.Form, to string) ([]byte, error) {
// --- HTML part ---
var htmlBuf bytes.Buffer
if err := emailTemplateAcknowledgement.Execute(&htmlBuf, map[string]string{
"FormName": form.Name,
"Brand": form.Brand(),
}); err != nil {
// template.Must guarantees valid templates; unreachable in
// normal operation.
htmlBuf.Reset()
fmt.Fprintf(&htmlBuf, "<p>Email generation error: %v</p>", err)
}
// --- Subject + plain-text body ---
subject := "Message received"
if tag := subjectTag(form, ""); tag != "" {
subject = tag + " " + subject
}
footer := ""
if brand := form.Brand(); brand != "" {
footer = "--\r\nDelivered by " + brand + "\r\n"
}
text := fmt.Sprintf(
"Hello,\r\n\r\n"+
"your message to %s was received. A reply will follow as soon as possible.\r\n"+
"Please do not respond to this automated receipt.\r\n\r\n%s",
form.Name, footer,
)
// Assemble multipart/alternative. The boundary comes first so that
// randomness failure aborts the message before anything is built.
boundary, err := randomBoundary()
if err != nil {
return nil, err
}
var msg bytes.Buffer
msg.WriteString(fmt.Sprintf("From: %s\r\n", sanitizeHeaderValue(form.From)))
msg.WriteString(fmt.Sprintf("To: %s\r\n", sanitizeHeaderValue(to)))
msg.WriteString(fmt.Sprintf("Subject: %s\r\n", sanitizeHeaderValue(subject)))
msg.WriteString(fmt.Sprintf("Date: %s\r\n", time.Now().UTC().Format(time.RFC1123Z)))
msg.WriteString(fmt.Sprintf("Reply-To: %s\r\n", sanitizeHeaderValue(form.To)))
msg.WriteString("MIME-Version: 1.0\r\n")
msg.WriteString(fmt.Sprintf("Content-Type: multipart/alternative; boundary=%s\r\n", boundary))
msg.WriteString("\r\n")
msg.WriteString(fmt.Sprintf("--%s\r\n", boundary))
msg.WriteString("Content-Type: text/plain; charset=UTF-8\r\n")
msg.WriteString("\r\n")
msg.WriteString(text)
msg.WriteString("\r\n")
msg.WriteString(fmt.Sprintf("--%s\r\n", boundary))
msg.WriteString("Content-Type: text/html; charset=UTF-8\r\n")
msg.WriteString("\r\n")
msg.WriteString(htmlBuf.String())
msg.WriteString("\r\n")
msg.WriteString(fmt.Sprintf("--%s--\r\n", boundary))
return msg.Bytes(), nil
}