diff --git a/apps/docs/content/docs/developers/api/documents.mdx b/apps/docs/content/docs/developers/api/documents.mdx index bd526e828..605fd639d 100644 --- a/apps/docs/content/docs/developers/api/documents.mdx +++ b/apps/docs/content/docs/developers/api/documents.mdx @@ -569,6 +569,32 @@ const document = await response.json(); + +### Omit document PDFs from email + +Set `meta.emailSettings.attachDocument` to `false` when creating or updating a document or template. Completion emails still go to the enabled recipients and owner, and retain their document links. This also omits PDFs from CC completion emails. + +| Parameter | Type | Default | Description | +| --------- | ---- | ------- | ----------- | +| `[meta.emailSettings.attachDocument]` | boolean | `true` | Attach completed document PDFs to emails. Set to `false` for links only. | + +For example, update an envelope with this request body: + +```json +{ + "envelopeId": "envelope_abc123", + "meta": { + "emailSettings": { + "attachDocument": false + } + } +} +``` + +When using a template with `/envelope/use`, pass the option in `override.emailSettings`. The document and template APIs, including API v1, accept the same boolean option in their email settings. Non-boolean values are rejected. + +If you omit the email settings on creation, the document inherits the team's defaults, which can inherit from the organisation. Existing documents and templates retain their saved settings when those defaults change. If you provide an email settings object without `attachDocument`, attachments stay enabled for backward compatibility. + --- ## Send Document diff --git a/apps/docs/content/docs/users/documents/send.mdx b/apps/docs/content/docs/users/documents/send.mdx index a87b1f128..76f948907 100644 --- a/apps/docs/content/docs/users/documents/send.mdx +++ b/apps/docs/content/docs/users/documents/send.mdx @@ -48,6 +48,12 @@ Or you can customize it before sending the document, by clicking on the **Docume ![Editor email settings](/document-signing/documenso-editor-email-settings.webp) +### Document PDF attachments + +In **Document Settings > Notifications**, clear **Attach completed document PDFs to emails** to send completion emails with document links only. This applies to the owner and all recipients, including CCs. The email notifications remain enabled, and recipients can access the document through their links. + +PDF attachments are enabled by default. Signing invitations and pending emails already include links without document PDFs. You can also set the attachment default for new documents and templates in your [email preferences](/docs/users/organisations/preferences/email). + ### Reply-to email (optional) Set a reply-to address so recipient replies go to a specific email instead of the default. diff --git a/apps/docs/content/docs/users/organisations/preferences/email.mdx b/apps/docs/content/docs/users/organisations/preferences/email.mdx index 12a4b31c1..d7e688cd1 100644 --- a/apps/docs/content/docs/users/organisations/preferences/email.mdx +++ b/apps/docs/content/docs/users/organisations/preferences/email.mdx @@ -25,6 +25,12 @@ To access the preferences, navigate to either the organisation or teams settings | **Reply to email** | Email address used in the "Reply To" field in outgoing emails. Leave blank to inherit from the organisation. | | **Default Email Settings** | Controls which emails are sent to recipients during document signing (e.g. signing invitations, completion notifications). Teams can inherit from the organisation or override with custom settings. | +### Document PDF attachments + +Under **Default Email Settings**, clear **Attach completed document PDFs to emails** to omit PDFs from completion emails to the owner and all recipients, including CCs. Emails still include document links. Attachments are enabled by default. + +Teams inherit this preference with the other email settings. Select **Override organisation settings** to configure it for a team, or **Inherit from organisation** to restore inheritance. Changes apply to new documents and templates; existing documents and templates keep their saved settings. You can change the attachment setting for an individual document or template in **Document Settings > Notifications**. + ## See Also - [Email Domains](/docs/users/organisations/email-domains) - Send emails from your own domain diff --git a/apps/remix/app/components/general/admin-global-settings-section.tsx b/apps/remix/app/components/general/admin-global-settings-section.tsx index 760684077..bd98f4ad3 100644 --- a/apps/remix/app/components/general/admin-global-settings-section.tsx +++ b/apps/remix/app/components/general/admin-global-settings-section.tsx @@ -10,6 +10,7 @@ import type { ReactNode } from 'react'; import { DetailsCard, DetailsValue } from '~/components/general/admin-details'; const EMAIL_SETTINGS_LABELS: Record = { + attachDocument: msg`Attach completed document PDFs`, recipientSigningRequest: msg`Recipient signing request`, recipientRemoved: msg`Recipient removed`, recipientSigned: msg`Recipient signed`, diff --git a/apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.email.tsx b/apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.email.tsx index 51938f124..fc38fbcfc 100644 --- a/apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.email.tsx +++ b/apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.email.tsx @@ -35,7 +35,7 @@ export default function OrganisationSettingsGeneral() { emailId, emailReplyTo: emailReplyTo || null, // emailReplyToName, - emailDocumentSettings, + emailDocumentSettings: emailDocumentSettings ?? undefined, includeSenderDetails: includeSenderDetails ?? undefined, }, }); diff --git a/packages/api/v1/schema.ts b/packages/api/v1/schema.ts index a4296731e..10b9d643c 100644 --- a/packages/api/v1/schema.ts +++ b/packages/api/v1/schema.ts @@ -8,7 +8,7 @@ import { ZDocumentActionAuthTypesSchema, ZRecipientActionAuthTypesSchema, } from '@documenso/lib/types/document-auth'; -import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email'; +import { ZDocumentEmailSettingsInputSchema } from '@documenso/lib/types/document-email'; import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment'; import { ZFieldMetaPrefillFieldsSchema, ZFieldMetaSchema } from '@documenso/lib/types/field-meta'; import { ZRecipientSigningOrderSchema } from '@documenso/lib/types/recipient'; @@ -172,7 +172,7 @@ export const ZCreateDocumentMutationSchema = z.object({ uploadSignatureEnabled: z.boolean().optional().default(true), drawSignatureEnabled: z.boolean().optional().default(true), distributionMethod: z.nativeEnum(DocumentDistributionMethod).optional(), - emailSettings: ZDocumentEmailSettingsSchema.optional(), + emailSettings: ZDocumentEmailSettingsInputSchema.optional(), }) .partial() .optional() @@ -341,7 +341,7 @@ export const ZGenerateDocumentFromTemplateMutationSchema = z.object({ typedSignatureEnabled: z.boolean(), uploadSignatureEnabled: z.boolean(), drawSignatureEnabled: z.boolean(), - emailSettings: ZDocumentEmailSettingsSchema, + emailSettings: ZDocumentEmailSettingsInputSchema, }) .partial() .optional(), diff --git a/packages/app-tests/e2e/envelope-editor-v2/envelope-email-attachments.spec.ts b/packages/app-tests/e2e/envelope-editor-v2/envelope-email-attachments.spec.ts new file mode 100644 index 000000000..73a5a8370 --- /dev/null +++ b/packages/app-tests/e2e/envelope-editor-v2/envelope-email-attachments.spec.ts @@ -0,0 +1,85 @@ +import { nanoid } from '@documenso/lib/universal/id'; +import { prisma } from '@documenso/prisma'; +import { expect, test } from '@playwright/test'; + +import { + addEnvelopeItemPdf, + getEnvelopeEditorSettingsTrigger, + openDocumentEnvelopeEditor, + openEmbeddedEnvelopeEditor, + openTemplateEnvelopeEditor, + persistEmbeddedEnvelope, + setRecipientEmail, + type TEnvelopeEditorSurface, +} from '../fixtures/envelope-editor'; +import { expectToastTextToBeVisible } from '../fixtures/generic'; + +const runAttachmentSettingsFlow = async (surface: TEnvelopeEditorSurface) => { + const { root, isEmbedded } = surface; + const externalId = `e2e-email-attachments-${nanoid()}`; + + if (isEmbedded && !surface.envelopeId) { + await addEnvelopeItemPdf(root); + await setRecipientEmail(root, 0, 'signer@example.com'); + } + + for (const attachDocument of [false, true, false]) { + await getEnvelopeEditorSettingsTrigger(root).click(); + await root.locator('input[name="externalId"]').fill(externalId); + await root.getByRole('button', { name: 'Notifications' }).click(); + const attachmentControl = root.getByRole('checkbox', { name: 'Attach completed document PDFs to emails' }); + await expect(attachmentControl).toBeChecked({ checked: !attachDocument }); + await attachmentControl.setChecked(attachDocument); + await expect(root.locator('#documentCompleted')).toBeChecked(); + await expect(root.locator('#ownerDocumentCompleted')).toBeChecked(); + await root.getByRole('button', { name: 'Update', exact: true }).click(); + + if (!isEmbedded) { + await expectToastTextToBeVisible(root, 'Envelope updated'); + const meta = await prisma.envelope.findUniqueOrThrow({ + where: { id: surface.envelopeId }, + include: { documentMeta: true }, + }); + expect(meta.documentMeta.emailSettings).toMatchObject({ attachDocument }); + await root.reload(); + } + } + + if (isEmbedded) { + await persistEmbeddedEnvelope(surface); + } + + const envelope = await prisma.envelope.findFirstOrThrow({ + where: { externalId, userId: surface.userId, teamId: surface.teamId, type: surface.envelopeType }, + include: { documentMeta: true }, + }); + expect(envelope.documentMeta.emailSettings).toMatchObject({ + attachDocument: false, + documentCompleted: true, + ownerDocumentCompleted: true, + }); +}; + +test.describe('document editor', () => { + test('save email attachment opt-out and toggle back', async ({ page }) => { + await runAttachmentSettingsFlow(await openDocumentEnvelopeEditor(page)); + }); +}); + +test.describe('template editor', () => { + test('save email attachment opt-out and toggle back', async ({ page }) => { + await runAttachmentSettingsFlow(await openTemplateEnvelopeEditor(page)); + }); +}); + +test.describe('embedded create', () => { + test('save email attachment opt-out and toggle back', async ({ page }) => { + await runAttachmentSettingsFlow(await openEmbeddedEnvelopeEditor(page, { envelopeType: 'DOCUMENT' })); + }); +}); + +test.describe('embedded edit', () => { + test('save email attachment opt-out and toggle back', async ({ page }) => { + await runAttachmentSettingsFlow(await openEmbeddedEnvelopeEditor(page, { envelopeType: 'TEMPLATE', mode: 'edit' })); + }); +}); diff --git a/packages/app-tests/e2e/organisations/organisation-team-preferences.spec.ts b/packages/app-tests/e2e/organisations/organisation-team-preferences.spec.ts index 895b0c5aa..cf15bb702 100644 --- a/packages/app-tests/e2e/organisations/organisation-team-preferences.spec.ts +++ b/packages/app-tests/e2e/organisations/organisation-team-preferences.spec.ts @@ -215,6 +215,8 @@ test('[ORGANISATIONS]: manage email preferences', async ({ page }) => { await page.getByRole('textbox', { name: 'Reply to email' }).click(); await page.getByRole('textbox', { name: 'Reply to email' }).fill('organisation@documenso.com'); + await page.getByRole('checkbox', { name: 'Attach completed document PDFs to emails' }).uncheck(); + // Update email document settings by enabling/disabling some checkboxes await page.getByRole('checkbox', { name: 'Email the owner when a recipient signs' }).uncheck(); await page.getByRole('checkbox', { name: 'Email the signer if the document is still pending' }).uncheck(); @@ -230,6 +232,7 @@ test('[ORGANISATIONS]: manage email preferences', async ({ page }) => { // Check that the team settings have inherited these values. expect(teamSettings.emailReplyTo).toEqual('organisation@documenso.com'); expect(teamSettings.emailDocumentSettings).toEqual({ + attachDocument: false, recipientSigningRequest: true, recipientRemoved: true, recipientSigned: false, // unchecked @@ -273,6 +276,7 @@ test('[ORGANISATIONS]: manage email preferences', async ({ page }) => { // Check that the team settings have overridden the organisation values. expect(updatedTeamSettings.emailReplyTo).toEqual('team@example.com'); expect(updatedTeamSettings.emailDocumentSettings).toEqual({ + attachDocument: true, recipientSigned: true, recipientSigningRequest: false, recipientRemoved: true, @@ -295,6 +299,7 @@ test('[ORGANISATIONS]: manage email preferences', async ({ page }) => { expect(teamOverrideDocumentMeta.emailReplyTo).toEqual('team@example.com'); expect(teamOverrideDocumentMeta.emailSettings).toEqual({ + attachDocument: true, recipientSigned: true, recipientSigningRequest: false, recipientRemoved: true, @@ -322,6 +327,7 @@ test('[ORGANISATIONS]: manage email preferences', async ({ page }) => { // Check that the team settings now inherit from organisation again. expect(inheritedTeamSettings.emailReplyTo).toEqual('organisation@documenso.com'); expect(inheritedTeamSettings.emailDocumentSettings).toEqual({ + attachDocument: false, recipientSigningRequest: true, recipientRemoved: true, recipientSigned: false, @@ -344,6 +350,7 @@ test('[ORGANISATIONS]: manage email preferences', async ({ page }) => { expect(documentMeta.emailReplyTo).toEqual('organisation@documenso.com'); expect(documentMeta.emailSettings).toEqual({ + attachDocument: false, recipientSigningRequest: true, recipientRemoved: true, recipientSigned: false, @@ -354,4 +361,22 @@ test('[ORGANISATIONS]: manage email preferences', async ({ page }) => { ownerDocumentCompleted: true, ownerDocumentCreated: true, }); + + // Re-enable attachments and verify that inherited defaults change only for new documents. + await page.goto(`/o/${organisation.url}/settings/email`); + const attachmentControl = page.getByRole('checkbox', { name: 'Attach completed document PDFs to emails' }); + await expect(attachmentControl).not.toBeChecked(); + await attachmentControl.check(); + await page.getByRole('button', { name: 'Save changes' }).first().click(); + await expect(page.getByText('Your email preferences have been updated').first()).toBeVisible(); + await page.reload(); + await expect(attachmentControl).toBeChecked(); + + const restoredSettings = await getTeamSettings({ teamId: team.id }); + expect(restoredSettings.emailDocumentSettings.attachDocument).toBe(true); + const newDocument = await seedTeamDocumentWithMeta(team); + const newMeta = await prisma.documentMeta.findUniqueOrThrow({ where: { id: newDocument.documentMetaId } }); + expect(newMeta.emailSettings?.attachDocument).toBe(true); + const existingMeta = await prisma.documentMeta.findUniqueOrThrow({ where: { id: document.documentMetaId } }); + expect(existingMeta.emailSettings?.attachDocument).toBe(false); }); diff --git a/packages/lib/jobs/definitions/emails/send-document-completed-emails.handler.test.ts b/packages/lib/jobs/definitions/emails/send-document-completed-emails.handler.test.ts new file mode 100644 index 000000000..40c0fa801 --- /dev/null +++ b/packages/lib/jobs/definitions/emails/send-document-completed-emails.handler.test.ts @@ -0,0 +1,190 @@ +import { DocumentDistributionMethod, DocumentSource, RecipientRole } from '@prisma/client'; +import type { ReactElement } from 'react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { DEFAULT_DOCUMENT_EMAIL_SETTINGS } from '../../../types/document-email'; +import type { JobRunIO } from '../../client/_internal/job'; +import { run } from './send-document-completed-emails.handler'; + +const mocks = vi.hoisted(() => ({ + findEnvelope: vi.fn(), + createAuditLog: vi.fn(), + findEnvelopeItems: vi.fn(), + getEmailContext: vi.fn(), + getFile: vi.fn(), + sendMail: vi.fn(), + checkLimits: vi.fn(), +})); + +vi.mock('@documenso/prisma', () => ({ + prisma: { + envelope: { findUnique: mocks.findEnvelope }, + envelopeItem: { findMany: mocks.findEnvelopeItems }, + documentAuditLog: { create: mocks.createAuditLog }, + }, +})); +vi.mock('@documenso/email/templates/document-completed', () => ({ + DocumentCompletedEmailTemplate: () => null, +})); +vi.mock('../../../client-only/providers/i18n-server', () => ({ + getI18nInstance: async () => ({ _: () => 'Signing Complete!' }), +})); +vi.mock('../../../constants/app', () => ({ NEXT_PUBLIC_WEBAPP_URL: () => 'http://localhost:3000' })); +vi.mock('../../../server-only/email/get-email-context', () => ({ getEmailContext: mocks.getEmailContext })); +vi.mock('../../../server-only/rate-limit/assert-organisation-rates-and-limits', () => ({ + assertOrganisationRatesAndLimits: mocks.checkLimits, +})); +vi.mock('../../../universal/upload/get-file.server', () => ({ getFileServerSide: mocks.getFile })); +vi.mock('../../../utils/document-audit-logs', () => ({ createDocumentAuditLogData: (data: unknown) => data })); +vi.mock('../../../utils/teams', () => ({ formatDocumentsPath: () => '/t/test-team/documents' })); +vi.mock('../../../utils/render-email-with-i18n', () => ({ + renderEmailWithI18N: async (template: ReactElement<{ downloadLink: string }>) => template.props.downloadLink, +})); + +const createEnvelope = (distributionMethod: DocumentDistributionMethod = DocumentDistributionMethod.EMAIL) => ({ + id: 'envelope_test', + secondaryId: 'document_1', + teamId: 1, + internalVersion: 2, + title: 'Confidential document', + source: DocumentSource.DOCUMENT, + team: { id: 1, url: 'test-team' }, + user: { id: 1, name: 'Owner', email: 'owner@example.com', disabled: false }, + documentMeta: { + distributionMethod, + emailSettings: { ...DEFAULT_DOCUMENT_EMAIL_SETTINGS }, + }, + envelopeItems: [ + { title: 'First document', documentData: { id: 'file_1', type: 'S3', data: 'file_1' } }, + { title: 'Second document', documentData: { id: 'file_2', type: 'BYTES', data: 'file_2' } }, + ], + recipients: [ + { id: 2, name: 'Signer', email: 'signer@example.com', role: RecipientRole.SIGNER, token: 'signer-token' }, + { id: 3, name: 'CC', email: 'cc@example.com', role: RecipientRole.CC, token: 'cc-token' }, + ], +}); + +let envelope = createEnvelope(); +const io = { logger: { warn: vi.fn() } } as unknown as JobRunIO; +const runJob = () => run({ payload: { envelopeId: envelope.id }, io }); + +beforeEach(() => { + vi.clearAllMocks(); + envelope = createEnvelope(); + mocks.findEnvelope.mockImplementation(async () => envelope); + mocks.findEnvelopeItems.mockImplementation(async () => envelope.envelopeItems); + mocks.getFile.mockResolvedValue(new Uint8Array([1, 2, 3])); + mocks.getEmailContext.mockResolvedValue({ + branding: {}, + emailLanguage: 'en', + senderEmail: 'sender@example.com', + replyToEmail: 'reply@example.com', + organisationId: 'org_test', + claims: {}, + emailsDisabled: false, + emailTransport: { sendMail: mocks.sendMail }, + }); +}); + +describe('completion email document attachments', () => { + it('keeps email content, links, audit logs, and CC metering when PDFs are disabled', async () => { + envelope.documentMeta.emailSettings.attachDocument = false; + // Loading a PDF would fail: link-only email must still succeed. + mocks.getFile.mockRejectedValue(new Error('PDF storage unavailable')); + + await runJob(); + + expect(mocks.findEnvelopeItems).not.toHaveBeenCalled(); + expect(mocks.getFile).not.toHaveBeenCalled(); + expect(mocks.sendMail).toHaveBeenCalledTimes(3); + const emails = mocks.sendMail.mock.calls.map(([email]) => email); + expect(emails.map((email) => email.to[0].address)).toEqual([ + 'owner@example.com', + 'signer@example.com', + 'cc@example.com', + ]); + for (const email of emails) { + expect(email).toMatchObject({ attachments: [], subject: 'Signing Complete!' }); + expect(email.html).toBe(email.text); + } + expect(emails[0].html).toBe('http://localhost:3000/t/test-team/documents/envelope_test'); + expect(emails[1].html).toBe('http://localhost:3000/sign/signer-token/complete'); + expect(emails[2].html).toBe('http://localhost:3000/sign/cc-token/complete'); + expect(mocks.createAuditLog).toHaveBeenCalledTimes(3); + expect(mocks.checkLimits).toHaveBeenCalledOnce(); + }); + + it.each([true, undefined])('attaches all PDFs by default and when enabled (%s)', async (attachDocument) => { + mocks.findEnvelope.mockResolvedValue({ + ...envelope, + documentMeta: { ...envelope.documentMeta, emailSettings: { attachDocument } }, + }); + + await runJob(); + + expect(mocks.getFile).toHaveBeenCalledTimes(2); + expect(mocks.sendMail).toHaveBeenCalledTimes(3); + for (const [email] of mocks.sendMail.mock.calls) { + expect(email.attachments).toEqual([ + { filename: 'First document.pdf', content: Buffer.from([1, 2, 3]), contentType: 'application/pdf' }, + { filename: 'Second document.pdf', content: Buffer.from([1, 2, 3]), contentType: 'application/pdf' }, + ]); + } + }); + + it('retains legacy single-document filenames', async () => { + envelope.internalVersion = 1; + envelope.envelopeItems = [envelope.envelopeItems[0]]; + await runJob(); + expect(mocks.sendMail.mock.calls[0][0].attachments[0].filename).toBe('Confidential document.pdf'); + }); + + it('sends link-only owner emails when recipient emails are disabled', async () => { + envelope.documentMeta.emailSettings.attachDocument = false; + envelope.documentMeta.emailSettings.documentCompleted = false; + await runJob(); + expect(mocks.findEnvelopeItems).not.toHaveBeenCalled(); + expect(mocks.getFile).not.toHaveBeenCalled(); + expect(mocks.sendMail).toHaveBeenCalledOnce(); + expect(mocks.sendMail.mock.calls[0][0].to[0].address).toBe('owner@example.com'); + }); + + it('sends link-only recipient emails when owner emails are disabled', async () => { + envelope.documentMeta.emailSettings.attachDocument = false; + envelope.documentMeta.emailSettings.ownerDocumentCompleted = false; + await runJob(); + expect(mocks.findEnvelopeItems).not.toHaveBeenCalled(); + expect(mocks.getFile).not.toHaveBeenCalled(); + expect(mocks.sendMail).toHaveBeenCalledTimes(2); + }); + + it('sends a single link-only completion email when the owner is also a recipient', async () => { + envelope.documentMeta.emailSettings.attachDocument = false; + envelope.recipients[0].email = envelope.user.email; + await runJob(); + expect(mocks.sendMail).toHaveBeenCalledTimes(2); + expect(mocks.sendMail.mock.calls[0][0]).toMatchObject({ + to: [{ address: 'owner@example.com' }], + attachments: [], + html: 'http://localhost:3000/t/test-team/documents/envelope_test', + }); + }); + + it('preserves owner-only link emails when distribution is NONE', async () => { + envelope.documentMeta.distributionMethod = DocumentDistributionMethod.NONE; + envelope.documentMeta.emailSettings.attachDocument = false; + await runJob(); + expect(mocks.findEnvelopeItems).not.toHaveBeenCalled(); + expect(mocks.getFile).not.toHaveBeenCalled(); + expect(mocks.sendMail).toHaveBeenCalledOnce(); + }); + + it('does not load PDFs or send mail when both completion emails are disabled', async () => { + envelope.documentMeta.emailSettings.documentCompleted = false; + envelope.documentMeta.emailSettings.ownerDocumentCompleted = false; + await runJob(); + expect(mocks.findEnvelopeItems).not.toHaveBeenCalled(); + expect(mocks.getFile).not.toHaveBeenCalled(); + expect(mocks.sendMail).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/lib/jobs/definitions/emails/send-document-completed-emails.handler.ts b/packages/lib/jobs/definitions/emails/send-document-completed-emails.handler.ts index cd0059b05..25d69bdc8 100644 --- a/packages/lib/jobs/definitions/emails/send-document-completed-emails.handler.ts +++ b/packages/lib/jobs/definitions/emails/send-document-completed-emails.handler.ts @@ -26,17 +26,6 @@ export const run = async ({ payload, io }: { payload: TSendDocumentCompletedEmai const envelope = await prisma.envelope.findUnique({ where: unsafeBuildEnvelopeIdQuery({ type: 'envelopeId', id: envelopeId }, EnvelopeType.DOCUMENT), include: { - envelopeItems: { - include: { - documentData: { - select: { - type: true, - id: true, - data: true, - }, - }, - }, - }, documentMeta: true, recipients: true, user: { @@ -83,15 +72,39 @@ export const run = async ({ payload, io }: { payload: TSendDocumentCompletedEmai const { user: owner } = envelope; + const emailSettings = extractDerivedDocumentEmailSettings(envelope.documentMeta); + const isDocumentCompletedEmailEnabled = emailSettings.documentCompleted; + const isOwnerDocumentCompletedEmailEnabled = emailSettings.ownerDocumentCompleted; + + if (!isDocumentCompletedEmailEnabled && !isOwnerDocumentCompletedEmailEnabled) { + return; + } + + // Fetch PDF data only when attachments are enabled. Database-backed PDFs can + // contain the entire file in documentData.data. + const envelopeItems = emailSettings.attachDocument + ? await prisma.envelopeItem.findMany({ + where: { envelopeId: envelope.id }, + include: { + documentData: { + select: { + type: true, + data: true, + }, + }, + }, + }) + : []; + const completedDocumentEmailAttachments = await Promise.all( - envelope.envelopeItems.map(async (envelopeItem) => { + envelopeItems.map(async (envelopeItem) => { const file = await getFileServerSide(envelopeItem.documentData); // Use the envelope title for version 1, and the envelope item title for version 2. - const fileNameToUse = envelope.internalVersion === 1 ? envelope.title : envelopeItem.title + '.pdf'; + const fileNameToUse = envelope.internalVersion === 1 ? envelope.title : `${envelopeItem.title}.pdf`; return { - filename: fileNameToUse.endsWith('.pdf') ? fileNameToUse : fileNameToUse + '.pdf', + filename: fileNameToUse.endsWith('.pdf') ? fileNameToUse : `${fileNameToUse}.pdf`, content: Buffer.from(file), contentType: 'application/pdf', }; @@ -108,10 +121,6 @@ export const run = async ({ payload, io }: { payload: TSendDocumentCompletedEmai documentOwnerDownloadLink = `${NEXT_PUBLIC_WEBAPP_URL()}/t/${envelope.team.url}/documents/${envelope.id}`; } - const emailSettings = extractDerivedDocumentEmailSettings(envelope.documentMeta); - const isDocumentCompletedEmailEnabled = emailSettings.documentCompleted; - const isOwnerDocumentCompletedEmailEnabled = emailSettings.ownerDocumentCompleted; - // Send email to document owner if: // 1. Owner document completed emails are enabled AND // 2. Either: diff --git a/packages/lib/server-only/webhooks/trigger/generate-sample-data.ts b/packages/lib/server-only/webhooks/trigger/generate-sample-data.ts index af902f8ba..d24d6d130 100644 --- a/packages/lib/server-only/webhooks/trigger/generate-sample-data.ts +++ b/packages/lib/server-only/webhooks/trigger/generate-sample-data.ts @@ -437,6 +437,7 @@ export const generateSampleWebhookPayload = (event: WebhookTriggerEvents, webhoo dateFormat: 'yyyy-MM-dd hh:mm a', redirectUrl: '', emailSettings: { + attachDocument: true, documentDeleted: true, documentPending: true, recipientSigned: true, diff --git a/packages/lib/translations/de/web.po b/packages/lib/translations/de/web.po index 7d5554eb2..2e6220970 100644 --- a/packages/lib/translations/de/web.po +++ b/packages/lib/translations/de/web.po @@ -15322,3 +15322,15 @@ msgstr "Ihr Verifizierungscode:" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx msgid "your-domain.com another-domain.com" msgstr "your-domain.com another-domain.com" + +#: apps/remix/app/components/general/admin-global-settings-section.tsx +msgid "Attach completed document PDFs" +msgstr "" + +#: packages/ui/components/document/document-email-checkboxes.tsx +msgid "Attach completed document PDFs to emails" +msgstr "" + +#: packages/ui/components/document/document-email-checkboxes.tsx +msgid "Includes document PDFs in completion emails to the owner and all recipients, including CCs. Disable this to send emails with document links only." +msgstr "" diff --git a/packages/lib/translations/en/web.po b/packages/lib/translations/en/web.po index 5580bdab8..461d3aec2 100644 --- a/packages/lib/translations/en/web.po +++ b/packages/lib/translations/en/web.po @@ -15317,3 +15317,15 @@ msgstr "Your verification code:" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx msgid "your-domain.com another-domain.com" msgstr "your-domain.com another-domain.com" + +#: apps/remix/app/components/general/admin-global-settings-section.tsx +msgid "Attach completed document PDFs" +msgstr "Attach completed document PDFs" + +#: packages/ui/components/document/document-email-checkboxes.tsx +msgid "Attach completed document PDFs to emails" +msgstr "Attach completed document PDFs to emails" + +#: packages/ui/components/document/document-email-checkboxes.tsx +msgid "Includes document PDFs in completion emails to the owner and all recipients, including CCs. Disable this to send emails with document links only." +msgstr "Includes document PDFs in completion emails to the owner and all recipients, including CCs. Disable this to send emails with document links only." diff --git a/packages/lib/translations/es/web.po b/packages/lib/translations/es/web.po index 8e7abeac6..ac1f44216 100644 --- a/packages/lib/translations/es/web.po +++ b/packages/lib/translations/es/web.po @@ -15322,3 +15322,15 @@ msgstr "Su código de verificación:" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx msgid "your-domain.com another-domain.com" msgstr "su-dominio.com otro-dominio.com" + +#: apps/remix/app/components/general/admin-global-settings-section.tsx +msgid "Attach completed document PDFs" +msgstr "" + +#: packages/ui/components/document/document-email-checkboxes.tsx +msgid "Attach completed document PDFs to emails" +msgstr "" + +#: packages/ui/components/document/document-email-checkboxes.tsx +msgid "Includes document PDFs in completion emails to the owner and all recipients, including CCs. Disable this to send emails with document links only." +msgstr "" diff --git a/packages/lib/translations/fr/web.po b/packages/lib/translations/fr/web.po index 085e2fe1a..770701c2c 100644 --- a/packages/lib/translations/fr/web.po +++ b/packages/lib/translations/fr/web.po @@ -15322,3 +15322,15 @@ msgstr "Votre code de vérification :" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx msgid "your-domain.com another-domain.com" msgstr "your-domain.com another-domain.com" + +#: apps/remix/app/components/general/admin-global-settings-section.tsx +msgid "Attach completed document PDFs" +msgstr "" + +#: packages/ui/components/document/document-email-checkboxes.tsx +msgid "Attach completed document PDFs to emails" +msgstr "" + +#: packages/ui/components/document/document-email-checkboxes.tsx +msgid "Includes document PDFs in completion emails to the owner and all recipients, including CCs. Disable this to send emails with document links only." +msgstr "" diff --git a/packages/lib/translations/it/web.po b/packages/lib/translations/it/web.po index 238d37e09..1371ee93e 100644 --- a/packages/lib/translations/it/web.po +++ b/packages/lib/translations/it/web.po @@ -15322,3 +15322,15 @@ msgstr "Il tuo codice di verifica:" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx msgid "your-domain.com another-domain.com" msgstr "tuo-dominio.com altro-dominio.com" + +#: apps/remix/app/components/general/admin-global-settings-section.tsx +msgid "Attach completed document PDFs" +msgstr "" + +#: packages/ui/components/document/document-email-checkboxes.tsx +msgid "Attach completed document PDFs to emails" +msgstr "" + +#: packages/ui/components/document/document-email-checkboxes.tsx +msgid "Includes document PDFs in completion emails to the owner and all recipients, including CCs. Disable this to send emails with document links only." +msgstr "" diff --git a/packages/lib/translations/ja/web.po b/packages/lib/translations/ja/web.po index 4e451daf9..5cd6031a3 100644 --- a/packages/lib/translations/ja/web.po +++ b/packages/lib/translations/ja/web.po @@ -15322,3 +15322,15 @@ msgstr "認証コード:" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx msgid "your-domain.com another-domain.com" msgstr "your-domain.com another-domain.com" + +#: apps/remix/app/components/general/admin-global-settings-section.tsx +msgid "Attach completed document PDFs" +msgstr "" + +#: packages/ui/components/document/document-email-checkboxes.tsx +msgid "Attach completed document PDFs to emails" +msgstr "" + +#: packages/ui/components/document/document-email-checkboxes.tsx +msgid "Includes document PDFs in completion emails to the owner and all recipients, including CCs. Disable this to send emails with document links only." +msgstr "" diff --git a/packages/lib/translations/ko/web.po b/packages/lib/translations/ko/web.po index a4c12929d..fdb709bf8 100644 --- a/packages/lib/translations/ko/web.po +++ b/packages/lib/translations/ko/web.po @@ -15322,3 +15322,15 @@ msgstr "인증 코드:" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx msgid "your-domain.com another-domain.com" msgstr "your-domain.com another-domain.com" + +#: apps/remix/app/components/general/admin-global-settings-section.tsx +msgid "Attach completed document PDFs" +msgstr "" + +#: packages/ui/components/document/document-email-checkboxes.tsx +msgid "Attach completed document PDFs to emails" +msgstr "" + +#: packages/ui/components/document/document-email-checkboxes.tsx +msgid "Includes document PDFs in completion emails to the owner and all recipients, including CCs. Disable this to send emails with document links only." +msgstr "" diff --git a/packages/lib/translations/nl/web.po b/packages/lib/translations/nl/web.po index 21c56f4d4..23d2900f5 100644 --- a/packages/lib/translations/nl/web.po +++ b/packages/lib/translations/nl/web.po @@ -15322,3 +15322,15 @@ msgstr "Uw verificatiecode:" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx msgid "your-domain.com another-domain.com" msgstr "your-domain.com another-domain.com" + +#: apps/remix/app/components/general/admin-global-settings-section.tsx +msgid "Attach completed document PDFs" +msgstr "" + +#: packages/ui/components/document/document-email-checkboxes.tsx +msgid "Attach completed document PDFs to emails" +msgstr "" + +#: packages/ui/components/document/document-email-checkboxes.tsx +msgid "Includes document PDFs in completion emails to the owner and all recipients, including CCs. Disable this to send emails with document links only." +msgstr "" diff --git a/packages/lib/translations/pl/web.po b/packages/lib/translations/pl/web.po index 207cae377..62191108f 100644 --- a/packages/lib/translations/pl/web.po +++ b/packages/lib/translations/pl/web.po @@ -15359,3 +15359,14 @@ msgstr "Powiększ" msgid "Zoom out" msgstr "Pomniejsz" +#: apps/remix/app/components/general/admin-global-settings-section.tsx +msgid "Attach completed document PDFs" +msgstr "" + +#: packages/ui/components/document/document-email-checkboxes.tsx +msgid "Attach completed document PDFs to emails" +msgstr "" + +#: packages/ui/components/document/document-email-checkboxes.tsx +msgid "Includes document PDFs in completion emails to the owner and all recipients, including CCs. Disable this to send emails with document links only." +msgstr "" diff --git a/packages/lib/translations/pt-BR/web.po b/packages/lib/translations/pt-BR/web.po index 2f8321151..eaa3df8e9 100644 --- a/packages/lib/translations/pt-BR/web.po +++ b/packages/lib/translations/pt-BR/web.po @@ -15317,3 +15317,15 @@ msgstr "Seu código de verificação:" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx msgid "your-domain.com another-domain.com" msgstr "seu-dominio.com outro-dominio.com" + +#: apps/remix/app/components/general/admin-global-settings-section.tsx +msgid "Attach completed document PDFs" +msgstr "" + +#: packages/ui/components/document/document-email-checkboxes.tsx +msgid "Attach completed document PDFs to emails" +msgstr "" + +#: packages/ui/components/document/document-email-checkboxes.tsx +msgid "Includes document PDFs in completion emails to the owner and all recipients, including CCs. Disable this to send emails with document links only." +msgstr "" diff --git a/packages/lib/translations/zh/web.po b/packages/lib/translations/zh/web.po index d0f89f4ca..cff5b4ef0 100644 --- a/packages/lib/translations/zh/web.po +++ b/packages/lib/translations/zh/web.po @@ -15322,3 +15322,15 @@ msgstr "您的验证码:" #: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.sso.tsx msgid "your-domain.com another-domain.com" msgstr "your-domain.com another-domain.com" + +#: apps/remix/app/components/general/admin-global-settings-section.tsx +msgid "Attach completed document PDFs" +msgstr "" + +#: packages/ui/components/document/document-email-checkboxes.tsx +msgid "Attach completed document PDFs to emails" +msgstr "" + +#: packages/ui/components/document/document-email-checkboxes.tsx +msgid "Includes document PDFs in completion emails to the owner and all recipients, including CCs. Disable this to send emails with document links only." +msgstr "" diff --git a/packages/lib/types/document-email-api.test.ts b/packages/lib/types/document-email-api.test.ts new file mode 100644 index 000000000..2f56a2dd9 --- /dev/null +++ b/packages/lib/types/document-email-api.test.ts @@ -0,0 +1,101 @@ +import { describe, expect, it } from 'vitest'; +import { ZCreateDocumentMutationSchema, ZGenerateDocumentFromTemplateMutationSchema } from '../../api/v1/schema'; +import { ZCreateDocumentPayloadSchema } from '../../trpc/server/document-router/create-document.types'; +import { ZCreateEnvelopePayloadSchema } from '../../trpc/server/envelope-router/create-envelope.types'; +import { ZUpdateEnvelopeRequestSchema } from '../../trpc/server/envelope-router/update-envelope.types'; +import { ZUseEnvelopePayloadSchema } from '../../trpc/server/envelope-router/use-envelope.types'; +import { ZUpdateOrganisationSettingsRequestSchema } from '../../trpc/server/organisation-router/update-organisation-settings.types'; +import { ZUpdateTeamSettingsRequestSchema } from '../../trpc/server/team-router/update-team-settings.types'; +import { + ZCreateDocumentFromTemplateRequestSchema, + ZCreateTemplateV2RequestSchema, +} from '../../trpc/server/template-router/schema'; + +const requestCases = [ + { + name: 'v1 document creation', + schema: ZCreateDocumentMutationSchema, + body: { title: 'Document', recipients: [] }, + field: 'meta', + }, + { + name: 'v1 template use', + schema: ZGenerateDocumentFromTemplateMutationSchema, + body: { recipients: [] }, + field: 'meta', + }, + { name: 'document creation', schema: ZCreateDocumentPayloadSchema, body: { title: 'Document' }, field: 'meta' }, + { + name: 'envelope creation', + schema: ZCreateEnvelopePayloadSchema, + body: { title: 'Document', type: 'DOCUMENT' }, + field: 'meta', + }, + { + name: 'envelope update', + schema: ZUpdateEnvelopeRequestSchema, + body: { envelopeId: 'envelope_test' }, + field: 'meta', + }, + { + name: 'envelope template use', + schema: ZUseEnvelopePayloadSchema, + body: { envelopeId: 'envelope_test' }, + field: 'override', + }, + { name: 'template creation', schema: ZCreateTemplateV2RequestSchema, body: { title: 'Template' }, field: 'meta' }, + { + name: 'document from template', + schema: ZCreateDocumentFromTemplateRequestSchema, + body: { templateId: 1, recipients: [] }, + field: 'override', + }, +] as const; + +describe.each(requestCases)('$name email settings API', ({ schema, body, field }) => { + it.each([true, false])('accepts and retains attachDocument=%s', (attachDocument) => { + const request = { ...body, [field]: { emailSettings: { attachDocument } } }; + const result = schema.parse(request); + expect(result).toMatchObject({ [field]: { emailSettings: { attachDocument } } }); + }); + + it('rejects non-boolean attachment controls instead of enabling attachments', () => { + const request = { ...body, [field]: { emailSettings: { attachDocument: 'false' } } }; + expect(schema.safeParse(request).success).toBe(false); + }); +}); + +describe('default email settings API', () => { + it.each([true, false])('retains organisation and team attachment defaults (%s)', (attachDocument) => { + expect( + ZUpdateOrganisationSettingsRequestSchema.parse({ + organisationId: 'org_test', + data: { emailDocumentSettings: { attachDocument } }, + }), + ).toMatchObject({ data: { emailDocumentSettings: { attachDocument } } }); + expect( + ZUpdateTeamSettingsRequestSchema.parse({ teamId: 1, data: { emailDocumentSettings: { attachDocument } } }), + ).toMatchObject({ data: { emailDocumentSettings: { attachDocument } } }); + }); + + it('rejects malformed organisation and team defaults', () => { + expect( + ZUpdateOrganisationSettingsRequestSchema.safeParse({ + organisationId: 'org_test', + data: { emailDocumentSettings: { attachDocument: 'false' } }, + }).success, + ).toBe(false); + expect( + ZUpdateTeamSettingsRequestSchema.safeParse({ + teamId: 1, + data: { emailDocumentSettings: { attachDocument: 'false' } }, + }).success, + ).toBe(false); + }); + + it('retains null as the team inheritance control', () => { + expect(ZUpdateTeamSettingsRequestSchema.parse({ teamId: 1, data: { emailDocumentSettings: null } })).toMatchObject({ + data: { emailDocumentSettings: null }, + }); + }); +}); diff --git a/packages/lib/types/document-email.test.ts b/packages/lib/types/document-email.test.ts new file mode 100644 index 000000000..ab95f89f9 --- /dev/null +++ b/packages/lib/types/document-email.test.ts @@ -0,0 +1,94 @@ +import { DocumentDistributionMethod, type DocumentMeta } from '@prisma/client'; +import { describe, expect, it } from 'vitest'; + +import { extractDerivedDocumentMeta } from '../utils/document'; +import { generateDefaultOrganisationSettings } from '../utils/organisations'; +import { extractDerivedTeamSettings, generateDefaultTeamSettings } from '../utils/teams'; +import { + DEFAULT_DOCUMENT_EMAIL_SETTINGS, + extractDerivedDocumentEmailSettings, + ZDocumentEmailSettingsInputSchema, + ZDocumentEmailSettingsSchema, +} from './document-email'; +import { ZDocumentMetaCreateSchema, ZDocumentMetaUpdateSchema } from './document-meta'; + +describe('document email attachments', () => { + it('keeps attachments enabled for absent and legacy settings', () => { + expect(extractDerivedDocumentEmailSettings().attachDocument).toBe(true); + expect(ZDocumentEmailSettingsSchema.parse(null).attachDocument).toBe(true); + expect(ZDocumentEmailSettingsInputSchema.parse({ documentCompleted: false })).toMatchObject({ + attachDocument: true, + documentCompleted: false, + }); + }); + + it('disables attachments independently of notification events', () => { + expect(ZDocumentEmailSettingsInputSchema.parse({ attachDocument: false })).toEqual({ + ...DEFAULT_DOCUMENT_EMAIL_SETTINGS, + attachDocument: false, + }); + }); + + it.each(['false', 0, null, [], {}])('rejects invalid attachment values: %j', (attachDocument) => { + expect(ZDocumentEmailSettingsInputSchema.safeParse({ attachDocument }).success).toBe(false); + expect(ZDocumentMetaCreateSchema.safeParse({ emailSettings: { attachDocument } }).success).toBe(false); + expect(ZDocumentMetaUpdateSchema.safeParse({ emailSettings: { attachDocument } }).success).toBe(false); + }); + + it('does not replace an explicit opt-out with defaults when another input is invalid', () => { + expect( + ZDocumentMetaCreateSchema.safeParse({ + emailSettings: { attachDocument: false, documentCompleted: 'true' }, + }).success, + ).toBe(false); + }); + + it('preserves the owner attachment setting when email distribution is disabled', () => { + const settings = extractDerivedDocumentEmailSettings({ + distributionMethod: DocumentDistributionMethod.NONE, + emailSettings: { ...DEFAULT_DOCUMENT_EMAIL_SETTINGS, attachDocument: false }, + } as DocumentMeta); + + expect(settings).toMatchObject({ + attachDocument: false, + documentCompleted: false, + ownerDocumentCompleted: true, + }); + }); + + it('inherits organisation defaults, allows team overrides, and restores inheritance', () => { + const organisationSettings = generateDefaultOrganisationSettings(); + organisationSettings.emailDocumentSettings = { ...DEFAULT_DOCUMENT_EMAIL_SETTINGS, attachDocument: false }; + const teamSettings = generateDefaultTeamSettings(); + + const inherited = extractDerivedTeamSettings(organisationSettings, teamSettings); + expect(extractDerivedDocumentMeta(inherited, undefined).emailSettings.attachDocument).toBe(false); + + teamSettings.emailDocumentSettings = { ...DEFAULT_DOCUMENT_EMAIL_SETTINGS, attachDocument: true }; + const overridden = extractDerivedTeamSettings(organisationSettings, teamSettings); + expect(extractDerivedDocumentMeta(overridden, undefined).emailSettings.attachDocument).toBe(true); + + teamSettings.emailDocumentSettings = null; + expect( + extractDerivedDocumentMeta(extractDerivedTeamSettings(organisationSettings, teamSettings), undefined) + .emailSettings.attachDocument, + ).toBe(false); + }); + + it('allows a document or template to override inherited attachment defaults in either direction', () => { + const settings = generateDefaultOrganisationSettings(); + + expect( + extractDerivedDocumentMeta(settings, { + emailSettings: { ...DEFAULT_DOCUMENT_EMAIL_SETTINGS, attachDocument: false }, + }).emailSettings.attachDocument, + ).toBe(false); + + settings.emailDocumentSettings = { ...DEFAULT_DOCUMENT_EMAIL_SETTINGS, attachDocument: false }; + expect( + extractDerivedDocumentMeta(settings, { + emailSettings: { ...DEFAULT_DOCUMENT_EMAIL_SETTINGS, attachDocument: true }, + }).emailSettings.attachDocument, + ).toBe(true); + }); +}); diff --git a/packages/lib/types/document-email.ts b/packages/lib/types/document-email.ts index 04c521378..a486da233 100644 --- a/packages/lib/types/document-email.ts +++ b/packages/lib/types/document-email.ts @@ -14,8 +14,14 @@ export enum DocumentEmailEvents { OwnerDocumentCreated = 'ownerDocumentCreated', } -export const ZDocumentEmailSettingsSchema = z +export const ZDocumentEmailSettingsInputSchema = z .object({ + attachDocument: z + .boolean() + .describe( + 'Whether to attach the completed document PDFs to emails sent to the owner and recipients. Document links are always included.', + ) + .default(true), recipientSigningRequest: z .boolean() .describe('Whether to send an email to all recipients that the document is ready for them to sign.') @@ -55,8 +61,13 @@ export const ZDocumentEmailSettingsSchema = z .describe('Whether to send an email to the document owner when a document is created from a direct template.') .default(true), }) - .strip() - .catch(() => ({ ...DEFAULT_DOCUMENT_EMAIL_SETTINGS })); + .strip(); + +// Persisted settings may be absent or predate new options. API inputs must use +// the input schema so invalid values cannot silently enable document attachments. +export const ZDocumentEmailSettingsSchema = ZDocumentEmailSettingsInputSchema.catch(() => ({ + ...DEFAULT_DOCUMENT_EMAIL_SETTINGS, +})); export type TDocumentEmailSettings = z.infer; @@ -68,6 +79,7 @@ export const extractDerivedDocumentEmailSettings = (documentMeta?: DocumentMeta } return { + attachDocument: emailSettings.attachDocument, recipientSigningRequest: false, recipientRemoved: false, recipientSigned: false, @@ -81,6 +93,7 @@ export const extractDerivedDocumentEmailSettings = (documentMeta?: DocumentMeta }; export const DEFAULT_DOCUMENT_EMAIL_SETTINGS: TDocumentEmailSettings = { + attachDocument: true, recipientSigningRequest: true, recipientRemoved: true, recipientSigned: true, diff --git a/packages/lib/types/document-meta.ts b/packages/lib/types/document-meta.ts index 2ca4f5fde..e41db1252 100644 --- a/packages/lib/types/document-meta.ts +++ b/packages/lib/types/document-meta.ts @@ -9,7 +9,7 @@ import { msg } from '@lingui/core/macro'; import { DocumentDistributionMethod, DocumentSigningOrder } from '@prisma/client'; import { z } from 'zod'; -import { ZDocumentEmailSettingsSchema } from './document-email'; +import { ZDocumentEmailSettingsInputSchema } from './document-email'; /** * The full document response schema. @@ -125,7 +125,7 @@ export const ZDocumentMetaCreateSchema = z.object({ drawSignatureEnabled: ZDocumentMetaDrawSignatureEnabledSchema.optional(), emailId: z.string().nullish(), emailReplyTo: zEmail().nullish(), - emailSettings: ZDocumentEmailSettingsSchema.nullish(), + emailSettings: ZDocumentEmailSettingsInputSchema.nullish(), envelopeExpirationPeriod: ZEnvelopeExpirationPeriod.nullish(), reminderSettings: ZEnvelopeReminderSettings.nullish(), }); diff --git a/packages/trpc/server/document-router/distribute-document.types.ts b/packages/trpc/server/document-router/distribute-document.types.ts index 2c9de4d08..29f829d80 100644 --- a/packages/trpc/server/document-router/distribute-document.types.ts +++ b/packages/trpc/server/document-router/distribute-document.types.ts @@ -1,5 +1,5 @@ import { ZDocumentLiteSchema } from '@documenso/lib/types/document'; -import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email'; +import { ZDocumentEmailSettingsInputSchema } from '@documenso/lib/types/document-email'; import { ZDocumentMetaDateFormatSchema, ZDocumentMetaDistributionMethodSchema, @@ -39,7 +39,7 @@ export const ZDistributeDocumentRequestSchema = z.object({ language: ZDocumentMetaLanguageSchema.optional(), emailId: z.string().nullish(), emailReplyTo: zEmail().nullish(), - emailSettings: ZDocumentEmailSettingsSchema.optional(), + emailSettings: ZDocumentEmailSettingsInputSchema.optional(), }) .optional(), }); diff --git a/packages/trpc/server/embedding-router/create-embedding-document.types.ts b/packages/trpc/server/embedding-router/create-embedding-document.types.ts index 0c15f08b3..fce43ae94 100644 --- a/packages/trpc/server/embedding-router/create-embedding-document.types.ts +++ b/packages/trpc/server/embedding-router/create-embedding-document.types.ts @@ -1,4 +1,4 @@ -import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email'; +import { ZDocumentEmailSettingsInputSchema } from '@documenso/lib/types/document-email'; import { ZDocumentMetaDateFormatSchema, ZDocumentMetaDistributionMethodSchema, @@ -66,7 +66,7 @@ export const ZCreateEmbeddingDocumentRequestSchema = z.object({ typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(), drawSignatureEnabled: ZDocumentMetaDrawSignatureEnabledSchema.optional(), uploadSignatureEnabled: ZDocumentMetaUploadSignatureEnabledSchema.optional(), - emailSettings: ZDocumentEmailSettingsSchema.optional(), + emailSettings: ZDocumentEmailSettingsInputSchema.optional(), }) .optional(), }); diff --git a/packages/trpc/server/embedding-router/create-embedding-template.types.ts b/packages/trpc/server/embedding-router/create-embedding-template.types.ts index 05d50b7f3..996293695 100644 --- a/packages/trpc/server/embedding-router/create-embedding-template.types.ts +++ b/packages/trpc/server/embedding-router/create-embedding-template.types.ts @@ -1,4 +1,4 @@ -import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email'; +import { ZDocumentEmailSettingsInputSchema } from '@documenso/lib/types/document-email'; import { ZDocumentMetaDateFormatSchema, ZDocumentMetaDistributionMethodSchema, @@ -62,7 +62,7 @@ export const ZCreateEmbeddingTemplateRequestSchema = z.object({ typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(), drawSignatureEnabled: ZDocumentMetaDrawSignatureEnabledSchema.optional(), uploadSignatureEnabled: ZDocumentMetaUploadSignatureEnabledSchema.optional(), - emailSettings: ZDocumentEmailSettingsSchema.optional(), + emailSettings: ZDocumentEmailSettingsInputSchema.optional(), }) .optional(), }); diff --git a/packages/trpc/server/embedding-router/update-embedding-document.types.ts b/packages/trpc/server/embedding-router/update-embedding-document.types.ts index 238499c1c..7ae48565a 100644 --- a/packages/trpc/server/embedding-router/update-embedding-document.types.ts +++ b/packages/trpc/server/embedding-router/update-embedding-document.types.ts @@ -1,4 +1,4 @@ -import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email'; +import { ZDocumentEmailSettingsInputSchema } from '@documenso/lib/types/document-email'; import { ZDocumentMetaDateFormatSchema, ZDocumentMetaDistributionMethodSchema, @@ -67,7 +67,7 @@ export const ZUpdateEmbeddingDocumentRequestSchema = z.object({ typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(), drawSignatureEnabled: ZDocumentMetaDrawSignatureEnabledSchema.optional(), uploadSignatureEnabled: ZDocumentMetaUploadSignatureEnabledSchema.optional(), - emailSettings: ZDocumentEmailSettingsSchema.optional(), + emailSettings: ZDocumentEmailSettingsInputSchema.optional(), }) .optional(), }); diff --git a/packages/trpc/server/embedding-router/update-embedding-template.types.ts b/packages/trpc/server/embedding-router/update-embedding-template.types.ts index 3f7103057..81c5dd7de 100644 --- a/packages/trpc/server/embedding-router/update-embedding-template.types.ts +++ b/packages/trpc/server/embedding-router/update-embedding-template.types.ts @@ -1,4 +1,4 @@ -import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email'; +import { ZDocumentEmailSettingsInputSchema } from '@documenso/lib/types/document-email'; import { ZDocumentMetaDateFormatSchema, ZDocumentMetaDistributionMethodSchema, @@ -66,7 +66,7 @@ export const ZUpdateEmbeddingTemplateRequestSchema = z.object({ typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(), drawSignatureEnabled: ZDocumentMetaDrawSignatureEnabledSchema.optional(), uploadSignatureEnabled: ZDocumentMetaUploadSignatureEnabledSchema.optional(), - emailSettings: ZDocumentEmailSettingsSchema.optional(), + emailSettings: ZDocumentEmailSettingsInputSchema.optional(), }) .optional(), }); diff --git a/packages/trpc/server/envelope-router/use-envelope.types.ts b/packages/trpc/server/envelope-router/use-envelope.types.ts index cd9400286..d7b66488d 100644 --- a/packages/trpc/server/envelope-router/use-envelope.types.ts +++ b/packages/trpc/server/envelope-router/use-envelope.types.ts @@ -1,5 +1,5 @@ import { ZEnvelopeExpirationPeriod } from '@documenso/lib/constants/envelope-expiration'; -import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email'; +import { ZDocumentEmailSettingsInputSchema } from '@documenso/lib/types/document-email'; import { ZDocumentFormValuesSchema } from '@documenso/lib/types/document-form-values'; import { ZDocumentMetaDateFormatSchema, @@ -89,7 +89,7 @@ export const ZUseEnvelopePayloadSchema = z.object({ dateFormat: ZDocumentMetaDateFormatSchema.optional(), redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(), distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(), - emailSettings: ZDocumentEmailSettingsSchema.optional(), + emailSettings: ZDocumentEmailSettingsInputSchema.optional(), language: ZDocumentMetaLanguageSchema.optional(), typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(), uploadSignatureEnabled: ZDocumentMetaUploadSignatureEnabledSchema.optional(), diff --git a/packages/trpc/server/organisation-router/update-organisation-settings.types.ts b/packages/trpc/server/organisation-router/update-organisation-settings.types.ts index 62caa07fc..57152296f 100644 --- a/packages/trpc/server/organisation-router/update-organisation-settings.types.ts +++ b/packages/trpc/server/organisation-router/update-organisation-settings.types.ts @@ -4,7 +4,7 @@ import { ZEnvelopeReminderSettings } from '@documenso/lib/constants/envelope-rem import { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n'; import { ZCssVarsSchema } from '@documenso/lib/types/css-vars'; import { ZDefaultRecipientsSchema } from '@documenso/lib/types/default-recipients'; -import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email'; +import { ZDocumentEmailSettingsInputSchema } from '@documenso/lib/types/document-email'; import { ZDocumentMetaDateFormatSchema, ZDocumentMetaTimezoneSchema } from '@documenso/lib/types/document-meta'; import { DocumentVisibility } from '@documenso/lib/types/document-visibility'; import { ZSanitizeBrandingCssWarningSchema } from '@documenso/lib/utils/sanitize-branding-css'; @@ -41,7 +41,7 @@ export const ZUpdateOrganisationSettingsRequestSchema = z.object({ emailId: z.string().nullish(), emailReplyTo: zEmail().nullish(), // emailReplyToName: z.string().optional(), - emailDocumentSettings: ZDocumentEmailSettingsSchema.optional(), + emailDocumentSettings: ZDocumentEmailSettingsInputSchema.optional(), // AI features settings. aiFeaturesEnabled: z.boolean().optional(), diff --git a/packages/trpc/server/team-router/update-team-settings.types.ts b/packages/trpc/server/team-router/update-team-settings.types.ts index 72990ce77..921b6475d 100644 --- a/packages/trpc/server/team-router/update-team-settings.types.ts +++ b/packages/trpc/server/team-router/update-team-settings.types.ts @@ -4,7 +4,7 @@ import { ZEnvelopeReminderSettings } from '@documenso/lib/constants/envelope-rem import { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n'; import { ZCssVarsSchema } from '@documenso/lib/types/css-vars'; import { ZDefaultRecipientsSchema } from '@documenso/lib/types/default-recipients'; -import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email'; +import { ZDocumentEmailSettingsInputSchema } from '@documenso/lib/types/document-email'; import { ZDocumentMetaDateFormatSchema, ZDocumentMetaTimezoneSchema } from '@documenso/lib/types/document-meta'; import { DocumentVisibility } from '@documenso/lib/types/document-visibility'; import { ZSanitizeBrandingCssWarningSchema } from '@documenso/lib/utils/sanitize-branding-css'; @@ -44,7 +44,7 @@ export const ZUpdateTeamSettingsRequestSchema = z.object({ emailId: z.string().nullish(), emailReplyTo: zEmail().nullish(), // emailReplyToName: z.string().nullish(), - emailDocumentSettings: ZDocumentEmailSettingsSchema.nullish(), + emailDocumentSettings: ZDocumentEmailSettingsInputSchema.nullish(), // Default recipients settings. defaultRecipients: ZDefaultRecipientsSchema.nullish(), diff --git a/packages/trpc/server/template-router/schema.ts b/packages/trpc/server/template-router/schema.ts index 913828b28..e73fb83a3 100644 --- a/packages/trpc/server/template-router/schema.ts +++ b/packages/trpc/server/template-router/schema.ts @@ -1,7 +1,7 @@ import { ZEnvelopeExpirationPeriod } from '@documenso/lib/constants/envelope-expiration'; import { ZDocumentSchema } from '@documenso/lib/types/document'; import { ZDocumentAccessAuthTypesSchema, ZDocumentActionAuthTypesSchema } from '@documenso/lib/types/document-auth'; -import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email'; +import { ZDocumentEmailSettingsInputSchema } from '@documenso/lib/types/document-email'; import { ZDocumentFormValuesSchema } from '@documenso/lib/types/document-form-values'; import { ZDocumentMetaDateFormatSchema, @@ -60,7 +60,7 @@ export const ZTemplateMetaUpsertSchema = z.object({ distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(), emailId: z.string().nullish(), emailReplyTo: zEmail().nullish(), - emailSettings: ZDocumentEmailSettingsSchema.optional(), + emailSettings: ZDocumentEmailSettingsInputSchema.optional(), redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(), language: ZDocumentMetaLanguageSchema.optional(), typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(), @@ -142,7 +142,7 @@ export const ZCreateDocumentFromTemplateRequestSchema = z.object({ dateFormat: ZDocumentMetaDateFormatSchema.optional(), redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(), distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(), - emailSettings: ZDocumentEmailSettingsSchema.optional(), + emailSettings: ZDocumentEmailSettingsInputSchema.optional(), language: ZDocumentMetaLanguageSchema.optional(), typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(), uploadSignatureEnabled: ZDocumentMetaUploadSignatureEnabledSchema.optional(), diff --git a/packages/ui/components/document/document-email-checkboxes.tsx b/packages/ui/components/document/document-email-checkboxes.tsx index 0d2a44dd8..5a28579ac 100644 --- a/packages/ui/components/document/document-email-checkboxes.tsx +++ b/packages/ui/components/document/document-email-checkboxes.tsx @@ -21,6 +21,33 @@ export const DocumentEmailCheckboxes = ({ value, onChange, className, hiddenEven return (
+
+ onChange({ ...value, attachDocument: Boolean(checked) })} + /> + + +
{!isHidden(DocumentEmailEvents.RecipientSigned) && (