125 lines
3.5 KiB
Go
125 lines
3.5 KiB
Go
// Package mail sends transactional emails via SMTP with STARTTLS.
|
|
package mail
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"net"
|
|
"net/smtp"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Config holds SMTP connection settings sourced from environment variables.
|
|
type Config struct {
|
|
Host string // SMTP_HOST
|
|
Port string // SMTP_PORT (default "587")
|
|
Username string // SMTP_USER
|
|
Password string // SMTP_PASS
|
|
From string // SMTP_FROM
|
|
AdminEmail string // ADMIN_EMAIL
|
|
BaseURL string // BASE_URL (used in link generation)
|
|
}
|
|
|
|
// Mailer sends email using the provided Config.
|
|
type Mailer struct {
|
|
cfg Config
|
|
}
|
|
|
|
// New returns a Mailer. Returns an error if required fields are missing.
|
|
func New(cfg Config) (*Mailer, error) {
|
|
if cfg.Host == "" || cfg.From == "" {
|
|
return nil, fmt.Errorf("mail: SMTP_HOST and SMTP_FROM are required")
|
|
}
|
|
if cfg.Port == "" {
|
|
cfg.Port = "587"
|
|
}
|
|
return &Mailer{cfg: cfg}, nil
|
|
}
|
|
|
|
// Configured reports whether the mailer has enough config to send.
|
|
func (m *Mailer) Configured() bool {
|
|
return m.cfg.Host != "" && m.cfg.From != ""
|
|
}
|
|
|
|
// SendPasswordReset sends a password-reset link to the given address.
|
|
func (m *Mailer) SendPasswordReset(toAddr, toName, token string) error {
|
|
link := strings.TrimRight(m.cfg.BaseURL, "/") + "/reset?token=" + token
|
|
subject := "Reset your Arcline Portal password"
|
|
body := fmt.Sprintf(`Hi %s,
|
|
|
|
Someone requested a password reset for your Arcline Portal account.
|
|
If that was you, click the link below to set a new password.
|
|
The link expires in 1 hour.
|
|
|
|
%s
|
|
|
|
If you did not request this, you can safely ignore this email.
|
|
|
|
— Arcline IT
|
|
`, toName, link)
|
|
return m.send(toAddr, subject, body)
|
|
}
|
|
|
|
// SendTicketCreated notifies the admin that a new ticket was opened.
|
|
func (m *Mailer) SendTicketCreated(clientName, subject, body string, ticketID int64) error {
|
|
if m.cfg.AdminEmail == "" {
|
|
return nil
|
|
}
|
|
link := strings.TrimRight(m.cfg.BaseURL, "/") + fmt.Sprintf("/tickets/%d", ticketID)
|
|
msg := fmt.Sprintf(`New support ticket from %s
|
|
|
|
Subject: %s
|
|
|
|
%s
|
|
|
|
---
|
|
View ticket: %s
|
|
`, clientName, subject, body, link)
|
|
return m.send(m.cfg.AdminEmail, fmt.Sprintf("[Arcline Portal] New ticket: %s", subject), msg)
|
|
}
|
|
|
|
// SendTicketReply notifies a party that a reply was added to their ticket.
|
|
// toAddr is the recipient; fromName is who replied.
|
|
func (m *Mailer) SendTicketReply(toAddr, toName, fromName, ticketSubject, replyBody string, ticketID int64) error {
|
|
if toAddr == "" {
|
|
return nil
|
|
}
|
|
link := strings.TrimRight(m.cfg.BaseURL, "/") + fmt.Sprintf("/tickets/%d", ticketID)
|
|
msg := fmt.Sprintf(`Hi %s,
|
|
|
|
%s replied to your ticket "%s":
|
|
|
|
---
|
|
%s
|
|
---
|
|
|
|
View the full thread: %s
|
|
|
|
— Arcline IT
|
|
`, toName, fromName, ticketSubject, replyBody, link)
|
|
return m.send(toAddr, fmt.Sprintf("[Arcline Portal] Re: %s", ticketSubject), msg)
|
|
}
|
|
|
|
// send composes and sends a plain-text email via SMTP STARTTLS.
|
|
func (m *Mailer) send(to, subject, body string) error {
|
|
addr := net.JoinHostPort(m.cfg.Host, m.cfg.Port)
|
|
|
|
var buf bytes.Buffer
|
|
fmt.Fprintf(&buf, "From: %s\r\n", m.cfg.From)
|
|
fmt.Fprintf(&buf, "To: %s\r\n", to)
|
|
fmt.Fprintf(&buf, "Subject: %s\r\n", subject)
|
|
fmt.Fprintf(&buf, "Date: %s\r\n", time.Now().Format("Mon, 02 Jan 2006 15:04:05 -0700"))
|
|
fmt.Fprintf(&buf, "MIME-Version: 1.0\r\n")
|
|
fmt.Fprintf(&buf, "Content-Type: text/plain; charset=UTF-8\r\n")
|
|
fmt.Fprintf(&buf, "\r\n")
|
|
fmt.Fprintf(&buf, "%s", strings.ReplaceAll(body, "\n", "\r\n"))
|
|
|
|
var auth smtp.Auth
|
|
if m.cfg.Username != "" {
|
|
auth = smtp.PlainAuth("", m.cfg.Username, m.cfg.Password, m.cfg.Host)
|
|
}
|
|
|
|
return smtp.SendMail(addr, auth, m.cfg.From, []string{to}, buf.Bytes())
|
|
}
|