feat: add queue for sending emails

This commit is contained in:
Ephraim Atta-Duncan
2024-04-07 20:10:33 +00:00
parent 574098f103
commit fc329464ec
10 changed files with 159 additions and 118 deletions

View File

@ -1,3 +1,4 @@
import type { SendMailOptions } from 'nodemailer';
import { createTransport } from 'nodemailer'; import { createTransport } from 'nodemailer';
import { ResendTransport } from '@documenso/nodemailer-resend'; import { ResendTransport } from '@documenso/nodemailer-resend';
@ -54,3 +55,4 @@ const getTransport = () => {
}; };
export const mailer = getTransport(); export const mailer = getTransport();
export type MailOptions = SendMailOptions;

View File

@ -2,7 +2,6 @@
import { createElement } from 'react'; import { createElement } from 'react';
import { mailer } from '@documenso/email/mailer';
import { render } from '@documenso/email/render'; import { render } from '@documenso/email/render';
import DocumentCancelTemplate from '@documenso/email/templates/document-cancel'; import DocumentCancelTemplate from '@documenso/email/templates/document-cancel';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
@ -92,18 +91,21 @@ export const deleteDocument = async ({
assetBaseUrl, assetBaseUrl,
}); });
await mailer.sendMail({ await queueJob({
to: { job: 'send-mail',
address: recipient.email, args: {
name: recipient.name, 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 }),
}); });
}), }),
); );

View File

@ -1,6 +1,5 @@
import { createElement } from 'react'; import { createElement } from 'react';
import { mailer } from '@documenso/email/mailer';
import { render } from '@documenso/email/render'; import { render } from '@documenso/email/render';
import { DocumentInviteEmailTemplate } from '@documenso/email/templates/document-invite'; import { DocumentInviteEmailTemplate } from '@documenso/email/templates/document-invite';
import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email'; 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]; const { actionVerb } = RECIPIENT_ROLES_DESCRIPTION[recipient.role];
await prisma.$transaction( await queueJob({
async (tx) => { job: 'send-mail',
await mailer.sendMail({ args: {
to: { to: {
address: email, address: email,
name, name,
}, },
from: { from: {
name: FROM_NAME, name: FROM_NAME,
address: FROM_ADDRESS, address: FROM_ADDRESS,
}, },
subject: customEmail?.subject subject: customEmail?.subject
? renderCustomEmailTemplate(customEmail.subject, customEmailTemplate) ? renderCustomEmailTemplate(customEmail.subject, customEmailTemplate)
: `Please ${actionVerb.toLowerCase()} this document`, : `Please ${actionVerb.toLowerCase()} this document`,
html: render(template), html: render(template),
text: render(template, { plainText: true }), 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,
},
},
});
}, },
// 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,
},
},
});
}), }),
); );
}; };

View File

@ -2,7 +2,6 @@
import { createElement } from 'react'; import { createElement } from 'react';
import { mailer } from '@documenso/email/mailer';
import { render } from '@documenso/email/render'; import { render } from '@documenso/email/render';
import DocumentCancelTemplate from '@documenso/email/templates/document-cancel'; import DocumentCancelTemplate from '@documenso/email/templates/document-cancel';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
@ -49,18 +48,21 @@ export const superDeleteDocument = async ({ id, requestMetadata }: SuperDeleteDo
assetBaseUrl, assetBaseUrl,
}); });
await mailer.sendMail({ await queueJob({
to: { job: 'send-mail',
address: recipient.email, args: {
name: recipient.name, 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 }),
}); });
}), }),
); );

View File

@ -1,5 +1,7 @@
import type { WorkHandler } from 'pg-boss'; 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 { prisma } from '@documenso/prisma';
import { initQueue } from '.'; import { initQueue } from '.';
@ -12,6 +14,7 @@ import {
import { type SendPendingEmailOptions, sendPendingEmail } from '../document/send-pending-email'; import { type SendPendingEmailOptions, sendPendingEmail } from '../document/send-pending-email';
type JobOptions = { type JobOptions = {
'send-mail': MailOptions;
'send-completed-email': SendCompletedDocumentOptions; 'send-completed-email': SendCompletedDocumentOptions;
'send-pending-email': SendPendingEmailOptions; 'send-pending-email': SendPendingEmailOptions;
'create-document-audit-log': CreateDocumentAuditLogDataOptions; 'create-document-audit-log': CreateDocumentAuditLogDataOptions;
@ -20,25 +23,42 @@ type JobOptions = {
export const jobHandlers: { export const jobHandlers: {
[K in keyof JobOptions]: WorkHandler<JobOptions[K]>; [K in keyof JobOptions]: WorkHandler<JobOptions[K]>;
} = { } = {
'send-completed-email': async ({ data: { documentId, requestMetadata } }) => { 'send-completed-email': async ({ id, name, data: { documentId, requestMetadata } }) => {
console.log('Running Queue: ', name, ' ', id);
await sendCompletedEmail({ await sendCompletedEmail({
documentId, documentId,
requestMetadata, 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({ await sendPendingEmail({
documentId, documentId,
recipientId, 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 // Audit Logs Queue
'create-document-audit-log': async ({ 'create-document-audit-log': async ({
name,
data: { documentId, type, requestMetadata, user, data }, data: { documentId, type, requestMetadata, user, data },
id, id,
}) => { }) => {
console.log('Running Queue ID', id); console.log('Running Queue: ', name, ' ', id);
await prisma.documentAuditLog.create({ await prisma.documentAuditLog.create({
data: createDocumentAuditLogData({ data: createDocumentAuditLogData({

View File

@ -2,7 +2,6 @@ import { createElement } from 'react';
import { z } from 'zod'; import { z } from 'zod';
import { mailer } from '@documenso/email/mailer';
import { render } from '@documenso/email/render'; import { render } from '@documenso/email/render';
import { ConfirmTeamEmailTemplate } from '@documenso/email/templates/confirm-team-email'; import { ConfirmTeamEmailTemplate } from '@documenso/email/templates/confirm-team-email';
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app'; 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';
import { Prisma } from '@documenso/prisma/client'; import { Prisma } from '@documenso/prisma/client';
import { queueJob } from '../queue/job';
export type CreateTeamEmailVerificationOptions = { export type CreateTeamEmailVerificationOptions = {
userId: number; userId: number;
teamId: number; teamId: number;
@ -122,14 +123,17 @@ export const sendTeamEmailVerificationEmail = async (
token, token,
}); });
await mailer.sendMail({ await queueJob({
to: email, job: 'send-mail',
from: { args: {
name: FROM_NAME, to: email,
address: FROM_ADDRESS, 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 }),
}); });
}; };

View File

@ -2,7 +2,6 @@ import { createElement } from 'react';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { mailer } from '@documenso/email/mailer';
import { render } from '@documenso/email/render'; import { render } from '@documenso/email/render';
import type { TeamInviteEmailProps } from '@documenso/email/templates/team-invite'; import type { TeamInviteEmailProps } from '@documenso/email/templates/team-invite';
import { TeamInviteEmailTemplate } 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 { TeamMemberInviteStatus } from '@documenso/prisma/client';
import type { TCreateTeamMemberInvitesMutationSchema } from '@documenso/trpc/server/team-router/schema'; import type { TCreateTeamMemberInvitesMutationSchema } from '@documenso/trpc/server/team-router/schema';
import { queueJob } from '../queue/job';
export type CreateTeamMemberInvitesOptions = { export type CreateTeamMemberInvitesOptions = {
userId: number; userId: number;
userName: string; userName: string;
@ -148,14 +149,17 @@ export const sendTeamMemberInviteEmail = async ({
...emailTemplateOptions, ...emailTemplateOptions,
}); });
await mailer.sendMail({ await queueJob({
to: email, job: 'send-mail',
from: { args: {
name: FROM_NAME, to: email,
address: FROM_ADDRESS, 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 }),
}); });
}; };

View File

@ -1,6 +1,5 @@
import { createElement } from 'react'; import { createElement } from 'react';
import { mailer } from '@documenso/email/mailer';
import { render } from '@documenso/email/render'; import { render } from '@documenso/email/render';
import { TeamEmailRemovedTemplate } from '@documenso/email/templates/team-email-removed'; import { TeamEmailRemovedTemplate } from '@documenso/email/templates/team-email-removed';
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app'; 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 { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/teams';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import { queueJob } from '../queue/job';
export type DeleteTeamEmailOptions = { export type DeleteTeamEmailOptions = {
userId: number; userId: number;
userEmail: string; userEmail: string;
@ -73,18 +74,21 @@ export const deleteTeamEmail = async ({ userId, userEmail, teamId }: DeleteTeamE
teamUrl: team.url, teamUrl: team.url,
}); });
await mailer.sendMail({ await queueJob({
to: { job: 'create-document-audit-log',
address: team.owner.email, args: {
name: team.owner.name ?? '', 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) { } catch (e) {
// Todo: Teams - Alert us. // Todo: Teams - Alert us.

View File

@ -1,6 +1,5 @@
import { createElement } from 'react'; import { createElement } from 'react';
import { mailer } from '@documenso/email/mailer';
import { render } from '@documenso/email/render'; import { render } from '@documenso/email/render';
import { TeamTransferRequestTemplate } from '@documenso/email/templates/team-transfer-request'; import { TeamTransferRequestTemplate } from '@documenso/email/templates/team-transfer-request';
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app'; 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 { createTokenVerification } from '@documenso/lib/utils/token-verification';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import { queueJob } from '../queue/job';
export type RequestTeamOwnershipTransferOptions = { export type RequestTeamOwnershipTransferOptions = {
/** /**
* The ID of the user initiating the transfer. * The ID of the user initiating the transfer.
@ -93,15 +94,18 @@ export const requestTeamOwnershipTransfer = async ({
token, token,
}); });
await mailer.sendMail({ await queueJob({
to: newOwnerUser.email, job: 'create-document-audit-log',
from: { args: {
name: FROM_NAME, to: newOwnerUser.email,
address: FROM_ADDRESS, 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 }, { timeout: 30_000 },

View File

@ -2,12 +2,12 @@ import { createElement } from 'react';
import { PDFDocument } from 'pdf-lib'; import { PDFDocument } from 'pdf-lib';
import { mailer } from '@documenso/email/mailer';
import { renderAsync } from '@documenso/email/render'; import { renderAsync } from '@documenso/email/render';
import { DocumentSelfSignedEmailTemplate } from '@documenso/email/templates/document-self-signed'; import { DocumentSelfSignedEmailTemplate } from '@documenso/email/templates/document-self-signed';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { FROM_ADDRESS, FROM_NAME, SERVICE_USER_EMAIL } from '@documenso/lib/constants/email'; 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 { 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 { alphaid } from '@documenso/lib/universal/id';
import { getFile } from '@documenso/lib/universal/upload/get-file'; import { getFile } from '@documenso/lib/universal/upload/get-file';
import { putFile } from '@documenso/lib/universal/upload/put-file'; import { putFile } from '@documenso/lib/universal/upload/put-file';
@ -160,19 +160,22 @@ export const singleplayerRouter = router({
]); ]);
// Send email to signer. // Send email to signer.
await mailer.sendMail({ await queueJob({
to: { job: 'send-mail',
address: signer.email, args: {
name: signer.name, 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; return token;