Philipp Heckel 4 лет назад
Родитель
Сommit
11b5ac49c0
7 измененных файлов с 258 добавлено и 15 удалено
  1. 5 2
      cmd/serve.go
  2. 5 2
      docs/publish.md
  3. 2 0
      server/config.go
  4. 85 11
      server/mailer.go
  5. 0 0
      server/mailer_emoji.json
  6. 141 0
      server/mailer_test.go
  7. 20 0
      util/util.go

+ 5 - 2
cmd/serve.go

@@ -12,6 +12,7 @@ import (
 
 var flagsServe = []cli.Flag{
 	&cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG_FILE"}, Value: "/etc/ntfy/server.yml", DefaultText: "/etc/ntfy/server.yml", Usage: "config file"},
+	altsrc.NewStringFlag(&cli.StringFlag{Name: "base-url", Aliases: []string{"B"}, EnvVars: []string{"NTFY_BASE_URL"}, Usage: "externally visible base URL for this host (e.g. https://ntfy.sh)"}),
 	altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-http", Aliases: []string{"l"}, EnvVars: []string{"NTFY_LISTEN_HTTP"}, Value: server.DefaultListenHTTP, Usage: "ip:port used to as HTTP listen address"}),
 	altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-https", Aliases: []string{"L"}, EnvVars: []string{"NTFY_LISTEN_HTTPS"}, Usage: "ip:port used to as HTTPS listen address"}),
 	altsrc.NewStringFlag(&cli.StringFlag{Name: "key-file", Aliases: []string{"K"}, EnvVars: []string{"NTFY_KEY_FILE"}, Usage: "private key file, if listen-https is set"}),
@@ -57,6 +58,7 @@ func execServe(c *cli.Context) error {
 	}
 
 	// Read all the options
+	baseURL := c.String("base-url")
 	listenHTTP := c.String("listen-http")
 	listenHTTPS := c.String("listen-https")
 	keyFile := c.String("key-file")
@@ -93,12 +95,13 @@ func execServe(c *cli.Context) error {
 		return errors.New("if set, certificate file must exist")
 	} else if listenHTTPS != "" && (keyFile == "" || certFile == "") {
 		return errors.New("if listen-https is set, both key-file and cert-file must be set")
-	} else if smtpAddr != "" && (smtpUser == "" || smtpPass == "" || smtpFrom == "") {
-		return errors.New("if smtp-addr is set, smtp-user, smtp-pass and smtp-from must also be set")
+	} else if smtpAddr != "" && (baseURL == "" || smtpUser == "" || smtpPass == "" || smtpFrom == "") {
+		return errors.New("if smtp-addr is set, base-url, smtp-user, smtp-pass and smtp-from must also be set")
 	}
 
 	// Run server
 	conf := server.NewConfig()
+	conf.BaseURL = baseURL
 	conf.ListenHTTP = listenHTTP
 	conf.ListenHTTPS = listenHTTPS
 	conf.KeyFile = keyFile

+ 5 - 2
docs/publish.md

@@ -593,9 +593,11 @@ Here's an example with a custom message, tags and a priority:
     ```
 
 ## Publish as e-mail
-You can forward messages to e-mail by specifying an e-mail address in the header. This can be useful for messages that 
+You can forward messages to e-mail by specifying an address in the header. This can be useful for messages that 
 you'd like to persist longer, or to blast-notify yourself on all possible channels. Since ntfy does not provide auth,
-the [rate limiting](#limitations) is pretty strict (see below).
+the [rate limiting](#limitations) is pretty strict (see below). In the default configuration, you get 16 e-mails per 
+visitor (IP address) and then after that one per hour. On top of that, your IP address appears in the e-mail body. This 
+is to prevent abuse. 
 
 === "Command line (curl)"
     ```
@@ -825,5 +827,6 @@ and can be passed as **HTTP headers** or **query parameters in the URL**. They a
 | `X-Priority` | `Priority`, `prio`, `p` | [Message priority](#message-priority) |
 | `X-Tags` | `Tags`, `Tag`, `ta` | [Tags and emojis](#tags-emojis) |
 | `X-Delay` | `Delay`, `X-At`, `At`, `X-In`, `In` | Timestamp or duration for [delayed delivery](#scheduled-delivery) |
+| `X-Email` | `X-E-Mail`, `Email`, `E-Mail`, `mail`, `e` | E-mail address for [e-mail delivery](#publish-as-e-mail) |
 | `X-Cache` | `Cache` | Allows disabling [message caching](#message-caching) |
 | `X-Firebase` | `Firebase` | Allows disabling [sending to Firebase](#disable-firebase) |

+ 2 - 0
server/config.go

@@ -33,6 +33,7 @@ const (
 
 // Config is the main config struct for the application. Use New to instantiate a default config struct.
 type Config struct {
+	BaseURL                      string
 	ListenHTTP                   string
 	ListenHTTPS                  string
 	KeyFile                      string
@@ -63,6 +64,7 @@ type Config struct {
 // NewConfig instantiates a default new server config
 func NewConfig() *Config {
 	return &Config{
+		BaseURL:                      "",
 		ListenHTTP:                   DefaultListenHTTP,
 		ListenHTTPS:                  "",
 		KeyFile:                      "",

+ 85 - 11
server/mailer.go

@@ -1,10 +1,14 @@
 package server
 
 import (
+	_ "embed" // required by go:embed
+	"encoding/json"
 	"fmt"
+	"heckel.io/ntfy/util"
 	"net"
 	"net/smtp"
 	"strings"
+	"time"
 )
 
 type mailer interface {
@@ -15,29 +19,99 @@ type smtpMailer struct {
 	config *Config
 }
 
-func (s *smtpMailer) Send(from, to string, m *message) error {
+func (s *smtpMailer) Send(senderIP, to string, m *message) error {
 	host, _, err := net.SplitHostPort(s.config.SMTPAddr)
 	if err != nil {
 		return err
 	}
+	message, err := formatMail(s.config.BaseURL, senderIP, s.config.SMTPFrom, to, m)
+	if err != nil {
+		return err
+	}
+	auth := smtp.PlainAuth("", s.config.SMTPUser, s.config.SMTPPass, host)
+	return smtp.SendMail(s.config.SMTPAddr, auth, s.config.SMTPFrom, []string{to}, []byte(message))
+}
+
+func formatMail(baseURL, senderIP, from, to string, m *message) (string, error) {
+	topicURL := baseURL + "/" + m.Topic
 	subject := m.Title
 	if subject == "" {
 		subject = m.Message
 	}
-	subject += " - " + m.Topic
 	subject = strings.ReplaceAll(strings.ReplaceAll(subject, "\r", ""), "\n", " ")
 	message := m.Message
+	trailer := ""
 	if len(m.Tags) > 0 {
-		message += "\nTags: " + strings.Join(m.Tags, ", ") // FIXME emojis
+		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 {
-		message += fmt.Sprintf("\nPriority: %d", m.Priority) // FIXME to string
+		priority, err := util.PriorityString(m.Priority)
+		if err != nil {
+			return "", err
+		}
+		if trailer != "" {
+			trailer += "\n"
+		}
+		trailer += fmt.Sprintf("Priority: %s", priority)
 	}
-	message += fmt.Sprintf("\n\n--\nMessage was sent via %s by client %s", m.Topic, from) // FIXME short URL
-	msg := []byte(fmt.Sprintf("From: %s\r\n"+
-		"To: %s\r\n"+
-		"Subject: %s\r\n\r\n"+
-		"%s\r\n", s.config.SMTPFrom, to, subject, message))
-	auth := smtp.PlainAuth("", s.config.SMTPUser, s.config.SMTPPass, host)
-	return smtp.SendMail(s.config.SMTPAddr, auth, s.config.SMTPFrom, []string{to}, msg)
+	if trailer != "" {
+		message += "\n\n" + trailer
+	}
+	body := `Content-Type: text/plain; charset="utf-8"
+From: "{shortTopicURL}" <{from}>
+To: {to}
+Subject: {subject}
+
+{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.InStringList(e.Aliases, t) {
+				emojisOut = append(emojisOut, e.Emoji)
+				continue nextTag
+			}
+		}
+		tagsOut = append(tagsOut, t)
+	}
+	return
 }

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
server/mailer_emoji.json


+ 141 - 0
server/mailer_test.go

@@ -0,0 +1,141 @@
+package server
+
+import (
+	"github.com/stretchr/testify/require"
+	"testing"
+)
+
+func TestFormatMail_Basic(t *testing.T) {
+	actual, _ := formatMail("https://ntfy.sh", "1.2.3.4", "ntfy@ntfy.sh", "phil@example.com", &message{
+		ID:      "abc",
+		Time:    1640382204,
+		Event:   "message",
+		Topic:   "alerts",
+		Message: "A simple message",
+	})
+	expected := `Content-Type: text/plain; charset="utf-8"
+From: "ntfy.sh/alerts" <ntfy@ntfy.sh>
+To: phil@example.com
+Subject: A simple message
+
+A simple message
+
+--
+This message was sent by 1.2.3.4 at Fri, 24 Dec 2021 21:43:24 UTC via https://ntfy.sh/alerts`
+	require.Equal(t, expected, actual)
+}
+
+func TestFormatMail_JustEmojis(t *testing.T) {
+	actual, _ := formatMail("https://ntfy.sh", "1.2.3.4", "ntfy@ntfy.sh", "phil@example.com", &message{
+		ID:      "abc",
+		Time:    1640382204,
+		Event:   "message",
+		Topic:   "alerts",
+		Message: "A simple message",
+		Tags:    []string{"grinning"},
+	})
+	expected := `Content-Type: text/plain; charset="utf-8"
+From: "ntfy.sh/alerts" <ntfy@ntfy.sh>
+To: phil@example.com
+Subject: 😀 A simple message
+
+A simple message
+
+--
+This message was sent by 1.2.3.4 at Fri, 24 Dec 2021 21:43:24 UTC via https://ntfy.sh/alerts`
+	require.Equal(t, expected, actual)
+}
+
+func TestFormatMail_JustOtherTags(t *testing.T) {
+	actual, _ := formatMail("https://ntfy.sh", "1.2.3.4", "ntfy@ntfy.sh", "phil@example.com", &message{
+		ID:      "abc",
+		Time:    1640382204,
+		Event:   "message",
+		Topic:   "alerts",
+		Message: "A simple message",
+		Tags:    []string{"not-an-emoji"},
+	})
+	expected := `Content-Type: text/plain; charset="utf-8"
+From: "ntfy.sh/alerts" <ntfy@ntfy.sh>
+To: phil@example.com
+Subject: A simple message
+
+A simple message
+
+Tags: not-an-emoji
+
+--
+This message was sent by 1.2.3.4 at Fri, 24 Dec 2021 21:43:24 UTC via https://ntfy.sh/alerts`
+	require.Equal(t, expected, actual)
+}
+
+func TestFormatMail_JustPriority(t *testing.T) {
+	actual, _ := formatMail("https://ntfy.sh", "1.2.3.4", "ntfy@ntfy.sh", "phil@example.com", &message{
+		ID:       "abc",
+		Time:     1640382204,
+		Event:    "message",
+		Topic:    "alerts",
+		Message:  "A simple message",
+		Priority: 2,
+	})
+	expected := `Content-Type: text/plain; charset="utf-8"
+From: "ntfy.sh/alerts" <ntfy@ntfy.sh>
+To: phil@example.com
+Subject: A simple message
+
+A simple message
+
+Priority: low
+
+--
+This message was sent by 1.2.3.4 at Fri, 24 Dec 2021 21:43:24 UTC via https://ntfy.sh/alerts`
+	require.Equal(t, expected, actual)
+}
+
+func TestFormatMail_UTF8Subject(t *testing.T) {
+	actual, _ := formatMail("https://ntfy.sh", "1.2.3.4", "ntfy@ntfy.sh", "phil@example.com", &message{
+		ID:      "abc",
+		Time:    1640382204,
+		Event:   "message",
+		Topic:   "alerts",
+		Message: "A simple message",
+		Title:   " :: A not so simple title öäüß ¡Hola, señor!",
+	})
+	expected := `Content-Type: text/plain; charset="utf-8"
+From: "ntfy.sh/alerts" <ntfy@ntfy.sh>
+To: phil@example.com
+Subject:  :: A not so simple title öäüß ¡Hola, señor!
+
+A simple message
+
+--
+This message was sent by 1.2.3.4 at Fri, 24 Dec 2021 21:43:24 UTC via https://ntfy.sh/alerts`
+	require.Equal(t, expected, actual)
+}
+
+func TestFormatMail_WithAllTheThings(t *testing.T) {
+	actual, _ := formatMail("https://ntfy.sh", "1.2.3.4", "ntfy@ntfy.sh", "phil@example.com", &message{
+		ID:       "abc",
+		Time:     1640382204,
+		Event:    "message",
+		Topic:    "alerts",
+		Priority: 5,
+		Tags:     []string{"warning", "skull", "tag123", "other"},
+		Title:    "Oh no 🙈\nThis is a message across\nmultiple lines",
+		Message:  "A message that contains monkeys 🙉\nNo really, though. Monkeys!",
+	})
+	expected := `Content-Type: text/plain; charset="utf-8"
+From: "ntfy.sh/alerts" <ntfy@ntfy.sh>
+To: phil@example.com
+Subject: ⚠️ 💀 Oh no 🙈 This is a message across multiple lines
+
+A message that contains monkeys 🙉
+No really, though. Monkeys!
+
+Tags: tag123, other
+Priority: urgent
+
+--
+This message was sent by 1.2.3.4 at Fri, 24 Dec 2021 21:43:24 UTC via https://ntfy.sh/alerts`
+	require.Equal(t, expected, actual)
+}

+ 20 - 0
util/util.go

@@ -134,6 +134,26 @@ func ParsePriority(priority string) (int, error) {
 	}
 }
 
+// PriorityString converts a priority number to a string
+func PriorityString(priority int) (string, error) {
+	switch priority {
+	case 0:
+		return "default", nil
+	case 1:
+		return "min", nil
+	case 2:
+		return "low", nil
+	case 3:
+		return "default", nil
+	case 4:
+		return "high", nil
+	case 5:
+		return "urgent", nil
+	default:
+		return "", errInvalidPriority
+	}
+}
+
 // ExpandHome replaces "~" with the user's home directory
 func ExpandHome(path string) string {
 	return os.ExpandEnv(strings.ReplaceAll(path, "~", "$HOME"))

Некоторые файлы не были показаны из-за большого количества измененных файлов