mirror of
https://github.com/binwiederhier/ntfy.git
synced 2024-12-14 11:47:33 +00:00
Continued e-mail support
This commit is contained in:
parent
6b46eb46e2
commit
f553cdb282
10 changed files with 132 additions and 10 deletions
|
@ -45,6 +45,11 @@ func WithDelay(delay string) PublishOption {
|
||||||
return WithHeader("X-Delay", delay)
|
return WithHeader("X-Delay", delay)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithEmail instructs the server to also send the message to the given e-mail address
|
||||||
|
func WithEmail(email string) PublishOption {
|
||||||
|
return WithHeader("X-Email", email)
|
||||||
|
}
|
||||||
|
|
||||||
// WithNoCache instructs the server not to cache the message server-side
|
// WithNoCache instructs the server not to cache the message server-side
|
||||||
func WithNoCache() PublishOption {
|
func WithNoCache() PublishOption {
|
||||||
return WithHeader("X-Cache", "no")
|
return WithHeader("X-Cache", "no")
|
||||||
|
|
|
@ -2,10 +2,13 @@ package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
"heckel.io/ntfy/client"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -24,3 +27,11 @@ func newTestApp() (*cli.App, *bytes.Buffer, *bytes.Buffer, *bytes.Buffer) {
|
||||||
app.ErrWriter = &stderr
|
app.ErrWriter = &stderr
|
||||||
return app, &stdin, &stdout, &stderr
|
return app, &stdin, &stdout, &stderr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toMessage(t *testing.T, s string) *client.Message {
|
||||||
|
var m *client.Message
|
||||||
|
if err := json.NewDecoder(strings.NewReader(s)).Decode(&m); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ var cmdPublish = &cli.Command{
|
||||||
&cli.StringFlag{Name: "priority", Aliases: []string{"p"}, Usage: "priority of the message (1=min, 2=low, 3=default, 4=high, 5=max)"},
|
&cli.StringFlag{Name: "priority", Aliases: []string{"p"}, Usage: "priority of the message (1=min, 2=low, 3=default, 4=high, 5=max)"},
|
||||||
&cli.StringFlag{Name: "tags", Aliases: []string{"tag", "T"}, Usage: "comma separated list of tags and emojis"},
|
&cli.StringFlag{Name: "tags", Aliases: []string{"tag", "T"}, Usage: "comma separated list of tags and emojis"},
|
||||||
&cli.StringFlag{Name: "delay", Aliases: []string{"at", "in", "D"}, Usage: "delay/schedule message"},
|
&cli.StringFlag{Name: "delay", Aliases: []string{"at", "in", "D"}, Usage: "delay/schedule message"},
|
||||||
|
&cli.StringFlag{Name: "email", Aliases: []string{"e-mail", "mail", "e"}, Usage: "also send to e-mail address"},
|
||||||
&cli.BoolFlag{Name: "no-cache", Aliases: []string{"C"}, Usage: "do not cache message server-side"},
|
&cli.BoolFlag{Name: "no-cache", Aliases: []string{"C"}, Usage: "do not cache message server-side"},
|
||||||
&cli.BoolFlag{Name: "no-firebase", Aliases: []string{"F"}, Usage: "do not forward message to Firebase"},
|
&cli.BoolFlag{Name: "no-firebase", Aliases: []string{"F"}, Usage: "do not forward message to Firebase"},
|
||||||
&cli.BoolFlag{Name: "quiet", Aliases: []string{"q"}, Usage: "do print message"},
|
&cli.BoolFlag{Name: "quiet", Aliases: []string{"q"}, Usage: "do print message"},
|
||||||
|
@ -33,6 +34,7 @@ Examples:
|
||||||
ntfy pub --tags=warning,skull backups "Backups failed" # Add tags/emojis to message
|
ntfy pub --tags=warning,skull backups "Backups failed" # Add tags/emojis to message
|
||||||
ntfy pub --delay=10s delayed_topic Laterzz # Delay message by 10s
|
ntfy pub --delay=10s delayed_topic Laterzz # Delay message by 10s
|
||||||
ntfy pub --at=8:30am delayed_topic Laterzz # Send message at 8:30am
|
ntfy pub --at=8:30am delayed_topic Laterzz # Send message at 8:30am
|
||||||
|
ntfy pub -e phil@example.com alerts 'App is down!' # Also send email to phil@example.com
|
||||||
ntfy trigger mywebhook # Sending without message, useful for webhooks
|
ntfy trigger mywebhook # Sending without message, useful for webhooks
|
||||||
|
|
||||||
Please also check out the docs on publishing messages. Especially for the --tags and --delay options,
|
Please also check out the docs on publishing messages. Especially for the --tags and --delay options,
|
||||||
|
@ -54,6 +56,7 @@ func execPublish(c *cli.Context) error {
|
||||||
priority := c.String("priority")
|
priority := c.String("priority")
|
||||||
tags := c.String("tags")
|
tags := c.String("tags")
|
||||||
delay := c.String("delay")
|
delay := c.String("delay")
|
||||||
|
email := c.String("email")
|
||||||
noCache := c.Bool("no-cache")
|
noCache := c.Bool("no-cache")
|
||||||
noFirebase := c.Bool("no-firebase")
|
noFirebase := c.Bool("no-firebase")
|
||||||
quiet := c.Bool("quiet")
|
quiet := c.Bool("quiet")
|
||||||
|
@ -75,6 +78,9 @@ func execPublish(c *cli.Context) error {
|
||||||
if delay != "" {
|
if delay != "" {
|
||||||
options = append(options, client.WithDelay(delay))
|
options = append(options, client.WithDelay(delay))
|
||||||
}
|
}
|
||||||
|
if email != "" {
|
||||||
|
options = append(options, client.WithEmail(email))
|
||||||
|
}
|
||||||
if noCache {
|
if noCache {
|
||||||
options = append(options, client.WithNoCache())
|
options = append(options, client.WithNoCache())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"heckel.io/ntfy/test"
|
||||||
"heckel.io/ntfy/util"
|
"heckel.io/ntfy/util"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
@ -16,3 +18,19 @@ func TestCLI_Publish_Subscribe_Poll_Real_Server(t *testing.T) {
|
||||||
require.Nil(t, app2.Run([]string{"ntfy", "subscribe", "--poll", "ntfytest"}))
|
require.Nil(t, app2.Run([]string{"ntfy", "subscribe", "--poll", "ntfytest"}))
|
||||||
require.Contains(t, stdout.String(), testMessage)
|
require.Contains(t, stdout.String(), testMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCLI_Publish_Subscribe_Poll(t *testing.T) {
|
||||||
|
s, port := test.StartServer(t)
|
||||||
|
defer test.StopServer(t, s, port)
|
||||||
|
topic := fmt.Sprintf("http://127.0.0.1:%d/mytopic", port)
|
||||||
|
|
||||||
|
app, _, stdout, _ := newTestApp()
|
||||||
|
require.Nil(t, app.Run([]string{"ntfy", "publish", topic, "some message"}))
|
||||||
|
m := toMessage(t, stdout.String())
|
||||||
|
require.Equal(t, "some message", m.Message)
|
||||||
|
|
||||||
|
app2, _, stdout, _ := newTestApp()
|
||||||
|
require.Nil(t, app2.Run([]string{"ntfy", "subscribe", "--poll", topic}))
|
||||||
|
m = toMessage(t, stdout.String())
|
||||||
|
require.Equal(t, "some message", m.Message)
|
||||||
|
}
|
||||||
|
|
|
@ -592,6 +592,69 @@ Here's an example with a custom message, tags and a priority:
|
||||||
file_get_contents('https://ntfy.sh/mywebhook/publish?message=Webhook+triggered&priority=high&tags=warning,skull');
|
file_get_contents('https://ntfy.sh/mywebhook/publish?message=Webhook+triggered&priority=high&tags=warning,skull');
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 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'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).
|
||||||
|
|
||||||
|
=== "Command line (curl)"
|
||||||
|
```
|
||||||
|
curl -H "Email: phil@example.com" -d "You've Got Mail" ntfy.sh/alerts
|
||||||
|
curl -d "You've Got Mail" "ntfy.sh/alerts?email=phil@example.com"
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "ntfy CLI"
|
||||||
|
```
|
||||||
|
ntfy publish \
|
||||||
|
--email=phil@example.com \
|
||||||
|
alerts "You've Got Mail"
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "HTTP"
|
||||||
|
``` http
|
||||||
|
POST /alerts HTTP/1.1
|
||||||
|
Host: ntfy.sh
|
||||||
|
Email: phil@example.com
|
||||||
|
|
||||||
|
You've Got Mail
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "JavaScript"
|
||||||
|
``` javascript
|
||||||
|
fetch('https://ntfy.sh/alerts', {
|
||||||
|
method: 'POST',
|
||||||
|
body: "You've Got Mail",
|
||||||
|
headers: { 'Email': 'phil@example.com' }
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "Go"
|
||||||
|
``` go
|
||||||
|
req, _ := http.NewRequest("POST", "https://ntfy.sh/alerts", strings.NewReader("You've Got Mail"))
|
||||||
|
req.Header.Set("Email", "phil@example.com")
|
||||||
|
http.DefaultClient.Do(req)
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "Python"
|
||||||
|
``` python
|
||||||
|
requests.post("https://ntfy.sh/alerts",
|
||||||
|
data="You've Got Mail",
|
||||||
|
headers={ "Email": "phil@example.com" })
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "PHP"
|
||||||
|
``` php-inline
|
||||||
|
file_get_contents('https://ntfy.sh/alerts', false, stream_context_create([
|
||||||
|
'http' => [
|
||||||
|
'method' => 'POST',
|
||||||
|
'header' =>
|
||||||
|
"Content-Type: text/plain\r\n" .
|
||||||
|
"Email: phil@example.com",
|
||||||
|
'content' => 'You've Got Mail'
|
||||||
|
]
|
||||||
|
]));
|
||||||
|
```
|
||||||
|
|
||||||
## Advanced features
|
## Advanced features
|
||||||
|
|
||||||
### Message caching
|
### Message caching
|
||||||
|
@ -746,7 +809,8 @@ but just in case, let's list them all:
|
||||||
| Limit | Description |
|
| Limit | Description |
|
||||||
|---|---|
|
|---|---|
|
||||||
| **Message length** | Each message can be up to 512 bytes long. Longer messages are truncated. |
|
| **Message length** | Each message can be up to 512 bytes long. Longer messages are truncated. |
|
||||||
| **Requests per second** | By default, the server is configured to allow 60 requests at once, and then refills the your allowed requests bucket at a rate of one request per 10 seconds. You can read more about this in the [rate limiting](config.md#rate-limiting) section. |
|
| **Requests** | By default, the server is configured to allow 60 requests at once, and then refills the your allowed requests bucket at a rate of one request per 10 seconds. You can read more about this in the [rate limiting](config.md#rate-limiting) section. |
|
||||||
|
| **E-mails** | By default, the server is configured to allow 16 e-mails at once, and then refills the your allowed e-mail bucket at a rate of one per hour. You can read more about this in the [rate limiting](config.md#rate-limiting) section. |
|
||||||
| **Subscription limits** | By default, the server allows each visitor to keep 30 connections to the server open. |
|
| **Subscription limits** | By default, the server allows each visitor to keep 30 connections to the server open. |
|
||||||
| **Total number of topics** | By default, the server is configured to allow 5,000 topics. The ntfy.sh server has higher limits though. |
|
| **Total number of topics** | By default, the server is configured to allow 5,000 topics. The ntfy.sh server has higher limits though. |
|
||||||
|
|
||||||
|
|
|
@ -8,14 +8,14 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type mailer interface {
|
type mailer interface {
|
||||||
Send(to string, m *message) error
|
Send(from, to string, m *message) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type smtpMailer struct {
|
type smtpMailer struct {
|
||||||
config *Config
|
config *Config
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *smtpMailer) Send(to string, m *message) error {
|
func (s *smtpMailer) Send(from, 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
|
||||||
|
@ -26,10 +26,18 @@ func (s *smtpMailer) Send(to string, m *message) error {
|
||||||
}
|
}
|
||||||
subject += " - " + m.Topic
|
subject += " - " + m.Topic
|
||||||
subject = strings.ReplaceAll(strings.ReplaceAll(subject, "\r", ""), "\n", " ")
|
subject = strings.ReplaceAll(strings.ReplaceAll(subject, "\r", ""), "\n", " ")
|
||||||
|
message := m.Message
|
||||||
|
if len(m.Tags) > 0 {
|
||||||
|
message += "\nTags: " + strings.Join(m.Tags, ", ") // FIXME emojis
|
||||||
|
}
|
||||||
|
if m.Priority != 0 && m.Priority != 3 {
|
||||||
|
message += fmt.Sprintf("\nPriority: %d", m.Priority) // FIXME to string
|
||||||
|
}
|
||||||
|
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"+
|
msg := []byte(fmt.Sprintf("From: %s\r\n"+
|
||||||
"To: %s\r\n"+
|
"To: %s\r\n"+
|
||||||
"Subject: %s\r\n\r\n"+
|
"Subject: %s\r\n\r\n"+
|
||||||
"%s\r\n", s.config.SMTPFrom, to, subject, m.Message))
|
"%s\r\n", s.config.SMTPFrom, to, subject, message))
|
||||||
auth := smtp.PlainAuth("", s.config.SMTPUser, s.config.SMTPPass, host)
|
auth := smtp.PlainAuth("", s.config.SMTPUser, s.config.SMTPPass, host)
|
||||||
return smtp.SendMail(s.config.SMTPAddr, auth, s.config.SMTPFrom, []string{to}, msg)
|
return smtp.SendMail(s.config.SMTPAddr, auth, s.config.SMTPFrom, []string{to}, msg)
|
||||||
}
|
}
|
||||||
|
|
|
@ -344,7 +344,7 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito
|
||||||
}
|
}
|
||||||
if s.mailer != nil && email != "" && !delayed {
|
if s.mailer != nil && email != "" && !delayed {
|
||||||
go func() {
|
go func() {
|
||||||
if err := s.mailer.Send(email, m); err != nil {
|
if err := s.mailer.Send(v.ip, email, m); err != nil {
|
||||||
log.Printf("Unable to send email: %v", err.Error())
|
log.Printf("Unable to send email: %v", err.Error())
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
@ -772,7 +772,7 @@ func (s *Server) visitor(r *http.Request) *visitor {
|
||||||
}
|
}
|
||||||
v, exists := s.visitors[ip]
|
v, exists := s.visitors[ip]
|
||||||
if !exists {
|
if !exists {
|
||||||
s.visitors[ip] = newVisitor(s.config)
|
s.visitors[ip] = newVisitor(s.config, ip)
|
||||||
return s.visitors[ip]
|
return s.visitors[ip]
|
||||||
}
|
}
|
||||||
v.Keepalive()
|
v.Keepalive()
|
||||||
|
|
|
@ -511,10 +511,10 @@ func TestServer_Curl_Publish_Poll(t *testing.T) {
|
||||||
|
|
||||||
type testMailer struct {
|
type testMailer struct {
|
||||||
count int
|
count int
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *testMailer) Send(to string, m *message) error {
|
func (t *testMailer) Send(from, to string, m *message) error {
|
||||||
t.mu.Lock()
|
t.mu.Lock()
|
||||||
defer t.mu.Unlock()
|
defer t.mu.Unlock()
|
||||||
t.count++
|
t.count++
|
||||||
|
|
|
@ -17,6 +17,7 @@ const (
|
||||||
// visitor represents an API user, and its associated rate.Limiter used for rate limiting
|
// visitor represents an API user, and its associated rate.Limiter used for rate limiting
|
||||||
type visitor struct {
|
type visitor struct {
|
||||||
config *Config
|
config *Config
|
||||||
|
ip string
|
||||||
requests *rate.Limiter
|
requests *rate.Limiter
|
||||||
emails *rate.Limiter
|
emails *rate.Limiter
|
||||||
subscriptions *util.Limiter
|
subscriptions *util.Limiter
|
||||||
|
@ -24,9 +25,10 @@ type visitor struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func newVisitor(conf *Config) *visitor {
|
func newVisitor(conf *Config, ip string) *visitor {
|
||||||
return &visitor{
|
return &visitor{
|
||||||
config: conf,
|
config: conf,
|
||||||
|
ip: ip,
|
||||||
requests: rate.NewLimiter(rate.Every(conf.VisitorRequestLimitReplenish), conf.VisitorRequestLimitBurst),
|
requests: rate.NewLimiter(rate.Every(conf.VisitorRequestLimitReplenish), conf.VisitorRequestLimitBurst),
|
||||||
emails: rate.NewLimiter(rate.Every(conf.VisitorEmailLimitReplenish), conf.VisitorEmailLimitBurst),
|
emails: rate.NewLimiter(rate.Every(conf.VisitorEmailLimitReplenish), conf.VisitorEmailLimitBurst),
|
||||||
subscriptions: util.NewLimiter(int64(conf.VisitorSubscriptionLimit)),
|
subscriptions: util.NewLimiter(int64(conf.VisitorSubscriptionLimit)),
|
||||||
|
@ -34,6 +36,10 @@ func newVisitor(conf *Config) *visitor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *visitor) IP() string {
|
||||||
|
return v.ip
|
||||||
|
}
|
||||||
|
|
||||||
func (v *visitor) RequestAllowed() error {
|
func (v *visitor) RequestAllowed() error {
|
||||||
if !v.requests.Allow() {
|
if !v.requests.Allow() {
|
||||||
return errHTTPTooManyRequests
|
return errHTTPTooManyRequests
|
||||||
|
|
|
@ -15,8 +15,12 @@ func init() {
|
||||||
|
|
||||||
// StartServer starts a server.Server with a random port and waits for the server to be up
|
// StartServer starts a server.Server with a random port and waits for the server to be up
|
||||||
func StartServer(t *testing.T) (*server.Server, int) {
|
func StartServer(t *testing.T) (*server.Server, int) {
|
||||||
|
return StartServerWithConfig(t, server.NewConfig())
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartServerWithConfig starts a server.Server with a random port and waits for the server to be up
|
||||||
|
func StartServerWithConfig(t *testing.T, conf *server.Config) (*server.Server, int) {
|
||||||
port := 10000 + rand.Intn(20000)
|
port := 10000 + rand.Intn(20000)
|
||||||
conf := server.NewConfig()
|
|
||||||
conf.ListenHTTP = fmt.Sprintf(":%d", port)
|
conf.ListenHTTP = fmt.Sprintf(":%d", port)
|
||||||
s, err := server.New(conf)
|
s, err := server.New(conf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
Loading…
Reference in a new issue