mirror of
https://github.com/binwiederhier/ntfy.git
synced 2026-05-15 07:35:49 -06:00
Refine
This commit is contained in:
parent
61dd788dac
commit
6aebc5c677
12 changed files with 83 additions and 65 deletions
10
cmd/serve.go
10
cmd/serve.go
|
|
@ -71,7 +71,7 @@ var flagsServe = append(
|
|||
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-user", Aliases: []string{"smtp_sender_user"}, EnvVars: []string{"NTFY_SMTP_SENDER_USER"}, Usage: "SMTP user (if e-mail sending is enabled)"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-pass", Aliases: []string{"smtp_sender_pass"}, EnvVars: []string{"NTFY_SMTP_SENDER_PASS"}, Usage: "SMTP password (if e-mail sending is enabled)"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-from", Aliases: []string{"smtp_sender_from"}, EnvVars: []string{"NTFY_SMTP_SENDER_FROM"}, Usage: "SMTP sender address (if e-mail sending is enabled)"}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "smtp-sender-email-verify", Aliases: []string{"smtp_sender_email_verify"}, EnvVars: []string{"NTFY_SMTP_SENDER_EMAIL_VERIFY"}, Value: false, Usage: "require verified email addresses for sending email notifications"}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "smtp-sender-verify", Aliases: []string{"smtp_sender_verify"}, EnvVars: []string{"NTFY_SMTP_SENDER_VERIFY"}, Value: false, Usage: "require verified email addresses for sending email notifications"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-server-listen", Aliases: []string{"smtp_server_listen"}, EnvVars: []string{"NTFY_SMTP_SERVER_LISTEN"}, Usage: "SMTP server address (ip:port) for incoming emails, e.g. :25"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-server-domain", Aliases: []string{"smtp_server_domain"}, EnvVars: []string{"NTFY_SMTP_SERVER_DOMAIN"}, Usage: "SMTP domain for incoming e-mail, e.g. ntfy.sh"}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-server-addr-prefix", Aliases: []string{"smtp_server_addr_prefix"}, EnvVars: []string{"NTFY_SMTP_SERVER_ADDR_PREFIX"}, Usage: "SMTP email address prefix for topics to prevent spam (e.g. 'ntfy-')"}),
|
||||
|
|
@ -185,7 +185,7 @@ func execServe(c *cli.Context) error {
|
|||
smtpSenderUser := c.String("smtp-sender-user")
|
||||
smtpSenderPass := c.String("smtp-sender-pass")
|
||||
smtpSenderFrom := c.String("smtp-sender-from")
|
||||
smtpSenderEmailVerify := c.Bool("smtp-sender-email-verify")
|
||||
smtpSenderVerify := c.Bool("smtp-sender-verify")
|
||||
smtpServerListen := c.String("smtp-server-listen")
|
||||
smtpServerDomain := c.String("smtp-server-domain")
|
||||
smtpServerAddrPrefix := c.String("smtp-server-addr-prefix")
|
||||
|
|
@ -312,8 +312,8 @@ func execServe(c *cli.Context) error {
|
|||
return errors.New("if listen-https is set, both key-file and cert-file must be set")
|
||||
} else if smtpSenderAddr != "" && (baseURL == "" || smtpSenderFrom == "") {
|
||||
return errors.New("if smtp-sender-addr is set, base-url, and smtp-sender-from must also be set")
|
||||
} else if smtpSenderEmailVerify && smtpSenderAddr == "" {
|
||||
return errors.New("if smtp-sender-email-verify is set, smtp-sender-addr must also be set")
|
||||
} else if smtpSenderVerify && smtpSenderAddr == "" {
|
||||
return errors.New("if smtp-sender-verify is set, smtp-sender-addr must also be set")
|
||||
} else if smtpServerListen != "" && smtpServerDomain == "" {
|
||||
return errors.New("if smtp-server-listen is set, smtp-server-domain must also be set")
|
||||
} else if attachmentCacheDir != "" && baseURL == "" {
|
||||
|
|
@ -475,7 +475,7 @@ func execServe(c *cli.Context) error {
|
|||
conf.SMTPSenderUser = smtpSenderUser
|
||||
conf.SMTPSenderPass = smtpSenderPass
|
||||
conf.SMTPSenderFrom = smtpSenderFrom
|
||||
conf.SMTPSenderEmailVerify = smtpSenderEmailVerify
|
||||
conf.SMTPSenderVerify = smtpSenderVerify
|
||||
conf.SMTPServerListen = smtpServerListen
|
||||
conf.SMTPServerDomain = smtpServerDomain
|
||||
conf.SMTPServerAddrPrefix = smtpServerAddrPrefix
|
||||
|
|
|
|||
|
|
@ -355,7 +355,7 @@ This generator helps you configure your self-hosted ntfy instance. It's not full
|
|||
</div>
|
||||
<div class="cg-field cg-inline-field">
|
||||
<label>Require email verification</label>
|
||||
<select data-key="smtp-sender-email-verify">
|
||||
<select data-key="smtp-sender-verify">
|
||||
<option value="">No (default)</option>
|
||||
<option value="true">Yes</option>
|
||||
</select>
|
||||
|
|
@ -1039,7 +1039,7 @@ configured for `ntfy.sh`):
|
|||
```
|
||||
|
||||
By default, any user (including anonymous users) can send email notifications to any address. To require email
|
||||
address verification, set `smtp-sender-email-verify` to `true`. When enabled, anonymous users cannot send emails,
|
||||
address verification, set `smtp-sender-verify` to `true`. When enabled, anonymous users cannot send emails,
|
||||
and authenticated users can only send to email addresses they have verified in their account settings. Users can
|
||||
also use `yes`/`true`/`1` as the `X-Email` value to send to their first verified address.
|
||||
|
||||
|
|
@ -1049,7 +1049,7 @@ also use `yes`/`true`/`1` as the `X-Email` value to send to their first verified
|
|||
smtp-sender-user: "AKIDEADBEEFAFFE12345"
|
||||
smtp-sender-pass: "Abd13Kf+sfAk2DzifjafldkThisIsNotARealKeyOMG."
|
||||
smtp-sender-from: "ntfy@ntfy.sh"
|
||||
smtp-sender-email-verify: true
|
||||
smtp-sender-verify: true
|
||||
```
|
||||
|
||||
Please also refer to the [rate limiting](#rate-limiting) settings below, specifically `visitor-email-limit-burst`
|
||||
|
|
@ -2221,7 +2221,7 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`).
|
|||
| `smtp-sender-user` | `NTFY_SMTP_SENDER_USER` | *string* | - | SMTP user; only used if e-mail sending is enabled |
|
||||
| `smtp-sender-pass` | `NTFY_SMTP_SENDER_PASS` | *string* | - | SMTP password; only used if e-mail sending is enabled |
|
||||
| `smtp-sender-from` | `NTFY_SMTP_SENDER_FROM` | *e-mail address* | - | SMTP sender e-mail address; only used if e-mail sending is enabled |
|
||||
| `smtp-sender-email-verify` | `NTFY_SMTP_SENDER_EMAIL_VERIFY` | *bool* | `false` | If true, require verified email addresses for email notifications; anonymous email sending is disabled |
|
||||
| `smtp-sender-verify` | `NTFY_SMTP_SENDER_VERIFY` | *bool* | `false` | If true, require verified email addresses for email notifications; anonymous email sending is disabled |
|
||||
| `smtp-server-listen` | `NTFY_SMTP_SERVER_LISTEN` | `[ip]:port` | - | Defines the IP address and port the SMTP server will listen on, e.g. `:25` or `1.2.3.4:25` |
|
||||
| `smtp-server-domain` | `NTFY_SMTP_SERVER_DOMAIN` | *domain name* | - | SMTP server e-mail domain, e.g. `ntfy.sh` |
|
||||
| `smtp-server-addr-prefix` | `NTFY_SMTP_SERVER_ADDR_PREFIX` | *string* | - | Optional prefix for the e-mail addresses to prevent spam, e.g. `ntfy-` |
|
||||
|
|
|
|||
2
docs/static/js/config-generator.js
vendored
2
docs/static/js/config-generator.js
vendored
|
|
@ -125,7 +125,7 @@
|
|||
{ key: "smtp-sender-from", env: "NTFY_SMTP_SENDER_FROM", section: "smtp-out" },
|
||||
{ key: "smtp-sender-user", env: "NTFY_SMTP_SENDER_USER", section: "smtp-out" },
|
||||
{ key: "smtp-sender-pass", env: "NTFY_SMTP_SENDER_PASS", section: "smtp-out" },
|
||||
{ key: "smtp-sender-email-verify", env: "NTFY_SMTP_SENDER_EMAIL_VERIFY", section: "smtp-out" },
|
||||
{ key: "smtp-sender-verify", env: "NTFY_SMTP_SENDER_VERIFY", section: "smtp-out" },
|
||||
{ key: "smtp-server-listen", env: "NTFY_SMTP_SERVER_LISTEN", section: "smtp-in" },
|
||||
{ key: "smtp-server-domain", env: "NTFY_SMTP_SERVER_DOMAIN", section: "smtp-in" },
|
||||
{ key: "smtp-server-addr-prefix", env: "NTFY_SMTP_SERVER_ADDR_PREFIX", section: "smtp-in" },
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
package mail
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"mime"
|
||||
"net"
|
||||
"net/smtp"
|
||||
|
|
@ -12,6 +10,7 @@ import (
|
|||
"time"
|
||||
|
||||
"heckel.io/ntfy/v2/log"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -31,8 +30,9 @@ type Config struct {
|
|||
// Sender sends emails and manages email verification codes
|
||||
type Sender struct {
|
||||
config *Config
|
||||
verifyCodes map[string]verifyCode // keyed by email
|
||||
codes map[string]verifyCode // Verification codes, keyed by email
|
||||
mu sync.Mutex
|
||||
closeChan chan struct{}
|
||||
}
|
||||
|
||||
type verifyCode struct {
|
||||
|
|
@ -42,10 +42,18 @@ type verifyCode struct {
|
|||
|
||||
// NewSender creates a new mail Sender with the given SMTP config
|
||||
func NewSender(config *Config) *Sender {
|
||||
return &Sender{
|
||||
s := &Sender{
|
||||
config: config,
|
||||
verifyCodes: make(map[string]verifyCode),
|
||||
codes: make(map[string]verifyCode),
|
||||
closeChan: make(chan struct{}),
|
||||
}
|
||||
go s.expireLoop()
|
||||
return s
|
||||
}
|
||||
|
||||
// Close stops the background expiry loop
|
||||
func (s *Sender) Close() {
|
||||
close(s.closeChan)
|
||||
}
|
||||
|
||||
// Send sends a plain text email via SMTP
|
||||
|
|
@ -76,14 +84,11 @@ Content-Type: text/plain; charset="utf-8"
|
|||
return smtp.SendMail(s.config.SMTPAddr, auth, s.config.From, []string{to}, []byte(message))
|
||||
}
|
||||
|
||||
// SendVerification generates a 6-digit code, stores it in-memory, and sends a verification email
|
||||
// SendVerification generates a random code, stores it in-memory, and sends a verification email
|
||||
func (s *Sender) SendVerification(to string) error {
|
||||
code, err := generateCode()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
code := util.RandomString(verifyCodeLength)
|
||||
s.mu.Lock()
|
||||
s.verifyCodes[to] = verifyCode{
|
||||
s.codes[to] = verifyCode{
|
||||
code: code,
|
||||
expires: time.Now().Add(verifyCodeExpiry),
|
||||
}
|
||||
|
|
@ -96,31 +101,34 @@ func (s *Sender) SendVerification(to string) error {
|
|||
func (s *Sender) CheckVerification(email, code string) bool {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
vc, ok := s.verifyCodes[email]
|
||||
vc, ok := s.codes[email]
|
||||
if !ok || time.Now().After(vc.expires) || vc.code != code {
|
||||
return false
|
||||
}
|
||||
delete(s.verifyCodes, email)
|
||||
delete(s.codes, email)
|
||||
return true
|
||||
}
|
||||
|
||||
// ExpireVerificationCodes removes expired entries from the in-memory map
|
||||
func (s *Sender) ExpireVerificationCodes() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
now := time.Now()
|
||||
for email, vc := range s.verifyCodes {
|
||||
if now.After(vc.expires) {
|
||||
delete(s.verifyCodes, email)
|
||||
func (s *Sender) expireLoop() {
|
||||
ticker := time.NewTicker(time.Minute)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
s.expireVerificationCodes()
|
||||
case <-s.closeChan:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func generateCode() (string, error) {
|
||||
max := big.NewInt(1000000) // 0-999999
|
||||
n, err := rand.Int(rand.Reader, max)
|
||||
if err != nil {
|
||||
return "", err
|
||||
func (s *Sender) expireVerificationCodes() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
now := time.Now()
|
||||
for email, vc := range s.codes {
|
||||
if now.After(vc.expires) {
|
||||
delete(s.codes, email)
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("%06d", n.Int64()), nil
|
||||
}
|
||||
|
|
@ -135,7 +135,7 @@ type Config struct {
|
|||
SMTPSenderUser string
|
||||
SMTPSenderPass string
|
||||
SMTPSenderFrom string
|
||||
SMTPSenderEmailVerify bool
|
||||
SMTPSenderVerify bool
|
||||
SMTPServerListen string
|
||||
SMTPServerDomain string
|
||||
SMTPServerAddrPrefix string
|
||||
|
|
@ -240,7 +240,7 @@ func NewConfig() *Config {
|
|||
SMTPSenderUser: "",
|
||||
SMTPSenderPass: "",
|
||||
SMTPSenderFrom: "",
|
||||
SMTPSenderEmailVerify: false,
|
||||
SMTPSenderVerify: false,
|
||||
SMTPServerListen: "",
|
||||
SMTPServerDomain: "",
|
||||
SMTPServerAddrPrefix: "",
|
||||
|
|
|
|||
|
|
@ -441,6 +441,9 @@ func (s *Server) Stop() {
|
|||
if s.smtpServer != nil {
|
||||
s.smtpServer.Close()
|
||||
}
|
||||
if s.mailSender != nil {
|
||||
s.mailSender.Close()
|
||||
}
|
||||
if s.attachment != nil {
|
||||
s.attachment.Close()
|
||||
}
|
||||
|
|
@ -883,14 +886,14 @@ func (s *Server) handlePublishInternal(r *http.Request, v *visitor) (*model.Mess
|
|||
return nil, errHTTPInsufficientStorageUnifiedPush.With(t)
|
||||
} else if !util.ContainsIP(s.config.VisitorRequestExemptPrefixes, v.ip) && !vrate.MessageAllowed() {
|
||||
return nil, errHTTPTooManyRequestsLimitMessages.With(t)
|
||||
} else if email != "" {
|
||||
if !vrate.EmailAllowed() {
|
||||
return nil, errHTTPTooManyRequestsLimitEmails.With(t)
|
||||
}
|
||||
if email != "" {
|
||||
var httpErr *errHTTP
|
||||
email, httpErr = s.convertEmailAddress(v.User(), email)
|
||||
if httpErr != nil {
|
||||
return nil, httpErr.With(t)
|
||||
} else if !vrate.EmailAllowed() {
|
||||
return nil, errHTTPTooManyRequestsLimitEmails.With(t)
|
||||
}
|
||||
}
|
||||
if call != "" {
|
||||
|
|
|
|||
|
|
@ -193,18 +193,14 @@
|
|||
# - smtp-sender-addr is the hostname:port of the SMTP server
|
||||
# - smtp-sender-from is the e-mail address of the sender
|
||||
# - smtp-sender-user/smtp-sender-pass are the username and password of the SMTP user (leave blank for no auth)
|
||||
# - smtp-sender-verify is a flag that forces email recipient verification when enabled. If set to true,
|
||||
# only verified email recipients can be used in the X-Email header.
|
||||
#
|
||||
# smtp-sender-addr:
|
||||
# smtp-sender-from:
|
||||
# smtp-sender-user:
|
||||
# smtp-sender-pass:
|
||||
|
||||
# If set to true, only verified email recipients will receive email notifications.
|
||||
# Anonymous users will not be able to send emails, and authenticated users must verify
|
||||
# their email addresses first. Users can use "yes"/"true"/"1" as the email value to
|
||||
# send to their first verified address.
|
||||
#
|
||||
# smtp-sender-email-verify: false
|
||||
# smtp-sender-verify: false
|
||||
|
||||
# If enabled, ntfy will launch a lightweight SMTP server for incoming messages. Once configured, users can send
|
||||
# emails to a topic e-mail address to publish messages to a topic.
|
||||
|
|
|
|||
|
|
@ -636,6 +636,10 @@ func (s *Server) handleAccountEmailVerify(w http.ResponseWriter, r *http.Request
|
|||
} else if util.Contains(emails, req.Email) {
|
||||
return errHTTPConflictEmailExists
|
||||
}
|
||||
// Check email rate limit (counts against the user's email quota)
|
||||
if !v.EmailAllowed() {
|
||||
return errHTTPTooManyRequestsLimitEmails
|
||||
}
|
||||
// Send verification email
|
||||
logvr(v, r).Tag(tagAccount).Field("email", req.Email).Debug("Sending email verification")
|
||||
if err := s.mailSender.SendVerification(req.Email); err != nil {
|
||||
|
|
@ -680,24 +684,21 @@ func (s *Server) handleAccountEmailDelete(w http.ResponseWriter, r *http.Request
|
|||
}
|
||||
|
||||
// convertEmailAddress checks the email address against the user's verified email list.
|
||||
// If smtp-sender-email-verify is false (default), the email is passed through as-is for
|
||||
// If smtp-sender-verify is false (default), the email is passed through as-is for
|
||||
// backwards compatibility. If true, the user must be authenticated and the email must be
|
||||
// in their verified list. "yes"/"true"/"1" resolves to the first verified email.
|
||||
func (s *Server) convertEmailAddress(u *user.User, email string) (string, *errHTTP) {
|
||||
if !s.config.SMTPSenderEmailVerify {
|
||||
if !s.config.SMTPSenderVerify {
|
||||
return email, nil
|
||||
}
|
||||
if u == nil {
|
||||
} else if u == nil {
|
||||
return "", errHTTPBadRequestAnonymousEmailNotAllowed
|
||||
}
|
||||
if s.userManager == nil {
|
||||
} else if s.userManager == nil {
|
||||
return email, nil
|
||||
}
|
||||
emails, err := s.userManager.Emails(u.ID)
|
||||
if err != nil {
|
||||
return "", errHTTPInternalError
|
||||
}
|
||||
if len(emails) == 0 {
|
||||
} else if len(emails) == 0 {
|
||||
return "", errHTTPBadRequestEmailAddressNotVerified
|
||||
}
|
||||
if toBool(email) {
|
||||
|
|
|
|||
|
|
@ -15,9 +15,6 @@ func (s *Server) execManager() {
|
|||
s.pruneAttachments()
|
||||
s.pruneMessages()
|
||||
s.pruneAndNotifyWebPushSubscriptions()
|
||||
if s.mailSender != nil {
|
||||
s.mailSender.ExpireVerificationCodes()
|
||||
}
|
||||
|
||||
// Message count
|
||||
messagesCached, err := s.messageCache.MessagesCount()
|
||||
|
|
|
|||
|
|
@ -226,6 +226,7 @@
|
|||
"account_basics_emails_dialog_verify_button": "Add email",
|
||||
"account_basics_emails_dialog_code_label": "Verification code",
|
||||
"account_basics_emails_dialog_code_placeholder": "e.g. 123456",
|
||||
"account_basics_emails_dialog_code_invalid": "Verification code is invalid or expired, please try again",
|
||||
"account_basics_emails_dialog_check_verification_button": "Confirm",
|
||||
"account_basics_cannot_edit_or_delete_provisioned_user": "A provisioned user cannot be edited or deleted",
|
||||
"account_usage_title": "Usage",
|
||||
|
|
|
|||
|
|
@ -47,6 +47,14 @@ export class IncorrectPasswordError extends Error {
|
|||
}
|
||||
}
|
||||
|
||||
export class EmailVerificationCodeInvalidError extends Error {
|
||||
static CODE = 40051; // errHTTPBadRequestEmailVerificationCodeInvalid
|
||||
|
||||
constructor() {
|
||||
super("Email verification code invalid or expired");
|
||||
}
|
||||
}
|
||||
|
||||
export const throwAppError = async (response) => {
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
console.log(`[Error] HTTP ${response.status}`, response);
|
||||
|
|
@ -63,6 +71,8 @@ export const throwAppError = async (response) => {
|
|||
throw new AccountCreateLimitReachedError();
|
||||
} else if (error.code === IncorrectPasswordError.CODE) {
|
||||
throw new IncorrectPasswordError();
|
||||
} else if (error.code === EmailVerificationCodeInvalidError.CODE) {
|
||||
throw new EmailVerificationCodeInvalidError();
|
||||
} else if (error?.error) {
|
||||
throw new Error(`Error ${error.code}: ${error.error}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ import UpgradeDialog from "./UpgradeDialog";
|
|||
import { AccountContext } from "./App";
|
||||
import DialogFooter from "./DialogFooter";
|
||||
import { Paragraph } from "./styles";
|
||||
import { IncorrectPasswordError, UnauthorizedError } from "../app/errors";
|
||||
import { EmailVerificationCodeInvalidError, IncorrectPasswordError, UnauthorizedError } from "../app/errors";
|
||||
import { ProChip } from "./SubscriptionPopup";
|
||||
import session from "../app/Session";
|
||||
|
||||
|
|
@ -478,6 +478,8 @@ const AddEmailDialog = (props) => {
|
|||
console.log(`[Account] Error confirming email verification`, e);
|
||||
if (e instanceof UnauthorizedError) {
|
||||
await session.resetAndRedirect(routes.login);
|
||||
} else if (e instanceof EmailVerificationCodeInvalidError) {
|
||||
setError(t("account_basics_emails_dialog_code_invalid"));
|
||||
} else {
|
||||
setError(e.message);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue