New Issue Checklist
Issue Description
Verification and password reset emails are dispatched without awaiting the mail adapter and without a rejection handler anywhere in the chain. A mail adapter that rejects, which is what an outage at the email provider looks like, produces an unhandled promise rejection.
The adapter call itself is never awaited, in either flow and in both the adapter-specific and sendMail fallback branches:
// src/Controllers/UserController.js
if (this.adapter.sendVerificationEmail) {
this.adapter.sendVerificationEmail(options); // 181
} else {
this.adapter.sendMail(this.defaultVerificationEmail(options)); // 183
}
if (this.adapter.sendPasswordResetEmail) {
this.adapter.sendPasswordResetEmail(options); // 293
} else {
this.adapter.sendMail(this.defaultResetPasswordEmail(options)); // 295
}
Several callers of the surrounding controller methods are also not awaited:
| Location |
Call |
Runs on |
src/RestWrite.js:1214 |
userController.sendVerificationEmail(…) |
user signup |
src/Routers/UsersRouter.js:685 |
userController.sendVerificationEmail(user, req) |
POST /verificationEmailRequest |
src/Controllers/UserController.js:227 |
this.sendVerificationEmail(aUser, req) |
resend from the verification page |
UsersRouter.handleResetRequest at line 630 does await userController.sendPasswordResetEmail(email), but that await covers only token generation, since the adapter call inside resolves before delivery is attempted.
I want to separate two things here, because I think only one of them is unambiguously a defect.
The unhandled rejection is a defect. No .catch() exists at any level, so a rejecting mail adapter surfaces as an unhandled rejection with no indication of which email failed or for which user. Depending on the Node version and process configuration that either logs a bare warning or terminates the process, so an outage at the email provider can take down an otherwise healthy server. This is the same shape as #10634, where RedisCacheAdapter write methods reject into unawaited call sites.
Whether the client should learn about the failure is a design question, and I am not assuming the answer. The signup path is explicitly commented // Fire and forget! (src/RestWrite.js:1213), so the non-blocking dispatch is deliberate, and there are good reasons for it. Blocking signup on an email provider is a poor tradeoff, and for the password reset flow specifically, awaiting delivery leaks timing that helps an attacker distinguish registered from unregistered addresses, which is the enumeration exposure that resetPasswordSuccessOnInvalidEmail exists to close. So "return 200 regardless" may well be the intended contract.
What that leaves is a question for maintainers: should a failed send at minimum be logged with the adapter error and the target flow, so operators can detect a broken email pipeline, while delivery stays non-blocking? Today a completely dead mail adapter is silent in the logs, and the only signal is a stray unhandled rejection warning with no context.
Steps to reproduce
- Configure Parse Server with a mail adapter whose
sendVerificationEmail or sendPasswordResetEmail returns a rejected promise.
- Sign up a user with
verifyUserEmails: true, or call POST /requestPasswordReset.
const server = await reconfigureServer({
verifyUserEmails: true,
publicServerURL: 'http://localhost:8378/1',
emailAdapter: {
sendVerificationEmail: () => Promise.reject(new Error('Email provider is down')),
sendPasswordResetEmail: () => Promise.reject(new Error('Email provider is down')),
sendMail: () => Promise.reject(new Error('Email provider is down')),
},
});
process.on('unhandledRejection', reason => console.log('unhandled:', reason.message));
const user = new Parse.User();
user.setUsername('zebra');
user.setPassword('password');
user.setEmail('zebra@example.com');
await user.signUp();
Actual Outcome
The request succeeds. unhandled: Email provider is down is printed. Nothing is written to the Parse Server log identifying the failed email, the flow it belonged to, or the user it was addressed to.
Expected Outcome
At minimum, the rejection is caught and logged with the adapter error and enough context to identify the flow, and no unhandled rejection is produced. Whether the response to the client should change is the maintainers' call, and I would not change it without direction, given the enumeration tradeoff on the password reset path.
Happy to open a PR once there is a decision on scope.
Environment
Server
- Parse Server version:
9.10.1-alpha.6
- Operating system:
macOS 15.5
- Local or remote host:
local
Database
- System (MongoDB or Postgres):
MongoDB
- Database version:
8.0
- Local or remote host:
local
Client
- SDK (iOS, Android, JavaScript, PHP, Unity, etc):
JavaScript
- SDK version:
as vendored by parse-server
Logs
No Parse Server log entry is produced for the failed send. The only output is the runtime's unhandled rejection warning.
New Issue Checklist
Issue Description
Verification and password reset emails are dispatched without awaiting the mail adapter and without a rejection handler anywhere in the chain. A mail adapter that rejects, which is what an outage at the email provider looks like, produces an unhandled promise rejection.
The adapter call itself is never awaited, in either flow and in both the adapter-specific and
sendMailfallback branches:Several callers of the surrounding controller methods are also not awaited:
src/RestWrite.js:1214userController.sendVerificationEmail(…)src/Routers/UsersRouter.js:685userController.sendVerificationEmail(user, req)POST /verificationEmailRequestsrc/Controllers/UserController.js:227this.sendVerificationEmail(aUser, req)UsersRouter.handleResetRequestat line 630 doesawait userController.sendPasswordResetEmail(email), but that await covers only token generation, since the adapter call inside resolves before delivery is attempted.I want to separate two things here, because I think only one of them is unambiguously a defect.
The unhandled rejection is a defect. No
.catch()exists at any level, so a rejecting mail adapter surfaces as an unhandled rejection with no indication of which email failed or for which user. Depending on the Node version and process configuration that either logs a bare warning or terminates the process, so an outage at the email provider can take down an otherwise healthy server. This is the same shape as #10634, whereRedisCacheAdapterwrite methods reject into unawaited call sites.Whether the client should learn about the failure is a design question, and I am not assuming the answer. The signup path is explicitly commented
// Fire and forget!(src/RestWrite.js:1213), so the non-blocking dispatch is deliberate, and there are good reasons for it. Blocking signup on an email provider is a poor tradeoff, and for the password reset flow specifically, awaiting delivery leaks timing that helps an attacker distinguish registered from unregistered addresses, which is the enumeration exposure thatresetPasswordSuccessOnInvalidEmailexists to close. So "return 200 regardless" may well be the intended contract.What that leaves is a question for maintainers: should a failed send at minimum be logged with the adapter error and the target flow, so operators can detect a broken email pipeline, while delivery stays non-blocking? Today a completely dead mail adapter is silent in the logs, and the only signal is a stray unhandled rejection warning with no context.
Steps to reproduce
sendVerificationEmailorsendPasswordResetEmailreturns a rejected promise.verifyUserEmails: true, or callPOST /requestPasswordReset.Actual Outcome
The request succeeds.
unhandled: Email provider is downis printed. Nothing is written to the Parse Server log identifying the failed email, the flow it belonged to, or the user it was addressed to.Expected Outcome
At minimum, the rejection is caught and logged with the adapter error and enough context to identify the flow, and no unhandled rejection is produced. Whether the response to the client should change is the maintainers' call, and I would not change it without direction, given the enumeration tradeoff on the password reset path.
Happy to open a PR once there is a decision on scope.
Environment
Server
9.10.1-alpha.6macOS 15.5localDatabase
MongoDB8.0localClient
JavaScriptas vendored by parse-serverLogs
No Parse Server log entry is produced for the failed send. The only output is the runtime's unhandled rejection warning.