From fc329464ec1f0a06356a06d89b29fe6e660b4298 Mon Sep 17 00:00:00 2001 From: Ephraim Atta-Duncan Date: Sun, 7 Apr 2024 20:10:33 +0000 Subject: [PATCH] feat: add queue for sending emails --- packages/email/mailer.ts | 2 + .../server-only/document/delete-document.ts | 26 ++++--- .../server-only/document/resend-document.tsx | 74 +++++++++---------- .../document/super-delete-document.ts | 26 ++++--- packages/lib/server-only/queue/job.ts | 26 ++++++- .../team/create-team-email-verification.ts | 22 +++--- .../team/create-team-member-invites.ts | 22 +++--- .../lib/server-only/team/delete-team-email.ts | 28 ++++--- .../team/request-team-ownership-transfer.ts | 22 +++--- .../trpc/server/singleplayer-router/router.ts | 29 ++++---- 10 files changed, 159 insertions(+), 118 deletions(-) diff --git a/packages/email/mailer.ts b/packages/email/mailer.ts index 3956e6a2b..61b70a4e2 100644 --- a/packages/email/mailer.ts +++ b/packages/email/mailer.ts @@ -1,3 +1,4 @@ +import type { SendMailOptions } from 'nodemailer'; import { createTransport } from 'nodemailer'; import { ResendTransport } from '@documenso/nodemailer-resend'; @@ -54,3 +55,4 @@ const getTransport = () => { }; export const mailer = getTransport(); +export type MailOptions = SendMailOptions; diff --git a/packages/lib/server-only/document/delete-document.ts b/packages/lib/server-only/document/delete-document.ts index c3a9254cb..a4291d5a4 100644 --- a/packages/lib/server-only/document/delete-document.ts +++ b/packages/lib/server-only/document/delete-document.ts @@ -2,7 +2,6 @@ import { createElement } from 'react'; -import { mailer } from '@documenso/email/mailer'; import { render } from '@documenso/email/render'; import DocumentCancelTemplate from '@documenso/email/templates/document-cancel'; import { prisma } from '@documenso/prisma'; @@ -92,18 +91,21 @@ export const deleteDocument = async ({ assetBaseUrl, }); - await mailer.sendMail({ - to: { - address: recipient.email, - name: recipient.name, + await queueJob({ + job: 'send-mail', + args: { + to: { + address: recipient.email, + name: recipient.name, + }, + from: { + name: FROM_NAME, + address: FROM_ADDRESS, + }, + subject: 'Document Cancelled', + html: render(template), + text: render(template, { plainText: true }), }, - from: { - name: FROM_NAME, - address: FROM_ADDRESS, - }, - subject: 'Document Cancelled', - html: render(template), - text: render(template, { plainText: true }), }); }), ); diff --git a/packages/lib/server-only/document/resend-document.tsx b/packages/lib/server-only/document/resend-document.tsx index 5b8fe8965..1eb1f8c6a 100644 --- a/packages/lib/server-only/document/resend-document.tsx +++ b/packages/lib/server-only/document/resend-document.tsx @@ -1,6 +1,5 @@ import { createElement } from 'react'; -import { mailer } from '@documenso/email/mailer'; import { render } from '@documenso/email/render'; import { DocumentInviteEmailTemplate } from '@documenso/email/templates/document-invite'; import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email'; @@ -110,45 +109,42 @@ export const resendDocument = async ({ const { actionVerb } = RECIPIENT_ROLES_DESCRIPTION[recipient.role]; - await prisma.$transaction( - async (tx) => { - await mailer.sendMail({ - to: { - address: email, - name, - }, - from: { - name: FROM_NAME, - address: FROM_ADDRESS, - }, - subject: customEmail?.subject - ? renderCustomEmailTemplate(customEmail.subject, customEmailTemplate) - : `Please ${actionVerb.toLowerCase()} this document`, - html: render(template), - text: render(template, { plainText: true }), - }); - - await queueJob({ - job: 'create-document-audit-log', - args: { - type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT, - documentId: document.id, - user, - requestMetadata, - data: { - emailType: recipientEmailType, - recipientEmail: recipient.email, - recipientName: recipient.name, - recipientRole: recipient.role, - recipientId: recipient.id, - isResending: true, - }, - }, - }); + await queueJob({ + job: 'send-mail', + args: { + to: { + address: email, + name, + }, + from: { + name: FROM_NAME, + address: FROM_ADDRESS, + }, + subject: customEmail?.subject + ? renderCustomEmailTemplate(customEmail.subject, customEmailTemplate) + : `Please ${actionVerb.toLowerCase()} this document`, + html: render(template), + text: render(template, { plainText: true }), }, - // Hopefully the queue makes this redundant - { timeout: 30_000 }, - ); + }); + + await queueJob({ + job: 'create-document-audit-log', + args: { + type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT, + documentId: document.id, + user, + requestMetadata, + data: { + emailType: recipientEmailType, + recipientEmail: recipient.email, + recipientName: recipient.name, + recipientRole: recipient.role, + recipientId: recipient.id, + isResending: true, + }, + }, + }); }), ); }; diff --git a/packages/lib/server-only/document/super-delete-document.ts b/packages/lib/server-only/document/super-delete-document.ts index cce9a123c..d3bea2f9b 100644 --- a/packages/lib/server-only/document/super-delete-document.ts +++ b/packages/lib/server-only/document/super-delete-document.ts @@ -2,7 +2,6 @@ import { createElement } from 'react'; -import { mailer } from '@documenso/email/mailer'; import { render } from '@documenso/email/render'; import DocumentCancelTemplate from '@documenso/email/templates/document-cancel'; import { prisma } from '@documenso/prisma'; @@ -49,18 +48,21 @@ export const superDeleteDocument = async ({ id, requestMetadata }: SuperDeleteDo assetBaseUrl, }); - await mailer.sendMail({ - to: { - address: recipient.email, - name: recipient.name, + await queueJob({ + job: 'send-mail', + args: { + to: { + address: recipient.email, + name: recipient.name, + }, + from: { + name: FROM_NAME, + address: FROM_ADDRESS, + }, + subject: 'Document Cancelled', + html: render(template), + text: render(template, { plainText: true }), }, - from: { - name: FROM_NAME, - address: FROM_ADDRESS, - }, - subject: 'Document Cancelled', - html: render(template), - text: render(template, { plainText: true }), }); }), ); diff --git a/packages/lib/server-only/queue/job.ts b/packages/lib/server-only/queue/job.ts index 3910c3e9a..e4e355dc1 100644 --- a/packages/lib/server-only/queue/job.ts +++ b/packages/lib/server-only/queue/job.ts @@ -1,5 +1,7 @@ import type { WorkHandler } from 'pg-boss'; +import type { MailOptions } from '@documenso/email/mailer'; +import { mailer } from '@documenso/email/mailer'; import { prisma } from '@documenso/prisma'; import { initQueue } from '.'; @@ -12,6 +14,7 @@ import { import { type SendPendingEmailOptions, sendPendingEmail } from '../document/send-pending-email'; type JobOptions = { + 'send-mail': MailOptions; 'send-completed-email': SendCompletedDocumentOptions; 'send-pending-email': SendPendingEmailOptions; 'create-document-audit-log': CreateDocumentAuditLogDataOptions; @@ -20,25 +23,42 @@ type JobOptions = { export const jobHandlers: { [K in keyof JobOptions]: WorkHandler; } = { - 'send-completed-email': async ({ data: { documentId, requestMetadata } }) => { + 'send-completed-email': async ({ id, name, data: { documentId, requestMetadata } }) => { + console.log('Running Queue: ', name, ' ', id); + await sendCompletedEmail({ documentId, requestMetadata, }); }, - 'send-pending-email': async ({ data: { documentId, recipientId } }) => { + 'send-pending-email': async ({ id, name, data: { documentId, recipientId } }) => { + console.log('Running Queue: ', name, ' ', id); + await sendPendingEmail({ documentId, recipientId, }); }, + 'send-mail': async ({ id, name, data: { attachments, to, from, subject, html, text } }) => { + console.log('Running Queue: ', name, ' ', id); + + await mailer.sendMail({ + to, + from, + subject, + html, + text, + attachments, + }); + }, // Audit Logs Queue 'create-document-audit-log': async ({ + name, data: { documentId, type, requestMetadata, user, data }, id, }) => { - console.log('Running Queue ID', id); + console.log('Running Queue: ', name, ' ', id); await prisma.documentAuditLog.create({ data: createDocumentAuditLogData({ diff --git a/packages/lib/server-only/team/create-team-email-verification.ts b/packages/lib/server-only/team/create-team-email-verification.ts index 86cded7a9..3ea375a24 100644 --- a/packages/lib/server-only/team/create-team-email-verification.ts +++ b/packages/lib/server-only/team/create-team-email-verification.ts @@ -2,7 +2,6 @@ import { createElement } from 'react'; import { z } from 'zod'; -import { mailer } from '@documenso/email/mailer'; import { render } from '@documenso/email/render'; import { ConfirmTeamEmailTemplate } from '@documenso/email/templates/confirm-team-email'; import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app'; @@ -13,6 +12,8 @@ import { createTokenVerification } from '@documenso/lib/utils/token-verification import { prisma } from '@documenso/prisma'; import { Prisma } from '@documenso/prisma/client'; +import { queueJob } from '../queue/job'; + export type CreateTeamEmailVerificationOptions = { userId: number; teamId: number; @@ -122,14 +123,17 @@ export const sendTeamEmailVerificationEmail = async ( token, }); - await mailer.sendMail({ - to: email, - from: { - name: FROM_NAME, - address: FROM_ADDRESS, + await queueJob({ + job: 'send-mail', + args: { + to: email, + from: { + name: FROM_NAME, + address: FROM_ADDRESS, + }, + subject: `A request to use your email has been initiated by ${teamName} on Documenso`, + html: render(template), + text: render(template, { plainText: true }), }, - subject: `A request to use your email has been initiated by ${teamName} on Documenso`, - html: render(template), - text: render(template, { plainText: true }), }); }; diff --git a/packages/lib/server-only/team/create-team-member-invites.ts b/packages/lib/server-only/team/create-team-member-invites.ts index f167d2112..728edae35 100644 --- a/packages/lib/server-only/team/create-team-member-invites.ts +++ b/packages/lib/server-only/team/create-team-member-invites.ts @@ -2,7 +2,6 @@ import { createElement } from 'react'; import { nanoid } from 'nanoid'; -import { mailer } from '@documenso/email/mailer'; import { render } from '@documenso/email/render'; import type { TeamInviteEmailProps } from '@documenso/email/templates/team-invite'; import { TeamInviteEmailTemplate } from '@documenso/email/templates/team-invite'; @@ -15,6 +14,8 @@ import { prisma } from '@documenso/prisma'; import { TeamMemberInviteStatus } from '@documenso/prisma/client'; import type { TCreateTeamMemberInvitesMutationSchema } from '@documenso/trpc/server/team-router/schema'; +import { queueJob } from '../queue/job'; + export type CreateTeamMemberInvitesOptions = { userId: number; userName: string; @@ -148,14 +149,17 @@ export const sendTeamMemberInviteEmail = async ({ ...emailTemplateOptions, }); - await mailer.sendMail({ - to: email, - from: { - name: FROM_NAME, - address: FROM_ADDRESS, + await queueJob({ + job: 'send-mail', + args: { + to: email, + from: { + name: FROM_NAME, + address: FROM_ADDRESS, + }, + subject: `You have been invited to join ${emailTemplateOptions.teamName} on Documenso`, + html: render(template), + text: render(template, { plainText: true }), }, - subject: `You have been invited to join ${emailTemplateOptions.teamName} on Documenso`, - html: render(template), - text: render(template, { plainText: true }), }); }; diff --git a/packages/lib/server-only/team/delete-team-email.ts b/packages/lib/server-only/team/delete-team-email.ts index c5139a971..bf98ce3c1 100644 --- a/packages/lib/server-only/team/delete-team-email.ts +++ b/packages/lib/server-only/team/delete-team-email.ts @@ -1,6 +1,5 @@ import { createElement } from 'react'; -import { mailer } from '@documenso/email/mailer'; import { render } from '@documenso/email/render'; import { TeamEmailRemovedTemplate } from '@documenso/email/templates/team-email-removed'; import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app'; @@ -8,6 +7,8 @@ import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email'; import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/teams'; import { prisma } from '@documenso/prisma'; +import { queueJob } from '../queue/job'; + export type DeleteTeamEmailOptions = { userId: number; userEmail: string; @@ -73,18 +74,21 @@ export const deleteTeamEmail = async ({ userId, userEmail, teamId }: DeleteTeamE teamUrl: team.url, }); - await mailer.sendMail({ - to: { - address: team.owner.email, - name: team.owner.name ?? '', + await queueJob({ + job: 'create-document-audit-log', + args: { + to: { + address: team.owner.email, + name: team.owner.name ?? '', + }, + from: { + name: FROM_NAME, + address: FROM_ADDRESS, + }, + subject: `Team email has been revoked for ${team.name}`, + html: render(template), + text: render(template, { plainText: true }), }, - from: { - name: FROM_NAME, - address: FROM_ADDRESS, - }, - subject: `Team email has been revoked for ${team.name}`, - html: render(template), - text: render(template, { plainText: true }), }); } catch (e) { // Todo: Teams - Alert us. diff --git a/packages/lib/server-only/team/request-team-ownership-transfer.ts b/packages/lib/server-only/team/request-team-ownership-transfer.ts index 92fd5b61e..c858f1935 100644 --- a/packages/lib/server-only/team/request-team-ownership-transfer.ts +++ b/packages/lib/server-only/team/request-team-ownership-transfer.ts @@ -1,6 +1,5 @@ import { createElement } from 'react'; -import { mailer } from '@documenso/email/mailer'; import { render } from '@documenso/email/render'; import { TeamTransferRequestTemplate } from '@documenso/email/templates/team-transfer-request'; import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app'; @@ -8,6 +7,8 @@ import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email'; import { createTokenVerification } from '@documenso/lib/utils/token-verification'; import { prisma } from '@documenso/prisma'; +import { queueJob } from '../queue/job'; + export type RequestTeamOwnershipTransferOptions = { /** * The ID of the user initiating the transfer. @@ -93,15 +94,18 @@ export const requestTeamOwnershipTransfer = async ({ token, }); - await mailer.sendMail({ - to: newOwnerUser.email, - from: { - name: FROM_NAME, - address: FROM_ADDRESS, + await queueJob({ + job: 'create-document-audit-log', + args: { + to: newOwnerUser.email, + from: { + name: FROM_NAME, + address: FROM_ADDRESS, + }, + subject: `You have been requested to take ownership of team ${team.name} on Documenso`, + html: render(template), + text: render(template, { plainText: true }), }, - subject: `You have been requested to take ownership of team ${team.name} on Documenso`, - html: render(template), - text: render(template, { plainText: true }), }); }, { timeout: 30_000 }, diff --git a/packages/trpc/server/singleplayer-router/router.ts b/packages/trpc/server/singleplayer-router/router.ts index 33b125110..7d2407350 100644 --- a/packages/trpc/server/singleplayer-router/router.ts +++ b/packages/trpc/server/singleplayer-router/router.ts @@ -2,12 +2,12 @@ import { createElement } from 'react'; import { PDFDocument } from 'pdf-lib'; -import { mailer } from '@documenso/email/mailer'; import { renderAsync } from '@documenso/email/render'; import { DocumentSelfSignedEmailTemplate } from '@documenso/email/templates/document-self-signed'; import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; import { FROM_ADDRESS, FROM_NAME, SERVICE_USER_EMAIL } from '@documenso/lib/constants/email'; import { insertFieldInPDF } from '@documenso/lib/server-only/pdf/insert-field-in-pdf'; +import { queueJob } from '@documenso/lib/server-only/queue/job'; import { alphaid } from '@documenso/lib/universal/id'; import { getFile } from '@documenso/lib/universal/upload/get-file'; import { putFile } from '@documenso/lib/universal/upload/put-file'; @@ -160,19 +160,22 @@ export const singleplayerRouter = router({ ]); // Send email to signer. - await mailer.sendMail({ - to: { - address: signer.email, - name: signer.name, + await queueJob({ + job: 'send-mail', + args: { + to: { + address: signer.email, + name: signer.name, + }, + from: { + name: FROM_NAME, + address: FROM_ADDRESS, + }, + subject: 'Document signed', + html, + text, + attachments: [{ content: signedPdfBuffer, filename: documentName }], }, - from: { - name: FROM_NAME, - address: FROM_ADDRESS, - }, - subject: 'Document signed', - html, - text, - attachments: [{ content: signedPdfBuffer, filename: documentName }], }); return token;