// Copyright (c) 2026 Petr BalvĂ­n (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(`

Message received

Hello,

your message to {{.FormName}} was received. A reply will follow as soon as possible.

Please do not respond to this automated receipt.

{{if .Brand}}

Delivered by {{.Brand}}

{{end}}
`)) // 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, "

Email generation error: %v

", 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 }