mirror of
https://github.com/documenso/documenso.git
synced 2026-07-25 17:35:05 +10:00
Merge branch 'main' into fix/team-permissions
This commit is contained in:
@@ -1,6 +1,5 @@
|
|||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import { jobs } from '@documenso/lib/jobs/client';
|
import { jobs } from '@documenso/lib/jobs/client';
|
||||||
import { sendPendingEmail } from '@documenso/lib/server-only/document/send-pending-email';
|
|
||||||
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
||||||
import { triggerWebhook } from '@documenso/lib/server-only/webhooks/trigger/trigger-webhook';
|
import { triggerWebhook } from '@documenso/lib/server-only/webhooks/trigger/trigger-webhook';
|
||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||||
@@ -474,9 +473,12 @@ export const executeTspSign = async (opts: ExecuteTspSignOptions): Promise<Execu
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (pendingRecipients.length > 0) {
|
if (pendingRecipients.length > 0) {
|
||||||
await sendPendingEmail({
|
await jobs.triggerJob({
|
||||||
id: { type: 'envelopeId', id: envelope.id },
|
name: 'send.document.pending.email',
|
||||||
recipientId: recipient.id,
|
payload: {
|
||||||
|
envelopeId: envelope.id,
|
||||||
|
recipientId: recipient.id,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// TSP envelopes are forced SEQUENTIAL at send-time; this branch always
|
// TSP envelopes are forced SEQUENTIAL at send-time; this branch always
|
||||||
|
|||||||
@@ -4,11 +4,14 @@ import { SEND_CONFIRMATION_EMAIL_JOB_DEFINITION } from './definitions/emails/sen
|
|||||||
import { SEND_DOCUMENT_CANCELLED_EMAILS_JOB_DEFINITION } from './definitions/emails/send-document-cancelled-emails';
|
import { SEND_DOCUMENT_CANCELLED_EMAILS_JOB_DEFINITION } from './definitions/emails/send-document-cancelled-emails';
|
||||||
import { SEND_DOCUMENT_COMPLETED_EMAILS_JOB_DEFINITION } from './definitions/emails/send-document-completed-emails';
|
import { SEND_DOCUMENT_COMPLETED_EMAILS_JOB_DEFINITION } from './definitions/emails/send-document-completed-emails';
|
||||||
import { SEND_DOCUMENT_CREATED_FROM_DIRECT_TEMPLATE_EMAIL_JOB_DEFINITION } from './definitions/emails/send-document-created-from-direct-template-email';
|
import { SEND_DOCUMENT_CREATED_FROM_DIRECT_TEMPLATE_EMAIL_JOB_DEFINITION } from './definitions/emails/send-document-created-from-direct-template-email';
|
||||||
|
import { SEND_DOCUMENT_DELETED_EMAILS_JOB_DEFINITION } from './definitions/emails/send-document-deleted-emails';
|
||||||
|
import { SEND_DOCUMENT_PENDING_EMAIL_JOB_DEFINITION } from './definitions/emails/send-document-pending-email';
|
||||||
import { SEND_ORGANISATION_LIMIT_ALERT_EMAIL_JOB_DEFINITION } from './definitions/emails/send-organisation-limit-alert-email';
|
import { SEND_ORGANISATION_LIMIT_ALERT_EMAIL_JOB_DEFINITION } from './definitions/emails/send-organisation-limit-alert-email';
|
||||||
import { SEND_ORGANISATION_MEMBER_JOINED_EMAIL_JOB_DEFINITION } from './definitions/emails/send-organisation-member-joined-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_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_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_PASSWORD_RESET_SUCCESS_EMAIL_JOB_DEFINITION } from './definitions/emails/send-password-reset-success-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_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_SIGNING_REJECTION_EMAILS_JOB_DEFINITION } from './definitions/emails/send-rejection-emails';
|
||||||
import { SEND_SIGNING_EMAIL_JOB_DEFINITION } from './definitions/emails/send-signing-email';
|
import { SEND_SIGNING_EMAIL_JOB_DEFINITION } from './definitions/emails/send-signing-email';
|
||||||
@@ -44,9 +47,12 @@ export const jobsClient = new JobClient([
|
|||||||
SEND_PASSWORD_RESET_SUCCESS_EMAIL_JOB_DEFINITION,
|
SEND_PASSWORD_RESET_SUCCESS_EMAIL_JOB_DEFINITION,
|
||||||
SEND_SIGNING_REJECTION_EMAILS_JOB_DEFINITION,
|
SEND_SIGNING_REJECTION_EMAILS_JOB_DEFINITION,
|
||||||
SEND_RECIPIENT_SIGNED_EMAIL_JOB_DEFINITION,
|
SEND_RECIPIENT_SIGNED_EMAIL_JOB_DEFINITION,
|
||||||
|
SEND_RECIPIENT_REMOVED_EMAIL_JOB_DEFINITION,
|
||||||
SEND_DOCUMENT_COMPLETED_EMAILS_JOB_DEFINITION,
|
SEND_DOCUMENT_COMPLETED_EMAILS_JOB_DEFINITION,
|
||||||
|
SEND_DOCUMENT_DELETED_EMAILS_JOB_DEFINITION,
|
||||||
SEND_DOCUMENT_CANCELLED_EMAILS_JOB_DEFINITION,
|
SEND_DOCUMENT_CANCELLED_EMAILS_JOB_DEFINITION,
|
||||||
SEND_DOCUMENT_CREATED_FROM_DIRECT_TEMPLATE_EMAIL_JOB_DEFINITION,
|
SEND_DOCUMENT_CREATED_FROM_DIRECT_TEMPLATE_EMAIL_JOB_DEFINITION,
|
||||||
|
SEND_DOCUMENT_PENDING_EMAIL_JOB_DEFINITION,
|
||||||
SEND_OWNER_RECIPIENT_EXPIRED_EMAIL_JOB_DEFINITION,
|
SEND_OWNER_RECIPIENT_EXPIRED_EMAIL_JOB_DEFINITION,
|
||||||
BACKPORT_SUBSCRIPTION_CLAIM_JOB_DEFINITION,
|
BACKPORT_SUBSCRIPTION_CLAIM_JOB_DEFINITION,
|
||||||
BULK_SEND_TEMPLATE_JOB_DEFINITION,
|
BULK_SEND_TEMPLATE_JOB_DEFINITION,
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import DocumentCancelTemplate from '@documenso/email/templates/document-cancel';
|
||||||
|
import { msg } from '@lingui/core/macro';
|
||||||
|
import { createElement } from 'react';
|
||||||
|
|
||||||
|
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 { isRecipientEmailValidForSending } from '../../../utils/recipients';
|
||||||
|
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
|
||||||
|
import type { JobRunIO } from '../../client/_internal/job';
|
||||||
|
import type { TSendDocumentDeletedEmailsJobDefinition } from './send-document-deleted-emails';
|
||||||
|
|
||||||
|
export const run = async ({ payload, io }: { payload: TSendDocumentDeletedEmailsJobDefinition; io: JobRunIO }) => {
|
||||||
|
const { teamId, documentName, inviterName, inviterEmail, meta, recipients } = payload;
|
||||||
|
|
||||||
|
if (recipients.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { branding, emailLanguage, senderEmail, replyToEmail, emailsDisabled, emailTransport } = await getEmailContext({
|
||||||
|
emailType: 'RECIPIENT',
|
||||||
|
source: {
|
||||||
|
type: 'team',
|
||||||
|
teamId,
|
||||||
|
},
|
||||||
|
meta,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Don't send cancellation emails if the organisation has email sending
|
||||||
|
// disabled. Re-checked here (not just at enqueue time) because the org can be
|
||||||
|
// disabled between the delete request and this job running.
|
||||||
|
if (emailsDisabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||||
|
const i18n = await getI18nInstance(emailLanguage);
|
||||||
|
|
||||||
|
for (const recipient of recipients) {
|
||||||
|
await io.runTask(`send-document-deleted-emails-${recipient.email}`, async () => {
|
||||||
|
if (!isRecipientEmailValidForSending(recipient)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const template = createElement(DocumentCancelTemplate, {
|
||||||
|
documentName,
|
||||||
|
inviterName: inviterName || undefined,
|
||||||
|
inviterEmail,
|
||||||
|
assetBaseUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [html, text] = await Promise.all([
|
||||||
|
renderEmailWithI18N(template, { lang: emailLanguage, branding }),
|
||||||
|
renderEmailWithI18N(template, { lang: emailLanguage, branding, plainText: true }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await emailTransport.sendMail({
|
||||||
|
to: {
|
||||||
|
address: recipient.email,
|
||||||
|
name: recipient.name,
|
||||||
|
},
|
||||||
|
from: senderEmail,
|
||||||
|
replyTo: replyToEmail,
|
||||||
|
subject: i18n._(msg`Document Cancelled`),
|
||||||
|
html,
|
||||||
|
text,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import type { JobDefinition } from '../../client/_internal/job';
|
||||||
|
|
||||||
|
const SEND_DOCUMENT_DELETED_EMAILS_JOB_DEFINITION_ID = 'send.document.deleted.emails';
|
||||||
|
|
||||||
|
const SEND_DOCUMENT_DELETED_EMAILS_JOB_DEFINITION_SCHEMA = z.object({
|
||||||
|
teamId: z.number(),
|
||||||
|
documentName: z.string(),
|
||||||
|
inviterName: z.string().optional(),
|
||||||
|
inviterEmail: z.string(),
|
||||||
|
/**
|
||||||
|
* The document's email meta (sender, reply-to, language). Captured before the
|
||||||
|
* envelope is hard-deleted so `getEmailContext` resolves the exact same
|
||||||
|
* sender/reply-to/language the inline send used.
|
||||||
|
*/
|
||||||
|
meta: z
|
||||||
|
.object({
|
||||||
|
emailId: z.string().nullable().optional(),
|
||||||
|
emailReplyTo: z.string().nullable().optional(),
|
||||||
|
language: z.string().optional(),
|
||||||
|
})
|
||||||
|
.nullable(),
|
||||||
|
recipients: z
|
||||||
|
.object({
|
||||||
|
email: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
})
|
||||||
|
.array(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TSendDocumentDeletedEmailsJobDefinition = z.infer<
|
||||||
|
typeof SEND_DOCUMENT_DELETED_EMAILS_JOB_DEFINITION_SCHEMA
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const SEND_DOCUMENT_DELETED_EMAILS_JOB_DEFINITION = {
|
||||||
|
id: SEND_DOCUMENT_DELETED_EMAILS_JOB_DEFINITION_ID,
|
||||||
|
name: 'Send Document Deleted Emails',
|
||||||
|
version: '1.0.0',
|
||||||
|
trigger: {
|
||||||
|
name: SEND_DOCUMENT_DELETED_EMAILS_JOB_DEFINITION_ID,
|
||||||
|
schema: SEND_DOCUMENT_DELETED_EMAILS_JOB_DEFINITION_SCHEMA,
|
||||||
|
},
|
||||||
|
handler: async ({ payload, io }) => {
|
||||||
|
const handler = await import('./send-document-deleted-emails.handler');
|
||||||
|
|
||||||
|
await handler.run({ payload, io });
|
||||||
|
},
|
||||||
|
} as const satisfies JobDefinition<
|
||||||
|
typeof SEND_DOCUMENT_DELETED_EMAILS_JOB_DEFINITION_ID,
|
||||||
|
TSendDocumentDeletedEmailsJobDefinition
|
||||||
|
>;
|
||||||
+14
-21
@@ -1,27 +1,24 @@
|
|||||||
import { DocumentPendingEmailTemplate } from '@documenso/email/templates/document-pending';
|
import { DocumentPendingEmailTemplate } from '@documenso/email/templates/document-pending';
|
||||||
|
import { unsafeBuildEnvelopeIdQuery } from '@documenso/lib/utils/envelope';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { msg } from '@lingui/core/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { EnvelopeType } from '@prisma/client';
|
import { EnvelopeType } from '@prisma/client';
|
||||||
import { createElement } from 'react';
|
import { createElement } from 'react';
|
||||||
|
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 { isRecipientEmailValidForSending } from '../../../utils/recipients';
|
||||||
|
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
|
||||||
|
import type { JobRunIO } from '../../client/_internal/job';
|
||||||
|
import type { TSendDocumentPendingEmailJobDefinition } from './send-document-pending-email';
|
||||||
|
|
||||||
import { getI18nInstance } from '../../client-only/providers/i18n-server';
|
export const run = async ({ payload }: { payload: TSendDocumentPendingEmailJobDefinition; io: JobRunIO }) => {
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
const { envelopeId, recipientId } = payload;
|
||||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
|
||||||
import type { EnvelopeIdOptions } from '../../utils/envelope';
|
|
||||||
import { unsafeBuildEnvelopeIdQuery } from '../../utils/envelope';
|
|
||||||
import { isRecipientEmailValidForSending } from '../../utils/recipients';
|
|
||||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
|
||||||
import { getEmailContext } from '../email/get-email-context';
|
|
||||||
|
|
||||||
export interface SendPendingEmailOptions {
|
|
||||||
id: EnvelopeIdOptions;
|
|
||||||
recipientId: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const sendPendingEmail = async ({ id, recipientId }: SendPendingEmailOptions) => {
|
|
||||||
const envelope = await prisma.envelope.findFirst({
|
const envelope = await prisma.envelope.findFirst({
|
||||||
where: {
|
where: {
|
||||||
...unsafeBuildEnvelopeIdQuery(id, EnvelopeType.DOCUMENT),
|
...unsafeBuildEnvelopeIdQuery({ type: 'envelopeId', id: envelopeId }, EnvelopeType.DOCUMENT),
|
||||||
recipients: {
|
recipients: {
|
||||||
some: {
|
some: {
|
||||||
id: recipientId,
|
id: recipientId,
|
||||||
@@ -38,12 +35,8 @@ export const sendPendingEmail = async ({ id, recipientId }: SendPendingEmailOpti
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!envelope) {
|
if (!envelope || envelope.recipients.length === 0) {
|
||||||
throw new Error('Document not found');
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
if (envelope.recipients.length === 0) {
|
|
||||||
throw new Error('Document has no recipients');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { branding, emailLanguage, senderEmail, replyToEmail, emailsDisabled, emailTransport } = await getEmailContext({
|
const { branding, emailLanguage, senderEmail, replyToEmail, emailsDisabled, emailTransport } = await getEmailContext({
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import type { JobDefinition } from '../../client/_internal/job';
|
||||||
|
|
||||||
|
const SEND_DOCUMENT_PENDING_EMAIL_JOB_DEFINITION_ID = 'send.document.pending.email';
|
||||||
|
|
||||||
|
const SEND_DOCUMENT_PENDING_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
|
||||||
|
envelopeId: z.string(),
|
||||||
|
recipientId: z.number(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TSendDocumentPendingEmailJobDefinition = z.infer<typeof SEND_DOCUMENT_PENDING_EMAIL_JOB_DEFINITION_SCHEMA>;
|
||||||
|
|
||||||
|
export const SEND_DOCUMENT_PENDING_EMAIL_JOB_DEFINITION = {
|
||||||
|
id: SEND_DOCUMENT_PENDING_EMAIL_JOB_DEFINITION_ID,
|
||||||
|
name: 'Send Document Pending Email',
|
||||||
|
version: '1.0.0',
|
||||||
|
trigger: {
|
||||||
|
name: SEND_DOCUMENT_PENDING_EMAIL_JOB_DEFINITION_ID,
|
||||||
|
schema: SEND_DOCUMENT_PENDING_EMAIL_JOB_DEFINITION_SCHEMA,
|
||||||
|
},
|
||||||
|
handler: async ({ payload, io }) => {
|
||||||
|
const handler = await import('./send-document-pending-email.handler');
|
||||||
|
|
||||||
|
await handler.run({ payload, io });
|
||||||
|
},
|
||||||
|
} as const satisfies JobDefinition<
|
||||||
|
typeof SEND_DOCUMENT_PENDING_EMAIL_JOB_DEFINITION_ID,
|
||||||
|
TSendDocumentPendingEmailJobDefinition
|
||||||
|
>;
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import RecipientRemovedFromDocumentTemplate from '@documenso/email/templates/recipient-removed-from-document';
|
||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
|
import { msg } from '@lingui/core/macro';
|
||||||
|
import { createElement } from 'react';
|
||||||
|
|
||||||
|
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 { assertOrganisationRatesAndLimits } from '../../../server-only/rate-limit/assert-organisation-rates-and-limits';
|
||||||
|
import { extractDerivedDocumentEmailSettings } from '../../../types/document-email';
|
||||||
|
import { isRecipientEmailValidForSending } from '../../../utils/recipients';
|
||||||
|
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
|
||||||
|
import type { JobRunIO } from '../../client/_internal/job';
|
||||||
|
import type { TSendRecipientRemovedEmailJobDefinition } from './send-recipient-removed-email';
|
||||||
|
|
||||||
|
export const run = async ({ payload, io }: { payload: TSendRecipientRemovedEmailJobDefinition; io: JobRunIO }) => {
|
||||||
|
const { envelopeId, recipientEmail, recipientName, inviterName } = payload;
|
||||||
|
|
||||||
|
const envelope = await prisma.envelope.findFirst({
|
||||||
|
where: {
|
||||||
|
id: envelopeId,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
documentMeta: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// The envelope may have been deleted between the recipient removal and this
|
||||||
|
// job running. Treat as a no-op so the job doesn't retry forever.
|
||||||
|
if (!envelope || !recipientEmail || !isRecipientEmailValidForSending({ email: recipientEmail })) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-checked at send time (not just at enqueue) so the email honors the
|
||||||
|
// document's current "recipient removed" setting.
|
||||||
|
const isRecipientRemovedEmailEnabled = extractDerivedDocumentEmailSettings(envelope.documentMeta).recipientRemoved;
|
||||||
|
|
||||||
|
if (!isRecipientRemovedEmailEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { branding, emailLanguage, senderEmail, replyToEmail, organisationId, claims, emailsDisabled, emailTransport } =
|
||||||
|
await getEmailContext({
|
||||||
|
emailType: 'RECIPIENT',
|
||||||
|
source: {
|
||||||
|
type: 'team',
|
||||||
|
teamId: envelope.teamId,
|
||||||
|
},
|
||||||
|
meta: envelope.documentMeta,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Don't send the removal email if the organisation has email sending disabled.
|
||||||
|
if (emailsDisabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Meter the removal email against the organisation email quota/stats.
|
||||||
|
// Add/remove churn can be used to blast unsolicited removal emails outside
|
||||||
|
// the email limits.
|
||||||
|
try {
|
||||||
|
await assertOrganisationRatesAndLimits({
|
||||||
|
organisationId,
|
||||||
|
organisationClaim: claims,
|
||||||
|
type: 'email',
|
||||||
|
count: 1,
|
||||||
|
});
|
||||||
|
} catch (_err) {
|
||||||
|
io.logger.warn({
|
||||||
|
msg: 'Recipient removed email dropped: org email limit exceeded',
|
||||||
|
organisationId,
|
||||||
|
envelopeId: envelope.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||||
|
|
||||||
|
const template = createElement(RecipientRemovedFromDocumentTemplate, {
|
||||||
|
documentName: envelope.title,
|
||||||
|
inviterName: inviterName || undefined,
|
||||||
|
assetBaseUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
const i18n = await getI18nInstance(emailLanguage);
|
||||||
|
|
||||||
|
await io.runTask('send-recipient-removed-email', async () => {
|
||||||
|
const [html, text] = await Promise.all([
|
||||||
|
renderEmailWithI18N(template, { lang: emailLanguage, branding }),
|
||||||
|
renderEmailWithI18N(template, { lang: emailLanguage, branding, plainText: true }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await emailTransport.sendMail({
|
||||||
|
to: {
|
||||||
|
address: recipientEmail,
|
||||||
|
name: recipientName,
|
||||||
|
},
|
||||||
|
from: senderEmail,
|
||||||
|
replyTo: replyToEmail,
|
||||||
|
subject: i18n._(msg`You have been removed from a document`),
|
||||||
|
html,
|
||||||
|
text,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import type { JobDefinition } from '../../client/_internal/job';
|
||||||
|
|
||||||
|
const SEND_RECIPIENT_REMOVED_EMAIL_JOB_DEFINITION_ID = 'send.recipient.removed.email';
|
||||||
|
|
||||||
|
const SEND_RECIPIENT_REMOVED_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
|
||||||
|
envelopeId: z.string(),
|
||||||
|
recipientEmail: z.string(),
|
||||||
|
recipientName: z.string(),
|
||||||
|
inviterName: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TSendRecipientRemovedEmailJobDefinition = z.infer<
|
||||||
|
typeof SEND_RECIPIENT_REMOVED_EMAIL_JOB_DEFINITION_SCHEMA
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const SEND_RECIPIENT_REMOVED_EMAIL_JOB_DEFINITION = {
|
||||||
|
id: SEND_RECIPIENT_REMOVED_EMAIL_JOB_DEFINITION_ID,
|
||||||
|
name: 'Send Recipient Removed Email',
|
||||||
|
version: '1.0.0',
|
||||||
|
trigger: {
|
||||||
|
name: SEND_RECIPIENT_REMOVED_EMAIL_JOB_DEFINITION_ID,
|
||||||
|
schema: SEND_RECIPIENT_REMOVED_EMAIL_JOB_DEFINITION_SCHEMA,
|
||||||
|
},
|
||||||
|
handler: async ({ payload, io }) => {
|
||||||
|
const handler = await import('./send-recipient-removed-email.handler');
|
||||||
|
|
||||||
|
await handler.run({ payload, io });
|
||||||
|
},
|
||||||
|
} as const satisfies JobDefinition<
|
||||||
|
typeof SEND_RECIPIENT_REMOVED_EMAIL_JOB_DEFINITION_ID,
|
||||||
|
TSendRecipientRemovedEmailJobDefinition
|
||||||
|
>;
|
||||||
@@ -29,7 +29,6 @@ import { assertRecipientNotExpired } from '../../utils/recipients';
|
|||||||
import { getIsRecipientsTurnToSign } from '../recipient/get-is-recipient-turn';
|
import { getIsRecipientsTurnToSign } from '../recipient/get-is-recipient-turn';
|
||||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||||
import { isRecipientAuthorized } from './is-recipient-authorized';
|
import { isRecipientAuthorized } from './is-recipient-authorized';
|
||||||
import { sendPendingEmail } from './send-pending-email';
|
|
||||||
|
|
||||||
export type CompleteDocumentWithTokenOptions = {
|
export type CompleteDocumentWithTokenOptions = {
|
||||||
token: string;
|
token: string;
|
||||||
@@ -389,7 +388,13 @@ export const completeDocumentWithToken = async ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (pendingRecipients.length > 0) {
|
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) {
|
if (envelope.documentMeta?.signingOrder === DocumentSigningOrder.SEQUENTIAL) {
|
||||||
const [nextRecipient] = pendingRecipients;
|
const [nextRecipient] = pendingRecipients;
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import DocumentCancelTemplate from '@documenso/email/templates/document-cancel';
|
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { msg } from '@lingui/core/macro';
|
|
||||||
import type { DocumentMeta, Envelope, Recipient, User } from '@prisma/client';
|
import type { DocumentMeta, Envelope, Recipient, User } from '@prisma/client';
|
||||||
import { DocumentStatus, EnvelopeType, RecipientRole, SendStatus, WebhookTriggerEvents } from '@prisma/client';
|
import { DocumentStatus, EnvelopeType, RecipientRole, SendStatus, WebhookTriggerEvents } from '@prisma/client';
|
||||||
import { createElement } from 'react';
|
|
||||||
|
|
||||||
import { getI18nInstance } from '../../client-only/providers/i18n-server';
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
|
||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||||
|
import { jobs } from '../../jobs/client';
|
||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||||
import { mapEnvelopeToWebhookDocumentPayload, ZWebhookDocumentSchema } from '../../types/webhook-payload';
|
import { mapEnvelopeToWebhookDocumentPayload, ZWebhookDocumentSchema } from '../../types/webhook-payload';
|
||||||
@@ -16,7 +12,6 @@ import { isDocumentCompleted } from '../../utils/document';
|
|||||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||||
import { type EnvelopeIdOptions, unsafeBuildEnvelopeIdQuery } from '../../utils/envelope';
|
import { type EnvelopeIdOptions, unsafeBuildEnvelopeIdQuery } from '../../utils/envelope';
|
||||||
import { isRecipientEmailValidForSending } from '../../utils/recipients';
|
import { isRecipientEmailValidForSending } from '../../utils/recipients';
|
||||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
|
||||||
import { getEmailContext } from '../email/get-email-context';
|
import { getEmailContext } from '../email/get-email-context';
|
||||||
import { getMemberRoles } from '../team/get-member-roles';
|
import { getMemberRoles } from '../team/get-member-roles';
|
||||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||||
@@ -125,7 +120,7 @@ const handleDocumentOwnerDelete = async ({ envelope, user, requestMetadata }: Ha
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { branding, emailLanguage, senderEmail, replyToEmail, emailsDisabled, emailTransport } = await getEmailContext({
|
const { emailLanguage, emailsDisabled } = await getEmailContext({
|
||||||
emailType: 'RECIPIENT',
|
emailType: 'RECIPIENT',
|
||||||
source: {
|
source: {
|
||||||
type: 'team',
|
type: 'team',
|
||||||
@@ -192,50 +187,40 @@ const handleDocumentOwnerDelete = async ({ envelope, user, requestMetadata }: Ha
|
|||||||
return deletedEnvelope;
|
return deletedEnvelope;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send cancellation emails to recipients.
|
// Enqueue cancellation emails as a background job. The envelope (and its
|
||||||
await Promise.all(
|
// documentMeta) is hard-deleted above, so the job can't look it up later —
|
||||||
envelope.recipients.map(async (recipient) => {
|
// pass a self-contained payload with the recipients to notify.
|
||||||
if (
|
const recipientsToNotify = envelope.recipients
|
||||||
recipient.sendStatus !== SendStatus.SENT ||
|
.filter(
|
||||||
!isRecipientEmailValidForSending(recipient) ||
|
(recipient) =>
|
||||||
recipient.role === RecipientRole.CC
|
recipient.sendStatus === SendStatus.SENT &&
|
||||||
) {
|
recipient.role !== RecipientRole.CC &&
|
||||||
return;
|
isRecipientEmailValidForSending(recipient),
|
||||||
}
|
)
|
||||||
|
.map((recipient) => ({
|
||||||
|
email: recipient.email,
|
||||||
|
name: recipient.name,
|
||||||
|
}));
|
||||||
|
|
||||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
if (recipientsToNotify.length > 0) {
|
||||||
|
await jobs.triggerJob({
|
||||||
const template = createElement(DocumentCancelTemplate, {
|
name: 'send.document.deleted.emails',
|
||||||
|
payload: {
|
||||||
|
teamId: envelope.teamId,
|
||||||
documentName: envelope.title,
|
documentName: envelope.title,
|
||||||
inviterName: user.name || undefined,
|
inviterName: user.name || undefined,
|
||||||
inviterEmail: user.email,
|
inviterEmail: user.email,
|
||||||
assetBaseUrl,
|
meta: envelope.documentMeta
|
||||||
});
|
? {
|
||||||
|
emailId: envelope.documentMeta.emailId,
|
||||||
const [html, text] = await Promise.all([
|
emailReplyTo: envelope.documentMeta.emailReplyTo,
|
||||||
renderEmailWithI18N(template, { lang: emailLanguage, branding }),
|
language: emailLanguage,
|
||||||
renderEmailWithI18N(template, {
|
}
|
||||||
lang: emailLanguage,
|
: null,
|
||||||
branding,
|
recipients: recipientsToNotify,
|
||||||
plainText: true,
|
},
|
||||||
}),
|
});
|
||||||
]);
|
}
|
||||||
|
|
||||||
const i18n = await getI18nInstance(emailLanguage);
|
|
||||||
|
|
||||||
await emailTransport.sendMail({
|
|
||||||
to: {
|
|
||||||
address: recipient.email,
|
|
||||||
name: recipient.name,
|
|
||||||
},
|
|
||||||
from: senderEmail,
|
|
||||||
replyTo: replyToEmail,
|
|
||||||
subject: i18n._(msg`Document Cancelled`),
|
|
||||||
html,
|
|
||||||
text,
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return deletedEnvelope;
|
return deletedEnvelope;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,24 +1,16 @@
|
|||||||
import RecipientRemovedFromDocumentTemplate from '@documenso/email/templates/recipient-removed-from-document';
|
|
||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { msg } from '@lingui/core/macro';
|
|
||||||
import { EnvelopeType, RecipientRole, SendStatus } from '@prisma/client';
|
import { EnvelopeType, RecipientRole, SendStatus } from '@prisma/client';
|
||||||
import { createElement } from 'react';
|
|
||||||
|
|
||||||
import { getI18nInstance } from '../../client-only/providers/i18n-server';
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
|
||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||||
|
import { jobs } from '../../jobs/client';
|
||||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||||
import { logger } from '../../utils/logger';
|
|
||||||
import { canRecipientBeModified, isRecipientEmailValidForSending } from '../../utils/recipients';
|
import { canRecipientBeModified, isRecipientEmailValidForSending } from '../../utils/recipients';
|
||||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
|
||||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||||
import { getEmailContext } from '../email/get-email-context';
|
|
||||||
import { assertEnvelopeMutable } from '../envelope/assert-envelope-mutable';
|
import { assertEnvelopeMutable } from '../envelope/assert-envelope-mutable';
|
||||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||||
import { assertOrganisationRatesAndLimits } from '../rate-limit/assert-organisation-rates-and-limits';
|
|
||||||
|
|
||||||
export interface DeleteEnvelopeRecipientOptions {
|
export interface DeleteEnvelopeRecipientOptions {
|
||||||
userId: number;
|
userId: number;
|
||||||
@@ -148,75 +140,16 @@ export const deleteEnvelopeRecipient = async ({
|
|||||||
envelope.type === EnvelopeType.DOCUMENT &&
|
envelope.type === EnvelopeType.DOCUMENT &&
|
||||||
isRecipientEmailValidForSending(recipientToDelete)
|
isRecipientEmailValidForSending(recipientToDelete)
|
||||||
) {
|
) {
|
||||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
// Enqueue the "removed from document" email as a background job so a
|
||||||
|
// transient mail outage doesn't fail the request and the send is retried.
|
||||||
const template = createElement(RecipientRemovedFromDocumentTemplate, {
|
await jobs.triggerJob({
|
||||||
documentName: envelope.title,
|
name: 'send.recipient.removed.email',
|
||||||
inviterName: envelope.team?.name || user.name || undefined,
|
payload: {
|
||||||
assetBaseUrl,
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
branding,
|
|
||||||
emailLanguage,
|
|
||||||
senderEmail,
|
|
||||||
replyToEmail,
|
|
||||||
organisationId,
|
|
||||||
claims,
|
|
||||||
emailsDisabled,
|
|
||||||
emailTransport,
|
|
||||||
} = await getEmailContext({
|
|
||||||
emailType: 'RECIPIENT',
|
|
||||||
source: {
|
|
||||||
type: 'team',
|
|
||||||
teamId: envelope.teamId,
|
|
||||||
},
|
|
||||||
meta: envelope.documentMeta,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Don't send the removal email if the organisation has email sending disabled.
|
|
||||||
if (emailsDisabled) {
|
|
||||||
return deletedRecipient;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Meter the removal email against the organisation email quota/stats.
|
|
||||||
// Add/remove churn can be used to blast unsolicited removal emails
|
|
||||||
// outside the email limits.
|
|
||||||
try {
|
|
||||||
await assertOrganisationRatesAndLimits({
|
|
||||||
organisationId,
|
|
||||||
organisationClaim: claims,
|
|
||||||
type: 'email',
|
|
||||||
count: 1,
|
|
||||||
});
|
|
||||||
} catch (_err) {
|
|
||||||
logger.warn({
|
|
||||||
msg: 'Recipient removed email dropped: org email limit exceeded',
|
|
||||||
organisationId,
|
|
||||||
recipientId: recipientToDelete.id,
|
|
||||||
envelopeId: envelope.id,
|
envelopeId: envelope.id,
|
||||||
});
|
recipientEmail: recipientToDelete.email,
|
||||||
|
recipientName: recipientToDelete.name,
|
||||||
return deletedRecipient;
|
inviterName: envelope.team?.name || user.name || undefined,
|
||||||
}
|
|
||||||
|
|
||||||
const [html, text] = await Promise.all([
|
|
||||||
renderEmailWithI18N(template, { lang: emailLanguage, branding }),
|
|
||||||
renderEmailWithI18N(template, { lang: emailLanguage, branding, plainText: true }),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const i18n = await getI18nInstance(emailLanguage);
|
|
||||||
|
|
||||||
await emailTransport.sendMail({
|
|
||||||
to: {
|
|
||||||
address: recipientToDelete.email,
|
|
||||||
name: recipientToDelete.name,
|
|
||||||
},
|
},
|
||||||
from: senderEmail,
|
|
||||||
replyTo: replyToEmail,
|
|
||||||
subject: i18n._(msg`You have been removed from a document`),
|
|
||||||
html,
|
|
||||||
text,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import RecipientRemovedFromDocumentTemplate from '@documenso/email/templates/recipient-removed-from-document';
|
|
||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||||
import type { TRecipientAccessAuthTypes } from '@documenso/lib/types/document-auth';
|
import type { TRecipientAccessAuthTypes } from '@documenso/lib/types/document-auth';
|
||||||
import { type TRecipientActionAuthTypes, ZRecipientAuthOptionsSchema } from '@documenso/lib/types/document-auth';
|
import { type TRecipientActionAuthTypes, ZRecipientAuthOptionsSchema } from '@documenso/lib/types/document-auth';
|
||||||
@@ -7,25 +6,18 @@ import { nanoid } from '@documenso/lib/universal/id';
|
|||||||
import { createDocumentAuditLogData, diffRecipientChanges } from '@documenso/lib/utils/document-audit-logs';
|
import { createDocumentAuditLogData, diffRecipientChanges } from '@documenso/lib/utils/document-audit-logs';
|
||||||
import { createRecipientAuthOptions } from '@documenso/lib/utils/document-auth';
|
import { createRecipientAuthOptions } from '@documenso/lib/utils/document-auth';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { msg } from '@lingui/core/macro';
|
|
||||||
import type { Recipient } from '@prisma/client';
|
import type { Recipient } from '@prisma/client';
|
||||||
import { EnvelopeType, RecipientRole, SendStatus, SigningStatus } from '@prisma/client';
|
import { EnvelopeType, RecipientRole, SendStatus, SigningStatus } from '@prisma/client';
|
||||||
import { createElement } from 'react';
|
|
||||||
import { isDeepEqual } from 'remeda';
|
import { isDeepEqual } from 'remeda';
|
||||||
|
|
||||||
import { getI18nInstance } from '../../client-only/providers/i18n-server';
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
|
||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||||
|
import { jobs } from '../../jobs/client';
|
||||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||||
import { type EnvelopeIdOptions, mapSecondaryIdToDocumentId } from '../../utils/envelope';
|
import { type EnvelopeIdOptions, mapSecondaryIdToDocumentId } from '../../utils/envelope';
|
||||||
import { logger } from '../../utils/logger';
|
|
||||||
import { canRecipientBeModified, isRecipientEmailValidForSending } from '../../utils/recipients';
|
import { canRecipientBeModified, isRecipientEmailValidForSending } from '../../utils/recipients';
|
||||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
|
||||||
import { getEmailContext } from '../email/get-email-context';
|
|
||||||
import { assertEnvelopeMutable } from '../envelope/assert-envelope-mutable';
|
import { assertEnvelopeMutable } from '../envelope/assert-envelope-mutable';
|
||||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||||
import { assertCompatibleRecipientRole } from '../signature-level/assert-compatible-recipient-role';
|
import { assertCompatibleRecipientRole } from '../signature-level/assert-compatible-recipient-role';
|
||||||
import { assertOrganisationRatesAndLimits } from '../rate-limit/assert-organisation-rates-and-limits';
|
|
||||||
|
|
||||||
export interface SetDocumentRecipientsOptions {
|
export interface SetDocumentRecipientsOptions {
|
||||||
userId: number;
|
userId: number;
|
||||||
@@ -88,16 +80,6 @@ export const setDocumentRecipients = async ({
|
|||||||
throw new Error('Document already complete');
|
throw new Error('Document already complete');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { branding, emailLanguage, senderEmail, replyToEmail, organisationId, claims, emailsDisabled, emailTransport } =
|
|
||||||
await getEmailContext({
|
|
||||||
emailType: 'RECIPIENT',
|
|
||||||
source: {
|
|
||||||
type: 'team',
|
|
||||||
teamId,
|
|
||||||
},
|
|
||||||
meta: envelope.documentMeta,
|
|
||||||
});
|
|
||||||
|
|
||||||
const recipientsHaveActionAuth = recipients.some(
|
const recipientsHaveActionAuth = recipients.some(
|
||||||
(recipient) => recipient.actionAuth && recipient.actionAuth.length > 0,
|
(recipient) => recipient.actionAuth && recipient.actionAuth.length > 0,
|
||||||
);
|
);
|
||||||
@@ -290,67 +272,29 @@ export const setDocumentRecipients = async ({
|
|||||||
|
|
||||||
const isRecipientRemovedEmailEnabled = extractDerivedDocumentEmailSettings(envelope.documentMeta).recipientRemoved;
|
const isRecipientRemovedEmailEnabled = extractDerivedDocumentEmailSettings(envelope.documentMeta).recipientRemoved;
|
||||||
|
|
||||||
// Send emails to deleted recipients who have emails.
|
if (isRecipientRemovedEmailEnabled) {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
removedRecipients.map(async (recipient) => {
|
removedRecipients.map(async (recipient) => {
|
||||||
if (
|
if (
|
||||||
emailsDisabled ||
|
recipient.sendStatus !== SendStatus.SENT ||
|
||||||
recipient.sendStatus !== SendStatus.SENT ||
|
recipient.role === RecipientRole.CC ||
|
||||||
recipient.role === RecipientRole.CC ||
|
!isRecipientEmailValidForSending(recipient)
|
||||||
!isRecipientRemovedEmailEnabled ||
|
) {
|
||||||
!isRecipientEmailValidForSending(recipient)
|
return;
|
||||||
) {
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Meter against the organisation email quota/stats so add/remove churn
|
await jobs.triggerJob({
|
||||||
// can't be used to send unsolicited "removed" emails outside the limits.
|
name: 'send.recipient.removed.email',
|
||||||
try {
|
payload: {
|
||||||
await assertOrganisationRatesAndLimits({
|
envelopeId: envelope.id,
|
||||||
organisationId,
|
recipientEmail: recipient.email,
|
||||||
organisationClaim: claims,
|
recipientName: recipient.name,
|
||||||
type: 'email',
|
inviterName: user.name || undefined,
|
||||||
count: 1,
|
},
|
||||||
});
|
});
|
||||||
} catch (_err) {
|
}),
|
||||||
logger.warn({
|
);
|
||||||
msg: 'Recipient removed email dropped: org email limit exceeded',
|
}
|
||||||
organisationId,
|
|
||||||
recipientId: recipient.id,
|
|
||||||
envelopeId: envelope.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
|
||||||
|
|
||||||
const template = createElement(RecipientRemovedFromDocumentTemplate, {
|
|
||||||
documentName: envelope.title,
|
|
||||||
inviterName: user.name || undefined,
|
|
||||||
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 emailTransport.sendMail({
|
|
||||||
to: {
|
|
||||||
address: recipient.email,
|
|
||||||
name: recipient.name,
|
|
||||||
},
|
|
||||||
from: senderEmail,
|
|
||||||
replyTo: replyToEmail,
|
|
||||||
subject: i18n._(msg`You have been removed from a document`),
|
|
||||||
html,
|
|
||||||
text,
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter out recipients that have been removed or have been updated.
|
// Filter out recipients that have been removed or have been updated.
|
||||||
|
|||||||
+256
-255
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user