Send "X-Email: yes" to primary

This commit is contained in:
binwiederhier
2026-06-20 14:15:12 -04:00
parent 2ee8717e0c
commit bb2ca0facf
4 changed files with 51 additions and 7 deletions
+2 -1
View File
@@ -1049,7 +1049,8 @@ 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-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 primary verified address (falling back to their
first verified address if no primary is designated).
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`
and `visitor-email-limit-burst`. Setting these conservatively is necessary to avoid abuse. and `visitor-email-limit-burst`. Setting these conservatively is necessary to avoid abuse.
+3 -2
View File
@@ -3252,7 +3252,8 @@ you'd like to persist longer, or to blast-notify yourself on all possible channe
Usage is easy: Simply pass the `X-Email` header (or any of its aliases: `X-E-mail`, `Email`, `E-mail`, `Mail`, or `e`). Usage is easy: Simply pass the `X-Email` header (or any of its aliases: `X-E-mail`, `Email`, `E-mail`, `Mail`, or `e`).
Only one e-mail address is supported. If the server has [`smtp-sender-verify`](config.md#e-mail-notifications) enabled (ntfy.sh has this enabled), Only one e-mail address is supported. If the server has [`smtp-sender-verify`](config.md#e-mail-notifications) enabled (ntfy.sh has this enabled),
you can also pass `yes`, `true`, or `1` to send to your first verified email address. you can also pass `yes`, `true`, or `1` to send to your **primary email address** (the one marked primary in the web app's
[Account section](https://ntfy.sh/account)). If you haven't designated a primary address, it falls back to your first verified address.
ntfy allows anonymous email sending (if enabled), so the rate limiting is pretty strict (see [limitations](#limitations)). In the ntfy allows anonymous email sending (if enabled), so the rate limiting is pretty strict (see [limitations](#limitations)). In the
default configuration, you get **16 e-mails per visitor** (IP address) and then after that one per hour. On top of default configuration, you get **16 e-mails per visitor** (IP address) and then after that one per hour. On top of
@@ -3702,7 +3703,7 @@ all the supported fields:
| `icon` | - | *string* | `https://example.com/icon.png` | URL to use as notification [icon](#icons) | | `icon` | - | *string* | `https://example.com/icon.png` | URL to use as notification [icon](#icons) |
| `filename` | - | *string* | `file.jpg` | File name of the attachment | | `filename` | - | *string* | `file.jpg` | File name of the attachment |
| `delay` | - | *string* | `30min`, `9am` | Timestamp or duration for delayed delivery | | `delay` | - | *string* | `30min`, `9am` | Timestamp or duration for delayed delivery |
| `email` | - | *e-mail address or 'yes'* | `phil@example.com` or `yes` | E-mail address for e-mail notifications, or `yes` to use first verified address | | `email` | - | *e-mail address or 'yes'* | `phil@example.com` or `yes` | E-mail address for e-mail notifications, or `yes` to use your primary verified address |
| `call` | - | *phone number or 'yes'* | `+1222334444` or `yes` | Phone number to use for [voice call](#phone-calls) | | `call` | - | *phone number or 'yes'* | `+1222334444` or `yes` | Phone number to use for [voice call](#phone-calls) |
| `sequence_id` | - | *string* | `my-sequence-123` | Sequence ID for [updating/deleting notifications](#updating-deleting-notifications) | | `sequence_id` | - | *string* | `my-sequence-123` | Sequence ID for [updating/deleting notifications](#updating-deleting-notifications) |
+9 -2
View File
@@ -866,7 +866,8 @@ func (s *Server) handleAccountPasswordReset(w http.ResponseWriter, r *http.Reque
// 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-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 user's primary email (falling
// back to the first verified email if no primary is designated).
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.SMTPSenderVerify { if !s.config.SMTPSenderVerify {
if toBool(email) { if toBool(email) {
@@ -885,7 +886,13 @@ func (s *Server) convertEmailAddress(u *user.User, email string) (string, *errHT
return "", errHTTPBadRequestEmailAddressNotVerified return "", errHTTPBadRequestEmailAddressNotVerified
} }
if toBool(email) { if toBool(email) {
return emails[0], nil primary, err := s.userManager.PrimaryEmail(u.ID)
if err != nil {
return "", errHTTPInternalError
} else if primary != "" {
return primary, nil
}
return emails[0], nil // No primary designated (e.g. provisioned user); fall back to first verified
} else if util.Contains(emails, email) { } else if util.Contains(emails, email) {
return email, nil return email, nil
} }
+37 -2
View File
@@ -1336,14 +1336,16 @@ func TestServer_DailyMessageQuotaFromDatabase(t *testing.T) {
} }
type testMailer struct { type testMailer struct {
count int count int
mu sync.Mutex lastTo string
mu sync.Mutex
} }
func (t *testMailer) SendNotification(to string, m *model.Message, senderIP string) error { func (t *testMailer) SendNotification(to string, m *model.Message, senderIP string) error {
t.mu.Lock() t.mu.Lock()
defer t.mu.Unlock() defer t.mu.Unlock()
t.count++ t.count++
t.lastTo = to
return nil return nil
} }
@@ -1357,6 +1359,12 @@ func (t *testMailer) Count() int {
return t.count return t.count
} }
func (t *testMailer) LastTo() string {
t.mu.Lock()
defer t.mu.Unlock()
return t.lastTo
}
func (t *testMailer) SendEmailVerification(to, link string) error { return nil } func (t *testMailer) SendEmailVerification(to, link string) error { return nil }
func (t *testMailer) SendPasswordReset(to, link string) error { return nil } func (t *testMailer) SendPasswordReset(to, link string) error { return nil }
@@ -1653,6 +1661,33 @@ func TestServer_PublishEmailVerify_BoolValue(t *testing.T) {
}) })
} }
func TestServer_PublishEmailVerify_BoolValueUsesPrimary(t *testing.T) {
forEachBackend(t, func(t *testing.T, databaseURL string) {
conf := newTestConfigWithAuthFile(t, databaseURL)
conf.SMTPSenderVerify = true
s := newTestServer(t, conf)
mailer := &testMailer{}
s.mailer = mailer
defer s.closeDatabases()
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser, false))
u, err := s.userManager.User("phil")
require.Nil(t, err)
// Two verified emails; the primary is NOT the alphabetically-first one
require.Nil(t, s.userManager.AddEmail(u.ID, "aaa@example.com"))
require.Nil(t, s.userManager.AddEmail(u.ID, "zzz@example.com"))
require.Nil(t, s.userManager.SetPrimaryEmail(u.ID, "zzz@example.com"))
// "yes" must resolve to the primary email, not emails[0] (alphabetically first)
response := request(t, s, "PUT", "/mytopic", "hi", map[string]string{
"Email": "yes",
"Authorization": util.BasicAuth("phil", "phil"),
})
require.Equal(t, 200, response.Code)
require.Equal(t, "zzz@example.com", mailer.LastTo())
})
}
func TestServer_PublishEmailVerify_BoolValue_NoVerify(t *testing.T) { func TestServer_PublishEmailVerify_BoolValue_NoVerify(t *testing.T) {
forEachBackend(t, func(t *testing.T, databaseURL string) { forEachBackend(t, func(t *testing.T, databaseURL string) {
s := newTestServer(t, newTestConfig(t, databaseURL)) s := newTestServer(t, newTestConfig(t, databaseURL))