mirror of
https://github.com/documenso/documenso.git
synced 2026-06-22 20:32:07 +10:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 844af17ec2 |
@@ -1,12 +1,21 @@
|
||||
import { JobClient } from './client/client';
|
||||
import { SEND_2FA_TOKEN_EMAIL_JOB_DEFINITION } from './definitions/emails/send-2fa-token-email';
|
||||
import { SEND_COMPLETED_EMAIL_JOB_DEFINITION } from './definitions/emails/send-completed-email';
|
||||
import { SEND_CONFIRMATION_EMAIL_JOB_DEFINITION } from './definitions/emails/send-confirmation-email';
|
||||
import { SEND_DIRECT_TEMPLATE_CREATED_EMAIL_JOB_DEFINITION } from './definitions/emails/send-direct-template-created-email';
|
||||
import { SEND_DOCUMENT_CANCELLED_EMAILS_JOB_DEFINITION } from './definitions/emails/send-document-cancelled-emails';
|
||||
import { SEND_DOCUMENT_DELETED_EMAILS_JOB_DEFINITION } from './definitions/emails/send-document-deleted-emails';
|
||||
import { SEND_DOCUMENT_SUPER_DELETE_EMAIL_JOB_DEFINITION } from './definitions/emails/send-document-super-delete-email';
|
||||
import { SEND_FORGOT_PASSWORD_EMAIL_JOB_DEFINITION } from './definitions/emails/send-forgot-password-email';
|
||||
import { SEND_ORGANISATION_MEMBER_JOINED_EMAIL_JOB_DEFINITION } from './definitions/emails/send-organisation-member-joined-email';
|
||||
import { SEND_ORGANISATION_MEMBER_LEFT_EMAIL_JOB_DEFINITION } from './definitions/emails/send-organisation-member-left-email';
|
||||
import { SEND_OWNER_RECIPIENT_EXPIRED_EMAIL_JOB_DEFINITION } from './definitions/emails/send-owner-recipient-expired-email';
|
||||
import { SEND_PASSWORD_RESET_SUCCESS_EMAIL_JOB_DEFINITION } from './definitions/emails/send-password-reset-success-email';
|
||||
import { SEND_PENDING_EMAIL_JOB_DEFINITION } from './definitions/emails/send-pending-email';
|
||||
import { SEND_RECIPIENT_REMOVED_EMAIL_JOB_DEFINITION } from './definitions/emails/send-recipient-removed-email';
|
||||
import { SEND_RECIPIENT_SIGNED_EMAIL_JOB_DEFINITION } from './definitions/emails/send-recipient-signed-email';
|
||||
import { SEND_SIGNING_REJECTION_EMAILS_JOB_DEFINITION } from './definitions/emails/send-rejection-emails';
|
||||
import { SEND_RESEND_DOCUMENT_EMAIL_JOB_DEFINITION } from './definitions/emails/send-resend-document-email';
|
||||
import { SEND_SIGNING_EMAIL_JOB_DEFINITION } from './definitions/emails/send-signing-email';
|
||||
import { SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION } from './definitions/emails/send-team-deleted-email';
|
||||
import { BACKPORT_SUBSCRIPTION_CLAIM_JOB_DEFINITION } from './definitions/internal/backport-subscription-claims';
|
||||
@@ -39,6 +48,15 @@ export const jobsClient = new JobClient([
|
||||
EXPIRE_RECIPIENTS_SWEEP_JOB_DEFINITION,
|
||||
PROCESS_RECIPIENT_EXPIRED_JOB_DEFINITION,
|
||||
CLEANUP_RATE_LIMITS_JOB_DEFINITION,
|
||||
SEND_PENDING_EMAIL_JOB_DEFINITION,
|
||||
SEND_FORGOT_PASSWORD_EMAIL_JOB_DEFINITION,
|
||||
SEND_DOCUMENT_SUPER_DELETE_EMAIL_JOB_DEFINITION,
|
||||
SEND_COMPLETED_EMAIL_JOB_DEFINITION,
|
||||
SEND_RECIPIENT_REMOVED_EMAIL_JOB_DEFINITION,
|
||||
SEND_DOCUMENT_DELETED_EMAILS_JOB_DEFINITION,
|
||||
SEND_2FA_TOKEN_EMAIL_JOB_DEFINITION,
|
||||
SEND_RESEND_DOCUMENT_EMAIL_JOB_DEFINITION,
|
||||
SEND_DIRECT_TEMPLATE_CREATED_EMAIL_JOB_DEFINITION,
|
||||
] as const);
|
||||
|
||||
export const jobs = jobsClient;
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { AccessAuth2FAEmailTemplate } from '@documenso/email/templates/access-auth-2fa';
|
||||
import { isRecipientEmailValidForSending } from '@documenso/lib/utils/recipients';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
|
||||
import { AppError, AppErrorCode } from '../../../errors/app-error';
|
||||
import { TWO_FACTOR_EMAIL_EXPIRATION_MINUTES } from '../../../server-only/2fa/email/constants';
|
||||
import { generateTwoFactorTokenFromEmail } from '../../../server-only/2fa/email/generate-2fa-token-from-email';
|
||||
import { getEmailContext } from '../../../server-only/email/get-email-context';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../../types/document-audit-logs';
|
||||
import { createDocumentAuditLogData } from '../../../utils/document-audit-logs';
|
||||
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
|
||||
import type { JobRunIO } from '../../client/_internal/job';
|
||||
import type { TSend2FATokenEmailJobDefinition } from './send-2fa-token-email';
|
||||
|
||||
export const run = async ({
|
||||
payload,
|
||||
io,
|
||||
}: {
|
||||
payload: TSend2FATokenEmailJobDefinition;
|
||||
io: JobRunIO;
|
||||
}) => {
|
||||
const { envelopeId, recipientId } = payload;
|
||||
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: {
|
||||
id: envelopeId,
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
recipients: {
|
||||
some: {
|
||||
id: recipientId,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
recipients: {
|
||||
where: {
|
||||
id: recipientId,
|
||||
},
|
||||
},
|
||||
documentMeta: true,
|
||||
team: {
|
||||
select: {
|
||||
teamEmail: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
const [recipient] = envelope.recipients;
|
||||
|
||||
if (!recipient) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Recipient not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (!isRecipientEmailValidForSending(recipient)) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Recipient is missing email address',
|
||||
});
|
||||
}
|
||||
|
||||
const twoFactorTokenToken = await generateTwoFactorTokenFromEmail({
|
||||
envelopeId,
|
||||
email: recipient.email,
|
||||
});
|
||||
|
||||
const { branding, emailLanguage, senderEmail, replyToEmail } = await getEmailContext({
|
||||
emailType: 'RECIPIENT',
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId: envelope.teamId,
|
||||
},
|
||||
meta: envelope.documentMeta,
|
||||
});
|
||||
|
||||
const i18n = await getI18nInstance(emailLanguage);
|
||||
|
||||
const subject = i18n._(msg`Your two-factor authentication code`);
|
||||
|
||||
const template = createElement(AccessAuth2FAEmailTemplate, {
|
||||
documentTitle: envelope.title,
|
||||
userName: recipient.name,
|
||||
userEmail: recipient.email,
|
||||
code: twoFactorTokenToken,
|
||||
expiresInMinutes: TWO_FACTOR_EMAIL_EXPIRATION_MINUTES,
|
||||
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
});
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, { lang: emailLanguage, branding }),
|
||||
renderEmailWithI18N(template, { lang: emailLanguage, branding, plainText: true }),
|
||||
]);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: {
|
||||
address: recipient.email,
|
||||
name: recipient.name,
|
||||
},
|
||||
from: senderEmail,
|
||||
replyTo: replyToEmail,
|
||||
subject,
|
||||
html,
|
||||
text,
|
||||
});
|
||||
|
||||
await prisma.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_ACCESS_AUTH_2FA_REQUESTED,
|
||||
envelopeId: envelope.id,
|
||||
data: {
|
||||
recipientEmail: recipient.email,
|
||||
recipientName: recipient.name,
|
||||
recipientId: recipient.id,
|
||||
},
|
||||
}),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { type JobDefinition } from '../../client/_internal/job';
|
||||
|
||||
const SEND_2FA_TOKEN_EMAIL_JOB_DEFINITION_ID = 'send.2fa.token.email';
|
||||
|
||||
const SEND_2FA_TOKEN_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
|
||||
envelopeId: z.string(),
|
||||
recipientId: z.number(),
|
||||
});
|
||||
|
||||
export type TSend2FATokenEmailJobDefinition = z.infer<
|
||||
typeof SEND_2FA_TOKEN_EMAIL_JOB_DEFINITION_SCHEMA
|
||||
>;
|
||||
|
||||
export const SEND_2FA_TOKEN_EMAIL_JOB_DEFINITION = {
|
||||
id: SEND_2FA_TOKEN_EMAIL_JOB_DEFINITION_ID,
|
||||
name: 'Send 2FA Token Email',
|
||||
version: '1.0.0',
|
||||
trigger: {
|
||||
name: SEND_2FA_TOKEN_EMAIL_JOB_DEFINITION_ID,
|
||||
schema: SEND_2FA_TOKEN_EMAIL_JOB_DEFINITION_SCHEMA,
|
||||
},
|
||||
handler: async ({ payload, io }) => {
|
||||
const handler = await import('./send-2fa-token-email.handler');
|
||||
|
||||
await handler.run({ payload, io });
|
||||
},
|
||||
} as const satisfies JobDefinition<
|
||||
typeof SEND_2FA_TOKEN_EMAIL_JOB_DEFINITION_ID,
|
||||
TSend2FATokenEmailJobDefinition
|
||||
>;
|
||||
@@ -0,0 +1,20 @@
|
||||
import { sendCompletedEmail } from '../../../server-only/document/send-completed-email';
|
||||
import type { JobRunIO } from '../../client/_internal/job';
|
||||
import type { TSendCompletedEmailJobDefinition } from './send-completed-email';
|
||||
|
||||
export const run = async ({
|
||||
payload,
|
||||
}: {
|
||||
payload: TSendCompletedEmailJobDefinition;
|
||||
io: JobRunIO;
|
||||
}) => {
|
||||
const { envelopeId, requestMetadata } = payload;
|
||||
|
||||
await sendCompletedEmail({
|
||||
id: {
|
||||
type: 'envelopeId',
|
||||
id: envelopeId,
|
||||
},
|
||||
requestMetadata,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZRequestMetadataSchema } from '../../../universal/extract-request-metadata';
|
||||
import { type JobDefinition } from '../../client/_internal/job';
|
||||
|
||||
const SEND_COMPLETED_EMAIL_JOB_DEFINITION_ID = 'send.document.completed.email';
|
||||
|
||||
const SEND_COMPLETED_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
|
||||
envelopeId: z.string(),
|
||||
requestMetadata: ZRequestMetadataSchema.optional(),
|
||||
});
|
||||
|
||||
export type TSendCompletedEmailJobDefinition = z.infer<
|
||||
typeof SEND_COMPLETED_EMAIL_JOB_DEFINITION_SCHEMA
|
||||
>;
|
||||
|
||||
export const SEND_COMPLETED_EMAIL_JOB_DEFINITION = {
|
||||
id: SEND_COMPLETED_EMAIL_JOB_DEFINITION_ID,
|
||||
name: 'Send Completed Email',
|
||||
version: '1.0.0',
|
||||
trigger: {
|
||||
name: SEND_COMPLETED_EMAIL_JOB_DEFINITION_ID,
|
||||
schema: SEND_COMPLETED_EMAIL_JOB_DEFINITION_SCHEMA,
|
||||
},
|
||||
handler: async ({ payload, io }) => {
|
||||
const handler = await import('./send-completed-email.handler');
|
||||
|
||||
await handler.run({ payload, io });
|
||||
},
|
||||
} as const satisfies JobDefinition<
|
||||
typeof SEND_COMPLETED_EMAIL_JOB_DEFINITION_ID,
|
||||
TSendCompletedEmailJobDefinition
|
||||
>;
|
||||
@@ -0,0 +1,105 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { DocumentSource } from '@prisma/client';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { DocumentCreatedFromDirectTemplateEmailTemplate } from '@documenso/email/templates/document-created-from-direct-template';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
|
||||
import { getEmailContext } from '../../../server-only/email/get-email-context';
|
||||
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
|
||||
import { formatDocumentsPath } from '../../../utils/teams';
|
||||
import type { JobRunIO } from '../../client/_internal/job';
|
||||
import type { TSendDirectTemplateCreatedEmailJobDefinition } from './send-direct-template-created-email';
|
||||
|
||||
export const run = async ({
|
||||
payload,
|
||||
}: {
|
||||
payload: TSendDirectTemplateCreatedEmailJobDefinition;
|
||||
io: JobRunIO;
|
||||
}) => {
|
||||
const { envelopeId, teamId, directRecipientId } = payload;
|
||||
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: {
|
||||
id: envelopeId,
|
||||
source: DocumentSource.TEMPLATE_DIRECT_LINK,
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
documentMeta: true,
|
||||
team: {
|
||||
select: {
|
||||
url: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!envelope) {
|
||||
throw new Error('Document not found');
|
||||
}
|
||||
|
||||
const directRecipient = await prisma.recipient.findFirst({
|
||||
where: {
|
||||
id: directRecipientId,
|
||||
envelopeId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!directRecipient) {
|
||||
throw new Error('Direct recipient not found on envelope');
|
||||
}
|
||||
|
||||
const { branding, emailLanguage, senderEmail } = await getEmailContext({
|
||||
emailType: 'RECIPIENT',
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId,
|
||||
},
|
||||
meta: envelope.documentMeta,
|
||||
});
|
||||
|
||||
const templateOwner = envelope.user;
|
||||
|
||||
const documentLink = `${NEXT_PUBLIC_WEBAPP_URL()}${formatDocumentsPath(envelope.team?.url)}/${
|
||||
envelope.id
|
||||
}`;
|
||||
|
||||
const emailTemplate = createElement(DocumentCreatedFromDirectTemplateEmailTemplate, {
|
||||
recipientName: directRecipient.email,
|
||||
recipientRole: directRecipient.role,
|
||||
documentLink,
|
||||
documentName: envelope.title,
|
||||
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000',
|
||||
});
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(emailTemplate, { lang: emailLanguage, branding }),
|
||||
renderEmailWithI18N(emailTemplate, { lang: emailLanguage, branding, plainText: true }),
|
||||
]);
|
||||
|
||||
const i18n = await getI18nInstance(emailLanguage);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: [
|
||||
{
|
||||
name: templateOwner.name || '',
|
||||
address: templateOwner.email,
|
||||
},
|
||||
],
|
||||
from: senderEmail,
|
||||
subject: i18n._(msg`Document created from direct template`),
|
||||
html,
|
||||
text,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { type JobDefinition } from '../../client/_internal/job';
|
||||
|
||||
const SEND_DIRECT_TEMPLATE_CREATED_EMAIL_JOB_DEFINITION_ID = 'send.direct.template.created.email';
|
||||
|
||||
const SEND_DIRECT_TEMPLATE_CREATED_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
|
||||
envelopeId: z.string(),
|
||||
teamId: z.number(),
|
||||
directRecipientId: z.number(),
|
||||
});
|
||||
|
||||
export type TSendDirectTemplateCreatedEmailJobDefinition = z.infer<
|
||||
typeof SEND_DIRECT_TEMPLATE_CREATED_EMAIL_JOB_DEFINITION_SCHEMA
|
||||
>;
|
||||
|
||||
export const SEND_DIRECT_TEMPLATE_CREATED_EMAIL_JOB_DEFINITION = {
|
||||
id: SEND_DIRECT_TEMPLATE_CREATED_EMAIL_JOB_DEFINITION_ID,
|
||||
name: 'Send Direct Template Created Email',
|
||||
version: '1.0.0',
|
||||
trigger: {
|
||||
name: SEND_DIRECT_TEMPLATE_CREATED_EMAIL_JOB_DEFINITION_ID,
|
||||
schema: SEND_DIRECT_TEMPLATE_CREATED_EMAIL_JOB_DEFINITION_SCHEMA,
|
||||
},
|
||||
handler: async ({ payload, io }) => {
|
||||
const handler = await import('./send-direct-template-created-email.handler');
|
||||
|
||||
await handler.run({ payload, io });
|
||||
},
|
||||
} as const satisfies JobDefinition<
|
||||
typeof SEND_DIRECT_TEMPLATE_CREATED_EMAIL_JOB_DEFINITION_ID,
|
||||
TSendDirectTemplateCreatedEmailJobDefinition
|
||||
>;
|
||||
@@ -82,38 +82,36 @@ export const run = async ({
|
||||
isRecipientEmailValidForSending(recipient),
|
||||
);
|
||||
|
||||
await io.runTask('send-cancellation-emails', async () => {
|
||||
await Promise.all(
|
||||
recipientsToNotify.map(async (recipient) => {
|
||||
const template = createElement(DocumentCancelTemplate, {
|
||||
documentName: envelope.title,
|
||||
inviterName: documentOwner.name || undefined,
|
||||
inviterEmail: documentOwner.email,
|
||||
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
cancellationReason: cancellationReason || 'The document has been cancelled.',
|
||||
});
|
||||
await Promise.all(
|
||||
recipientsToNotify.map(async (recipient) => {
|
||||
const template = createElement(DocumentCancelTemplate, {
|
||||
documentName: envelope.title,
|
||||
inviterName: documentOwner.name || undefined,
|
||||
inviterEmail: documentOwner.email,
|
||||
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
cancellationReason: cancellationReason || 'The document has been cancelled.',
|
||||
});
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, { lang: emailLanguage, branding }),
|
||||
renderEmailWithI18N(template, {
|
||||
lang: emailLanguage,
|
||||
branding,
|
||||
plainText: true,
|
||||
}),
|
||||
]);
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, { lang: emailLanguage, branding }),
|
||||
renderEmailWithI18N(template, {
|
||||
lang: emailLanguage,
|
||||
branding,
|
||||
plainText: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: {
|
||||
name: recipient.name,
|
||||
address: recipient.email,
|
||||
},
|
||||
from: senderEmail,
|
||||
replyTo: replyToEmail,
|
||||
subject: i18n._(msg`Document "${envelope.title}" Cancelled`),
|
||||
html,
|
||||
text,
|
||||
});
|
||||
}),
|
||||
);
|
||||
});
|
||||
await mailer.sendMail({
|
||||
to: {
|
||||
name: recipient.name,
|
||||
address: recipient.email,
|
||||
},
|
||||
from: senderEmail,
|
||||
replyTo: replyToEmail,
|
||||
subject: i18n._(msg`Document "${envelope.title}" Cancelled`),
|
||||
html,
|
||||
text,
|
||||
});
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { DocumentSuperDeleteEmailTemplate } from '@documenso/email/templates/document-super-delete';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
|
||||
import { getEmailContext } from '../../../server-only/email/get-email-context';
|
||||
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
|
||||
import type { JobRunIO } from '../../client/_internal/job';
|
||||
import type { TSendDocumentSuperDeleteEmailJobDefinition } from './send-document-super-delete-email';
|
||||
|
||||
export const run = async ({
|
||||
payload,
|
||||
io,
|
||||
}: {
|
||||
payload: TSendDocumentSuperDeleteEmailJobDefinition;
|
||||
io: JobRunIO;
|
||||
}) => {
|
||||
const { userId, documentTitle, reason, teamId } = payload;
|
||||
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: { id: userId },
|
||||
select: { email: true, name: true },
|
||||
});
|
||||
|
||||
const { branding, senderEmail, emailLanguage } = await getEmailContext({
|
||||
emailType: 'RECIPIENT',
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId,
|
||||
},
|
||||
meta: null,
|
||||
});
|
||||
|
||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||
|
||||
const template = createElement(DocumentSuperDeleteEmailTemplate, {
|
||||
documentName: documentTitle,
|
||||
reason,
|
||||
assetBaseUrl,
|
||||
});
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, { lang: emailLanguage, branding }),
|
||||
renderEmailWithI18N(template, {
|
||||
lang: emailLanguage,
|
||||
branding,
|
||||
plainText: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
const i18n = await getI18nInstance(emailLanguage);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: {
|
||||
address: user.email,
|
||||
name: user.name || '',
|
||||
},
|
||||
from: senderEmail,
|
||||
subject: i18n._(msg`Document Deleted!`),
|
||||
html,
|
||||
text,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { type JobDefinition } from '../../client/_internal/job';
|
||||
|
||||
const SEND_DOCUMENT_SUPER_DELETE_EMAIL_JOB_DEFINITION_ID = 'send.document.super.delete.email';
|
||||
|
||||
const SEND_DOCUMENT_SUPER_DELETE_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
|
||||
userId: z.number(),
|
||||
documentTitle: z.string(),
|
||||
reason: z.string(),
|
||||
teamId: z.number(),
|
||||
});
|
||||
|
||||
export type TSendDocumentSuperDeleteEmailJobDefinition = z.infer<
|
||||
typeof SEND_DOCUMENT_SUPER_DELETE_EMAIL_JOB_DEFINITION_SCHEMA
|
||||
>;
|
||||
|
||||
export const SEND_DOCUMENT_SUPER_DELETE_EMAIL_JOB_DEFINITION = {
|
||||
id: SEND_DOCUMENT_SUPER_DELETE_EMAIL_JOB_DEFINITION_ID,
|
||||
name: 'Send Document Super Delete Email',
|
||||
version: '1.0.0',
|
||||
trigger: {
|
||||
name: SEND_DOCUMENT_SUPER_DELETE_EMAIL_JOB_DEFINITION_ID,
|
||||
schema: SEND_DOCUMENT_SUPER_DELETE_EMAIL_JOB_DEFINITION_SCHEMA,
|
||||
},
|
||||
handler: async ({ payload, io }) => {
|
||||
const handler = await import('./send-document-super-delete-email.handler');
|
||||
|
||||
await handler.run({ payload, io });
|
||||
},
|
||||
} as const satisfies JobDefinition<
|
||||
typeof SEND_DOCUMENT_SUPER_DELETE_EMAIL_JOB_DEFINITION_ID,
|
||||
TSendDocumentSuperDeleteEmailJobDefinition
|
||||
>;
|
||||
@@ -0,0 +1,72 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { ForgotPasswordTemplate } from '@documenso/email/templates/forgot-password';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
|
||||
import { env } from '../../../utils/env';
|
||||
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
|
||||
import type { JobRunIO } from '../../client/_internal/job';
|
||||
import type { TSendForgotPasswordEmailJobDefinition } from './send-forgot-password-email';
|
||||
|
||||
export const run = async ({
|
||||
payload,
|
||||
io,
|
||||
}: {
|
||||
payload: TSendForgotPasswordEmailJobDefinition;
|
||||
io: JobRunIO;
|
||||
}) => {
|
||||
const { userId } = payload;
|
||||
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
include: {
|
||||
passwordResetTokens: {
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
take: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (user.passwordResetTokens.length === 0) {
|
||||
throw new Error('No password reset token found for user');
|
||||
}
|
||||
|
||||
const token = user.passwordResetTokens[0].token;
|
||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||
const resetPasswordLink = `${NEXT_PUBLIC_WEBAPP_URL()}/reset-password/${token}`;
|
||||
|
||||
const template = createElement(ForgotPasswordTemplate, {
|
||||
assetBaseUrl,
|
||||
resetPasswordLink,
|
||||
});
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template),
|
||||
renderEmailWithI18N(template, { plainText: true }),
|
||||
]);
|
||||
|
||||
const i18n = await getI18nInstance();
|
||||
|
||||
await mailer.sendMail({
|
||||
to: {
|
||||
address: user.email,
|
||||
name: user.name || '',
|
||||
},
|
||||
from: {
|
||||
name: env('NEXT_PRIVATE_SMTP_FROM_NAME') || 'Documenso',
|
||||
address: env('NEXT_PRIVATE_SMTP_FROM_ADDRESS') || 'noreply@documenso.com',
|
||||
},
|
||||
subject: i18n._(msg`Forgot Password?`),
|
||||
html,
|
||||
text,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { type JobDefinition } from '../../client/_internal/job';
|
||||
|
||||
const SEND_FORGOT_PASSWORD_EMAIL_JOB_DEFINITION_ID = 'send.forgot.password.email';
|
||||
|
||||
const SEND_FORGOT_PASSWORD_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
|
||||
userId: z.number(),
|
||||
});
|
||||
|
||||
export type TSendForgotPasswordEmailJobDefinition = z.infer<
|
||||
typeof SEND_FORGOT_PASSWORD_EMAIL_JOB_DEFINITION_SCHEMA
|
||||
>;
|
||||
|
||||
export const SEND_FORGOT_PASSWORD_EMAIL_JOB_DEFINITION = {
|
||||
id: SEND_FORGOT_PASSWORD_EMAIL_JOB_DEFINITION_ID,
|
||||
name: 'Send Forgot Password Email',
|
||||
version: '1.0.0',
|
||||
trigger: {
|
||||
name: SEND_FORGOT_PASSWORD_EMAIL_JOB_DEFINITION_ID,
|
||||
schema: SEND_FORGOT_PASSWORD_EMAIL_JOB_DEFINITION_SCHEMA,
|
||||
},
|
||||
handler: async ({ payload, io }) => {
|
||||
const handler = await import('./send-forgot-password-email.handler');
|
||||
|
||||
await handler.run({ payload, io });
|
||||
},
|
||||
} as const satisfies JobDefinition<
|
||||
typeof SEND_FORGOT_PASSWORD_EMAIL_JOB_DEFINITION_ID,
|
||||
TSendForgotPasswordEmailJobDefinition
|
||||
>;
|
||||
+28
-33
@@ -80,41 +80,36 @@ export const run = async ({
|
||||
continue;
|
||||
}
|
||||
|
||||
await io.runTask(
|
||||
`send-organisation-member-joined-email--${invitedMember.id}_${member.id}`,
|
||||
async () => {
|
||||
const emailContent = createElement(OrganisationJoinEmailTemplate, {
|
||||
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
baseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
memberName: invitedMember.user.name || '',
|
||||
memberEmail: invitedMember.user.email,
|
||||
organisationName: organisation.name,
|
||||
organisationUrl: organisation.url,
|
||||
});
|
||||
const emailContent = createElement(OrganisationJoinEmailTemplate, {
|
||||
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
baseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
memberName: invitedMember.user.name || '',
|
||||
memberEmail: invitedMember.user.email,
|
||||
organisationName: organisation.name,
|
||||
organisationUrl: organisation.url,
|
||||
});
|
||||
|
||||
// !: Replace with the actual language of the recipient later
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(emailContent, {
|
||||
lang: emailLanguage,
|
||||
branding,
|
||||
}),
|
||||
renderEmailWithI18N(emailContent, {
|
||||
lang: emailLanguage,
|
||||
branding,
|
||||
plainText: true,
|
||||
}),
|
||||
]);
|
||||
// !: Replace with the actual language of the recipient later
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(emailContent, {
|
||||
lang: emailLanguage,
|
||||
branding,
|
||||
}),
|
||||
renderEmailWithI18N(emailContent, {
|
||||
lang: emailLanguage,
|
||||
branding,
|
||||
plainText: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
const i18n = await getI18nInstance(emailLanguage);
|
||||
const i18n = await getI18nInstance(emailLanguage);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: member.user.email,
|
||||
from: senderEmail,
|
||||
subject: i18n._(msg`A new member has joined your organisation`),
|
||||
html,
|
||||
text,
|
||||
});
|
||||
},
|
||||
);
|
||||
await mailer.sendMail({
|
||||
to: member.user.email,
|
||||
from: senderEmail,
|
||||
subject: i18n._(msg`A new member has joined your organisation`),
|
||||
html,
|
||||
text,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
+27
-32
@@ -75,40 +75,35 @@ export const run = async ({
|
||||
continue;
|
||||
}
|
||||
|
||||
await io.runTask(
|
||||
`send-organisation-member-left-email--${oldMember.id}_${member.id}`,
|
||||
async () => {
|
||||
const emailContent = createElement(OrganisationLeaveEmailTemplate, {
|
||||
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
baseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
memberName: oldMember.name || '',
|
||||
memberEmail: oldMember.email,
|
||||
organisationName: organisation.name,
|
||||
organisationUrl: organisation.url,
|
||||
});
|
||||
const emailContent = createElement(OrganisationLeaveEmailTemplate, {
|
||||
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
baseUrl: NEXT_PUBLIC_WEBAPP_URL(),
|
||||
memberName: oldMember.name || '',
|
||||
memberEmail: oldMember.email,
|
||||
organisationName: organisation.name,
|
||||
organisationUrl: organisation.url,
|
||||
});
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(emailContent, {
|
||||
lang: emailLanguage,
|
||||
branding,
|
||||
}),
|
||||
renderEmailWithI18N(emailContent, {
|
||||
lang: emailLanguage,
|
||||
branding,
|
||||
plainText: true,
|
||||
}),
|
||||
]);
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(emailContent, {
|
||||
lang: emailLanguage,
|
||||
branding,
|
||||
}),
|
||||
renderEmailWithI18N(emailContent, {
|
||||
lang: emailLanguage,
|
||||
branding,
|
||||
plainText: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
const i18n = await getI18nInstance(emailLanguage);
|
||||
const i18n = await getI18nInstance(emailLanguage);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: member.user.email,
|
||||
from: senderEmail,
|
||||
subject: i18n._(msg`A member has left your organisation`),
|
||||
html,
|
||||
text,
|
||||
});
|
||||
},
|
||||
);
|
||||
await mailer.sendMail({
|
||||
to: member.user.email,
|
||||
from: senderEmail,
|
||||
subject: i18n._(msg`A member has left your organisation`),
|
||||
html,
|
||||
text,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { sendPendingEmail } from '../../../server-only/document/send-pending-email';
|
||||
import type { JobRunIO } from '../../client/_internal/job';
|
||||
import type { TSendPendingEmailJobDefinition } from './send-pending-email';
|
||||
|
||||
export const run = async ({
|
||||
payload,
|
||||
}: {
|
||||
payload: TSendPendingEmailJobDefinition;
|
||||
io: JobRunIO;
|
||||
}) => {
|
||||
const { envelopeId, recipientId } = payload;
|
||||
|
||||
await sendPendingEmail({
|
||||
id: {
|
||||
type: 'envelopeId',
|
||||
id: envelopeId,
|
||||
},
|
||||
recipientId,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { type JobDefinition } from '../../client/_internal/job';
|
||||
|
||||
const SEND_PENDING_EMAIL_JOB_DEFINITION_ID = 'send.document.pending.email';
|
||||
|
||||
const SEND_PENDING_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
|
||||
envelopeId: z.string(),
|
||||
recipientId: z.number(),
|
||||
});
|
||||
|
||||
export type TSendPendingEmailJobDefinition = z.infer<
|
||||
typeof SEND_PENDING_EMAIL_JOB_DEFINITION_SCHEMA
|
||||
>;
|
||||
|
||||
export const SEND_PENDING_EMAIL_JOB_DEFINITION = {
|
||||
id: SEND_PENDING_EMAIL_JOB_DEFINITION_ID,
|
||||
name: 'Send Pending Email',
|
||||
version: '1.0.0',
|
||||
trigger: {
|
||||
name: SEND_PENDING_EMAIL_JOB_DEFINITION_ID,
|
||||
schema: SEND_PENDING_EMAIL_JOB_DEFINITION_SCHEMA,
|
||||
},
|
||||
handler: async ({ payload, io }) => {
|
||||
const handler = await import('./send-pending-email.handler');
|
||||
|
||||
await handler.run({ payload, io });
|
||||
},
|
||||
} as const satisfies JobDefinition<
|
||||
typeof SEND_PENDING_EMAIL_JOB_DEFINITION_ID,
|
||||
TSendPendingEmailJobDefinition
|
||||
>;
|
||||
@@ -105,25 +105,23 @@ export const run = async ({
|
||||
assetBaseUrl,
|
||||
});
|
||||
|
||||
await io.runTask('send-recipient-signed-email', async () => {
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, { lang: emailLanguage, branding }),
|
||||
renderEmailWithI18N(template, {
|
||||
lang: emailLanguage,
|
||||
branding,
|
||||
plainText: true,
|
||||
}),
|
||||
]);
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, { lang: emailLanguage, branding }),
|
||||
renderEmailWithI18N(template, {
|
||||
lang: emailLanguage,
|
||||
branding,
|
||||
plainText: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: {
|
||||
name: owner.name ?? '',
|
||||
address: owner.email,
|
||||
},
|
||||
from: senderEmail,
|
||||
subject: i18n._(msg`${recipientReference} has signed "${envelope.title}"`),
|
||||
html,
|
||||
text,
|
||||
});
|
||||
await mailer.sendMail({
|
||||
to: {
|
||||
name: owner.name ?? '',
|
||||
address: owner.email,
|
||||
},
|
||||
from: senderEmail,
|
||||
subject: i18n._(msg`${recipientReference} has signed "${envelope.title}"`),
|
||||
html,
|
||||
text,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import {
|
||||
DocumentStatus,
|
||||
EnvelopeType,
|
||||
OrganisationType,
|
||||
RecipientRole,
|
||||
SigningStatus,
|
||||
} from '@prisma/client';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { DocumentInviteEmailTemplate } from '@documenso/email/templates/document-invite';
|
||||
import {
|
||||
RECIPIENT_ROLES_DESCRIPTION,
|
||||
RECIPIENT_ROLE_TO_EMAIL_TYPE,
|
||||
} from '@documenso/lib/constants/recipient-roles';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { renderCustomEmailTemplate } from '@documenso/lib/utils/render-custom-email-template';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
|
||||
import { getEmailContext } from '../../../server-only/email/get-email-context';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../../types/document-email';
|
||||
import { isDocumentCompleted } from '../../../utils/document';
|
||||
import { isRecipientEmailValidForSending } from '../../../utils/recipients';
|
||||
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
|
||||
import type { JobRunIO } from '../../client/_internal/job';
|
||||
import type { TSendResendDocumentEmailJobDefinition } from './send-resend-document-email';
|
||||
|
||||
export const run = async ({
|
||||
payload,
|
||||
}: {
|
||||
payload: TSendResendDocumentEmailJobDefinition;
|
||||
io: JobRunIO;
|
||||
}) => {
|
||||
const { envelopeId, userId, recipientIds, requestMetadata } = payload;
|
||||
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: {
|
||||
id: envelopeId,
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
},
|
||||
include: {
|
||||
recipients: true,
|
||||
documentMeta: true,
|
||||
team: {
|
||||
select: {
|
||||
teamEmail: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!envelope) {
|
||||
throw new Error('Document not found');
|
||||
}
|
||||
|
||||
if (envelope.status === DocumentStatus.DRAFT) {
|
||||
throw new Error('Can not send draft document');
|
||||
}
|
||||
|
||||
if (isDocumentCompleted(envelope.status)) {
|
||||
throw new Error('Can not send completed document');
|
||||
}
|
||||
|
||||
const isRecipientSigningRequestEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||
envelope.documentMeta,
|
||||
).recipientSigningRequest;
|
||||
|
||||
if (!isRecipientSigningRequestEmailEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { branding, emailLanguage, organisationType, senderEmail, replyToEmail } =
|
||||
await getEmailContext({
|
||||
emailType: 'RECIPIENT',
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId: envelope.teamId,
|
||||
},
|
||||
meta: envelope.documentMeta,
|
||||
});
|
||||
|
||||
const recipientsToRemind = envelope.recipients.filter(
|
||||
(recipient) =>
|
||||
recipientIds.includes(recipient.id) &&
|
||||
recipient.signingStatus === SigningStatus.NOT_SIGNED &&
|
||||
recipient.role !== RecipientRole.CC,
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
recipientsToRemind.map(async (recipient) => {
|
||||
if (recipient.role === RecipientRole.CC || !isRecipientEmailValidForSending(recipient)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const i18n = await getI18nInstance(emailLanguage);
|
||||
|
||||
const recipientEmailType = RECIPIENT_ROLE_TO_EMAIL_TYPE[recipient.role];
|
||||
|
||||
const { email, name } = recipient;
|
||||
const selfSigner = email === user.email;
|
||||
|
||||
const recipientActionVerb = i18n
|
||||
._(RECIPIENT_ROLES_DESCRIPTION[recipient.role].actionVerb)
|
||||
.toLowerCase();
|
||||
|
||||
let emailMessage = envelope.documentMeta.message || '';
|
||||
let emailSubject = i18n._(msg`Reminder: Please ${recipientActionVerb} this document`);
|
||||
|
||||
if (selfSigner) {
|
||||
emailMessage = i18n._(
|
||||
msg`You have initiated the document ${`"${envelope.title}"`} that requires you to ${recipientActionVerb} it.`,
|
||||
);
|
||||
emailSubject = i18n._(msg`Reminder: Please ${recipientActionVerb} your document`);
|
||||
}
|
||||
|
||||
if (organisationType === OrganisationType.ORGANISATION) {
|
||||
emailSubject = i18n._(
|
||||
msg`Reminder: ${envelope.team.name} invited you to ${recipientActionVerb} a document`,
|
||||
);
|
||||
emailMessage =
|
||||
envelope.documentMeta.message ||
|
||||
i18n._(
|
||||
msg`${user.name || user.email} on behalf of "${envelope.team.name}" has invited you to ${recipientActionVerb} the document "${envelope.title}".`,
|
||||
);
|
||||
}
|
||||
|
||||
const customEmailTemplate = {
|
||||
'signer.name': name,
|
||||
'signer.email': email,
|
||||
'document.name': envelope.title,
|
||||
};
|
||||
|
||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||
const signDocumentLink = `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`;
|
||||
|
||||
const template = createElement(DocumentInviteEmailTemplate, {
|
||||
documentName: envelope.title,
|
||||
inviterName: user.name || undefined,
|
||||
inviterEmail:
|
||||
organisationType === OrganisationType.ORGANISATION
|
||||
? envelope.team?.teamEmail?.email || user.email
|
||||
: user.email,
|
||||
assetBaseUrl,
|
||||
signDocumentLink,
|
||||
customBody: renderCustomEmailTemplate(emailMessage, customEmailTemplate),
|
||||
role: recipient.role,
|
||||
selfSigner,
|
||||
organisationType,
|
||||
teamName: envelope.team?.name,
|
||||
});
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, {
|
||||
lang: emailLanguage,
|
||||
branding,
|
||||
}),
|
||||
renderEmailWithI18N(template, {
|
||||
lang: emailLanguage,
|
||||
branding,
|
||||
plainText: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: {
|
||||
address: email,
|
||||
name,
|
||||
},
|
||||
from: senderEmail,
|
||||
replyTo: replyToEmail,
|
||||
subject: envelope.documentMeta.subject
|
||||
? renderCustomEmailTemplate(
|
||||
i18n._(msg`Reminder: ${envelope.documentMeta.subject}`),
|
||||
customEmailTemplate,
|
||||
)
|
||||
: emailSubject,
|
||||
html,
|
||||
text,
|
||||
});
|
||||
|
||||
await prisma.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
|
||||
envelopeId: envelope.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
emailType: recipientEmailType,
|
||||
recipientEmail: recipient.email,
|
||||
recipientName: recipient.name,
|
||||
recipientRole: recipient.role,
|
||||
recipientId: recipient.id,
|
||||
isResending: true,
|
||||
},
|
||||
}),
|
||||
});
|
||||
}),
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { type JobDefinition } from '../../client/_internal/job';
|
||||
|
||||
const SEND_RESEND_DOCUMENT_EMAIL_JOB_DEFINITION_ID = 'send.resend.document.email';
|
||||
|
||||
const SEND_RESEND_DOCUMENT_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
|
||||
envelopeId: z.string(),
|
||||
userId: z.number(),
|
||||
teamId: z.number(),
|
||||
recipientIds: z.array(z.number()),
|
||||
requestMetadata: z.any().optional(),
|
||||
});
|
||||
|
||||
export type TSendResendDocumentEmailJobDefinition = z.infer<
|
||||
typeof SEND_RESEND_DOCUMENT_EMAIL_JOB_DEFINITION_SCHEMA
|
||||
>;
|
||||
|
||||
export const SEND_RESEND_DOCUMENT_EMAIL_JOB_DEFINITION = {
|
||||
id: SEND_RESEND_DOCUMENT_EMAIL_JOB_DEFINITION_ID,
|
||||
name: 'Send Resend Document Email',
|
||||
version: '1.0.0',
|
||||
trigger: {
|
||||
name: SEND_RESEND_DOCUMENT_EMAIL_JOB_DEFINITION_ID,
|
||||
schema: SEND_RESEND_DOCUMENT_EMAIL_JOB_DEFINITION_SCHEMA,
|
||||
},
|
||||
handler: async ({ payload, io }) => {
|
||||
const handler = await import('./send-resend-document-email.handler');
|
||||
|
||||
await handler.run({ payload, io });
|
||||
},
|
||||
} as const satisfies JobDefinition<
|
||||
typeof SEND_RESEND_DOCUMENT_EMAIL_JOB_DEFINITION_ID,
|
||||
TSendResendDocumentEmailJobDefinition
|
||||
>;
|
||||
@@ -21,7 +21,6 @@ import { signPdf } from '@documenso/signing';
|
||||
|
||||
import { NEXT_PRIVATE_USE_PLAYWRIGHT_PDF } from '../../../constants/app';
|
||||
import { AppError, AppErrorCode } from '../../../errors/app-error';
|
||||
import { sendCompletedEmail } from '../../../server-only/document/send-completed-email';
|
||||
import { getAuditLogsPdf } from '../../../server-only/htmltopdf/get-audit-logs-pdf';
|
||||
import { getCertificatePdf } from '../../../server-only/htmltopdf/get-certificate-pdf';
|
||||
import { insertFieldInPDFV1 } from '../../../server-only/pdf/insert-field-in-pdf-v1';
|
||||
@@ -323,20 +322,21 @@ export const run = async ({
|
||||
};
|
||||
});
|
||||
|
||||
await io.runTask('send-completed-email', async () => {
|
||||
let shouldSendCompletedEmail = sendEmail && !isResealing && !isRejected;
|
||||
let shouldSendCompletedEmail = sendEmail && !isResealing && !isRejected;
|
||||
|
||||
if (isResealing && !isDocumentCompleted(envelopeStatus)) {
|
||||
shouldSendCompletedEmail = sendEmail;
|
||||
}
|
||||
if (isResealing && !isDocumentCompleted(envelopeStatus)) {
|
||||
shouldSendCompletedEmail = sendEmail;
|
||||
}
|
||||
|
||||
if (shouldSendCompletedEmail) {
|
||||
await sendCompletedEmail({
|
||||
id: { type: 'envelopeId', id: envelopeId },
|
||||
if (shouldSendCompletedEmail) {
|
||||
await io.triggerJob('trigger-send-completed-email', {
|
||||
name: 'send.document.completed.email',
|
||||
payload: {
|
||||
envelopeId,
|
||||
requestMetadata,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const updatedEnvelope = await prisma.envelope.findFirstOrThrow({
|
||||
where: {
|
||||
|
||||
@@ -1,30 +1,21 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { DocumentStatus, SendStatus } from '@prisma/client';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import DocumentCancelTemplate from '@documenso/email/templates/document-cancel';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n-server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { jobs } from '../../jobs/client';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
import { isRecipientEmailValidForSending } from '../../utils/recipients';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { getEmailContext } from '../email/get-email-context';
|
||||
|
||||
export type AdminSuperDeleteDocumentOptions = {
|
||||
envelopeId: string;
|
||||
reason: string;
|
||||
requestMetadata?: RequestMetadata;
|
||||
};
|
||||
|
||||
export const adminSuperDeleteDocument = async ({
|
||||
envelopeId,
|
||||
reason,
|
||||
requestMetadata,
|
||||
}: AdminSuperDeleteDocumentOptions) => {
|
||||
const envelope = await prisma.envelope.findUnique({
|
||||
@@ -32,7 +23,6 @@ export const adminSuperDeleteDocument = async ({
|
||||
id: envelopeId,
|
||||
},
|
||||
include: {
|
||||
recipients: true,
|
||||
documentMeta: true,
|
||||
user: {
|
||||
select: {
|
||||
@@ -50,75 +40,14 @@ export const adminSuperDeleteDocument = async ({
|
||||
});
|
||||
}
|
||||
|
||||
const { branding, settings, senderEmail, replyToEmail } = await getEmailContext({
|
||||
emailType: 'RECIPIENT',
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId: envelope.teamId,
|
||||
},
|
||||
meta: envelope.documentMeta,
|
||||
});
|
||||
|
||||
const { status, user } = envelope;
|
||||
const { user } = envelope;
|
||||
|
||||
const isDocumentDeletedEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||
envelope.documentMeta,
|
||||
).documentDeleted;
|
||||
|
||||
const recipientsToNotify = envelope.recipients.filter((recipient) =>
|
||||
isRecipientEmailValidForSending(recipient),
|
||||
);
|
||||
|
||||
// if the document is pending, send cancellation emails to all recipients
|
||||
if (
|
||||
status === DocumentStatus.PENDING &&
|
||||
recipientsToNotify.length > 0 &&
|
||||
isDocumentDeletedEmailEnabled
|
||||
) {
|
||||
await Promise.all(
|
||||
recipientsToNotify.map(async (recipient) => {
|
||||
if (recipient.sendStatus !== SendStatus.SENT) {
|
||||
return;
|
||||
}
|
||||
|
||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||
const template = createElement(DocumentCancelTemplate, {
|
||||
documentName: envelope.title,
|
||||
inviterName: user.name || undefined,
|
||||
inviterEmail: user.email,
|
||||
assetBaseUrl,
|
||||
});
|
||||
|
||||
const lang = envelope.documentMeta?.language ?? settings.documentLanguage;
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, { lang, branding }),
|
||||
renderEmailWithI18N(template, {
|
||||
lang,
|
||||
branding,
|
||||
plainText: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
const i18n = await getI18nInstance(lang);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: {
|
||||
address: recipient.email,
|
||||
name: recipient.name,
|
||||
},
|
||||
from: senderEmail,
|
||||
replyTo: replyToEmail,
|
||||
subject: i18n._(msg`Document Cancelled`),
|
||||
html,
|
||||
text,
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// always hard delete if deleted from admin
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
// Always hard delete if deleted from admin.
|
||||
const deletedEnvelope = await prisma.$transaction(async (tx) => {
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
envelopeId,
|
||||
@@ -133,4 +62,21 @@ export const adminSuperDeleteDocument = async ({
|
||||
|
||||
return await tx.envelope.delete({ where: { id: envelopeId } });
|
||||
});
|
||||
|
||||
// Notify the document owner after the hard delete transaction commits.
|
||||
// We only send the owner notification; recipient cancellation emails are
|
||||
// omitted because the recipients are hard-deleted with the envelope.
|
||||
if (isDocumentDeletedEmailEnabled) {
|
||||
await jobs.triggerJob({
|
||||
name: 'send.document.super.delete.email',
|
||||
payload: {
|
||||
userId: user.id,
|
||||
documentTitle: envelope.title,
|
||||
reason,
|
||||
teamId: envelope.teamId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return deletedEnvelope;
|
||||
};
|
||||
|
||||
@@ -33,7 +33,6 @@ import { assertRecipientNotExpired } from '../../utils/recipients';
|
||||
import { getIsRecipientsTurnToSign } from '../recipient/get-is-recipient-turn';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
import { isRecipientAuthorized } from './is-recipient-authorized';
|
||||
import { sendPendingEmail } from './send-pending-email';
|
||||
|
||||
export type CompleteDocumentWithTokenOptions = {
|
||||
token: string;
|
||||
@@ -317,7 +316,13 @@ export const completeDocumentWithToken = async ({
|
||||
});
|
||||
|
||||
if (pendingRecipients.length > 0) {
|
||||
await sendPendingEmail({ id, recipientId: recipient.id });
|
||||
await jobs.triggerJob({
|
||||
name: 'send.document.pending.email',
|
||||
payload: {
|
||||
envelopeId: envelope.id,
|
||||
recipientId: recipient.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (envelope.documentMeta?.signingOrder === DocumentSigningOrder.SEQUENTIAL) {
|
||||
const [nextRecipient] = pendingRecipients;
|
||||
|
||||
@@ -1,35 +1,12 @@
|
||||
import { createElement } from 'react';
|
||||
import { DocumentStatus, EnvelopeType, RecipientRole, SigningStatus } from '@prisma/client';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import {
|
||||
DocumentStatus,
|
||||
EnvelopeType,
|
||||
OrganisationType,
|
||||
RecipientRole,
|
||||
SigningStatus,
|
||||
} from '@prisma/client';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { DocumentInviteEmailTemplate } from '@documenso/email/templates/document-invite';
|
||||
import { resolveExpiresAt } from '@documenso/lib/constants/envelope-expiration';
|
||||
import {
|
||||
RECIPIENT_ROLES_DESCRIPTION,
|
||||
RECIPIENT_ROLE_TO_EMAIL_TYPE,
|
||||
} from '@documenso/lib/constants/recipient-roles';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { renderCustomEmailTemplate } from '@documenso/lib/utils/render-custom-email-template';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n-server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||
import { jobs } from '../../jobs/client';
|
||||
import { isDocumentCompleted } from '../../utils/document';
|
||||
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
||||
import { isRecipientEmailValidForSending } from '../../utils/recipients';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { getEmailContext } from '../email/get-email-context';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
|
||||
export type ResendDocumentOptions = {
|
||||
@@ -47,17 +24,6 @@ export const resendDocument = async ({
|
||||
teamId,
|
||||
requestMetadata,
|
||||
}: ResendDocumentOptions) => {
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id,
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
@@ -70,12 +36,6 @@ export const resendDocument = async ({
|
||||
include: {
|
||||
recipients: true,
|
||||
documentMeta: true,
|
||||
team: {
|
||||
select: {
|
||||
teamEmail: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -120,138 +80,20 @@ export const resendDocument = async ({
|
||||
});
|
||||
}
|
||||
|
||||
const isRecipientSigningRequestEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||
envelope.documentMeta,
|
||||
).recipientSigningRequest;
|
||||
|
||||
if (!isRecipientSigningRequestEmailEnabled) {
|
||||
return envelope;
|
||||
}
|
||||
|
||||
const { branding, emailLanguage, organisationType, senderEmail, replyToEmail } =
|
||||
await getEmailContext({
|
||||
emailType: 'RECIPIENT',
|
||||
source: {
|
||||
type: 'team',
|
||||
// Dispatch the email sending to a background job so that email delivery
|
||||
// failures don't block the resend operation and can be retried independently.
|
||||
if (recipientsToRemind.length > 0) {
|
||||
await jobs.triggerJob({
|
||||
name: 'send.resend.document.email',
|
||||
payload: {
|
||||
envelopeId: envelope.id,
|
||||
userId,
|
||||
teamId: envelope.teamId,
|
||||
recipientIds: recipientsToRemind.map((r) => r.id),
|
||||
requestMetadata,
|
||||
},
|
||||
meta: envelope.documentMeta,
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
recipientsToRemind.map(async (recipient) => {
|
||||
if (recipient.role === RecipientRole.CC || !isRecipientEmailValidForSending(recipient)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const i18n = await getI18nInstance(emailLanguage);
|
||||
|
||||
const recipientEmailType = RECIPIENT_ROLE_TO_EMAIL_TYPE[recipient.role];
|
||||
|
||||
const { email, name } = recipient;
|
||||
const selfSigner = email === user.email;
|
||||
|
||||
const recipientActionVerb = i18n
|
||||
._(RECIPIENT_ROLES_DESCRIPTION[recipient.role].actionVerb)
|
||||
.toLowerCase();
|
||||
|
||||
let emailMessage = envelope.documentMeta.message || '';
|
||||
let emailSubject = i18n._(msg`Reminder: Please ${recipientActionVerb} this document`);
|
||||
|
||||
if (selfSigner) {
|
||||
emailMessage = i18n._(
|
||||
msg`You have initiated the document ${`"${envelope.title}"`} that requires you to ${recipientActionVerb} it.`,
|
||||
);
|
||||
emailSubject = i18n._(msg`Reminder: Please ${recipientActionVerb} your document`);
|
||||
}
|
||||
|
||||
if (organisationType === OrganisationType.ORGANISATION) {
|
||||
emailSubject = i18n._(
|
||||
msg`Reminder: ${envelope.team.name} invited you to ${recipientActionVerb} a document`,
|
||||
);
|
||||
emailMessage =
|
||||
envelope.documentMeta.message ||
|
||||
i18n._(
|
||||
msg`${user.name || user.email} on behalf of "${envelope.team.name}" has invited you to ${recipientActionVerb} the document "${envelope.title}".`,
|
||||
);
|
||||
}
|
||||
|
||||
const customEmailTemplate = {
|
||||
'signer.name': name,
|
||||
'signer.email': email,
|
||||
'document.name': envelope.title,
|
||||
};
|
||||
|
||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||
const signDocumentLink = `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`;
|
||||
|
||||
const template = createElement(DocumentInviteEmailTemplate, {
|
||||
documentName: envelope.title,
|
||||
inviterName: user.name || undefined,
|
||||
inviterEmail:
|
||||
organisationType === OrganisationType.ORGANISATION
|
||||
? envelope.team?.teamEmail?.email || user.email
|
||||
: user.email,
|
||||
assetBaseUrl,
|
||||
signDocumentLink,
|
||||
customBody: renderCustomEmailTemplate(emailMessage, customEmailTemplate),
|
||||
role: recipient.role,
|
||||
selfSigner,
|
||||
organisationType,
|
||||
teamName: envelope.team?.name,
|
||||
});
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, {
|
||||
lang: emailLanguage,
|
||||
branding,
|
||||
}),
|
||||
renderEmailWithI18N(template, {
|
||||
lang: emailLanguage,
|
||||
branding,
|
||||
plainText: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
await prisma.$transaction(
|
||||
async (tx) => {
|
||||
await mailer.sendMail({
|
||||
to: {
|
||||
address: email,
|
||||
name,
|
||||
},
|
||||
from: senderEmail,
|
||||
replyTo: replyToEmail,
|
||||
subject: envelope.documentMeta.subject
|
||||
? renderCustomEmailTemplate(
|
||||
i18n._(msg`Reminder: ${envelope.documentMeta.subject}`),
|
||||
customEmailTemplate,
|
||||
)
|
||||
: emailSubject,
|
||||
html,
|
||||
text,
|
||||
});
|
||||
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
|
||||
envelopeId: envelope.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
emailType: recipientEmailType,
|
||||
recipientEmail: recipient.email,
|
||||
recipientName: recipient.name,
|
||||
recipientRole: recipient.role,
|
||||
recipientId: recipient.id,
|
||||
isResending: true,
|
||||
},
|
||||
}),
|
||||
});
|
||||
},
|
||||
{ timeout: 30_000 },
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return envelope;
|
||||
};
|
||||
|
||||
@@ -22,6 +22,8 @@ import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { formatDocumentsPath } from '../../utils/teams';
|
||||
import { getEmailContext } from '../email/get-email-context';
|
||||
|
||||
const TWENTY_MB_IN_BYTES = 20 * 1024 * 1024;
|
||||
|
||||
export interface SendDocumentOptions {
|
||||
id: EnvelopeIdOptions;
|
||||
requestMetadata?: RequestMetadata;
|
||||
@@ -81,7 +83,7 @@ export const sendCompletedEmail = async ({ id, requestMetadata }: SendDocumentOp
|
||||
|
||||
const { user: owner } = envelope;
|
||||
|
||||
const completedDocumentEmailAttachments = await Promise.all(
|
||||
let completedDocumentEmailAttachments = await Promise.all(
|
||||
envelope.envelopeItems.map(async (envelopeItem) => {
|
||||
const file = await getFileServerSide(envelopeItem.documentData);
|
||||
|
||||
@@ -97,6 +99,16 @@ export const sendCompletedEmail = async ({ id, requestMetadata }: SendDocumentOp
|
||||
}),
|
||||
);
|
||||
|
||||
const allAttachmentsSize = completedDocumentEmailAttachments.reduce(
|
||||
(acc, attachment) => acc + attachment.content.length,
|
||||
0,
|
||||
);
|
||||
|
||||
// If the total size of attachments exceeds 20MB, do not include attachments and instead provide a download link in the email body.
|
||||
if (allAttachmentsSize > TWENTY_MB_IN_BYTES) {
|
||||
completedDocumentEmailAttachments = [];
|
||||
}
|
||||
|
||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||
|
||||
let documentOwnerDownloadLink = `${NEXT_PUBLIC_WEBAPP_URL()}${formatDocumentsPath(
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import type { Field, Signature } from '@prisma/client';
|
||||
import {
|
||||
DocumentSigningOrder,
|
||||
@@ -18,14 +15,11 @@ import { DateTime } from 'luxon';
|
||||
import { match } from 'ts-pattern';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { mailer } from '@documenso/email/mailer';
|
||||
import { DocumentCreatedFromDirectTemplateEmailTemplate } from '@documenso/email/templates/document-created-from-direct-template';
|
||||
import { jobs } from '@documenso/lib/jobs/client';
|
||||
import { nanoid, prefixedId } from '@documenso/lib/universal/id';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { TSignFieldWithTokenMutationSchema } from '@documenso/trpc/server/field-router/schema';
|
||||
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n-server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE, RECIPIENT_DIFF_TYPE } from '../../types/document-audit-logs';
|
||||
import type { TRecipientActionAuthTypes } from '../../types/document-auth';
|
||||
@@ -48,8 +42,6 @@ import {
|
||||
extractDocumentAuthMethods,
|
||||
} from '../../utils/document-auth';
|
||||
import { mapSecondaryIdToTemplateId } from '../../utils/envelope';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { formatDocumentsPath } from '../../utils/teams';
|
||||
import { sendDocument } from '../document/send-document';
|
||||
import { validateFieldAuth } from '../document/validate-field-auth';
|
||||
import { getEmailContext } from '../email/get-email-context';
|
||||
@@ -156,7 +148,7 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
});
|
||||
}
|
||||
|
||||
const { branding, settings, senderEmail, emailLanguage } = await getEmailContext({
|
||||
const { settings } = await getEmailContext({
|
||||
emailType: 'INTERNAL',
|
||||
source: {
|
||||
type: 'team',
|
||||
@@ -164,7 +156,7 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
},
|
||||
});
|
||||
|
||||
const { recipients, directLink, user: templateOwner } = directTemplateEnvelope;
|
||||
const { recipients, directLink } = directTemplateEnvelope;
|
||||
|
||||
const directTemplateRecipient = recipients.find(
|
||||
(recipient) => recipient.id === directLink.directTemplateRecipientId,
|
||||
@@ -755,37 +747,6 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
});
|
||||
}
|
||||
|
||||
// Send email to template owner.
|
||||
const emailTemplate = createElement(DocumentCreatedFromDirectTemplateEmailTemplate, {
|
||||
recipientName: directRecipientEmail,
|
||||
recipientRole: directTemplateRecipient.role,
|
||||
documentLink: `${NEXT_PUBLIC_WEBAPP_URL()}${formatDocumentsPath(createdEnvelope.team?.url)}/${
|
||||
createdEnvelope.id
|
||||
}`,
|
||||
documentName: createdEnvelope.title,
|
||||
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000',
|
||||
});
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(emailTemplate, { lang: emailLanguage, branding }),
|
||||
renderEmailWithI18N(emailTemplate, { lang: emailLanguage, branding, plainText: true }),
|
||||
]);
|
||||
|
||||
const i18n = await getI18nInstance(emailLanguage);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: [
|
||||
{
|
||||
name: templateOwner.name || '',
|
||||
address: templateOwner.email,
|
||||
},
|
||||
],
|
||||
from: senderEmail,
|
||||
subject: i18n._(msg`Document created from direct template`),
|
||||
html,
|
||||
text,
|
||||
});
|
||||
|
||||
return {
|
||||
createdEnvelope,
|
||||
token: createdDirectRecipient.token,
|
||||
@@ -793,6 +754,17 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
};
|
||||
});
|
||||
|
||||
// Dispatch the email notification to the template owner as a background job.
|
||||
// This is outside the transaction so email failures don't roll back document creation.
|
||||
await jobs.triggerJob({
|
||||
name: 'send.direct.template.created.email',
|
||||
payload: {
|
||||
envelopeId: createdEnvelope.id,
|
||||
teamId: createdEnvelope.teamId,
|
||||
directRecipientId: recipientId,
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
// This handles sending emails and sealing the document if required.
|
||||
await sendDocument({
|
||||
|
||||
@@ -3,7 +3,7 @@ import crypto from 'crypto';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { ONE_DAY } from '../../constants/time';
|
||||
import { sendForgotPassword } from '../auth/send-forgot-password';
|
||||
import { jobs } from '../../jobs/client';
|
||||
|
||||
export const forgotPassword = async ({ email }: { email: string }) => {
|
||||
const user = await prisma.user.findFirst({
|
||||
@@ -46,7 +46,10 @@ export const forgotPassword = async ({ email }: { email: string }) => {
|
||||
},
|
||||
});
|
||||
|
||||
await sendForgotPassword({
|
||||
userId: user.id,
|
||||
}).catch((err) => console.error(err));
|
||||
await jobs.triggerJob({
|
||||
name: 'send.forgot.password.email',
|
||||
payload: {
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { adminSuperDeleteDocument } from '@documenso/lib/server-only/admin/admin-super-delete-document';
|
||||
import { sendDeleteEmail } from '@documenso/lib/server-only/document/send-delete-email';
|
||||
|
||||
import { adminProcedure } from '../trpc';
|
||||
import {
|
||||
@@ -19,10 +18,9 @@ export const deleteDocumentRoute = adminProcedure
|
||||
},
|
||||
});
|
||||
|
||||
await sendDeleteEmail({ envelopeId: id, reason });
|
||||
|
||||
await adminSuperDeleteDocument({
|
||||
envelopeId: id,
|
||||
reason,
|
||||
requestMetadata: ctx.metadata.requestMetadata,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,8 +2,8 @@ import { EnvelopeType } from '@prisma/client';
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { jobs } from '@documenso/lib/jobs/client';
|
||||
import { TWO_FACTOR_EMAIL_EXPIRATION_MINUTES } from '@documenso/lib/server-only/2fa/email/constants';
|
||||
import { send2FATokenEmail } from '@documenso/lib/server-only/2fa/email/send-2fa-token-email';
|
||||
import { assertRateLimit } from '@documenso/lib/server-only/rate-limit/rate-limit-middleware';
|
||||
import { request2FAEmailRateLimit } from '@documenso/lib/server-only/rate-limit/rate-limits';
|
||||
import { DocumentAuth } from '@documenso/lib/types/document-auth';
|
||||
@@ -30,8 +30,6 @@ export const accessAuthRequest2FAEmailRoute = procedure
|
||||
|
||||
assertRateLimit(rateLimitResult);
|
||||
|
||||
const user = ctx.user;
|
||||
|
||||
// Get document and recipient by token
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: {
|
||||
@@ -72,18 +70,14 @@ export const accessAuthRequest2FAEmailRoute = procedure
|
||||
});
|
||||
}
|
||||
|
||||
// if (user && recipient.email !== user.email) {
|
||||
// throw new TRPCError({
|
||||
// code: 'UNAUTHORIZED',
|
||||
// message: 'User does not match recipient',
|
||||
// });
|
||||
// }
|
||||
|
||||
const expiresAt = DateTime.now().plus({ minutes: TWO_FACTOR_EMAIL_EXPIRATION_MINUTES });
|
||||
|
||||
await send2FATokenEmail({
|
||||
token,
|
||||
envelopeId: envelope.id,
|
||||
await jobs.triggerJob({
|
||||
name: 'send.2fa.token.email',
|
||||
payload: {
|
||||
envelopeId: envelope.id,
|
||||
recipientId: recipient.id,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user