fix: email smtp auth mode (#4571)

Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
This commit is contained in:
Piyush Gupta
2025-01-10 15:00:28 +05:30
committed by GitHub
parent 5970ff917f
commit 9905199055
13 changed files with 117 additions and 59 deletions
+3
View File
@@ -46,6 +46,9 @@ SMTP_SECURE_ENABLED=0
SMTP_USER=smtpUser
SMTP_PASSWORD=smtpPassword
# If set to 0, the server will not require SMTP_USER and SMTP_PASSWORD(default is 1)
# SMTP_AUTHENTICATED=
# If set to 0, the server will accept connections without requiring authorization from the list of supplied CAs (default is 1).
# SMTP_REJECT_UNAUTHORIZED_TLS=0
@@ -54,6 +54,7 @@ These variables are present inside your machines docker-compose file. Restart
| SMTP_PORT | Host Port of your SMTP server. | optional (required if email services are to be enabled) | |
| SMTP_USER | Username for your SMTP Server. | optional (required if email services are to be enabled) | |
| SMTP_PASSWORD | Password for your SMTP Server. | optional (required if email services are to be enabled) | |
| SMTP_AUTHENTICATED | If set to 0, the server will not require SMTP_USER and SMTP_PASSWORD(default is 1) | optional | |
| SMTP_SECURE_ENABLED | SMTP secure connection. For using TLS, set to 1 else to 0. | optional (required if email services are to be enabled) | |
| SMTP_REJECT_UNAUTHORIZED_TLS | If set to 0, the server will accept connections without requiring authorization from the list of supplied CAs. | optional | 1 |
| TURNSTILE_SITE_KEY | Site key for Turnstile. | optional | |
@@ -1,5 +1,6 @@
"use client";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { Button } from "@/modules/ui/components/button";
import { CodeBlock } from "@/modules/ui/components/code-block";
import { LoadingSpinner } from "@/modules/ui/components/loading-spinner";
@@ -38,8 +39,13 @@ export const EmailTab = ({ surveyId, email }: EmailTabProps) => {
const sendPreviewEmail = async () => {
try {
await sendEmbedSurveyPreviewEmailAction({ surveyId });
toast.success(t("environments.surveys.summary.email_sent"));
const val = await sendEmbedSurveyPreviewEmailAction({ surveyId });
if (val?.data) {
toast.success(t("environments.surveys.summary.email_sent"));
} else {
const errorMessage = getFormattedErrorMessage(val);
toast.error(errorMessage);
}
} catch (err) {
if (err instanceof AuthenticationError) {
toast.error(t("common.not_authenticated"));
@@ -1,6 +1,7 @@
"use client";
import { inviteOrganizationMemberAction } from "@/app/setup/organization/[organizationId]/invite/actions";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { Alert, AlertDescription, AlertTitle } from "@/modules/ui/components/alert";
import { Button } from "@/modules/ui/components/button";
import { FormControl, FormError, FormField, FormItem, FormProvider } from "@/modules/ui/components/form";
@@ -34,13 +35,16 @@ export const InviteMembers = ({ IS_SMTP_CONFIGURED, organizationId }: InviteMemb
for (const member of Object.values(data)) {
try {
if (!member.email) continue;
await inviteOrganizationMemberAction({
const inviteResponse = await inviteOrganizationMemberAction({
email: member.email.toLowerCase(),
name: member.name,
organizationId,
});
if (IS_SMTP_CONFIGURED) {
if (inviteResponse?.data) {
toast.success(`${t("setup.invite.invitation_sent_to")} ${member.email}!`);
} else {
const errorMessage = getFormattedErrorMessage(inviteResponse);
toast.error(errorMessage);
}
} catch (error) {
toast.error(`${t("setup.invite.failed_to_invite")} ${member.email}.`);
@@ -20,6 +20,5 @@ export const resendVerificationEmailAction = actionClient
if (user.emailVerified) {
throw new InvalidInputError("Email address has already been verified");
}
await sendVerificationEmail(user);
return { success: true };
return await sendVerificationEmail(user);
});
+47 -35
View File
@@ -5,6 +5,7 @@ import type SMTPTransport from "nodemailer/lib/smtp-transport";
import {
DEBUG,
MAIL_FROM,
SMTP_AUTHENTICATED,
SMTP_HOST,
SMTP_PASSWORD,
SMTP_PORT,
@@ -16,6 +17,7 @@ import {
import { createInviteToken, createToken, createTokenForLinkSurvey } from "@formbricks/lib/jwt";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import type { TLinkSurveyEmailData } from "@formbricks/types/email";
import { InvalidInputError } from "@formbricks/types/errors";
import type { TResponse } from "@formbricks/types/responses";
import type { TSurvey } from "@formbricks/types/surveys/types";
import { TUserEmail, TUserLocale } from "@formbricks/types/user";
@@ -48,27 +50,37 @@ const getEmailSubject = (projectName: string): string => {
return `${projectName} User Insights - Last Week by Formbricks`;
};
export const sendEmail = async (emailData: SendEmailDataProps): Promise<void> => {
if (!IS_SMTP_CONFIGURED) return;
export const sendEmail = async (emailData: SendEmailDataProps): Promise<boolean> => {
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 transporter = createTransport({
host: SMTP_HOST,
port: SMTP_PORT,
secure: SMTP_SECURE_ENABLED, // true for 465, false for other ports
auth: {
user: SMTP_USER,
pass: SMTP_PASSWORD,
},
tls: {
rejectUnauthorized: SMTP_REJECT_UNAUTHORIZED_TLS,
},
logger: DEBUG,
debug: DEBUG,
} as SMTPTransport.Options);
const emailDefaults = {
from: `Formbricks <${MAIL_FROM ?? "noreply@formbricks.com"}>`,
};
await transporter.sendMail({ ...emailDefaults, ...emailData });
const emailDefaults = {
from: `Formbricks <${MAIL_FROM ?? "noreply@formbricks.com"}>`,
};
await transporter.sendMail({ ...emailDefaults, ...emailData });
return true;
} catch (error) {
throw new InvalidInputError("Incorrect SMTP credentials");
}
};
export const sendVerificationEmail = async ({
@@ -79,14 +91,14 @@ export const sendVerificationEmail = async ({
id: string;
email: TUserEmail;
locale: TUserLocale;
}): Promise<void> => {
}): Promise<boolean> => {
const token = createToken(id, email, {
expiresIn: "1d",
});
const verifyLink = `${WEBAPP_URL}/auth/verify?token=${encodeURIComponent(token)}`;
const verificationRequestLink = `${WEBAPP_URL}/auth/verification-requested?token=${encodeURIComponent(token)}`;
const html = await render(VerificationEmail({ verificationRequestLink, verifyLink, locale }));
await sendEmail({
return await sendEmail({
to: email,
subject: translateEmailText("verification_email_subject", locale),
html,
@@ -97,13 +109,13 @@ export const sendForgotPasswordEmail = async (user: {
id: string;
email: TUserEmail;
locale: TUserLocale;
}): Promise<void> => {
}): Promise<boolean> => {
const token = createToken(user.id, user.email, {
expiresIn: "1d",
});
const verifyLink = `${WEBAPP_URL}/auth/forgot-password/reset?token=${encodeURIComponent(token)}`;
const html = await render(ForgotPasswordEmail({ verifyLink, locale: user.locale }));
await sendEmail({
return await sendEmail({
to: user.email,
subject: "Reset your Formbricks password",
html,
@@ -113,9 +125,9 @@ export const sendForgotPasswordEmail = async (user: {
export const sendPasswordResetNotifyEmail = async (user: {
email: string;
locale: TUserLocale;
}): Promise<void> => {
}): Promise<boolean> => {
const html = await render(PasswordResetNotifyEmail({ locale: user.locale }));
await sendEmail({
return await sendEmail({
to: user.email,
subject: "Your Formbricks password has been changed",
html,
@@ -130,7 +142,7 @@ export const sendInviteMemberEmail = async (
isOnboardingInvite?: boolean,
inviteMessage?: string,
locale = "en-US"
): Promise<void> => {
): Promise<boolean> => {
const token = createInviteToken(inviteId, email, {
expiresIn: "7d",
});
@@ -141,14 +153,14 @@ export const sendInviteMemberEmail = async (
const html = await render(
OnboardingInviteEmail({ verifyLink, inviteMessage, inviterName, locale, inviteeName })
);
await sendEmail({
return await sendEmail({
to: email,
subject: `${inviterName} needs a hand setting up Formbricks. Can you help out?`,
html,
});
} else {
const html = await render(InviteEmail({ inviteeName, inviterName, verifyLink, locale }));
await sendEmail({
return await sendEmail({
to: email,
subject: `You're invited to collaborate on Formbricks!`,
html,
@@ -214,9 +226,9 @@ export const sendEmbedSurveyPreviewEmail = async (
environmentId: string,
locale: string,
logoUrl?: string
): Promise<void> => {
): Promise<boolean> => {
const html = await render(EmbedSurveyPreviewEmail({ html: innerHtml, environmentId, locale, logoUrl }));
await sendEmail({
return await sendEmail({
to,
subject,
html,
@@ -229,17 +241,17 @@ export const sendEmailCustomizationPreviewEmail = async (
userName: string,
locale: string,
logoUrl?: string
): Promise<void> => {
): Promise<boolean> => {
const emailHtmlBody = await render(EmailCustomizationPreviewEmail({ userName, locale, logoUrl }));
await sendEmail({
return await sendEmail({
to,
subject,
html: emailHtmlBody,
});
};
export const sendLinkSurveyToVerifiedEmail = async (data: TLinkSurveyEmailData): Promise<void> => {
export const sendLinkSurveyToVerifiedEmail = async (data: TLinkSurveyEmailData): Promise<boolean> => {
const surveyId = data.surveyId;
const email = data.email;
const surveyName = data.surveyName;
@@ -256,7 +268,7 @@ export const sendLinkSurveyToVerifiedEmail = async (data: TLinkSurveyEmailData):
const surveyLink = getSurveyLink();
const html = await render(LinkSurveyEmail({ surveyName, surveyLink, locale, logoUrl }));
await sendEmail({
return await sendEmail({
to: data.email,
subject: "Your survey is ready to be filled out.",
html,
@@ -94,8 +94,16 @@ export const MemberActions = ({ organization, member, invite, showDeleteButton }
try {
if (!invite) return;
await resendInviteAction({ inviteId: invite.id, organizationId: organization.id });
toast.success(t("environments.settings.general.invitation_sent_once_more"));
const resendInviteResponse = await resendInviteAction({
inviteId: invite.id,
organizationId: organization.id,
});
if (resendInviteResponse?.data) {
toast.success(t("environments.settings.general.invitation_sent_once_more"));
} else {
const errorMessage = getFormattedErrorMessage(resendInviteResponse);
toast.error(errorMessage);
}
} catch (err) {
toast.error(`${t("common.error")}: ${err.message}`);
}
+1
View File
@@ -40,6 +40,7 @@ x-environment: &environment
# SMTP_PORT:
# SMTP_USER:
# SMTP_PASSWORD:
# SMTP_AUTHENTICATED:
# (Additional option for TLS (port 465) only)
# SMTP_SECURE_ENABLED: 1
+5
View File
@@ -235,6 +235,9 @@ EOT
echo -n "Enter your SMTP password: "
read smtp_password
echo -n "Enable Authenticated SMTP? Enter 1 for yes and 0 for no(default is 1): "
read smtp_authenticated
echo -n "Enable Secure SMTP (use SSL)? Enter 1 for yes and 0 for no: "
read smtp_secure_enabled
@@ -245,6 +248,7 @@ EOT
smtp_port=""
smtp_user=""
smtp_password=""
smtp_authenticated=1
smtp_secure_enabled=0
fi
@@ -271,6 +275,7 @@ EOT
sed -i "s|# SMTP_SECURE_ENABLED:|SMTP_SECURE_ENABLED: $smtp_secure_enabled|" docker-compose.yml
sed -i "s|# SMTP_USER:|SMTP_USER: \"$smtp_user\"|" docker-compose.yml
sed -i "s|# SMTP_PASSWORD:|SMTP_PASSWORD: \"$smtp_password\"|" docker-compose.yml
sed -i "s|# SMTP_AUTHENTICATED:|SMTP_AUTHENTICATED: $smtp_authenticated|" docker-compose.yml
fi
awk -v domain_name="$domain_name" -v hsts_enabled="$hsts_enabled" '
+30 -15
View File
@@ -19,20 +19,34 @@ Harvest user-insights, build irresistible experiences.
# Formbricks Helm Chart: Comprehensive Documentation
1. [Introduction](#introduction)
2. [Prerequisites](#prerequisites)
3. [Chart Components](#chart-components)
4. [Installation](#installation)
- [Quick Start](#quick-start)
- [Usage Examples](#usage-examples)
5. [Configuration](#configuration)
6. [Environment Variables](#environment-variables)
7. [Scaling](#scaling)
8. [Upgrading Formbricks](#upgrading-formbricks)
9. [Support](#support)
10. [Full Values Documentation](#full-values-documentation)
11. [Contribution](#contribution)
12. [MicroK8s Installation and Formbricks Deployment](#microk8s-installation-and-formbricks-deployment)
- [Formbricks Helm Chart: Comprehensive Documentation](#formbricks-helm-chart-comprehensive-documentation)
- [Introduction](#introduction)
- [Prerequisites](#prerequisites)
- [Chart Components](#chart-components)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Usage Examples](#usage-examples)
- [Scaling PostgreSQL and Redis](#scaling-postgresql-and-redis)
- [Configuration](#configuration)
- [Environment Variables](#environment-variables)
- [Scaling](#scaling)
- [With Auto Scaling (Kubernetes Metrics Server Requirement)](#with-auto-scaling-kubernetes-metrics-server-requirement)
- [Customizing Autoscaling](#customizing-autoscaling)
- [Kubernetes Metrics Server Requirement](#kubernetes-metrics-server-requirement)
- [Advanced Autoscaling Configuration](#advanced-autoscaling-configuration)
- [Upgrading Formbricks](#upgrading-formbricks)
- [Upgrade Process](#upgrade-process)
- [Common Upgrade Scenarios](#common-upgrade-scenarios)
- [1. Updating Environment Variables](#1-updating-environment-variables)
- [2. Enabling or Disabling Features](#2-enabling-or-disabling-features)
- [3. Scaling Resources](#3-scaling-resources)
- [4. Updating Autoscaling Configuration](#4-updating-autoscaling-configuration)
- [5. Changing Database Credentials](#5-changing-database-credentials)
- [Using a Values File for Complex Upgrades](#using-a-values-file-for-complex-upgrades)
- [Support](#support)
- [Full Values Documentation](#full-values-documentation)
- [✍️ Contribution](#-contribution)
- [MicroK8s Installation and Formbricks Deployment](#microk8s-installation-and-formbricks-deployment)
- [MicroK8s Quick Setup](#microk8s-quick-setup)
- [Deploying Formbricks on MicroK8s](#deploying-formbricks-on-microk8s)
@@ -206,7 +220,8 @@ These documents provide detailed information on scaling and configuring high ava
--set env.SMTP_HOST=smtp.example.com \
--set env.SMTP_PORT=587 \
--set env.SMTP_USER=user@example.com \
--set env.SMTP_PASSWORD=password123
--set env.SMTP_PASSWORD=password123 \
--set env.SMTP_AUTHENTICATED=1
```
5. **Installation with Custom Resource Limits**:
+1
View File
@@ -72,6 +72,7 @@ export const SMTP_PORT = env.SMTP_PORT;
export const SMTP_SECURE_ENABLED = env.SMTP_SECURE_ENABLED === "1";
export const SMTP_USER = env.SMTP_USER;
export const SMTP_PASSWORD = env.SMTP_PASSWORD;
export const SMTP_AUTHENTICATED = env.SMTP_AUTHENTICATED !== "0";
export const SMTP_REJECT_UNAUTHORIZED_TLS = env.SMTP_REJECT_UNAUTHORIZED_TLS !== "0";
export const MAIL_FROM = env.MAIL_FROM;
+3 -1
View File
@@ -78,10 +78,11 @@ export const env = createEnv({
SLACK_CLIENT_ID: z.string().optional(),
SLACK_CLIENT_SECRET: z.string().optional(),
SMTP_HOST: z.string().min(1).optional(),
SMTP_PASSWORD: z.string().min(1).optional(),
SMTP_PORT: z.string().min(1).optional(),
SMTP_SECURE_ENABLED: z.enum(["1", "0"]).optional(),
SMTP_USER: z.string().min(1).optional(),
SMTP_PASSWORD: z.string().min(1).optional(),
SMTP_AUTHENTICATED: z.enum(["1", "0"]).optional(),
SMTP_REJECT_UNAUTHORIZED_TLS: z.enum(["1", "0"]).optional(),
STRIPE_SECRET_KEY: z.string().optional(),
STRIPE_WEBHOOK_SECRET: z.string().optional(),
@@ -206,6 +207,7 @@ export const env = createEnv({
SMTP_SECURE_ENABLED: process.env.SMTP_SECURE_ENABLED,
SMTP_USER: process.env.SMTP_USER,
SMTP_REJECT_UNAUTHORIZED_TLS: process.env.SMTP_REJECT_UNAUTHORIZED_TLS,
SMTP_AUTHENTICATED: process.env.SMTP_AUTHENTICATED,
STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY,
STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET,
TELEMETRY_DISABLED: process.env.TELEMETRY_DISABLED,
+1
View File
@@ -166,6 +166,7 @@
"SMTP_SECURE_ENABLED",
"SMTP_USER",
"SMTP_REJECT_UNAUTHORIZED_TLS",
"SMTP_AUTHENTICATED",
"STRAPI_API_KEY",
"STRIPE_SECRET_KEY",
"STRIPE_WEBHOOK_SECRET",