Files
documenso/packages/lib/server-only/document/send-completed-email.ts
Catalin Pit 23a0537648 feat: add global settings for teams (#1391)
## Description

This PR introduces global settings for teams. At the moment, it allows
team admins to configure the following:
* The default visibility of the documents uploaded to the team account
* Whether to include the document owner (sender) details when sending
emails to the recipients.

### Include Sender Details

If the Sender Details setting is enabled, the emails sent by the team
will include the sender's name:

> "Example User" on behalf of "Example Team" has invited you to sign
"document.pdf"

Otherwise, the email will say:

> "Example Team" has invited you to sign "document.pdf"

### Default Document Visibility

This new option allows users to set the default visibility for the
documents uploaded to the team account. It can have the following
values:
* Everyone
* Manager and above
* Admins only

If the default document visibility isn't set, the document will be set
to the role of the user who created the document:
* If a user with the "User" role creates a document, the document's
visibility is set to "Everyone".
* Manager role -> "Manager and above"
* Admin role -> "Admins only"

Otherwise, if there is a default document visibility value, it uses that
value.

#### Gotcha

To avoid issues, the `document owner` and the `recipient` can access the
document irrespective of their role. For example:
* If a team member with the role "Member" uploads a document and the
default document visibility is "Admins", only the document owner and
admins can access the document.
  * Similar to the other scenarios.

* If an admin uploads a document and the default document visibility is
"Admins", the recipient can access the document.

* The admins have access to all the documents.
* Managers have access to documents with the visibility set to
"Everyone" and "Manager and above"
* Members have access only to the documents with the visibility set to
"Everyone".

## Testing Performed

Tested it locally.
2024-11-08 22:50:49 +11:00

221 lines
6.9 KiB
TypeScript

import { createElement } from 'react';
import { msg } from '@lingui/macro';
import { mailer } from '@documenso/email/mailer';
import { DocumentCompletedEmailTemplate } from '@documenso/email/templates/document-completed';
import { prisma } from '@documenso/prisma';
import { DocumentSource } from '@documenso/prisma/client';
import { getI18nInstance } from '../../client-only/providers/i18n.server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
import type { RequestMetadata } from '../../universal/extract-request-metadata';
import { getFile } from '../../universal/upload/get-file';
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
import { renderCustomEmailTemplate } from '../../utils/render-custom-email-template';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
import { teamGlobalSettingsToBranding } from '../../utils/team-global-settings-to-branding';
export interface SendDocumentOptions {
documentId: number;
requestMetadata?: RequestMetadata;
}
export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDocumentOptions) => {
const document = await prisma.document.findUnique({
where: {
id: documentId,
},
include: {
documentData: true,
documentMeta: true,
Recipient: true,
User: true,
team: {
select: {
id: true,
url: true,
teamGlobalSettings: true,
},
},
},
});
if (!document) {
throw new Error('Document not found');
}
const isDirectTemplate = document?.source === DocumentSource.TEMPLATE_DIRECT_LINK;
if (document.Recipient.length === 0) {
throw new Error('Document has no recipients');
}
const { User: owner } = document;
const completedDocument = await getFile(document.documentData);
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
let documentOwnerDownloadLink = `${NEXT_PUBLIC_WEBAPP_URL()}/documents/${document.id}`;
if (document.team?.url) {
documentOwnerDownloadLink = `${NEXT_PUBLIC_WEBAPP_URL()}/t/${document.team.url}/documents/${
document.id
}`;
}
const i18n = await getI18nInstance(document.documentMeta?.language);
const isDocumentCompletedEmailEnabled = extractDerivedDocumentEmailSettings(
document.documentMeta,
).documentCompleted;
// If the document owner is not a recipient, OR recipient emails are disabled, then send the email to them separately.
if (
!document.Recipient.find((recipient) => recipient.email === owner.email) ||
!isDocumentCompletedEmailEnabled
) {
const template = createElement(DocumentCompletedEmailTemplate, {
documentName: document.title,
assetBaseUrl,
downloadLink: documentOwnerDownloadLink,
});
const branding = document.team?.teamGlobalSettings
? teamGlobalSettingsToBranding(document.team.teamGlobalSettings)
: undefined;
const [html, text] = await Promise.all([
renderEmailWithI18N(template, { lang: document.documentMeta?.language, branding }),
renderEmailWithI18N(template, {
lang: document.documentMeta?.language,
branding,
plainText: true,
}),
]);
await mailer.sendMail({
to: [
{
name: owner.name || '',
address: owner.email,
},
],
from: {
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
},
subject: i18n._(msg`Signing Complete!`),
html,
text,
attachments: [
{
filename: document.title.endsWith('.pdf') ? document.title : document.title + '.pdf',
content: Buffer.from(completedDocument),
},
],
});
await prisma.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
documentId: document.id,
user: null,
requestMetadata,
data: {
emailType: 'DOCUMENT_COMPLETED',
recipientEmail: owner.email,
recipientName: owner.name ?? '',
recipientId: owner.id,
recipientRole: 'OWNER',
isResending: false,
},
}),
});
}
if (!isDocumentCompletedEmailEnabled) {
return;
}
await Promise.all(
document.Recipient.map(async (recipient) => {
const customEmailTemplate = {
'signer.name': recipient.name,
'signer.email': recipient.email,
'document.name': document.title,
};
const downloadLink = `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}/complete`;
const template = createElement(DocumentCompletedEmailTemplate, {
documentName: document.title,
assetBaseUrl,
downloadLink: recipient.email === owner.email ? documentOwnerDownloadLink : downloadLink,
customBody:
isDirectTemplate && document.documentMeta?.message
? renderCustomEmailTemplate(document.documentMeta.message, customEmailTemplate)
: undefined,
});
const branding = document.team?.teamGlobalSettings
? teamGlobalSettingsToBranding(document.team.teamGlobalSettings)
: undefined;
const [html, text] = await Promise.all([
renderEmailWithI18N(template, { lang: document.documentMeta?.language, branding }),
renderEmailWithI18N(template, {
lang: document.documentMeta?.language,
branding,
plainText: true,
}),
]);
await mailer.sendMail({
to: [
{
name: recipient.name,
address: recipient.email,
},
],
from: {
name: process.env.NEXT_PRIVATE_SMTP_FROM_NAME || 'Documenso',
address: process.env.NEXT_PRIVATE_SMTP_FROM_ADDRESS || 'noreply@documenso.com',
},
subject:
isDirectTemplate && document.documentMeta?.subject
? renderCustomEmailTemplate(document.documentMeta.subject, customEmailTemplate)
: i18n._(msg`Signing Complete!`),
html,
text,
attachments: [
{
filename: document.title.endsWith('.pdf') ? document.title : document.title + '.pdf',
content: Buffer.from(completedDocument),
},
],
});
await prisma.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
documentId: document.id,
user: null,
requestMetadata,
data: {
emailType: 'DOCUMENT_COMPLETED',
recipientEmail: recipient.email,
recipientName: recipient.name,
recipientId: recipient.id,
recipientRole: recipient.role,
isResending: false,
},
}),
});
}),
);
};