mirror of
https://github.com/binwiederhier/ntfy.git
synced 2026-05-15 07:35:49 -06:00
154 lines
3.7 KiB
Go
154 lines
3.7 KiB
Go
package mail
|
|
|
|
import (
|
|
"fmt"
|
|
"mime"
|
|
"net"
|
|
"net/smtp"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"heckel.io/ntfy/v2/log"
|
|
"heckel.io/ntfy/v2/util"
|
|
)
|
|
|
|
const (
|
|
verifyCodeExpiry = 10 * time.Minute
|
|
verifyCodeLength = 6
|
|
verifyCodeSubject = "ntfy email verification"
|
|
)
|
|
|
|
// Config holds the SMTP configuration for the mail sender
|
|
type Config struct {
|
|
SMTPAddr string // SMTP server address (host:port)
|
|
SMTPUser string // SMTP auth username
|
|
SMTPPass string // SMTP auth password
|
|
From string // Sender email address
|
|
}
|
|
|
|
// Sender sends emails and manages email verification codes
|
|
type Sender struct {
|
|
config *Config
|
|
codes map[string]verifyCode // Verification codes, keyed by email
|
|
mu sync.Mutex
|
|
closeChan chan struct{}
|
|
}
|
|
|
|
type verifyCode struct {
|
|
code string
|
|
expires time.Time
|
|
}
|
|
|
|
// NewSender creates a new mail Sender with the given SMTP config
|
|
func NewSender(config *Config) *Sender {
|
|
s := &Sender{
|
|
config: config,
|
|
codes: make(map[string]verifyCode),
|
|
closeChan: make(chan struct{}),
|
|
}
|
|
go s.expireLoop()
|
|
return s
|
|
}
|
|
|
|
// Close stops the background expiry loop
|
|
func (s *Sender) Close() {
|
|
close(s.closeChan)
|
|
}
|
|
|
|
// Addr returns the SMTP server address
|
|
func (s *Sender) Addr() string {
|
|
return s.config.SMTPAddr
|
|
}
|
|
|
|
// User returns the SMTP username
|
|
func (s *Sender) User() string {
|
|
return s.config.SMTPUser
|
|
}
|
|
|
|
// From returns the sender email address
|
|
func (s *Sender) From() string {
|
|
return s.config.From
|
|
}
|
|
|
|
// SendRaw sends a raw email message via SMTP
|
|
func (s *Sender) 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)
|
|
}
|
|
|
|
// Send sends a plain text email via SMTP
|
|
func (s *Sender) 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("mail").Field("email_to", to).Debug("Sending email")
|
|
return s.SendRaw(to, []byte(message))
|
|
}
|
|
|
|
// SendVerification generates a random code, stores it in-memory, and sends a verification email
|
|
func (s *Sender) SendVerification(to string) error {
|
|
code := util.RandomString(verifyCodeLength)
|
|
s.mu.Lock()
|
|
s.codes[to] = verifyCode{
|
|
code: code,
|
|
expires: time.Now().Add(verifyCodeExpiry),
|
|
}
|
|
s.mu.Unlock()
|
|
body := fmt.Sprintf("Your ntfy email verification code is: %s\n\nThis code expires in 10 minutes.", code)
|
|
return s.Send(to, verifyCodeSubject, body)
|
|
}
|
|
|
|
// CheckVerification checks if the code matches and hasn't expired. Removes the entry on success.
|
|
func (s *Sender) CheckVerification(email, code string) bool {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
vc, ok := s.codes[email]
|
|
if !ok || time.Now().After(vc.expires) || vc.code != code {
|
|
return false
|
|
}
|
|
delete(s.codes, email)
|
|
return true
|
|
}
|
|
|
|
func (s *Sender) expireLoop() {
|
|
ticker := time.NewTicker(time.Minute)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
s.expireVerificationCodes()
|
|
case <-s.closeChan:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Sender) expireVerificationCodes() {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
now := time.Now()
|
|
for email, vc := range s.codes {
|
|
if now.After(vc.expires) {
|
|
delete(s.codes, email)
|
|
}
|
|
}
|
|
}
|