diff --git a/apps/landing/playwright.config.ts b/apps/landing/playwright.config.ts deleted file mode 100644 index 0a35429a1..000000000 --- a/apps/landing/playwright.config.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { devices, PlaywrightTestConfig } from "@playwright/test"; - -const ci = process.env.CI === "true"; - -// Use process.env.PORT by default and fallback to port 3000 -const PORT = process.env.PORT || 3000; - -// Set webServer.url and use.baseURL with the location of the WebServer respecting the correct set port -const baseURL = `http://localhost:${PORT}`; - -// Reference: https://playwright.dev/docs/test-configuration -const config: PlaywrightTestConfig = { - // Artifacts folder where screenshots, videos, and traces are stored. - outputDir: "test-results/", - projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }], - use: { - viewport: { width: 1280, height: 720 }, - baseURL, - permissions: ["clipboard-read"], - trace: "retain-on-failure", - }, - webServer: { - command: `NODE_ENV=test yarn dev --port ${PORT}`, - url: baseURL, - timeout: 120 * 1000, - reuseExistingServer: !ci, - }, - reporter: [ - [ci ? "github" : "list"], - ["html", { open: !ci ? "on-failure" : "never" }], - ], - workers: 1, -}; -export default config; diff --git a/packages/backend/trpc/routers/auth.ts b/packages/backend/trpc/routers/auth.ts index d563330a3..d2ccd2533 100644 --- a/packages/backend/trpc/routers/auth.ts +++ b/packages/backend/trpc/routers/auth.ts @@ -1,10 +1,10 @@ import { prisma } from "@rallly/database"; -import { sendEmail } from "@rallly/emails"; import { absoluteUrl } from "@rallly/utils"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; import { createToken, decryptToken } from "../../session"; +import { emailClient } from "../../utils/email-client"; import { generateOtp } from "../../utils/nanoid"; import { publicProcedure, router } from "../trpc"; import { LoginTokenPayload, RegistrationTokenPayload } from "../types"; @@ -107,7 +107,7 @@ export const auth = router({ code, }); - await sendEmail("RegisterEmail", { + await emailClient.sendTemplate("RegisterEmail", { to: input.email, subject: `${input.name}, please verify your email address`, props: { @@ -193,7 +193,7 @@ export const auth = router({ code, }); - await sendEmail("LoginEmail", { + await emailClient.sendTemplate("LoginEmail", { to: input.email, subject: `${code} is your 6-digit code`, props: { diff --git a/packages/backend/trpc/routers/polls.ts b/packages/backend/trpc/routers/polls.ts index c25ef46c1..2cd18e4b2 100644 --- a/packages/backend/trpc/routers/polls.ts +++ b/packages/backend/trpc/routers/polls.ts @@ -1,5 +1,4 @@ import { prisma } from "@rallly/database"; -import { sendEmail } from "@rallly/emails"; import { absoluteUrl, shortUrl } from "@rallly/utils"; import { TRPCError } from "@trpc/server"; import dayjs from "dayjs"; @@ -10,6 +9,7 @@ import * as ics from "ics"; import { z } from "zod"; import { getTimeZoneAbbreviation } from "../../utils/date"; +import { emailClient } from "../../utils/email-client"; import { nanoid } from "../../utils/nanoid"; import { possiblyPublicProcedure, @@ -133,7 +133,7 @@ export const polls = router({ }); if (user) { - await sendEmail("NewPollEmail", { + await emailClient.sendTemplate("NewPollEmail", { to: user.email, subject: `Let's find a date for ${poll.title}`, props: { @@ -678,7 +678,7 @@ export const polls = router({ }); } - const emailToHost = sendEmail("FinalizeHostEmail", { + const emailToHost = emailClient.sendTemplate("FinalizeHostEmail", { subject: `Date booked for ${poll.title}`, to: poll.user.email, props: { @@ -702,7 +702,7 @@ export const polls = router({ }); const emailsToParticipants = participantsToEmail.map((p) => { - return sendEmail("FinalizeParticipantEmail", { + return emailClient.sendTemplate("FinalizeParticipantEmail", { subject: `Date booked for ${poll.title}`, to: p.email, props: { diff --git a/packages/backend/trpc/routers/polls/comments.ts b/packages/backend/trpc/routers/polls/comments.ts index b54d69705..d876dd1ef 100644 --- a/packages/backend/trpc/routers/polls/comments.ts +++ b/packages/backend/trpc/routers/polls/comments.ts @@ -1,9 +1,9 @@ import { prisma } from "@rallly/database"; -import { sendEmail } from "@rallly/emails"; import { absoluteUrl } from "@rallly/utils"; import { z } from "zod"; import { createToken } from "../../../session"; +import { emailClient } from "../../../utils/email-client"; import { publicProcedure, router } from "../../trpc"; import { DisableNotificationsPayload } from "../../types"; @@ -82,7 +82,7 @@ export const comments = router({ { ttl: 0 }, ); emailsToSend.push( - sendEmail("NewCommentEmail", { + emailClient.sendTemplate("NewCommentEmail", { to: email, subject: `${authorName} has commented on ${poll.title}`, props: { diff --git a/packages/backend/trpc/routers/polls/participants.ts b/packages/backend/trpc/routers/polls/participants.ts index ae74f476d..d2fa318d8 100644 --- a/packages/backend/trpc/routers/polls/participants.ts +++ b/packages/backend/trpc/routers/polls/participants.ts @@ -1,10 +1,10 @@ import { prisma } from "@rallly/database"; -import { sendEmail } from "@rallly/emails"; import { absoluteUrl } from "@rallly/utils"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; import { createToken } from "../../../session"; +import { emailClient } from "../../../utils/email-client"; import { publicProcedure, router } from "../../trpc"; import { DisableNotificationsPayload } from "../../types"; @@ -107,7 +107,7 @@ export const participants = router({ ); emailsToSend.push( - sendEmail("NewParticipantConfirmationEmail", { + emailClient.sendTemplate("NewParticipantConfirmationEmail", { to: email, subject: `Thanks for responding to ${poll.title}`, props: { @@ -144,7 +144,7 @@ export const participants = router({ { ttl: 0 }, ); emailsToSend.push( - sendEmail("NewParticipantEmail", { + emailClient.sendTemplate("NewParticipantEmail", { to: email, subject: `${participant.name} has responded to ${poll.title}`, props: { diff --git a/packages/backend/utils/email-client.ts b/packages/backend/utils/email-client.ts new file mode 100644 index 000000000..4cffbf1ae --- /dev/null +++ b/packages/backend/utils/email-client.ts @@ -0,0 +1,19 @@ +import { EmailClient, SupportedEmailProviders } from "@rallly/emails"; + +const env = process.env["NODE" + "_ENV"]; + +export const emailClient = new EmailClient({ + openPreviews: env === "developement", + useTestServer: env === "test", + provider: { + name: process.env.EMAIL_PROVIDER as SupportedEmailProviders, + }, + mail: { + from: { + name: "Rallly", + address: + (process.env.NOREPLY_EMAIL as string) || + (process.env.SUPPORT_EMAIL as string), + }, + }, +}); diff --git a/packages/emails/package.json b/packages/emails/package.json index d9e46c677..1da271767 100644 --- a/packages/emails/package.json +++ b/packages/emails/package.json @@ -7,8 +7,8 @@ "lint": "eslint ./src", "lint:tsc": "tsc --noEmit" }, - "main": "./src/index.tsx", - "types": "./src/index.tsx", + "main": "./src/index.ts", + "types": "./src/index.ts", "dependencies": { "@aws-sdk/client-ses": "^3.292.0", "@aws-sdk/credential-provider-node": "^3.292.0", @@ -16,12 +16,14 @@ "@react-email/render": "0.0.6", "clsx": "^1.2.1", "nodemailer": "^6.9.1", + "preview-email": "^3.0.19", "react-email": "^1.9.1" }, "devDependencies": { "@rallly/tailwind-config": "*", "@rallly/tsconfig": "*", "@rallly/utils": "*", - "@types/nodemailer": "^6.4.7" + "@types/nodemailer": "^6.4.7", + "@types/preview-email": "^3.0.1" } } diff --git a/packages/emails/src/index.tsx b/packages/emails/src/index.ts similarity index 100% rename from packages/emails/src/index.tsx rename to packages/emails/src/index.ts diff --git a/packages/emails/src/send-email.tsx b/packages/emails/src/send-email.tsx index 7cffbc423..60d9aad1f 100644 --- a/packages/emails/src/send-email.tsx +++ b/packages/emails/src/send-email.tsx @@ -3,6 +3,7 @@ import { defaultProvider } from "@aws-sdk/credential-provider-node"; import { render } from "@react-email/render"; import { createTransport, Transporter } from "nodemailer"; import type Mail from "nodemailer/lib/mailer"; +import previewEmail from "preview-email"; import React from "react"; import * as templates from "./templates"; @@ -17,66 +18,6 @@ type TemplateProps = React.ComponentProps< type TemplateComponent = Templates[T]; -const env = process.env["NODE" + "_ENV"] || "development"; - -let transport: Transporter; - -const getTransport = () => { - if (transport) { - // Reuse the transport if it exists - return transport; - } - - if (env === "test") { - transport = createTransport({ port: 4025 }); - return transport; - } - - switch (process.env.EMAIL_PROVIDER) { - case "ses": - { - const ses = new aws.SES({ - region: process.env["AWS" + "_REGION"], - credentialDefaultProvider: defaultProvider, - }); - - transport = createTransport({ - SES: { - ses, - aws, - sendingRate: 10, - }, - }); - } - break; - default: { - const hasAuth = process.env.SMTP_USER || process.env.SMTP_PWD; - transport = createTransport({ - host: process.env.SMTP_HOST, - port: process.env.SMTP_PORT - ? parseInt(process.env.SMTP_PORT) - : undefined, - secure: process.env.SMTP_SECURE === "true", - auth: hasAuth - ? { - user: process.env.SMTP_USER, - pass: process.env.SMTP_PWD, - } - : undefined, - tls: - process.env.SMTP_TLS_ENABLED === "true" - ? { - ciphers: "SSLv3", - rejectUnauthorized: false, - } - : undefined, - }); - } - } - - return transport; -}; - type SendEmailOptions = { to: string; subject: string; @@ -84,42 +25,146 @@ type SendEmailOptions = { attachments?: Mail.Options["attachments"]; }; -export const sendEmail = async ( - templateName: T, - options: SendEmailOptions, -) => { - if (!process.env.SUPPORT_EMAIL) { - console.info("SUPPORT_EMAIL not configured - skipping email send"); - return; - } +type EmailProviderConfig = + | { + name: "ses"; + // config defined through env vars + } + | { + name: "smtp"; + // config defined through env vars + }; - const Template = templates[templateName] as TemplateComponent; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const html = render(