mirror of
https://github.com/multipleof4/ntfy.git
synced 2026-10-08 21:05:21 +00:00
141 lines
4.5 KiB
Go
141 lines
4.5 KiB
Go
package mail
|
|
|
|
import (
|
|
"fmt"
|
|
"mime"
|
|
"net"
|
|
"net/smtp"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"heckel.io/ntfy/v2/log"
|
|
"heckel.io/ntfy/v2/model"
|
|
)
|
|
|
|
const (
|
|
tagMail = "mail"
|
|
|
|
emailVerificationSubject = "Verify your email for ntfy"
|
|
passwordResetSubject = "Reset your ntfy password"
|
|
)
|
|
|
|
// Config holds the SMTP configuration for the mail sender
|
|
type Config struct {
|
|
BaseURL string // ntfy base URL, used to build topic URLs in notification emails
|
|
SMTPAddr string // SMTP server address (host:port)
|
|
SMTPUser string // SMTP auth username
|
|
SMTPPass string // SMTP auth password
|
|
From string // Sender email address
|
|
}
|
|
|
|
// Sender sends all of ntfy's outgoing email: notification emails (the email-on-publish feature)
|
|
// as well as the magic-link emails for email verification and password reset. realSender is the
|
|
// SMTP-backed implementation; tests inject a fake.
|
|
type Sender interface {
|
|
SendNotification(to string, m *model.Message, senderIP string) error
|
|
NotificationCounts() (total int64, success int64, failure int64)
|
|
SendEmailVerification(to, link string) error
|
|
SendPasswordReset(to, link string) error
|
|
}
|
|
|
|
// realSender is the SMTP-backed implementation of Sender. Pending verification/reset state lives
|
|
// in the database (see user.Manager), not in this struct.
|
|
type realSender struct {
|
|
config *Config
|
|
success int64
|
|
failure int64
|
|
mu sync.Mutex
|
|
}
|
|
|
|
// NewSender creates a new mail Sender with the given SMTP config
|
|
func NewSender(config *Config) Sender {
|
|
return &realSender{config: config}
|
|
}
|
|
|
|
// SendNotification formats a ntfy message into a notification email and sends it via SMTP. It
|
|
// tracks success/failure counts, exposed via Counts (used for the server stats).
|
|
func (s *realSender) SendNotification(to string, m *model.Message, senderIP string) error {
|
|
message, err := formatMail(s.config.BaseURL, senderIP, s.config.From, to, m)
|
|
if err != nil {
|
|
s.count(false)
|
|
return err
|
|
}
|
|
log.Tag(tagMail).Field("email_to", to).Debug("Sending notification email")
|
|
err = s.sendRaw(to, []byte(message))
|
|
s.count(err == nil)
|
|
return err
|
|
}
|
|
|
|
// NotificationCounts returns the number of notification emails sent, broken down into total, success and failure
|
|
func (s *realSender) NotificationCounts() (total int64, success int64, failure int64) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
return s.success + s.failure, s.success, s.failure
|
|
}
|
|
|
|
// SendEmailVerification sends an email containing a magic link to verify ownership of the
|
|
// recipient address. The link carries a one-time token validated against the database.
|
|
func (s *realSender) SendEmailVerification(to, link string) error {
|
|
body := fmt.Sprintf(`Click the link below to verify this email address for your ntfy account:
|
|
|
|
%s
|
|
|
|
This link expires in 24 hours. If you did not request this, you can safely ignore this email.`, link)
|
|
return s.send(to, emailVerificationSubject, body)
|
|
}
|
|
|
|
// SendPasswordReset sends an email containing a magic link to set a new password. The link
|
|
// carries a one-time token validated against the database.
|
|
func (s *realSender) SendPasswordReset(to, link string) error {
|
|
body := fmt.Sprintf(`Click the link below to set a new password for your ntfy account:
|
|
|
|
%s
|
|
|
|
This link expires in 1 hour. If you did not request this, you can safely ignore this email -- your password will not change.`, link)
|
|
return s.send(to, passwordResetSubject, body)
|
|
}
|
|
|
|
// send sends a plain text email via SMTP
|
|
func (s *realSender) send(to, subject, body string) error {
|
|
date := time.Now().UTC().Format(time.RFC1123Z)
|
|
encodedSubject := mime.BEncoding.Encode("utf-8", subject)
|
|
message := `From: ntfy <{from}>
|
|
To: {to}
|
|
Date: {date}
|
|
Subject: {subject}
|
|
Content-Type: text/plain; charset="utf-8"
|
|
|
|
{body}`
|
|
message = strings.ReplaceAll(message, "{from}", s.config.From)
|
|
message = strings.ReplaceAll(message, "{to}", to)
|
|
message = strings.ReplaceAll(message, "{date}", date)
|
|
message = strings.ReplaceAll(message, "{subject}", encodedSubject)
|
|
message = strings.ReplaceAll(message, "{body}", body)
|
|
log.Tag(tagMail).Field("email_to", to).Debug("Sending email")
|
|
return s.sendRaw(to, []byte(message))
|
|
}
|
|
|
|
// sendRaw sends a raw email message via SMTP
|
|
func (s *realSender) sendRaw(to string, message []byte) error {
|
|
host, _, err := net.SplitHostPort(s.config.SMTPAddr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var auth smtp.Auth
|
|
if s.config.SMTPUser != "" {
|
|
auth = smtp.PlainAuth("", s.config.SMTPUser, s.config.SMTPPass, host)
|
|
}
|
|
return smtp.SendMail(s.config.SMTPAddr, auth, s.config.From, []string{to}, message)
|
|
}
|
|
|
|
func (s *realSender) count(ok bool) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if ok {
|
|
s.success++
|
|
} else {
|
|
s.failure++
|
|
}
|
|
}
|