Test / test (push) Successful in 2m1s
Release / gates (push) Successful in 1m57s
Release / build (amd64, freebsd) (push) Successful in 1m26s
Release / build (amd64, linux) (push) Successful in 1m30s
Release / build (arm64, freebsd) (push) Successful in 1m28s
Release / build (arm64, linux) (push) Successful in 1m49s
Release / build (loong64, linux) (push) Successful in 1m30s
Release / build (riscv64, linux) (push) Successful in 1m29s
Release / release (push) Successful in 41s
Assisted-by: GLM 5.3 Flash
121 lines
4.7 KiB
Go
121 lines
4.7 KiB
Go
// 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
|
|
}
|