diff --git a/docs/publish.md b/docs/publish.md index 8a728f31..9f7e0355 100644 --- a/docs/publish.md +++ b/docs/publish.md @@ -2582,6 +2582,11 @@ format is: ntfy-$topic@ntfy.sh ``` +If [access control](config.md#access-control) is enabled, and the target topic does not support anonymous writes, e-mail publishing won't work without providing an authorized access token. That will change the format of the e-mail's recipient address to +``` +ntfy-$topic+$token@ntfy.sh +``` + As of today, e-mail publishing only supports adding a [message title](#message-title) (the e-mail subject). Tags, priority, delay and other features are not supported (yet). Here's an example that will publish a message with the title `You've Got Mail` to topic `sometopic` (see [ntfy.sh/sometopic](https://ntfy.sh/sometopic)): diff --git a/server/smtp_server.go b/server/smtp_server.go index 3e1df55f..f2c9dc9b 100644 --- a/server/smtp_server.go +++ b/server/smtp_server.go @@ -65,6 +65,7 @@ type smtpSession struct { backend *smtpBackend conn *smtp.Conn topic string + token string mu sync.Mutex } @@ -81,6 +82,7 @@ func (s *smtpSession) Mail(from string, opts *smtp.MailOptions) error { func (s *smtpSession) Rcpt(to string) error { logem(s.conn).Field("smtp_rcpt_to", to).Debug("RCPT TO: %s", to) return s.withFailCount(func() error { + token := "" conf := s.backend.config addressList, err := mail.ParseAddressList(to) if err != nil { @@ -92,18 +94,27 @@ func (s *smtpSession) Rcpt(to string) error { if !strings.HasSuffix(to, "@"+conf.SMTPServerDomain) { return errInvalidDomain } + // remove @ntfy.sh from end of email to = strings.TrimSuffix(to, "@"+conf.SMTPServerDomain) if conf.SMTPServerAddrPrefix != "" { if !strings.HasPrefix(to, conf.SMTPServerAddrPrefix) { return errInvalidAddress } + // remove ntfy- from beginning of email to = strings.TrimPrefix(to, conf.SMTPServerAddrPrefix) } + // if email contains token, split topic and token + if strings.Contains(to, "+") { + parts := strings.Split(to, "+") + to = parts[0] + token = parts[1] + } if !topicRegex.MatchString(to) { return errInvalidTopic } s.mu.Lock() s.topic = to + s.token = token s.mu.Unlock() return nil }) @@ -177,6 +188,9 @@ func (s *smtpSession) publishMessage(m *message) error { if m.Title != "" { req.Header.Set("Title", m.Title) } + if s.token != "" { + req.Header.Add("Authorization", "Bearer "+s.token) + } rr := httptest.NewRecorder() s.backend.handler(rr, req) if rr.Code != http.StatusOK { diff --git a/server/smtp_server_test.go b/server/smtp_server_test.go index 701ef211..49085d79 100644 --- a/server/smtp_server_test.go +++ b/server/smtp_server_test.go @@ -492,6 +492,27 @@ L0VOIj4KClRoaXMgaXMgYSB0ZXN0IG1lc3NhZ2UgZnJvbSBUcnVlTkFTIENPUkUuCg== writeAndReadUntilLine(t, email, c, scanner, "554 5.0.0 Error: transaction failed, blame it on the weather: multipart message nested too deep") } +func TestSmtpBackend_PlaintextWithToken(t *testing.T) { + email := `EHLO example.com +MAIL FROM: phil@example.com +RCPT TO: ntfy-mytopic+tk_KLORUqSqvNRLpY11DfkHVbHu9NGG2@ntfy.sh +DATA +Subject: Very short mail + +what's up +. +` + s, c, _, scanner := newTestSMTPServer(t, func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, "/mytopic", r.URL.Path) + require.Equal(t, "Very short mail", r.Header.Get("Title")) + require.Equal(t, "Bearer tk_KLORUqSqvNRLpY11DfkHVbHu9NGG2", r.Header.Get("Authorization")) + require.Equal(t, "what's up", readAll(t, r.Body)) + }) + defer s.Close() + defer c.Close() + writeAndReadUntilLine(t, email, c, scanner, "250 2.0.0 OK: queued") +} + type smtpHandlerFunc func(http.ResponseWriter, *http.Request) func newTestSMTPServer(t *testing.T, handler smtpHandlerFunc) (s *smtp.Server, c net.Conn, conf *Config, scanner *bufio.Scanner) {