diff --git a/mail/sender.go b/mail/sender.go index 9efb2f6c..5511cb04 100644 --- a/mail/sender.go +++ b/mail/sender.go @@ -56,8 +56,23 @@ func (s *Sender) Close() { close(s.closeChan) } -// Send sends a plain text email via SMTP -func (s *Sender) Send(to, subject, body string) error { +// Addr returns the SMTP server address +func (s *Sender) Addr() string { + return s.config.SMTPAddr +} + +// User returns the SMTP username +func (s *Sender) User() string { + return s.config.SMTPUser +} + +// From returns the sender email address +func (s *Sender) From() string { + return s.config.From +} + +// SendRaw sends a raw email message via SMTP +func (s *Sender) SendRaw(to string, message []byte) error { host, _, err := net.SplitHostPort(s.config.SMTPAddr) if err != nil { return err @@ -66,6 +81,11 @@ func (s *Sender) Send(to, subject, body string) error { if s.config.SMTPUser != "" { auth = smtp.PlainAuth("", s.config.SMTPUser, s.config.SMTPPass, host) } + return smtp.SendMail(s.config.SMTPAddr, auth, s.config.From, []string{to}, message) +} + +// Send sends a plain text email via SMTP +func (s *Sender) Send(to, subject, body string) error { date := time.Now().UTC().Format(time.RFC1123Z) encodedSubject := mime.BEncoding.Encode("utf-8", subject) message := `From: ntfy <{from}> @@ -81,7 +101,7 @@ Content-Type: text/plain; charset="utf-8" message = strings.ReplaceAll(message, "{subject}", encodedSubject) message = strings.ReplaceAll(message, "{body}", body) log.Tag("mail").Field("email_to", to).Debug("Sending email") - return smtp.SendMail(s.config.SMTPAddr, auth, s.config.From, []string{to}, []byte(message)) + return s.SendRaw(to, []byte(message)) } // SendVerification generates a random code, stores it in-memory, and sends a verification email diff --git a/server/errors.go b/server/errors.go index 16acc9cd..aab51df4 100644 --- a/server/errors.go +++ b/server/errors.go @@ -144,7 +144,7 @@ var ( errHTTPBadRequestSequenceIDInvalid = &errHTTP{40049, http.StatusBadRequest, "invalid request: sequence ID invalid", "https://ntfy.sh/docs/publish/#updating-deleting-notifications", nil} errHTTPBadRequestEmailAddressInvalid = &errHTTP{40050, http.StatusBadRequest, "invalid request: invalid e-mail address", "https://ntfy.sh/docs/publish/#e-mail-notifications", nil} errHTTPBadRequestEmailVerificationCodeInvalid = &errHTTP{40051, http.StatusBadRequest, "invalid request: email verification code invalid or expired", "", nil} - errHTTPBadRequestEmailAddressNotVerified = &errHTTP{40052, http.StatusBadRequest, "invalid request: email address not verified, or no matching verified email addresses found", "https://ntfy.sh/docs/publish/#e-mail-notifications", nil} + errHTTPBadRequestEmailAddressNotVerified = &errHTTP{40052, http.StatusBadRequest, "invalid request: email address not verified", "https://ntfy.sh/docs/publish/#e-mail-notifications", nil} errHTTPBadRequestAnonymousEmailNotAllowed = &errHTTP{40053, http.StatusBadRequest, "invalid request: anonymous email sending is not allowed", "https://ntfy.sh/docs/publish/#e-mail-notifications", nil} errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "page not found", "", nil} errHTTPUnauthorized = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication", nil} diff --git a/server/server.go b/server/server.go index 62213879..89abe518 100644 --- a/server/server.go +++ b/server/server.go @@ -179,13 +179,13 @@ 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, }) + mailer = &smtpSender{config: conf, sender: mailSender} } var stripe stripeAPI if payments.Available && conf.StripeSecretKey != "" { @@ -721,6 +721,7 @@ func (s *Server) configResponse() *apiConfigResponse { EnablePayments: s.config.StripeSecretKey != "", EnableCalls: s.config.TwilioAccount != "", EnableEmails: s.config.SMTPSenderFrom != "", + EnableEmailVerify: s.config.SMTPSenderVerify, EnableReservations: s.config.EnableReservations, EnableWebPush: s.config.WebPushPublicKey != "", BillingContact: s.config.BillingContact, @@ -1202,7 +1203,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *model.Message) (cache bo m.Icon = icon } email = readParam(r, "x-email", "x-e-mail", "email", "e-mail", "mail", "e") - if email != "" && !emailAddressRegex.MatchString(email) { + if email != "" && !emailAddressRegex.MatchString(email) && !toBool(email) { return false, false, "", "", "", false, "", errHTTPBadRequestEmailAddressInvalid } if s.smtpSender == nil && email != "" { diff --git a/server/server_account.go b/server/server_account.go index 39554348..9acdf450 100644 --- a/server/server_account.go +++ b/server/server_account.go @@ -689,6 +689,9 @@ func (s *Server) handleAccountEmailDelete(w http.ResponseWriter, r *http.Request // 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.SMTPSenderVerify { + if toBool(email) { + return "", errHTTPBadRequestEmailAddressInvalid + } return email, nil } else if u == nil { return "", errHTTPBadRequestAnonymousEmailNotAllowed diff --git a/server/smtp_sender.go b/server/smtp_sender.go index 4e5988ba..30966267 100644 --- a/server/smtp_sender.go +++ b/server/smtp_sender.go @@ -5,13 +5,12 @@ import ( "encoding/json" "fmt" "mime" - "net" - "net/smtp" "strings" "sync" "time" "heckel.io/ntfy/v2/log" + "heckel.io/ntfy/v2/mail" "heckel.io/ntfy/v2/model" "heckel.io/ntfy/v2/util" ) @@ -23,6 +22,7 @@ type mailer interface { type smtpSender struct { config *Config + sender *mail.Sender success int64 failure int64 mu sync.Mutex @@ -30,31 +30,23 @@ type smtpSender struct { func (s *smtpSender) Send(v *visitor, m *model.Message, to string) error { return s.withCount(v, m, func() error { - host, _, err := net.SplitHostPort(s.config.SMTPSenderAddr) + message, err := formatMail(s.config.BaseURL, v.ip.String(), s.sender.From(), to, m) if err != nil { return err } - message, err := formatMail(s.config.BaseURL, v.ip.String(), s.config.SMTPSenderFrom, to, m) - if err != nil { - return err - } - var auth smtp.Auth - if s.config.SMTPSenderUser != "" { - auth = smtp.PlainAuth("", s.config.SMTPSenderUser, s.config.SMTPSenderPass, host) - } ev := logvm(v, m). Tag(tagEmail). Fields(log.Context{ - "email_via": s.config.SMTPSenderAddr, - "email_user": s.config.SMTPSenderUser, + "email_via": s.sender.Addr(), + "email_user": s.sender.User(), "email_to": to, }) if ev.IsTrace() { ev.Field("email_body", message).Trace("Sending email") } else if ev.IsDebug() { - ev.Debug("Sending email") + ev.Info("Sending email") } - return smtp.SendMail(s.config.SMTPSenderAddr, auth, s.config.SMTPSenderFrom, []string{to}, []byte(message)) + return s.sender.SendRaw(to, []byte(message)) }) } diff --git a/server/types.go b/server/types.go index c9d4688d..1f69d3de 100644 --- a/server/types.go +++ b/server/types.go @@ -312,6 +312,7 @@ type apiConfigResponse struct { EnablePayments bool `json:"enable_payments"` EnableCalls bool `json:"enable_calls"` EnableEmails bool `json:"enable_emails"` + EnableEmailVerify bool `json:"enable_email_verify"` EnableReservations bool `json:"enable_reservations"` EnableWebPush bool `json:"enable_web_push"` BillingContact string `json:"billing_contact"` diff --git a/web/public/static/langs/en.json b/web/public/static/langs/en.json index 984db05c..617dce5b 100644 --- a/web/public/static/langs/en.json +++ b/web/public/static/langs/en.json @@ -215,7 +215,7 @@ "account_basics_phone_numbers_dialog_check_verification_button": "Confirm code", "account_basics_phone_numbers_dialog_channel_sms": "SMS", "account_basics_phone_numbers_dialog_channel_call": "Call", - "account_basics_emails_title": "Verified email recipients", + "account_basics_emails_title": "Email addresses", "account_basics_emails_description": "For email notifications", "account_basics_emails_no_emails_yet": "No verified emails yet", "account_basics_emails_copied_to_clipboard": "Email address copied to clipboard", diff --git a/web/src/components/Account.jsx b/web/src/components/Account.jsx index 0bca6120..de76eac3 100644 --- a/web/src/components/Account.jsx +++ b/web/src/components/Account.jsx @@ -84,7 +84,7 @@ const Basics = () => { - + @@ -355,7 +355,7 @@ const AccountType = () => { ); }; -const VerifiedEmails = () => { +const Emails = () => { const { t } = useTranslation(); const { account } = useContext(AccountContext); const [dialogKey, setDialogKey] = useState(0); @@ -388,7 +388,7 @@ const VerifiedEmails = () => { } }; - if (!config.enable_emails) { + if (!config.enable_email_verify) { return null; }