mirror of
https://github.com/documenso/documenso.git
synced 2026-06-22 04:12:06 +10:00
fix: correctly log cc emails (#2913)
This commit is contained in:
@@ -3,12 +3,13 @@ import DocumentCancelTemplate from '@documenso/email/templates/document-cancel';
|
||||
import { isRecipientEmailValidForSending } from '@documenso/lib/utils/recipients';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { EnvelopeType, ReadStatus, SendStatus, SigningStatus } from '@prisma/client';
|
||||
import { EnvelopeType, ReadStatus, RecipientRole, SendStatus, SigningStatus } from '@prisma/client';
|
||||
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 { unsafeBuildEnvelopeIdQuery } from '../../../utils/envelope';
|
||||
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
|
||||
@@ -32,6 +33,7 @@ export const run = async ({ payload, io }: { payload: TSendDocumentCancelledEmai
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
disabled: true,
|
||||
},
|
||||
},
|
||||
documentMeta: true,
|
||||
@@ -46,17 +48,38 @@ export const run = async ({ payload, io }: { payload: TSendDocumentCancelledEmai
|
||||
},
|
||||
});
|
||||
|
||||
const { branding, emailLanguage, senderEmail, replyToEmail } = await getEmailContext({
|
||||
emailType: 'RECIPIENT',
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId: envelope.teamId,
|
||||
},
|
||||
meta: envelope.documentMeta,
|
||||
});
|
||||
const { branding, emailLanguage, senderEmail, replyToEmail, isOrganisationOwnerDisabled, organisationId, claims } =
|
||||
await getEmailContext({
|
||||
emailType: 'RECIPIENT',
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId: envelope.teamId,
|
||||
},
|
||||
meta: envelope.documentMeta,
|
||||
});
|
||||
|
||||
const { documentMeta, user: documentOwner } = envelope;
|
||||
|
||||
// Don't send cancellation emails on behalf of a disabled (e.g. banned) account.
|
||||
if (isOrganisationOwnerDisabled || documentOwner.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// A recipientCount of 0 means unlimited recipients are allowed.
|
||||
const maximumRecipientCount = claims.recipientCount;
|
||||
|
||||
if (maximumRecipientCount > 0 && envelope.recipients.length > maximumRecipientCount) {
|
||||
io.logger.warn({
|
||||
msg: 'Cancellation email dropped: org recipient limit exceeded',
|
||||
organisationId,
|
||||
recipientCount: envelope.recipients.length,
|
||||
maximumRecipientCount,
|
||||
envelopeId: envelope.id,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if document cancellation emails are enabled
|
||||
const isEmailEnabled = extractDerivedDocumentEmailSettings(documentMeta).documentDeleted;
|
||||
|
||||
@@ -66,9 +89,13 @@ export const run = async ({ payload, io }: { payload: TSendDocumentCancelledEmai
|
||||
|
||||
const i18n = await getI18nInstance(emailLanguage);
|
||||
|
||||
// Send cancellation emails to all recipients who have been sent the document or viewed it
|
||||
// Send cancellation emails to recipients who have been sent the document or viewed it.
|
||||
// CC recipients are excluded because they were never actually emailed about the document
|
||||
// (CC recipients are created with sendStatus=SENT by default but never receive a signing
|
||||
// invitation), so notifying them about a cancellation they never knew about is unsolicited.
|
||||
const recipientsToNotify = envelope.recipients.filter(
|
||||
(recipient) =>
|
||||
recipient.role !== RecipientRole.CC &&
|
||||
(recipient.sendStatus === SendStatus.SENT || recipient.readStatus === ReadStatus.OPENED) &&
|
||||
recipient.signingStatus !== SigningStatus.REJECTED &&
|
||||
isRecipientEmailValidForSending(recipient),
|
||||
@@ -77,6 +104,28 @@ export const run = async ({ payload, io }: { payload: TSendDocumentCancelledEmai
|
||||
await io.runTask('send-cancellation-emails', async () => {
|
||||
await Promise.all(
|
||||
recipientsToNotify.map(async (recipient) => {
|
||||
// Meter the cancellation email against the organisation email quota/stats.
|
||||
// The recipient never opted in, so this notification is unsolicited and
|
||||
// must be bounded by the same org limits as other outbound emails.
|
||||
try {
|
||||
await assertOrganisationRatesAndLimits({
|
||||
organisationId,
|
||||
organisationClaim: claims,
|
||||
type: 'email',
|
||||
count: 1,
|
||||
});
|
||||
} catch (_err) {
|
||||
io.logger.warn({
|
||||
msg: 'Cancellation email dropped: org email limit exceeded',
|
||||
organisationId,
|
||||
recipientId: recipient.id,
|
||||
envelopeId: envelope.id,
|
||||
});
|
||||
|
||||
// On rate/quota exceeded, skip this recipient and continue with the rest.
|
||||
return;
|
||||
}
|
||||
|
||||
const template = createElement(DocumentCancelTemplate, {
|
||||
documentName: envelope.title,
|
||||
inviterName: documentOwner.name || undefined,
|
||||
|
||||
@@ -2,12 +2,13 @@ import { mailer } from '@documenso/email/mailer';
|
||||
import { DocumentCompletedEmailTemplate } from '@documenso/email/templates/document-completed';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { DocumentSource, EnvelopeType } from '@prisma/client';
|
||||
import { DocumentSource, EnvelopeType, RecipientRole } from '@prisma/client';
|
||||
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 { DOCUMENT_AUDIT_LOG_TYPE } from '../../../types/document-audit-logs';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../../types/document-email';
|
||||
import { getFileServerSide } from '../../../universal/upload/get-file.server';
|
||||
@@ -44,6 +45,7 @@ export const run = async ({ payload, io }: { payload: TSendDocumentCompletedEmai
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
disabled: true,
|
||||
},
|
||||
},
|
||||
team: {
|
||||
@@ -65,14 +67,20 @@ export const run = async ({ payload, io }: { payload: TSendDocumentCompletedEmai
|
||||
throw new Error('Document has no recipients');
|
||||
}
|
||||
|
||||
const { branding, emailLanguage, senderEmail, replyToEmail } = await getEmailContext({
|
||||
emailType: 'RECIPIENT',
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId: envelope.teamId,
|
||||
},
|
||||
meta: envelope.documentMeta,
|
||||
});
|
||||
const { branding, emailLanguage, senderEmail, replyToEmail, isOrganisationOwnerDisabled, organisationId, claims } =
|
||||
await getEmailContext({
|
||||
emailType: 'RECIPIENT',
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId: envelope.teamId,
|
||||
},
|
||||
meta: envelope.documentMeta,
|
||||
});
|
||||
|
||||
// Don't send completion emails on behalf of a disabled (e.g. banned) account.
|
||||
if (envelope.user.disabled || isOrganisationOwnerDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { user: owner } = envelope;
|
||||
|
||||
@@ -172,6 +180,30 @@ export const run = async ({ payload, io }: { payload: TSendDocumentCompletedEmai
|
||||
|
||||
await Promise.all(
|
||||
recipientsToNotify.map(async (recipient) => {
|
||||
// A CC recipient never asked to be part of this document, so their completion
|
||||
// email is effectively unsolicited. Meter it against the organisation email
|
||||
// quota/stats so it is correctly logged.
|
||||
if (recipient.role === RecipientRole.CC) {
|
||||
try {
|
||||
await assertOrganisationRatesAndLimits({
|
||||
organisationId,
|
||||
organisationClaim: claims,
|
||||
type: 'email',
|
||||
count: 1,
|
||||
});
|
||||
} catch (_err) {
|
||||
io.logger.warn({
|
||||
msg: 'CC completion email dropped: org email limit exceeded',
|
||||
organisationId,
|
||||
recipientId: recipient.id,
|
||||
envelopeId: envelope.id,
|
||||
});
|
||||
|
||||
// On rate/quota exceeded, early return to allow other recipients to be processed.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const customEmailTemplate = {
|
||||
'signer.name': recipient.name,
|
||||
'signer.email': recipient.email,
|
||||
|
||||
@@ -55,6 +55,11 @@ export const run = async ({ payload, io }: { payload: TSendSigningEmailJobDefini
|
||||
},
|
||||
include: {
|
||||
documentMeta: true,
|
||||
user: {
|
||||
select: {
|
||||
disabled: true,
|
||||
},
|
||||
},
|
||||
team: {
|
||||
select: {
|
||||
teamEmail: true,
|
||||
@@ -84,15 +89,29 @@ export const run = async ({ payload, io }: { payload: TSendSigningEmailJobDefini
|
||||
return;
|
||||
}
|
||||
|
||||
const { branding, emailLanguage, settings, organisationType, senderEmail, replyToEmail, organisationId, claims } =
|
||||
await getEmailContext({
|
||||
emailType: 'RECIPIENT',
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId: envelope.teamId,
|
||||
},
|
||||
meta: envelope.documentMeta,
|
||||
});
|
||||
const {
|
||||
branding,
|
||||
emailLanguage,
|
||||
settings,
|
||||
organisationType,
|
||||
senderEmail,
|
||||
replyToEmail,
|
||||
organisationId,
|
||||
claims,
|
||||
isOrganisationOwnerDisabled,
|
||||
} = await getEmailContext({
|
||||
emailType: 'RECIPIENT',
|
||||
source: {
|
||||
type: 'team',
|
||||
teamId: envelope.teamId,
|
||||
},
|
||||
meta: envelope.documentMeta,
|
||||
});
|
||||
|
||||
// Don't send signing invitations on behalf of a disabled (e.g. banned) account.
|
||||
if (envelope.user.disabled || isOrganisationOwnerDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const customEmail = envelope?.documentMeta;
|
||||
const isDirectTemplate = envelope.source === DocumentSource.TEMPLATE_DIRECT_LINK;
|
||||
|
||||
@@ -17,6 +17,7 @@ import { getI18nInstance } from '../../../client-only/providers/i18n-server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
|
||||
import { RECIPIENT_ROLES_DESCRIPTION } from '../../../constants/recipient-roles';
|
||||
import { getEmailContext } from '../../../server-only/email/get-email-context';
|
||||
import { assertOrganisationRatesAndLimits } from '../../../server-only/rate-limit/assert-organisation-rates-and-limits';
|
||||
import { updateRecipientNextReminder } from '../../../server-only/recipient/update-recipient-next-reminder';
|
||||
import { triggerWebhook } from '../../../server-only/webhooks/trigger/trigger-webhook';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE, DOCUMENT_EMAIL_TYPE } from '../../../types/document-audit-logs';
|
||||
@@ -100,7 +101,16 @@ export const run = async ({ payload, io }: { payload: TProcessSigningReminderJob
|
||||
return;
|
||||
}
|
||||
|
||||
const { branding, emailLanguage, organisationType, senderEmail, replyToEmail } = await getEmailContext({
|
||||
const {
|
||||
branding,
|
||||
emailLanguage,
|
||||
organisationType,
|
||||
senderEmail,
|
||||
replyToEmail,
|
||||
isOrganisationOwnerDisabled,
|
||||
organisationId,
|
||||
claims,
|
||||
} = await getEmailContext({
|
||||
emailType: 'RECIPIENT',
|
||||
source: {
|
||||
type: 'team',
|
||||
@@ -109,6 +119,12 @@ export const run = async ({ payload, io }: { payload: TProcessSigningReminderJob
|
||||
meta: envelope.documentMeta,
|
||||
});
|
||||
|
||||
// Don't send reminders on behalf of a disabled (e.g. banned) account.
|
||||
if (envelope.user.disabled || isOrganisationOwnerDisabled) {
|
||||
io.logger.info(`Envelope ${envelope.id} owner is disabled, skipping reminder`);
|
||||
return;
|
||||
}
|
||||
|
||||
const i18n = await getI18nInstance(emailLanguage);
|
||||
|
||||
const recipientActionVerb = i18n._(RECIPIENT_ROLES_DESCRIPTION[recipient.role].actionVerb).toLowerCase();
|
||||
@@ -139,61 +155,84 @@ export const run = async ({ payload, io }: { payload: TProcessSigningReminderJob
|
||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||
const signDocumentLink = `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`;
|
||||
|
||||
io.logger.info(
|
||||
`Sending signing reminder for envelope ${envelope.id} to recipient ${recipient.id} (${recipient.email})`,
|
||||
);
|
||||
|
||||
const template = createElement(DocumentReminderEmailTemplate, {
|
||||
recipientName: recipient.name,
|
||||
documentName: envelope.title,
|
||||
assetBaseUrl,
|
||||
signDocumentLink,
|
||||
customBody: emailMessage,
|
||||
role: recipient.role,
|
||||
});
|
||||
|
||||
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: emailSubject,
|
||||
html,
|
||||
text,
|
||||
});
|
||||
|
||||
await prisma.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
|
||||
envelopeId: envelope.id,
|
||||
data: {
|
||||
recipientEmail: recipient.email,
|
||||
recipientName: recipient.name,
|
||||
// Meter reminder emails against the organisation email quota/stats. Reminders
|
||||
// are unsolicited (the recipient didn't opt in to them) and can recur, so they
|
||||
// must be bounded by the same org limits as other outbound emails.
|
||||
const isRateLimited = await assertOrganisationRatesAndLimits({
|
||||
organisationId,
|
||||
organisationClaim: claims,
|
||||
type: 'email',
|
||||
count: 1,
|
||||
})
|
||||
.then(() => false)
|
||||
.catch((_err) => {
|
||||
io.logger.warn({
|
||||
msg: 'Signing reminder dropped: org email limit exceeded',
|
||||
organisationId,
|
||||
recipientId: recipient.id,
|
||||
recipientRole: recipient.role,
|
||||
emailType: DOCUMENT_EMAIL_TYPE.REMINDER,
|
||||
isResending: false,
|
||||
},
|
||||
}),
|
||||
});
|
||||
envelopeId: envelope.id,
|
||||
});
|
||||
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.DOCUMENT_REMINDER_SENT,
|
||||
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(envelope)),
|
||||
userId: envelope.userId,
|
||||
teamId: envelope.teamId,
|
||||
});
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!isRateLimited) {
|
||||
io.logger.info(
|
||||
`Sending signing reminder for envelope ${envelope.id} to recipient ${recipient.id} (${recipient.email})`,
|
||||
);
|
||||
|
||||
const template = createElement(DocumentReminderEmailTemplate, {
|
||||
recipientName: recipient.name,
|
||||
documentName: envelope.title,
|
||||
assetBaseUrl,
|
||||
signDocumentLink,
|
||||
customBody: emailMessage,
|
||||
role: recipient.role,
|
||||
});
|
||||
|
||||
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: emailSubject,
|
||||
html,
|
||||
text,
|
||||
});
|
||||
|
||||
await prisma.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
|
||||
envelopeId: envelope.id,
|
||||
data: {
|
||||
recipientEmail: recipient.email,
|
||||
recipientName: recipient.name,
|
||||
recipientId: recipient.id,
|
||||
recipientRole: recipient.role,
|
||||
emailType: DOCUMENT_EMAIL_TYPE.REMINDER,
|
||||
isResending: false,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
await triggerWebhook({
|
||||
event: WebhookTriggerEvents.DOCUMENT_REMINDER_SENT,
|
||||
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(envelope)),
|
||||
userId: envelope.userId,
|
||||
teamId: envelope.teamId,
|
||||
});
|
||||
}
|
||||
|
||||
// Compute the next reminder time (repeat interval).
|
||||
if (recipient.sentAt) {
|
||||
|
||||
@@ -74,6 +74,7 @@ export type EmailContextResponse = {
|
||||
};
|
||||
replyToEmail: string | undefined;
|
||||
emailLanguage: string;
|
||||
isOrganisationOwnerDisabled: boolean;
|
||||
};
|
||||
|
||||
export const getEmailContext = async (options: GetEmailContextOptions): Promise<EmailContextResponse> => {
|
||||
@@ -135,6 +136,11 @@ const handleOrganisationEmailContext = async (organisationId: string) => {
|
||||
id: organisationId,
|
||||
},
|
||||
include: {
|
||||
owner: {
|
||||
select: {
|
||||
disabled: true,
|
||||
},
|
||||
},
|
||||
organisationClaim: true,
|
||||
organisationGlobalSettings: true,
|
||||
emailDomains: {
|
||||
@@ -167,6 +173,7 @@ const handleOrganisationEmailContext = async (organisationId: string) => {
|
||||
claims,
|
||||
organisationId: organisation.id,
|
||||
organisationType: organisation.type,
|
||||
isOrganisationOwnerDisabled: organisation.owner.disabled,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -179,6 +186,12 @@ const handleTeamEmailContext = async (teamId: number) => {
|
||||
teamGlobalSettings: true,
|
||||
organisation: {
|
||||
include: {
|
||||
owner: {
|
||||
select: {
|
||||
id: true,
|
||||
disabled: true,
|
||||
},
|
||||
},
|
||||
organisationClaim: true,
|
||||
organisationGlobalSettings: true,
|
||||
emailDomains: {
|
||||
@@ -212,6 +225,7 @@ const handleTeamEmailContext = async (teamId: number) => {
|
||||
claims,
|
||||
organisationId: organisation.id,
|
||||
organisationType: organisation.type,
|
||||
isOrganisationOwnerDisabled: organisation.owner.disabled,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-log
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { EnvelopeType, SendStatus } from '@prisma/client';
|
||||
import { EnvelopeType, RecipientRole, SendStatus } from '@prisma/client';
|
||||
import { createElement } from 'react';
|
||||
|
||||
import { getI18nInstance } from '../../client-only/providers/i18n-server';
|
||||
@@ -12,11 +12,13 @@ import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
import { logger } from '../../utils/logger';
|
||||
import { canRecipientBeModified, isRecipientEmailValidForSending } from '../../utils/recipients';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
import { getEmailContext } from '../email/get-email-context';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
import { assertOrganisationRatesAndLimits } from '../rate-limit/assert-organisation-rates-and-limits';
|
||||
|
||||
export interface DeleteEnvelopeRecipientOptions {
|
||||
userId: number;
|
||||
@@ -137,6 +139,7 @@ export const deleteEnvelopeRecipient = async ({
|
||||
// Send email to deleted recipient.
|
||||
if (
|
||||
recipientToDelete.sendStatus === SendStatus.SENT &&
|
||||
recipientToDelete.role !== RecipientRole.CC &&
|
||||
isRecipientRemovedEmailEnabled &&
|
||||
envelope.type === EnvelopeType.DOCUMENT &&
|
||||
isRecipientEmailValidForSending(recipientToDelete)
|
||||
@@ -149,7 +152,7 @@ export const deleteEnvelopeRecipient = async ({
|
||||
assetBaseUrl,
|
||||
});
|
||||
|
||||
const { branding, emailLanguage, senderEmail, replyToEmail } = await getEmailContext({
|
||||
const { branding, emailLanguage, senderEmail, replyToEmail, organisationId, claims } = await getEmailContext({
|
||||
emailType: 'RECIPIENT',
|
||||
source: {
|
||||
type: 'team',
|
||||
@@ -158,6 +161,27 @@ export const deleteEnvelopeRecipient = async ({
|
||||
meta: envelope.documentMeta,
|
||||
});
|
||||
|
||||
// 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,
|
||||
});
|
||||
|
||||
return deletedRecipient;
|
||||
}
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, { lang: emailLanguage, branding }),
|
||||
renderEmailWithI18N(template, { lang: emailLanguage, branding, plainText: true }),
|
||||
|
||||
@@ -19,10 +19,12 @@ import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||
import { type EnvelopeIdOptions, mapSecondaryIdToDocumentId } from '../../utils/envelope';
|
||||
import { logger } from '../../utils/logger';
|
||||
import { canRecipientBeModified, 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';
|
||||
import { assertOrganisationRatesAndLimits } from '../rate-limit/assert-organisation-rates-and-limits';
|
||||
|
||||
export interface SetDocumentRecipientsOptions {
|
||||
userId: number;
|
||||
@@ -83,7 +85,7 @@ export const setDocumentRecipients = async ({
|
||||
throw new Error('Document already complete');
|
||||
}
|
||||
|
||||
const { branding, emailLanguage, senderEmail, replyToEmail } = await getEmailContext({
|
||||
const { branding, emailLanguage, senderEmail, replyToEmail, organisationId, claims } = await getEmailContext({
|
||||
emailType: 'RECIPIENT',
|
||||
source: {
|
||||
type: 'team',
|
||||
@@ -287,6 +289,26 @@ export const setDocumentRecipients = async ({
|
||||
return;
|
||||
}
|
||||
|
||||
// Meter against the organisation email quota/stats so add/remove churn
|
||||
// can't be used to send unsolicited "removed" emails outside the 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: recipient.id,
|
||||
envelopeId: envelope.id,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||
|
||||
const template = createElement(RecipientRemovedFromDocumentTemplate, {
|
||||
|
||||
Reference in New Issue
Block a user