Fully working email feature
This commit is contained in:
		
							parent
							
								
									f553cdb282
								
							
						
					
					
						commit
						11b5ac49c0
					
				
					 7 changed files with 259 additions and 15 deletions
				
			
		|  | @ -12,6 +12,7 @@ import ( | ||||||
| 
 | 
 | ||||||
| var flagsServe = []cli.Flag{ | 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"}, | 	&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-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: "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"}), | 	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 | 	// Read all the options | ||||||
|  | 	baseURL := c.String("base-url") | ||||||
| 	listenHTTP := c.String("listen-http") | 	listenHTTP := c.String("listen-http") | ||||||
| 	listenHTTPS := c.String("listen-https") | 	listenHTTPS := c.String("listen-https") | ||||||
| 	keyFile := c.String("key-file") | 	keyFile := c.String("key-file") | ||||||
|  | @ -93,12 +95,13 @@ func execServe(c *cli.Context) error { | ||||||
| 		return errors.New("if set, certificate file must exist") | 		return errors.New("if set, certificate file must exist") | ||||||
| 	} else if listenHTTPS != "" && (keyFile == "" || certFile == "") { | 	} else if listenHTTPS != "" && (keyFile == "" || certFile == "") { | ||||||
| 		return errors.New("if listen-https is set, both key-file and cert-file must be set") | 		return errors.New("if listen-https is set, both key-file and cert-file must be set") | ||||||
| 	} else if smtpAddr != "" && (smtpUser == "" || smtpPass == "" || smtpFrom == "") { | 	} else if smtpAddr != "" && (baseURL == "" || smtpUser == "" || smtpPass == "" || smtpFrom == "") { | ||||||
| 		return errors.New("if smtp-addr is set, smtp-user, smtp-pass and smtp-from must also be set") | 		return errors.New("if smtp-addr is set, base-url, smtp-user, smtp-pass and smtp-from must also be set") | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Run server | 	// Run server | ||||||
| 	conf := server.NewConfig() | 	conf := server.NewConfig() | ||||||
|  | 	conf.BaseURL = baseURL | ||||||
| 	conf.ListenHTTP = listenHTTP | 	conf.ListenHTTP = listenHTTP | ||||||
| 	conf.ListenHTTPS = listenHTTPS | 	conf.ListenHTTPS = listenHTTPS | ||||||
| 	conf.KeyFile = keyFile | 	conf.KeyFile = keyFile | ||||||
|  |  | ||||||
|  | @ -593,9 +593,11 @@ Here's an example with a custom message, tags and a priority: | ||||||
|     ``` |     ``` | ||||||
| 
 | 
 | ||||||
| ## Publish as e-mail | ## 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, | 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)" | === "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-Priority` | `Priority`, `prio`, `p` | [Message priority](#message-priority) | | ||||||
| | `X-Tags` | `Tags`, `Tag`, `ta` | [Tags and emojis](#tags-emojis) | | | `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-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-Cache` | `Cache` | Allows disabling [message caching](#message-caching) | | ||||||
| | `X-Firebase` | `Firebase` | Allows disabling [sending to Firebase](#disable-firebase) | | | `X-Firebase` | `Firebase` | Allows disabling [sending to Firebase](#disable-firebase) | | ||||||
|  |  | ||||||
|  | @ -33,6 +33,7 @@ const ( | ||||||
| 
 | 
 | ||||||
| // Config is the main config struct for the application. Use New to instantiate a default config struct. | // Config is the main config struct for the application. Use New to instantiate a default config struct. | ||||||
| type Config struct { | type Config struct { | ||||||
|  | 	BaseURL                      string | ||||||
| 	ListenHTTP                   string | 	ListenHTTP                   string | ||||||
| 	ListenHTTPS                  string | 	ListenHTTPS                  string | ||||||
| 	KeyFile                      string | 	KeyFile                      string | ||||||
|  | @ -63,6 +64,7 @@ type Config struct { | ||||||
| // NewConfig instantiates a default new server config | // NewConfig instantiates a default new server config | ||||||
| func NewConfig() *Config { | func NewConfig() *Config { | ||||||
| 	return &Config{ | 	return &Config{ | ||||||
|  | 		BaseURL:                      "", | ||||||
| 		ListenHTTP:                   DefaultListenHTTP, | 		ListenHTTP:                   DefaultListenHTTP, | ||||||
| 		ListenHTTPS:                  "", | 		ListenHTTPS:                  "", | ||||||
| 		KeyFile:                      "", | 		KeyFile:                      "", | ||||||
|  |  | ||||||
|  | @ -1,10 +1,14 @@ | ||||||
| package server | package server | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	_ "embed" // required by go:embed | ||||||
|  | 	"encoding/json" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"heckel.io/ntfy/util" | ||||||
| 	"net" | 	"net" | ||||||
| 	"net/smtp" | 	"net/smtp" | ||||||
| 	"strings" | 	"strings" | ||||||
|  | 	"time" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| type mailer interface { | type mailer interface { | ||||||
|  | @ -15,29 +19,99 @@ type smtpMailer struct { | ||||||
| 	config *Config | 	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) | 	host, _, err := net.SplitHostPort(s.config.SMTPAddr) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		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 | 	subject := m.Title | ||||||
| 	if subject == "" { | 	if subject == "" { | ||||||
| 		subject = m.Message | 		subject = m.Message | ||||||
| 	} | 	} | ||||||
| 	subject += " - " + m.Topic |  | ||||||
| 	subject = strings.ReplaceAll(strings.ReplaceAll(subject, "\r", ""), "\n", " ") | 	subject = strings.ReplaceAll(strings.ReplaceAll(subject, "\r", ""), "\n", " ") | ||||||
| 	message := m.Message | 	message := m.Message | ||||||
|  | 	trailer := "" | ||||||
| 	if len(m.Tags) > 0 { | 	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 { | 	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 | ||||||
| 		} | 		} | ||||||
| 	message += fmt.Sprintf("\n\n--\nMessage was sent via %s by client %s", m.Topic, from) // FIXME short URL | 		if trailer != "" { | ||||||
| 	msg := []byte(fmt.Sprintf("From: %s\r\n"+ | 			trailer += "\n" | ||||||
| 		"To: %s\r\n"+ | 		} | ||||||
| 		"Subject: %s\r\n\r\n"+ | 		trailer += fmt.Sprintf("Priority: %s", priority) | ||||||
| 		"%s\r\n", s.config.SMTPFrom, to, subject, message)) | 	} | ||||||
| 	auth := smtp.PlainAuth("", s.config.SMTPUser, s.config.SMTPPass, host) | 	if trailer != "" { | ||||||
| 	return smtp.SendMail(s.config.SMTPAddr, auth, s.config.SMTPFrom, []string{to}, msg) | 		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 | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										1
									
								
								server/mailer_emoji.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								server/mailer_emoji.json
									
										
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										141
									
								
								server/mailer_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								server/mailer_test.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -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
									
								
								util/util.go
									
										
									
									
									
								
							
							
						
						
									
										20
									
								
								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 | // ExpandHome replaces "~" with the user's home directory | ||||||
| func ExpandHome(path string) string { | func ExpandHome(path string) string { | ||||||
| 	return os.ExpandEnv(strings.ReplaceAll(path, "~", "$HOME")) | 	return os.ExpandEnv(strings.ReplaceAll(path, "~", "$HOME")) | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue