mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-13 21:01:21 +08:00
Error when sending email takes too long
This commit is contained in:
parent
f15de038fe
commit
b7d4277510
@ -6,8 +6,9 @@ import { EMAIL_TEMPLATES_METADATA, renderEmailTemplate } from '@stackframe/stack
|
||||
import { ProjectsCrud } from '@stackframe/stack-shared/dist/interface/crud/projects';
|
||||
import { UsersCrud } from '@stackframe/stack-shared/dist/interface/crud/users';
|
||||
import { getEnvVariable } from '@stackframe/stack-shared/dist/utils/env';
|
||||
import { StackAssertionError } from '@stackframe/stack-shared/dist/utils/errors';
|
||||
import { filterUndefined } from '@stackframe/stack-shared/dist/utils/objects';
|
||||
import { StackAssertionError, captureError } from '@stackframe/stack-shared/dist/utils/errors';
|
||||
import { filterUndefined, pick } from '@stackframe/stack-shared/dist/utils/objects';
|
||||
import { runAsynchronously, wait } from '@stackframe/stack-shared/dist/utils/promises';
|
||||
import { Result } from '@stackframe/stack-shared/dist/utils/results';
|
||||
import { typedToUppercase } from '@stackframe/stack-shared/dist/utils/strings';
|
||||
import nodemailer from 'nodemailer';
|
||||
@ -75,113 +76,130 @@ export async function sendEmailWithKnownErrorTypes(options: SendEmailOptions): P
|
||||
canRetry: boolean,
|
||||
message?: string,
|
||||
}>> {
|
||||
return await traceSpan('sending email to ' + JSON.stringify(options.to), async () => {
|
||||
try {
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: options.emailConfig.host,
|
||||
port: options.emailConfig.port,
|
||||
secure: options.emailConfig.secure,
|
||||
auth: {
|
||||
user: options.emailConfig.username,
|
||||
pass: options.emailConfig.password,
|
||||
},
|
||||
});
|
||||
let finished = false;
|
||||
runAsynchronously(async () => {
|
||||
await wait(5000);
|
||||
if (!finished) {
|
||||
captureError("email-send-timeout", new StackAssertionError("Email send took longer than 8s; maybe the email service is too slow?", {
|
||||
config: options.emailConfig.type === 'shared' ? "shared" : pick(options.emailConfig, ['host', 'port', 'username', 'senderEmail', 'senderName']),
|
||||
to: options.to,
|
||||
subject: options.subject,
|
||||
html: options.html,
|
||||
text: options.text,
|
||||
}));
|
||||
}
|
||||
});
|
||||
try {
|
||||
return await traceSpan('sending email to ' + JSON.stringify(options.to), async () => {
|
||||
try {
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: options.emailConfig.host,
|
||||
port: options.emailConfig.port,
|
||||
secure: options.emailConfig.secure,
|
||||
auth: {
|
||||
user: options.emailConfig.username,
|
||||
pass: options.emailConfig.password,
|
||||
},
|
||||
});
|
||||
|
||||
await transporter.sendMail({
|
||||
from: `"${options.emailConfig.senderName}" <${options.emailConfig.senderEmail}>`,
|
||||
...options,
|
||||
});
|
||||
await transporter.sendMail({
|
||||
from: `"${options.emailConfig.senderName}" <${options.emailConfig.senderEmail}>`,
|
||||
...options,
|
||||
});
|
||||
|
||||
return Result.ok(undefined);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
const code = (error as any).code as string | undefined;
|
||||
const responseCode = (error as any).responseCode as number | undefined;
|
||||
const errorNumber = (error as any).errno as number | undefined;
|
||||
return Result.ok(undefined);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
const code = (error as any).code as string | undefined;
|
||||
const responseCode = (error as any).responseCode as number | undefined;
|
||||
const errorNumber = (error as any).errno as number | undefined;
|
||||
|
||||
const getServerResponse = (error: any) => {
|
||||
if (error?.response) {
|
||||
return `\nResponse from the email server:\n${error.response}`;
|
||||
const getServerResponse = (error: any) => {
|
||||
if (error?.response) {
|
||||
return `\nResponse from the email server:\n${error.response}`;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
if (errorNumber === -3008 || code === 'EDNS') {
|
||||
return Result.error({
|
||||
rawError: error,
|
||||
errorType: 'HOST_NOT_FOUND',
|
||||
canRetry: false,
|
||||
message: 'Failed to connect to the email host. Please make sure the email host configuration is correct.'
|
||||
} as const);
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
if (errorNumber === -3008 || code === 'EDNS') {
|
||||
return Result.error({
|
||||
rawError: error,
|
||||
errorType: 'HOST_NOT_FOUND',
|
||||
canRetry: false,
|
||||
message: 'Failed to connect to the email host. Please make sure the email host configuration is correct.'
|
||||
} as const);
|
||||
if (responseCode === 535 || code === 'EAUTH') {
|
||||
return Result.error({
|
||||
rawError: error,
|
||||
errorType: 'AUTH_FAILED',
|
||||
canRetry: false,
|
||||
message: 'Failed to authenticate with the email server. Please check your email credentials configuration.',
|
||||
} as const);
|
||||
}
|
||||
|
||||
if (responseCode === 450) {
|
||||
return Result.error({
|
||||
rawError: error,
|
||||
errorType: 'TEMPORARY',
|
||||
canRetry: true,
|
||||
message: 'The email server returned a temporary error. This could be due to a temporary network issue or a temporary block on the email server. Please try again later.\n\nError: ' + getServerResponse(error),
|
||||
} as const);
|
||||
}
|
||||
|
||||
if (responseCode === 553) {
|
||||
return Result.error({
|
||||
rawError: error,
|
||||
errorType: 'INVALID_EMAIL_ADDRESS',
|
||||
canRetry: false,
|
||||
message: 'The email address provided is invalid. Please verify both the recipient and sender email addresses configuration are correct.\n\nError:' + getServerResponse(error),
|
||||
} as const);
|
||||
}
|
||||
|
||||
if (error.message.includes('Unexpected socket close')) {
|
||||
return Result.error({
|
||||
rawError: error,
|
||||
errorType: 'SOCKET_CLOSED',
|
||||
canRetry: false,
|
||||
message: 'Connection to email server was lost unexpectedly. This could be due to incorrect email server port configuration or a temporary network issue. Please verify your configuration and try again.',
|
||||
} as const);
|
||||
}
|
||||
}
|
||||
|
||||
if (responseCode === 535 || code === 'EAUTH') {
|
||||
// ============ temporary error ============
|
||||
const temporaryErrorIndicators = [
|
||||
"450 ",
|
||||
"Client network socket disconnected before secure TLS connection was established",
|
||||
"Too many requests",
|
||||
...options.emailConfig.host.includes("resend") ? [
|
||||
// Resend is a bit unreliable, so we'll retry even in some cases where it may send duplicate emails
|
||||
"ECONNRESET",
|
||||
] : [],
|
||||
];
|
||||
if (temporaryErrorIndicators.some(indicator => error instanceof Error && error.message.includes(indicator))) {
|
||||
// this can happen occasionally (especially with certain unreliable email providers)
|
||||
// so let's retry
|
||||
return Result.error({
|
||||
rawError: error,
|
||||
errorType: 'AUTH_FAILED',
|
||||
canRetry: false,
|
||||
message: 'Failed to authenticate with the email server. Please check your email credentials configuration.',
|
||||
} as const);
|
||||
}
|
||||
|
||||
if (responseCode === 450) {
|
||||
return Result.error({
|
||||
rawError: error,
|
||||
errorType: 'TEMPORARY',
|
||||
errorType: 'UNKNOWN',
|
||||
canRetry: true,
|
||||
message: 'The email server returned a temporary error. This could be due to a temporary network issue or a temporary block on the email server. Please try again later.\n\nError: ' + getServerResponse(error),
|
||||
message: 'Failed to send email, but error is possibly transient due to the internet connection. Please check your email configuration and try again later.',
|
||||
} as const);
|
||||
}
|
||||
|
||||
if (responseCode === 553) {
|
||||
return Result.error({
|
||||
rawError: error,
|
||||
errorType: 'INVALID_EMAIL_ADDRESS',
|
||||
canRetry: false,
|
||||
message: 'The email address provided is invalid. Please verify both the recipient and sender email addresses configuration are correct.\n\nError:' + getServerResponse(error),
|
||||
} as const);
|
||||
}
|
||||
|
||||
if (error.message.includes('Unexpected socket close')) {
|
||||
return Result.error({
|
||||
rawError: error,
|
||||
errorType: 'SOCKET_CLOSED',
|
||||
canRetry: false,
|
||||
message: 'Connection to email server was lost unexpectedly. This could be due to incorrect email server port configuration or a temporary network issue. Please verify your configuration and try again.',
|
||||
} as const);
|
||||
}
|
||||
}
|
||||
|
||||
// ============ temporary error ============
|
||||
const temporaryErrorIndicators = [
|
||||
"450 ",
|
||||
"Client network socket disconnected before secure TLS connection was established",
|
||||
"Too many requests",
|
||||
...options.emailConfig.host.includes("resend") ? [
|
||||
// Resend is a bit unreliable, so we'll retry even in some cases where it may send duplicate emails
|
||||
"ECONNRESET",
|
||||
] : [],
|
||||
];
|
||||
if (temporaryErrorIndicators.some(indicator => error instanceof Error && error.message.includes(indicator))) {
|
||||
// this can happen occasionally (especially with certain unreliable email providers)
|
||||
// so let's retry
|
||||
// ============ unknown error ============
|
||||
return Result.error({
|
||||
rawError: error,
|
||||
errorType: 'UNKNOWN',
|
||||
canRetry: true,
|
||||
message: 'Failed to send email, but error is possibly transient due to the internet connection. Please check your email configuration and try again later.',
|
||||
canRetry: false,
|
||||
message: 'An unknown error occurred while sending the email.',
|
||||
} as const);
|
||||
}
|
||||
|
||||
// ============ unknown error ============
|
||||
return Result.error({
|
||||
rawError: error,
|
||||
errorType: 'UNKNOWN',
|
||||
canRetry: false,
|
||||
message: 'An unknown error occurred while sending the email.',
|
||||
} as const);
|
||||
}
|
||||
});
|
||||
});
|
||||
} finally {
|
||||
finished = true;
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendEmail(options: SendEmailOptions) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user