fix: add email disable flag (#2931)

This commit is contained in:
David Nguyen
2026-06-05 17:55:10 +10:00
committed by GitHub
parent 0ecde7ac1e
commit ebf5b75a19
19 changed files with 116 additions and 198 deletions
@@ -126,7 +126,7 @@ const handleDocumentOwnerDelete = async ({ envelope, user, requestMetadata }: Ha
return;
}
const { branding, emailLanguage, senderEmail, replyToEmail } = await getEmailContext({
const { branding, emailLanguage, senderEmail, replyToEmail, emailsDisabled } = await getEmailContext({
emailType: 'RECIPIENT',
source: {
type: 'team',
@@ -187,7 +187,9 @@ const handleDocumentOwnerDelete = async ({ envelope, user, requestMetadata }: Ha
const isEnvelopeDeleteEmailEnabled = extractDerivedDocumentEmailSettings(envelope.documentMeta).documentDeleted;
if (!isEnvelopeDeleteEmailEnabled) {
// Skip sending if the email is disabled for this document or the organisation
// has email sending disabled entirely.
if (!isEnvelopeDeleteEmailEnabled || emailsDisabled) {
return deletedEnvelope;
}
@@ -151,15 +151,28 @@ export const resendDocument = async ({ id, userId, recipients, teamId, requestMe
return envelope;
}
const { branding, emailLanguage, organisationType, senderEmail, replyToEmail, organisationId, claims } =
await getEmailContext({
emailType: 'RECIPIENT',
source: {
type: 'team',
teamId: envelope.teamId,
},
meta: envelope.documentMeta,
});
const {
branding,
emailLanguage,
organisationType,
senderEmail,
replyToEmail,
organisationId,
claims,
emailsDisabled,
} = await getEmailContext({
emailType: 'RECIPIENT',
source: {
type: 'team',
teamId: envelope.teamId,
},
meta: envelope.documentMeta,
});
// Don't resend any emails if the organisation has email sending disabled.
if (user.disabled || emailsDisabled) {
return envelope;
}
// Assert that there is enough quota to send the emails.
await assertOrganisationRatesAndLimits({
@@ -47,7 +47,7 @@ export const sendPendingEmail = async ({ id, recipientId }: SendPendingEmailOpti
throw new Error('Document has no recipients');
}
const { branding, emailLanguage, senderEmail, replyToEmail } = await getEmailContext({
const { branding, emailLanguage, senderEmail, replyToEmail, emailsDisabled } = await getEmailContext({
emailType: 'RECIPIENT',
source: {
type: 'team',
@@ -56,6 +56,11 @@ export const sendPendingEmail = async ({ id, recipientId }: SendPendingEmailOpti
meta: envelope.documentMeta,
});
// Don't send any emails if the organisation has email sending disabled.
if (emailsDisabled) {
return;
}
const isDocumentPendingEmailEnabled = extractDerivedDocumentEmailSettings(envelope.documentMeta).documentPending;
if (!isDocumentPendingEmailEnabled) {
@@ -66,6 +66,12 @@ export type EmailContextResponse = {
branding: BrandingSettings;
settings: Omit<OrganisationGlobalSettings, 'id'>;
claims: OrganisationClaim;
/**
* Whether the organisation is prevented from sending emails.
*
* When true, ALL emails sent on behalf of this organisation must be skipped.
*/
emailsDisabled: boolean;
organisationId: string;
organisationType: OrganisationType;
senderEmail: {
@@ -74,7 +80,6 @@ export type EmailContextResponse = {
};
replyToEmail: string | undefined;
emailLanguage: string;
isOrganisationOwnerDisabled: boolean;
};
export const getEmailContext = async (options: GetEmailContextOptions): Promise<EmailContextResponse> => {
@@ -171,9 +176,9 @@ const handleOrganisationEmailContext = async (organisationId: string) => {
),
settings: organisation.organisationGlobalSettings,
claims,
emailsDisabled: organisation.owner.disabled || claims.flags.disableEmails === true,
organisationId: organisation.id,
organisationType: organisation.type,
isOrganisationOwnerDisabled: organisation.owner.disabled,
};
};
@@ -223,9 +228,9 @@ const handleTeamEmailContext = async (teamId: number) => {
branding: teamGlobalSettingsToBranding(teamSettings, teamId, claims.flags.hidePoweredBy ?? false),
settings: teamSettings,
claims,
emailsDisabled: organisation.owner.disabled || claims.flags.disableEmails === true,
organisationId: organisation.id,
organisationType: organisation.type,
isOrganisationOwnerDisabled: organisation.owner.disabled,
};
};
@@ -187,7 +187,7 @@ export const sendOrganisationMemberInviteEmail = async ({
organisationName: organisation.name,
});
const { branding, emailLanguage, senderEmail } = await getEmailContext({
const { branding, emailLanguage, senderEmail, emailsDisabled } = await getEmailContext({
emailType: 'INTERNAL',
source: {
type: 'organisation',
@@ -195,6 +195,12 @@ export const sendOrganisationMemberInviteEmail = async ({
},
});
// Member invites can be sent to anyone, so block them when the organisation has email
// sending disabled.
if (emailsDisabled) {
return;
}
const [html, text] = await Promise.all([
renderEmailWithI18N(template, {
lang: emailLanguage,
@@ -6,7 +6,6 @@ import { OrganisationMemberRole, OrganisationType, Prisma, type SubscriptionClai
import { IS_BILLING_ENABLED } from '../../constants/app';
import { ORGANISATION_INTERNAL_GROUPS } from '../../constants/organisations';
import { AppError, AppErrorCode } from '../../errors/app-error';
import type { InternalClaim } from '../../types/subscription';
import { INTERNAL_CLAIM_ID } from '../../types/subscription';
import { generateDatabaseId, prefixedId } from '../../universal/id';
import { generateDefaultOrganisationSettings } from '../../utils/organisations';
@@ -18,7 +17,7 @@ type CreateOrganisationOptions = {
type: OrganisationType;
url?: string;
customerId?: string;
claim: InternalClaim;
claim: Omit<SubscriptionClaim, 'createdAt' | 'updatedAt'>;
};
export const createOrganisation = async ({ name, url, type, userId, customerId, claim }: CreateOrganisationOptions) => {
@@ -152,14 +152,20 @@ export const deleteEnvelopeRecipient = async ({
assetBaseUrl,
});
const { branding, emailLanguage, senderEmail, replyToEmail, organisationId, claims } = await getEmailContext({
emailType: 'RECIPIENT',
source: {
type: 'team',
teamId: envelope.teamId,
},
meta: envelope.documentMeta,
});
const { branding, emailLanguage, senderEmail, replyToEmail, organisationId, claims, emailsDisabled } =
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
@@ -85,14 +85,15 @@ export const setDocumentRecipients = async ({
throw new Error('Document already complete');
}
const { branding, emailLanguage, senderEmail, replyToEmail, organisationId, claims } = await getEmailContext({
emailType: 'RECIPIENT',
source: {
type: 'team',
teamId,
},
meta: envelope.documentMeta,
});
const { branding, emailLanguage, senderEmail, replyToEmail, organisationId, claims, emailsDisabled } =
await getEmailContext({
emailType: 'RECIPIENT',
source: {
type: 'team',
teamId,
},
meta: envelope.documentMeta,
});
const recipientsHaveActionAuth = recipients.some(
(recipient) => recipient.actionAuth && recipient.actionAuth.length > 0,
@@ -281,6 +282,7 @@ export const setDocumentRecipients = async ({
await Promise.all(
removedRecipients.map(async (recipient) => {
if (
emailsDisabled ||
recipient.sendStatus !== SendStatus.SENT ||
recipient.role === RecipientRole.CC ||
!isRecipientRemovedEmailEnabled ||
@@ -1,5 +1,3 @@
import { INTERNAL_CLAIM_ID, internalClaims } from '@documenso/lib/types/subscription';
import { prisma } from '@documenso/prisma';
import type { SubscriptionClaim } from '@prisma/client';
import { AppError, AppErrorCode } from '../../errors/app-error';
@@ -12,12 +10,6 @@ export const getSubscriptionClaim = async (
});
if (!subscriptionClaim) {
// Temporary fallback for free claim so we don't break self-hosters who somehow removed it
// from the database.
if (claimId === INTERNAL_CLAIM_ID.FREE) {
return internalClaims[INTERNAL_CLAIM_ID.FREE];
}
throw new AppError(AppErrorCode.NOT_FOUND, {
message: `Subscription claim ${claimId} not found`,
});