[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,14 +11,48 @@ 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="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="text-xl font-medium text-custom-text-100">Secure emails from your own instance</div>
<div className="text-sm font-normal text-custom-text-300"> <div className="text-sm font-normal text-custom-text-300">
Plane can send useful emails to you and your users from your own instance without talking to the Internet. Plane can send useful emails to you and your users from your own instance without talking to the Internet.
@ -27,6 +62,15 @@ const InstanceEmailPage = observer(() => {
</div> </div>
</div> </div>
</div> </div>
{isLoading ? (
<Loader>
<Loader.Item width="24px" height="16px" className="rounded-full" />
</Loader>
) : (
<ToggleSwitch value={isSMTPEnabled} onChange={handleToggle} size="sm" disabled={isSubmitting} />
)}
</div>
{isSMTPEnabled && !isLoading && (
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4"> <div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
{formattedConfig ? ( {formattedConfig ? (
<InstanceEmailForm config={formattedConfig} /> <InstanceEmailForm config={formattedConfig} />
@ -40,6 +84,7 @@ const InstanceEmailPage = observer(() => {
</Loader> </Loader>
)} )}
</div> </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";