124 lines
3.5 KiB
Go
124 lines
3.5 KiB
Go
package outbound
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"mime"
|
|
"net"
|
|
"net/smtp"
|
|
"net/textproto"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/example/notify-gateway/internal/config"
|
|
)
|
|
|
|
func mailBytes(o config.OutboundConfig, p map[string]any, id string) []byte {
|
|
title, _ := p["title"].(string)
|
|
message, _ := p["message"].(string)
|
|
title = strings.NewReplacer("\r", " ", "\n", " ").Replace(title)
|
|
// Force base64 for non-ASCII bodies and wrap per MIME line-length limits.
|
|
encoded := base64.StdEncoding.EncodeToString([]byte(message))
|
|
var body strings.Builder
|
|
for len(encoded) > 76 {
|
|
body.WriteString(encoded[:76] + "\r\n")
|
|
encoded = encoded[76:]
|
|
}
|
|
body.WriteString(encoded + "\r\n")
|
|
headers := []string{"From: " + o.From, "To: " + strings.Join(o.To, ", "), "Subject: " + mime.QEncoding.Encode("utf-8", title), "Date: " + time.Now().Format(time.RFC1123Z), "MIME-Version: 1.0", "Content-Type: text/plain; charset=utf-8", "Content-Transfer-Encoding: base64", "Auto-Submitted: auto-generated", "X-Notify-Gateway: 1"}
|
|
if id != "" {
|
|
headers = append(headers, "Message-ID: <"+id+"@notify-gateway.local>")
|
|
}
|
|
return []byte(strings.Join(headers, "\r\n") + "\r\n\r\n" + body.String())
|
|
}
|
|
|
|
func sendSMTP(ctx context.Context, o config.OutboundConfig, p map[string]any, id string) (resp Response, err error) {
|
|
return sendSMTPWithTLS(ctx, o, p, id, &tls.Config{ServerName: o.SMTPHost, MinVersion: tls.VersionTLS12})
|
|
}
|
|
|
|
func sendSMTPWithTLS(ctx context.Context, o config.OutboundConfig, p map[string]any, id string, tlsConfig *tls.Config) (resp Response, err error) {
|
|
defer func() {
|
|
if err != nil {
|
|
var e *textproto.Error
|
|
if errors.As(err, &e) {
|
|
resp.StatusCode = e.Code
|
|
resp.Retryable = e.Code >= 400 && e.Code < 500
|
|
} else {
|
|
resp.Retryable = true
|
|
}
|
|
err = errors.New("SMTP delivery failed")
|
|
}
|
|
}()
|
|
password, err := config.Secret(o.Password)
|
|
if err != nil {
|
|
return Response{}, err
|
|
}
|
|
timeout := o.TimeoutS
|
|
if timeout == 0 {
|
|
timeout = 15
|
|
}
|
|
dialer := net.Dialer{Timeout: time.Duration(timeout) * time.Second}
|
|
address := net.JoinHostPort(o.SMTPHost, strconv.Itoa(o.SMTPPort))
|
|
conn, err := dialer.DialContext(ctx, "tcp", address)
|
|
if err != nil {
|
|
return resp, err
|
|
}
|
|
defer conn.Close()
|
|
rawConn := conn
|
|
stop := context.AfterFunc(ctx, func() { rawConn.Close() })
|
|
defer stop()
|
|
if err = conn.SetDeadline(time.Now().Add(time.Duration(timeout) * time.Second)); err != nil {
|
|
return resp, err
|
|
}
|
|
if o.TLSMode == "tls" {
|
|
secure := tls.Client(conn, tlsConfig)
|
|
if err = secure.HandshakeContext(ctx); err != nil {
|
|
return resp, err
|
|
}
|
|
conn = secure
|
|
}
|
|
c, err := smtp.NewClient(conn, o.SMTPHost)
|
|
if err != nil {
|
|
return resp, err
|
|
}
|
|
defer c.Close()
|
|
if o.TLSMode == "starttls" {
|
|
if ok, _ := c.Extension("STARTTLS"); !ok {
|
|
return resp, fmt.Errorf("STARTTLS required")
|
|
}
|
|
if err = c.StartTLS(tlsConfig); err != nil {
|
|
return resp, err
|
|
}
|
|
}
|
|
if o.Username != "" {
|
|
if err = c.Auth(smtp.PlainAuth("", o.Username, password, o.SMTPHost)); err != nil {
|
|
return resp, err
|
|
}
|
|
}
|
|
if err = c.Mail(o.From); err != nil {
|
|
return resp, err
|
|
}
|
|
for _, to := range o.To {
|
|
if err = c.Rcpt(to); err != nil {
|
|
return resp, err
|
|
}
|
|
}
|
|
w, err := c.Data()
|
|
if err != nil {
|
|
return resp, err
|
|
}
|
|
if _, err = w.Write(mailBytes(o, p, id)); err != nil {
|
|
return resp, err
|
|
}
|
|
if err = w.Close(); err != nil {
|
|
return resp, err
|
|
}
|
|
// DATA has been acknowledged. A failed QUIT must not cause another delivery.
|
|
_ = c.Quit()
|
|
return Response{StatusCode: 250}, nil
|
|
}
|