[WEB-4479] feat: enable/disable SMTP configuration (#7393)

* feat: api update instance configuration

* chore: add enable_smtp key

* fix: empty string for enable_smtp key

* chore: update email_port and email_from

* fix: handled smtp enable disable

* fix: error handling

* fix: refactor

* fix: removed enabled toast

* fix: refactor

---------

Co-authored-by: gakshita <akshitagoyal1516@gmail.com>
Co-authored-by: Akshita Goyal <36129505+gakshita@users.noreply.github.com>
This commit is contained in:
Sangeetha 2025-07-16 01:04:18 +05:30 committed by GitHub
parent da5390fa03
commit 99127ff8e4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 155 additions and 24 deletions

View file

@ -49,9 +49,9 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
EMAIL_USE_TLS: config["EMAIL_USE_TLS"], EMAIL_USE_TLS: config["EMAIL_USE_TLS"],
EMAIL_USE_SSL: config["EMAIL_USE_SSL"], EMAIL_USE_SSL: config["EMAIL_USE_SSL"],
EMAIL_FROM: config["EMAIL_FROM"], EMAIL_FROM: config["EMAIL_FROM"],
ENABLE_SMTP: config["ENABLE_SMTP"],
}, },
}); });
const emailFormFields: TControllerInputFormField[] = [ const emailFormFields: TControllerInputFormField[] = [
{ {
key: "EMAIL_HOST", key: "EMAIL_HOST",
@ -101,7 +101,7 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
]; ];
const onSubmit = async (formData: EmailFormValues) => { const onSubmit = async (formData: EmailFormValues) => {
const payload: Partial<EmailFormValues> = { ...formData }; const payload: Partial<EmailFormValues> = { ...formData, ENABLE_SMTP: "1" };
await updateInstanceConfigurations(payload) await updateInstanceConfigurations(payload)
.then(() => .then(() =>

View file

@ -1,8 +1,9 @@
"use client"; "use client";
import { useEffect, useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import useSWR from "swr"; import useSWR from "swr";
import { Loader } from "@plane/ui"; import { Loader, setToast, TOAST_TYPE, ToggleSwitch } from "@plane/ui";
// hooks // hooks
import { useInstance } from "@/hooks/store"; import { useInstance } from "@/hooks/store";
// components // components
@ -10,36 +11,80 @@ import { InstanceEmailForm } from "./email-config-form";
const InstanceEmailPage = observer(() => { const InstanceEmailPage = observer(() => {
// store // store
const { fetchInstanceConfigurations, formattedConfig } = useInstance(); const { fetchInstanceConfigurations, formattedConfig, disableEmail } = useInstance();
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations()); const { isLoading } = useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSMTPEnabled, setIsSMTPEnabled] = useState(false);
const handleToggle = async () => {
if (isSMTPEnabled) {
setIsSubmitting(true);
try {
await disableEmail();
setIsSMTPEnabled(false);
setToast({
title: "Email feature disabled",
message: "Email feature has been disabled",
type: TOAST_TYPE.SUCCESS,
});
} catch (error) {
setToast({
title: "Error disabling email",
message: "Failed to disable email feature. Please try again.",
type: TOAST_TYPE.ERROR,
});
} finally {
setIsSubmitting(false);
}
return;
}
setIsSMTPEnabled(true);
};
useEffect(() => {
if (formattedConfig) {
setIsSMTPEnabled(formattedConfig.ENABLE_SMTP === "1");
}
}, [formattedConfig]);
return ( return (
<> <>
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col"> <div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0"> <div className="flex items-center justify-between gap-4 border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
<div className="text-xl font-medium text-custom-text-100">Secure emails from your own instance</div> <div className="py-4 space-y-1 flex-shrink-0">
<div className="text-sm font-normal text-custom-text-300"> <div className="text-xl font-medium text-custom-text-100">Secure emails from your own instance</div>
Plane can send useful emails to you and your users from your own instance without talking to the Internet.
<div className="text-sm font-normal text-custom-text-300"> <div className="text-sm font-normal text-custom-text-300">
Set it up below and please test your settings before you save them.&nbsp; Plane can send useful emails to you and your users from your own instance without talking to the Internet.
<span className="text-red-400">Misconfigs can lead to email bounces and errors.</span> <div className="text-sm font-normal text-custom-text-300">
Set it up below and please test your settings before you save them.&nbsp;
<span className="text-red-400">Misconfigs can lead to email bounces and errors.</span>
</div>
</div> </div>
</div> </div>
</div> {isLoading ? (
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4"> <Loader>
{formattedConfig ? ( <Loader.Item width="24px" height="16px" className="rounded-full" />
<InstanceEmailForm config={formattedConfig} />
) : (
<Loader className="space-y-10">
<Loader.Item height="50px" width="75%" />
<Loader.Item height="50px" width="75%" />
<Loader.Item height="50px" width="40%" />
<Loader.Item height="50px" width="40%" />
<Loader.Item height="50px" width="20%" />
</Loader> </Loader>
) : (
<ToggleSwitch value={isSMTPEnabled} onChange={handleToggle} size="sm" disabled={isSubmitting} />
)} )}
</div> </div>
{isSMTPEnabled && !isLoading && (
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
{formattedConfig ? (
<InstanceEmailForm config={formattedConfig} />
) : (
<Loader className="space-y-10">
<Loader.Item height="50px" width="75%" />
<Loader.Item height="50px" width="75%" />
<Loader.Item height="50px" width="40%" />
<Loader.Item height="50px" width="40%" />
<Loader.Item height="50px" width="20%" />
</Loader>
)}
</div>
)}
</div> </div>
</> </>
); );

View file

@ -32,6 +32,7 @@ export interface IInstanceStore {
fetchInstanceAdmins: () => Promise<IInstanceAdmin[] | undefined>; fetchInstanceAdmins: () => Promise<IInstanceAdmin[] | undefined>;
fetchInstanceConfigurations: () => Promise<IInstanceConfiguration[] | undefined>; fetchInstanceConfigurations: () => Promise<IInstanceConfiguration[] | undefined>;
updateInstanceConfigurations: (data: Partial<IFormattedInstanceConfiguration>) => Promise<IInstanceConfiguration[]>; updateInstanceConfigurations: (data: Partial<IFormattedInstanceConfiguration>) => Promise<IInstanceConfiguration[]>;
disableEmail: () => Promise<void>;
} }
export class InstanceStore implements IInstanceStore { export class InstanceStore implements IInstanceStore {
@ -187,4 +188,30 @@ export class InstanceStore implements IInstanceStore {
throw error; throw error;
} }
}; };
disableEmail = async () => {
const instanceConfigurations = this.instanceConfigurations;
try {
runInAction(() => {
this.instanceConfigurations = this.instanceConfigurations?.map((config) => {
if (
[
"EMAIL_HOST",
"EMAIL_PORT",
"EMAIL_HOST_USER",
"EMAIL_HOST_PASSWORD",
"EMAIL_FROM",
"ENABLE_SMTP",
].includes(config.key)
)
return { ...config, value: "" };
return config;
});
});
await this.instanceService.disableEmail();
} catch (error) {
console.error("Error disabling the email");
this.instanceConfigurations = instanceConfigurations;
}
};
} }

View file

@ -1,7 +1,11 @@
from .instance import InstanceEndpoint, SignUpScreenVisitedEndpoint from .instance import InstanceEndpoint, SignUpScreenVisitedEndpoint
from .configuration import EmailCredentialCheckEndpoint, InstanceConfigurationEndpoint from .configuration import (
EmailCredentialCheckEndpoint,
InstanceConfigurationEndpoint,
DisableEmailFeatureEndpoint,
)
from .admin import ( from .admin import (

View file

@ -9,6 +9,7 @@ from smtplib import (
# Django imports # Django imports
from django.core.mail import BadHeaderError, EmailMultiAlternatives, get_connection from django.core.mail import BadHeaderError, EmailMultiAlternatives, get_connection
from django.db.models import Q, Case, When, Value
# Third party imports # Third party imports
from rest_framework import status from rest_framework import status
@ -57,6 +58,34 @@ class InstanceConfigurationEndpoint(BaseAPIView):
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
class DisableEmailFeatureEndpoint(BaseAPIView):
permission_classes = [InstanceAdminPermission]
@invalidate_cache(path="/api/instances/", user=False)
def delete(self, request):
try:
InstanceConfiguration.objects.filter(
Q(
key__in=[
"EMAIL_HOST",
"EMAIL_HOST_USER",
"EMAIL_HOST_PASSWORD",
"ENABLE_SMTP",
"EMAIL_PORT",
"EMAIL_FROM",
]
)
).update(
value=Case(When(key="ENABLE_SMTP", then=Value("0")), default=Value(""))
)
return Response(status=status.HTTP_200_OK)
except Exception:
return Response(
{"error": "Failed to disable email configuration"},
status=status.HTTP_400_BAD_REQUEST,
)
class EmailCredentialCheckEndpoint(BaseAPIView): class EmailCredentialCheckEndpoint(BaseAPIView):
def post(self, request): def post(self, request):
receiver_email = request.data.get("receiver_email", False) receiver_email = request.data.get("receiver_email", False)

View file

@ -89,6 +89,12 @@ class Command(BaseCommand):
"category": "GITLAB", "category": "GITLAB",
"is_encrypted": False, "is_encrypted": False,
}, },
{
"key": "ENABLE_SMTP",
"value": os.environ.get("ENABLE_SMTP", "0"),
"category": "SMTP",
"is_encrypted": False,
},
{ {
"key": "GITLAB_CLIENT_SECRET", "key": "GITLAB_CLIENT_SECRET",
"value": os.environ.get("GITLAB_CLIENT_SECRET"), "value": os.environ.get("GITLAB_CLIENT_SECRET"),

View file

@ -6,6 +6,7 @@ from plane.license.api.views import (
InstanceAdminSignInEndpoint, InstanceAdminSignInEndpoint,
InstanceAdminSignUpEndpoint, InstanceAdminSignUpEndpoint,
InstanceConfigurationEndpoint, InstanceConfigurationEndpoint,
DisableEmailFeatureEndpoint,
InstanceEndpoint, InstanceEndpoint,
SignUpScreenVisitedEndpoint, SignUpScreenVisitedEndpoint,
InstanceAdminUserMeEndpoint, InstanceAdminUserMeEndpoint,
@ -35,6 +36,11 @@ urlpatterns = [
InstanceConfigurationEndpoint.as_view(), InstanceConfigurationEndpoint.as_view(),
name="instance-configuration", name="instance-configuration",
), ),
path(
"configurations/disable-email-feature/",
DisableEmailFeatureEndpoint.as_view(),
name="disable-email-configuration",
),
path( path(
"admins/sign-in/", "admins/sign-in/",
InstanceAdminSignInEndpoint.as_view(), InstanceAdminSignInEndpoint.as_view(),

View file

@ -122,4 +122,17 @@ export class InstanceService extends APIService {
throw error?.response?.data; throw error?.response?.data;
}); });
} }
/**
* Disables the email configuration
* @returns {Promise<void>} Promise resolving to void
* @throws {Error} If the API request fails
*/
async disableEmail(): Promise<void> {
return this.delete("/api/instances/configurations/disable-email-feature/")
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
} }

View file

@ -5,4 +5,5 @@ export type TInstanceEmailConfigurationKeys =
| "EMAIL_HOST_PASSWORD" | "EMAIL_HOST_PASSWORD"
| "EMAIL_USE_TLS" | "EMAIL_USE_TLS"
| "EMAIL_USE_SSL" | "EMAIL_USE_SSL"
| "EMAIL_FROM"; | "EMAIL_FROM"
| "ENABLE_SMTP";