toeverything/AFFiNE · error · EmailServiceNotConfigured
email_service_not_configured
email_service_not_configured
Error message
Email service is not configured.
What it means
Mailer.send throws EmailServiceNotConfigured (internal_server_error / email_service_not_configured) when MailSender.configured is false — meaning no SMTP transport was ever created because the mailer SMTP config (MAILER_HOST/PORT/USER/PASSWORD/SENDER, set up on the config.init event) is absent and the environment is not a test env. trySend()/send(command, true) suppress this and return false instead.
Solutions
- Configure SMTP: set the MAILER_* env vars (host, port, user, password, sender) or the equivalent app-config mailer.SMTP block and restart
- Use mailer.trySend(command) for optional mail so unconfigured SMTP degrades to false instead of throwing
- Catch EmailServiceNotConfigured in flows that must continue without email and surface a 'mail not configured' state to the operator
- In dev, use an Ethereal/test SMTP account or set env.testing to use the mock transport
Example fix
// before
await mailer.send(inviteMail);
// after
const ok = await mailer.trySend(inviteMail); // false instead of throwing when SMTP unset
if (!ok) logger.warn('mail skipped: SMTP not configured'); Defensive patterns
Strategy: fallback
Validate before calling
// only attempt send when mail is actually configured
if (!mailerConfigured()) { // e.g. !!process.env.MAILER_HOST && !env.testing
logger.warn('SMTP not configured; skipping mail');
return false;
}
await mailer.send(command); Type guard
function isEmailServiceNotConfigured(e: unknown): boolean {
return (e as { extensions?: { code?: string } }).extensions?.code === 'email_service_not_configured';
} Try / catch
try {
await mailer.send(command);
} catch (e) {
if (isEmailServiceNotConfigured(e)) {
return false; // degrade gracefully; surface 'configure SMTP' to the operator
}
throw e;
} Prevention
- Prefer mailer.trySend() for all optional/notification mail
- Set MAILER_* env vars (or app-config mailer.SMTP) before enabling email-dependent features
- Add a startup warning when mailer config is absent but email flows are enabled
When it happens
Trigger: Calling mailer.send directly (or a code path that requires email, like verification mails) on a self-hosted deployment with no MAILER_* configuration; sending before the config.init event has run; overriding mail config with empty values.
Common situations: Fresh self-hosted installs that skipped the email setup step in the environment file; staging environments cloned without the mailer env block; features that call send() (throwing variant) instead of trySend() for optional notifications.
Related errors
AI-assisted analysis of toeverything/AFFiNE@2af30773ae (2026-08-18).
Data as JSON: /api/errors/4f01902a50ae3137.
Report an issue: GitHub.
Appendix: source
Thrown at packages/backend/server/src/core/mail/mailer.ts:91
recipientUserId: metadata.recipientUserId,
actorUserId: metadata.actorUserId,
workspaceId: metadata.workspaceId,
notificationId: metadata.notificationId,
abuseSubjectKey: metadata.abuseSubjectKey,
payload: serializePayload(command),
expiresAt: metadata.expiresAt,
maxAttempts: 0,
lastErrorCode: options.reason,
});
return false;
}
async send(command: MailCommand, suppressError = false) {
if (!this.sender.configured) {
if (suppressError) {
return false;
}
throw new EmailServiceNotConfigured();
}
let reservationId: string | undefined;
let deliveryId: string | undefined;
try {
const metadata = command.metadata ?? {};
const deduped = metadata.dedupeKey
? await this.models.mailDelivery.findByDedupeKey(metadata.dedupeKey)
: null;
if (deduped) {
return !['failed', 'canceled', 'skipped'].includes(deduped.status);
}
const decision = await this.runtime.assertMailDeliveryQuotaV1({
mailName: command.name as MailName,
recipient: {
email: command.to,
userId: metadata.recipientUserId,View on GitHub (pinned to 2af30773ae)