This commit is contained in:
binwiederhier 2026-03-30 08:26:56 -04:00
parent 61dd788dac
commit 6aebc5c677
12 changed files with 83 additions and 65 deletions

View file

@ -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-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-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.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-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-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-')"}), 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") smtpSenderUser := c.String("smtp-sender-user")
smtpSenderPass := c.String("smtp-sender-pass") smtpSenderPass := c.String("smtp-sender-pass")
smtpSenderFrom := c.String("smtp-sender-from") 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") smtpServerListen := c.String("smtp-server-listen")
smtpServerDomain := c.String("smtp-server-domain") smtpServerDomain := c.String("smtp-server-domain")
smtpServerAddrPrefix := c.String("smtp-server-addr-prefix") 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") return errors.New("if listen-https is set, both key-file and cert-file must be set")
} else if smtpSenderAddr != "" && (baseURL == "" || smtpSenderFrom == "") { } else if smtpSenderAddr != "" && (baseURL == "" || smtpSenderFrom == "") {
return errors.New("if smtp-sender-addr is set, base-url, and smtp-sender-from must also be set") return errors.New("if smtp-sender-addr is set, base-url, and smtp-sender-from must also be set")
} else if smtpSenderEmailVerify && smtpSenderAddr == "" { } else if smtpSenderVerify && smtpSenderAddr == "" {
return errors.New("if smtp-sender-email-verify is set, smtp-sender-addr must also be set") return errors.New("if smtp-sender-verify is set, smtp-sender-addr must also be set")
} else if smtpServerListen != "" && smtpServerDomain == "" { } else if smtpServerListen != "" && smtpServerDomain == "" {
return errors.New("if smtp-server-listen is set, smtp-server-domain must also be set") return errors.New("if smtp-server-listen is set, smtp-server-domain must also be set")
} else if attachmentCacheDir != "" && baseURL == "" { } else if attachmentCacheDir != "" && baseURL == "" {
@ -475,7 +475,7 @@ func execServe(c *cli.Context) error {
conf.SMTPSenderUser = smtpSenderUser conf.SMTPSenderUser = smtpSenderUser
conf.SMTPSenderPass = smtpSenderPass conf.SMTPSenderPass = smtpSenderPass
conf.SMTPSenderFrom = smtpSenderFrom conf.SMTPSenderFrom = smtpSenderFrom
conf.SMTPSenderEmailVerify = smtpSenderEmailVerify conf.SMTPSenderVerify = smtpSenderVerify
conf.SMTPServerListen = smtpServerListen conf.SMTPServerListen = smtpServerListen
conf.SMTPServerDomain = smtpServerDomain conf.SMTPServerDomain = smtpServerDomain
conf.SMTPServerAddrPrefix = smtpServerAddrPrefix conf.SMTPServerAddrPrefix = smtpServerAddrPrefix

View file

@ -355,7 +355,7 @@ This generator helps you configure your self-hosted ntfy instance. It's not full
</div> </div>
<div class="cg-field cg-inline-field"> <div class="cg-field cg-inline-field">
<label>Require email verification</label> <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="">No (default)</option>
<option value="true">Yes</option> <option value="true">Yes</option>
</select> </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 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 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. 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-user: "AKIDEADBEEFAFFE12345"
smtp-sender-pass: "Abd13Kf+sfAk2DzifjafldkThisIsNotARealKeyOMG." smtp-sender-pass: "Abd13Kf+sfAk2DzifjafldkThisIsNotARealKeyOMG."
smtp-sender-from: "ntfy@ntfy.sh" 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` 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-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-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-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-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-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-` | | `smtp-server-addr-prefix` | `NTFY_SMTP_SERVER_ADDR_PREFIX` | *string* | - | Optional prefix for the e-mail addresses to prevent spam, e.g. `ntfy-` |

View file

@ -125,7 +125,7 @@
{ key: "smtp-sender-from", env: "NTFY_SMTP_SENDER_FROM", section: "smtp-out" }, { 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-user", env: "NTFY_SMTP_SENDER_USER", section: "smtp-out" },
{ key: "smtp-sender-pass", env: "NTFY_SMTP_SENDER_PASS", 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-listen", env: "NTFY_SMTP_SERVER_LISTEN", section: "smtp-in" },
{ key: "smtp-server-domain", env: "NTFY_SMTP_SERVER_DOMAIN", 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" }, { key: "smtp-server-addr-prefix", env: "NTFY_SMTP_SERVER_ADDR_PREFIX", section: "smtp-in" },

View file

@ -1,9 +1,7 @@
package mail package mail
import ( import (
"crypto/rand"
"fmt" "fmt"
"math/big"
"mime" "mime"
"net" "net"
"net/smtp" "net/smtp"
@ -12,6 +10,7 @@ import (
"time" "time"
"heckel.io/ntfy/v2/log" "heckel.io/ntfy/v2/log"
"heckel.io/ntfy/v2/util"
) )
const ( const (
@ -30,9 +29,10 @@ type Config struct {
// Sender sends emails and manages email verification codes // Sender sends emails and manages email verification codes
type Sender struct { type Sender struct {
config *Config config *Config
verifyCodes map[string]verifyCode // keyed by email codes map[string]verifyCode // Verification codes, keyed by email
mu sync.Mutex mu sync.Mutex
closeChan chan struct{}
} }
type verifyCode struct { type verifyCode struct {
@ -42,10 +42,18 @@ type verifyCode struct {
// NewSender creates a new mail Sender with the given SMTP config // NewSender creates a new mail Sender with the given SMTP config
func NewSender(config *Config) *Sender { func NewSender(config *Config) *Sender {
return &Sender{ s := &Sender{
config: config, 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 // 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)) 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 { func (s *Sender) SendVerification(to string) error {
code, err := generateCode() code := util.RandomString(verifyCodeLength)
if err != nil {
return err
}
s.mu.Lock() s.mu.Lock()
s.verifyCodes[to] = verifyCode{ s.codes[to] = verifyCode{
code: code, code: code,
expires: time.Now().Add(verifyCodeExpiry), expires: time.Now().Add(verifyCodeExpiry),
} }
@ -96,31 +101,34 @@ func (s *Sender) SendVerification(to string) error {
func (s *Sender) CheckVerification(email, code string) bool { func (s *Sender) CheckVerification(email, code string) bool {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
vc, ok := s.verifyCodes[email] vc, ok := s.codes[email]
if !ok || time.Now().After(vc.expires) || vc.code != code { if !ok || time.Now().After(vc.expires) || vc.code != code {
return false return false
} }
delete(s.verifyCodes, email) delete(s.codes, email)
return true return true
} }
// ExpireVerificationCodes removes expired entries from the in-memory map func (s *Sender) expireLoop() {
func (s *Sender) ExpireVerificationCodes() { ticker := time.NewTicker(time.Minute)
s.mu.Lock() defer ticker.Stop()
defer s.mu.Unlock() for {
now := time.Now() select {
for email, vc := range s.verifyCodes { case <-ticker.C:
if now.After(vc.expires) { s.expireVerificationCodes()
delete(s.verifyCodes, email) case <-s.closeChan:
return
} }
} }
} }
func generateCode() (string, error) { func (s *Sender) expireVerificationCodes() {
max := big.NewInt(1000000) // 0-999999 s.mu.Lock()
n, err := rand.Int(rand.Reader, max) defer s.mu.Unlock()
if err != nil { now := time.Now()
return "", err for email, vc := range s.codes {
if now.After(vc.expires) {
delete(s.codes, email)
}
} }
return fmt.Sprintf("%06d", n.Int64()), nil
} }

View file

@ -135,7 +135,7 @@ type Config struct {
SMTPSenderUser string SMTPSenderUser string
SMTPSenderPass string SMTPSenderPass string
SMTPSenderFrom string SMTPSenderFrom string
SMTPSenderEmailVerify bool SMTPSenderVerify bool
SMTPServerListen string SMTPServerListen string
SMTPServerDomain string SMTPServerDomain string
SMTPServerAddrPrefix string SMTPServerAddrPrefix string
@ -240,7 +240,7 @@ func NewConfig() *Config {
SMTPSenderUser: "", SMTPSenderUser: "",
SMTPSenderPass: "", SMTPSenderPass: "",
SMTPSenderFrom: "", SMTPSenderFrom: "",
SMTPSenderEmailVerify: false, SMTPSenderVerify: false,
SMTPServerListen: "", SMTPServerListen: "",
SMTPServerDomain: "", SMTPServerDomain: "",
SMTPServerAddrPrefix: "", SMTPServerAddrPrefix: "",

View file

@ -441,6 +441,9 @@ func (s *Server) Stop() {
if s.smtpServer != nil { if s.smtpServer != nil {
s.smtpServer.Close() s.smtpServer.Close()
} }
if s.mailSender != nil {
s.mailSender.Close()
}
if s.attachment != nil { if s.attachment != nil {
s.attachment.Close() s.attachment.Close()
} }
@ -883,14 +886,14 @@ func (s *Server) handlePublishInternal(r *http.Request, v *visitor) (*model.Mess
return nil, errHTTPInsufficientStorageUnifiedPush.With(t) return nil, errHTTPInsufficientStorageUnifiedPush.With(t)
} else if !util.ContainsIP(s.config.VisitorRequestExemptPrefixes, v.ip) && !vrate.MessageAllowed() { } else if !util.ContainsIP(s.config.VisitorRequestExemptPrefixes, v.ip) && !vrate.MessageAllowed() {
return nil, errHTTPTooManyRequestsLimitMessages.With(t) return nil, errHTTPTooManyRequestsLimitMessages.With(t)
} else if email != "" { }
if !vrate.EmailAllowed() { if email != "" {
return nil, errHTTPTooManyRequestsLimitEmails.With(t)
}
var httpErr *errHTTP var httpErr *errHTTP
email, httpErr = s.convertEmailAddress(v.User(), email) email, httpErr = s.convertEmailAddress(v.User(), email)
if httpErr != nil { if httpErr != nil {
return nil, httpErr.With(t) return nil, httpErr.With(t)
} else if !vrate.EmailAllowed() {
return nil, errHTTPTooManyRequestsLimitEmails.With(t)
} }
} }
if call != "" { if call != "" {

View file

@ -193,18 +193,14 @@
# - smtp-sender-addr is the hostname:port of the SMTP server # - smtp-sender-addr is the hostname:port of the SMTP server
# - smtp-sender-from is the e-mail address of the sender # - 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-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-addr:
# smtp-sender-from: # smtp-sender-from:
# smtp-sender-user: # smtp-sender-user:
# smtp-sender-pass: # smtp-sender-pass:
# smtp-sender-verify: false
# 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
# If enabled, ntfy will launch a lightweight SMTP server for incoming messages. Once configured, users can send # 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. # emails to a topic e-mail address to publish messages to a topic.

View file

@ -636,6 +636,10 @@ func (s *Server) handleAccountEmailVerify(w http.ResponseWriter, r *http.Request
} else if util.Contains(emails, req.Email) { } else if util.Contains(emails, req.Email) {
return errHTTPConflictEmailExists return errHTTPConflictEmailExists
} }
// Check email rate limit (counts against the user's email quota)
if !v.EmailAllowed() {
return errHTTPTooManyRequestsLimitEmails
}
// Send verification email // Send verification email
logvr(v, r).Tag(tagAccount).Field("email", req.Email).Debug("Sending email verification") logvr(v, r).Tag(tagAccount).Field("email", req.Email).Debug("Sending email verification")
if err := s.mailSender.SendVerification(req.Email); err != nil { 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. // 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 // 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. // in their verified list. "yes"/"true"/"1" resolves to the first verified email.
func (s *Server) convertEmailAddress(u *user.User, email string) (string, *errHTTP) { func (s *Server) convertEmailAddress(u *user.User, email string) (string, *errHTTP) {
if !s.config.SMTPSenderEmailVerify { if !s.config.SMTPSenderVerify {
return email, nil return email, nil
} } else if u == nil {
if u == nil {
return "", errHTTPBadRequestAnonymousEmailNotAllowed return "", errHTTPBadRequestAnonymousEmailNotAllowed
} } else if s.userManager == nil {
if s.userManager == nil {
return email, nil return email, nil
} }
emails, err := s.userManager.Emails(u.ID) emails, err := s.userManager.Emails(u.ID)
if err != nil { if err != nil {
return "", errHTTPInternalError return "", errHTTPInternalError
} } else if len(emails) == 0 {
if len(emails) == 0 {
return "", errHTTPBadRequestEmailAddressNotVerified return "", errHTTPBadRequestEmailAddressNotVerified
} }
if toBool(email) { if toBool(email) {

View file

@ -15,9 +15,6 @@ func (s *Server) execManager() {
s.pruneAttachments() s.pruneAttachments()
s.pruneMessages() s.pruneMessages()
s.pruneAndNotifyWebPushSubscriptions() s.pruneAndNotifyWebPushSubscriptions()
if s.mailSender != nil {
s.mailSender.ExpireVerificationCodes()
}
// Message count // Message count
messagesCached, err := s.messageCache.MessagesCount() messagesCached, err := s.messageCache.MessagesCount()

View file

@ -226,6 +226,7 @@
"account_basics_emails_dialog_verify_button": "Add email", "account_basics_emails_dialog_verify_button": "Add email",
"account_basics_emails_dialog_code_label": "Verification code", "account_basics_emails_dialog_code_label": "Verification code",
"account_basics_emails_dialog_code_placeholder": "e.g. 123456", "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_emails_dialog_check_verification_button": "Confirm",
"account_basics_cannot_edit_or_delete_provisioned_user": "A provisioned user cannot be edited or deleted", "account_basics_cannot_edit_or_delete_provisioned_user": "A provisioned user cannot be edited or deleted",
"account_usage_title": "Usage", "account_usage_title": "Usage",

View file

@ -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) => { export const throwAppError = async (response) => {
if (response.status === 401 || response.status === 403) { if (response.status === 401 || response.status === 403) {
console.log(`[Error] HTTP ${response.status}`, response); console.log(`[Error] HTTP ${response.status}`, response);
@ -63,6 +71,8 @@ export const throwAppError = async (response) => {
throw new AccountCreateLimitReachedError(); throw new AccountCreateLimitReachedError();
} else if (error.code === IncorrectPasswordError.CODE) { } else if (error.code === IncorrectPasswordError.CODE) {
throw new IncorrectPasswordError(); throw new IncorrectPasswordError();
} else if (error.code === EmailVerificationCodeInvalidError.CODE) {
throw new EmailVerificationCodeInvalidError();
} else if (error?.error) { } else if (error?.error) {
throw new Error(`Error ${error.code}: ${error.error}`); throw new Error(`Error ${error.code}: ${error.error}`);
} }

View file

@ -53,7 +53,7 @@ import UpgradeDialog from "./UpgradeDialog";
import { AccountContext } from "./App"; import { AccountContext } from "./App";
import DialogFooter from "./DialogFooter"; import DialogFooter from "./DialogFooter";
import { Paragraph } from "./styles"; import { Paragraph } from "./styles";
import { IncorrectPasswordError, UnauthorizedError } from "../app/errors"; import { EmailVerificationCodeInvalidError, IncorrectPasswordError, UnauthorizedError } from "../app/errors";
import { ProChip } from "./SubscriptionPopup"; import { ProChip } from "./SubscriptionPopup";
import session from "../app/Session"; import session from "../app/Session";
@ -478,6 +478,8 @@ const AddEmailDialog = (props) => {
console.log(`[Account] Error confirming email verification`, e); console.log(`[Account] Error confirming email verification`, e);
if (e instanceof UnauthorizedError) { if (e instanceof UnauthorizedError) {
await session.resetAndRedirect(routes.login); await session.resetAndRedirect(routes.login);
} else if (e instanceof EmailVerificationCodeInvalidError) {
setError(t("account_basics_emails_dialog_code_invalid"));
} else { } else {
setError(e.message); setError(e.message);
} }