diff --git a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx
new file mode 100644
index 000000000..e13bcd030
--- /dev/null
+++ b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.branding.tsx
@@ -0,0 +1,94 @@
+import { useLingui } from '@lingui/react/macro';
+import { Loader } from 'lucide-react';
+
+import { putFile } from '@documenso/lib/universal/upload/put-file';
+import { trpc } from '@documenso/trpc/react';
+import { useToast } from '@documenso/ui/primitives/use-toast';
+
+import {
+ BrandingPreferencesForm,
+ type TBrandingPreferencesFormSchema,
+} from '~/components/forms/branding-preferences-form';
+import { SettingsHeader } from '~/components/general/settings-header';
+import { useCurrentTeam } from '~/providers/team';
+import { appMetaTags } from '~/utils/meta';
+
+export function meta() {
+ return appMetaTags('Branding Preferences');
+}
+
+export default function TeamsSettingsPage() {
+ const team = useCurrentTeam();
+
+ const { t } = useLingui();
+ const { toast } = useToast();
+
+ const { data: teamWithSettings, isLoading: isLoadingTeam } = trpc.team.get.useQuery({
+ teamReference: team.id,
+ });
+
+ const { mutateAsync: updateTeamSettings } = trpc.team.settings.update.useMutation();
+
+ const onBrandingPreferencesFormSubmit = async (data: TBrandingPreferencesFormSchema) => {
+ try {
+ const { brandingEnabled, brandingLogo, brandingUrl, brandingCompanyDetails } = data;
+
+ let uploadedBrandingLogo = teamWithSettings?.teamSettings?.brandingLogo;
+
+ if (brandingLogo) {
+ uploadedBrandingLogo = JSON.stringify(await putFile(brandingLogo));
+ }
+
+ if (brandingLogo === null) {
+ uploadedBrandingLogo = '';
+ }
+
+ await updateTeamSettings({
+ teamId: team.id,
+ data: {
+ brandingEnabled,
+ brandingLogo: uploadedBrandingLogo || null,
+ brandingUrl: brandingUrl || null,
+ brandingCompanyDetails: brandingCompanyDetails || null,
+ },
+ });
+
+ toast({
+ title: t`Branding preferences updated`,
+ description: t`Your branding preferences have been updated`,
+ });
+ } catch (err) {
+ toast({
+ title: t`Something went wrong`,
+ description: t`We were unable to update your branding preferences at this time, please try again later`,
+ variant: 'destructive',
+ });
+ }
+ };
+
+ if (isLoadingTeam || !teamWithSettings) {
+ return (
+
+ );
+}
diff --git a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.preferences.tsx b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.document.tsx
similarity index 61%
rename from apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.preferences.tsx
rename to apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.document.tsx
index c7d631de9..45c376077 100644
--- a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.preferences.tsx
+++ b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.document.tsx
@@ -2,14 +2,9 @@ import { useLingui } from '@lingui/react/macro';
import { Loader } from 'lucide-react';
import { DocumentSignatureType } from '@documenso/lib/constants/document';
-import { putFile } from '@documenso/lib/universal/upload/put-file';
import { trpc } from '@documenso/trpc/react';
import { useToast } from '@documenso/ui/primitives/use-toast';
-import {
- BrandingPreferencesForm,
- type TBrandingPreferencesFormSchema,
-} from '~/components/forms/branding-preferences-form';
import {
DocumentPreferencesForm,
type TDocumentPreferencesFormSchema,
@@ -19,7 +14,7 @@ import { useCurrentTeam } from '~/providers/team';
import { appMetaTags } from '~/utils/meta';
export function meta() {
- return appMetaTags('Preferences');
+ return appMetaTags('Document Preferences');
}
export default function TeamsSettingsPage() {
@@ -39,6 +34,8 @@ export default function TeamsSettingsPage() {
const {
documentVisibility,
documentLanguage,
+ documentTimezone,
+ documentDateFormat,
includeSenderDetails,
includeSigningCertificate,
signatureTypes,
@@ -49,6 +46,8 @@ export default function TeamsSettingsPage() {
data: {
documentVisibility,
documentLanguage,
+ documentTimezone,
+ documentDateFormat,
includeSenderDetails,
includeSigningCertificate,
...(signatureTypes.length === 0
@@ -78,43 +77,6 @@ export default function TeamsSettingsPage() {
}
};
- const onBrandingPreferencesFormSubmit = async (data: TBrandingPreferencesFormSchema) => {
- try {
- const { brandingEnabled, brandingLogo, brandingUrl, brandingCompanyDetails } = data;
-
- let uploadedBrandingLogo = teamWithSettings?.teamSettings?.brandingLogo;
-
- if (brandingLogo) {
- uploadedBrandingLogo = JSON.stringify(await putFile(brandingLogo));
- }
-
- if (brandingLogo === null) {
- uploadedBrandingLogo = '';
- }
-
- await updateTeamSettings({
- teamId: team.id,
- data: {
- brandingEnabled,
- brandingLogo: uploadedBrandingLogo || null,
- brandingUrl: brandingUrl || null,
- brandingCompanyDetails: brandingCompanyDetails || null,
- },
- });
-
- toast({
- title: t`Branding preferences updated`,
- description: t`Your branding preferences have been updated`,
- });
- } catch (err) {
- toast({
- title: t`Something went wrong`,
- description: t`We were unable to update your branding preferences at this time, please try again later`,
- variant: 'destructive',
- });
- }
- };
-
if (isLoadingTeam || !teamWithSettings) {
return (
@@ -126,7 +88,7 @@ export default function TeamsSettingsPage() {
return (
@@ -137,21 +99,6 @@ export default function TeamsSettingsPage() {
onFormSubmit={onDocumentPreferencesSubmit}
/>
-
-
-
-
);
}
diff --git a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.email.tsx b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.email.tsx
new file mode 100644
index 000000000..f1fed0374
--- /dev/null
+++ b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.email.tsx
@@ -0,0 +1,78 @@
+import { useLingui } from '@lingui/react/macro';
+
+import { trpc } from '@documenso/trpc/react';
+import { SpinnerBox } from '@documenso/ui/primitives/spinner';
+import { useToast } from '@documenso/ui/primitives/use-toast';
+
+import {
+ EmailPreferencesForm,
+ type TEmailPreferencesFormSchema,
+} from '~/components/forms/email-preferences-form';
+import { SettingsHeader } from '~/components/general/settings-header';
+import { useCurrentTeam } from '~/providers/team';
+import { appMetaTags } from '~/utils/meta';
+
+export function meta() {
+ return appMetaTags('Settings');
+}
+
+export default function TeamEmailSettingsGeneral() {
+ const { t } = useLingui();
+ const { toast } = useToast();
+
+ const team = useCurrentTeam();
+
+ const { data: teamWithSettings, isLoading: isLoadingTeam } = trpc.team.get.useQuery({
+ teamReference: team.url,
+ });
+
+ const { mutateAsync: updateTeamSettings } = trpc.team.settings.update.useMutation();
+
+ const onEmailPreferencesSubmit = async (data: TEmailPreferencesFormSchema) => {
+ try {
+ const { emailId, emailReplyTo, emailDocumentSettings } = data;
+
+ await updateTeamSettings({
+ teamId: team.id,
+ data: {
+ emailId,
+ emailReplyTo,
+ // emailReplyToName,
+ emailDocumentSettings,
+ },
+ });
+
+ toast({
+ title: t`Email preferences updated`,
+ description: t`Your email preferences have been updated`,
+ });
+ } catch (err) {
+ toast({
+ title: t`Something went wrong!`,
+ description: t`We were unable to update your email preferences at this time, please try again later`,
+ variant: 'destructive',
+ });
+ }
+ };
+
+ if (isLoadingTeam || !teamWithSettings) {
+ return
;
+ }
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/package-lock.json b/package-lock.json
index da1f7c26f..e07bdf900 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -514,6 +514,534 @@
"node": ">=18.0.0"
}
},
+ "node_modules/@aws-sdk/client-sesv2": {
+ "version": "3.828.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sesv2/-/client-sesv2-3.828.0.tgz",
+ "integrity": "sha512-pJQE+D1Su2sbYcSwqgq/fgkPMY1c/h90ntvwknGiTlHJRWoF8MRHJ65+PaLBiS6nGvjEtTE1+Y1YQDA7etrRNg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/sha256-browser": "5.2.0",
+ "@aws-crypto/sha256-js": "5.2.0",
+ "@aws-sdk/core": "3.826.0",
+ "@aws-sdk/credential-provider-node": "3.828.0",
+ "@aws-sdk/middleware-host-header": "3.821.0",
+ "@aws-sdk/middleware-logger": "3.821.0",
+ "@aws-sdk/middleware-recursion-detection": "3.821.0",
+ "@aws-sdk/middleware-user-agent": "3.828.0",
+ "@aws-sdk/region-config-resolver": "3.821.0",
+ "@aws-sdk/signature-v4-multi-region": "3.826.0",
+ "@aws-sdk/types": "3.821.0",
+ "@aws-sdk/util-endpoints": "3.828.0",
+ "@aws-sdk/util-user-agent-browser": "3.821.0",
+ "@aws-sdk/util-user-agent-node": "3.828.0",
+ "@smithy/config-resolver": "^4.1.4",
+ "@smithy/core": "^3.5.3",
+ "@smithy/fetch-http-handler": "^5.0.4",
+ "@smithy/hash-node": "^4.0.4",
+ "@smithy/invalid-dependency": "^4.0.4",
+ "@smithy/middleware-content-length": "^4.0.4",
+ "@smithy/middleware-endpoint": "^4.1.11",
+ "@smithy/middleware-retry": "^4.1.12",
+ "@smithy/middleware-serde": "^4.0.8",
+ "@smithy/middleware-stack": "^4.0.4",
+ "@smithy/node-config-provider": "^4.1.3",
+ "@smithy/node-http-handler": "^4.0.6",
+ "@smithy/protocol-http": "^5.1.2",
+ "@smithy/smithy-client": "^4.4.3",
+ "@smithy/types": "^4.3.1",
+ "@smithy/url-parser": "^4.0.4",
+ "@smithy/util-base64": "^4.0.0",
+ "@smithy/util-body-length-browser": "^4.0.0",
+ "@smithy/util-body-length-node": "^4.0.0",
+ "@smithy/util-defaults-mode-browser": "^4.0.19",
+ "@smithy/util-defaults-mode-node": "^4.0.19",
+ "@smithy/util-endpoints": "^3.0.6",
+ "@smithy/util-middleware": "^4.0.4",
+ "@smithy/util-retry": "^4.0.5",
+ "@smithy/util-utf8": "^4.0.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/client-sso": {
+ "version": "3.828.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.828.0.tgz",
+ "integrity": "sha512-qxw8JcPTaFaBwTBUr4YmLajaMh3En65SuBWAKEtjctbITRRekzR7tvr/TkwoyVOh+XoAtkwOn+BQeQbX+/wgHw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/sha256-browser": "5.2.0",
+ "@aws-crypto/sha256-js": "5.2.0",
+ "@aws-sdk/core": "3.826.0",
+ "@aws-sdk/middleware-host-header": "3.821.0",
+ "@aws-sdk/middleware-logger": "3.821.0",
+ "@aws-sdk/middleware-recursion-detection": "3.821.0",
+ "@aws-sdk/middleware-user-agent": "3.828.0",
+ "@aws-sdk/region-config-resolver": "3.821.0",
+ "@aws-sdk/types": "3.821.0",
+ "@aws-sdk/util-endpoints": "3.828.0",
+ "@aws-sdk/util-user-agent-browser": "3.821.0",
+ "@aws-sdk/util-user-agent-node": "3.828.0",
+ "@smithy/config-resolver": "^4.1.4",
+ "@smithy/core": "^3.5.3",
+ "@smithy/fetch-http-handler": "^5.0.4",
+ "@smithy/hash-node": "^4.0.4",
+ "@smithy/invalid-dependency": "^4.0.4",
+ "@smithy/middleware-content-length": "^4.0.4",
+ "@smithy/middleware-endpoint": "^4.1.11",
+ "@smithy/middleware-retry": "^4.1.12",
+ "@smithy/middleware-serde": "^4.0.8",
+ "@smithy/middleware-stack": "^4.0.4",
+ "@smithy/node-config-provider": "^4.1.3",
+ "@smithy/node-http-handler": "^4.0.6",
+ "@smithy/protocol-http": "^5.1.2",
+ "@smithy/smithy-client": "^4.4.3",
+ "@smithy/types": "^4.3.1",
+ "@smithy/url-parser": "^4.0.4",
+ "@smithy/util-base64": "^4.0.0",
+ "@smithy/util-body-length-browser": "^4.0.0",
+ "@smithy/util-body-length-node": "^4.0.0",
+ "@smithy/util-defaults-mode-browser": "^4.0.19",
+ "@smithy/util-defaults-mode-node": "^4.0.19",
+ "@smithy/util-endpoints": "^3.0.6",
+ "@smithy/util-middleware": "^4.0.4",
+ "@smithy/util-retry": "^4.0.5",
+ "@smithy/util-utf8": "^4.0.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/core": {
+ "version": "3.826.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.826.0.tgz",
+ "integrity": "sha512-BGbQYzWj3ps+dblq33FY5tz/SsgJCcXX0zjQlSC07tYvU1jHTUvsefphyig+fY38xZ4wdKjbTop+KUmXUYrOXw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.821.0",
+ "@aws-sdk/xml-builder": "3.821.0",
+ "@smithy/core": "^3.5.3",
+ "@smithy/node-config-provider": "^4.1.3",
+ "@smithy/property-provider": "^4.0.4",
+ "@smithy/protocol-http": "^5.1.2",
+ "@smithy/signature-v4": "^5.1.2",
+ "@smithy/smithy-client": "^4.4.3",
+ "@smithy/types": "^4.3.1",
+ "@smithy/util-base64": "^4.0.0",
+ "@smithy/util-body-length-browser": "^4.0.0",
+ "@smithy/util-middleware": "^4.0.4",
+ "@smithy/util-utf8": "^4.0.0",
+ "fast-xml-parser": "4.4.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-env": {
+ "version": "3.826.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.826.0.tgz",
+ "integrity": "sha512-DK3pQY8+iKK3MGDdC3uOZQ2psU01obaKlTYhEwNu4VWzgwQL4Vi3sWj4xSWGEK41vqZxiRLq6fOq7ysRI+qEZA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.826.0",
+ "@aws-sdk/types": "3.821.0",
+ "@smithy/property-provider": "^4.0.4",
+ "@smithy/types": "^4.3.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-http": {
+ "version": "3.826.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.826.0.tgz",
+ "integrity": "sha512-N+IVZBh+yx/9GbMZTKO/gErBi/FYZQtcFRItoLbY+6WU+0cSWyZYfkoeOxHmQV3iX9k65oljERIWUmL9x6OSQg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.826.0",
+ "@aws-sdk/types": "3.821.0",
+ "@smithy/fetch-http-handler": "^5.0.4",
+ "@smithy/node-http-handler": "^4.0.6",
+ "@smithy/property-provider": "^4.0.4",
+ "@smithy/protocol-http": "^5.1.2",
+ "@smithy/smithy-client": "^4.4.3",
+ "@smithy/types": "^4.3.1",
+ "@smithy/util-stream": "^4.2.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-ini": {
+ "version": "3.828.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.828.0.tgz",
+ "integrity": "sha512-T3DJMo2/j7gCPpFg2+xEHWgua05t8WP89ye7PaZxA2Fc6CgScHkZsJZTri1QQIU2h+eOZ75EZWkeFLIPgN0kRQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.826.0",
+ "@aws-sdk/credential-provider-env": "3.826.0",
+ "@aws-sdk/credential-provider-http": "3.826.0",
+ "@aws-sdk/credential-provider-process": "3.826.0",
+ "@aws-sdk/credential-provider-sso": "3.828.0",
+ "@aws-sdk/credential-provider-web-identity": "3.828.0",
+ "@aws-sdk/nested-clients": "3.828.0",
+ "@aws-sdk/types": "3.821.0",
+ "@smithy/credential-provider-imds": "^4.0.6",
+ "@smithy/property-provider": "^4.0.4",
+ "@smithy/shared-ini-file-loader": "^4.0.4",
+ "@smithy/types": "^4.3.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-node": {
+ "version": "3.828.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.828.0.tgz",
+ "integrity": "sha512-9z3iPwVYOQYNzVZj8qycZaS/BOSKRXWA+QVNQlfEnQ4sA4sOcKR4kmV2h+rJcuBsSFfmOF62ZDxyIBGvvM4t/w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/credential-provider-env": "3.826.0",
+ "@aws-sdk/credential-provider-http": "3.826.0",
+ "@aws-sdk/credential-provider-ini": "3.828.0",
+ "@aws-sdk/credential-provider-process": "3.826.0",
+ "@aws-sdk/credential-provider-sso": "3.828.0",
+ "@aws-sdk/credential-provider-web-identity": "3.828.0",
+ "@aws-sdk/types": "3.821.0",
+ "@smithy/credential-provider-imds": "^4.0.6",
+ "@smithy/property-provider": "^4.0.4",
+ "@smithy/shared-ini-file-loader": "^4.0.4",
+ "@smithy/types": "^4.3.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-process": {
+ "version": "3.826.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.826.0.tgz",
+ "integrity": "sha512-kURrc4amu3NLtw1yZw7EoLNEVhmOMRUTs+chaNcmS+ERm3yK0nKjaJzmKahmwlTQTSl3wJ8jjK7x962VPo+zWw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.826.0",
+ "@aws-sdk/types": "3.821.0",
+ "@smithy/property-provider": "^4.0.4",
+ "@smithy/shared-ini-file-loader": "^4.0.4",
+ "@smithy/types": "^4.3.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-sso": {
+ "version": "3.828.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.828.0.tgz",
+ "integrity": "sha512-9CEAXzUDSzOjOCb3XfM15TZhTaM+l07kumZyx2z8NC6T2U4qbCJqn4h8mFlRvYrs6cBj2SN40sD3r5Wp0Cq2Kw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/client-sso": "3.828.0",
+ "@aws-sdk/core": "3.826.0",
+ "@aws-sdk/token-providers": "3.828.0",
+ "@aws-sdk/types": "3.821.0",
+ "@smithy/property-provider": "^4.0.4",
+ "@smithy/shared-ini-file-loader": "^4.0.4",
+ "@smithy/types": "^4.3.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-web-identity": {
+ "version": "3.828.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.828.0.tgz",
+ "integrity": "sha512-MguDhGHlQBeK9CQ/P4NOY0whAJ4HJU4x+f1dphg3I1sGlccFqfB8Moor2vXNKu0Th2kvAwkn9pr7gGb/+NGR9g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.826.0",
+ "@aws-sdk/nested-clients": "3.828.0",
+ "@aws-sdk/types": "3.821.0",
+ "@smithy/property-provider": "^4.0.4",
+ "@smithy/types": "^4.3.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/middleware-host-header": {
+ "version": "3.821.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.821.0.tgz",
+ "integrity": "sha512-xSMR+sopSeWGx5/4pAGhhfMvGBHioVBbqGvDs6pG64xfNwM5vq5s5v6D04e2i+uSTj4qGa71dLUs5I0UzAK3sw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.821.0",
+ "@smithy/protocol-http": "^5.1.2",
+ "@smithy/types": "^4.3.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/middleware-logger": {
+ "version": "3.821.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.821.0.tgz",
+ "integrity": "sha512-0cvI0ipf2tGx7fXYEEN5fBeZDz2RnHyb9xftSgUsEq7NBxjV0yTZfLJw6Za5rjE6snC80dRN8+bTNR1tuG89zA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.821.0",
+ "@smithy/types": "^4.3.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/middleware-recursion-detection": {
+ "version": "3.821.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.821.0.tgz",
+ "integrity": "sha512-efmaifbhBoqKG3bAoEfDdcM8hn1psF+4qa7ykWuYmfmah59JBeqHLfz5W9m9JoTwoKPkFcVLWZxnyZzAnVBOIg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.821.0",
+ "@smithy/protocol-http": "^5.1.2",
+ "@smithy/types": "^4.3.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/middleware-sdk-s3": {
+ "version": "3.826.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.826.0.tgz",
+ "integrity": "sha512-8F0qWaYKfvD/de1AKccXuigM+gb/IZSncCqxdnFWqd+TFzo9qI9Hh+TpUhWOMYSgxsMsYQ8ipmLzlD/lDhjrmA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.826.0",
+ "@aws-sdk/types": "3.821.0",
+ "@aws-sdk/util-arn-parser": "3.804.0",
+ "@smithy/core": "^3.5.3",
+ "@smithy/node-config-provider": "^4.1.3",
+ "@smithy/protocol-http": "^5.1.2",
+ "@smithy/signature-v4": "^5.1.2",
+ "@smithy/smithy-client": "^4.4.3",
+ "@smithy/types": "^4.3.1",
+ "@smithy/util-config-provider": "^4.0.0",
+ "@smithy/util-middleware": "^4.0.4",
+ "@smithy/util-stream": "^4.2.2",
+ "@smithy/util-utf8": "^4.0.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/middleware-user-agent": {
+ "version": "3.828.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.828.0.tgz",
+ "integrity": "sha512-nixvI/SETXRdmrVab4D9LvXT3lrXkwAWGWk2GVvQvzlqN1/M/RfClj+o37Sn4FqRkGH9o9g7Fqb1YqZ4mqDAtA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.826.0",
+ "@aws-sdk/types": "3.821.0",
+ "@aws-sdk/util-endpoints": "3.828.0",
+ "@smithy/core": "^3.5.3",
+ "@smithy/protocol-http": "^5.1.2",
+ "@smithy/types": "^4.3.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/nested-clients": {
+ "version": "3.828.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.828.0.tgz",
+ "integrity": "sha512-xmeOILiR9LvfC8MctgeRXXN8nQTwbOvO4wHvgE8tDRsjnBpyyO0j50R4+viHXdMUGtgGkHEXRv8fFNBq54RgnA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/sha256-browser": "5.2.0",
+ "@aws-crypto/sha256-js": "5.2.0",
+ "@aws-sdk/core": "3.826.0",
+ "@aws-sdk/middleware-host-header": "3.821.0",
+ "@aws-sdk/middleware-logger": "3.821.0",
+ "@aws-sdk/middleware-recursion-detection": "3.821.0",
+ "@aws-sdk/middleware-user-agent": "3.828.0",
+ "@aws-sdk/region-config-resolver": "3.821.0",
+ "@aws-sdk/types": "3.821.0",
+ "@aws-sdk/util-endpoints": "3.828.0",
+ "@aws-sdk/util-user-agent-browser": "3.821.0",
+ "@aws-sdk/util-user-agent-node": "3.828.0",
+ "@smithy/config-resolver": "^4.1.4",
+ "@smithy/core": "^3.5.3",
+ "@smithy/fetch-http-handler": "^5.0.4",
+ "@smithy/hash-node": "^4.0.4",
+ "@smithy/invalid-dependency": "^4.0.4",
+ "@smithy/middleware-content-length": "^4.0.4",
+ "@smithy/middleware-endpoint": "^4.1.11",
+ "@smithy/middleware-retry": "^4.1.12",
+ "@smithy/middleware-serde": "^4.0.8",
+ "@smithy/middleware-stack": "^4.0.4",
+ "@smithy/node-config-provider": "^4.1.3",
+ "@smithy/node-http-handler": "^4.0.6",
+ "@smithy/protocol-http": "^5.1.2",
+ "@smithy/smithy-client": "^4.4.3",
+ "@smithy/types": "^4.3.1",
+ "@smithy/url-parser": "^4.0.4",
+ "@smithy/util-base64": "^4.0.0",
+ "@smithy/util-body-length-browser": "^4.0.0",
+ "@smithy/util-body-length-node": "^4.0.0",
+ "@smithy/util-defaults-mode-browser": "^4.0.19",
+ "@smithy/util-defaults-mode-node": "^4.0.19",
+ "@smithy/util-endpoints": "^3.0.6",
+ "@smithy/util-middleware": "^4.0.4",
+ "@smithy/util-retry": "^4.0.5",
+ "@smithy/util-utf8": "^4.0.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/region-config-resolver": {
+ "version": "3.821.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.821.0.tgz",
+ "integrity": "sha512-t8og+lRCIIy5nlId0bScNpCkif8sc0LhmtaKsbm0ZPm3sCa/WhCbSZibjbZ28FNjVCV+p0D9RYZx0VDDbtWyjw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.821.0",
+ "@smithy/node-config-provider": "^4.1.3",
+ "@smithy/types": "^4.3.1",
+ "@smithy/util-config-provider": "^4.0.0",
+ "@smithy/util-middleware": "^4.0.4",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/signature-v4-multi-region": {
+ "version": "3.826.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.826.0.tgz",
+ "integrity": "sha512-3fEi/zy6tpMzomYosksGtu7jZqGFcdBXoL7YRsG7OEeQzBbOW9B+fVaQZ4jnsViSjzA/yKydLahMrfPnt+iaxg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/middleware-sdk-s3": "3.826.0",
+ "@aws-sdk/types": "3.821.0",
+ "@smithy/protocol-http": "^5.1.2",
+ "@smithy/signature-v4": "^5.1.2",
+ "@smithy/types": "^4.3.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/token-providers": {
+ "version": "3.828.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.828.0.tgz",
+ "integrity": "sha512-JdOjI/TxkfQpY/bWbdGMdCiePESXTbtl6MfnJxz35zZ3tfHvBnxAWCoYJirdmjzY/j/dFo5oEyS6mQuXAG9w2w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.826.0",
+ "@aws-sdk/nested-clients": "3.828.0",
+ "@aws-sdk/types": "3.821.0",
+ "@smithy/property-provider": "^4.0.4",
+ "@smithy/shared-ini-file-loader": "^4.0.4",
+ "@smithy/types": "^4.3.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/types": {
+ "version": "3.821.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.821.0.tgz",
+ "integrity": "sha512-Znroqdai1a90TlxGaJ+FK1lwC0fHpo97Xjsp5UKGR5JODYm7f9+/fF17ebO1KdoBr/Rm0UIFiF5VmI8ts9F1eA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.3.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/util-endpoints": {
+ "version": "3.828.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.828.0.tgz",
+ "integrity": "sha512-RvKch111SblqdkPzg3oCIdlGxlQs+k+P7Etory9FmxPHyPDvsP1j1c74PmgYqtzzMWmoXTjd+c9naUHh9xG8xg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.821.0",
+ "@smithy/types": "^4.3.1",
+ "@smithy/util-endpoints": "^3.0.6",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/util-user-agent-browser": {
+ "version": "3.821.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.821.0.tgz",
+ "integrity": "sha512-irWZHyM0Jr1xhC+38OuZ7JB6OXMLPZlj48thElpsO1ZSLRkLZx5+I7VV6k3sp2yZ7BYbKz/G2ojSv4wdm7XTLw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.821.0",
+ "@smithy/types": "^4.3.1",
+ "bowser": "^2.11.0",
+ "tslib": "^2.6.2"
+ }
+ },
+ "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/util-user-agent-node": {
+ "version": "3.828.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.828.0.tgz",
+ "integrity": "sha512-LdN6fTBzTlQmc8O8f1wiZN0qF3yBWVGis7NwpWK7FUEzP9bEZRxYfIkV9oV9zpt6iNRze1SedK3JQVB/udxBoA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/middleware-user-agent": "3.828.0",
+ "@aws-sdk/types": "3.821.0",
+ "@smithy/node-config-provider": "^4.1.3",
+ "@smithy/types": "^4.3.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "aws-crt": ">=1.0.0"
+ },
+ "peerDependenciesMeta": {
+ "aws-crt": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/xml-builder": {
+ "version": "3.821.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.821.0.tgz",
+ "integrity": "sha512-DIIotRnefVL6DiaHtO6/21DhJ4JZnnIwdNbpwiAhdt/AVbttcE4yw925gsjur0OGv5BTYXQXU3YnANBYnZjuQA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.3.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
"node_modules/@aws-sdk/client-sso": {
"version": "3.812.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.812.0.tgz",
@@ -9906,12 +10434,12 @@
}
},
"node_modules/@smithy/abort-controller": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.3.tgz",
- "integrity": "sha512-AqXFf6DXnuRBXy4SoK/n1mfgHaKaq36bmkphmD1KO0nHq6xK/g9KHSW4HEsPQUBCGdIEfuJifGHwxFXPIFay9Q==",
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.4.tgz",
+ "integrity": "sha512-gJnEjZMvigPDQWHrW3oPrFhQtkrgqBkyjj3pCIdF3A5M6vsZODG93KNlfJprv6bp4245bdT32fsHK4kkH3KYDA==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/types": "^4.3.0",
+ "@smithy/types": "^4.3.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -9944,15 +10472,15 @@
}
},
"node_modules/@smithy/config-resolver": {
- "version": "4.1.3",
- "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.1.3.tgz",
- "integrity": "sha512-N5e7ofiyYDmHxnPnqF8L4KtsbSDwyxFRfDK9bp1d9OyPO4ytRLd0/XxCqi5xVaaqB65v4woW8uey6jND6zxzxQ==",
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.1.4.tgz",
+ "integrity": "sha512-prmU+rDddxHOH0oNcwemL+SwnzcG65sBF2yXRO7aeXIn/xTlq2pX7JLVbkBnVLowHLg4/OL4+jBmv9hVrVGS+w==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/node-config-provider": "^4.1.2",
- "@smithy/types": "^4.3.0",
+ "@smithy/node-config-provider": "^4.1.3",
+ "@smithy/types": "^4.3.1",
"@smithy/util-config-provider": "^4.0.0",
- "@smithy/util-middleware": "^4.0.3",
+ "@smithy/util-middleware": "^4.0.4",
"tslib": "^2.6.2"
},
"engines": {
@@ -9960,17 +10488,18 @@
}
},
"node_modules/@smithy/core": {
- "version": "3.4.0",
- "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.4.0.tgz",
- "integrity": "sha512-dDYISQo7k0Ml/rXlFIjkTmTcQze/LxhtIRAEmZ6HJ/EI0inVxVEVnrUXJ7jPx6ZP0GHUhFm40iQcCgS5apXIXA==",
+ "version": "3.5.3",
+ "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.5.3.tgz",
+ "integrity": "sha512-xa5byV9fEguZNofCclv6v9ra0FYh5FATQW/da7FQUVTic94DfrN/NvmKZjrMyzbpqfot9ZjBaO8U1UeTbmSLuA==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/middleware-serde": "^4.0.6",
- "@smithy/protocol-http": "^5.1.1",
- "@smithy/types": "^4.3.0",
+ "@smithy/middleware-serde": "^4.0.8",
+ "@smithy/protocol-http": "^5.1.2",
+ "@smithy/types": "^4.3.1",
+ "@smithy/util-base64": "^4.0.0",
"@smithy/util-body-length-browser": "^4.0.0",
- "@smithy/util-middleware": "^4.0.3",
- "@smithy/util-stream": "^4.2.1",
+ "@smithy/util-middleware": "^4.0.4",
+ "@smithy/util-stream": "^4.2.2",
"@smithy/util-utf8": "^4.0.0",
"tslib": "^2.6.2"
},
@@ -9979,15 +10508,15 @@
}
},
"node_modules/@smithy/credential-provider-imds": {
- "version": "4.0.5",
- "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.0.5.tgz",
- "integrity": "sha512-saEAGwrIlkb9XxX/m5S5hOtzjoJPEK6Qw2f9pYTbIsMPOFyGSXBBTw95WbOyru8A1vIS2jVCCU1Qhz50QWG3IA==",
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.0.6.tgz",
+ "integrity": "sha512-hKMWcANhUiNbCJouYkZ9V3+/Qf9pteR1dnwgdyzR09R4ODEYx8BbUysHwRSyex4rZ9zapddZhLFTnT4ZijR4pw==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/node-config-provider": "^4.1.2",
- "@smithy/property-provider": "^4.0.3",
- "@smithy/types": "^4.3.0",
- "@smithy/url-parser": "^4.0.3",
+ "@smithy/node-config-provider": "^4.1.3",
+ "@smithy/property-provider": "^4.0.4",
+ "@smithy/types": "^4.3.1",
+ "@smithy/url-parser": "^4.0.4",
"tslib": "^2.6.2"
},
"engines": {
@@ -10065,14 +10594,14 @@
}
},
"node_modules/@smithy/fetch-http-handler": {
- "version": "5.0.3",
- "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.0.3.tgz",
- "integrity": "sha512-yBZwavI31roqTndNI7ONHqesfH01JmjJK6L3uUpZAhyAmr86LN5QiPzfyZGIxQmed8VEK2NRSQT3/JX5V1njfQ==",
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.0.4.tgz",
+ "integrity": "sha512-AMtBR5pHppYMVD7z7G+OlHHAcgAN7v0kVKEpHuTO4Gb199Gowh0taYi9oDStFeUhetkeP55JLSVlTW1n9rFtUw==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/protocol-http": "^5.1.1",
- "@smithy/querystring-builder": "^4.0.3",
- "@smithy/types": "^4.3.0",
+ "@smithy/protocol-http": "^5.1.2",
+ "@smithy/querystring-builder": "^4.0.4",
+ "@smithy/types": "^4.3.1",
"@smithy/util-base64": "^4.0.0",
"tslib": "^2.6.2"
},
@@ -10096,12 +10625,12 @@
}
},
"node_modules/@smithy/hash-node": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.0.3.tgz",
- "integrity": "sha512-W5Uhy6v/aYrgtjh9y0YP332gIQcwccQ+EcfWhllL0B9rPae42JngTTUpb8W6wuxaNFzqps4xq5klHckSSOy5fw==",
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.0.4.tgz",
+ "integrity": "sha512-qnbTPUhCVnCgBp4z4BUJUhOEkVwxiEi1cyFM+Zj6o+aY8OFGxUQleKWq8ltgp3dujuhXojIvJWdoqpm6dVO3lQ==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/types": "^4.3.0",
+ "@smithy/types": "^4.3.1",
"@smithy/util-buffer-from": "^4.0.0",
"@smithy/util-utf8": "^4.0.0",
"tslib": "^2.6.2"
@@ -10125,12 +10654,12 @@
}
},
"node_modules/@smithy/invalid-dependency": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.0.3.tgz",
- "integrity": "sha512-1Bo8Ur1ZGqxvwTqBmv6DZEn0rXtwJGeqiiO2/JFcCtz3nBakOqeXbJBElXJMMzd0ghe8+eB6Dkw98nMYctgizg==",
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.0.4.tgz",
+ "integrity": "sha512-bNYMi7WKTJHu0gn26wg8OscncTt1t2b8KcsZxvOv56XA6cyXtOAAAaNP7+m45xfppXfOatXF3Sb1MNsLUgVLTw==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/types": "^4.3.0",
+ "@smithy/types": "^4.3.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -10164,13 +10693,13 @@
}
},
"node_modules/@smithy/middleware-content-length": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.0.3.tgz",
- "integrity": "sha512-NE/Zph4BP5u16bzYq2csq9qD0T6UBLeg4AuNrwNJ7Gv9uLYaGEgelZUOdRndGdMGcUfSGvNlXGb2aA2hPCwJ6g==",
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.0.4.tgz",
+ "integrity": "sha512-F7gDyfI2BB1Kc+4M6rpuOLne5LOcEknH1n6UQB69qv+HucXBR1rkzXBnQTB2q46sFy1PM/zuSJOB532yc8bg3w==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/protocol-http": "^5.1.1",
- "@smithy/types": "^4.3.0",
+ "@smithy/protocol-http": "^5.1.2",
+ "@smithy/types": "^4.3.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -10178,18 +10707,18 @@
}
},
"node_modules/@smithy/middleware-endpoint": {
- "version": "4.1.7",
- "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.1.7.tgz",
- "integrity": "sha512-KDzM7Iajo6K7eIWNNtukykRT4eWwlHjCEsULZUaSfi/SRSBK8BPRqG5FsVfp58lUxcvre8GT8AIPIqndA0ERKw==",
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.1.11.tgz",
+ "integrity": "sha512-zDogwtRLzKl58lVS8wPcARevFZNBOOqnmzWWxVe9XiaXU2CADFjvJ9XfNibgkOWs08sxLuSr81NrpY4mgp9OwQ==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/core": "^3.4.0",
- "@smithy/middleware-serde": "^4.0.6",
- "@smithy/node-config-provider": "^4.1.2",
- "@smithy/shared-ini-file-loader": "^4.0.3",
- "@smithy/types": "^4.3.0",
- "@smithy/url-parser": "^4.0.3",
- "@smithy/util-middleware": "^4.0.3",
+ "@smithy/core": "^3.5.3",
+ "@smithy/middleware-serde": "^4.0.8",
+ "@smithy/node-config-provider": "^4.1.3",
+ "@smithy/shared-ini-file-loader": "^4.0.4",
+ "@smithy/types": "^4.3.1",
+ "@smithy/url-parser": "^4.0.4",
+ "@smithy/util-middleware": "^4.0.4",
"tslib": "^2.6.2"
},
"engines": {
@@ -10197,18 +10726,18 @@
}
},
"node_modules/@smithy/middleware-retry": {
- "version": "4.1.8",
- "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.1.8.tgz",
- "integrity": "sha512-e2OtQgFzzlSG0uCjcJmi02QuFSRTrpT11Eh2EcqqDFy7DYriteHZJkkf+4AsxsrGDugAtPFcWBz1aq06sSX5fQ==",
+ "version": "4.1.12",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.1.12.tgz",
+ "integrity": "sha512-wvIH70c4e91NtRxdaLZF+mbLZ/HcC6yg7ySKUiufL6ESp6zJUSnJucZ309AvG9nqCFHSRB5I6T3Ez1Q9wCh0Ww==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/node-config-provider": "^4.1.2",
- "@smithy/protocol-http": "^5.1.1",
- "@smithy/service-error-classification": "^4.0.4",
- "@smithy/smithy-client": "^4.3.0",
- "@smithy/types": "^4.3.0",
- "@smithy/util-middleware": "^4.0.3",
- "@smithy/util-retry": "^4.0.4",
+ "@smithy/node-config-provider": "^4.1.3",
+ "@smithy/protocol-http": "^5.1.2",
+ "@smithy/service-error-classification": "^4.0.5",
+ "@smithy/smithy-client": "^4.4.3",
+ "@smithy/types": "^4.3.1",
+ "@smithy/util-middleware": "^4.0.4",
+ "@smithy/util-retry": "^4.0.5",
"tslib": "^2.6.2",
"uuid": "^9.0.1"
},
@@ -10217,13 +10746,13 @@
}
},
"node_modules/@smithy/middleware-serde": {
- "version": "4.0.6",
- "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.0.6.tgz",
- "integrity": "sha512-YECyl7uNII+jCr/9qEmCu8xYL79cU0fqjo0qxpcVIU18dAPHam/iYwcknAu4Jiyw1uN+sAx7/SMf/Kmef/Jjsg==",
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.0.8.tgz",
+ "integrity": "sha512-iSSl7HJoJaGyMIoNn2B7czghOVwJ9nD7TMvLhMWeSB5vt0TnEYyRRqPJu/TqW76WScaNvYYB8nRoiBHR9S1Ddw==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/protocol-http": "^5.1.1",
- "@smithy/types": "^4.3.0",
+ "@smithy/protocol-http": "^5.1.2",
+ "@smithy/types": "^4.3.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -10231,12 +10760,12 @@
}
},
"node_modules/@smithy/middleware-stack": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.0.3.tgz",
- "integrity": "sha512-baeV7t4jQfQtFxBADFmnhmqBmqR38dNU5cvEgHcMK/Kp3D3bEI0CouoX2Sr/rGuntR+Eg0IjXdxnGGTc6SbIkw==",
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.0.4.tgz",
+ "integrity": "sha512-kagK5ggDrBUCCzI93ft6DjteNSfY8Ulr83UtySog/h09lTIOAJ/xUSObutanlPT0nhoHAkpmW9V5K8oPyLh+QA==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/types": "^4.3.0",
+ "@smithy/types": "^4.3.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -10244,14 +10773,14 @@
}
},
"node_modules/@smithy/node-config-provider": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.1.2.tgz",
- "integrity": "sha512-SUvNup8iU1v7fmM8XPk+27m36udmGCfSz+VZP5Gb0aJ3Ne0X28K/25gnsrg3X1rWlhcnhzNUUysKW/Ied46ivQ==",
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.1.3.tgz",
+ "integrity": "sha512-HGHQr2s59qaU1lrVH6MbLlmOBxadtzTsoO4c+bF5asdgVik3I8o7JIOzoeqWc5MjVa+vD36/LWE0iXKpNqooRw==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/property-provider": "^4.0.3",
- "@smithy/shared-ini-file-loader": "^4.0.3",
- "@smithy/types": "^4.3.0",
+ "@smithy/property-provider": "^4.0.4",
+ "@smithy/shared-ini-file-loader": "^4.0.4",
+ "@smithy/types": "^4.3.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -10259,15 +10788,15 @@
}
},
"node_modules/@smithy/node-http-handler": {
- "version": "4.0.5",
- "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.0.5.tgz",
- "integrity": "sha512-T7QglZC1vS7SPT44/1qSIAQEx5bFKb3LfO6zw/o4Xzt1eC5HNoH1TkS4lMYA9cWFbacUhx4hRl/blLun4EOCkg==",
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.0.6.tgz",
+ "integrity": "sha512-NqbmSz7AW2rvw4kXhKGrYTiJVDHnMsFnX4i+/FzcZAfbOBauPYs2ekuECkSbtqaxETLLTu9Rl/ex6+I2BKErPA==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/abort-controller": "^4.0.3",
- "@smithy/protocol-http": "^5.1.1",
- "@smithy/querystring-builder": "^4.0.3",
- "@smithy/types": "^4.3.0",
+ "@smithy/abort-controller": "^4.0.4",
+ "@smithy/protocol-http": "^5.1.2",
+ "@smithy/querystring-builder": "^4.0.4",
+ "@smithy/types": "^4.3.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -10275,12 +10804,12 @@
}
},
"node_modules/@smithy/property-provider": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.0.3.tgz",
- "integrity": "sha512-Wcn17QNdawJZcZZPBuMuzyBENVi1AXl4TdE0jvzo4vWX2x5df/oMlmr/9M5XAAC6+yae4kWZlOYIsNsgDrMU9A==",
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.0.4.tgz",
+ "integrity": "sha512-qHJ2sSgu4FqF4U/5UUp4DhXNmdTrgmoAai6oQiM+c5RZ/sbDwJ12qxB1M6FnP+Tn/ggkPZf9ccn4jqKSINaquw==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/types": "^4.3.0",
+ "@smithy/types": "^4.3.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -10288,12 +10817,12 @@
}
},
"node_modules/@smithy/protocol-http": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.1.tgz",
- "integrity": "sha512-Vsay2mzq05DwNi9jK01yCFtfvu9HimmgC7a4HTs7lhX12Sx8aWsH0mfz6q/02yspSp+lOB+Q2HJwi4IV2GKz7A==",
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.2.tgz",
+ "integrity": "sha512-rOG5cNLBXovxIrICSBm95dLqzfvxjEmuZx4KK3hWwPFHGdW3lxY0fZNXfv2zebfRO7sJZ5pKJYHScsqopeIWtQ==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/types": "^4.3.0",
+ "@smithy/types": "^4.3.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -10301,12 +10830,12 @@
}
},
"node_modules/@smithy/querystring-builder": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.0.3.tgz",
- "integrity": "sha512-UUzIWMVfPmDZcOutk2/r1vURZqavvQW0OHvgsyNV0cKupChvqg+/NKPRMaMEe+i8tP96IthMFeZOZWpV+E4RAw==",
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.0.4.tgz",
+ "integrity": "sha512-SwREZcDnEYoh9tLNgMbpop+UTGq44Hl9tdj3rf+yeLcfH7+J8OXEBaMc2kDxtyRHu8BhSg9ADEx0gFHvpJgU8w==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/types": "^4.3.0",
+ "@smithy/types": "^4.3.1",
"@smithy/util-uri-escape": "^4.0.0",
"tslib": "^2.6.2"
},
@@ -10315,12 +10844,12 @@
}
},
"node_modules/@smithy/querystring-parser": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.0.3.tgz",
- "integrity": "sha512-K5M4ZJQpFCblOJ5Oyw7diICpFg1qhhR47m2/5Ef1PhGE19RaIZf50tjYFrxa6usqcuXyTiFPGo4d1geZdH4YcQ==",
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.0.4.tgz",
+ "integrity": "sha512-6yZf53i/qB8gRHH/l2ZwUG5xgkPgQF15/KxH0DdXMDHjesA9MeZje/853ifkSY0x4m5S+dfDZ+c4x439PF0M2w==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/types": "^4.3.0",
+ "@smithy/types": "^4.3.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -10328,24 +10857,24 @@
}
},
"node_modules/@smithy/service-error-classification": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.0.4.tgz",
- "integrity": "sha512-W5ScbQ1bTzgH91kNEE2CvOzM4gXlDOqdow4m8vMFSIXCel2scbHwjflpVNnC60Y3F1m5i7w2gQg9lSnR+JsJAA==",
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.0.5.tgz",
+ "integrity": "sha512-LvcfhrnCBvCmTee81pRlh1F39yTS/+kYleVeLCwNtkY8wtGg8V/ca9rbZZvYIl8OjlMtL6KIjaiL/lgVqHD2nA==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/types": "^4.3.0"
+ "@smithy/types": "^4.3.1"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@smithy/shared-ini-file-loader": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.0.3.tgz",
- "integrity": "sha512-vHwlrqhZGIoLwaH8vvIjpHnloShqdJ7SUPNM2EQtEox+yEDFTVQ7E+DLZ+6OhnYEgFUwPByJyz6UZaOu2tny6A==",
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.0.4.tgz",
+ "integrity": "sha512-63X0260LoFBjrHifPDs+nM9tV0VMkOTl4JRMYNuKh/f5PauSjowTfvF3LogfkWdcPoxsA9UjqEOgjeYIbhb7Nw==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/types": "^4.3.0",
+ "@smithy/types": "^4.3.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -10353,16 +10882,16 @@
}
},
"node_modules/@smithy/signature-v4": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.1.1.tgz",
- "integrity": "sha512-zy8Repr5zvT0ja+Tf5wjV/Ba6vRrhdiDcp/ww6cvqYbSEudIkziDe3uppNRlFoCViyJXdPnLcwyZdDLA4CHzSg==",
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.1.2.tgz",
+ "integrity": "sha512-d3+U/VpX7a60seHziWnVZOHuEgJlclufjkS6zhXvxcJgkJq4UWdH5eOBLzHRMx6gXjsdT9h6lfpmLzbrdupHgQ==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/is-array-buffer": "^4.0.0",
- "@smithy/protocol-http": "^5.1.1",
- "@smithy/types": "^4.3.0",
+ "@smithy/protocol-http": "^5.1.2",
+ "@smithy/types": "^4.3.1",
"@smithy/util-hex-encoding": "^4.0.0",
- "@smithy/util-middleware": "^4.0.3",
+ "@smithy/util-middleware": "^4.0.4",
"@smithy/util-uri-escape": "^4.0.0",
"@smithy/util-utf8": "^4.0.0",
"tslib": "^2.6.2"
@@ -10372,17 +10901,17 @@
}
},
"node_modules/@smithy/smithy-client": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.3.0.tgz",
- "integrity": "sha512-DNsRA38pN6tYHUjebmwD9e4KcgqTLldYQb2gC6K+oxXYdCTxPn6wV9+FvOa6wrU2FQEnGJoi+3GULzOTKck/tg==",
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.4.3.tgz",
+ "integrity": "sha512-xxzNYgA0HD6ETCe5QJubsxP0hQH3QK3kbpJz3QrosBCuIWyEXLR/CO5hFb2OeawEKUxMNhz3a1nuJNN2np2RMA==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/core": "^3.4.0",
- "@smithy/middleware-endpoint": "^4.1.7",
- "@smithy/middleware-stack": "^4.0.3",
- "@smithy/protocol-http": "^5.1.1",
- "@smithy/types": "^4.3.0",
- "@smithy/util-stream": "^4.2.1",
+ "@smithy/core": "^3.5.3",
+ "@smithy/middleware-endpoint": "^4.1.11",
+ "@smithy/middleware-stack": "^4.0.4",
+ "@smithy/protocol-http": "^5.1.2",
+ "@smithy/types": "^4.3.1",
+ "@smithy/util-stream": "^4.2.2",
"tslib": "^2.6.2"
},
"engines": {
@@ -10390,9 +10919,9 @@
}
},
"node_modules/@smithy/types": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.3.0.tgz",
- "integrity": "sha512-+1iaIQHthDh9yaLhRzaoQxRk+l9xlk+JjMFxGRhNLz+m9vKOkjNeU8QuB4w3xvzHyVR/BVlp/4AXDHjoRIkfgQ==",
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.3.1.tgz",
+ "integrity": "sha512-UqKOQBL2x6+HWl3P+3QqFD4ncKq0I8Nuz9QItGv5WuKuMHuuwlhvqcZCoXGfc+P1QmfJE7VieykoYYmrOoFJxA==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.6.2"
@@ -10402,13 +10931,13 @@
}
},
"node_modules/@smithy/url-parser": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.0.3.tgz",
- "integrity": "sha512-n5/DnosDu/tweOqUUNtUbu7eRIR4J/Wz9nL7V5kFYQQVb8VYdj7a4G5NJHCw6o21ul7CvZoJkOpdTnsQDLT0tQ==",
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.0.4.tgz",
+ "integrity": "sha512-eMkc144MuN7B0TDA4U2fKs+BqczVbk3W+qIvcoCY6D1JY3hnAdCuhCZODC+GAeaxj0p6Jroz4+XMUn3PCxQQeQ==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/querystring-parser": "^4.0.3",
- "@smithy/types": "^4.3.0",
+ "@smithy/querystring-parser": "^4.0.4",
+ "@smithy/types": "^4.3.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -10479,14 +11008,14 @@
}
},
"node_modules/@smithy/util-defaults-mode-browser": {
- "version": "4.0.15",
- "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.15.tgz",
- "integrity": "sha512-bJJ/B8owQbHAflatSq92f9OcV8858DJBQF1Y3GRjB8psLyUjbISywszYPFw16beREHO/C3I3taW4VGH+tOuwrQ==",
+ "version": "4.0.19",
+ "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.19.tgz",
+ "integrity": "sha512-mvLMh87xSmQrV5XqnUYEPoiFFeEGYeAKIDDKdhE2ahqitm8OHM3aSvhqL6rrK6wm1brIk90JhxDf5lf2hbrLbQ==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/property-provider": "^4.0.3",
- "@smithy/smithy-client": "^4.3.0",
- "@smithy/types": "^4.3.0",
+ "@smithy/property-provider": "^4.0.4",
+ "@smithy/smithy-client": "^4.4.3",
+ "@smithy/types": "^4.3.1",
"bowser": "^2.11.0",
"tslib": "^2.6.2"
},
@@ -10495,17 +11024,17 @@
}
},
"node_modules/@smithy/util-defaults-mode-node": {
- "version": "4.0.15",
- "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.15.tgz",
- "integrity": "sha512-8CUrEW2Ni5q+NmYkj8wsgkfqoP7l4ZquptFbq92yQE66xevc4SxqP2zH6tMtN158kgBqBDsZ+qlrRwXWOjCR8A==",
+ "version": "4.0.19",
+ "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.19.tgz",
+ "integrity": "sha512-8tYnx+LUfj6m+zkUUIrIQJxPM1xVxfRBvoGHua7R/i6qAxOMjqR6CpEpDwKoIs1o0+hOjGvkKE23CafKL0vJ9w==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/config-resolver": "^4.1.3",
- "@smithy/credential-provider-imds": "^4.0.5",
- "@smithy/node-config-provider": "^4.1.2",
- "@smithy/property-provider": "^4.0.3",
- "@smithy/smithy-client": "^4.3.0",
- "@smithy/types": "^4.3.0",
+ "@smithy/config-resolver": "^4.1.4",
+ "@smithy/credential-provider-imds": "^4.0.6",
+ "@smithy/node-config-provider": "^4.1.3",
+ "@smithy/property-provider": "^4.0.4",
+ "@smithy/smithy-client": "^4.4.3",
+ "@smithy/types": "^4.3.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -10513,13 +11042,13 @@
}
},
"node_modules/@smithy/util-endpoints": {
- "version": "3.0.5",
- "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.0.5.tgz",
- "integrity": "sha512-PjDpqLk24/vAl340tmtCA++Q01GRRNH9cwL9qh46NspAX9S+IQVcK+GOzPt0GLJ6KYGyn8uOgo2kvJhiThclJw==",
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.0.6.tgz",
+ "integrity": "sha512-YARl3tFL3WgPuLzljRUnrS2ngLiUtkwhQtj8PAL13XZSyUiNLQxwG3fBBq3QXFqGFUXepIN73pINp3y8c2nBmA==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/node-config-provider": "^4.1.2",
- "@smithy/types": "^4.3.0",
+ "@smithy/node-config-provider": "^4.1.3",
+ "@smithy/types": "^4.3.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -10539,12 +11068,12 @@
}
},
"node_modules/@smithy/util-middleware": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.0.3.tgz",
- "integrity": "sha512-iIsC6qZXxkD7V3BzTw3b1uK8RVC1M8WvwNxK1PKrH9FnxntCd30CSunXjL/8iJBE8Z0J14r2P69njwIpRG4FBQ==",
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.0.4.tgz",
+ "integrity": "sha512-9MLKmkBmf4PRb0ONJikCbCwORACcil6gUWojwARCClT7RmLzF04hUR4WdRprIXal7XVyrddadYNfp2eF3nrvtQ==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/types": "^4.3.0",
+ "@smithy/types": "^4.3.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -10552,13 +11081,13 @@
}
},
"node_modules/@smithy/util-retry": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.0.4.tgz",
- "integrity": "sha512-Aoqr9W2jDYGrI6OxljN8VmLDQIGO4VdMAUKMf9RGqLG8hn6or+K41NEy1Y5dtum9q8F7e0obYAuKl2mt/GnpZg==",
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.0.5.tgz",
+ "integrity": "sha512-V7MSjVDTlEt/plmOFBn1762Dyu5uqMrV2Pl2X0dYk4XvWfdWJNe9Bs5Bzb56wkCuiWjSfClVMGcsuKrGj7S/yg==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/service-error-classification": "^4.0.4",
- "@smithy/types": "^4.3.0",
+ "@smithy/service-error-classification": "^4.0.5",
+ "@smithy/types": "^4.3.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -10566,14 +11095,14 @@
}
},
"node_modules/@smithy/util-stream": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.2.1.tgz",
- "integrity": "sha512-W3IR0x5DY6iVtjj5p902oNhD+Bz7vs5S+p6tppbPa509rV9BdeXZjGuRSCtVEad9FA0Mba+tNUtUmtnSI1nwUw==",
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.2.2.tgz",
+ "integrity": "sha512-aI+GLi7MJoVxg24/3J1ipwLoYzgkB4kUfogZfnslcYlynj3xsQ0e7vk4TnTro9hhsS5PvX1mwmkRqqHQjwcU7w==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/fetch-http-handler": "^5.0.3",
- "@smithy/node-http-handler": "^4.0.5",
- "@smithy/types": "^4.3.0",
+ "@smithy/fetch-http-handler": "^5.0.4",
+ "@smithy/node-http-handler": "^4.0.6",
+ "@smithy/types": "^4.3.1",
"@smithy/util-base64": "^4.0.0",
"@smithy/util-buffer-from": "^4.0.0",
"@smithy/util-hex-encoding": "^4.0.0",
@@ -10610,13 +11139,13 @@
}
},
"node_modules/@smithy/util-waiter": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.0.4.tgz",
- "integrity": "sha512-73aeIvHjtSB6fd9I08iFaQIGTICKpLrI3EtlWAkStVENGo1ARMq9qdoD4QwkY0RUp6A409xlgbD9NCCfCF5ieg==",
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.0.5.tgz",
+ "integrity": "sha512-4QvC49HTteI1gfemu0I1syWovJgPvGn7CVUoN9ZFkdvr/cCFkrEL7qNCdx/2eICqDWEGnnr68oMdSIPCLAriSQ==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/abort-controller": "^4.0.3",
- "@smithy/types": "^4.3.0",
+ "@smithy/abort-controller": "^4.0.4",
+ "@smithy/types": "^4.3.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -36037,6 +36566,7 @@
"license": "MIT",
"dependencies": {
"@aws-sdk/client-s3": "^3.410.0",
+ "@aws-sdk/client-sesv2": "^3.410.0",
"@aws-sdk/cloudfront-signer": "^3.410.0",
"@aws-sdk/s3-request-presigner": "^3.410.0",
"@aws-sdk/signature-v4-crt": "^3.410.0",
diff --git a/packages/app-tests/e2e/features/include-document-certificate.spec.ts b/packages/app-tests/e2e/features/include-document-certificate.spec.ts
index 0f8c30be7..b447b09c2 100644
--- a/packages/app-tests/e2e/features/include-document-certificate.spec.ts
+++ b/packages/app-tests/e2e/features/include-document-certificate.spec.ts
@@ -244,7 +244,7 @@ test.describe('Signing Certificate Tests', () => {
await apiSignin({
page,
email: owner.email,
- redirectPath: `/t/${team.url}/settings/preferences`,
+ redirectPath: `/t/${team.url}/settings/document`,
});
await page
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 dff2d5ddd..90cb6ae34 100644
--- a/packages/app-tests/e2e/organisations/organisation-team-preferences.spec.ts
+++ b/packages/app-tests/e2e/organisations/organisation-team-preferences.spec.ts
@@ -8,7 +8,7 @@ import { seedUser } from '@documenso/prisma/seed/users';
import { apiSignin } from '../fixtures/authentication';
-test('[ORGANISATIONS]: manage preferences', async ({ page }) => {
+test('[ORGANISATIONS]: manage document preferences', async ({ page }) => {
const { user, organisation, team } = await seedUser({
isPersonalOrganisation: false,
});
@@ -16,7 +16,7 @@ test('[ORGANISATIONS]: manage preferences', async ({ page }) => {
await apiSignin({
page,
email: user.email,
- redirectPath: `/o/${organisation.url}/settings/preferences`,
+ redirectPath: `/o/${organisation.url}/settings/document`,
});
// Update document preferences.
@@ -24,26 +24,25 @@ test('[ORGANISATIONS]: manage preferences', async ({ page }) => {
await page.getByRole('option', { name: 'Only managers and above can' }).click();
await page.getByRole('combobox').filter({ hasText: 'English' }).click();
await page.getByRole('option', { name: 'German' }).click();
- await page.getByTestId('signature-types-combobox').click();
+
+ // Set default timezone
+ await page.getByRole('combobox').filter({ hasText: 'Local timezone' }).click();
+ await page.getByRole('option', { name: 'Australia/Perth' }).click();
+
+ // Set default date
+ await page.getByRole('combobox').filter({ hasText: 'yyyy-MM-dd hh:mm a' }).click();
+ await page.getByRole('option', { name: 'DD/MM/YYYY' }).click();
+
+ await page.getByTestId('signature-types-trigger').click();
await page.getByRole('option', { name: 'Draw' }).click();
await page.getByRole('option', { name: 'Upload' }).click();
- await page.getByRole('combobox').nth(3).click();
+ await page.getByTestId('include-sender-details-trigger').click();
await page.getByRole('option', { name: 'No' }).click();
- await page.getByRole('combobox').filter({ hasText: 'Yes' }).click();
+ await page.getByTestId('include-signing-certificate-trigger').click();
await page.getByRole('option', { name: 'No' }).click();
await page.getByRole('button', { name: 'Update' }).first().click();
await expect(page.getByText('Your document preferences have been updated').first()).toBeVisible();
- // Update branding.
- await page.getByTestId('enable-branding').click();
- await page.getByRole('option', { name: 'Yes' }).click();
- await page.getByRole('textbox', { name: 'Brand Website' }).click();
- await page.getByRole('textbox', { name: 'Brand Website' }).fill('https://documenso.com');
- await page.getByRole('textbox', { name: 'Brand Details' }).click();
- await page.getByRole('textbox', { name: 'Brand Details' }).fill('BrandDetails');
- await page.getByRole('button', { name: 'Update' }).nth(1).click();
- await expect(page.getByText('Your branding preferences have been updated').first()).toBeVisible();
-
const teamSettings = await getTeamSettings({
teamId: team.id,
});
@@ -51,34 +50,30 @@ test('[ORGANISATIONS]: manage preferences', async ({ page }) => {
// Check that the team settings have inherited these values.
expect(teamSettings.documentVisibility).toEqual(DocumentVisibility.MANAGER_AND_ABOVE);
expect(teamSettings.documentLanguage).toEqual('de');
+ expect(teamSettings.documentTimezone).toEqual('Australia/Perth');
+ expect(teamSettings.documentDateFormat).toEqual('dd/MM/yyyy hh:mm a');
expect(teamSettings.includeSenderDetails).toEqual(false);
expect(teamSettings.includeSigningCertificate).toEqual(false);
expect(teamSettings.typedSignatureEnabled).toEqual(true);
expect(teamSettings.uploadSignatureEnabled).toEqual(false);
expect(teamSettings.drawSignatureEnabled).toEqual(false);
- expect(teamSettings.brandingEnabled).toEqual(true);
- expect(teamSettings.brandingUrl).toEqual('https://documenso.com');
- expect(teamSettings.brandingCompanyDetails).toEqual('BrandDetails');
// Edit the team settings
- await page.goto(`/t/${team.url}/settings/preferences`);
+ await page.goto(`/t/${team.url}/settings/document`);
- await page
- .getByRole('group')
- .locator('div')
- .filter({
- hasText: 'Default Document Visibility',
- })
- .getByRole('combobox')
- .click();
+ await page.getByTestId('document-visibility-trigger').click();
await page.getByRole('option', { name: 'Everyone can access and view' }).click();
- await page
- .getByRole('group')
- .locator('div')
- .filter({ hasText: 'Default Document Language' })
- .getByRole('combobox')
- .click();
+ await page.getByTestId('document-language-trigger').click();
await page.getByRole('option', { name: 'Polish' }).click();
+
+ // Override team timezone settings
+ await page.getByTestId('document-timezone-trigger').click();
+ await page.getByRole('option', { name: 'Europe/London' }).click();
+
+ // Override team date format settings
+ await page.getByTestId('document-date-format-trigger').click();
+ await page.getByRole('option', { name: 'MM/DD/YYYY' }).click();
+
await page.getByRole('button', { name: 'Update' }).first().click();
await expect(page.getByText('Your document preferences have been updated').first()).toBeVisible();
@@ -89,6 +84,8 @@ test('[ORGANISATIONS]: manage preferences', async ({ page }) => {
// Check that the team settings have inherited/overriden the correct values.
expect(updatedTeamSettings.documentVisibility).toEqual(DocumentVisibility.EVERYONE);
expect(updatedTeamSettings.documentLanguage).toEqual('pl');
+ expect(updatedTeamSettings.documentTimezone).toEqual('Europe/London');
+ expect(updatedTeamSettings.documentDateFormat).toEqual('MM/dd/yyyy hh:mm a');
expect(updatedTeamSettings.includeSenderDetails).toEqual(false);
expect(updatedTeamSettings.includeSigningCertificate).toEqual(false);
expect(updatedTeamSettings.typedSignatureEnabled).toEqual(true);
@@ -110,4 +107,228 @@ test('[ORGANISATIONS]: manage preferences', async ({ page }) => {
expect(documentMeta.uploadSignatureEnabled).toEqual(false);
expect(documentMeta.drawSignatureEnabled).toEqual(false);
expect(documentMeta.language).toEqual('pl');
+ expect(documentMeta.timezone).toEqual('Europe/London');
+ expect(documentMeta.dateFormat).toEqual('MM/dd/yyyy hh:mm a');
+});
+
+test('[ORGANISATIONS]: manage branding preferences', async ({ page }) => {
+ const { user, organisation, team } = await seedUser({
+ isPersonalOrganisation: false,
+ });
+
+ await apiSignin({
+ page,
+ email: user.email,
+ redirectPath: `/o/${organisation.url}/settings/branding`,
+ });
+
+ // Update branding preferences.
+ await page.getByTestId('enable-branding').click();
+ await page.getByRole('option', { name: 'Yes' }).click();
+ await page.getByRole('textbox', { name: 'Brand Website' }).click();
+ await page.getByRole('textbox', { name: 'Brand Website' }).fill('https://documenso.com');
+ await page.getByRole('textbox', { name: 'Brand Details' }).click();
+ await page.getByRole('textbox', { name: 'Brand Details' }).fill('BrandDetails');
+ await page.getByRole('button', { name: 'Update' }).first().click();
+ await expect(page.getByText('Your branding preferences have been updated').first()).toBeVisible();
+
+ const teamSettings = await getTeamSettings({
+ teamId: team.id,
+ });
+
+ // Check that the team settings have inherited these values.
+ expect(teamSettings.brandingEnabled).toEqual(true);
+ expect(teamSettings.brandingUrl).toEqual('https://documenso.com');
+ expect(teamSettings.brandingCompanyDetails).toEqual('BrandDetails');
+
+ // Edit the team branding settings
+ await page.goto(`/t/${team.url}/settings/branding`);
+
+ // Override team settings with different values
+ await page.getByTestId('enable-branding').click();
+ await page.getByRole('option', { name: 'Yes' }).click();
+ await page.getByRole('textbox', { name: 'Brand Website' }).click();
+ await page.getByRole('textbox', { name: 'Brand Website' }).fill('https://example.com');
+ await page.getByRole('textbox', { name: 'Brand Details' }).click();
+ await page.getByRole('textbox', { name: 'Brand Details' }).fill('UpdatedBrandDetails');
+ await page.getByRole('button', { name: 'Update' }).first().click();
+ await expect(page.getByText('Your branding preferences have been updated').first()).toBeVisible();
+
+ const updatedTeamSettings = await getTeamSettings({
+ teamId: team.id,
+ });
+
+ // Check that the team settings have overridden the organisation values.
+ expect(updatedTeamSettings.brandingEnabled).toEqual(true);
+ expect(updatedTeamSettings.brandingUrl).toEqual('https://example.com');
+ expect(updatedTeamSettings.brandingCompanyDetails).toEqual('UpdatedBrandDetails');
+
+ // Test inheritance by setting team back to inherit from organisation
+ await page.getByTestId('enable-branding').click();
+ await page.getByRole('option', { name: 'Inherit from organisation' }).click();
+ await page.getByRole('button', { name: 'Update' }).first().click();
+ await expect(page.getByText('Your branding preferences have been updated').first()).toBeVisible();
+
+ await page.waitForTimeout(2000);
+
+ const inheritedTeamSettings = await getTeamSettings({
+ teamId: team.id,
+ });
+
+ // Check that the team settings now inherit from organisation again.
+ expect(inheritedTeamSettings.brandingEnabled).toEqual(true);
+ expect(inheritedTeamSettings.brandingUrl).toEqual('https://documenso.com');
+ expect(inheritedTeamSettings.brandingCompanyDetails).toEqual('BrandDetails');
+
+ // Verify that a document can be created successfully with the branding settings
+ const document = await seedTeamDocumentWithMeta(team);
+
+ // Confirm the document was created successfully with the team's branding settings
+ expect(document).toBeDefined();
+ expect(document.teamId).toEqual(team.id);
+});
+
+test('[ORGANISATIONS]: manage email preferences', async ({ page }) => {
+ const { user, organisation, team } = await seedUser({
+ isPersonalOrganisation: false,
+ });
+
+ await apiSignin({
+ page,
+ email: user.email,
+ redirectPath: `/o/${organisation.url}/settings/email`,
+ });
+
+ // Update email preferences at organisation level.
+ // Set reply to email
+ await page.getByRole('textbox', { name: 'Reply to email' }).click();
+ await page.getByRole('textbox', { name: 'Reply to email' }).fill('organisation@documenso.com');
+
+ // Update email document settings by enabling/disabling some checkboxes
+ await page.getByRole('checkbox', { name: 'Send recipient signed email' }).uncheck();
+ await page.getByRole('checkbox', { name: 'Send document pending email' }).uncheck();
+ await page.getByRole('checkbox', { name: 'Send document deleted email' }).uncheck();
+
+ await page.getByRole('button', { name: 'Update' }).first().click();
+ await expect(page.getByText('Your email preferences have been updated').first()).toBeVisible();
+
+ const teamSettings = await getTeamSettings({
+ teamId: team.id,
+ });
+
+ // Check that the team settings have inherited these values.
+ expect(teamSettings.emailReplyTo).toEqual('organisation@documenso.com');
+ expect(teamSettings.emailDocumentSettings).toEqual({
+ recipientSigningRequest: true,
+ recipientRemoved: true,
+ recipientSigned: false, // unchecked
+ documentPending: false, // unchecked
+ documentCompleted: true,
+ documentDeleted: false, // unchecked
+ ownerDocumentCompleted: true,
+ });
+
+ // Edit the team email settings
+ await page.goto(`/t/${team.url}/settings/email`);
+
+ // Override team settings with different values
+ await page.getByRole('textbox', { name: 'Reply to email' }).click();
+ await page.getByRole('textbox', { name: 'Reply to email' }).fill('team@example.com');
+
+ // Change email document settings inheritance to controlled
+ await page.getByRole('combobox').filter({ hasText: 'Inherit from organisation' }).click();
+ await page.getByRole('option', { name: 'Override organisation settings' }).click();
+
+ // Update some email settings
+ await page.getByRole('checkbox', { name: 'Send recipient signing request email' }).uncheck();
+ await page
+ .getByRole('checkbox', { name: 'Send document completed email', exact: true })
+ .uncheck();
+ await page
+ .getByRole('checkbox', { name: 'Send document completed email to the owner' })
+ .uncheck();
+
+ await page.getByRole('button', { name: 'Update' }).first().click();
+ await expect(page.getByText('Your email preferences have been updated').first()).toBeVisible();
+
+ const updatedTeamSettings = await getTeamSettings({
+ teamId: team.id,
+ });
+
+ // Check that the team settings have overridden the organisation values.
+ expect(updatedTeamSettings.emailReplyTo).toEqual('team@example.com');
+ expect(updatedTeamSettings.emailDocumentSettings).toEqual({
+ recipientSigned: true,
+ recipientSigningRequest: false,
+ recipientRemoved: true,
+ documentPending: true,
+ documentCompleted: false,
+ documentDeleted: true,
+ ownerDocumentCompleted: false,
+ });
+
+ // Verify that a document can be created successfully with the team email settings
+ const teamOverrideDocument = await seedTeamDocumentWithMeta(team);
+
+ const teamOverrideDocumentMeta = await prisma.documentMeta.findFirstOrThrow({
+ where: {
+ documentId: teamOverrideDocument.id,
+ },
+ });
+
+ expect(teamOverrideDocumentMeta.emailReplyTo).toEqual('team@example.com');
+ expect(teamOverrideDocumentMeta.emailSettings).toEqual({
+ recipientSigned: true,
+ recipientSigningRequest: false,
+ recipientRemoved: true,
+ documentPending: true,
+ documentCompleted: false,
+ documentDeleted: true,
+ ownerDocumentCompleted: false,
+ });
+
+ // Test inheritance by setting team back to inherit from organisation
+ await page.getByRole('textbox', { name: 'Reply to email' }).fill('');
+ await page.getByRole('combobox').filter({ hasText: 'Override organisation settings' }).click();
+ await page.getByRole('option', { name: 'Inherit from organisation' }).click();
+ await page.getByRole('button', { name: 'Update' }).first().click();
+ await expect(page.getByText('Your email preferences have been updated').first()).toBeVisible();
+
+ await page.waitForTimeout(1000);
+
+ const inheritedTeamSettings = await getTeamSettings({
+ teamId: team.id,
+ });
+
+ // Check that the team settings now inherit from organisation again.
+ expect(inheritedTeamSettings.emailReplyTo).toEqual('organisation@documenso.com');
+ expect(inheritedTeamSettings.emailDocumentSettings).toEqual({
+ recipientSigningRequest: true,
+ recipientRemoved: true,
+ recipientSigned: false,
+ documentPending: false,
+ documentCompleted: true,
+ documentDeleted: false,
+ ownerDocumentCompleted: true,
+ });
+
+ // Verify that a document can be created successfully with the email settings
+ const document = await seedTeamDocumentWithMeta(team);
+
+ const documentMeta = await prisma.documentMeta.findFirstOrThrow({
+ where: {
+ documentId: document.id,
+ },
+ });
+
+ expect(documentMeta.emailReplyTo).toEqual('organisation@documenso.com');
+ expect(documentMeta.emailSettings).toEqual({
+ recipientSigningRequest: true,
+ recipientRemoved: true,
+ recipientSigned: false,
+ documentPending: false,
+ documentCompleted: true,
+ documentDeleted: false,
+ ownerDocumentCompleted: true,
+ });
});
diff --git a/packages/app-tests/e2e/teams/team-signature-settings.spec.ts b/packages/app-tests/e2e/teams/team-signature-settings.spec.ts
index 987fedc23..bebf91371 100644
--- a/packages/app-tests/e2e/teams/team-signature-settings.spec.ts
+++ b/packages/app-tests/e2e/teams/team-signature-settings.spec.ts
@@ -15,7 +15,7 @@ test('[TEAMS]: check that default team signature settings are all enabled', asyn
await apiSignin({
page,
email: user.email,
- redirectPath: `/t/${team.url}/settings/preferences`,
+ redirectPath: `/t/${team.url}/settings/document`,
});
const document = await seedTeamDocumentWithMeta(team);
@@ -45,17 +45,17 @@ test('[TEAMS]: check signature modes can be disabled', async ({ page }) => {
await apiSignin({
page,
email: user.email,
- redirectPath: `/t/${team.url}/settings/preferences`,
+ redirectPath: `/t/${team.url}/settings/document`,
});
const allTabs = ['Type', 'Upload', 'Draw'];
const tabTest = [['Type', 'Upload', 'Draw'], ['Type', 'Upload'], ['Type']];
for (const tabs of tabTest) {
- await page.goto(`/t/${team.url}/settings/preferences`);
+ await page.goto(`/t/${team.url}/settings/document`);
// Update combobox to have the correct tabs
- await page.getByTestId('signature-types-combobox').click();
+ await page.getByTestId('signature-types-trigger').click();
await expect(page.getByRole('option', { name: 'Type' })).toBeVisible();
await expect(page.getByRole('option', { name: 'Upload' })).toBeVisible();
@@ -112,17 +112,17 @@ test('[TEAMS]: check signature modes work for templates', async ({ page }) => {
await apiSignin({
page,
email: user.email,
- redirectPath: `/t/${team.url}/settings/preferences`,
+ redirectPath: `/t/${team.url}/settings/document`,
});
const allTabs = ['Type', 'Upload', 'Draw'];
const tabTest = [['Type', 'Upload', 'Draw'], ['Type', 'Upload'], ['Type']];
for (const tabs of tabTest) {
- await page.goto(`/t/${team.url}/settings/preferences`);
+ await page.goto(`/t/${team.url}/settings/document`);
// Update combobox to have the correct tabs
- await page.getByTestId('signature-types-combobox').click();
+ await page.getByTestId('signature-types-trigger').click();
await expect(page.getByRole('option', { name: 'Type' })).toBeVisible();
await expect(page.getByRole('option', { name: 'Upload' })).toBeVisible();
diff --git a/packages/ee/server-only/lib/create-email-domain.ts b/packages/ee/server-only/lib/create-email-domain.ts
new file mode 100644
index 000000000..b50a55965
--- /dev/null
+++ b/packages/ee/server-only/lib/create-email-domain.ts
@@ -0,0 +1,154 @@
+import { CreateEmailIdentityCommand, SESv2Client } from '@aws-sdk/client-sesv2';
+import { EmailDomainStatus } from '@prisma/client';
+import { generateKeyPair } from 'crypto';
+import { promisify } from 'util';
+
+import { DOCUMENSO_ENCRYPTION_KEY } from '@documenso/lib/constants/crypto';
+import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
+import { symmetricEncrypt } from '@documenso/lib/universal/crypto';
+import { generateDatabaseId } from '@documenso/lib/universal/id';
+import { generateEmailDomainRecords } from '@documenso/lib/utils/email-domains';
+import { env } from '@documenso/lib/utils/env';
+import { prisma } from '@documenso/prisma';
+
+export const getSesClient = () => {
+ const accessKeyId = env('NEXT_PRIVATE_SES_ACCESS_KEY_ID');
+ const secretAccessKey = env('NEXT_PRIVATE_SES_SECRET_ACCESS_KEY');
+ const region = env('NEXT_PRIVATE_SES_REGION');
+
+ if (!accessKeyId || !secretAccessKey || !region) {
+ throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
+ message: 'Missing AWS SES credentials',
+ });
+ }
+
+ return new SESv2Client({
+ region,
+ credentials: {
+ accessKeyId,
+ secretAccessKey,
+ },
+ });
+};
+
+/**
+ * Removes first and last line, then removes all newlines
+ */
+const flattenKey = (key: string) => {
+ return key.trim().split('\n').slice(1, -1).join('');
+};
+
+export async function verifyDomainWithDKIM(domain: string, selector: string, privateKey: string) {
+ const command = new CreateEmailIdentityCommand({
+ EmailIdentity: domain,
+ DkimSigningAttributes: {
+ DomainSigningSelector: selector,
+ DomainSigningPrivateKey: privateKey,
+ },
+ });
+
+ return await getSesClient().send(command);
+}
+
+type CreateEmailDomainOptions = {
+ domain: string;
+ organisationId: string;
+};
+
+type DomainRecord = {
+ name: string;
+ value: string;
+ type: string;
+};
+
+export const createEmailDomain = async ({ domain, organisationId }: CreateEmailDomainOptions) => {
+ const encryptionKey = DOCUMENSO_ENCRYPTION_KEY;
+
+ if (!encryptionKey) {
+ throw new Error('Missing DOCUMENSO_ENCRYPTION_KEY');
+ }
+
+ const selector = `documenso-${organisationId}`.replace(/[_.]/g, '-');
+ const recordName = `${selector}._domainkey.${domain}`;
+
+ // Check if domain already exists
+ const existingDomain = await prisma.emailDomain.findUnique({
+ where: {
+ domain,
+ },
+ });
+
+ if (existingDomain) {
+ throw new AppError(AppErrorCode.ALREADY_EXISTS, {
+ message: 'Domain already exists in database',
+ });
+ }
+
+ // Generate DKIM key pair
+ const generateKeyPairAsync = promisify(generateKeyPair);
+
+ const { publicKey, privateKey } = await generateKeyPairAsync('rsa', {
+ modulusLength: 2048,
+ publicKeyEncoding: {
+ type: 'spki',
+ format: 'pem',
+ },
+ privateKeyEncoding: {
+ type: 'pkcs8',
+ format: 'pem',
+ },
+ });
+
+ // Format public key for DNS record
+ const publicKeyFlattened = flattenKey(publicKey);
+ const privateKeyFlattened = flattenKey(privateKey);
+
+ // Create DNS records
+ const records: DomainRecord[] = generateEmailDomainRecords(recordName, publicKeyFlattened);
+
+ const encryptedPrivateKey = symmetricEncrypt({
+ key: encryptionKey,
+ data: privateKeyFlattened,
+ });
+
+ const emailDomain = await prisma.$transaction(async (tx) => {
+ await verifyDomainWithDKIM(domain, selector, privateKeyFlattened).catch((err) => {
+ if (err.name === 'AlreadyExistsException') {
+ throw new AppError(AppErrorCode.ALREADY_EXISTS, {
+ message: 'Domain already exists in SES',
+ });
+ }
+
+ throw err;
+ });
+
+ // Create email domain record.
+ return await tx.emailDomain.create({
+ data: {
+ id: generateDatabaseId('email_domain'),
+ domain,
+ status: EmailDomainStatus.PENDING,
+ organisationId,
+ selector: recordName,
+ publicKey: publicKeyFlattened,
+ privateKey: encryptedPrivateKey,
+ },
+ select: {
+ id: true,
+ status: true,
+ organisationId: true,
+ domain: true,
+ selector: true,
+ publicKey: true,
+ createdAt: true,
+ updatedAt: true,
+ emails: true,
+ },
+ });
+ });
+
+ return {
+ emailDomain,
+ records,
+ };
+};
diff --git a/packages/ee/server-only/lib/delete-email-domain.ts b/packages/ee/server-only/lib/delete-email-domain.ts
new file mode 100644
index 000000000..0598d9425
--- /dev/null
+++ b/packages/ee/server-only/lib/delete-email-domain.ts
@@ -0,0 +1,52 @@
+import { DeleteEmailIdentityCommand } from '@aws-sdk/client-sesv2';
+
+import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
+import { prisma } from '@documenso/prisma';
+
+import { getSesClient } from './create-email-domain';
+
+type DeleteEmailDomainOptions = {
+ emailDomainId: string;
+};
+
+/**
+ * Delete the email domain and SES email identity.
+ *
+ * Permission is assumed to be checked in the caller.
+ */
+export const deleteEmailDomain = async ({ emailDomainId }: DeleteEmailDomainOptions) => {
+ const emailDomain = await prisma.emailDomain.findUnique({
+ where: {
+ id: emailDomainId,
+ },
+ });
+
+ if (!emailDomain) {
+ throw new AppError(AppErrorCode.NOT_FOUND, {
+ message: 'Email domain not found',
+ });
+ }
+
+ const sesClient = getSesClient();
+
+ await sesClient
+ .send(
+ new DeleteEmailIdentityCommand({
+ EmailIdentity: emailDomain.domain,
+ }),
+ )
+ .catch((err) => {
+ console.error(err);
+
+ // Do nothing if it no longer exists in SES.
+ if (err.name === 'NotFoundException') {
+ return;
+ }
+ });
+
+ await prisma.emailDomain.delete({
+ where: {
+ id: emailDomainId,
+ },
+ });
+};
diff --git a/packages/ee/server-only/lib/verify-email-domain.ts b/packages/ee/server-only/lib/verify-email-domain.ts
new file mode 100644
index 000000000..0c898016e
--- /dev/null
+++ b/packages/ee/server-only/lib/verify-email-domain.ts
@@ -0,0 +1,45 @@
+import { GetEmailIdentityCommand } from '@aws-sdk/client-sesv2';
+import { EmailDomainStatus } from '@prisma/client';
+
+import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
+import { prisma } from '@documenso/prisma';
+
+import { getSesClient } from './create-email-domain';
+
+export const verifyEmailDomain = async (emailDomainId: string) => {
+ const emailDomain = await prisma.emailDomain.findUnique({
+ where: {
+ id: emailDomainId,
+ },
+ });
+
+ if (!emailDomain) {
+ throw new AppError(AppErrorCode.NOT_FOUND, {
+ message: 'Email domain not found',
+ });
+ }
+
+ const sesClient = getSesClient();
+
+ const response = await sesClient.send(
+ new GetEmailIdentityCommand({
+ EmailIdentity: emailDomain.domain,
+ }),
+ );
+
+ const isVerified = response.VerificationStatus === 'SUCCESS';
+
+ const updatedEmailDomain = await prisma.emailDomain.update({
+ where: {
+ id: emailDomainId,
+ },
+ data: {
+ status: isVerified ? EmailDomainStatus.ACTIVE : EmailDomainStatus.PENDING,
+ },
+ });
+
+ return {
+ emailDomain: updatedEmailDomain,
+ isVerified,
+ };
+};
diff --git a/packages/lib/constants/email.ts b/packages/lib/constants/email.ts
index f385e1748..3eb597ce1 100644
--- a/packages/lib/constants/email.ts
+++ b/packages/lib/constants/email.ts
@@ -3,6 +3,11 @@ import { env } from '../utils/env';
export const FROM_ADDRESS = env('NEXT_PRIVATE_SMTP_FROM_ADDRESS') || 'noreply@documenso.com';
export const FROM_NAME = env('NEXT_PRIVATE_SMTP_FROM_NAME') || 'Documenso';
+export const DOCUMENSO_INTERNAL_EMAIL = {
+ name: FROM_NAME,
+ address: FROM_ADDRESS,
+};
+
export const SERVICE_USER_EMAIL = 'serviceaccount@documenso.com';
export const EMAIL_VERIFICATION_STATE = {
diff --git a/packages/lib/jobs/definitions/emails/send-document-cancelled-emails.handler.ts b/packages/lib/jobs/definitions/emails/send-document-cancelled-emails.handler.ts
index 245d34463..43cbc7846 100644
--- a/packages/lib/jobs/definitions/emails/send-document-cancelled-emails.handler.ts
+++ b/packages/lib/jobs/definitions/emails/send-document-cancelled-emails.handler.ts
@@ -9,7 +9,6 @@ import { prisma } from '@documenso/prisma';
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
-import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
import { getEmailContext } from '../../../server-only/email/get-email-context';
import { extractDerivedDocumentEmailSettings } from '../../../types/document-email';
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
@@ -43,11 +42,13 @@ export const run = async ({
},
});
- const { branding, settings } = await getEmailContext({
+ const { branding, emailLanguage, senderEmail, replyToEmail } = await getEmailContext({
+ emailType: 'RECIPIENT',
source: {
type: 'team',
teamId: document.teamId,
},
+ meta: document.documentMeta || null,
});
const { documentMeta, user: documentOwner } = document;
@@ -59,9 +60,7 @@ export const run = async ({
return;
}
- const lang = documentMeta?.language ?? settings.documentLanguage;
-
- const i18n = await getI18nInstance(lang);
+ const i18n = await getI18nInstance(emailLanguage);
// Send cancellation emails to all recipients who have been sent the document or viewed it
const recipientsToNotify = document.recipients.filter(
@@ -82,9 +81,9 @@ export const run = async ({
});
const [html, text] = await Promise.all([
- renderEmailWithI18N(template, { lang, branding }),
+ renderEmailWithI18N(template, { lang: emailLanguage, branding }),
renderEmailWithI18N(template, {
- lang,
+ lang: emailLanguage,
branding,
plainText: true,
}),
@@ -95,10 +94,8 @@ export const run = async ({
name: recipient.name,
address: recipient.email,
},
- from: {
- name: FROM_NAME,
- address: FROM_ADDRESS,
- },
+ from: senderEmail,
+ replyTo: replyToEmail,
subject: i18n._(msg`Document "${document.title}" Cancelled`),
html,
text,
diff --git a/packages/lib/jobs/definitions/emails/send-organisation-member-joined-email.handler.ts b/packages/lib/jobs/definitions/emails/send-organisation-member-joined-email.handler.ts
index 37b1db387..444ce2985 100644
--- a/packages/lib/jobs/definitions/emails/send-organisation-member-joined-email.handler.ts
+++ b/packages/lib/jobs/definitions/emails/send-organisation-member-joined-email.handler.ts
@@ -8,7 +8,6 @@ import { prisma } from '@documenso/prisma';
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
-import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '../../../constants/organisations';
import { getEmailContext } from '../../../server-only/email/get-email-context';
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
@@ -56,7 +55,8 @@ export const run = async ({
},
});
- const { branding, settings } = await getEmailContext({
+ const { branding, emailLanguage, senderEmail } = await getEmailContext({
+ emailType: 'INTERNAL',
source: {
type: 'organisation',
organisationId: organisation.id,
@@ -80,29 +80,24 @@ export const run = async ({
organisationUrl: organisation.url,
});
- const lang = settings.documentLanguage;
-
// !: Replace with the actual language of the recipient later
const [html, text] = await Promise.all([
renderEmailWithI18N(emailContent, {
- lang,
+ lang: emailLanguage,
branding,
}),
renderEmailWithI18N(emailContent, {
- lang,
+ lang: emailLanguage,
branding,
plainText: true,
}),
]);
- const i18n = await getI18nInstance(lang);
+ const i18n = await getI18nInstance(emailLanguage);
await mailer.sendMail({
to: member.user.email,
- from: {
- name: FROM_NAME,
- address: FROM_ADDRESS,
- },
+ from: senderEmail,
subject: i18n._(msg`A new member has joined your organisation`),
html,
text,
diff --git a/packages/lib/jobs/definitions/emails/send-organisation-member-left-email.handler.ts b/packages/lib/jobs/definitions/emails/send-organisation-member-left-email.handler.ts
index 9ce4162ab..2bfc88aef 100644
--- a/packages/lib/jobs/definitions/emails/send-organisation-member-left-email.handler.ts
+++ b/packages/lib/jobs/definitions/emails/send-organisation-member-left-email.handler.ts
@@ -8,7 +8,6 @@ import { prisma } from '@documenso/prisma';
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
-import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '../../../constants/organisations';
import { getEmailContext } from '../../../server-only/email/get-email-context';
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
@@ -52,7 +51,8 @@ export const run = async ({
},
});
- const { branding, settings } = await getEmailContext({
+ const { branding, emailLanguage, senderEmail } = await getEmailContext({
+ emailType: 'INTERNAL',
source: {
type: 'organisation',
organisationId: organisation.id,
@@ -76,28 +76,23 @@ export const run = async ({
organisationUrl: organisation.url,
});
- const lang = settings.documentLanguage;
-
const [html, text] = await Promise.all([
renderEmailWithI18N(emailContent, {
- lang,
+ lang: emailLanguage,
branding,
}),
renderEmailWithI18N(emailContent, {
- lang,
+ lang: emailLanguage,
branding,
plainText: true,
}),
]);
- const i18n = await getI18nInstance(lang);
+ const i18n = await getI18nInstance(emailLanguage);
await mailer.sendMail({
to: member.user.email,
- from: {
- name: FROM_NAME,
- address: FROM_ADDRESS,
- },
+ from: senderEmail,
subject: i18n._(msg`A member has left your organisation`),
html,
text,
diff --git a/packages/lib/jobs/definitions/emails/send-recipient-signed-email.handler.ts b/packages/lib/jobs/definitions/emails/send-recipient-signed-email.handler.ts
index 6c094c606..7845333c1 100644
--- a/packages/lib/jobs/definitions/emails/send-recipient-signed-email.handler.ts
+++ b/packages/lib/jobs/definitions/emails/send-recipient-signed-email.handler.ts
@@ -8,7 +8,6 @@ import { prisma } from '@documenso/prisma';
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
-import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
import { getEmailContext } from '../../../server-only/email/get-email-context';
import { extractDerivedDocumentEmailSettings } from '../../../types/document-email';
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
@@ -71,17 +70,18 @@ export const run = async ({
return;
}
- const { branding, settings } = await getEmailContext({
+ const { branding, emailLanguage, senderEmail } = await getEmailContext({
+ emailType: 'INTERNAL',
source: {
type: 'team',
teamId: document.teamId,
},
+ meta: document.documentMeta || null,
});
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
- const lang = document.documentMeta?.language ?? settings.documentLanguage;
- const i18n = await getI18nInstance(lang);
+ const i18n = await getI18nInstance(emailLanguage);
const template = createElement(DocumentRecipientSignedEmailTemplate, {
documentName: document.title,
@@ -92,9 +92,9 @@ export const run = async ({
await io.runTask('send-recipient-signed-email', async () => {
const [html, text] = await Promise.all([
- renderEmailWithI18N(template, { lang, branding }),
+ renderEmailWithI18N(template, { lang: emailLanguage, branding }),
renderEmailWithI18N(template, {
- lang,
+ lang: emailLanguage,
branding,
plainText: true,
}),
@@ -105,10 +105,7 @@ export const run = async ({
name: owner.name ?? '',
address: owner.email,
},
- from: {
- name: FROM_NAME,
- address: FROM_ADDRESS,
- },
+ from: senderEmail,
subject: i18n._(msg`${recipientReference} has signed "${document.title}"`),
html,
text,
diff --git a/packages/lib/jobs/definitions/emails/send-rejection-emails.handler.ts b/packages/lib/jobs/definitions/emails/send-rejection-emails.handler.ts
index 25ff654a7..8faa098f8 100644
--- a/packages/lib/jobs/definitions/emails/send-rejection-emails.handler.ts
+++ b/packages/lib/jobs/definitions/emails/send-rejection-emails.handler.ts
@@ -10,7 +10,7 @@ import { prisma } from '@documenso/prisma';
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
-import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
+import { DOCUMENSO_INTERNAL_EMAIL } from '../../../constants/email';
import { getEmailContext } from '../../../server-only/email/get-email-context';
import { extractDerivedDocumentEmailSettings } from '../../../types/document-email';
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
@@ -52,7 +52,7 @@ export const run = async ({
}),
]);
- const { documentMeta, user: documentOwner } = document;
+ const { user: documentOwner } = document;
const isEmailEnabled = extractDerivedDocumentEmailSettings(
document.documentMeta,
@@ -62,16 +62,16 @@ export const run = async ({
return;
}
- const { branding, settings } = await getEmailContext({
+ const { branding, emailLanguage, senderEmail, replyToEmail } = await getEmailContext({
+ emailType: 'RECIPIENT',
source: {
type: 'team',
teamId: document.teamId,
},
+ meta: document.documentMeta || null,
});
- const lang = documentMeta?.language ?? settings.documentLanguage;
-
- const i18n = await getI18nInstance(lang);
+ const i18n = await getI18nInstance(emailLanguage);
// Send confirmation email to the recipient who rejected
await io.runTask('send-rejection-confirmation-email', async () => {
@@ -84,9 +84,9 @@ export const run = async ({
});
const [html, text] = await Promise.all([
- renderEmailWithI18N(recipientTemplate, { lang, branding }),
+ renderEmailWithI18N(recipientTemplate, { lang: emailLanguage, branding }),
renderEmailWithI18N(recipientTemplate, {
- lang,
+ lang: emailLanguage,
branding,
plainText: true,
}),
@@ -97,10 +97,8 @@ export const run = async ({
name: recipient.name,
address: recipient.email,
},
- from: {
- name: FROM_NAME,
- address: FROM_ADDRESS,
- },
+ from: senderEmail,
+ replyTo: replyToEmail,
subject: i18n._(msg`Document "${document.title}" - Rejection Confirmed`),
html,
text,
@@ -120,9 +118,9 @@ export const run = async ({
});
const [html, text] = await Promise.all([
- renderEmailWithI18N(ownerTemplate, { lang, branding }),
+ renderEmailWithI18N(ownerTemplate, { lang: emailLanguage, branding }),
renderEmailWithI18N(ownerTemplate, {
- lang,
+ lang: emailLanguage,
branding,
plainText: true,
}),
@@ -133,10 +131,7 @@ export const run = async ({
name: documentOwner.name || '',
address: documentOwner.email,
},
- from: {
- name: FROM_NAME,
- address: FROM_ADDRESS,
- },
+ from: DOCUMENSO_INTERNAL_EMAIL, // Purposefully using internal email here.
subject: i18n._(msg`Document "${document.title}" - Rejected by ${recipient.name}`),
html,
text,
diff --git a/packages/lib/jobs/definitions/emails/send-signing-email.handler.ts b/packages/lib/jobs/definitions/emails/send-signing-email.handler.ts
index edc585a11..dc5bde691 100644
--- a/packages/lib/jobs/definitions/emails/send-signing-email.handler.ts
+++ b/packages/lib/jobs/definitions/emails/send-signing-email.handler.ts
@@ -15,7 +15,6 @@ import { prisma } from '@documenso/prisma';
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
-import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
import {
RECIPIENT_ROLES_DESCRIPTION,
RECIPIENT_ROLE_TO_EMAIL_TYPE,
@@ -80,12 +79,15 @@ export const run = async ({
return;
}
- const { branding, settings, organisationType } = await getEmailContext({
- source: {
- type: 'team',
- teamId: document.teamId,
- },
- });
+ const { branding, emailLanguage, settings, organisationType, senderEmail, replyToEmail } =
+ await getEmailContext({
+ emailType: 'RECIPIENT',
+ source: {
+ type: 'team',
+ teamId: document.teamId,
+ },
+ meta: document.documentMeta || null,
+ });
const customEmail = document?.documentMeta;
const isDirectTemplate = document.source === DocumentSource.TEMPLATE_DIRECT_LINK;
@@ -95,9 +97,7 @@ export const run = async ({
const { email, name } = recipient;
const selfSigner = email === user.email;
- const lang = documentMeta?.language ?? settings.documentLanguage;
-
- const i18n = await getI18nInstance(lang);
+ const i18n = await getI18nInstance(emailLanguage);
const recipientActionVerb = i18n
._(RECIPIENT_ROLES_DESCRIPTION[recipient.role].actionVerb)
@@ -166,9 +166,9 @@ export const run = async ({
await io.runTask('send-signing-email', async () => {
const [html, text] = await Promise.all([
- renderEmailWithI18N(template, { lang, branding }),
+ renderEmailWithI18N(template, { lang: emailLanguage, branding }),
renderEmailWithI18N(template, {
- lang,
+ lang: emailLanguage,
branding,
plainText: true,
}),
@@ -179,10 +179,8 @@ export const run = async ({
name: recipient.name,
address: recipient.email,
},
- from: {
- name: FROM_NAME,
- address: FROM_ADDRESS,
- },
+ from: senderEmail,
+ replyTo: replyToEmail,
subject: renderCustomEmailTemplate(
documentMeta?.subject || emailSubject,
customEmailTemplate,
diff --git a/packages/lib/jobs/definitions/internal/bulk-send-template.handler.ts b/packages/lib/jobs/definitions/internal/bulk-send-template.handler.ts
index a7ee7d6b2..98e6daba9 100644
--- a/packages/lib/jobs/definitions/internal/bulk-send-template.handler.ts
+++ b/packages/lib/jobs/definitions/internal/bulk-send-template.handler.ts
@@ -13,7 +13,6 @@ import { prisma } from '@documenso/prisma';
import { getI18nInstance } from '../../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app';
-import { FROM_ADDRESS, FROM_NAME } from '../../../constants/email';
import { AppError } from '../../../errors/app-error';
import { getEmailContext } from '../../../server-only/email/get-email-context';
import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n';
@@ -162,24 +161,23 @@ export const run = async ({
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(),
});
- const { branding, settings } = await getEmailContext({
+ const { branding, emailLanguage, senderEmail } = await getEmailContext({
+ emailType: 'INTERNAL',
source: {
type: 'team',
teamId,
},
});
- const lang = template.templateMeta?.language ?? settings.documentLanguage;
-
- const i18n = await getI18nInstance(lang);
+ const i18n = await getI18nInstance(emailLanguage);
const [html, text] = await Promise.all([
renderEmailWithI18N(completionTemplate, {
- lang,
+ lang: emailLanguage,
branding,
}),
renderEmailWithI18N(completionTemplate, {
- lang,
+ lang: emailLanguage,
branding,
plainText: true,
}),
@@ -190,10 +188,7 @@ export const run = async ({
name: user.name || '',
address: user.email,
},
- from: {
- name: FROM_NAME,
- address: FROM_ADDRESS,
- },
+ from: senderEmail,
subject: i18n._(msg`Bulk Send Complete: ${template.title}`),
html,
text,
diff --git a/packages/lib/package.json b/packages/lib/package.json
index 5d2a4053e..374793db8 100644
--- a/packages/lib/package.json
+++ b/packages/lib/package.json
@@ -16,6 +16,7 @@
},
"dependencies": {
"@aws-sdk/client-s3": "^3.410.0",
+ "@aws-sdk/client-sesv2": "^3.410.0",
"@aws-sdk/cloudfront-signer": "^3.410.0",
"@aws-sdk/s3-request-presigner": "^3.410.0",
"@aws-sdk/signature-v4-crt": "^3.410.0",
diff --git a/packages/lib/server-only/document-meta/upsert-document-meta.ts b/packages/lib/server-only/document-meta/upsert-document-meta.ts
index a9edede98..08ceadcba 100644
--- a/packages/lib/server-only/document-meta/upsert-document-meta.ts
+++ b/packages/lib/server-only/document-meta/upsert-document-meta.ts
@@ -23,6 +23,8 @@ export type CreateDocumentMetaOptions = {
password?: string;
dateFormat?: string;
redirectUrl?: string;
+ emailId?: string | null;
+ emailReplyTo?: string | null;
emailSettings?: TDocumentEmailSettings;
signingOrder?: DocumentSigningOrder;
allowDictateNextSigner?: boolean;
@@ -46,6 +48,8 @@ export const upsertDocumentMeta = async ({
redirectUrl,
signingOrder,
allowDictateNextSigner,
+ emailId,
+ emailReplyTo,
emailSettings,
distributionMethod,
typedSignatureEnabled,
@@ -54,7 +58,7 @@ export const upsertDocumentMeta = async ({
language,
requestMetadata,
}: CreateDocumentMetaOptions) => {
- const { documentWhereInput } = await getDocumentWhereInput({
+ const { documentWhereInput, team } = await getDocumentWhereInput({
documentId,
userId,
teamId,
@@ -75,6 +79,22 @@ export const upsertDocumentMeta = async ({
const { documentMeta: originalDocumentMeta } = document;
+ // Validate the emailId belongs to the organisation.
+ if (emailId) {
+ const email = await prisma.organisationEmail.findFirst({
+ where: {
+ id: emailId,
+ organisationId: team.organisationId,
+ },
+ });
+
+ if (!email) {
+ throw new AppError(AppErrorCode.NOT_FOUND, {
+ message: 'Email not found',
+ });
+ }
+ }
+
return await prisma.$transaction(async (tx) => {
const upsertedDocumentMeta = await tx.documentMeta.upsert({
where: {
@@ -90,6 +110,8 @@ export const upsertDocumentMeta = async ({
redirectUrl,
signingOrder,
allowDictateNextSigner,
+ emailId,
+ emailReplyTo,
emailSettings,
distributionMethod,
typedSignatureEnabled,
@@ -106,6 +128,8 @@ export const upsertDocumentMeta = async ({
redirectUrl,
signingOrder,
allowDictateNextSigner,
+ emailId,
+ emailReplyTo,
emailSettings,
distributionMethod,
typedSignatureEnabled,
diff --git a/packages/lib/server-only/document/create-document-v2.ts b/packages/lib/server-only/document/create-document-v2.ts
index 1fdb1f962..419bc8935 100644
--- a/packages/lib/server-only/document/create-document-v2.ts
+++ b/packages/lib/server-only/document/create-document-v2.ts
@@ -24,6 +24,7 @@ import {
} from '../../types/webhook-payload';
import { getFileServerSide } from '../../universal/upload/get-file.server';
import { putPdfFileServerSide } from '../../universal/upload/put-file.server';
+import { extractDerivedDocumentMeta } from '../../utils/document';
import { createDocumentAuthOptions, createRecipientAuthOptions } from '../../utils/document-auth';
import { determineDocumentVisibility } from '../../utils/document-visibility';
import { buildTeamWhereQuery } from '../../utils/teams';
@@ -134,6 +135,24 @@ export const createDocumentV2 = async ({
const visibility = determineDocumentVisibility(settings.documentVisibility, teamRole);
+ const emailId = meta?.emailId;
+
+ // Validate that the email ID belongs to the organisation.
+ if (emailId) {
+ const email = await prisma.organisationEmail.findFirst({
+ where: {
+ id: emailId,
+ organisationId: team.organisationId,
+ },
+ });
+
+ if (!email) {
+ throw new AppError(AppErrorCode.NOT_FOUND, {
+ message: 'Email not found',
+ });
+ }
+ }
+
return await prisma.$transaction(async (tx) => {
const document = await tx.document.create({
data: {
@@ -148,15 +167,7 @@ export const createDocumentV2 = async ({
formValues,
source: DocumentSource.DOCUMENT,
documentMeta: {
- create: {
- ...meta,
- signingOrder: meta?.signingOrder || undefined,
- emailSettings: meta?.emailSettings || undefined,
- language: meta?.language || settings.documentLanguage,
- typedSignatureEnabled: meta?.typedSignatureEnabled ?? settings.typedSignatureEnabled,
- uploadSignatureEnabled: meta?.uploadSignatureEnabled ?? settings.uploadSignatureEnabled,
- drawSignatureEnabled: meta?.drawSignatureEnabled ?? settings.drawSignatureEnabled,
- },
+ create: extractDerivedDocumentMeta(settings, meta),
},
},
});
diff --git a/packages/lib/server-only/document/create-document.ts b/packages/lib/server-only/document/create-document.ts
index e0fb096fc..1f1bab522 100644
--- a/packages/lib/server-only/document/create-document.ts
+++ b/packages/lib/server-only/document/create-document.ts
@@ -15,6 +15,7 @@ import {
import { prefixedId } from '../../universal/id';
import { getFileServerSide } from '../../universal/upload/get-file.server';
import { putPdfFileServerSide } from '../../universal/upload/put-file.server';
+import { extractDerivedDocumentMeta } from '../../utils/document';
import { determineDocumentVisibility } from '../../utils/document-visibility';
import { buildTeamWhereQuery } from '../../utils/teams';
import { getTeamById } from '../team/get-team';
@@ -30,6 +31,7 @@ export type CreateDocumentOptions = {
formValues?: Record
;
normalizePdf?: boolean;
timezone?: string;
+ userTimezone?: string;
requestMetadata: ApiRequestMetadata;
folderId?: string;
};
@@ -44,6 +46,7 @@ export const createDocument = async ({
formValues,
requestMetadata,
timezone,
+ userTimezone,
folderId,
}: CreateDocumentOptions) => {
const team = await getTeamById({ userId, teamId });
@@ -101,6 +104,10 @@ export const createDocument = async ({
}
}
+ // userTimezone is last because it's always passed in regardless of the organisation/team settings
+ // for uploads from the frontend
+ const timezoneToUse = timezone || settings.documentTimezone || userTimezone;
+
return await prisma.$transaction(async (tx) => {
const document = await tx.document.create({
data: {
@@ -117,13 +124,9 @@ export const createDocument = async ({
formValues,
source: DocumentSource.DOCUMENT,
documentMeta: {
- create: {
- language: settings.documentLanguage,
- timezone: timezone,
- typedSignatureEnabled: settings.typedSignatureEnabled,
- uploadSignatureEnabled: settings.uploadSignatureEnabled,
- drawSignatureEnabled: settings.drawSignatureEnabled,
- },
+ create: extractDerivedDocumentMeta(settings, {
+ timezone: timezoneToUse,
+ }),
},
},
});
diff --git a/packages/lib/server-only/document/delete-document.ts b/packages/lib/server-only/document/delete-document.ts
index 4aea1ec1c..67511bdd6 100644
--- a/packages/lib/server-only/document/delete-document.ts
+++ b/packages/lib/server-only/document/delete-document.ts
@@ -10,7 +10,6 @@ import { prisma } from '@documenso/prisma';
import { getI18nInstance } from '../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
-import { FROM_ADDRESS, FROM_NAME } from '../../constants/email';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
@@ -151,11 +150,13 @@ const handleDocumentOwnerDelete = async ({
return;
}
- const { branding, settings } = await getEmailContext({
+ const { branding, emailLanguage, senderEmail, replyToEmail } = await getEmailContext({
+ emailType: 'RECIPIENT',
source: {
type: 'team',
teamId: document.teamId,
},
+ meta: document.documentMeta || null,
});
// Soft delete completed documents.
@@ -232,28 +233,24 @@ const handleDocumentOwnerDelete = async ({
assetBaseUrl,
});
- const lang = document.documentMeta?.language ?? settings.documentLanguage;
-
const [html, text] = await Promise.all([
- renderEmailWithI18N(template, { lang, branding }),
+ renderEmailWithI18N(template, { lang: emailLanguage, branding }),
renderEmailWithI18N(template, {
- lang,
+ lang: emailLanguage,
branding,
plainText: true,
}),
]);
- const i18n = await getI18nInstance(lang);
+ const i18n = await getI18nInstance(emailLanguage);
await mailer.sendMail({
to: {
address: recipient.email,
name: recipient.name,
},
- from: {
- name: FROM_NAME,
- address: FROM_ADDRESS,
- },
+ from: senderEmail,
+ replyTo: replyToEmail,
subject: i18n._(msg`Document Cancelled`),
html,
text,
diff --git a/packages/lib/server-only/document/resend-document.ts b/packages/lib/server-only/document/resend-document.ts
index 4ec896102..bf64a2e6b 100644
--- a/packages/lib/server-only/document/resend-document.ts
+++ b/packages/lib/server-only/document/resend-document.ts
@@ -5,7 +5,6 @@ import { DocumentStatus, OrganisationType, RecipientRole, SigningStatus } from '
import { mailer } from '@documenso/email/mailer';
import { DocumentInviteEmailTemplate } from '@documenso/email/templates/document-invite';
-import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email';
import {
RECIPIENT_ROLES_DESCRIPTION,
RECIPIENT_ROLE_TO_EMAIL_TYPE,
@@ -96,12 +95,15 @@ export const resendDocument = async ({
return;
}
- const { branding, settings, organisationType } = await getEmailContext({
- source: {
- type: 'team',
- teamId: document.teamId,
- },
- });
+ const { branding, emailLanguage, organisationType, senderEmail, replyToEmail } =
+ await getEmailContext({
+ emailType: 'RECIPIENT',
+ source: {
+ type: 'team',
+ teamId: document.teamId,
+ },
+ meta: document.documentMeta || null,
+ });
await Promise.all(
recipientsToRemind.map(async (recipient) => {
@@ -109,8 +111,7 @@ export const resendDocument = async ({
return;
}
- const lang = document.documentMeta?.language ?? settings.documentLanguage;
- const i18n = await getI18nInstance(lang);
+ const i18n = await getI18nInstance(emailLanguage);
const recipientEmailType = RECIPIENT_ROLE_TO_EMAIL_TYPE[recipient.role];
@@ -169,11 +170,11 @@ export const resendDocument = async ({
const [html, text] = await Promise.all([
renderEmailWithI18N(template, {
- lang,
+ lang: emailLanguage,
branding,
}),
renderEmailWithI18N(template, {
- lang,
+ lang: emailLanguage,
branding,
plainText: true,
}),
@@ -186,10 +187,8 @@ export const resendDocument = async ({
address: email,
name,
},
- from: {
- name: FROM_NAME,
- address: FROM_ADDRESS,
- },
+ from: senderEmail,
+ replyTo: replyToEmail,
subject: customEmail?.subject
? renderCustomEmailTemplate(
i18n._(msg`Reminder: ${customEmail.subject}`),
diff --git a/packages/lib/server-only/document/send-completed-email.ts b/packages/lib/server-only/document/send-completed-email.ts
index 38322f77c..c69ef772b 100644
--- a/packages/lib/server-only/document/send-completed-email.ts
+++ b/packages/lib/server-only/document/send-completed-email.ts
@@ -14,7 +14,6 @@ import { extractDerivedDocumentEmailSettings } from '../../types/document-email'
import type { RequestMetadata } from '../../universal/extract-request-metadata';
import { getFileServerSide } from '../../universal/upload/get-file.server';
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
-import { env } from '../../utils/env';
import { renderCustomEmailTemplate } from '../../utils/render-custom-email-template';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
import { formatDocumentsPath } from '../../utils/teams';
@@ -54,11 +53,13 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
throw new Error('Document has no recipients');
}
- const { branding, settings } = await getEmailContext({
+ const { branding, emailLanguage, senderEmail, replyToEmail } = await getEmailContext({
+ emailType: 'RECIPIENT',
source: {
type: 'team',
teamId: document.teamId,
},
+ meta: document.documentMeta || null,
});
const { user: owner } = document;
@@ -97,18 +98,16 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
downloadLink: documentOwnerDownloadLink,
});
- const lang = document.documentMeta?.language ?? settings.documentLanguage;
-
const [html, text] = await Promise.all([
- renderEmailWithI18N(template, { lang, branding }),
+ renderEmailWithI18N(template, { lang: emailLanguage, branding }),
renderEmailWithI18N(template, {
- lang,
+ lang: emailLanguage,
branding,
plainText: true,
}),
]);
- const i18n = await getI18nInstance(lang);
+ const i18n = await getI18nInstance(emailLanguage);
await mailer.sendMail({
to: [
@@ -117,10 +116,8 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
address: owner.email,
},
],
- from: {
- name: env('NEXT_PRIVATE_SMTP_FROM_NAME') || 'Documenso',
- address: env('NEXT_PRIVATE_SMTP_FROM_ADDRESS') || 'noreply@documenso.com',
- },
+ from: senderEmail,
+ replyTo: replyToEmail,
subject: i18n._(msg`Signing Complete!`),
html,
text,
@@ -174,18 +171,16 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
: undefined,
});
- const lang = document.documentMeta?.language ?? settings.documentLanguage;
-
const [html, text] = await Promise.all([
- renderEmailWithI18N(template, { lang, branding }),
+ renderEmailWithI18N(template, { lang: emailLanguage, branding }),
renderEmailWithI18N(template, {
- lang,
+ lang: emailLanguage,
branding,
plainText: true,
}),
]);
- const i18n = await getI18nInstance(lang);
+ const i18n = await getI18nInstance(emailLanguage);
await mailer.sendMail({
to: [
@@ -194,10 +189,8 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo
address: recipient.email,
},
],
- from: {
- name: env('NEXT_PRIVATE_SMTP_FROM_NAME') || 'Documenso',
- address: env('NEXT_PRIVATE_SMTP_FROM_ADDRESS') || 'noreply@documenso.com',
- },
+ from: senderEmail,
+ replyTo: replyToEmail,
subject:
isDirectTemplate && document.documentMeta?.subject
? renderCustomEmailTemplate(document.documentMeta.subject, customEmailTemplate)
diff --git a/packages/lib/server-only/document/send-delete-email.ts b/packages/lib/server-only/document/send-delete-email.ts
index 0c969574f..5ac1043ed 100644
--- a/packages/lib/server-only/document/send-delete-email.ts
+++ b/packages/lib/server-only/document/send-delete-email.ts
@@ -10,7 +10,6 @@ import { getI18nInstance } from '../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
-import { env } from '../../utils/env';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
import { getEmailContext } from '../email/get-email-context';
@@ -44,11 +43,13 @@ export const sendDeleteEmail = async ({ documentId, reason }: SendDeleteEmailOpt
return;
}
- const { branding, settings } = await getEmailContext({
+ const { branding, emailLanguage, senderEmail } = await getEmailContext({
+ emailType: 'INTERNAL',
source: {
type: 'team',
teamId: document.teamId,
},
+ meta: document.documentMeta || null,
});
const { email, name } = document.user;
@@ -61,28 +62,23 @@ export const sendDeleteEmail = async ({ documentId, reason }: SendDeleteEmailOpt
assetBaseUrl,
});
- const lang = document.documentMeta?.language ?? settings.documentLanguage;
-
const [html, text] = await Promise.all([
- renderEmailWithI18N(template, { lang, branding }),
+ renderEmailWithI18N(template, { lang: emailLanguage, branding }),
renderEmailWithI18N(template, {
- lang,
+ lang: emailLanguage,
branding,
plainText: true,
}),
]);
- const i18n = await getI18nInstance(lang);
+ const i18n = await getI18nInstance(emailLanguage);
await mailer.sendMail({
to: {
address: email,
name: name || '',
},
- from: {
- name: env('NEXT_PRIVATE_SMTP_FROM_NAME') || 'Documenso',
- address: env('NEXT_PRIVATE_SMTP_FROM_ADDRESS') || 'noreply@documenso.com',
- },
+ from: senderEmail,
subject: i18n._(msg`Document Deleted!`),
html,
text,
diff --git a/packages/lib/server-only/document/send-pending-email.ts b/packages/lib/server-only/document/send-pending-email.ts
index 9726cd5cc..7609eb3a3 100644
--- a/packages/lib/server-only/document/send-pending-email.ts
+++ b/packages/lib/server-only/document/send-pending-email.ts
@@ -9,7 +9,6 @@ import { prisma } from '@documenso/prisma';
import { getI18nInstance } from '../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
-import { env } from '../../utils/env';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
import { getEmailContext } from '../email/get-email-context';
@@ -46,11 +45,13 @@ export const sendPendingEmail = async ({ documentId, recipientId }: SendPendingE
throw new Error('Document has no recipients');
}
- const { branding, settings } = await getEmailContext({
+ const { branding, emailLanguage, senderEmail, replyToEmail } = await getEmailContext({
+ emailType: 'RECIPIENT',
source: {
type: 'team',
teamId: document.teamId,
},
+ meta: document.documentMeta || null,
});
const isDocumentPendingEmailEnabled = extractDerivedDocumentEmailSettings(
@@ -72,28 +73,24 @@ export const sendPendingEmail = async ({ documentId, recipientId }: SendPendingE
assetBaseUrl,
});
- const lang = document.documentMeta?.language ?? settings.documentLanguage;
-
const [html, text] = await Promise.all([
- renderEmailWithI18N(template, { lang, branding }),
+ renderEmailWithI18N(template, { lang: emailLanguage, branding }),
renderEmailWithI18N(template, {
- lang,
+ lang: emailLanguage,
branding,
plainText: true,
}),
]);
- const i18n = await getI18nInstance(lang);
+ const i18n = await getI18nInstance(emailLanguage);
await mailer.sendMail({
to: {
address: email,
name,
},
- from: {
- name: env('NEXT_PRIVATE_SMTP_FROM_NAME') || 'Documenso',
- address: env('NEXT_PRIVATE_SMTP_FROM_ADDRESS') || 'noreply@documenso.com',
- },
+ from: senderEmail,
+ replyTo: replyToEmail,
subject: i18n._(msg`Waiting for others to complete signing.`),
html,
text,
diff --git a/packages/lib/server-only/document/super-delete-document.ts b/packages/lib/server-only/document/super-delete-document.ts
index 85dc5aab2..ae2e2bf4d 100644
--- a/packages/lib/server-only/document/super-delete-document.ts
+++ b/packages/lib/server-only/document/super-delete-document.ts
@@ -9,7 +9,6 @@ import { prisma } from '@documenso/prisma';
import { getI18nInstance } from '../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
-import { FROM_ADDRESS, FROM_NAME } from '../../constants/email';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
@@ -41,11 +40,13 @@ export const superDeleteDocument = async ({ id, requestMetadata }: SuperDeleteDo
});
}
- const { branding, settings } = await getEmailContext({
+ const { branding, settings, senderEmail, replyToEmail } = await getEmailContext({
+ emailType: 'RECIPIENT',
source: {
type: 'team',
teamId: document.teamId,
},
+ meta: document.documentMeta || null,
});
const { status, user } = document;
@@ -92,10 +93,8 @@ export const superDeleteDocument = async ({ id, requestMetadata }: SuperDeleteDo
address: recipient.email,
name: recipient.name,
},
- from: {
- name: FROM_NAME,
- address: FROM_ADDRESS,
- },
+ from: senderEmail,
+ replyTo: replyToEmail,
subject: i18n._(msg`Document Cancelled`),
html,
text,
diff --git a/packages/lib/server-only/document/update-document.ts b/packages/lib/server-only/document/update-document.ts
index accb9fadf..0c1586498 100644
--- a/packages/lib/server-only/document/update-document.ts
+++ b/packages/lib/server-only/document/update-document.ts
@@ -1,5 +1,6 @@
import { DocumentVisibility } from '@prisma/client';
import { DocumentStatus, TeamMemberRole } from '@prisma/client';
+import { isDeepEqual } from 'remeda';
import { match } from 'ts-pattern';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
@@ -128,9 +129,11 @@ export const updateDocument = async ({
const isTitleSame = data.title === undefined || data.title === document.title;
const isExternalIdSame = data.externalId === undefined || data.externalId === document.externalId;
const isGlobalAccessSame =
- documentGlobalAccessAuth === undefined || documentGlobalAccessAuth === newGlobalAccessAuth;
+ documentGlobalAccessAuth === undefined ||
+ isDeepEqual(documentGlobalAccessAuth, newGlobalAccessAuth);
const isGlobalActionSame =
- documentGlobalActionAuth === undefined || documentGlobalActionAuth === newGlobalActionAuth;
+ documentGlobalActionAuth === undefined ||
+ isDeepEqual(documentGlobalActionAuth, newGlobalActionAuth);
const isDocumentVisibilitySame =
data.visibility === undefined || data.visibility === document.visibility;
diff --git a/packages/lib/server-only/email/get-email-context.ts b/packages/lib/server-only/email/get-email-context.ts
index 5b8961008..56964ce28 100644
--- a/packages/lib/server-only/email/get-email-context.ts
+++ b/packages/lib/server-only/email/get-email-context.ts
@@ -1,16 +1,34 @@
import type { BrandingSettings } from '@documenso/email/providers/branding';
import { prisma } from '@documenso/prisma';
-import type { OrganisationType } from '@documenso/prisma/client';
-import { type OrganisationClaim, type OrganisationGlobalSettings } from '@documenso/prisma/client';
+import type {
+ DocumentMeta,
+ EmailDomain,
+ Organisation,
+ OrganisationEmail,
+ OrganisationType,
+} from '@documenso/prisma/client';
+import {
+ EmailDomainStatus,
+ type OrganisationClaim,
+ type OrganisationGlobalSettings,
+} from '@documenso/prisma/client';
+import { DOCUMENSO_INTERNAL_EMAIL } from '../../constants/email';
import { AppError, AppErrorCode } from '../../errors/app-error';
import {
organisationGlobalSettingsToBranding,
teamGlobalSettingsToBranding,
} from '../../utils/team-global-settings-to-branding';
-import { getTeamSettings } from '../team/get-team-settings';
+import { extractDerivedTeamSettings } from '../../utils/teams';
-type GetEmailContextOptions = {
+type EmailMetaOption = Partial>;
+
+type BaseGetEmailContextOptions = {
+ /**
+ * The source to extract the email context from.
+ * - "Team" will use the team settings followed by the inherited organisation settings
+ * - "Organisation" will use the organisation settings
+ */
source:
| {
type: 'team';
@@ -20,37 +38,112 @@ type GetEmailContextOptions = {
type: 'organisation';
organisationId: string;
};
+
+ /**
+ * The email type being sent, used to determine what email sender and language to use.
+ * - INTERNAL: Emails to users, such as team invites, etc.
+ * - RECIPIENT: Emails to recipients, such as document sent, document signed, etc.
+ */
+ emailType: 'INTERNAL' | 'RECIPIENT';
};
+type InternalGetEmailContextOptions = BaseGetEmailContextOptions & {
+ emailType: 'INTERNAL';
+ meta?: EmailMetaOption | null;
+};
+
+type RecipientGetEmailContextOptions = BaseGetEmailContextOptions & {
+ emailType: 'RECIPIENT';
+
+ /**
+ * Force meta options as a typesafe way to ensure developers don't forget to
+ * pass it in if it is available.
+ */
+ meta: EmailMetaOption | null;
+};
+
+type GetEmailContextOptions = InternalGetEmailContextOptions | RecipientGetEmailContextOptions;
+
type EmailContextResponse = {
+ allowedEmails: OrganisationEmail[];
branding: BrandingSettings;
settings: Omit;
claims: OrganisationClaim;
organisationType: OrganisationType;
+ senderEmail: {
+ name: string;
+ address: string;
+ };
+ replyToEmail: string | undefined;
+ emailLanguage: string;
};
export const getEmailContext = async (
options: GetEmailContextOptions,
): Promise => {
- const { source } = options;
+ const { source, meta } = options;
+ let emailContext: Omit;
+
+ if (source.type === 'organisation') {
+ emailContext = await handleOrganisationEmailContext(source.organisationId);
+ } else {
+ emailContext = await handleTeamEmailContext(source.teamId);
+ }
+
+ const emailLanguage = meta?.language || emailContext.settings.documentLanguage;
+
+ // Immediate return for internal emails.
+ if (options.emailType === 'INTERNAL') {
+ return {
+ ...emailContext,
+ senderEmail: DOCUMENSO_INTERNAL_EMAIL,
+ replyToEmail: undefined,
+ emailLanguage, // Not sure if we want to use this for internal emails.
+ };
+ }
+
+ const replyToEmail = meta?.emailReplyTo || emailContext.settings.emailReplyTo || undefined;
+ const senderEmailId = meta?.emailId || emailContext.settings.emailId;
+
+ const foundSenderEmail = emailContext.allowedEmails.find((email) => email.id === senderEmailId);
+
+ // Reset the emailId to null if not found.
+ if (!foundSenderEmail) {
+ emailContext.settings.emailId = null;
+ }
+
+ const senderEmail = foundSenderEmail
+ ? {
+ name: foundSenderEmail.emailName,
+ address: foundSenderEmail.email,
+ }
+ : DOCUMENSO_INTERNAL_EMAIL;
+
+ return {
+ ...emailContext,
+ senderEmail,
+ replyToEmail,
+ emailLanguage,
+ };
+};
+
+const handleOrganisationEmailContext = async (organisationId: string) => {
const organisation = await prisma.organisation.findFirst({
- where:
- source.type === 'organisation'
- ? {
- id: source.organisationId,
- }
- : {
- teams: {
- some: {
- id: source.teamId,
- },
- },
- },
+ where: {
+ id: organisationId,
+ },
include: {
- subscription: true,
organisationClaim: true,
organisationGlobalSettings: true,
+ emailDomains: {
+ omit: {
+ privateKey: true,
+ },
+ include: {
+ emails: true,
+ },
+ },
},
});
@@ -60,27 +153,64 @@ export const getEmailContext = async (
const claims = organisation.organisationClaim;
- if (source.type === 'organisation') {
- return {
- branding: organisationGlobalSettingsToBranding(
- organisation.organisationGlobalSettings,
- organisation.id,
- claims.flags.hidePoweredBy ?? false,
- ),
- settings: organisation.organisationGlobalSettings,
- claims,
- organisationType: organisation.type,
- };
- }
-
- const teamSettings = await getTeamSettings({
- teamId: source.teamId,
- });
+ const allowedEmails = getAllowedEmails(organisation);
return {
+ allowedEmails,
+ branding: organisationGlobalSettingsToBranding(
+ organisation.organisationGlobalSettings,
+ organisation.id,
+ claims.flags.hidePoweredBy ?? false,
+ ),
+ settings: organisation.organisationGlobalSettings,
+ claims,
+ organisationType: organisation.type,
+ };
+};
+
+const handleTeamEmailContext = async (teamId: number) => {
+ const team = await prisma.team.findFirst({
+ where: {
+ id: teamId,
+ },
+ include: {
+ teamGlobalSettings: true,
+ organisation: {
+ include: {
+ organisationClaim: true,
+ organisationGlobalSettings: true,
+ emailDomains: {
+ omit: {
+ privateKey: true,
+ },
+ include: {
+ emails: true,
+ },
+ },
+ },
+ },
+ },
+ });
+
+ if (!team) {
+ throw new AppError(AppErrorCode.NOT_FOUND);
+ }
+
+ const organisation = team.organisation;
+ const claims = organisation.organisationClaim;
+
+ const allowedEmails = getAllowedEmails(organisation);
+
+ const teamSettings = extractDerivedTeamSettings(
+ organisation.organisationGlobalSettings,
+ team.teamGlobalSettings,
+ );
+
+ return {
+ allowedEmails,
branding: teamGlobalSettingsToBranding(
teamSettings,
- source.teamId,
+ teamId,
claims.flags.hidePoweredBy ?? false,
),
settings: teamSettings,
@@ -88,3 +218,18 @@ export const getEmailContext = async (
organisationType: organisation.type,
};
};
+
+const getAllowedEmails = (
+ organisation: Organisation & {
+ emailDomains: (Pick & { emails: OrganisationEmail[] })[];
+ organisationClaim: OrganisationClaim;
+ },
+) => {
+ if (!organisation.organisationClaim.flags.emailDomains) {
+ return [];
+ }
+
+ return organisation.emailDomains
+ .filter((emailDomain) => emailDomain.status === EmailDomainStatus.ACTIVE)
+ .flatMap((emailDomain) => emailDomain.emails);
+};
diff --git a/packages/lib/server-only/organisation/create-organisation-member-invites.ts b/packages/lib/server-only/organisation/create-organisation-member-invites.ts
index 032a49d03..209b1aeca 100644
--- a/packages/lib/server-only/organisation/create-organisation-member-invites.ts
+++ b/packages/lib/server-only/organisation/create-organisation-member-invites.ts
@@ -9,7 +9,6 @@ import { syncMemberCountWithStripeSeatPlan } from '@documenso/ee/server-only/str
import { mailer } from '@documenso/email/mailer';
import { OrganisationInviteEmailTemplate } from '@documenso/email/templates/organisation-invite';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
-import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email';
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { isOrganisationRoleWithinUserHierarchy } from '@documenso/lib/utils/organisations';
@@ -190,7 +189,8 @@ export const sendOrganisationMemberInviteEmail = async ({
organisationName: organisation.name,
});
- const { branding, settings } = await getEmailContext({
+ const { branding, emailLanguage, senderEmail } = await getEmailContext({
+ emailType: 'INTERNAL',
source: {
type: 'organisation',
organisationId: organisation.id,
@@ -199,24 +199,21 @@ export const sendOrganisationMemberInviteEmail = async ({
const [html, text] = await Promise.all([
renderEmailWithI18N(template, {
- lang: settings.documentLanguage,
+ lang: emailLanguage,
branding,
}),
renderEmailWithI18N(template, {
- lang: settings.documentLanguage,
+ lang: emailLanguage,
branding,
plainText: true,
}),
]);
- const i18n = await getI18nInstance(settings.documentLanguage);
+ const i18n = await getI18nInstance(emailLanguage);
await mailer.sendMail({
to: email,
- from: {
- name: FROM_NAME,
- address: FROM_ADDRESS,
- },
+ from: senderEmail,
subject: i18n._(msg`You have been invited to join ${organisation.name} on Documenso`),
html,
text,
diff --git a/packages/lib/server-only/recipient/delete-document-recipient.ts b/packages/lib/server-only/recipient/delete-document-recipient.ts
index a364fa410..a9b7790b9 100644
--- a/packages/lib/server-only/recipient/delete-document-recipient.ts
+++ b/packages/lib/server-only/recipient/delete-document-recipient.ts
@@ -11,7 +11,6 @@ import { prisma } from '@documenso/prisma';
import { getI18nInstance } from '../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
-import { FROM_ADDRESS, FROM_NAME } from '../../constants/email';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
@@ -125,31 +124,29 @@ export const deleteDocumentRecipient = async ({
assetBaseUrl,
});
- const { branding, settings } = await getEmailContext({
+ const { branding, emailLanguage, senderEmail, replyToEmail } = await getEmailContext({
+ emailType: 'RECIPIENT',
source: {
type: 'team',
teamId: document.teamId,
},
+ meta: document.documentMeta || null,
});
- const lang = document.documentMeta?.language ?? settings.documentLanguage;
-
const [html, text] = await Promise.all([
- renderEmailWithI18N(template, { lang, branding }),
- renderEmailWithI18N(template, { lang, branding, plainText: true }),
+ renderEmailWithI18N(template, { lang: emailLanguage, branding }),
+ renderEmailWithI18N(template, { lang: emailLanguage, branding, plainText: true }),
]);
- const i18n = await getI18nInstance(lang);
+ const i18n = await getI18nInstance(emailLanguage);
await mailer.sendMail({
to: {
address: recipientToDelete.email,
name: recipientToDelete.name,
},
- from: {
- name: FROM_NAME,
- address: FROM_ADDRESS,
- },
+ from: senderEmail,
+ replyTo: replyToEmail,
subject: i18n._(msg`You have been removed from a document`),
html,
text,
diff --git a/packages/lib/server-only/recipient/set-document-recipients.ts b/packages/lib/server-only/recipient/set-document-recipients.ts
index abe55dc58..25ff0b480 100644
--- a/packages/lib/server-only/recipient/set-document-recipients.ts
+++ b/packages/lib/server-only/recipient/set-document-recipients.ts
@@ -25,7 +25,6 @@ import { prisma } from '@documenso/prisma';
import { getI18nInstance } from '../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
-import { FROM_ADDRESS, FROM_NAME } from '../../constants/email';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
import { canRecipientBeModified } from '../../utils/recipients';
@@ -71,13 +70,6 @@ export const setDocumentRecipients = async ({
},
});
- const { branding, settings } = await getEmailContext({
- source: {
- type: 'team',
- teamId,
- },
- });
-
const user = await prisma.user.findFirstOrThrow({
where: {
id: userId,
@@ -97,6 +89,15 @@ export const setDocumentRecipients = async ({
throw new Error('Document already complete');
}
+ const { branding, emailLanguage, senderEmail, replyToEmail } = await getEmailContext({
+ emailType: 'RECIPIENT',
+ source: {
+ type: 'team',
+ teamId,
+ },
+ meta: document.documentMeta || null,
+ });
+
const recipientsHaveActionAuth = recipients.some(
(recipient) => recipient.actionAuth && recipient.actionAuth.length > 0,
);
@@ -302,24 +303,20 @@ export const setDocumentRecipients = async ({
assetBaseUrl,
});
- const lang = document.documentMeta?.language ?? settings.documentLanguage;
-
const [html, text] = await Promise.all([
- renderEmailWithI18N(template, { lang, branding }),
- renderEmailWithI18N(template, { lang, branding, plainText: true }),
+ renderEmailWithI18N(template, { lang: emailLanguage, branding }),
+ renderEmailWithI18N(template, { lang: emailLanguage, branding, plainText: true }),
]);
- const i18n = await getI18nInstance(lang);
+ const i18n = await getI18nInstance(emailLanguage);
await mailer.sendMail({
to: {
address: recipient.email,
name: recipient.name,
},
- from: {
- name: FROM_NAME,
- address: FROM_ADDRESS,
- },
+ from: senderEmail,
+ replyTo: replyToEmail,
subject: i18n._(msg`You have been removed from a document`),
html,
text,
diff --git a/packages/lib/server-only/team/create-team-email-verification.ts b/packages/lib/server-only/team/create-team-email-verification.ts
index de4dcddd2..f2565d6a8 100644
--- a/packages/lib/server-only/team/create-team-email-verification.ts
+++ b/packages/lib/server-only/team/create-team-email-verification.ts
@@ -8,14 +8,12 @@ import { z } from 'zod';
import { mailer } from '@documenso/email/mailer';
import { ConfirmTeamEmailTemplate } from '@documenso/email/templates/confirm-team-email';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
-import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email';
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/teams';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { createTokenVerification } from '@documenso/lib/utils/token-verification';
import { prisma } from '@documenso/prisma';
import { getI18nInstance } from '../../client-only/providers/i18n-server';
-import type { SupportedLanguageCodes } from '../../constants/i18n';
import { env } from '../../utils/env';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
import { buildTeamWhereQuery } from '../../utils/teams';
@@ -122,33 +120,28 @@ export const sendTeamEmailVerificationEmail = async (email: string, token: strin
token,
});
- const { branding, settings } = await getEmailContext({
+ const { branding, emailLanguage, senderEmail } = await getEmailContext({
+ emailType: 'INTERNAL',
source: {
type: 'team',
teamId: team.id,
},
});
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
- const lang = settings.documentLanguage as SupportedLanguageCodes;
-
const [html, text] = await Promise.all([
- renderEmailWithI18N(template, { lang, branding }),
+ renderEmailWithI18N(template, { lang: emailLanguage, branding }),
renderEmailWithI18N(template, {
- lang,
+ lang: emailLanguage,
branding,
plainText: true,
}),
]);
- const i18n = await getI18nInstance(lang);
+ const i18n = await getI18nInstance(emailLanguage);
await mailer.sendMail({
to: email,
- from: {
- name: FROM_NAME,
- address: FROM_ADDRESS,
- },
+ from: senderEmail,
subject: i18n._(
msg`A request to use your email has been initiated by ${team.name} on Documenso`,
),
diff --git a/packages/lib/server-only/team/delete-team-email.ts b/packages/lib/server-only/team/delete-team-email.ts
index da6e44f2b..4e4b538e5 100644
--- a/packages/lib/server-only/team/delete-team-email.ts
+++ b/packages/lib/server-only/team/delete-team-email.ts
@@ -5,7 +5,6 @@ import { msg } from '@lingui/core/macro';
import { mailer } from '@documenso/email/mailer';
import { TeamEmailRemovedTemplate } from '@documenso/email/templates/team-email-removed';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
-import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email';
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/teams';
import { prisma } from '@documenso/prisma';
@@ -27,7 +26,8 @@ export type DeleteTeamEmailOptions = {
* The user must either be part of the team with the required permissions, or the owner of the email.
*/
export const deleteTeamEmail = async ({ userId, userEmail, teamId }: DeleteTeamEmailOptions) => {
- const { branding, settings } = await getEmailContext({
+ const { branding, emailLanguage, senderEmail } = await getEmailContext({
+ emailType: 'INTERNAL',
source: {
type: 'team',
teamId,
@@ -82,24 +82,19 @@ export const deleteTeamEmail = async ({ userId, userEmail, teamId }: DeleteTeamE
teamUrl: team.url,
});
- const lang = settings.documentLanguage;
-
const [html, text] = await Promise.all([
- renderEmailWithI18N(template, { lang, branding }),
- renderEmailWithI18N(template, { lang, branding, plainText: true }),
+ renderEmailWithI18N(template, { lang: emailLanguage, branding }),
+ renderEmailWithI18N(template, { lang: emailLanguage, branding, plainText: true }),
]);
- const i18n = await getI18nInstance(lang);
+ const i18n = await getI18nInstance(emailLanguage);
await mailer.sendMail({
to: {
address: team.organisation.owner.email,
name: team.organisation.owner.name ?? '',
},
- from: {
- name: FROM_NAME,
- address: FROM_ADDRESS,
- },
+ from: senderEmail,
subject: i18n._(msg`Team email has been revoked for ${team.name}`),
html,
text,
diff --git a/packages/lib/server-only/team/delete-team.ts b/packages/lib/server-only/team/delete-team.ts
index ccaefa7d4..9ae1cfc68 100644
--- a/packages/lib/server-only/team/delete-team.ts
+++ b/packages/lib/server-only/team/delete-team.ts
@@ -7,7 +7,6 @@ import { uniqueBy } from 'remeda';
import { mailer } from '@documenso/email/mailer';
import { TeamDeleteEmailTemplate } from '@documenso/email/templates/team-delete';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
-import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { prisma } from '@documenso/prisma';
@@ -130,28 +129,24 @@ export const sendTeamDeleteEmail = async ({
teamUrl: team.url,
});
- const { branding, settings } = await getEmailContext({
+ const { branding, emailLanguage, senderEmail } = await getEmailContext({
+ emailType: 'INTERNAL',
source: {
type: 'organisation',
organisationId,
},
});
- const lang = settings.documentLanguage;
-
const [html, text] = await Promise.all([
- renderEmailWithI18N(template, { lang, branding }),
- renderEmailWithI18N(template, { lang, branding, plainText: true }),
+ renderEmailWithI18N(template, { lang: emailLanguage, branding }),
+ renderEmailWithI18N(template, { lang: emailLanguage, branding, plainText: true }),
]);
- const i18n = await getI18nInstance(lang);
+ const i18n = await getI18nInstance(emailLanguage);
await mailer.sendMail({
to: email,
- from: {
- name: FROM_NAME,
- address: FROM_ADDRESS,
- },
+ from: senderEmail,
subject: i18n._(msg`Team "${team.name}" has been deleted on Documenso`),
html,
text,
diff --git a/packages/lib/server-only/team/get-team-settings.ts b/packages/lib/server-only/team/get-team-settings.ts
index 6eb0b0817..0bdac9e69 100644
--- a/packages/lib/server-only/team/get-team-settings.ts
+++ b/packages/lib/server-only/team/get-team-settings.ts
@@ -33,5 +33,13 @@ export const getTeamSettings = async ({ userId, teamId }: GetTeamSettingsOptions
const organisationSettings = team.organisation.organisationGlobalSettings;
const teamSettings = team.teamGlobalSettings;
+ // Override branding settings if inherit is enabled.
+ if (teamSettings.brandingEnabled === null) {
+ teamSettings.brandingEnabled = organisationSettings.brandingEnabled;
+ teamSettings.brandingLogo = organisationSettings.brandingLogo;
+ teamSettings.brandingUrl = organisationSettings.brandingUrl;
+ teamSettings.brandingCompanyDetails = organisationSettings.brandingCompanyDetails;
+ }
+
return extractDerivedTeamSettings(organisationSettings, teamSettings);
};
diff --git a/packages/lib/server-only/template/create-document-from-direct-template.ts b/packages/lib/server-only/template/create-document-from-direct-template.ts
index 9292a8116..bed997aab 100644
--- a/packages/lib/server-only/template/create-document-from-direct-template.ts
+++ b/packages/lib/server-only/template/create-document-from-direct-template.ts
@@ -3,7 +3,6 @@ import { createElement } from 'react';
import { msg } from '@lingui/core/macro';
import type { Field, Signature } from '@prisma/client';
import {
- DocumentSigningOrder,
DocumentSource,
DocumentStatus,
FieldType,
@@ -25,8 +24,6 @@ import type { TSignFieldWithTokenMutationSchema } from '@documenso/trpc/server/f
import { getI18nInstance } from '../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
-import { DEFAULT_DOCUMENT_DATE_FORMAT } from '../../constants/date-formats';
-import { DEFAULT_DOCUMENT_TIME_ZONE } from '../../constants/time-zones';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
import type { TRecipientActionAuthTypes } from '../../types/document-auth';
@@ -38,6 +35,7 @@ import {
} from '../../types/webhook-payload';
import type { ApiRequestMetadata } from '../../universal/extract-request-metadata';
import { isRequiredField } from '../../utils/advanced-fields-helpers';
+import { extractDerivedDocumentMeta } from '../../utils/document';
import type { CreateDocumentAuditLogDataResponse } from '../../utils/document-audit-logs';
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
import {
@@ -45,7 +43,6 @@ import {
createRecipientAuthOptions,
extractDocumentAuthMethods,
} from '../../utils/document-auth';
-import { env } from '../../utils/env';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
import { formatDocumentsPath } from '../../utils/teams';
import { sendDocument } from '../document/send-document';
@@ -116,7 +113,8 @@ export const createDocumentFromDirectTemplate = async ({
throw new AppError(AppErrorCode.INVALID_REQUEST, { message: 'Invalid or missing template' });
}
- const { branding, settings } = await getEmailContext({
+ const { branding, settings, senderEmail, emailLanguage } = await getEmailContext({
+ emailType: 'INTERNAL',
source: {
type: 'team',
teamId: template.teamId,
@@ -169,13 +167,7 @@ export const createDocumentFromDirectTemplate = async ({
const nonDirectTemplateRecipients = template.recipients.filter(
(recipient) => recipient.id !== directTemplateRecipient.id,
);
-
- const metaTimezone = template.templateMeta?.timezone || DEFAULT_DOCUMENT_TIME_ZONE;
- const metaDateFormat = template.templateMeta?.dateFormat || DEFAULT_DOCUMENT_DATE_FORMAT;
- const metaEmailMessage = template.templateMeta?.message || '';
- const metaEmailSubject = template.templateMeta?.subject || '';
- const metaLanguage = template.templateMeta?.language ?? settings.documentLanguage;
- const metaSigningOrder = template.templateMeta?.signingOrder || DocumentSigningOrder.PARALLEL;
+ const derivedDocumentMeta = extractDerivedDocumentMeta(settings, template.templateMeta);
// Associate, validate and map to a query every direct template recipient field with the provided fields.
// Only process fields that are either required or have been signed by the user
@@ -234,7 +226,9 @@ export const createDocumentFromDirectTemplate = async ({
const typedSignature = isSignatureField && !isBase64 ? value : undefined;
if (templateField.type === FieldType.DATE) {
- customText = DateTime.now().setZone(metaTimezone).toFormat(metaDateFormat);
+ customText = DateTime.now()
+ .setZone(derivedDocumentMeta.timezone)
+ .toFormat(derivedDocumentMeta.dateFormat);
}
if (isSignatureField && !signatureImageAsBase64 && !typedSignature) {
@@ -318,18 +312,7 @@ export const createDocumentFromDirectTemplate = async ({
},
},
documentMeta: {
- create: {
- timezone: metaTimezone,
- dateFormat: metaDateFormat,
- message: metaEmailMessage,
- subject: metaEmailSubject,
- language: metaLanguage,
- signingOrder: metaSigningOrder,
- distributionMethod: template.templateMeta?.distributionMethod,
- typedSignatureEnabled: template.templateMeta?.typedSignatureEnabled,
- uploadSignatureEnabled: template.templateMeta?.uploadSignatureEnabled,
- drawSignatureEnabled: template.templateMeta?.drawSignatureEnabled,
- },
+ create: derivedDocumentMeta,
},
},
include: {
@@ -589,11 +572,11 @@ export const createDocumentFromDirectTemplate = async ({
});
const [html, text] = await Promise.all([
- renderEmailWithI18N(emailTemplate, { lang: metaLanguage, branding }),
- renderEmailWithI18N(emailTemplate, { lang: metaLanguage, branding, plainText: true }),
+ renderEmailWithI18N(emailTemplate, { lang: emailLanguage, branding }),
+ renderEmailWithI18N(emailTemplate, { lang: emailLanguage, branding, plainText: true }),
]);
- const i18n = await getI18nInstance(metaLanguage);
+ const i18n = await getI18nInstance(emailLanguage);
await mailer.sendMail({
to: [
@@ -602,10 +585,7 @@ export const createDocumentFromDirectTemplate = async ({
address: templateOwner.email,
},
],
- from: {
- name: env('NEXT_PRIVATE_SMTP_FROM_NAME') || 'Documenso',
- address: env('NEXT_PRIVATE_SMTP_FROM_ADDRESS') || 'noreply@documenso.com',
- },
+ from: senderEmail,
subject: i18n._(msg`Document created from direct template`),
html,
text,
diff --git a/packages/lib/server-only/template/create-document-from-template-legacy.ts b/packages/lib/server-only/template/create-document-from-template-legacy.ts
index 5dd98b032..e0a20f55e 100644
--- a/packages/lib/server-only/template/create-document-from-template-legacy.ts
+++ b/packages/lib/server-only/template/create-document-from-template-legacy.ts
@@ -3,6 +3,7 @@ import { DocumentSource, type RecipientRole } from '@prisma/client';
import { nanoid, prefixedId } from '@documenso/lib/universal/id';
import { prisma } from '@documenso/prisma';
+import { extractDerivedDocumentMeta } from '../../utils/document';
import { buildTeamWhereQuery } from '../../utils/teams';
import { getTeamSettings } from '../team/get-team-settings';
@@ -78,18 +79,7 @@ export const createDocumentFromTemplateLegacy = async ({
})),
},
documentMeta: {
- create: {
- subject: template.templateMeta?.subject,
- message: template.templateMeta?.message,
- timezone: template.templateMeta?.timezone,
- dateFormat: template.templateMeta?.dateFormat,
- redirectUrl: template.templateMeta?.redirectUrl,
- signingOrder: template.templateMeta?.signingOrder ?? undefined,
- language: template.templateMeta?.language || settings.documentLanguage,
- typedSignatureEnabled: template.templateMeta?.typedSignatureEnabled,
- uploadSignatureEnabled: template.templateMeta?.uploadSignatureEnabled,
- drawSignatureEnabled: template.templateMeta?.drawSignatureEnabled,
- },
+ create: extractDerivedDocumentMeta(settings, template.templateMeta),
},
},
diff --git a/packages/lib/server-only/template/create-document-from-template.ts b/packages/lib/server-only/template/create-document-from-template.ts
index afe2d5e95..35946a155 100644
--- a/packages/lib/server-only/template/create-document-from-template.ts
+++ b/packages/lib/server-only/template/create-document-from-template.ts
@@ -1,6 +1,5 @@
-import type { DocumentDistributionMethod } from '@prisma/client';
+import type { DocumentDistributionMethod, DocumentSigningOrder } from '@prisma/client';
import {
- DocumentSigningOrder,
DocumentSource,
type Field,
type Recipient,
@@ -40,6 +39,7 @@ import {
mapDocumentToWebhookDocumentPayload,
} from '../../types/webhook-payload';
import type { ApiRequestMetadata } from '../../universal/extract-request-metadata';
+import { extractDerivedDocumentMeta } from '../../utils/document';
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
import {
createDocumentAuthOptions,
@@ -378,7 +378,7 @@ export const createDocumentFromTemplate = async ({
visibility: template.visibility || settings.documentVisibility,
useLegacyFieldInsertion: template.useLegacyFieldInsertion ?? false,
documentMeta: {
- create: {
+ create: extractDerivedDocumentMeta(settings, {
subject: override?.subject || template.templateMeta?.subject,
message: override?.message || template.templateMeta?.message,
timezone: override?.timezone || template.templateMeta?.timezone,
@@ -387,13 +387,8 @@ export const createDocumentFromTemplate = async ({
redirectUrl: override?.redirectUrl || template.templateMeta?.redirectUrl,
distributionMethod:
override?.distributionMethod || template.templateMeta?.distributionMethod,
- // last `undefined` is due to JsonValue's
- emailSettings:
- override?.emailSettings || template.templateMeta?.emailSettings || undefined,
- signingOrder:
- override?.signingOrder ||
- template.templateMeta?.signingOrder ||
- DocumentSigningOrder.PARALLEL,
+ emailSettings: override?.emailSettings || template.templateMeta?.emailSettings,
+ signingOrder: override?.signingOrder || template.templateMeta?.signingOrder,
language:
override?.language || template.templateMeta?.language || settings.documentLanguage,
typedSignatureEnabled:
@@ -403,10 +398,8 @@ export const createDocumentFromTemplate = async ({
drawSignatureEnabled:
override?.drawSignatureEnabled ?? template.templateMeta?.drawSignatureEnabled,
allowDictateNextSigner:
- override?.allowDictateNextSigner ??
- template.templateMeta?.allowDictateNextSigner ??
- false,
- },
+ override?.allowDictateNextSigner ?? template.templateMeta?.allowDictateNextSigner,
+ }),
},
recipients: {
createMany: {
diff --git a/packages/lib/server-only/template/create-template.ts b/packages/lib/server-only/template/create-template.ts
index bd67eebe6..5019407a2 100644
--- a/packages/lib/server-only/template/create-template.ts
+++ b/packages/lib/server-only/template/create-template.ts
@@ -6,6 +6,7 @@ import { TemplateSchema } from '@documenso/prisma/generated/zod/modelSchema//Tem
import { AppError, AppErrorCode } from '../../errors/app-error';
import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth';
+import { extractDerivedDocumentMeta } from '../../utils/document';
import { createDocumentAuthOptions } from '../../utils/document-auth';
import { buildTeamWhereQuery } from '../../utils/teams';
import { getTeamSettings } from '../team/get-team-settings';
@@ -69,6 +70,24 @@ export const createTemplate = async ({
teamId,
});
+ const emailId = meta.emailId;
+
+ // Validate that the email ID belongs to the organisation.
+ if (emailId) {
+ const email = await prisma.organisationEmail.findFirst({
+ where: {
+ id: emailId,
+ organisationId: team.organisationId,
+ },
+ });
+
+ if (!email) {
+ throw new AppError(AppErrorCode.NOT_FOUND, {
+ message: 'Email not found',
+ });
+ }
+ }
+
return await prisma.template.create({
data: {
title,
@@ -86,14 +105,7 @@ export const createTemplate = async ({
publicDescription: data.publicDescription,
type: data.type,
templateMeta: {
- create: {
- ...meta,
- language: meta?.language ?? settings.documentLanguage,
- typedSignatureEnabled: meta?.typedSignatureEnabled ?? settings.typedSignatureEnabled,
- uploadSignatureEnabled: meta?.uploadSignatureEnabled ?? settings.uploadSignatureEnabled,
- drawSignatureEnabled: meta?.drawSignatureEnabled ?? settings.drawSignatureEnabled,
- emailSettings: meta?.emailSettings || undefined,
- },
+ create: extractDerivedDocumentMeta(settings, meta),
},
},
});
diff --git a/packages/lib/server-only/template/update-template.ts b/packages/lib/server-only/template/update-template.ts
index 4485fdc66..65314dd48 100644
--- a/packages/lib/server-only/template/update-template.ts
+++ b/packages/lib/server-only/template/update-template.ts
@@ -41,6 +41,7 @@ export const updateTemplate = async ({
templateMeta: true,
team: {
select: {
+ organisationId: true,
organisation: {
select: {
organisationClaim: true,
@@ -86,6 +87,24 @@ export const updateTemplate = async ({
globalActionAuth: newGlobalActionAuth,
});
+ const emailId = meta.emailId;
+
+ // Validate the emailId belongs to the organisation.
+ if (emailId) {
+ const email = await prisma.organisationEmail.findFirst({
+ where: {
+ id: emailId,
+ organisationId: template.team.organisationId,
+ },
+ });
+
+ if (!email) {
+ throw new AppError(AppErrorCode.NOT_FOUND, {
+ message: 'Email not found',
+ });
+ }
+ }
+
return await prisma.template.update({
where: {
id: templateId,
diff --git a/packages/lib/types/document-audit-logs.ts b/packages/lib/types/document-audit-logs.ts
index 5c3f5bd86..e11f9b31b 100644
--- a/packages/lib/types/document-audit-logs.ts
+++ b/packages/lib/types/document-audit-logs.ts
@@ -58,6 +58,9 @@ export const ZDocumentMetaDiffTypeSchema = z.enum([
'REDIRECT_URL',
'SUBJECT',
'TIMEZONE',
+ 'EMAIL_ID',
+ 'EMAIL_REPLY_TO',
+ 'EMAIL_SETTINGS',
]);
export const ZFieldDiffTypeSchema = z.enum(['DIMENSION', 'POSITION']);
@@ -109,6 +112,9 @@ export const ZDocumentAuditLogDocumentMetaSchema = z.union([
z.literal(DOCUMENT_META_DIFF_TYPE.REDIRECT_URL),
z.literal(DOCUMENT_META_DIFF_TYPE.SUBJECT),
z.literal(DOCUMENT_META_DIFF_TYPE.TIMEZONE),
+ z.literal(DOCUMENT_META_DIFF_TYPE.EMAIL_ID),
+ z.literal(DOCUMENT_META_DIFF_TYPE.EMAIL_REPLY_TO),
+ z.literal(DOCUMENT_META_DIFF_TYPE.EMAIL_SETTINGS),
]),
from: z.string().nullable(),
to: z.string().nullable(),
diff --git a/packages/lib/types/document-email.ts b/packages/lib/types/document-email.ts
index 06b44929d..0d2d2b52e 100644
--- a/packages/lib/types/document-email.ts
+++ b/packages/lib/types/document-email.ts
@@ -54,15 +54,7 @@ export const ZDocumentEmailSettingsSchema = z
.default(true),
})
.strip()
- .catch(() => ({
- recipientSigningRequest: true,
- recipientRemoved: true,
- recipientSigned: true,
- documentPending: true,
- documentCompleted: true,
- documentDeleted: true,
- ownerDocumentCompleted: true,
- }));
+ .catch(() => ({ ...DEFAULT_DOCUMENT_EMAIL_SETTINGS }));
export type TDocumentEmailSettings = z.infer;
@@ -88,3 +80,13 @@ export const extractDerivedDocumentEmailSettings = (
ownerDocumentCompleted: emailSettings.ownerDocumentCompleted,
};
};
+
+export const DEFAULT_DOCUMENT_EMAIL_SETTINGS: TDocumentEmailSettings = {
+ recipientSigningRequest: true,
+ recipientRemoved: true,
+ recipientSigned: true,
+ documentPending: true,
+ documentCompleted: true,
+ documentDeleted: true,
+ ownerDocumentCompleted: true,
+};
diff --git a/packages/lib/types/document.ts b/packages/lib/types/document.ts
index 2b2fe8284..b42801b72 100644
--- a/packages/lib/types/document.ts
+++ b/packages/lib/types/document.ts
@@ -58,6 +58,8 @@ export const ZDocumentSchema = DocumentSchema.pick({
allowDictateNextSigner: true,
language: true,
emailSettings: true,
+ emailId: true,
+ emailReplyTo: true,
}).nullable(),
folder: FolderSchema.pick({
id: true,
diff --git a/packages/lib/types/email-domain.ts b/packages/lib/types/email-domain.ts
new file mode 100644
index 000000000..2dbf79b2a
--- /dev/null
+++ b/packages/lib/types/email-domain.ts
@@ -0,0 +1,40 @@
+import type { z } from 'zod';
+
+import { EmailDomainSchema } from '@documenso/prisma/generated/zod/modelSchema/EmailDomainSchema';
+
+import { ZOrganisationEmailLiteSchema } from './organisation-email';
+
+/**
+ * The full email domain response schema.
+ *
+ * Mainly used for returning a single email domain from the API.
+ */
+export const ZEmailDomainSchema = EmailDomainSchema.pick({
+ id: true,
+ status: true,
+ organisationId: true,
+ domain: true,
+ selector: true,
+ publicKey: true,
+ createdAt: true,
+ updatedAt: true,
+}).extend({
+ emails: ZOrganisationEmailLiteSchema.array(),
+});
+
+export type TEmailDomain = z.infer;
+
+/**
+ * A version of the email domain response schema when returning multiple email domains at once from a single API endpoint.
+ */
+export const ZEmailDomainManySchema = EmailDomainSchema.pick({
+ id: true,
+ status: true,
+ organisationId: true,
+ domain: true,
+ selector: true,
+ createdAt: true,
+ updatedAt: true,
+});
+
+export type TEmailDomainMany = z.infer;
diff --git a/packages/lib/types/organisation-email.ts b/packages/lib/types/organisation-email.ts
new file mode 100644
index 000000000..c72d6a38d
--- /dev/null
+++ b/packages/lib/types/organisation-email.ts
@@ -0,0 +1,42 @@
+import { EmailDomainStatus } from '@prisma/client';
+import { z } from 'zod';
+
+import { OrganisationEmailSchema } from '@documenso/prisma/generated/zod/modelSchema/OrganisationEmailSchema';
+
+export const ZOrganisationEmailSchema = OrganisationEmailSchema.pick({
+ id: true,
+ createdAt: true,
+ updatedAt: true,
+ email: true,
+ emailName: true,
+ // replyTo: true,
+ emailDomainId: true,
+ organisationId: true,
+}).extend({
+ emailDomain: z.object({
+ id: z.string(),
+ status: z.nativeEnum(EmailDomainStatus),
+ }),
+});
+
+export type TOrganisationEmail = z.infer;
+
+/**
+ * A lite version of the organisation email response schema without relations.
+ */
+export const ZOrganisationEmailLiteSchema = OrganisationEmailSchema.pick({
+ id: true,
+ createdAt: true,
+ updatedAt: true,
+ email: true,
+ emailName: true,
+ // replyTo: true,
+ emailDomainId: true,
+ organisationId: true,
+});
+
+export const ZOrganisationEmailManySchema = ZOrganisationEmailLiteSchema.extend({
+ // Put anything extra here.
+});
+
+export type TOrganisationEmailMany = z.infer;
diff --git a/packages/lib/types/subscription.ts b/packages/lib/types/subscription.ts
index 34fc588ba..c01f8f4e8 100644
--- a/packages/lib/types/subscription.ts
+++ b/packages/lib/types/subscription.ts
@@ -19,6 +19,8 @@ export const ZClaimFlagsSchema = z.object({
unlimitedDocuments: z.boolean().optional(),
+ emailDomains: z.boolean().optional(),
+
embedAuthoring: z.boolean().optional(),
embedAuthoringWhiteLabel: z.boolean().optional(),
@@ -50,6 +52,10 @@ export const SUBSCRIPTION_CLAIM_FEATURE_FLAGS: Record<
key: 'hidePoweredBy',
label: 'Hide Documenso branding by',
},
+ emailDomains: {
+ key: 'emailDomains',
+ label: 'Email domains',
+ },
embedAuthoring: {
key: 'embedAuthoring',
label: 'Embed authoring',
@@ -128,6 +134,7 @@ export const internalClaims: InternalClaims = {
unlimitedDocuments: true,
allowCustomBranding: true,
hidePoweredBy: true,
+ emailDomains: true,
embedAuthoring: false,
embedAuthoringWhiteLabel: true,
embedSigning: false,
@@ -144,6 +151,7 @@ export const internalClaims: InternalClaims = {
unlimitedDocuments: true,
allowCustomBranding: true,
hidePoweredBy: true,
+ emailDomains: true,
embedAuthoring: true,
embedAuthoringWhiteLabel: true,
embedSigning: true,
diff --git a/packages/lib/types/template.ts b/packages/lib/types/template.ts
index f5f5dd6f0..1171ad18d 100644
--- a/packages/lib/types/template.ts
+++ b/packages/lib/types/template.ts
@@ -55,6 +55,8 @@ export const ZTemplateSchema = TemplateSchema.pick({
redirectUrl: true,
language: true,
emailSettings: true,
+ emailId: true,
+ emailReplyTo: true,
}).nullable(),
directLink: TemplateDirectLinkSchema.nullable(),
user: UserSchema.pick({
diff --git a/packages/lib/universal/id.ts b/packages/lib/universal/id.ts
index dbe159fbe..a6fb55ad5 100644
--- a/packages/lib/universal/id.ts
+++ b/packages/lib/universal/id.ts
@@ -11,7 +11,9 @@ export const prefixedId = (prefix: string, length = 16) => {
};
type DatabaseIdPrefix =
+ | 'email_domain'
| 'org'
+ | 'org_email'
| 'org_claim'
| 'org_group'
| 'org_setting'
diff --git a/packages/lib/utils/document-audit-logs.ts b/packages/lib/utils/document-audit-logs.ts
index 58797510f..7617439e6 100644
--- a/packages/lib/utils/document-audit-logs.ts
+++ b/packages/lib/utils/document-audit-logs.ts
@@ -205,12 +205,18 @@ export const diffDocumentMetaChanges = (
const oldTimezone = oldData?.timezone ?? '';
const oldPassword = oldData?.password ?? null;
const oldRedirectUrl = oldData?.redirectUrl ?? '';
+ const oldEmailId = oldData?.emailId || null;
+ const oldEmailReplyTo = oldData?.emailReplyTo || null;
+ const oldEmailSettings = oldData?.emailSettings || null;
const newDateFormat = newData?.dateFormat ?? '';
const newMessage = newData?.message ?? '';
const newSubject = newData?.subject ?? '';
const newTimezone = newData?.timezone ?? '';
const newRedirectUrl = newData?.redirectUrl ?? '';
+ const newEmailId = newData?.emailId || null;
+ const newEmailReplyTo = newData?.emailReplyTo || null;
+ const newEmailSettings = newData?.emailSettings || null;
if (oldDateFormat !== newDateFormat) {
diffs.push({
@@ -258,6 +264,30 @@ export const diffDocumentMetaChanges = (
});
}
+ if (oldEmailId !== newEmailId) {
+ diffs.push({
+ type: DOCUMENT_META_DIFF_TYPE.EMAIL_ID,
+ from: oldEmailId,
+ to: newEmailId,
+ });
+ }
+
+ if (oldEmailReplyTo !== newEmailReplyTo) {
+ diffs.push({
+ type: DOCUMENT_META_DIFF_TYPE.EMAIL_REPLY_TO,
+ from: oldEmailReplyTo,
+ to: newEmailReplyTo,
+ });
+ }
+
+ if (!isDeepEqual(oldEmailSettings, newEmailSettings)) {
+ diffs.push({
+ type: DOCUMENT_META_DIFF_TYPE.EMAIL_SETTINGS,
+ from: JSON.stringify(oldEmailSettings),
+ to: JSON.stringify(newEmailSettings),
+ });
+ }
+
return diffs;
};
diff --git a/packages/lib/utils/document.ts b/packages/lib/utils/document.ts
index 52ceca627..8bae70ca2 100644
--- a/packages/lib/utils/document.ts
+++ b/packages/lib/utils/document.ts
@@ -1,8 +1,62 @@
-import type { Document } from '@prisma/client';
-import { DocumentStatus } from '@prisma/client';
+import type {
+ Document,
+ DocumentMeta,
+ OrganisationGlobalSettings,
+ TemplateMeta,
+} from '@prisma/client';
+import { DocumentDistributionMethod, DocumentSigningOrder, DocumentStatus } from '@prisma/client';
+
+import { DEFAULT_DOCUMENT_TIME_ZONE } from '../constants/time-zones';
+import { DEFAULT_DOCUMENT_EMAIL_SETTINGS } from '../types/document-email';
export const isDocumentCompleted = (document: Pick | DocumentStatus) => {
const status = typeof document === 'string' ? document : document.status;
return status === DocumentStatus.COMPLETED || status === DocumentStatus.REJECTED;
};
+
+/**
+ * Extracts the derived document meta which should be used when creating a document
+ * from scratch, or from a template.
+ *
+ * Uses the following, the lower number overrides the higher number:
+ * 1. Merged organisation/team settings
+ * 2. Meta overrides
+ *
+ * @param settings - The merged organisation/team settings.
+ * @param overrideMeta - The meta to override the settings with.
+ * @returns The derived document meta.
+ */
+export const extractDerivedDocumentMeta = (
+ settings: Omit,
+ overrideMeta: Partial | undefined | null,
+) => {
+ const meta = overrideMeta ?? {};
+
+ // Note: If you update this you will also need to update `create-document-from-template.ts`
+ // since there is custom work there which allows 3 overrides.
+ return {
+ language: meta.language || settings.documentLanguage,
+ timezone: meta.timezone || settings.documentTimezone || DEFAULT_DOCUMENT_TIME_ZONE,
+ dateFormat: meta.dateFormat || settings.documentDateFormat,
+ message: meta.message || null,
+ subject: meta.subject || null,
+ password: meta.password || null,
+ redirectUrl: meta.redirectUrl || null,
+
+ signingOrder: meta.signingOrder || DocumentSigningOrder.PARALLEL,
+ allowDictateNextSigner: meta.allowDictateNextSigner ?? false,
+ distributionMethod: meta.distributionMethod || DocumentDistributionMethod.EMAIL, // Todo: Make this a setting.
+
+ // Signature settings.
+ typedSignatureEnabled: meta.typedSignatureEnabled ?? settings.typedSignatureEnabled,
+ uploadSignatureEnabled: meta.uploadSignatureEnabled ?? settings.uploadSignatureEnabled,
+ drawSignatureEnabled: meta.drawSignatureEnabled ?? settings.drawSignatureEnabled,
+
+ // Email settings.
+ emailId: meta.emailId ?? settings.emailId,
+ emailReplyTo: meta.emailReplyTo ?? settings.emailReplyTo,
+ emailSettings:
+ meta.emailSettings || settings.emailDocumentSettings || DEFAULT_DOCUMENT_EMAIL_SETTINGS,
+ } satisfies Omit;
+};
diff --git a/packages/lib/utils/email-domains.ts b/packages/lib/utils/email-domains.ts
new file mode 100644
index 000000000..cbb517ef0
--- /dev/null
+++ b/packages/lib/utils/email-domains.ts
@@ -0,0 +1,17 @@
+export const generateDkimRecord = (recordName: string, publicKeyFlattened: string) => {
+ return {
+ name: recordName,
+ value: `v=DKIM1; k=rsa; p=${publicKeyFlattened}`,
+ type: 'TXT',
+ };
+};
+
+export const AWS_SES_SPF_RECORD = {
+ name: `@`,
+ value: 'v=spf1 include:amazonses.com -all',
+ type: 'TXT',
+};
+
+export const generateEmailDomainRecords = (recordName: string, publicKeyFlattened: string) => {
+ return [generateDkimRecord(recordName, publicKeyFlattened), AWS_SES_SPF_RECORD];
+};
diff --git a/packages/lib/utils/organisations.ts b/packages/lib/utils/organisations.ts
index 539db071b..f8a41c5f8 100644
--- a/packages/lib/utils/organisations.ts
+++ b/packages/lib/utils/organisations.ts
@@ -7,11 +7,13 @@ import {
import type { ORGANISATION_MEMBER_ROLE_MAP } from '@documenso/lib/constants/organisations-translations';
+import { DEFAULT_DOCUMENT_DATE_FORMAT } from '../constants/date-formats';
import {
LOWEST_ORGANISATION_ROLE,
ORGANISATION_MEMBER_ROLE_HIERARCHY,
ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP,
} from '../constants/organisations';
+import { DEFAULT_DOCUMENT_EMAIL_SETTINGS } from '../types/document-email';
export const isPersonalLayout = (organisations: Pick[]) => {
return organisations.length === 1 && organisations[0].type === 'PERSONAL';
@@ -113,6 +115,9 @@ export const generateDefaultOrganisationSettings = (): Omit<
return {
documentVisibility: DocumentVisibility.EVERYONE,
documentLanguage: 'en',
+ documentTimezone: null, // Null means local timezone.
+ documentDateFormat: DEFAULT_DOCUMENT_DATE_FORMAT,
+
includeSenderDetails: true,
includeSigningCertificate: true,
@@ -124,5 +129,10 @@ export const generateDefaultOrganisationSettings = (): Omit<
brandingLogo: '',
brandingUrl: '',
brandingCompanyDetails: '',
+
+ emailId: null,
+ emailReplyTo: null,
+ // emailReplyToName: null,
+ emailDocumentSettings: DEFAULT_DOCUMENT_EMAIL_SETTINGS,
};
};
diff --git a/packages/lib/utils/teams.ts b/packages/lib/utils/teams.ts
index 9e72c462c..ecc8006fe 100644
--- a/packages/lib/utils/teams.ts
+++ b/packages/lib/utils/teams.ts
@@ -165,6 +165,9 @@ export const generateDefaultTeamSettings = (): Omit {
+ const { organisationId, domain } = input;
+ const { user } = ctx;
+
+ ctx.logger.info({
+ input: {
+ organisationId,
+ domain,
+ },
+ });
+
+ if (!IS_BILLING_ENABLED()) {
+ throw new AppError(AppErrorCode.INVALID_REQUEST, {
+ message: 'Billing is not enabled',
+ });
+ }
+
+ const organisation = await prisma.organisation.findFirst({
+ where: buildOrganisationWhereQuery({
+ organisationId,
+ userId: user.id,
+ roles: ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
+ }),
+ include: {
+ emailDomains: true,
+ organisationClaim: true,
+ },
+ });
+
+ if (!organisation) {
+ throw new AppError(AppErrorCode.UNAUTHORIZED);
+ }
+
+ if (!organisation.organisationClaim.flags.emailDomains) {
+ throw new AppError(AppErrorCode.INVALID_BODY, {
+ message: 'Email domains are not enabled for this organisation',
+ });
+ }
+
+ if (organisation.emailDomains.length >= 100) {
+ throw new AppError(AppErrorCode.INVALID_BODY, {
+ message: 'You have reached the maximum number of email domains',
+ });
+ }
+
+ return await createEmailDomain({
+ domain,
+ organisationId,
+ });
+ });
diff --git a/packages/trpc/server/enterprise-router/create-organisation-email-domain.types.ts b/packages/trpc/server/enterprise-router/create-organisation-email-domain.types.ts
new file mode 100644
index 000000000..3dcb3fc3b
--- /dev/null
+++ b/packages/trpc/server/enterprise-router/create-organisation-email-domain.types.ts
@@ -0,0 +1,27 @@
+import { z } from 'zod';
+
+import { ZEmailDomainSchema } from '@documenso/lib/types/email-domain';
+
+const domainRegex =
+ /^(?!https?:\/\/)(?!www\.)([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
+
+export const ZDomainSchema = z
+ .string()
+ .regex(domainRegex, { message: 'Invalid domain name' })
+ .toLowerCase();
+
+export const ZCreateOrganisationEmailDomainRequestSchema = z.object({
+ organisationId: z.string(),
+ domain: ZDomainSchema,
+});
+
+export const ZCreateOrganisationEmailDomainResponseSchema = z.object({
+ emailDomain: ZEmailDomainSchema,
+ records: z.array(
+ z.object({
+ name: z.string(),
+ value: z.string(),
+ type: z.string(),
+ }),
+ ),
+});
diff --git a/packages/trpc/server/enterprise-router/create-organisation-email.ts b/packages/trpc/server/enterprise-router/create-organisation-email.ts
new file mode 100644
index 000000000..6a047da37
--- /dev/null
+++ b/packages/trpc/server/enterprise-router/create-organisation-email.ts
@@ -0,0 +1,61 @@
+import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
+import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
+import { generateDatabaseId } from '@documenso/lib/universal/id';
+import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
+import { prisma } from '@documenso/prisma';
+
+import { authenticatedProcedure } from '../trpc';
+import {
+ ZCreateOrganisationEmailRequestSchema,
+ ZCreateOrganisationEmailResponseSchema,
+} from './create-organisation-email.types';
+
+export const createOrganisationEmailRoute = authenticatedProcedure
+ .input(ZCreateOrganisationEmailRequestSchema)
+ .output(ZCreateOrganisationEmailResponseSchema)
+ .mutation(async ({ input, ctx }) => {
+ const { email, emailName, emailDomainId } = input;
+ const { user } = ctx;
+
+ ctx.logger.info({
+ input: {
+ emailDomainId,
+ },
+ });
+
+ const emailDomain = await prisma.emailDomain.findFirst({
+ where: {
+ id: emailDomainId,
+ organisation: buildOrganisationWhereQuery({
+ organisationId: undefined,
+ userId: user.id,
+ roles: ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
+ }),
+ },
+ });
+
+ if (!emailDomain) {
+ throw new AppError(AppErrorCode.NOT_FOUND, {
+ message: 'Email domain not found',
+ });
+ }
+
+ const allowedEmailSuffix = '@' + emailDomain.domain;
+
+ if (!email.endsWith(allowedEmailSuffix)) {
+ throw new AppError(AppErrorCode.INVALID_BODY, {
+ message: 'Cannot create an email with a different domain',
+ });
+ }
+
+ await prisma.organisationEmail.create({
+ data: {
+ id: generateDatabaseId('org_email'),
+ organisationId: emailDomain.organisationId,
+ emailName,
+ // replyTo,
+ email,
+ emailDomainId,
+ },
+ });
+ });
diff --git a/packages/trpc/server/enterprise-router/create-organisation-email.types.ts b/packages/trpc/server/enterprise-router/create-organisation-email.types.ts
new file mode 100644
index 000000000..3de340790
--- /dev/null
+++ b/packages/trpc/server/enterprise-router/create-organisation-email.types.ts
@@ -0,0 +1,12 @@
+import { z } from 'zod';
+
+export const ZCreateOrganisationEmailRequestSchema = z.object({
+ emailDomainId: z.string(),
+ emailName: z.string().min(1).max(100),
+ email: z.string().email().toLowerCase(),
+
+ // This does not need to be validated to be part of the domain.
+ // replyTo: z.string().email().optional(),
+});
+
+export const ZCreateOrganisationEmailResponseSchema = z.void();
diff --git a/packages/trpc/server/billing/create-subscription.ts b/packages/trpc/server/enterprise-router/create-subscription.ts
similarity index 100%
rename from packages/trpc/server/billing/create-subscription.ts
rename to packages/trpc/server/enterprise-router/create-subscription.ts
diff --git a/packages/trpc/server/billing/create-subscription.types.ts b/packages/trpc/server/enterprise-router/create-subscription.types.ts
similarity index 100%
rename from packages/trpc/server/billing/create-subscription.types.ts
rename to packages/trpc/server/enterprise-router/create-subscription.types.ts
diff --git a/packages/trpc/server/enterprise-router/delete-organisation-email-domain.ts b/packages/trpc/server/enterprise-router/delete-organisation-email-domain.ts
new file mode 100644
index 000000000..1edadfbd7
--- /dev/null
+++ b/packages/trpc/server/enterprise-router/delete-organisation-email-domain.ts
@@ -0,0 +1,53 @@
+import { deleteEmailDomain } from '@documenso/ee/server-only/lib/delete-email-domain';
+import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
+import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
+import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
+import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
+import { prisma } from '@documenso/prisma';
+
+import { authenticatedProcedure } from '../trpc';
+import {
+ ZDeleteOrganisationEmailDomainRequestSchema,
+ ZDeleteOrganisationEmailDomainResponseSchema,
+} from './delete-organisation-email-domain.types';
+
+export const deleteOrganisationEmailDomainRoute = authenticatedProcedure
+ .input(ZDeleteOrganisationEmailDomainRequestSchema)
+ .output(ZDeleteOrganisationEmailDomainResponseSchema)
+ .mutation(async ({ input, ctx }) => {
+ const { emailDomainId } = input;
+ const { user } = ctx;
+
+ ctx.logger.info({
+ input: {
+ emailDomainId,
+ },
+ });
+
+ if (!IS_BILLING_ENABLED()) {
+ throw new AppError(AppErrorCode.INVALID_REQUEST, {
+ message: 'Billing is not enabled',
+ });
+ }
+
+ const emailDomain = await prisma.emailDomain.findFirst({
+ where: {
+ id: emailDomainId,
+ organisation: buildOrganisationWhereQuery({
+ organisationId: undefined,
+ userId: user.id,
+ roles: ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
+ }),
+ },
+ });
+
+ if (!emailDomain) {
+ throw new AppError(AppErrorCode.NOT_FOUND, {
+ message: 'Email domain not found',
+ });
+ }
+
+ await deleteEmailDomain({
+ emailDomainId: emailDomain.id,
+ });
+ });
diff --git a/packages/trpc/server/enterprise-router/delete-organisation-email-domain.types.ts b/packages/trpc/server/enterprise-router/delete-organisation-email-domain.types.ts
new file mode 100644
index 000000000..2ba8802da
--- /dev/null
+++ b/packages/trpc/server/enterprise-router/delete-organisation-email-domain.types.ts
@@ -0,0 +1,7 @@
+import { z } from 'zod';
+
+export const ZDeleteOrganisationEmailDomainRequestSchema = z.object({
+ emailDomainId: z.string(),
+});
+
+export const ZDeleteOrganisationEmailDomainResponseSchema = z.void();
diff --git a/packages/trpc/server/enterprise-router/delete-organisation-email.ts b/packages/trpc/server/enterprise-router/delete-organisation-email.ts
new file mode 100644
index 000000000..770649d71
--- /dev/null
+++ b/packages/trpc/server/enterprise-router/delete-organisation-email.ts
@@ -0,0 +1,45 @@
+import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
+import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
+import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
+import { prisma } from '@documenso/prisma';
+
+import { authenticatedProcedure } from '../trpc';
+import {
+ ZDeleteOrganisationEmailRequestSchema,
+ ZDeleteOrganisationEmailResponseSchema,
+} from './delete-organisation-email.types';
+
+export const deleteOrganisationEmailRoute = authenticatedProcedure
+ .input(ZDeleteOrganisationEmailRequestSchema)
+ .output(ZDeleteOrganisationEmailResponseSchema)
+ .mutation(async ({ input, ctx }) => {
+ const { emailId } = input;
+ const { user } = ctx;
+
+ ctx.logger.info({
+ input: {
+ emailId,
+ },
+ });
+
+ const email = await prisma.organisationEmail.findFirst({
+ where: {
+ id: emailId,
+ organisation: buildOrganisationWhereQuery({
+ organisationId: undefined,
+ userId: user.id,
+ roles: ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
+ }),
+ },
+ });
+
+ if (!email) {
+ throw new AppError(AppErrorCode.UNAUTHORIZED);
+ }
+
+ await prisma.organisationEmail.delete({
+ where: {
+ id: email.id,
+ },
+ });
+ });
diff --git a/packages/trpc/server/enterprise-router/delete-organisation-email.types.ts b/packages/trpc/server/enterprise-router/delete-organisation-email.types.ts
new file mode 100644
index 000000000..116d60d4b
--- /dev/null
+++ b/packages/trpc/server/enterprise-router/delete-organisation-email.types.ts
@@ -0,0 +1,7 @@
+import { z } from 'zod';
+
+export const ZDeleteOrganisationEmailRequestSchema = z.object({
+ emailId: z.string(),
+});
+
+export const ZDeleteOrganisationEmailResponseSchema = z.void();
diff --git a/packages/trpc/server/enterprise-router/find-organisation-email-domain.ts b/packages/trpc/server/enterprise-router/find-organisation-email-domain.ts
new file mode 100644
index 000000000..6901efb76
--- /dev/null
+++ b/packages/trpc/server/enterprise-router/find-organisation-email-domain.ts
@@ -0,0 +1,122 @@
+import type { EmailDomainStatus } from '@prisma/client';
+import { Prisma } from '@prisma/client';
+
+import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
+import type { FindResultResponse } from '@documenso/lib/types/search-params';
+import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
+import { prisma } from '@documenso/prisma';
+
+import { authenticatedProcedure } from '../trpc';
+import {
+ ZFindOrganisationEmailDomainsRequestSchema,
+ ZFindOrganisationEmailDomainsResponseSchema,
+} from './find-organisation-email-domain.types';
+
+export const findOrganisationEmailDomainsRoute = authenticatedProcedure
+ .input(ZFindOrganisationEmailDomainsRequestSchema)
+ .output(ZFindOrganisationEmailDomainsResponseSchema)
+ .query(async ({ input, ctx }) => {
+ const { organisationId, emailDomainId, statuses, query, page, perPage } = input;
+ const { user } = ctx;
+
+ ctx.logger.info({
+ input: {
+ organisationId,
+ },
+ });
+
+ return await findOrganisationEmailDomains({
+ userId: user.id,
+ organisationId,
+ emailDomainId,
+ statuses,
+ query,
+ page,
+ perPage,
+ });
+ });
+
+type FindOrganisationEmailDomainsOptions = {
+ userId: number;
+ organisationId: string;
+ emailDomainId?: string;
+ statuses?: EmailDomainStatus[];
+ query?: string;
+ page?: number;
+ perPage?: number;
+};
+
+export const findOrganisationEmailDomains = async ({
+ userId,
+ organisationId,
+ emailDomainId,
+ statuses = [],
+ query,
+ page = 1,
+ perPage = 100,
+}: FindOrganisationEmailDomainsOptions) => {
+ const organisation = await prisma.organisation.findFirst({
+ where: buildOrganisationWhereQuery({ organisationId, userId }),
+ });
+
+ if (!organisation) {
+ throw new AppError(AppErrorCode.NOT_FOUND);
+ }
+
+ const whereClause: Prisma.EmailDomainWhereInput = {
+ organisationId: organisation.id,
+ status: statuses.length > 0 ? { in: statuses } : undefined,
+ };
+
+ if (emailDomainId) {
+ whereClause.id = emailDomainId;
+ }
+
+ if (query) {
+ whereClause.domain = {
+ contains: query,
+ mode: Prisma.QueryMode.insensitive,
+ };
+ }
+
+ const [data, count] = await Promise.all([
+ prisma.emailDomain.findMany({
+ where: whereClause,
+ skip: Math.max(page - 1, 0) * perPage,
+ take: perPage,
+ orderBy: {
+ createdAt: 'desc',
+ },
+ select: {
+ id: true,
+ status: true,
+ organisationId: true,
+ domain: true,
+ selector: true,
+ createdAt: true,
+ updatedAt: true,
+ _count: {
+ select: {
+ emails: true,
+ },
+ },
+ },
+ }),
+ prisma.emailDomain.count({
+ where: whereClause,
+ }),
+ ]);
+
+ const mappedData = data.map((item) => ({
+ ...item,
+ emailCount: item._count.emails,
+ }));
+
+ return {
+ data: mappedData,
+ count,
+ currentPage: page,
+ perPage,
+ totalPages: Math.ceil(count / perPage),
+ } satisfies FindResultResponse;
+};
diff --git a/packages/trpc/server/enterprise-router/find-organisation-email-domain.types.ts b/packages/trpc/server/enterprise-router/find-organisation-email-domain.types.ts
new file mode 100644
index 000000000..1aa4bde0c
--- /dev/null
+++ b/packages/trpc/server/enterprise-router/find-organisation-email-domain.types.ts
@@ -0,0 +1,23 @@
+import { EmailDomainStatus } from '@prisma/client';
+import { z } from 'zod';
+
+import { ZEmailDomainManySchema } from '@documenso/lib/types/email-domain';
+import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
+
+export const ZFindOrganisationEmailDomainsRequestSchema = ZFindSearchParamsSchema.extend({
+ organisationId: z.string(),
+ emailDomainId: z.string().optional(),
+ statuses: z.nativeEnum(EmailDomainStatus).array().optional(),
+});
+
+export const ZFindOrganisationEmailDomainsResponseSchema = ZFindResultResponse.extend({
+ data: z.array(
+ ZEmailDomainManySchema.extend({
+ emailCount: z.number(),
+ }),
+ ),
+});
+
+export type TFindOrganisationEmailDomainsResponse = z.infer<
+ typeof ZFindOrganisationEmailDomainsResponseSchema
+>;
diff --git a/packages/trpc/server/enterprise-router/find-organisation-emails.ts b/packages/trpc/server/enterprise-router/find-organisation-emails.ts
new file mode 100644
index 000000000..9f0f759ac
--- /dev/null
+++ b/packages/trpc/server/enterprise-router/find-organisation-emails.ts
@@ -0,0 +1,105 @@
+import { Prisma } from '@prisma/client';
+
+import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
+import type { FindResultResponse } from '@documenso/lib/types/search-params';
+import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
+import { prisma } from '@documenso/prisma';
+
+import { authenticatedProcedure } from '../trpc';
+import {
+ ZFindOrganisationEmailsRequestSchema,
+ ZFindOrganisationEmailsResponseSchema,
+} from './find-organisation-emails.types';
+
+export const findOrganisationEmailsRoute = authenticatedProcedure
+ .input(ZFindOrganisationEmailsRequestSchema)
+ .output(ZFindOrganisationEmailsResponseSchema)
+ .query(async ({ input, ctx }) => {
+ const { organisationId, emailDomainId, query, page, perPage } = input;
+ const { user } = ctx;
+
+ ctx.logger.info({
+ input: {
+ organisationId,
+ },
+ });
+
+ return await findOrganisationEmails({
+ userId: user.id,
+ organisationId,
+ emailDomainId,
+ query,
+ page,
+ perPage,
+ });
+ });
+
+type FindOrganisationEmailsOptions = {
+ userId: number;
+ organisationId: string;
+ emailDomainId?: string;
+ query?: string;
+ page?: number;
+ perPage?: number;
+};
+
+export const findOrganisationEmails = async ({
+ userId,
+ organisationId,
+ emailDomainId,
+ query,
+ page = 1,
+ perPage = 100,
+}: FindOrganisationEmailsOptions) => {
+ const organisation = await prisma.organisation.findFirst({
+ where: buildOrganisationWhereQuery({ organisationId, userId }),
+ });
+
+ if (!organisation) {
+ throw new AppError(AppErrorCode.NOT_FOUND);
+ }
+
+ const whereClause: Prisma.OrganisationEmailWhereInput = {
+ organisationId: organisation.id,
+ emailDomainId,
+ };
+
+ if (query) {
+ whereClause.email = {
+ contains: query,
+ mode: Prisma.QueryMode.insensitive,
+ };
+ }
+
+ const [data, count] = await Promise.all([
+ prisma.organisationEmail.findMany({
+ where: whereClause,
+ skip: Math.max(page - 1, 0) * perPage,
+ take: perPage,
+ orderBy: {
+ createdAt: 'desc',
+ },
+ select: {
+ id: true,
+ createdAt: true,
+ updatedAt: true,
+ email: true,
+ emailName: true,
+ // replyTo: true,
+ emailDomainId: true,
+ organisationId: true,
+ },
+ }),
+ prisma.organisationEmail.count({
+ where: whereClause,
+ }),
+ ]);
+
+ return {
+ data,
+ count,
+ currentPage: page,
+ perPage,
+ totalPages: Math.ceil(count / perPage),
+ } satisfies FindResultResponse;
+};
diff --git a/packages/trpc/server/enterprise-router/find-organisation-emails.types.ts b/packages/trpc/server/enterprise-router/find-organisation-emails.types.ts
new file mode 100644
index 000000000..2e0d25e4e
--- /dev/null
+++ b/packages/trpc/server/enterprise-router/find-organisation-emails.types.ts
@@ -0,0 +1,15 @@
+import { z } from 'zod';
+
+import { ZOrganisationEmailManySchema } from '@documenso/lib/types/organisation-email';
+import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
+
+export const ZFindOrganisationEmailsRequestSchema = ZFindSearchParamsSchema.extend({
+ organisationId: z.string(),
+ emailDomainId: z.string().optional(),
+});
+
+export const ZFindOrganisationEmailsResponseSchema = ZFindResultResponse.extend({
+ data: ZOrganisationEmailManySchema.array(),
+});
+
+export type TFindOrganisationEmailsResponse = z.infer;
diff --git a/packages/trpc/server/billing/get-invoices.ts b/packages/trpc/server/enterprise-router/get-invoices.ts
similarity index 100%
rename from packages/trpc/server/billing/get-invoices.ts
rename to packages/trpc/server/enterprise-router/get-invoices.ts
diff --git a/packages/trpc/server/billing/get-invoices.types.ts b/packages/trpc/server/enterprise-router/get-invoices.types.ts
similarity index 100%
rename from packages/trpc/server/billing/get-invoices.types.ts
rename to packages/trpc/server/enterprise-router/get-invoices.types.ts
diff --git a/packages/trpc/server/enterprise-router/get-organisation-email-domain.ts b/packages/trpc/server/enterprise-router/get-organisation-email-domain.ts
new file mode 100644
index 000000000..c2834d2a3
--- /dev/null
+++ b/packages/trpc/server/enterprise-router/get-organisation-email-domain.ts
@@ -0,0 +1,63 @@
+import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
+import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
+import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
+import { prisma } from '@documenso/prisma';
+
+import { authenticatedProcedure } from '../trpc';
+import {
+ ZGetOrganisationEmailDomainRequestSchema,
+ ZGetOrganisationEmailDomainResponseSchema,
+} from './get-organisation-email-domain.types';
+
+export const getOrganisationEmailDomainRoute = authenticatedProcedure
+ .input(ZGetOrganisationEmailDomainRequestSchema)
+ .output(ZGetOrganisationEmailDomainResponseSchema)
+ .query(async ({ input, ctx }) => {
+ const { emailDomainId } = input;
+
+ ctx.logger.info({
+ input: {
+ emailDomainId,
+ },
+ });
+
+ return await getOrganisationEmailDomain({
+ userId: ctx.user.id,
+ emailDomainId,
+ });
+ });
+
+type GetOrganisationEmailDomainOptions = {
+ userId: number;
+ emailDomainId: string;
+};
+
+export const getOrganisationEmailDomain = async ({
+ userId,
+ emailDomainId,
+}: GetOrganisationEmailDomainOptions) => {
+ const emailDomain = await prisma.emailDomain.findFirst({
+ where: {
+ id: emailDomainId,
+ organisation: buildOrganisationWhereQuery({
+ organisationId: undefined,
+ userId,
+ roles: ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
+ }),
+ },
+ omit: {
+ privateKey: true,
+ },
+ include: {
+ emails: true,
+ },
+ });
+
+ if (!emailDomain) {
+ throw new AppError(AppErrorCode.NOT_FOUND, {
+ message: 'Email domain not found',
+ });
+ }
+
+ return emailDomain;
+};
diff --git a/packages/trpc/server/enterprise-router/get-organisation-email-domain.types.ts b/packages/trpc/server/enterprise-router/get-organisation-email-domain.types.ts
new file mode 100644
index 000000000..4fc9afaf7
--- /dev/null
+++ b/packages/trpc/server/enterprise-router/get-organisation-email-domain.types.ts
@@ -0,0 +1,13 @@
+import { z } from 'zod';
+
+import { ZEmailDomainSchema } from '@documenso/lib/types/email-domain';
+
+export const ZGetOrganisationEmailDomainRequestSchema = z.object({
+ emailDomainId: z.string(),
+});
+
+export const ZGetOrganisationEmailDomainResponseSchema = ZEmailDomainSchema;
+
+export type TGetOrganisationEmailDomainResponse = z.infer<
+ typeof ZGetOrganisationEmailDomainResponseSchema
+>;
diff --git a/packages/trpc/server/billing/get-plans.ts b/packages/trpc/server/enterprise-router/get-plans.ts
similarity index 100%
rename from packages/trpc/server/billing/get-plans.ts
rename to packages/trpc/server/enterprise-router/get-plans.ts
diff --git a/packages/trpc/server/billing/get-subscription.ts b/packages/trpc/server/enterprise-router/get-subscription.ts
similarity index 100%
rename from packages/trpc/server/billing/get-subscription.ts
rename to packages/trpc/server/enterprise-router/get-subscription.ts
diff --git a/packages/trpc/server/billing/get-subscription.types.ts b/packages/trpc/server/enterprise-router/get-subscription.types.ts
similarity index 100%
rename from packages/trpc/server/billing/get-subscription.types.ts
rename to packages/trpc/server/enterprise-router/get-subscription.types.ts
diff --git a/packages/trpc/server/billing/manage-subscription.ts b/packages/trpc/server/enterprise-router/manage-subscription.ts
similarity index 100%
rename from packages/trpc/server/billing/manage-subscription.ts
rename to packages/trpc/server/enterprise-router/manage-subscription.ts
diff --git a/packages/trpc/server/billing/manage-subscription.types.ts b/packages/trpc/server/enterprise-router/manage-subscription.types.ts
similarity index 100%
rename from packages/trpc/server/billing/manage-subscription.types.ts
rename to packages/trpc/server/enterprise-router/manage-subscription.types.ts
diff --git a/packages/trpc/server/enterprise-router/router.ts b/packages/trpc/server/enterprise-router/router.ts
new file mode 100644
index 000000000..3fcac5708
--- /dev/null
+++ b/packages/trpc/server/enterprise-router/router.ts
@@ -0,0 +1,46 @@
+import { router } from '../trpc';
+import { createOrganisationEmailRoute } from './create-organisation-email';
+import { createOrganisationEmailDomainRoute } from './create-organisation-email-domain';
+import { createSubscriptionRoute } from './create-subscription';
+import { deleteOrganisationEmailRoute } from './delete-organisation-email';
+import { deleteOrganisationEmailDomainRoute } from './delete-organisation-email-domain';
+import { findOrganisationEmailDomainsRoute } from './find-organisation-email-domain';
+import { findOrganisationEmailsRoute } from './find-organisation-emails';
+import { getInvoicesRoute } from './get-invoices';
+import { getOrganisationEmailDomainRoute } from './get-organisation-email-domain';
+import { getPlansRoute } from './get-plans';
+import { getSubscriptionRoute } from './get-subscription';
+import { manageSubscriptionRoute } from './manage-subscription';
+import { updateOrganisationEmailRoute } from './update-organisation-email';
+import { verifyOrganisationEmailDomainRoute } from './verify-organisation-email-domain';
+
+export const enterpriseRouter = router({
+ organisation: {
+ email: {
+ find: findOrganisationEmailsRoute,
+ create: createOrganisationEmailRoute,
+ update: updateOrganisationEmailRoute,
+ delete: deleteOrganisationEmailRoute,
+ },
+ emailDomain: {
+ get: getOrganisationEmailDomainRoute,
+ find: findOrganisationEmailDomainsRoute,
+ create: createOrganisationEmailDomainRoute,
+ delete: deleteOrganisationEmailDomainRoute,
+ verify: verifyOrganisationEmailDomainRoute,
+ },
+ },
+ billing: {
+ plans: {
+ get: getPlansRoute,
+ },
+ subscription: {
+ get: getSubscriptionRoute,
+ create: createSubscriptionRoute,
+ manage: manageSubscriptionRoute,
+ },
+ invoices: {
+ get: getInvoicesRoute,
+ },
+ },
+});
diff --git a/packages/trpc/server/enterprise-router/update-organisation-email.ts b/packages/trpc/server/enterprise-router/update-organisation-email.ts
new file mode 100644
index 000000000..59ca52435
--- /dev/null
+++ b/packages/trpc/server/enterprise-router/update-organisation-email.ts
@@ -0,0 +1,49 @@
+import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
+import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
+import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
+import { prisma } from '@documenso/prisma';
+
+import { authenticatedProcedure } from '../trpc';
+import {
+ ZUpdateOrganisationEmailRequestSchema,
+ ZUpdateOrganisationEmailResponseSchema,
+} from './update-organisation-email.types';
+
+export const updateOrganisationEmailRoute = authenticatedProcedure
+ .input(ZUpdateOrganisationEmailRequestSchema)
+ .output(ZUpdateOrganisationEmailResponseSchema)
+ .mutation(async ({ input, ctx }) => {
+ const { emailId, emailName } = input;
+ const { user } = ctx;
+
+ ctx.logger.info({
+ input: {
+ emailId,
+ },
+ });
+
+ const organisationEmail = await prisma.organisationEmail.findFirst({
+ where: {
+ id: emailId,
+ organisation: buildOrganisationWhereQuery({
+ organisationId: undefined,
+ userId: user.id,
+ roles: ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
+ }),
+ },
+ });
+
+ if (!organisationEmail) {
+ throw new AppError(AppErrorCode.UNAUTHORIZED);
+ }
+
+ await prisma.organisationEmail.update({
+ where: {
+ id: emailId,
+ },
+ data: {
+ emailName,
+ // replyTo,
+ },
+ });
+ });
diff --git a/packages/trpc/server/enterprise-router/update-organisation-email.types.ts b/packages/trpc/server/enterprise-router/update-organisation-email.types.ts
new file mode 100644
index 000000000..61222c1a2
--- /dev/null
+++ b/packages/trpc/server/enterprise-router/update-organisation-email.types.ts
@@ -0,0 +1,18 @@
+import { z } from 'zod';
+
+import { ZCreateOrganisationEmailRequestSchema } from './create-organisation-email.types';
+
+export const ZUpdateOrganisationEmailRequestSchema = z
+ .object({
+ emailId: z.string(),
+ })
+ .extend(
+ ZCreateOrganisationEmailRequestSchema.pick({
+ emailName: true,
+ // replyTo: true
+ }).shape,
+ );
+
+export const ZUpdateOrganisationEmailResponseSchema = z.void();
+
+export type TUpdateOrganisationEmailRequest = z.infer;
diff --git a/packages/trpc/server/enterprise-router/verify-organisation-email-domain.ts b/packages/trpc/server/enterprise-router/verify-organisation-email-domain.ts
new file mode 100644
index 000000000..436e1a33d
--- /dev/null
+++ b/packages/trpc/server/enterprise-router/verify-organisation-email-domain.ts
@@ -0,0 +1,59 @@
+import { verifyEmailDomain } from '@documenso/ee/server-only/lib/verify-email-domain';
+import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
+import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
+import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
+import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
+import { prisma } from '@documenso/prisma';
+
+import { authenticatedProcedure } from '../trpc';
+import {
+ ZVerifyOrganisationEmailDomainRequestSchema,
+ ZVerifyOrganisationEmailDomainResponseSchema,
+} from './verify-organisation-email-domain.types';
+
+export const verifyOrganisationEmailDomainRoute = authenticatedProcedure
+ .input(ZVerifyOrganisationEmailDomainRequestSchema)
+ .output(ZVerifyOrganisationEmailDomainResponseSchema)
+ .mutation(async ({ input, ctx }) => {
+ const { organisationId, emailDomainId } = input;
+ const { user } = ctx;
+
+ ctx.logger.info({
+ input: {
+ organisationId,
+ emailDomainId,
+ },
+ });
+
+ if (!IS_BILLING_ENABLED()) {
+ throw new AppError(AppErrorCode.INVALID_REQUEST, {
+ message: 'Billing is not enabled',
+ });
+ }
+
+ const organisation = await prisma.organisation.findFirst({
+ where: buildOrganisationWhereQuery({
+ organisationId,
+ userId: user.id,
+ roles: ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_ORGANISATION'],
+ }),
+ include: {
+ emailDomains: true,
+ },
+ });
+
+ if (!organisation) {
+ throw new AppError(AppErrorCode.UNAUTHORIZED);
+ }
+
+ // Filter down emails to verify a specific email, otherwise verify all emails regardless of status.
+ const emailsToVerify = organisation.emailDomains.filter((email) => {
+ if (emailDomainId && email.id !== emailDomainId) {
+ return false;
+ }
+
+ return true;
+ });
+
+ await Promise.all(emailsToVerify.map(async (email) => verifyEmailDomain(email.id)));
+ });
diff --git a/packages/trpc/server/enterprise-router/verify-organisation-email-domain.types.ts b/packages/trpc/server/enterprise-router/verify-organisation-email-domain.types.ts
new file mode 100644
index 000000000..49897b4a4
--- /dev/null
+++ b/packages/trpc/server/enterprise-router/verify-organisation-email-domain.types.ts
@@ -0,0 +1,8 @@
+import { z } from 'zod';
+
+export const ZVerifyOrganisationEmailDomainRequestSchema = z.object({
+ organisationId: z.string(),
+ emailDomainId: z.string().optional().describe('Leave blank to revalidate all emails'),
+});
+
+export const ZVerifyOrganisationEmailDomainResponseSchema = z.void();
diff --git a/packages/trpc/server/organisation-router/update-organisation-settings.ts b/packages/trpc/server/organisation-router/update-organisation-settings.ts
index 09061bf07..ca347bc10 100644
--- a/packages/trpc/server/organisation-router/update-organisation-settings.ts
+++ b/packages/trpc/server/organisation-router/update-organisation-settings.ts
@@ -26,16 +26,25 @@ export const updateOrganisationSettingsRoute = authenticatedProcedure
// Document related settings.
documentVisibility,
documentLanguage,
+ documentTimezone,
+ documentDateFormat,
includeSenderDetails,
includeSigningCertificate,
typedSignatureEnabled,
uploadSignatureEnabled,
drawSignatureEnabled,
+
// Branding related settings.
brandingEnabled,
brandingLogo,
brandingUrl,
brandingCompanyDetails,
+
+ // Email related settings.
+ emailId,
+ emailReplyTo,
+ // emailReplyToName,
+ emailDocumentSettings,
} = data;
if (Object.values(data).length === 0) {
@@ -61,6 +70,22 @@ export const updateOrganisationSettingsRoute = authenticatedProcedure
});
}
+ // Validate that the email ID belongs to the organisation.
+ if (emailId) {
+ const email = await prisma.organisationEmail.findFirst({
+ where: {
+ id: emailId,
+ organisationId,
+ },
+ });
+
+ if (!email) {
+ throw new AppError(AppErrorCode.NOT_FOUND, {
+ message: 'Email not found',
+ });
+ }
+ }
+
const derivedTypedSignatureEnabled =
typedSignatureEnabled ?? organisation.organisationGlobalSettings.typedSignatureEnabled;
const derivedUploadSignatureEnabled =
@@ -88,6 +113,8 @@ export const updateOrganisationSettingsRoute = authenticatedProcedure
// Document related settings.
documentVisibility,
documentLanguage,
+ documentTimezone,
+ documentDateFormat,
includeSenderDetails,
includeSigningCertificate,
typedSignatureEnabled,
@@ -99,6 +126,12 @@ export const updateOrganisationSettingsRoute = authenticatedProcedure
brandingLogo,
brandingUrl,
brandingCompanyDetails,
+
+ // Email related settings.
+ emailId,
+ emailReplyTo,
+ // emailReplyToName,
+ emailDocumentSettings,
},
},
},
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 99987ba21..c7b0ac75b 100644
--- a/packages/trpc/server/organisation-router/update-organisation-settings.types.ts
+++ b/packages/trpc/server/organisation-router/update-organisation-settings.types.ts
@@ -1,14 +1,22 @@
import { z } from 'zod';
import { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n';
+import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
+import {
+ ZDocumentMetaDateFormatSchema,
+ ZDocumentMetaTimezoneSchema,
+} from '../document-router/schema';
+
export const ZUpdateOrganisationSettingsRequestSchema = z.object({
organisationId: z.string(),
data: z.object({
// Document related settings.
documentVisibility: z.nativeEnum(DocumentVisibility).optional(),
documentLanguage: z.enum(SUPPORTED_LANGUAGE_CODES).optional(),
+ documentTimezone: ZDocumentMetaTimezoneSchema.nullish(), // Null means local timezone.
+ documentDateFormat: ZDocumentMetaDateFormatSchema.optional(),
includeSenderDetails: z.boolean().optional(),
includeSigningCertificate: z.boolean().optional(),
typedSignatureEnabled: z.boolean().optional(),
@@ -20,6 +28,12 @@ export const ZUpdateOrganisationSettingsRequestSchema = z.object({
brandingLogo: z.string().optional(),
brandingUrl: z.string().optional(),
brandingCompanyDetails: z.string().optional(),
+
+ // Email related settings.
+ emailId: z.string().nullish(),
+ emailReplyTo: z.string().email().nullish(),
+ // emailReplyToName: z.string().optional(),
+ emailDocumentSettings: ZDocumentEmailSettingsSchema.optional(),
}),
});
diff --git a/packages/trpc/server/router.ts b/packages/trpc/server/router.ts
index 861fd41ad..fbe35147c 100644
--- a/packages/trpc/server/router.ts
+++ b/packages/trpc/server/router.ts
@@ -1,9 +1,9 @@
import { adminRouter } from './admin-router/router';
import { apiTokenRouter } from './api-token-router/router';
import { authRouter } from './auth-router/router';
-import { billingRouter } from './billing/router';
import { documentRouter } from './document-router/router';
import { embeddingPresignRouter } from './embedding-router/_router';
+import { enterpriseRouter } from './enterprise-router/router';
import { fieldRouter } from './field-router/router';
import { folderRouter } from './folder-router/router';
import { organisationRouter } from './organisation-router/router';
@@ -16,8 +16,8 @@ import { router } from './trpc';
import { webhookRouter } from './webhook-router/router';
export const appRouter = router({
+ enterprise: enterpriseRouter,
auth: authRouter,
- billing: billingRouter,
profile: profileRouter,
document: documentRouter,
field: fieldRouter,
diff --git a/packages/trpc/server/team-router/update-team-settings.ts b/packages/trpc/server/team-router/update-team-settings.ts
index 72082a9a1..9cae5b330 100644
--- a/packages/trpc/server/team-router/update-team-settings.ts
+++ b/packages/trpc/server/team-router/update-team-settings.ts
@@ -1,3 +1,5 @@
+import { Prisma } from '@prisma/client';
+
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/teams';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { buildTeamWhereQuery } from '@documenso/lib/utils/teams';
@@ -26,6 +28,8 @@ export const updateTeamSettingsRoute = authenticatedProcedure
// Document related settings.
documentVisibility,
documentLanguage,
+ documentTimezone,
+ documentDateFormat,
includeSenderDetails,
includeSigningCertificate,
typedSignatureEnabled,
@@ -37,6 +41,12 @@ export const updateTeamSettingsRoute = authenticatedProcedure
brandingLogo,
brandingUrl,
brandingCompanyDetails,
+
+ // Email related settings.
+ emailId,
+ emailReplyTo,
+ // emailReplyToName,
+ emailDocumentSettings,
} = data;
if (Object.values(data).length === 0) {
@@ -70,6 +80,22 @@ export const updateTeamSettingsRoute = authenticatedProcedure
});
}
+ // Validate that the email ID belongs to the organisation.
+ if (emailId) {
+ const email = await prisma.organisationEmail.findFirst({
+ where: {
+ id: emailId,
+ organisationId: team.organisationId,
+ },
+ });
+
+ if (!email) {
+ throw new AppError(AppErrorCode.NOT_FOUND, {
+ message: 'Email not found',
+ });
+ }
+ }
+
await prisma.team.update({
where: {
id: teamId,
@@ -80,6 +106,8 @@ export const updateTeamSettingsRoute = authenticatedProcedure
// Document related settings.
documentVisibility,
documentLanguage,
+ documentTimezone,
+ documentDateFormat,
includeSenderDetails,
includeSigningCertificate,
typedSignatureEnabled,
@@ -91,6 +119,13 @@ export const updateTeamSettingsRoute = authenticatedProcedure
brandingLogo,
brandingUrl,
brandingCompanyDetails,
+
+ // Email related settings.
+ emailId,
+ emailReplyTo,
+ // emailReplyToName,
+ emailDocumentSettings:
+ emailDocumentSettings === null ? Prisma.DbNull : emailDocumentSettings,
},
},
},
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 5b7781b92..9f1fee8fd 100644
--- a/packages/trpc/server/team-router/update-team-settings.types.ts
+++ b/packages/trpc/server/team-router/update-team-settings.types.ts
@@ -1,8 +1,14 @@
import { z } from 'zod';
import { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n';
+import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
+import {
+ ZDocumentMetaDateFormatSchema,
+ ZDocumentMetaTimezoneSchema,
+} from '../document-router/schema';
+
/**
* Null = Inherit from organisation.
* Undefined = Do nothing
@@ -13,6 +19,8 @@ export const ZUpdateTeamSettingsRequestSchema = z.object({
// Document related settings.
documentVisibility: z.nativeEnum(DocumentVisibility).nullish(),
documentLanguage: z.enum(SUPPORTED_LANGUAGE_CODES).nullish(),
+ documentTimezone: ZDocumentMetaTimezoneSchema.nullish(),
+ documentDateFormat: ZDocumentMetaDateFormatSchema.nullish(),
includeSenderDetails: z.boolean().nullish(),
includeSigningCertificate: z.boolean().nullish(),
typedSignatureEnabled: z.boolean().nullish(),
@@ -24,6 +32,12 @@ export const ZUpdateTeamSettingsRequestSchema = z.object({
brandingLogo: z.string().nullish(),
brandingUrl: z.string().nullish(),
brandingCompanyDetails: z.string().nullish(),
+
+ // Email related settings.
+ emailId: z.string().nullish(),
+ emailReplyTo: z.string().email().nullish(),
+ // emailReplyToName: z.string().nullish(),
+ emailDocumentSettings: ZDocumentEmailSettingsSchema.nullish(),
}),
});
diff --git a/packages/trpc/server/template-router/schema.ts b/packages/trpc/server/template-router/schema.ts
index 41452915a..ff8bb0eca 100644
--- a/packages/trpc/server/template-router/schema.ts
+++ b/packages/trpc/server/template-router/schema.ts
@@ -64,6 +64,8 @@ export const ZTemplateMetaUpsertSchema = z.object({
timezone: ZDocumentMetaTimezoneSchema.optional(),
dateFormat: ZDocumentMetaDateFormatSchema.optional(),
distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(),
+ emailId: z.string().nullish(),
+ emailReplyTo: z.string().nullish(),
emailSettings: ZDocumentEmailSettingsSchema.optional(),
redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
language: ZDocumentMetaLanguageSchema.optional(),
diff --git a/packages/ui/components/document/document-read-only-fields.tsx b/packages/ui/components/document/document-read-only-fields.tsx
index 4dcf72104..80e998c26 100644
--- a/packages/ui/components/document/document-read-only-fields.tsx
+++ b/packages/ui/components/document/document-read-only-fields.tsx
@@ -23,7 +23,7 @@ import { FieldContent } from '../../primitives/document-flow/field-content';
export type DocumentReadOnlyFieldsProps = {
fields: DocumentField[];
- documentMeta?: DocumentMeta | TemplateMeta;
+ documentMeta?: Pick;
showFieldStatus?: boolean;
diff --git a/packages/ui/primitives/combobox.tsx b/packages/ui/primitives/combobox.tsx
index 93f4167f7..2f5bb6712 100644
--- a/packages/ui/primitives/combobox.tsx
+++ b/packages/ui/primitives/combobox.tsx
@@ -15,8 +15,10 @@ type ComboboxProps = {
options: string[];
value: string | null;
onChange: (_value: string | null) => void;
+ triggerPlaceholder?: string;
placeholder?: string;
disabled?: boolean;
+ testId?: string;
};
const Combobox = ({
@@ -25,7 +27,9 @@ const Combobox = ({
value,
onChange,
disabled = false,
+ triggerPlaceholder,
placeholder,
+ testId,
}: ComboboxProps) => {
const { _ } = useLingui();
@@ -47,8 +51,9 @@ const Combobox = ({
aria-expanded={open}
className={cn('my-2 w-full justify-between', className)}
disabled={disabled}
+ data-testid={testId}
>
- {value ? value : placeholderValue}
+ {value ? value : triggerPlaceholder || placeholderValue}
diff --git a/packages/ui/primitives/document-flow/add-subject.tsx b/packages/ui/primitives/document-flow/add-subject.tsx
index 5666bae91..6553597f2 100644
--- a/packages/ui/primitives/document-flow/add-subject.tsx
+++ b/packages/ui/primitives/document-flow/add-subject.tsx
@@ -5,13 +5,31 @@ import { Trans } from '@lingui/react/macro';
import type { Field, Recipient } from '@prisma/client';
import { DocumentDistributionMethod, DocumentStatus, RecipientRole } from '@prisma/client';
import { AnimatePresence, motion } from 'framer-motion';
+import { InfoIcon } from 'lucide-react';
import { useForm } from 'react-hook-form';
+import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
import type { TDocument } from '@documenso/lib/types/document';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import { formatSigningLink } from '@documenso/lib/utils/recipients';
+import { trpc } from '@documenso/trpc/react';
import { DocumentSendEmailMessageHelper } from '@documenso/ui/components/document/document-send-email-message-helper';
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from '@documenso/ui/primitives/form/form';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@documenso/ui/primitives/select';
import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
import { CopyTextButton } from '../../components/common/copy-text-button';
@@ -21,11 +39,10 @@ import {
mapFieldsWithRecipients,
} from '../../components/document/document-read-only-fields';
import { AvatarWithText } from '../avatar';
-import { FormErrorMessage } from '../form/form-error-message';
import { Input } from '../input';
-import { Label } from '../label';
import { useStep } from '../stepper';
import { Textarea } from '../textarea';
+import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip';
import { toast } from '../use-toast';
import { type TAddSubjectFormSchema, ZAddSubjectFormSchema } from './add-subject.types';
import {
@@ -56,15 +73,14 @@ export const AddSubjectFormPartial = ({
}: AddSubjectFormProps) => {
const { _ } = useLingui();
- const {
- register,
- handleSubmit,
- setValue,
- watch,
- formState: { errors, isSubmitting },
- } = useForm({
+ const organisation = useCurrentOrganisation();
+
+ const form = useForm({
defaultValues: {
meta: {
+ emailId: document.documentMeta?.emailId ?? null,
+ emailReplyTo: document.documentMeta?.emailReplyTo || undefined,
+ // emailReplyName: document.documentMeta?.emailReplyName || undefined,
subject: document.documentMeta?.subject ?? '',
message: document.documentMeta?.message ?? '',
distributionMethod:
@@ -75,6 +91,21 @@ export const AddSubjectFormPartial = ({
resolver: zodResolver(ZAddSubjectFormSchema),
});
+ const {
+ handleSubmit,
+ setValue,
+ watch,
+ formState: { isSubmitting },
+ } = form;
+
+ const { data: emailData, isLoading: isLoadingEmails } =
+ trpc.enterprise.organisation.email.find.useQuery({
+ organisationId: organisation.id,
+ perPage: 100,
+ });
+
+ const emails = emailData?.data || [];
+
const GoNextLabel = {
[DocumentDistributionMethod.EMAIL]: {
[DocumentStatus.DRAFT]: msg`Send`,
@@ -139,54 +170,141 @@ export const AddSubjectFormPartial = ({
initial={{ opacity: 0, y: 15 }}
animate={{ opacity: 1, y: 0, transition: { duration: 0.3 } }}
exit={{ opacity: 0, transition: { duration: 0.15 } }}
- className="flex flex-col gap-y-4 rounded-lg border p-4"
>
-
-
-
- Subject (Optional)
-
-
+
+
+ {emails.map((email) => (
+
+ {email.email}
+
+ ))}
-
-
-
- Message (Optional)
-
-
+ Documenso
+
+
+
-
+
+
+ )}
+ />
+ )}
-
-
+ (
+
+
+ Reply To Email {' '}
+ (Optional)
+
-
+
+
+
- setValue('meta.emailSettings', value)}
- />
+
+
+ )}
+ />
+
+ {/* (
+
+
+ Reply To Name {' '}
+ (Optional)
+
+
+
+
+
+
+
+
+ )}
+ /> */}
+
+ (
+
+
+ Subject {' '}
+ (Optional)
+
+
+
+
+
+
+
+ )}
+ />
+
+ (
+
+
+ Message {' '}
+ (Optional)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+ />
+
+ setValue('meta.emailSettings', value)}
+ />
+
+
)}
diff --git a/packages/ui/primitives/document-flow/add-subject.types.ts b/packages/ui/primitives/document-flow/add-subject.types.ts
index ea5e3e944..0b26c2909 100644
--- a/packages/ui/primitives/document-flow/add-subject.types.ts
+++ b/packages/ui/primitives/document-flow/add-subject.types.ts
@@ -5,6 +5,9 @@ import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-emai
export const ZAddSubjectFormSchema = z.object({
meta: z.object({
+ emailId: z.string().nullable(),
+ emailReplyTo: z.string().email().optional(),
+ // emailReplyName: z.string().optional(),
subject: z.string(),
message: z.string(),
distributionMethod: z
diff --git a/packages/ui/primitives/document-flow/field-content.tsx b/packages/ui/primitives/document-flow/field-content.tsx
index 3ebff09ae..544fb41c2 100644
--- a/packages/ui/primitives/document-flow/field-content.tsx
+++ b/packages/ui/primitives/document-flow/field-content.tsx
@@ -27,7 +27,7 @@ type FieldIconProps = {
fieldMeta?: TFieldMetaSchema | null;
signature?: Signature | null;
};
- documentMeta?: DocumentMeta | TemplateMeta;
+ documentMeta?: Pick;
};
/**
diff --git a/packages/ui/primitives/select.tsx b/packages/ui/primitives/select.tsx
index 61220d7ba..dda9caf74 100644
--- a/packages/ui/primitives/select.tsx
+++ b/packages/ui/primitives/select.tsx
@@ -1,7 +1,10 @@
import * as React from 'react';
import * as SelectPrimitive from '@radix-ui/react-select';
-import { Check, ChevronDown } from 'lucide-react';
+import { AnimatePresence } from 'framer-motion';
+import { Check, ChevronDown, Loader } from 'lucide-react';
+
+import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out';
import { cn } from '../lib/utils';
@@ -13,20 +16,33 @@ const SelectValue = SelectPrimitive.Value;
const SelectTrigger = React.forwardRef<
React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, children, ...props }, ref) => (
+ React.ComponentPropsWithoutRef & {
+ loading?: boolean;
+ }
+>(({ className, children, loading, ...props }, ref) => (
- {children}
-
-
-
+
+ {loading ? (
+
+
+
+ ) : (
+
+ {children}
+
+
+
+
+ )}
+
));
diff --git a/packages/ui/primitives/template-flow/add-template-settings.tsx b/packages/ui/primitives/template-flow/add-template-settings.tsx
index 16e40f3d6..d880b8edb 100644
--- a/packages/ui/primitives/template-flow/add-template-settings.tsx
+++ b/packages/ui/primitives/template-flow/add-template-settings.tsx
@@ -21,6 +21,7 @@ import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-emai
import type { TTemplate } from '@documenso/lib/types/template';
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
import { extractTeamSignatureSettings } from '@documenso/lib/utils/teams';
+import { trpc } from '@documenso/trpc/react';
import type { TDocumentMetaDateFormat } from '@documenso/trpc/server/document-router/schema';
import {
DocumentGlobalAuthAccessSelect,
@@ -120,6 +121,8 @@ export const AddTemplateSettingsFormPartial = ({
template.templateMeta?.distributionMethod || DocumentDistributionMethod.EMAIL,
redirectUrl: template.templateMeta?.redirectUrl ?? '',
language: template.templateMeta?.language ?? 'en',
+ emailId: template.templateMeta?.emailId ?? null,
+ emailReplyTo: template.templateMeta?.emailReplyTo ?? undefined,
emailSettings: ZDocumentEmailSettingsSchema.parse(template?.templateMeta?.emailSettings),
signatureTypes: extractTeamSignatureSettings(template?.templateMeta),
},
@@ -131,6 +134,14 @@ export const AddTemplateSettingsFormPartial = ({
const distributionMethod = form.watch('meta.distributionMethod');
const emailSettings = form.watch('meta.emailSettings');
+ const { data: emailData, isLoading: isLoadingEmails } =
+ trpc.enterprise.organisation.email.find.useQuery({
+ organisationId: organisation.id,
+ perPage: 100,
+ });
+
+ const emails = emailData?.data || [];
+
const canUpdateVisibility = match(currentTeamMemberRole)
.with(TeamMemberRole.ADMIN, () => true)
.with(
@@ -403,6 +414,68 @@ export const AddTemplateSettingsFormPartial = ({
+ {organisation.organisationClaim.flags.emailDomains && (
+ (
+
+
+ Email Sender
+
+
+
+
+ field.onChange(value === '-1' ? null : value)
+ }
+ >
+
+
+
+
+
+ {emails.map((email) => (
+
+ {email.email}
+
+ ))}
+
+ Documenso
+
+
+
+
+
+
+ )}
+ />
+ )}
+
+ (
+
+
+ Reply To Email {' '}
+ (Optional)
+
+
+
+
+
+
+
+
+ )}
+ />
+
(
-
-
- Message (Optional)
-
+
+ Message {' '}
+ (Optional)
+
+
+
+
+
+
+
+
-
+
@@ -443,8 +523,6 @@ export const AddTemplateSettingsFormPartial = ({
)}
/>
-
-
form.setValue('meta.emailSettings', value)}
diff --git a/packages/ui/primitives/template-flow/add-template-settings.types.tsx b/packages/ui/primitives/template-flow/add-template-settings.types.tsx
index 440b0b40d..08319b991 100644
--- a/packages/ui/primitives/template-flow/add-template-settings.types.tsx
+++ b/packages/ui/primitives/template-flow/add-template-settings.types.tsx
@@ -48,6 +48,8 @@ export const ZAddTemplateSettingsFormSchema = z.object({
.union([z.string(), z.enum(SUPPORTED_LANGUAGE_CODES)])
.optional()
.default('en'),
+ emailId: z.string().nullable(),
+ emailReplyTo: z.string().optional(),
emailSettings: ZDocumentEmailSettingsSchema,
signatureTypes: z.array(z.nativeEnum(DocumentSignatureType)).min(1, {
message: msg`At least one signature type must be enabled`.id,