{"record":{"id":"4f01902a50ae3137","repo":"toeverything/AFFiNE","slug":"email-service-not-configured","errorCode":"email_service_not_configured","errorMessage":"Email service is not configured.","messagePattern":"Email service is not configured\\.","errorType":"exception","errorClass":"EmailServiceNotConfigured","httpStatus":500,"severity":"error","filePath":"packages/backend/server/src/core/mail/mailer.ts","lineNumber":96,"sourceCode":"      recipientUserId: metadata.recipientUserId,\n      actorUserId: metadata.actorUserId,\n      workspaceId: metadata.workspaceId,\n      notificationId: metadata.notificationId,\n      abuseSubjectKey: metadata.abuseSubjectKey,\n      payload: serializePayload(command),\n      expiresAt: metadata.expiresAt,\n      maxAttempts: 0,\n      lastErrorCode: options.reason,\n    });\n    return false;\n  }\n\n  async send(command: MailCommand, suppressError = false) {\n    if (!this.sender.configured) {\n      if (suppressError) {\n        return false;\n      }\n      throw new EmailServiceNotConfigured();\n    }\n\n    let reservationId: string | undefined;\n    let deliveryId: string | undefined;\n    try {\n      const metadata = command.metadata ?? {};\n      const deduped = metadata.dedupeKey\n        ? await this.models.mailDelivery.findByDedupeKey(metadata.dedupeKey)\n        : null;\n      if (deduped) {\n        return !['failed', 'canceled', 'skipped'].includes(deduped.status);\n      }\n\n      const decision = await this.runtime.assertMailDeliveryQuotaV1({\n        mailName: command.name as MailName,\n        recipient: {\n          email: command.to,\n          domain: recipientDomain(command.to),","sourceCodeStart":78,"sourceCodeEnd":114,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/26c515e050211269e911f7d9cfe162a26c83ed98/packages/backend/server/src/core/mail/mailer.ts#L78-L114","documentation":"EmailServiceNotConfigured (code=email_service_not_configured) thrown by Mailer.send when this.sender.configured is false and the caller did not pass suppressError. The mailer refuses to enqueue/attempt delivery because no SMTP/transactional provider is set up. When suppressError=true the call returns false instead.","triggerScenarios":"Any mail-sending path (verification, invitation, notification, test mail) invoked on a deployment where the mail sender has no valid configuration. The check is on sender.configured, which is false until SMTP host/credentials (or the transactional provider) are provided.","commonSituations":"Fresh self-hosted install without SMTP env vars set. Mail config removed/rotated and not reapplied. A feature that sends mail enabled while mail is unconfigured (invites on, SMTP off).","solutions":["Configure SMTP (or the configured transactional provider) in env and restart the server; verify sender.configured is true via the admin mail status endpoint.","For non-critical mail paths, pass suppressError=true so the absence of mail degrades gracefully instead of failing the workflow.","Gate mail-dependent features (invitations, verification) behind a 'mail configured' check in the UI.","Add a startup warning/log when mail-dependent features are enabled but sender.configured is false."],"exampleFix":"// before\nasync send(command: MailCommand, suppressError = false) {\n  if (!this.sender.configured) {\n    if (suppressError) return false;\n    throw new EmailServiceNotConfigured();\n  }\n  ...\n}\n\n// caller side — pass suppressError for non-critical mail\nawait this.mailer.send(inviteMail, /* suppressError */ true);","handlingStrategy":"validation","validationCode":"async function assertMailConfigured(mailer) {\n  if (!mailer.sender.configured) {\n    throw new UserError('Email service is not configured');\n  }\n}","typeGuard":"function isEmailNotConfigured(e: unknown): boolean {\n  return e instanceof Error && (e as any).code === 'email_service_not_configured';\n}","tryCatchPattern":"// For non-critical mail, suppress; for critical, surface a clear message\ntry {\n  await mailer.send(mail);\n} catch (e) {\n  if (isEmailNotConfigured(e)) {\n    return res.status(503).send('Email not configured on this server');\n  }\n  throw e;\n}\n// OR: await mailer.send(mail, /* suppressError */ true);","preventionTips":["Configure SMTP env before enabling mail-dependent features.","Gate 'send invite' UI on a mail-configured flag.","Use suppressError=true for non-critical mail so workflows don't break."],"tags":["mail","smtp","configuration","env","self-hosted"],"backgroundTag":null,"analyzedSha":"26c515e050211269e911f7d9cfe162a26c83ed98","analyzedAt":"2026-08-12T13:15:16.447Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}