mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-07 11:20:39 -05:00
368 lines
10 KiB
TypeScript
368 lines
10 KiB
TypeScript
import { createTransport } from "nodemailer";
|
|
import type SMTPTransport from "nodemailer/lib/smtp-transport";
|
|
import {
|
|
renderEmailCustomizationPreviewEmail,
|
|
renderEmbedSurveyPreviewEmail,
|
|
renderForgotPasswordEmail,
|
|
renderInviteAcceptedEmail,
|
|
renderInviteEmail,
|
|
renderLinkSurveyEmail,
|
|
renderNewEmailVerification,
|
|
renderPasswordResetNotifyEmail,
|
|
renderResponseFinishedEmail,
|
|
renderVerificationEmail,
|
|
} from "@formbricks/email";
|
|
import { TEmailTemplateLegalProps } from "@formbricks/email/src/types/email";
|
|
import { logger } from "@formbricks/logger";
|
|
import type { TLinkSurveyEmailData } from "@formbricks/types/email";
|
|
import { InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors";
|
|
import type { TResponse } from "@formbricks/types/responses";
|
|
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/elements";
|
|
import type { TSurvey } from "@formbricks/types/surveys/types";
|
|
import { TUserEmail, TUserLocale } from "@formbricks/types/user";
|
|
import {
|
|
DEBUG,
|
|
IMPRINT_ADDRESS,
|
|
IMPRINT_URL,
|
|
MAIL_FROM,
|
|
MAIL_FROM_NAME,
|
|
PRIVACY_URL,
|
|
SMTP_AUTHENTICATED,
|
|
SMTP_HOST,
|
|
SMTP_PASSWORD,
|
|
SMTP_PORT,
|
|
SMTP_REJECT_UNAUTHORIZED_TLS,
|
|
SMTP_SECURE_ENABLED,
|
|
SMTP_USER,
|
|
TERMS_URL,
|
|
WEBAPP_URL,
|
|
} from "@/lib/constants";
|
|
import { getPublicDomain } from "@/lib/getPublicUrl";
|
|
import { createEmailChangeToken, createInviteToken, createToken, createTokenForLinkSurvey } from "@/lib/jwt";
|
|
import { getOrganizationByWorkspaceId } from "@/lib/organization/service";
|
|
import { getElementResponseMapping } from "@/lib/responses";
|
|
import { getTranslate } from "@/lingodotdev/server";
|
|
import { buildVerificationLinks } from "@/modules/auth/lib/verification-links";
|
|
import { resolveStorageUrl } from "@/modules/storage/utils";
|
|
|
|
export const IS_SMTP_CONFIGURED = Boolean(SMTP_HOST && SMTP_PORT);
|
|
|
|
const legalProps: TEmailTemplateLegalProps = {
|
|
privacyUrl: PRIVACY_URL || undefined,
|
|
termsUrl: TERMS_URL || undefined,
|
|
imprintUrl: IMPRINT_URL || undefined,
|
|
imprintAddress: IMPRINT_ADDRESS || undefined,
|
|
};
|
|
|
|
interface SendEmailDataProps {
|
|
to: string;
|
|
replyTo?: string;
|
|
subject: string;
|
|
text?: string;
|
|
html: string;
|
|
}
|
|
|
|
export const sendEmail = async (emailData: SendEmailDataProps): Promise<boolean> => {
|
|
if (!IS_SMTP_CONFIGURED) {
|
|
logger.info("SMTP is not configured, skipping email sending");
|
|
return false;
|
|
}
|
|
try {
|
|
const transporter = createTransport({
|
|
host: SMTP_HOST,
|
|
port: SMTP_PORT,
|
|
secure: SMTP_SECURE_ENABLED, // true for 465, false for other ports
|
|
...(SMTP_AUTHENTICATED
|
|
? {
|
|
auth: {
|
|
type: "LOGIN",
|
|
user: SMTP_USER,
|
|
pass: SMTP_PASSWORD,
|
|
},
|
|
}
|
|
: {}),
|
|
tls: {
|
|
rejectUnauthorized: SMTP_REJECT_UNAUTHORIZED_TLS,
|
|
},
|
|
logger: DEBUG,
|
|
debug: DEBUG,
|
|
} as SMTPTransport.Options);
|
|
|
|
const emailDefaults = {
|
|
from: `${MAIL_FROM_NAME ?? "Formbricks"} <${MAIL_FROM ?? "noreply@formbricks.com"}>`,
|
|
};
|
|
await transporter.sendMail({ ...emailDefaults, ...emailData });
|
|
|
|
return true;
|
|
} catch (error) {
|
|
logger.error(error, "Error in sendEmail");
|
|
throw new InvalidInputError("Incorrect SMTP credentials");
|
|
}
|
|
};
|
|
|
|
export const sendVerificationNewEmail = async (
|
|
id: string,
|
|
email: string,
|
|
locale: TUserLocale
|
|
): Promise<boolean> => {
|
|
try {
|
|
const t = await getTranslate(locale);
|
|
const token = createEmailChangeToken(id, email);
|
|
const verifyLink = `${WEBAPP_URL}/verify-email-change?token=${encodeURIComponent(token)}`;
|
|
|
|
const html = await renderNewEmailVerification({ verifyLink, t, ...legalProps });
|
|
|
|
return await sendEmail({
|
|
to: email,
|
|
subject: t("emails.verification_new_email_subject"),
|
|
html,
|
|
});
|
|
} catch (error) {
|
|
logger.error(error, "Error in sendVerificationNewEmail");
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
export const sendVerificationEmail = async ({
|
|
id,
|
|
email,
|
|
locale,
|
|
callbackUrl,
|
|
}: {
|
|
id: string;
|
|
email: TUserEmail;
|
|
locale: TUserLocale;
|
|
callbackUrl?: string;
|
|
}): Promise<boolean> => {
|
|
try {
|
|
const t = await getTranslate(locale);
|
|
const token = createToken(id, {
|
|
expiresIn: "1d",
|
|
});
|
|
const { verifyLink, verificationRequestLink } = buildVerificationLinks({
|
|
token,
|
|
webAppUrl: WEBAPP_URL,
|
|
callbackUrl,
|
|
});
|
|
|
|
const html = await renderVerificationEmail({
|
|
verificationRequestLink,
|
|
verifyLink,
|
|
t,
|
|
...legalProps,
|
|
});
|
|
|
|
return await sendEmail({
|
|
to: email,
|
|
subject: t("emails.verification_email_subject"),
|
|
html,
|
|
});
|
|
} catch (error) {
|
|
logger.error(error, "Error in sendVerificationEmail");
|
|
throw error; // Re-throw the error to maintain the original behavior
|
|
}
|
|
};
|
|
|
|
export const sendPasswordResetLinkEmail = async (user: {
|
|
email: TUserEmail;
|
|
locale: TUserLocale;
|
|
verifyLink: string;
|
|
linkValidityInMinutes: number;
|
|
}): Promise<boolean> => {
|
|
const t = await getTranslate(user.locale);
|
|
const html = await renderForgotPasswordEmail({
|
|
verifyLink: user.verifyLink,
|
|
linkValidityInMinutes: user.linkValidityInMinutes,
|
|
t,
|
|
...legalProps,
|
|
});
|
|
return await sendEmail({
|
|
to: user.email,
|
|
subject: t("emails.forgot_password_email_subject"),
|
|
html,
|
|
});
|
|
};
|
|
|
|
export const sendPasswordResetNotifyEmail = async (user: {
|
|
email: string;
|
|
locale: TUserLocale;
|
|
}): Promise<boolean> => {
|
|
const t = await getTranslate(user.locale);
|
|
const html = await renderPasswordResetNotifyEmail({ t, ...legalProps });
|
|
return await sendEmail({
|
|
to: user.email,
|
|
subject: t("emails.password_reset_notify_email_subject"),
|
|
html,
|
|
});
|
|
};
|
|
|
|
export const sendInviteMemberEmail = async (
|
|
inviteId: string,
|
|
email: string,
|
|
inviterName: string,
|
|
inviteeName: string
|
|
): Promise<boolean> => {
|
|
const token = createInviteToken(inviteId, email, {
|
|
expiresIn: "7d",
|
|
});
|
|
const t = await getTranslate();
|
|
|
|
const verifyLink = `${WEBAPP_URL}/invite?token=${encodeURIComponent(token)}`;
|
|
|
|
const html = await renderInviteEmail({ inviteeName, inviterName, verifyLink, t, ...legalProps });
|
|
return await sendEmail({
|
|
to: email,
|
|
subject: t("emails.invite_member_email_subject"),
|
|
html,
|
|
});
|
|
};
|
|
|
|
export const sendInviteAcceptedEmail = async (
|
|
inviterName: string,
|
|
inviteeName: string,
|
|
email: string,
|
|
inviterLocale?: TUserLocale
|
|
): Promise<void> => {
|
|
const t = await getTranslate(inviterLocale);
|
|
const html = await renderInviteAcceptedEmail({ inviteeName, inviterName, t, ...legalProps });
|
|
await sendEmail({
|
|
to: email,
|
|
subject: t("emails.invite_accepted_email_subject"),
|
|
html,
|
|
});
|
|
};
|
|
|
|
export const sendResponseFinishedEmail = async (
|
|
email: string,
|
|
locale: TUserLocale,
|
|
workspaceId: string,
|
|
survey: TSurvey,
|
|
response: TResponse,
|
|
responseCount: number
|
|
): Promise<void> => {
|
|
const t = await getTranslate(locale);
|
|
const personEmail = response.contactAttributes?.email;
|
|
const organization = await getOrganizationByWorkspaceId(workspaceId);
|
|
|
|
if (!organization) {
|
|
throw new ResourceNotFoundError("Organization", null);
|
|
}
|
|
|
|
// Pre-process the element response mapping before passing to email
|
|
const elements = getElementResponseMapping(survey, response);
|
|
|
|
// Resolve relative storage URLs to absolute URLs for email rendering
|
|
const elementsWithResolvedUrls = elements.map((element) => {
|
|
if (
|
|
(element.type === TSurveyElementTypeEnum.PictureSelection ||
|
|
element.type === TSurveyElementTypeEnum.FileUpload) &&
|
|
Array.isArray(element.response)
|
|
) {
|
|
return {
|
|
...element,
|
|
response: element.response.map((url) => resolveStorageUrl(url)),
|
|
};
|
|
}
|
|
|
|
return element;
|
|
});
|
|
|
|
const html = await renderResponseFinishedEmail({
|
|
survey,
|
|
responseCount,
|
|
response,
|
|
WEBAPP_URL,
|
|
workspaceId,
|
|
organization,
|
|
elements: elementsWithResolvedUrls,
|
|
t,
|
|
...legalProps,
|
|
});
|
|
|
|
await sendEmail({
|
|
to: email,
|
|
subject: personEmail
|
|
? t("emails.response_finished_email_subject_with_email", {
|
|
personEmail,
|
|
surveyName: survey.name,
|
|
})
|
|
: t("emails.response_finished_email_subject", {
|
|
surveyName: survey.name,
|
|
}),
|
|
replyTo: personEmail?.toString() ?? MAIL_FROM,
|
|
html,
|
|
});
|
|
};
|
|
|
|
export const sendEmbedSurveyPreviewEmail = async (
|
|
to: string,
|
|
innerHtml: string,
|
|
workspaceId: string,
|
|
locale: TUserLocale,
|
|
logoUrl?: string
|
|
): Promise<boolean> => {
|
|
const t = await getTranslate(locale);
|
|
// Resolve relative storage URLs to absolute URLs for email rendering
|
|
const resolvedLogoUrl = logoUrl ? resolveStorageUrl(logoUrl) : undefined;
|
|
const html = await renderEmbedSurveyPreviewEmail({
|
|
html: innerHtml,
|
|
workspaceId,
|
|
logoUrl: resolvedLogoUrl,
|
|
t,
|
|
...legalProps,
|
|
});
|
|
return await sendEmail({
|
|
to,
|
|
subject: t("emails.embed_survey_preview_email_subject"),
|
|
html,
|
|
});
|
|
};
|
|
|
|
export const sendEmailCustomizationPreviewEmail = async (
|
|
to: string,
|
|
userName: string,
|
|
locale: TUserLocale,
|
|
logoUrl?: string
|
|
): Promise<boolean> => {
|
|
const t = await getTranslate(locale);
|
|
// Resolve relative storage URLs to absolute URLs for email rendering
|
|
const resolvedLogoUrl = logoUrl ? resolveStorageUrl(logoUrl) : undefined;
|
|
const emailHtmlBody = await renderEmailCustomizationPreviewEmail({
|
|
userName,
|
|
logoUrl: resolvedLogoUrl,
|
|
t,
|
|
...legalProps,
|
|
});
|
|
|
|
return await sendEmail({
|
|
to,
|
|
subject: t("emails.email_customization_preview_email_subject"),
|
|
html: emailHtmlBody,
|
|
});
|
|
};
|
|
|
|
export const sendLinkSurveyToVerifiedEmail = async (data: TLinkSurveyEmailData): Promise<boolean> => {
|
|
const surveyId = data.surveyId;
|
|
const email = data.email;
|
|
const surveyName = data.surveyName;
|
|
const singleUseId = data.suId;
|
|
// Resolve relative storage URLs to absolute URLs for email rendering
|
|
const logoUrl = data.logoUrl ? resolveStorageUrl(data.logoUrl) : "";
|
|
const token = createTokenForLinkSurvey(surveyId, email);
|
|
const t = await getTranslate(data.locale);
|
|
const getSurveyLink = (): string => {
|
|
if (singleUseId) {
|
|
return `${getPublicDomain()}/s/${surveyId}?verify=${encodeURIComponent(token)}&suId=${singleUseId}`;
|
|
}
|
|
return `${getPublicDomain()}/s/${surveyId}?verify=${encodeURIComponent(token)}`;
|
|
};
|
|
const surveyLink = getSurveyLink();
|
|
|
|
const html = await renderLinkSurveyEmail({ surveyName, surveyLink, logoUrl, t, ...legalProps });
|
|
return await sendEmail({
|
|
to: data.email,
|
|
subject: t("emails.verified_link_survey_email_subject"),
|
|
html,
|
|
});
|
|
};
|