ntfy/server/smtp_sender.go
Karmanyaah Malhotra c2382d29a1 refactor visitor IPs and allow exempting IP Ranges
Use netip.Addr instead of storing addresses as strings. This requires
conversions at the database level and in tests, but is more memory
efficient otherwise, and facilitates the following.

Parse rate limit exemptions as netip.Prefix. This allows storing IP
ranges in the exemption list. Regular IP addresses (entered explicitly
or resolved from hostnames) are IPV4/32, denoting a range of one
address.
2022-10-05 16:04:42 -05:00

149 lines
3.8 KiB
Go

package server
import (
_ "embed" // required by go:embed
"encoding/json"
"fmt"
"heckel.io/ntfy/log"
"heckel.io/ntfy/util"
"mime"
"net"
"net/smtp"
"strings"
"sync"
"time"
)
type mailer interface {
Send(v *visitor, m *message, to string) error
Counts() (total int64, success int64, failure int64)
}
type smtpSender struct {
config *Config
success int64
failure int64
mu sync.Mutex
}
func (s *smtpSender) Send(v *visitor, m *message, to string) error {
return s.withCount(v, m, func() error {
host, _, err := net.SplitHostPort(s.config.SMTPSenderAddr)
if err != nil {
return err
}
message, err := formatMail(s.config.BaseURL, v.ip.String(), s.config.SMTPSenderFrom, to, m)
if err != nil {
return err
}
auth := smtp.PlainAuth("", s.config.SMTPSenderUser, s.config.SMTPSenderPass, host)
log.Debug("%s Sending mail: via=%s, user=%s, pass=***, to=%s", logMessagePrefix(v, m), s.config.SMTPSenderAddr, s.config.SMTPSenderUser, to)
log.Trace("%s Mail body: %s", logMessagePrefix(v, m), message)
return smtp.SendMail(s.config.SMTPSenderAddr, auth, s.config.SMTPSenderFrom, []string{to}, []byte(message))
})
}
func (s *smtpSender) Counts() (total int64, success int64, failure int64) {
s.mu.Lock()
defer s.mu.Unlock()
return s.success + s.failure, s.success, s.failure
}
func (s *smtpSender) withCount(v *visitor, m *message, fn func() error) error {
err := fn()
s.mu.Lock()
defer s.mu.Unlock()
if err != nil {
log.Debug("%s Sending mail failed: %s", logMessagePrefix(v, m), err.Error())
s.failure++
} else {
s.success++
}
return err
}
func formatMail(baseURL, senderIP, from, to string, m *message) (string, error) {
topicURL := baseURL + "/" + m.Topic
subject := m.Title
if subject == "" {
subject = m.Message
}
subject = strings.ReplaceAll(strings.ReplaceAll(subject, "\r", ""), "\n", " ")
message := m.Message
trailer := ""
if len(m.Tags) > 0 {
emojis, tags, err := toEmojis(m.Tags)
if err != nil {
return "", err
}
if len(emojis) > 0 {
subject = strings.Join(emojis, " ") + " " + subject
}
if len(tags) > 0 {
trailer = "Tags: " + strings.Join(tags, ", ")
}
}
if m.Priority != 0 && m.Priority != 3 {
priority, err := util.PriorityString(m.Priority)
if err != nil {
return "", err
}
if trailer != "" {
trailer += "\n"
}
trailer += fmt.Sprintf("Priority: %s", priority)
}
if trailer != "" {
message += "\n\n" + trailer
}
subject = mime.BEncoding.Encode("utf-8", subject)
body := `From: "{shortTopicURL}" <{from}>
To: {to}
Subject: {subject}
Content-Type: text/plain; charset="utf-8"
{message}
--
This message was sent by {ip} at {time} via {topicURL}`
body = strings.ReplaceAll(body, "{from}", from)
body = strings.ReplaceAll(body, "{to}", to)
body = strings.ReplaceAll(body, "{subject}", subject)
body = strings.ReplaceAll(body, "{message}", message)
body = strings.ReplaceAll(body, "{topicURL}", topicURL)
body = strings.ReplaceAll(body, "{shortTopicURL}", util.ShortTopicURL(topicURL))
body = strings.ReplaceAll(body, "{time}", time.Unix(m.Time, 0).UTC().Format(time.RFC1123))
body = strings.ReplaceAll(body, "{ip}", senderIP)
return body, nil
}
var (
//go:embed "mailer_emoji.json"
emojisJSON string
)
type emoji struct {
Emoji string `json:"emoji"`
Aliases []string `json:"aliases"`
}
func toEmojis(tags []string) (emojisOut []string, tagsOut []string, err error) {
var emojis []emoji
if err = json.Unmarshal([]byte(emojisJSON), &emojis); err != nil {
return nil, nil, err
}
tagsOut = make([]string, 0)
emojisOut = make([]string, 0)
nextTag:
for _, t := range tags { // TODO Super inefficient; we should just create a .json file with a map
for _, e := range emojis {
if util.Contains(e.Aliases, t) {
emojisOut = append(emojisOut, e.Emoji)
continue nextTag
}
}
tagsOut = append(tagsOut, t)
}
return
}