This commit is contained in:
santanoce 2026-05-13 15:49:25 +02:00 committed by GitHub
commit 7e1019ca5a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 73 additions and 1 deletions

View file

@ -1023,7 +1023,7 @@ you can set the `X-Email` header to [send messages via e-mail](publish.md#e-mail
On ntfy.sh, anonymous email sending was disabled due to abuse. To use the email notification feature,
you must verify your email in the web app's [Account section](https://ntfy.sh/account).
As of today, only SMTP servers with PLAIN auth and STARTLS are supported. To enable e-mail sending, you must set the
As of today, SMTP servers with PLAIN and LOGIN auth and STARTLS are supported. To enable e-mail sending, you must set the
following settings:
* `base-url` is the root URL for the ntfy server; this is needed for e-mail footer

View file

@ -1,10 +1,14 @@
package server
import (
"bytes"
_ "embed" // required by go:embed
"encoding/json"
"errors"
"fmt"
"mime"
"net/smtp"
"slices"
"strings"
"sync"
"time"
@ -15,11 +19,79 @@ import (
"heckel.io/ntfy/v2/util"
)
var (
errUnencryptedConnection = errors.New("unencrypted connection")
errWrongHostname = errors.New("wrong host name")
errNoSupportedAuth = errors.New("no supported auth mechanisms found")
errUnexpectedServerChallenge = errors.New("unexpected server challenge")
)
type mailer interface {
Send(v *visitor, m *model.Message, to string) error
Counts() (total int64, success int64, failure int64)
}
type plainOrLoginAuth struct {
identity string
username string
password string
host string
authMethod string
}
func PlainOrLoginAuth(identity, username, password, host string) smtp.Auth {
return &plainOrLoginAuth{identity: identity, username: username, password: password, host: host}
}
func isLocalhost(name string) bool {
return name == "localhost" || name == "127.0.0.1" || name == "::1"
}
func (a *plainOrLoginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
// Must have TLS, or else localhost server.
// Note: If TLS is not true, then we can't trust ANYTHING in ServerInfo.
// In particular, it doesn't matter if the server advertises PLAIN auth.
// That might just be the attacker saying
// "it's ok, you can trust me with your password."
if !server.TLS && !isLocalhost(server.Name) {
return "", nil, errUnencryptedConnection
}
if server.Name != a.host {
return "", nil, errWrongHostname
}
if slices.Contains(server.Auth, "PLAIN") {
a.authMethod = "PLAIN"
resp := []byte(a.identity + "\x00" + a.username + "\x00" + a.password)
return a.authMethod, resp, nil
} else if slices.Contains(server.Auth, "LOGIN") {
a.authMethod = "LOGIN"
return a.authMethod, nil, nil
} else {
return "", nil, errNoSupportedAuth
}
}
func (a *plainOrLoginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
if !more {
return nil, nil
}
if a.authMethod == "PLAIN" {
// We've already sent everything.
return nil, errUnexpectedServerChallenge
}
switch {
case bytes.Equal(fromServer, []byte("Username:")):
return []byte(a.username), nil
case bytes.Equal(fromServer, []byte("Password:")):
return []byte(a.password), nil
default:
return nil, fmt.Errorf("%w: %s", errUnexpectedServerChallenge, string(fromServer))
}
}
type smtpSender struct {
config *Config
sender *mail.Sender