email.ts 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669
  1. import nodemailer from 'nodemailer'
  2. import { Resend } from 'resend'
  3. import { ServerClient as PostmarkClient } from 'postmark'
  4. import Mailgun from 'mailgun.js'
  5. import FormData from 'form-data'
  6. import sgMail, { type MailDataRequired } from '@sendgrid/mail'
  7. import { SESClient, SendRawEmailCommand } from '@aws-sdk/client-ses'
  8. import {
  9. asLegacyMap,
  10. channelProvider,
  11. channelSettings,
  12. legacyProviderNamed,
  13. } from '@/features/integrations/Lib/messaging'
  14. import { messagingProvider } from '@/integrations/messaging/catalog'
  15. import { db } from './db'
  16. import { SYSTEM_SETTING_KEYS } from '@/features/admin/Schema/systemSettingsSchema'
  17. import { ORG_EMAIL_KEYS } from '@/features/email/Schema/emailSettingsSchema'
  18. export type EmailProvider = 'smtp' | 'resend' | 'postmark' | 'mailgun' | 'sendgrid' | 'ses'
  19. export interface SendMailOptions {
  20. from: string
  21. to: string
  22. /**
  23. * Where a reply should go when it is not the sender. A support request goes
  24. * out from the platform address, so without this the administrator's reply
  25. * button addresses the platform itself rather than the person asking.
  26. */
  27. replyTo?: string
  28. subject: string
  29. html: string
  30. /**
  31. * The plain-text half of the mail. A message with no text part scores worse
  32. * with spam filters, and some people read mail as text on purpose. Every
  33. * provider below takes one; the document mails generate theirs from the
  34. * same spec the HTML comes from, so the two cannot disagree.
  35. */
  36. text?: string
  37. attachments?: {
  38. filename: string
  39. content: Buffer
  40. }[]
  41. }
  42. // ─── Settings map helper ────────────────────────────────────────────────────
  43. type SettingsMap = Map<string, string>
  44. async function getSystemSettings(keys: string[]): Promise<SettingsMap> {
  45. const rows = await db.systemSetting.findMany({
  46. where: { key: { in: keys } },
  47. })
  48. return new Map(rows.map((r) => [r.key, r.value]))
  49. }
  50. // ─── System-level helpers (read from SystemSetting + env) ───────────────────
  51. async function getEmailProvider(): Promise<EmailProvider> {
  52. const setting = await db.systemSetting.findUnique({
  53. where: { key: SYSTEM_SETTING_KEYS.EMAIL_PROVIDER },
  54. })
  55. const value = setting?.value
  56. if (
  57. value === 'resend' ||
  58. value === 'postmark' ||
  59. value === 'mailgun' ||
  60. value === 'sendgrid' ||
  61. value === 'ses'
  62. ) {
  63. return value
  64. }
  65. return 'smtp'
  66. }
  67. export async function getFromAddress(): Promise<string> {
  68. const provider = await getEmailProvider()
  69. const keyMap: Record<EmailProvider, { email: string; name: string }> = {
  70. smtp: {
  71. email: SYSTEM_SETTING_KEYS.SMTP_FROM_EMAIL,
  72. name: SYSTEM_SETTING_KEYS.SMTP_FROM_NAME,
  73. },
  74. resend: {
  75. email: SYSTEM_SETTING_KEYS.RESEND_FROM_EMAIL,
  76. name: SYSTEM_SETTING_KEYS.RESEND_FROM_NAME,
  77. },
  78. postmark: {
  79. email: SYSTEM_SETTING_KEYS.POSTMARK_FROM_EMAIL,
  80. name: SYSTEM_SETTING_KEYS.POSTMARK_FROM_NAME,
  81. },
  82. mailgun: {
  83. email: SYSTEM_SETTING_KEYS.MAILGUN_FROM_EMAIL,
  84. name: SYSTEM_SETTING_KEYS.MAILGUN_FROM_NAME,
  85. },
  86. sendgrid: {
  87. email: SYSTEM_SETTING_KEYS.SENDGRID_FROM_EMAIL,
  88. name: SYSTEM_SETTING_KEYS.SENDGRID_FROM_NAME,
  89. },
  90. ses: {
  91. email: SYSTEM_SETTING_KEYS.SES_FROM_EMAIL,
  92. name: SYSTEM_SETTING_KEYS.SES_FROM_NAME,
  93. },
  94. }
  95. const keys = keyMap[provider]
  96. const rows = await db.systemSetting.findMany({
  97. where: { key: { in: [keys.email, keys.name] } },
  98. })
  99. const map = new Map(rows.map((r) => [r.key, r.value]))
  100. const fromEmail = map.get(keys.email) || process.env.SMTP_FROM_EMAIL || 'noreply@example.com'
  101. const fromName = map.get(keys.name) || 'Torqvoice'
  102. return `${fromName} <${fromEmail}>`
  103. }
  104. // ─── SMTP ──────────────────────────────────────────────────────────────────
  105. interface SmtpConfig {
  106. host: string
  107. port: number
  108. user?: string
  109. pass?: string
  110. secure: boolean
  111. rejectUnauthorized: boolean
  112. requireTls: boolean
  113. }
  114. function buildSmtpConfig(
  115. settings: SettingsMap,
  116. keys: {
  117. host: string
  118. port: string
  119. user: string
  120. pass: string
  121. secure: string
  122. rejectUnauthorized: string
  123. requireTls: string
  124. },
  125. useEnvFallback: boolean
  126. ): SmtpConfig {
  127. const host = settings.get(keys.host) || (useEnvFallback ? process.env.SMTP_HOST : undefined)
  128. const port =
  129. Number(settings.get(keys.port)) || (useEnvFallback ? Number(process.env.SMTP_PORT) : 0) || 587
  130. const user = settings.get(keys.user) || (useEnvFallback ? process.env.SMTP_USER : undefined)
  131. const pass = settings.get(keys.pass) || (useEnvFallback ? process.env.SMTP_PASS : undefined)
  132. const secureSetting = settings.get(keys.secure)
  133. const secure =
  134. secureSetting !== undefined
  135. ? secureSetting === 'true'
  136. : useEnvFallback
  137. ? process.env.SMTP_SECURE === 'true'
  138. : false
  139. const rejectSetting = settings.get(keys.rejectUnauthorized)
  140. const rejectUnauthorized =
  141. rejectSetting !== undefined
  142. ? rejectSetting !== 'false'
  143. : useEnvFallback
  144. ? process.env.SMTP_REJECT_UNAUTHORIZED !== 'false'
  145. : true
  146. const requireTlsSetting = settings.get(keys.requireTls)
  147. const requireTls = requireTlsSetting === 'true'
  148. if (!host) {
  149. throw new Error('SMTP is not configured. Configure SMTP in your email settings.')
  150. }
  151. return { host, port, user, pass, secure, rejectUnauthorized, requireTls }
  152. }
  153. function createSmtpTransporter(config: SmtpConfig) {
  154. return nodemailer.createTransport({
  155. host: config.host,
  156. port: config.port,
  157. secure: config.secure,
  158. auth: config.user
  159. ? {
  160. user: config.user,
  161. pass: config.pass,
  162. }
  163. : undefined,
  164. tls: {
  165. rejectUnauthorized: config.rejectUnauthorized,
  166. },
  167. requireTLS: config.requireTls,
  168. })
  169. }
  170. async function sendViaSmtpWithSettings(
  171. options: SendMailOptions,
  172. settings: SettingsMap,
  173. keys: {
  174. host: string
  175. port: string
  176. user: string
  177. pass: string
  178. secure: string
  179. rejectUnauthorized: string
  180. requireTls: string
  181. },
  182. useEnvFallback: boolean
  183. ) {
  184. const config = buildSmtpConfig(settings, keys, useEnvFallback)
  185. const transporter = createSmtpTransporter(config)
  186. await transporter.sendMail({
  187. from: options.from,
  188. to: options.to,
  189. replyTo: options.replyTo,
  190. subject: options.subject,
  191. html: options.html,
  192. text: options.text,
  193. attachments: options.attachments?.map((a) => ({
  194. filename: a.filename,
  195. content: a.content,
  196. })),
  197. })
  198. }
  199. async function sendViaSmtp(options: SendMailOptions) {
  200. const keys = [
  201. SYSTEM_SETTING_KEYS.SMTP_HOST,
  202. SYSTEM_SETTING_KEYS.SMTP_PORT,
  203. SYSTEM_SETTING_KEYS.SMTP_USER,
  204. SYSTEM_SETTING_KEYS.SMTP_PASS,
  205. SYSTEM_SETTING_KEYS.SMTP_SECURE,
  206. SYSTEM_SETTING_KEYS.SMTP_REJECT_UNAUTHORIZED,
  207. SYSTEM_SETTING_KEYS.SMTP_REQUIRE_TLS,
  208. ]
  209. const settings = await getSystemSettings(keys)
  210. await sendViaSmtpWithSettings(
  211. options,
  212. settings,
  213. {
  214. host: SYSTEM_SETTING_KEYS.SMTP_HOST,
  215. port: SYSTEM_SETTING_KEYS.SMTP_PORT,
  216. user: SYSTEM_SETTING_KEYS.SMTP_USER,
  217. pass: SYSTEM_SETTING_KEYS.SMTP_PASS,
  218. secure: SYSTEM_SETTING_KEYS.SMTP_SECURE,
  219. rejectUnauthorized: SYSTEM_SETTING_KEYS.SMTP_REJECT_UNAUTHORIZED,
  220. requireTls: SYSTEM_SETTING_KEYS.SMTP_REQUIRE_TLS,
  221. },
  222. true
  223. )
  224. }
  225. // ─── Resend ────────────────────────────────────────────────────────────────
  226. async function sendViaResendWithSettings(
  227. options: SendMailOptions,
  228. settings: SettingsMap,
  229. apiKeyField: string
  230. ) {
  231. const apiKey = settings.get(apiKeyField)
  232. if (!apiKey) {
  233. throw new Error('Resend is not configured. Add your Resend API key.')
  234. }
  235. const resend = new Resend(apiKey)
  236. await resend.emails.send({
  237. from: options.from,
  238. to: options.to,
  239. replyTo: options.replyTo,
  240. subject: options.subject,
  241. html: options.html,
  242. text: options.text,
  243. attachments: options.attachments?.map((a) => ({
  244. filename: a.filename,
  245. content: a.content,
  246. })),
  247. })
  248. }
  249. async function sendViaResend(options: SendMailOptions) {
  250. const settings = await getSystemSettings([SYSTEM_SETTING_KEYS.RESEND_API_KEY])
  251. await sendViaResendWithSettings(options, settings, SYSTEM_SETTING_KEYS.RESEND_API_KEY)
  252. }
  253. // ─── Postmark ──────────────────────────────────────────────────────────────
  254. async function sendViaPostmarkWithSettings(
  255. options: SendMailOptions,
  256. settings: SettingsMap,
  257. apiKeyField: string
  258. ) {
  259. const apiKey = settings.get(apiKeyField)
  260. if (!apiKey) {
  261. throw new Error('Postmark is not configured. Add your Server Token.')
  262. }
  263. const client = new PostmarkClient(apiKey)
  264. if (options.attachments?.length) {
  265. await client.sendEmail({
  266. From: options.from,
  267. To: options.to,
  268. ReplyTo: options.replyTo,
  269. Subject: options.subject,
  270. HtmlBody: options.html,
  271. TextBody: options.text,
  272. Attachments: options.attachments.map((a) => ({
  273. Name: a.filename,
  274. Content: a.content.toString('base64'),
  275. ContentType: 'application/octet-stream',
  276. ContentID: '',
  277. })),
  278. })
  279. } else {
  280. await client.sendEmail({
  281. From: options.from,
  282. To: options.to,
  283. ReplyTo: options.replyTo,
  284. Subject: options.subject,
  285. HtmlBody: options.html,
  286. TextBody: options.text,
  287. })
  288. }
  289. }
  290. async function sendViaPostmark(options: SendMailOptions) {
  291. const settings = await getSystemSettings([SYSTEM_SETTING_KEYS.POSTMARK_API_KEY])
  292. await sendViaPostmarkWithSettings(options, settings, SYSTEM_SETTING_KEYS.POSTMARK_API_KEY)
  293. }
  294. // ─── Mailgun ───────────────────────────────────────────────────────────────
  295. async function sendViaMailgunWithSettings(
  296. options: SendMailOptions,
  297. settings: SettingsMap,
  298. keys: { apiKey: string; domain: string; region: string }
  299. ) {
  300. const apiKey = settings.get(keys.apiKey)
  301. const domain = settings.get(keys.domain)
  302. const region = settings.get(keys.region) || 'us'
  303. if (!apiKey || !domain) {
  304. throw new Error('Mailgun is not configured. Add your API key and domain.')
  305. }
  306. const mailgun = new Mailgun(FormData)
  307. const mg = mailgun.client({
  308. username: 'api',
  309. key: apiKey,
  310. url: region === 'eu' ? 'https://api.eu.mailgun.net' : undefined,
  311. })
  312. await mg.messages.create(domain, {
  313. from: options.from,
  314. to: [options.to],
  315. ...(options.replyTo && { 'h:Reply-To': options.replyTo }),
  316. subject: options.subject,
  317. html: options.html,
  318. ...(options.text && { text: options.text }),
  319. ...(options.attachments?.length && {
  320. attachment: options.attachments.map((a) => ({
  321. filename: a.filename,
  322. data: a.content,
  323. })),
  324. }),
  325. })
  326. }
  327. async function sendViaMailgun(options: SendMailOptions) {
  328. const keys = [
  329. SYSTEM_SETTING_KEYS.MAILGUN_API_KEY,
  330. SYSTEM_SETTING_KEYS.MAILGUN_DOMAIN,
  331. SYSTEM_SETTING_KEYS.MAILGUN_REGION,
  332. ]
  333. const settings = await getSystemSettings(keys)
  334. await sendViaMailgunWithSettings(options, settings, {
  335. apiKey: SYSTEM_SETTING_KEYS.MAILGUN_API_KEY,
  336. domain: SYSTEM_SETTING_KEYS.MAILGUN_DOMAIN,
  337. region: SYSTEM_SETTING_KEYS.MAILGUN_REGION,
  338. })
  339. }
  340. // ─── SendGrid ──────────────────────────────────────────────────────────────
  341. async function sendViaSendGridWithSettings(
  342. options: SendMailOptions,
  343. settings: SettingsMap,
  344. apiKeyField: string
  345. ) {
  346. const apiKey = settings.get(apiKeyField)
  347. if (!apiKey) {
  348. throw new Error('SendGrid is not configured. Add your API key.')
  349. }
  350. sgMail.setApiKey(apiKey)
  351. const msg: MailDataRequired = {
  352. from: options.from,
  353. to: options.to,
  354. replyTo: options.replyTo,
  355. subject: options.subject,
  356. html: options.html,
  357. ...(options.text && { text: options.text }),
  358. }
  359. if (options.attachments?.length) {
  360. msg.attachments = options.attachments.map((a) => ({
  361. filename: a.filename,
  362. content: a.content.toString('base64'),
  363. type: 'application/octet-stream',
  364. disposition: 'attachment' as const,
  365. }))
  366. }
  367. await sgMail.send(msg)
  368. }
  369. async function sendViaSendGrid(options: SendMailOptions) {
  370. const settings = await getSystemSettings([SYSTEM_SETTING_KEYS.SENDGRID_API_KEY])
  371. await sendViaSendGridWithSettings(options, settings, SYSTEM_SETTING_KEYS.SENDGRID_API_KEY)
  372. }
  373. // ─── Amazon SES ────────────────────────────────────────────────────────────
  374. async function sendViaSesWithSettings(
  375. options: SendMailOptions,
  376. settings: SettingsMap,
  377. keys: { accessKeyId: string; secretAccessKey: string; region: string }
  378. ) {
  379. const accessKeyId = settings.get(keys.accessKeyId)
  380. const secretAccessKey = settings.get(keys.secretAccessKey)
  381. const region = settings.get(keys.region) || 'us-east-1'
  382. if (!accessKeyId || !secretAccessKey) {
  383. throw new Error('Amazon SES is not configured. Add your credentials.')
  384. }
  385. const transporter = nodemailer.createTransport({ streamTransport: true })
  386. const info = await transporter.sendMail({
  387. from: options.from,
  388. to: options.to,
  389. replyTo: options.replyTo,
  390. subject: options.subject,
  391. html: options.html,
  392. text: options.text,
  393. attachments: options.attachments?.map((a) => ({
  394. filename: a.filename,
  395. content: a.content,
  396. })),
  397. })
  398. const rawMessage = Buffer.isBuffer(info.message)
  399. ? info.message
  400. : await streamToBuffer(info.message)
  401. const client = new SESClient({
  402. region,
  403. credentials: { accessKeyId, secretAccessKey },
  404. })
  405. await client.send(new SendRawEmailCommand({ RawMessage: { Data: rawMessage } }))
  406. }
  407. async function sendViaSes(options: SendMailOptions) {
  408. const keys = [
  409. SYSTEM_SETTING_KEYS.SES_ACCESS_KEY_ID,
  410. SYSTEM_SETTING_KEYS.SES_SECRET_ACCESS_KEY,
  411. SYSTEM_SETTING_KEYS.SES_REGION,
  412. ]
  413. const settings = await getSystemSettings(keys)
  414. await sendViaSesWithSettings(options, settings, {
  415. accessKeyId: SYSTEM_SETTING_KEYS.SES_ACCESS_KEY_ID,
  416. secretAccessKey: SYSTEM_SETTING_KEYS.SES_SECRET_ACCESS_KEY,
  417. region: SYSTEM_SETTING_KEYS.SES_REGION,
  418. })
  419. }
  420. function streamToBuffer(stream: NodeJS.ReadableStream): Promise<Uint8Array> {
  421. return new Promise((resolve, reject) => {
  422. const chunks: Buffer[] = []
  423. stream.on('data', (chunk: Buffer) => chunks.push(chunk))
  424. stream.on('end', () => resolve(Buffer.concat(chunks)))
  425. stream.on('error', reject)
  426. })
  427. }
  428. // ─── Provider dispatch (shared by both system and org) ──────────────────────
  429. function sendWithProvider(
  430. provider: EmailProvider,
  431. options: SendMailOptions,
  432. settings: SettingsMap,
  433. keySet: 'system' | 'org'
  434. ) {
  435. if (keySet === 'org') {
  436. switch (provider) {
  437. case 'smtp':
  438. return sendViaSmtpWithSettings(
  439. options,
  440. settings,
  441. {
  442. host: ORG_EMAIL_KEYS.EMAIL_SMTP_HOST,
  443. port: ORG_EMAIL_KEYS.EMAIL_SMTP_PORT,
  444. user: ORG_EMAIL_KEYS.EMAIL_SMTP_USER,
  445. pass: ORG_EMAIL_KEYS.EMAIL_SMTP_PASS,
  446. secure: ORG_EMAIL_KEYS.EMAIL_SMTP_SECURE,
  447. rejectUnauthorized: ORG_EMAIL_KEYS.EMAIL_SMTP_REJECT_UNAUTHORIZED,
  448. requireTls: ORG_EMAIL_KEYS.EMAIL_SMTP_REQUIRE_TLS,
  449. },
  450. false
  451. )
  452. case 'resend':
  453. return sendViaResendWithSettings(options, settings, ORG_EMAIL_KEYS.EMAIL_RESEND_API_KEY)
  454. case 'postmark':
  455. return sendViaPostmarkWithSettings(options, settings, ORG_EMAIL_KEYS.EMAIL_POSTMARK_API_KEY)
  456. case 'mailgun':
  457. return sendViaMailgunWithSettings(options, settings, {
  458. apiKey: ORG_EMAIL_KEYS.EMAIL_MAILGUN_API_KEY,
  459. domain: ORG_EMAIL_KEYS.EMAIL_MAILGUN_DOMAIN,
  460. region: ORG_EMAIL_KEYS.EMAIL_MAILGUN_REGION,
  461. })
  462. case 'sendgrid':
  463. return sendViaSendGridWithSettings(options, settings, ORG_EMAIL_KEYS.EMAIL_SENDGRID_API_KEY)
  464. case 'ses':
  465. return sendViaSesWithSettings(options, settings, {
  466. accessKeyId: ORG_EMAIL_KEYS.EMAIL_SES_ACCESS_KEY_ID,
  467. secretAccessKey: ORG_EMAIL_KEYS.EMAIL_SES_SECRET_ACCESS_KEY,
  468. region: ORG_EMAIL_KEYS.EMAIL_SES_REGION,
  469. })
  470. }
  471. }
  472. // system keySet
  473. switch (provider) {
  474. case 'resend':
  475. return sendViaResend(options)
  476. case 'postmark':
  477. return sendViaPostmark(options)
  478. case 'mailgun':
  479. return sendViaMailgun(options)
  480. case 'sendgrid':
  481. return sendViaSendGrid(options)
  482. case 'ses':
  483. return sendViaSes(options)
  484. default:
  485. return sendViaSmtp(options)
  486. }
  487. }
  488. // ─── System-level router (unchanged behavior) ──────────────────────────────
  489. export async function sendMail(options: SendMailOptions) {
  490. const provider = await getEmailProvider()
  491. await sendWithProvider(provider, options, new Map(), 'system')
  492. }
  493. // ─── Org-level email ────────────────────────────────────────────────────────
  494. function isEmailProvider(value: string | null | undefined): value is EmailProvider {
  495. return (
  496. value === 'smtp' ||
  497. value === 'resend' ||
  498. value === 'postmark' ||
  499. value === 'mailgun' ||
  500. value === 'sendgrid' ||
  501. value === 'ses'
  502. )
  503. }
  504. /**
  505. * The workshop's own mail vendor, from the integration it connected. An
  506. * organization that never set one up falls through to the platform's mail
  507. * settings, which is what keeps email working everywhere by default.
  508. */
  509. async function getOrgEmailProvider(organizationId: string): Promise<EmailProvider | null> {
  510. const value = await channelProvider(organizationId, 'email')
  511. return isEmailProvider(value) ? value : null
  512. }
  513. export async function getOrgFromAddress(organizationId: string): Promise<string> {
  514. const provider = await getOrgEmailProvider(organizationId)
  515. if (!provider) {
  516. return getFromAddress()
  517. }
  518. const settings = await channelSettings(organizationId, 'email')
  519. return orgFromAddressFor(provider, settings)
  520. }
  521. /**
  522. * Send through one specific email connection rather than whichever the
  523. * organization is pointed at. The connection page's "send a test email" runs
  524. * through here, so the vendor being looked at is the vendor being tested,
  525. * even while a second one is still connected.
  526. */
  527. export async function sendMailThroughConnection(
  528. connectorId: string,
  529. credentials: Record<string, string>,
  530. settings: Record<string, unknown>,
  531. options: Omit<SendMailOptions, 'from'>
  532. ): Promise<{ from: string }> {
  533. const provider = messagingProvider(connectorId)
  534. if (!provider || !isEmailProvider(provider.legacyProvider)) {
  535. throw new Error(`${connectorId} is not an email integration`)
  536. }
  537. const map = asLegacyMap({
  538. connectionId: '',
  539. connectorId,
  540. provider,
  541. credentials,
  542. settings,
  543. })
  544. const from = orgFromAddressFor(provider.legacyProvider, map)
  545. await sendWithProvider(provider.legacyProvider, { ...options, from }, map, 'org')
  546. return { from }
  547. }
  548. function orgFromAddressFor(provider: EmailProvider, settings: SettingsMap): string {
  549. const keyMap: Record<EmailProvider, { email: string; name: string }> = {
  550. smtp: {
  551. email: ORG_EMAIL_KEYS.EMAIL_SMTP_FROM_EMAIL,
  552. name: ORG_EMAIL_KEYS.EMAIL_SMTP_FROM_NAME,
  553. },
  554. resend: {
  555. email: ORG_EMAIL_KEYS.EMAIL_RESEND_FROM_EMAIL,
  556. name: ORG_EMAIL_KEYS.EMAIL_RESEND_FROM_NAME,
  557. },
  558. postmark: {
  559. email: ORG_EMAIL_KEYS.EMAIL_POSTMARK_FROM_EMAIL,
  560. name: ORG_EMAIL_KEYS.EMAIL_POSTMARK_FROM_NAME,
  561. },
  562. mailgun: {
  563. email: ORG_EMAIL_KEYS.EMAIL_MAILGUN_FROM_EMAIL,
  564. name: ORG_EMAIL_KEYS.EMAIL_MAILGUN_FROM_NAME,
  565. },
  566. sendgrid: {
  567. email: ORG_EMAIL_KEYS.EMAIL_SENDGRID_FROM_EMAIL,
  568. name: ORG_EMAIL_KEYS.EMAIL_SENDGRID_FROM_NAME,
  569. },
  570. ses: {
  571. email: ORG_EMAIL_KEYS.EMAIL_SES_FROM_EMAIL,
  572. name: ORG_EMAIL_KEYS.EMAIL_SES_FROM_NAME,
  573. },
  574. }
  575. const keys = keyMap[provider]
  576. const fromEmail = settings.get(keys.email) || 'noreply@example.com'
  577. const fromName = settings.get(keys.name) || 'Torqvoice'
  578. return `${fromName} <${fromEmail}>`
  579. }
  580. export async function sendOrgMail(organizationId: string, options: SendMailOptions) {
  581. const settings = await channelSettings(organizationId, 'email')
  582. const provider = settings.get(ORG_EMAIL_KEYS.EMAIL_PROVIDER)
  583. if (!isEmailProvider(provider)) {
  584. // A workshop that named a vendor but never finished its setup used to
  585. // get that vendor's error. It still does, rather than a quiet send from
  586. // the platform's account that its customers would not recognise.
  587. const named = await legacyProviderNamed(organizationId, 'email')
  588. if (named) {
  589. throw new Error(`Email provider ${named} is not fully configured. Check its integration.`)
  590. }
  591. // Fall back to global platform email
  592. await sendMail(options)
  593. return
  594. }
  595. await sendWithProvider(provider, options, settings, 'org')
  596. }