WIP: Email verification

This commit is contained in:
binwiederhier
2026-03-29 22:47:38 -04:00
parent 266d0b9d37
commit 61dd788dac
23 changed files with 720 additions and 8 deletions
+29 -3
View File
@@ -36,6 +36,7 @@ import (
"heckel.io/ntfy/v2/db"
"heckel.io/ntfy/v2/db/pg"
"heckel.io/ntfy/v2/log"
"heckel.io/ntfy/v2/mail"
"heckel.io/ntfy/v2/message"
"heckel.io/ntfy/v2/model"
"heckel.io/ntfy/v2/payments"
@@ -57,6 +58,7 @@ type Server struct {
smtpServer *smtp.Server
smtpServerBackend *smtpBackend
smtpSender mailer
mailSender *mail.Sender
topics map[string]*topic
visitors map[string]*visitor // ip:<ip> or user:<user>
firebaseClient *firebaseClient
@@ -112,6 +114,8 @@ var (
apiAccountReservationPath = "/v1/account/reservation"
apiAccountPhonePath = "/v1/account/phone"
apiAccountPhoneVerifyPath = "/v1/account/phone/verify"
apiAccountEmailPath = "/v1/account/email"
apiAccountEmailVerifyPath = "/v1/account/email/verify"
apiAccountBillingPortalPath = "/v1/account/billing/portal"
apiAccountBillingWebhookPath = "/v1/account/billing/webhook"
apiAccountBillingSubscriptionPath = "/v1/account/billing/subscription"
@@ -173,8 +177,15 @@ const (
// subscriber (if configured).
func New(conf *Config) (*Server, error) {
var mailer mailer
var mailSender *mail.Sender
if conf.SMTPSenderAddr != "" {
mailer = &smtpSender{config: conf}
mailSender = mail.NewSender(&mail.Config{
SMTPAddr: conf.SMTPSenderAddr,
SMTPUser: conf.SMTPSenderUser,
SMTPPass: conf.SMTPSenderPass,
From: conf.SMTPSenderFrom,
})
}
var stripe stripeAPI
if payments.Available && conf.StripeSecretKey != "" {
@@ -278,6 +289,7 @@ func New(conf *Config) (*Server, error) {
attachment: attachmentStore,
firebaseClient: firebaseClient,
smtpSender: mailer,
mailSender: mailSender,
topics: topics,
userManager: userManager,
messages: messages,
@@ -594,6 +606,12 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit
return s.ensureUser(s.ensureCallsEnabled(s.withAccountSync(s.handleAccountPhoneNumberAdd)))(w, r, v)
} else if r.Method == http.MethodDelete && r.URL.Path == apiAccountPhonePath {
return s.ensureUser(s.ensureCallsEnabled(s.withAccountSync(s.handleAccountPhoneNumberDelete)))(w, r, v)
} else if r.Method == http.MethodPut && r.URL.Path == apiAccountEmailVerifyPath {
return s.ensureUser(s.ensureEmailsEnabled(s.withAccountSync(s.handleAccountEmailVerify)))(w, r, v)
} else if r.Method == http.MethodPut && r.URL.Path == apiAccountEmailPath {
return s.ensureUser(s.ensureEmailsEnabled(s.withAccountSync(s.handleAccountEmailAdd)))(w, r, v)
} else if r.Method == http.MethodDelete && r.URL.Path == apiAccountEmailPath {
return s.ensureUser(s.ensureEmailsEnabled(s.withAccountSync(s.handleAccountEmailDelete)))(w, r, v)
} else if r.Method == http.MethodPost && apiWebPushPath == r.URL.Path {
return s.ensureWebPushEnabled(s.limitRequests(s.handleWebPushUpdate))(w, r, v)
} else if r.Method == http.MethodDelete && apiWebPushPath == r.URL.Path {
@@ -865,9 +883,17 @@ 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 != "" && !vrate.EmailAllowed() {
return nil, errHTTPTooManyRequestsLimitEmails.With(t)
} else if call != "" {
} else if email != "" {
if !vrate.EmailAllowed() {
return nil, errHTTPTooManyRequestsLimitEmails.With(t)
}
var httpErr *errHTTP
email, httpErr = s.convertEmailAddress(v.User(), email)
if httpErr != nil {
return nil, httpErr.With(t)
}
}
if call != "" {
var httpErr *errHTTP
call, httpErr = s.convertPhoneNumber(v.User(), call)
if httpErr != nil {