fix: correctly log cc emails (#2913)

This commit is contained in:
David Nguyen
2026-06-02 15:04:02 +10:00
committed by GitHub
parent c50a01d004
commit d2f60b13fd
7 changed files with 284 additions and 85 deletions
@@ -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, {