//go:build linux || freebsd // Package email handles SMTP message composition and delivery. package email import ( "bytes" "fmt" "html/template" "net/smtp" "time" "sourcedock.dev/petrbalvin/nuntius/internal/config" "sourcedock.dev/petrbalvin/nuntius/pkg/contactform" ) // FormSender delivers contact form submissions for a single form // using its own SMTP credentials. Each form has its own FormSender // so credentials are isolated per tenant. type FormSender struct { form *config.Form } // NewFormSender returns a new FormSender bound to the given form. func NewFormSender(form *config.Form) *FormSender { return &FormSender{form: form} } // Send composes and sends a multi-part email (plain text + HTML) // for the given request. Returns an error if the SMTP round-trip fails. func (s *FormSender) Send(req contactform.Request) error { auth := smtp.PlainAuth("", s.form.SMTP.User, s.form.SMTP.Password, s.form.SMTP.Host) msg := compose(s.form.From, s.form.To, req, s.form.Name, s.form.Type) return smtp.SendMail(s.form.SMTP.AddrFor(), auth, s.form.From, []string{s.form.To}, msg) } // emailTemplateContact is the HTML body template for contact-type forms. var emailTemplateContact = template.Must(template.New("contact").Parse(`

Contact Form Submission

{{.FormName}}

{{if .Service}} {{end}}
From
{{.Name}} {{.Email}}
Service Interest
{{.Service}}
Received
{{.Received}}

Message

{{.Message}}

Delivered by nuntius — a contact form backend for Linux servers.

`)) // emailTemplateFeedback is the HTML body template for feedback-type forms. var emailTemplateFeedback = template.Must(template.New("feedback").Parse(`

New Feedback

{{.FormName}}

From

{{.Name}} {{.Email}}

Feedback

{{.Message}}

Delivered by nuntius

`)) // emailTemplateNewsletter is the HTML body template for newsletter-signup forms. var emailTemplateNewsletter = template.Must(template.New("newsletter").Parse(`

{{.FormName}} — new subscriber

Email

{{.Email}}

Delivered by nuntius

`)) // emailTemplateGeneric is the HTML body template for generic forms. var emailTemplateGeneric = template.Must(template.New("generic").Parse(`

Form Submission

{{.FormName}}

From

{{.Name}} {{.Email}}

Message

{{.Message}}

Delivered by nuntius

`)) func compose(from, to string, req contactform.Request, formName, formType string) []byte { received := time.Now().UTC().Format("January 2, 2006 at 15:04 UTC") // --- HTML part --- var htmlBuf bytes.Buffer tmpl := emailTemplateContact switch formType { case "newsletter": tmpl = emailTemplateNewsletter case "feedback": tmpl = emailTemplateFeedback case "generic": tmpl = emailTemplateGeneric } if err := tmpl.Execute(&htmlBuf, map[string]string{ "FormName": formName, "Name": req.Name, "Email": req.Email, "Service": req.Service, "Message": req.Message, "Received": received, }); err != nil { // template.Must guarantees valid templates; this path is // unreachable in normal operation. htmlBuf.Reset() fmt.Fprintf(&htmlBuf, "

Email generation error: %v

", err) } // --- Subject + plain-text body per form type --- var subject, text string switch formType { case "newsletter": subject = fmt.Sprintf("[nuntius/%s] New newsletter subscriber", formName) text = fmt.Sprintf( "New Newsletter Subscriber: %s\r\n"+ "---\r\n"+ "Email: %s\r\n"+ "Received: %s\r\n"+ "\r\n"+ "Delivered by nuntius\r\n", formName, req.Email, received, ) case "feedback": subject = fmt.Sprintf("[nuntius/%s] New feedback", formName) text = fmt.Sprintf( "New Feedback: %s\r\n"+ "---\r\n"+ "From: %s <%s>\r\n"+ "Received: %s\r\n"+ "\r\n"+ "%s\r\n\r\n"+ "Delivered by nuntius\r\n", formName, req.Name, req.Email, received, req.Message, ) case "generic": subject = fmt.Sprintf("[nuntius/%s] New submission", formName) text = fmt.Sprintf( "New Submission: %s\r\n"+ "---\r\n"+ "From: %s <%s>\r\n"+ "Received: %s\r\n"+ "\r\n"+ "%s\r\n\r\n"+ "Delivered by nuntius\r\n", formName, req.Name, req.Email, received, req.Message, ) default: // contact subject = fmt.Sprintf("[nuntius/%s] Contact form submission", formName) if req.Service != "" { subject = fmt.Sprintf("[nuntius/%s][%s] Contact form submission", formName, req.Service) } text = fmt.Sprintf( "Contact Form Submission: %s\r\n"+ "---\r\n"+ "From: %s <%s>\r\n"+ "Service interest: %s\r\n"+ "Received: %s\r\n"+ "\r\n"+ "%s\r\n\r\n"+ "Delivered by nuntius\r\n", formName, req.Name, req.Email, req.Service, received, req.Message, ) } // Assemble multipart/alternative message. boundary := "nuntius-boundary" var msg bytes.Buffer msg.WriteString(fmt.Sprintf("From: %s\r\n", from)) msg.WriteString(fmt.Sprintf("To: %s\r\n", to)) msg.WriteString(fmt.Sprintf("Subject: %s\r\n", subject)) msg.WriteString(fmt.Sprintf("Date: %s\r\n", time.Now().UTC().Format(time.RFC1123Z))) msg.WriteString(fmt.Sprintf("Reply-To: %s\r\n", req.Email)) 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() }