{"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":91,"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          userId: metadata.recipientUserId,","sourceCodeStart":73,"sourceCodeEnd":109,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/2af30773aecd567f09b346e7b72fc69143144057/packages/backend/server/src/core/mail/mailer.ts#L73-L109","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nawait mailer.send(inviteMail);\n\n// after\nconst ok = await mailer.trySend(inviteMail); // false instead of throwing when SMTP unset\nif (!ok) logger.warn('mail skipped: SMTP not configured');","handlingStrategy":"fallback","validationCode":"// only attempt send when mail is actually configured\nif (!mailerConfigured()) {  // e.g. !!process.env.MAILER_HOST && !env.testing\n  logger.warn('SMTP not configured; skipping mail');\n  return false;\n}\nawait mailer.send(command);","typeGuard":"function isEmailServiceNotConfigured(e: unknown): boolean {\n  return (e as { extensions?: { code?: string } }).extensions?.code === 'email_service_not_configured';\n}","tryCatchPattern":"try {\n  await mailer.send(command);\n} catch (e) {\n  if (isEmailServiceNotConfigured(e)) {\n    return false; // degrade gracefully; surface 'configure SMTP' to the operator\n  }\n  throw e;\n}","preventionTips":["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"],"tags":["mail","smtp","configuration","self-hosted"],"backgroundTag":"missing-smtp-config","analyzedSha":"2af30773aecd567f09b346e7b72fc69143144057","analyzedAt":"2026-08-18T21:16:52.546Z","contentChangedAt":"2026-08-18T21:16:52.546Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}