From 2ce0e6d4ce7888d0f0c55e4e35880c800678eb17 Mon Sep 17 00:00:00 2001 From: Denis Marinov Date: Thu, 12 Feb 2026 13:46:16 +0200 Subject: [PATCH 01/21] chore: Update docker-compose and Makefile configurations --- Makefile | 11 ++++++++--- docker-compose.yml | 7 +++++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 3741a2bc0b..d94a18cc39 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,15 @@ build: @echo "Building with HOST_URL=${HOST_URL}" cp .env.local_dev .env - cd apps/OpenSign && cp ../../.env.local_dev .env && npm install && npm run build - HOST_URL=${HOST_URL} docker compose up --build --force-recreate + HOST_URL=${HOST_URL} docker compose up -d --build --force-recreate -run: +up: @echo "Building with HOST_URL=${HOST_URL}" cp .env.local_dev .env docker compose up -d + +down: + docker compose down + +ssh: + docker compose exec server bash diff --git a/docker-compose.yml b/docker-compose.yml index 577c1ed74e..07606d7b16 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,8 @@ services: server: + build: + context: . + dockerfile: apps/OpenSignServer/Dockerhubfile image: opensign/opensignserver:main container_name: OpenSignServer-container volumes: @@ -8,7 +11,7 @@ services: - "8080:8080" depends_on: - mongo - env_file: .env.prod + env_file: .env environment: - NODE_ENV=production - SERVER_URL=${HOST_URL:-https://localhost:3001}/api/app @@ -29,7 +32,7 @@ services: container_name: OpenSign-container depends_on: - server - env_file: .env.prod + env_file: .env ports: - "3000:3000" networks: From a28e849d9fa6e3ae400554cad7bb2fd8f13aae7d Mon Sep 17 00:00:00 2001 From: Denis Marinov Date: Thu, 12 Feb 2026 13:48:31 +0200 Subject: [PATCH 02/21] chore: Remove redundant .env file copy from Makefile --- Makefile | 2 -- 1 file changed, 2 deletions(-) diff --git a/Makefile b/Makefile index d94a18cc39..fc768e990d 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,9 @@ build: @echo "Building with HOST_URL=${HOST_URL}" - cp .env.local_dev .env HOST_URL=${HOST_URL} docker compose up -d --build --force-recreate up: @echo "Building with HOST_URL=${HOST_URL}" - cp .env.local_dev .env docker compose up -d down: From fd6da5ec7f0b78be890a785a31c356e002d9a513 Mon Sep 17 00:00:00 2001 From: Denis Marinov Date: Thu, 12 Feb 2026 14:58:19 +0200 Subject: [PATCH 03/21] feat: Add Docker build configuration for OpenSign client --- docker-compose.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 07606d7b16..796c0d04d0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -28,6 +28,9 @@ services: networks: - app-network client: + build: + context: . + dockerfile: apps/OpenSign/Dockerhubfile image: opensign/opensign:main container_name: OpenSign-container depends_on: From 26311dc8a782023c2fbc3ce031caf564d7435cb9 Mon Sep 17 00:00:00 2001 From: Denis Marinov Date: Thu, 12 Feb 2026 15:51:45 +0200 Subject: [PATCH 04/21] feat: Enhance email configuration and handling in OpenSignServer - Updated .env.example and .env.local_dev to include new SMTP configuration options. - Refactored email sending logic in OpenSignServer to utilize new utility functions for resolving sender email and formatting headers. - Improved error handling for invalid email configurations. - Ensured compatibility with both SMTP and Mailgun email sending methods. --- .env.example | 6 + .env.local_dev | 10 +- apps/OpenSignServer/Utils.js | 57 ++++ .../cloud/parsefunction/SendMailOTPv1.js | 6 +- .../cloud/parsefunction/sendDeleteUserMail.js | 6 +- .../cloud/parsefunction/sendMailv3.js | 251 +++++++++++++++--- apps/OpenSignServer/index.js | 46 +++- 7 files changed, 330 insertions(+), 52 deletions(-) diff --git a/.env.example b/.env.example index 9d7a990a15..f98e96cacf 100644 --- a/.env.example +++ b/.env.example @@ -45,11 +45,17 @@ DO_REGION=us-west MAILGUN_API_KEY=XXXXX MAILGUN_DOMAIN=mail.yourdomain.com MAILGUN_SENDER=postmaster@mail.yourdomain.com + SMTP_ENABLE= SMTP_HOST= SMTP_PORT= +# SMTP auth username (for AWS SES this can be IAM SMTP credentials and not an email) +SMTP_USERNAME= +# Verified sender address shown in the From header SMTP_USER_EMAIL= SMTP_PASS= +# Optional SMTP envelope MAIL FROM override (defaults to SMTP_USER_EMAIL) +SMTP_MAIL_FROM= # Base64 encoded PFX or p12 document signing certificate file ********************************************************************************************************************* PFX_BASE64= diff --git a/.env.local_dev b/.env.local_dev index dc3d04fd21..f86e8eded7 100644 --- a/.env.local_dev +++ b/.env.local_dev @@ -44,11 +44,17 @@ USE_LOCAL=true MAILGUN_API_KEY= MAILGUN_DOMAIN=mail.yourdomain.com MAILGUN_SENDER=postmaster@mail.yourdomain.com + SMTP_ENABLE=true SMTP_HOST=smtp.yourhost.com -SMTP_PORT=443 +SMTP_PORT=587 +# SMTP auth username (for AWS SES this can be IAM SMTP credentials and not an email) +SMTP_USERNAME=AKIA... +# Verified sender address shown in the From header SMTP_USER_EMAIL=mailer@yourdomain.com -SMTP_PASS=password # if your password includes spaces then write password in single quotes ('asdf pasd asdf bgds'). +SMTP_PASS=password +# Optional SMTP envelope MAIL FROM override (defaults to SMTP_USER_EMAIL) +SMTP_MAIL_FROM= # Base64 encoded PFX or p12 document signing certificate file ********************************************************************************************************************* diff --git a/apps/OpenSignServer/Utils.js b/apps/OpenSignServer/Utils.js index 28b6cc79f2..f8566f8182 100644 --- a/apps/OpenSignServer/Utils.js +++ b/apps/OpenSignServer/Utils.js @@ -136,6 +136,63 @@ export const smtpsecure = process.env.SMTP_PORT && process.env.SMTP_PORT !== '46 export const smtpenable = process.env.SMTP_ENABLE && process.env.SMTP_ENABLE.toLowerCase() === 'true' ? true : false; export const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; +const normalizeMailValue = value => (typeof value === 'string' ? value.trim() : ''); + +export const isValidEmail = value => emailRegex.test(normalizeMailValue(value)); + +export const getResolvedMailSender = ({ isSmtp = smtpenable } = {}) => { + if (isSmtp) { + const smtpUserEmail = normalizeMailValue(process.env.SMTP_USER_EMAIL); + if (isValidEmail(smtpUserEmail)) { + return smtpUserEmail; + } + + const smtpUsername = normalizeMailValue(process.env.SMTP_USERNAME); + if (isValidEmail(smtpUsername)) { + return smtpUsername; + } + + throw new Error('Invalid SMTP sender. Set SMTP_USER_EMAIL to a valid email address.'); + } + + const mailgunSender = normalizeMailValue(process.env.MAILGUN_SENDER); + if (isValidEmail(mailgunSender)) { + return mailgunSender; + } + + throw new Error('Invalid MAILGUN_SENDER. Set it to a valid email address.'); +}; + +export const getSmtpEnvelopeFrom = senderEmail => { + const smtpMailFrom = normalizeMailValue(process.env.SMTP_MAIL_FROM); + if (!smtpMailFrom) { + return senderEmail; + } + + if (isValidEmail(smtpMailFrom)) { + return smtpMailFrom; + } + + throw new Error('Invalid SMTP_MAIL_FROM. Set it to a valid email address.'); +}; + +export const formatFromHeader = (displayName, senderEmail) => { + const safeSender = normalizeMailValue(senderEmail); + const safeDisplayName = normalizeMailValue(displayName); + return safeDisplayName ? `${safeDisplayName} <${safeSender}>` : safeSender; +}; + +export function signPayload(payload, secret) { + if (payload && secret) { + const signature = crypto + .createHmac('sha256', secret) + .update(JSON.stringify(payload)) + .digest('hex'); + return { 'x-webhook-signature': signature }; + } else { + return {}; + } +} // `generateId` is used to unique Id for fileAdapter export function generateId(length) { diff --git a/apps/OpenSignServer/cloud/parsefunction/SendMailOTPv1.js b/apps/OpenSignServer/cloud/parsefunction/SendMailOTPv1.js index 4054898226..e7a4bc07f5 100644 --- a/apps/OpenSignServer/cloud/parsefunction/SendMailOTPv1.js +++ b/apps/OpenSignServer/cloud/parsefunction/SendMailOTPv1.js @@ -1,4 +1,4 @@ -import { appName, smtpenable, updateMailCount } from '../../Utils.js'; +import { appName, formatFromHeader, getResolvedMailSender, smtpenable, updateMailCount } from '../../Utils.js'; async function getDocument(docId) { try { const query = new Parse.Query('contracts_Document'); @@ -26,10 +26,10 @@ async function sendMailOTPv1(request) { if (email) { const recipient = request.params.email; - const mailsender = smtpenable ? process.env.SMTP_USER_EMAIL : process.env.MAILGUN_SENDER; + const senderEmail = getResolvedMailSender({ isSmtp: smtpenable }); try { await Parse.Cloud.sendEmail({ - sender: AppName + ' <' + mailsender + '>', + sender: formatFromHeader(AppName, senderEmail), recipient: recipient, subject: `Your ${AppName} OTP`, text: 'otp email', diff --git a/apps/OpenSignServer/cloud/parsefunction/sendDeleteUserMail.js b/apps/OpenSignServer/cloud/parsefunction/sendDeleteUserMail.js index 86a33222cd..292c0971e9 100644 --- a/apps/OpenSignServer/cloud/parsefunction/sendDeleteUserMail.js +++ b/apps/OpenSignServer/cloud/parsefunction/sendDeleteUserMail.js @@ -1,4 +1,4 @@ -import { appName, smtpenable } from '../../Utils.js'; +import { appName, formatFromHeader, getResolvedMailSender, smtpenable } from '../../Utils.js'; export const errHtml = err => { return `Reset Password @@ -41,11 +41,11 @@ const sendDeleteUserMail = async req => { const serverUrl = process.env?.SERVER_URL?.replace(/\/app\/?$/, '/'); const deleteUrl = `${serverUrl}delete-account/${userId}`; - const mailsender = smtpenable ? process.env.SMTP_USER_EMAIL : process.env.MAILGUN_SENDER; + const senderEmail = getResolvedMailSender({ isSmtp: smtpenable }); // Render a simple HTML form. In production, consider using a templating engine. await Parse.Cloud.sendEmail({ - sender: app + ' <' + mailsender + '>', + sender: formatFromHeader(app, senderEmail), recipient: username, subject: `Account Deletion Request for ${username} – ${app}`, text: `Account Deletion Request for ${username} – ${app}`, diff --git a/apps/OpenSignServer/cloud/parsefunction/sendMailv3.js b/apps/OpenSignServer/cloud/parsefunction/sendMailv3.js index 2916769ca9..cd4d6a792f 100644 --- a/apps/OpenSignServer/cloud/parsefunction/sendMailv3.js +++ b/apps/OpenSignServer/cloud/parsefunction/sendMailv3.js @@ -2,11 +2,17 @@ import fs from 'node:fs'; import https from 'https'; import formData from 'form-data'; import Mailgun from 'mailgun.js'; -import { appName, smtpenable, smtpsecure, updateMailCount } from '../../Utils.js'; +import { + formatFromHeader, + getResolvedMailSender, + getSmtpEnvelopeFrom, + smtpenable, + smtpsecure, + updateMailCount, +} from '../../Utils.js'; import { createTransport } from 'nodemailer'; import axios from 'axios'; async function sendMailProvider(req) { - const app = appName; const extUserId = req.params?.extUserId || ''; const reportMsg = `

If you think this email is inappropriate or spam, you may file a complaint with OpenSign™ here.

`; @@ -22,15 +28,11 @@ async function sendMailProvider(req) { secure: smtpsecure, }; - // ✅ Add auth only if BOTH username & password exist - const smtpUser = process.env.SMTP_USERNAME; + const smtpUser = process.env.SMTP_USERNAME || process.env.SMTP_USER_EMAIL; const smtpPass = process.env.SMTP_PASS; if (smtpUser && smtpPass) { - transporterConfig.auth = { - user: process.env.SMTP_USERNAME ? process.env.SMTP_USERNAME : process.env.SMTP_USER_EMAIL, - pass: smtpPass, - }; + transporterConfig.auth = { user: smtpUser, pass: smtpPass }; } transporterSMTP = createTransport(transporterConfig); } else { @@ -40,45 +42,226 @@ async function sendMailProvider(req) { mailgunDomain = process.env.MAILGUN_DOMAIN; } } + if (req.params.url) { + const randomNumber = Math.floor(Math.random() * 5000); + const testPdf = `test_${randomNumber}.pdf`; + try { + let Pdf = fs.createWriteStream(testPdf); + const writeToLocalDisk = () => { + return new Promise((resolve, reject) => { + const isSecure = + new URL(req.params.url)?.protocol === 'https:' && + new URL(req.params.url)?.hostname !== 'localhost'; + if (isSecure) { + https + .get(req.params.url, async function (response) { + response.pipe(Pdf); + response.on('end', () => resolve('success')); + }) + .on('error', e => { + console.error(`error: ${e.message}`); + resolve('error'); + }); + } else { + const httpsAgent = new https.Agent({ rejectUnauthorized: false }); // Disable SSL validation + const localUrl = req.params.url; + const newlocalUrl = localUrl.replace( + 'https://localhost:3001/api', + 'http://localhost:8080' + ); + axios + .get(newlocalUrl, { responseType: 'stream', httpsAgent: httpsAgent }) + .then(response => { + response.data.pipe(Pdf); + Pdf.on('finish', () => resolve('success')); + Pdf.on('error', () => resolve('error')); + }) + .catch(e => { + console.log('error in localurl', e.message); + resolve('error'); + }); + } + }); + }; + // `writeToLocalDisk` is used to create pdf file from doc url + const ress = await writeToLocalDisk(); + if (ress) { + function readTolocal() { + return new Promise((resolve, reject) => { + setTimeout(() => { + let PdfBuffer = fs.readFileSync(Pdf.path); + resolve(PdfBuffer); + }, 100); + }); + } + // `PdfBuffer` used to create buffer from pdf file + let PdfBuffer = await readTolocal(); + const pdfName = req.params.pdfName && `${req.params.pdfName}.pdf`; + const filename = req.params.filename; + const file = { + filename: filename || pdfName || 'exported.pdf', + content: smtpenable ? PdfBuffer : undefined, + data: smtpenable ? undefined : PdfBuffer, + }; - const from = req.params.from || ''; - const mailsender = smtpenable ? process.env.SMTP_USER_EMAIL : process.env.MAILGUN_SENDER; - const replyto = req.params?.replyto || ''; - const messageParams = { - from: from + ' <' + mailsender + '>', - to: req.params.recipient, - subject: req.params.subject, - text: req.params.text || 'mail', - html: req.params?.html ? req.params.html + reportMsg : '', - bcc: req.params.bcc ? req.params.bcc : undefined, - replyTo: replyto ? replyto : undefined, - }; - - if (transporterSMTP) { - const res = await transporterSMTP.sendMail(messageParams); - console.log('smtp transporter res: ', res?.response); - if (!res.err) { - if (extUserId) { - await updateMailCount(extUserId); + let attachment; + const certificatePath = req.params.certificatePath || `./exports/certificate.pdf`; + if (fs.existsSync(certificatePath)) { + try { + // `certificateBuffer` used to create buffer from pdf file + const certificateBuffer = fs.readFileSync(certificatePath); + const certificate = { + filename: 'certificate.pdf', + content: smtpenable ? certificateBuffer : undefined, //fs.readFileSync('./exports/exported_file_1223.pdf'), + data: smtpenable ? undefined : certificateBuffer, + }; + attachment = [file, certificate]; + } catch (err) { + attachment = [file]; + console.log('Err in read certificate sendmailv3', err); + } + } else { + attachment = [file]; + } + const from = req.params.from || ''; + const senderEmail = getResolvedMailSender({ isSmtp: smtpenable }); + const replyto = req.params?.replyto || ''; + const messageParams = { + from: formatFromHeader(from, senderEmail), + to: req.params.recipient, + subject: req.params.subject, + text: req.params.text || 'mail', + html: req.params?.html ? req.params.html + reportMsg : '', + attachments: smtpenable ? attachment : undefined, + attachment: smtpenable ? undefined : attachment, + bcc: req.params.bcc ? req.params.bcc : undefined, + replyTo: replyto ? replyto : undefined, + }; + if (smtpenable) { + messageParams.envelope = { from: getSmtpEnvelopeFrom(senderEmail) }; + } + if (transporterSMTP) { + const res = await transporterSMTP.sendMail(messageParams); + console.log('smtp transporter res: ', res?.response); + if (!res.err) { + if (extUserId) { + await updateMailCount(extUserId); + } + if (fs.existsSync(certificatePath)) { + try { + fs.unlinkSync(certificatePath); + } catch (err) { + console.log('Err in unlink certificate sendmailv3'); + } + } + if (fs.existsSync(testPdf)) { + try { + fs.unlinkSync(testPdf); + } catch (err) { + console.log('Err in unlink pdf sendmailv3'); + } + } + return { status: 'success' }; + } + } else { + if (mailgunApiKey) { + const res = await mailgunClient.messages.create(mailgunDomain, messageParams); + console.log('mailgun res: ', res?.status); + if (res.status === 200) { + if (extUserId) { + await updateMailCount(extUserId); + } + if (fs.existsSync(certificatePath)) { + try { + fs.unlinkSync(certificatePath); + } catch (err) { + console.log('Err in unlink certificate sendmailv3'); + } + } + if (fs.existsSync(testPdf)) { + try { + fs.unlinkSync(testPdf); + } catch (err) { + console.log('Err in unlink pdf sendmailv3'); + } + } + return { status: 'success' }; + } + } else { + if (fs.existsSync(certificatePath)) { + try { + fs.unlinkSync(certificatePath); + } catch (err) { + console.log('Err in unlink certificate sendmailv3'); + } + } + if (fs.existsSync(testPdf)) { + try { + fs.unlinkSync(testPdf); + } catch (err) { + console.log('Err in unlink pdf sendmailv3'); + } + } + return { status: 'error' }; + } + } + } + } catch (err) { + console.log(`Error in sendmailv3: ${err}`); + if (fs.existsSync(testPdf)) { + try { + fs.unlinkSync(testPdf); + } catch (err) { + console.log('Err in unlink pdf sendmailv3'); + } + } + if (err) { + return { status: 'error' }; } - return { status: 'success' }; } } else { - if (mailgunApiKey) { - const res = await mailgunClient.messages.create(mailgunDomain, messageParams); - console.log('mailgun res: ', res?.status); - if (res.status === 200) { + const from = req.params.from || ''; + const senderEmail = getResolvedMailSender({ isSmtp: smtpenable }); + const replyto = req.params?.replyto || ''; + const messageParams = { + from: formatFromHeader(from, senderEmail), + to: req.params.recipient, + subject: req.params.subject, + text: req.params.text || 'mail', + html: req.params?.html ? req.params.html + reportMsg : '', + bcc: req.params.bcc ? req.params.bcc : undefined, + replyTo: replyto ? replyto : undefined, + }; + if (smtpenable) { + messageParams.envelope = { from: getSmtpEnvelopeFrom(senderEmail) }; + } + + if (transporterSMTP) { + const res = await transporterSMTP.sendMail(messageParams); + console.log('smtp transporter res: ', res?.response); + if (!res.err) { if (extUserId) { await updateMailCount(extUserId); } return { status: 'success' }; } } else { - return { status: 'error' }; + if (mailgunApiKey) { + const res = await mailgunClient.messages.create(mailgunDomain, messageParams); + console.log('mailgun res: ', res?.status); + if (res.status === 200) { + if (extUserId) { + await updateMailCount(extUserId); + } + return { status: 'success' }; + } + } else { + return { status: 'error' }; + } } } } catch (err) { - console.log(`sendmailv3 Error: ${err}`); + console.log(`Error in sendmailv3: ${err}`); if (err) { return { status: 'error' }; } diff --git a/apps/OpenSignServer/index.js b/apps/OpenSignServer/index.js index fe72c11ea5..10bb03eb11 100644 --- a/apps/OpenSignServer/index.js +++ b/apps/OpenSignServer/index.js @@ -14,7 +14,17 @@ import FSFilesAdapter from '@parse/fs-files-adapter'; import { app as customRoute } from './cloud/customRoute/customApp.js'; import { exec } from 'child_process'; import { createTransport } from 'nodemailer'; -import { appName, cloudServerUrl, serverAppId, smtpenable, smtpsecure, useLocal } from './Utils.js'; +import { + appName, + cloudServerUrl, + formatFromHeader, + getResolvedMailSender, + getSmtpEnvelopeFrom, + serverAppId, + smtpenable, + smtpsecure, + useLocal, +} from './Utils.js'; import { SSOAuth } from './auth/authadapter.js'; import runDbMigrations from './migrationdb/index.js'; import { validateSignedLocalUrl } from './cloud/parsefunction/getSignedUrl.js'; @@ -60,6 +70,8 @@ if (useLocal !== 'true') { let transporterMail; let mailgunClient; let mailgunDomain; +let senderAddress; +let smtpEnvelopeFrom; let isMailAdapter = false; if (smtpenable) { try { @@ -69,15 +81,11 @@ if (smtpenable) { secure: smtpsecure, }; - // ✅ Add auth only if BOTH username & password exist - const smtpUser = process.env.SMTP_USERNAME; + const smtpUser = process.env.SMTP_USERNAME || process.env.SMTP_USER_EMAIL; const smtpPass = process.env.SMTP_PASS; if (smtpUser && smtpPass) { - transporterConfig.auth = { - user: process.env.SMTP_USERNAME ? process.env.SMTP_USERNAME : process.env.SMTP_USER_EMAIL, - pass: smtpPass, - }; + transporterConfig.auth = { user: smtpUser, pass: smtpPass }; } transporterMail = createTransport(transporterConfig); await transporterMail.verify(); @@ -100,7 +108,19 @@ if (smtpenable) { console.log('Please provide valid Mailgun credentials'); } } -const mailsender = smtpenable ? process.env.SMTP_USER_EMAIL : process.env.MAILGUN_SENDER; +if (isMailAdapter) { + try { + const resolvedSender = getResolvedMailSender({ isSmtp: smtpenable }); + senderAddress = formatFromHeader(appName, resolvedSender); + if (smtpenable) { + smtpEnvelopeFrom = getSmtpEnvelopeFrom(resolvedSender); + } + } catch (err) { + isMailAdapter = false; + console.log(`Please provide valid sender configuration: ${err.message}`); + } +} + export const config = { databaseURI: process.env.DATABASE_URI || process.env.MONGODB_URI || 'mongodb://localhost:27017/dev', @@ -129,7 +149,7 @@ export const config = { module: 'parse-server-api-mail-adapter', options: { // The email address from which emails are sent. - sender: appName + ' <' + mailsender + '>', + sender: senderAddress, // The email templates. templates: { // The template used by Parse Server to send an email for password @@ -151,7 +171,13 @@ export const config = { if (mailgunClient) { const mailgunPayload = ApiPayloadConverter.mailgun(payload); await mailgunClient.messages.create(mailgunDomain, mailgunPayload); - } else if (transporterMail) await transporterMail.sendMail(payload); + } else if (transporterMail) { + const smtpPayload = + smtpenable && smtpEnvelopeFrom + ? { ...payload, envelope: { from: smtpEnvelopeFrom } } + : payload; + await transporterMail.sendMail(smtpPayload); + } }, }, }, From 837ab451b159cfc6fb03a7928f2b03ed7d3c2922 Mon Sep 17 00:00:00 2001 From: Denis Marinov Date: Thu, 12 Feb 2026 18:24:58 +0200 Subject: [PATCH 05/21] bugfix: docx file upload --- apps/OpenSignServer/cloud/customRoute/docxtopdf.js | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/OpenSignServer/cloud/customRoute/docxtopdf.js b/apps/OpenSignServer/cloud/customRoute/docxtopdf.js index 9617582f7f..c5f24c34e8 100644 --- a/apps/OpenSignServer/cloud/customRoute/docxtopdf.js +++ b/apps/OpenSignServer/cloud/customRoute/docxtopdf.js @@ -147,6 +147,7 @@ export default async function docxtopdf(req, res) { // ---- DOCX -> PDF conversion with concurrency control and timeout ---- const fileName = `${generatePdfName(16)}.pdf`; + const uploadedSizeBytes = Number(req.file.size || req.file.buffer?.length || 0); // Adjust timeout based on file size const timeoutMs = uploadedSizeBytes > 10 * 1024 * 1024 ? 120_000 : 90_000; From 6809780bd8dbba4990ce4d3d261154e6ff5c2ea0 Mon Sep 17 00:00:00 2001 From: damarinov Date: Mon, 16 Feb 2026 15:04:27 +0200 Subject: [PATCH 06/21] feat: Implement global email branding functionality (#4) - Added a new mailcatcher service to docker-compose for testing email delivery. - Introduced global email branding configuration, allowing customization of email logos, colors, footers, and wrappers. - Updated email sending functions to apply branding settings dynamically. - Created utility functions for fetching and updating email branding configurations. - Enhanced email templates to support branding tokens for consistent styling across all email communications. - Documented the new email branding feature in EMAIL_BRANDING_EXTENSION.md. --- .../preferences/MailTemplateEditor.jsx | 170 ++++++++++++++++++ apps/OpenSign/src/constant/Utils.js | 19 +- apps/OpenSign/src/pages/PdfRequestFiles.jsx | 5 +- .../src/reports/document/DocumentsReport.jsx | 5 +- .../src/reports/template/TemplatesReport.jsx | 5 +- apps/OpenSignServer/Utils.js | 113 ++++++++++-- .../customRoute/deleteAccount/deleteUtils.js | 41 +---- apps/OpenSignServer/cloud/main.js | 4 + .../cloud/parsefunction/ForwardDoc.js | 12 +- .../cloud/parsefunction/SendMailOTPv1.js | 4 +- .../cloud/parsefunction/createBatchDocs.js | 9 +- .../cloud/parsefunction/declinedocument.js | 13 +- .../parsefunction/getGlobalEmailBranding.js | 38 ++++ .../cloud/parsefunction/pdf/PDF.js | 26 ++- .../cloud/parsefunction/sendMailv3.js | 23 ++- .../updateGlobalEmailBranding.js | 46 +++++ .../files/email_brand_wrapper.html | 37 ++++ apps/OpenSignServer/index.js | 24 ++- docker-compose.yml | 8 + docs/EMAIL_BRANDING_EXTENSION.md | 99 ++++++++++ 20 files changed, 596 insertions(+), 105 deletions(-) create mode 100644 apps/OpenSignServer/cloud/parsefunction/getGlobalEmailBranding.js create mode 100644 apps/OpenSignServer/cloud/parsefunction/updateGlobalEmailBranding.js create mode 100644 apps/OpenSignServer/files/email_brand_wrapper.html create mode 100644 docs/EMAIL_BRANDING_EXTENSION.md diff --git a/apps/OpenSign/src/components/preferences/MailTemplateEditor.jsx b/apps/OpenSign/src/components/preferences/MailTemplateEditor.jsx index fd8ea5a6e1..a6510013e4 100644 --- a/apps/OpenSign/src/components/preferences/MailTemplateEditor.jsx +++ b/apps/OpenSign/src/components/preferences/MailTemplateEditor.jsx @@ -33,6 +33,13 @@ const MailTemplateEditor = ({ request: false, completion: false }); + const [brandingConfig, setBrandingConfig] = useState({ + logoUrl: "", + primaryColor: "#47a3ad", + footerText: "", + wrapperHtml: "" + }); + const [isBrandingLoader, setIsBrandingLoader] = useState(false); const [isalert, setIsAlert] = useState({ type: "success", msg: "" }); const [editorType, setEditorType] = useState({ request: "basic", @@ -47,6 +54,7 @@ const MailTemplateEditor = ({ useEffect(() => { fetchSubscription(); + fetchGlobalBrandingConfig(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [ info @@ -61,6 +69,28 @@ const MailTemplateEditor = ({ await tenantEmailTemplate(info); }; + const fetchGlobalBrandingConfig = withSessionValidation(async () => { + setIsBrandingLoader(true); + try { + const brandingRes = await Parse.Cloud.run("getglobalemailbranding"); + if (brandingRes) { + const parsed = JSON.parse(JSON.stringify(brandingRes)); + setBrandingConfig({ + logoUrl: parsed?.logoUrl || "", + primaryColor: parsed?.primaryColor || "#47a3ad", + footerText: parsed?.footerText || "", + wrapperHtml: parsed?.wrapperHtml || "" + }); + } + } catch (err) { + console.error("Error while fetching global email branding: ", err); + setIsAlert({ type: "danger", msg: t("something-went-wrong-mssg") }); + setTimeout(() => setIsAlert({ type: "", msg: "" }), 1500); + } finally { + setIsBrandingLoader(false); + } + }); + const tenantEmailTemplate = async (tenantRes) => { if (tenantRes === "user does not exist!") { alert(t("user-not-exist")); @@ -309,6 +339,54 @@ const MailTemplateEditor = ({ setEditorType((p) => ({ ...p, [flow]: editor })); }; + const handleBrandingInput = (field, value) => { + setBrandingConfig((prev) => ({ ...prev, [field]: value })); + }; + + const applyBrandingPreview = () => { + const wrapper = brandingConfig?.wrapperHtml || ""; + if (!wrapper) { + return ""; + } + return wrapper + .replaceAll("__APP_NAME__", appName) + .replaceAll("__LOGO_URL__", brandingConfig?.logoUrl || "") + .replaceAll("__PRIMARY_COLOR__", brandingConfig?.primaryColor || "#47a3ad") + .replaceAll("__HEADER_TEXT__", brandingConfig?.headerText || "Digital Signature Request") + .replaceAll( + "__FOOTER_TEXT__", + brandingConfig?.footerText || `This is an automated email from ${appName}.` + ) + .replaceAll( + "__EMAIL_BODY__", + "

Hello, this is a preview of your branded email wrapper.

The request/completion content will be injected here.

" + ); + }; + + const handleSaveBrandingConfig = withSessionValidation(async (e) => { + e.preventDefault(); + setIsBrandingLoader(true); + try { + const updateRes = await Parse.Cloud.run("updateglobalemailbranding", { + details: { + logoUrl: brandingConfig?.logoUrl || "", + primaryColor: brandingConfig?.primaryColor || "#47a3ad", + footerText: brandingConfig?.footerText || "", + wrapperHtml: brandingConfig?.wrapperHtml || "" + } + }); + if (updateRes) { + setIsAlert({ type: "success", msg: t("saved-successfully") }); + setTimeout(() => setIsAlert({ type: "", msg: "" }), 1500); + } + } catch (err) { + console.error("Error while saving global email branding: ", err); + setIsAlert({ type: "danger", msg: t("something-went-wrong-mssg") }); + setTimeout(() => setIsAlert({ type: "", msg: "" }), 1500); + } finally { + setIsBrandingLoader(false); + } + }); return ( <> {isalert.msg && {isalert.msg}} @@ -488,6 +566,98 @@ const MailTemplateEditor = ({ +

+ Global Email Branding +

+
+ {isBrandingLoader && ( +
+ +
+ )} +
+

+ Applies globally to request and completion emails. +

+
+
+ + handleBrandingInput("logoUrl", e.target.value)} + className="w-full op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content text-xs" + placeholder="https://example.com/logo.png" + /> +
+
+ +
+ handleBrandingInput("primaryColor", e.target.value)} + className="op-input op-input-bordered op-input-sm h-[34px] w-[52px] p-1" + /> + handleBrandingInput("primaryColor", e.target.value)} + className="w-full op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content text-xs" + placeholder="#47a3ad" + /> +
+
+
+
+ +