diff --git a/server/server_account.go b/server/server_account.go
index 9acdf450..7def814d 100644
--- a/server/server_account.go
+++ b/server/server_account.go
@@ -624,9 +624,11 @@ func (s *Server) handleAccountEmailVerify(w http.ResponseWriter, r *http.Request
return errHTTPBadRequestEmailAddressInvalid
}
// Check user is allowed to add emails
- if u == nil || (u.IsUser() && u.Tier == nil) {
+ if u == nil {
return errHTTPUnauthorized
- } else if u.IsUser() && u.Tier.EmailLimit == 0 {
+ } else if u.IsUser() && u.Tier != nil && u.Tier.EmailLimit == 0 {
+ return errHTTPUnauthorized
+ } else if u.IsUser() && u.Tier == nil && s.config.VisitorEmailLimitBurst == 0 {
return errHTTPUnauthorized
}
// Check if email already exists
@@ -641,7 +643,7 @@ func (s *Server) handleAccountEmailVerify(w http.ResponseWriter, r *http.Request
return errHTTPTooManyRequestsLimitEmails
}
// 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).Info("Sending email verification")
if err := s.mailSender.SendVerification(req.Email); err != nil {
return err
}
@@ -653,14 +655,12 @@ func (s *Server) handleAccountEmailAdd(w http.ResponseWriter, r *http.Request, v
req, err := readJSONWithLimit[apiAccountEmailAddRequest](r.Body, jsonBodyBytesLimit, false)
if err != nil {
return err
- }
- if !emailAddressRegex.MatchString(req.Email) {
+ } else if !emailAddressRegex.MatchString(req.Email) {
return errHTTPBadRequestEmailAddressInvalid
- }
- if !s.mailSender.CheckVerification(req.Email, req.Code) {
+ } else if !s.mailSender.CheckVerification(req.Email, req.Code) {
return errHTTPBadRequestEmailVerificationCodeInvalid
}
- logvr(v, r).Tag(tagAccount).Field("email", req.Email).Debug("Adding email as verified")
+ logvr(v, r).Tag(tagAccount).Field("email", req.Email).Info("Adding email as verified")
if err := s.userManager.AddEmail(u.ID, req.Email); err != nil {
return err
}
diff --git a/server/server_test.go b/server/server_test.go
index dfad1fed..133517d9 100644
--- a/server/server_test.go
+++ b/server/server_test.go
@@ -1692,6 +1692,52 @@ func TestServer_PublishEmailVerify_Disabled_Backwards_Compatible(t *testing.T) {
})
}
+func TestServer_AccountEmailVerify_UserWithoutTier(t *testing.T) {
+ // This test verifies that an authenticated user WITHOUT a tier can verify emails
+ // when the default visitor email limit allows it.
+ forEachBackend(t, func(t *testing.T, databaseURL string) {
+ conf := newTestConfigWithAuthFile(t, databaseURL)
+ conf.SMTPSenderVerify = true
+ conf.SMTPSenderAddr = "localhost:25" // Dummy SMTP server (will fail to send, but that's ok)
+ conf.SMTPSenderFrom = "noreply@example.com"
+ s := newTestServer(t, conf)
+ defer s.closeDatabases()
+
+ // Create a user without a tier
+ require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false))
+
+ // Verify email request should NOT return 401
+ response := request(t, s, "PUT", "/v1/account/email/verify", `{"email":"ben@example.com"}`, map[string]string{
+ "Authorization": util.BasicAuth("ben", "ben"),
+ })
+ // The request will fail (SMTP not available), but it must NOT be a 401
+ require.NotEqual(t, 401, response.Code)
+ })
+}
+
+func TestServer_AccountEmailVerify_UserWithoutTier_EmailLimitZero(t *testing.T) {
+ // This test verifies that a tier-less user is rejected when the server's
+ // visitor email limit is zero (email sending disabled).
+ forEachBackend(t, func(t *testing.T, databaseURL string) {
+ conf := newTestConfigWithAuthFile(t, databaseURL)
+ conf.SMTPSenderVerify = true
+ conf.SMTPSenderAddr = "localhost:25"
+ conf.SMTPSenderFrom = "noreply@example.com"
+ conf.VisitorEmailLimitBurst = 0
+ s := newTestServer(t, conf)
+ defer s.closeDatabases()
+
+ // Create a user without a tier
+ require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser, false))
+
+ // Should be rejected with 401 since email sending is disabled
+ response := request(t, s, "PUT", "/v1/account/email/verify", `{"email":"ben@example.com"}`, map[string]string{
+ "Authorization": util.BasicAuth("ben", "ben"),
+ })
+ require.Equal(t, 401, response.Code)
+ })
+}
+
func TestServer_PublishAndExpungeTopicAfter16Hours(t *testing.T) {
forEachBackend(t, func(t *testing.T, databaseURL string) {
t.Parallel()
diff --git a/server/visitor.go b/server/visitor.go
index 6d8fe6d1..d5e774d7 100644
--- a/server/visitor.go
+++ b/server/visitor.go
@@ -440,13 +440,17 @@ func configBasedVisitorLimits(conf *Config) *visitorLimits {
if conf.VisitorMessageDailyLimit > 0 {
messagesLimit = int64(conf.VisitorMessageDailyLimit)
}
+ var emailLimit int64
+ if conf.VisitorEmailLimitBurst > 0 {
+ emailLimit = replenishDurationToDailyLimit(conf.VisitorEmailLimitReplenish) // Approximation!
+ }
return &visitorLimits{
Basis: visitorLimitBasisIP,
RequestLimitBurst: conf.VisitorRequestLimitBurst,
RequestLimitReplenish: rate.Every(conf.VisitorRequestLimitReplenish),
MessageLimit: messagesLimit,
MessageExpiryDuration: conf.CacheDuration,
- EmailLimit: replenishDurationToDailyLimit(conf.VisitorEmailLimitReplenish), // Approximation!
+ EmailLimit: emailLimit,
EmailLimitBurst: conf.VisitorEmailLimitBurst,
EmailLimitReplenish: rate.Every(conf.VisitorEmailLimitReplenish),
CallLimit: visitorDefaultCallsLimit,
diff --git a/web/public/static/langs/en.json b/web/public/static/langs/en.json
index b809a06f..2e06cc64 100644
--- a/web/public/static/langs/en.json
+++ b/web/public/static/langs/en.json
@@ -251,6 +251,7 @@
"account_usage_messages_title": "Published messages",
"account_usage_emails_title": "Emails sent",
"account_usage_calls_title": "Phone calls made",
+ "account_usage_emails_none": "No email notifications can be sent with this account",
"account_usage_calls_none": "No phone calls can be made with this account",
"account_usage_reservations_title": "Reserved topics",
"account_usage_reservations_none": "No reserved topics for this account",
diff --git a/web/src/components/Account.jsx b/web/src/components/Account.jsx
index de76eac3..29f4872c 100644
--- a/web/src/components/Account.jsx
+++ b/web/src/components/Account.jsx
@@ -945,7 +945,9 @@ const Stats = () => {
)}
{account.role === Role.USER && account.limits.basis === LimitBasis.IP && (
- {t("account_usage_basis_ip_description")}
+
+ {t("account_usage_basis_ip_description")}
+
)}
);
diff --git a/web/src/components/Navigation.jsx b/web/src/components/Navigation.jsx
index 89381cb3..dad8abe8 100644
--- a/web/src/components/Navigation.jsx
+++ b/web/src/components/Navigation.jsx
@@ -117,7 +117,8 @@ const NavList = (props) => {
const isAdmin = account?.role === Role.ADMIN;
const isPaid = account?.billing?.subscription;
- const showUpgradeBanner = config.enable_payments && !isAdmin && !isPaid;
+ const hasTier = !!account?.tier;
+ const showUpgradeBanner = config.enable_payments && !isAdmin && !isPaid && !hasTier;
const showSubscriptionsList = props.subscriptions?.length > 0;
const showNotificationPermissionRequired = useNotificationPermissionListener(() => notifier.notRequested());
const showNotificationPermissionDenied = useNotificationPermissionListener(() => notifier.denied());