From 0d8312f5989966c4f5c30db22cbf0502be72d4e2 Mon Sep 17 00:00:00 2001 From: sangeethailango Date: Fri, 11 Jul 2025 17:08:03 +0530 Subject: [PATCH 1/9] feat: api update instance configuration --- apps/api/plane/license/api/views/__init__.py | 6 +++++- apps/api/plane/license/api/views/configuration.py | 13 +++++++++++++ apps/api/plane/license/urls.py | 6 ++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/apps/api/plane/license/api/views/__init__.py b/apps/api/plane/license/api/views/__init__.py index a2ef90facb6..7f30d53fe66 100644 --- a/apps/api/plane/license/api/views/__init__.py +++ b/apps/api/plane/license/api/views/__init__.py @@ -1,7 +1,11 @@ from .instance import InstanceEndpoint, SignUpScreenVisitedEndpoint -from .configuration import EmailCredentialCheckEndpoint, InstanceConfigurationEndpoint +from .configuration import ( + EmailCredentialCheckEndpoint, + InstanceConfigurationEndpoint, + DisableEmailFeatureEndpoint, +) from .admin import ( diff --git a/apps/api/plane/license/api/views/configuration.py b/apps/api/plane/license/api/views/configuration.py index 594a899eba4..4d1f4c31635 100644 --- a/apps/api/plane/license/api/views/configuration.py +++ b/apps/api/plane/license/api/views/configuration.py @@ -57,6 +57,19 @@ def patch(self, request): 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): + instance_configurations = InstanceConfiguration.objects.filter( + key__in=["EMAIL_HOST", "EMAIL_HOST_USER", "EMAIL_HOST_PASSWORD"] + ) + + instance_configurations.update(value="") + return Response(status=status.HTTP_200_OK) + + class EmailCredentialCheckEndpoint(BaseAPIView): def post(self, request): receiver_email = request.data.get("receiver_email", False) diff --git a/apps/api/plane/license/urls.py b/apps/api/plane/license/urls.py index 9c3adbf98ab..4d306924eaf 100644 --- a/apps/api/plane/license/urls.py +++ b/apps/api/plane/license/urls.py @@ -6,6 +6,7 @@ InstanceAdminSignInEndpoint, InstanceAdminSignUpEndpoint, InstanceConfigurationEndpoint, + DisableEmailFeatureEndpoint, InstanceEndpoint, SignUpScreenVisitedEndpoint, InstanceAdminUserMeEndpoint, @@ -35,6 +36,11 @@ InstanceConfigurationEndpoint.as_view(), name="instance-configuration", ), + path( + "configurations/disable-email-feature/", + DisableEmailFeatureEndpoint.as_view(), + name="disable-email-configuration", + ), path( "admins/sign-in/", InstanceAdminSignInEndpoint.as_view(), From f5fe094f25ba22228b41e6ed132b3e6fcfa408b5 Mon Sep 17 00:00:00 2001 From: sangeethailango Date: Fri, 11 Jul 2025 18:01:32 +0530 Subject: [PATCH 2/9] chore: add enable_smtp key --- apps/api/plane/license/api/views/configuration.py | 7 ++++++- .../license/management/commands/configure_instance.py | 6 ++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/apps/api/plane/license/api/views/configuration.py b/apps/api/plane/license/api/views/configuration.py index 4d1f4c31635..826d4139639 100644 --- a/apps/api/plane/license/api/views/configuration.py +++ b/apps/api/plane/license/api/views/configuration.py @@ -63,7 +63,12 @@ class DisableEmailFeatureEndpoint(BaseAPIView): @invalidate_cache(path="/api/instances/", user=False) def delete(self, request): instance_configurations = InstanceConfiguration.objects.filter( - key__in=["EMAIL_HOST", "EMAIL_HOST_USER", "EMAIL_HOST_PASSWORD"] + key__in=[ + "EMAIL_HOST", + "EMAIL_HOST_USER", + "EMAIL_HOST_PASSWORD", + "ENABLE_SMTP", + ] ) instance_configurations.update(value="") diff --git a/apps/api/plane/license/management/commands/configure_instance.py b/apps/api/plane/license/management/commands/configure_instance.py index 2e1b6a12356..1414c970c54 100644 --- a/apps/api/plane/license/management/commands/configure_instance.py +++ b/apps/api/plane/license/management/commands/configure_instance.py @@ -89,6 +89,12 @@ def handle(self, *args, **options): "category": "GITLAB", "is_encrypted": False, }, + { + "key": "ENABLE_SMTP", + "value": os.environ.get("ENABLE_SMTP", "0"), + "category": "SMTP", + "is_encrypted": False, + }, { "key": "GITLAB_CLIENT_SECRET", "value": os.environ.get("GITLAB_CLIENT_SECRET"), From fc94bec52dc559bb163639887d91a44d5475f68f Mon Sep 17 00:00:00 2001 From: sangeethailango Date: Fri, 11 Jul 2025 18:12:52 +0530 Subject: [PATCH 3/9] fix: empty string for enable_smtp key --- .../plane/license/api/views/configuration.py | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/apps/api/plane/license/api/views/configuration.py b/apps/api/plane/license/api/views/configuration.py index 826d4139639..929111f5b24 100644 --- a/apps/api/plane/license/api/views/configuration.py +++ b/apps/api/plane/license/api/views/configuration.py @@ -9,6 +9,7 @@ # Django imports from django.core.mail import BadHeaderError, EmailMultiAlternatives, get_connection +from django.db.models import Q, Case, When, Value # Third party imports from rest_framework import status @@ -62,16 +63,18 @@ class DisableEmailFeatureEndpoint(BaseAPIView): @invalidate_cache(path="/api/instances/", user=False) def delete(self, request): - instance_configurations = InstanceConfiguration.objects.filter( - key__in=[ - "EMAIL_HOST", - "EMAIL_HOST_USER", - "EMAIL_HOST_PASSWORD", - "ENABLE_SMTP", - ] + InstanceConfiguration.objects.filter( + Q( + key__in=[ + "EMAIL_HOST", + "EMAIL_HOST_USER", + "EMAIL_HOST_PASSWORD", + "ENABLE_SMTP", + ] + ) + ).update( + value=Case(When(key="ENABLE_SMTP", then=Value("0")), default=Value("")) ) - - instance_configurations.update(value="") return Response(status=status.HTTP_200_OK) From 0d37da0e898cfe43301f57e25d2a8afd274b4858 Mon Sep 17 00:00:00 2001 From: sangeethailango Date: Fri, 11 Jul 2025 18:34:24 +0530 Subject: [PATCH 4/9] chore: update email_port and email_from --- apps/api/plane/license/api/views/configuration.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/api/plane/license/api/views/configuration.py b/apps/api/plane/license/api/views/configuration.py index 929111f5b24..6890a165a60 100644 --- a/apps/api/plane/license/api/views/configuration.py +++ b/apps/api/plane/license/api/views/configuration.py @@ -70,6 +70,8 @@ def delete(self, request): "EMAIL_HOST_USER", "EMAIL_HOST_PASSWORD", "ENABLE_SMTP", + "EMAIL_PORT", + "EMAIL_FROM", ] ) ).update( From b22cb8f6b0d26f091087b02dcbb36ae59bce23f4 Mon Sep 17 00:00:00 2001 From: gakshita Date: Fri, 11 Jul 2025 18:54:43 +0530 Subject: [PATCH 5/9] fix: handled smtp enable disable --- .../(dashboard)/email/email-config-form.tsx | 6 +- .../app/(all)/(dashboard)/email/page.tsx | 81 ++++++++++++++----- apps/admin/core/store/instance.store.ts | 27 +++++++ .../services/src/instance/instance.service.ts | 13 +++ packages/types/src/instance/email.ts | 3 +- 5 files changed, 106 insertions(+), 24 deletions(-) diff --git a/apps/admin/app/(all)/(dashboard)/email/email-config-form.tsx b/apps/admin/app/(all)/(dashboard)/email/email-config-form.tsx index de7f33f5d82..cb21dcff6f5 100644 --- a/apps/admin/app/(all)/(dashboard)/email/email-config-form.tsx +++ b/apps/admin/app/(all)/(dashboard)/email/email-config-form.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { FC, useMemo, useState } from "react"; +import React, { FC, useEffect, useMemo, useState } from "react"; import { useForm } from "react-hook-form"; // types import { IFormattedInstanceConfiguration, TInstanceEmailConfigurationKeys } from "@plane/types"; @@ -49,9 +49,9 @@ export const InstanceEmailForm: FC = (props) => { EMAIL_USE_TLS: config["EMAIL_USE_TLS"], EMAIL_USE_SSL: config["EMAIL_USE_SSL"], EMAIL_FROM: config["EMAIL_FROM"], + ENABLE_SMTP: config["ENABLE_SMTP"], }, }); - const emailFormFields: TControllerInputFormField[] = [ { key: "EMAIL_HOST", @@ -101,7 +101,7 @@ export const InstanceEmailForm: FC = (props) => { ]; const onSubmit = async (formData: EmailFormValues) => { - const payload: Partial = { ...formData }; + const payload: Partial = { ...formData, ENABLE_SMTP: "1" }; await updateInstanceConfigurations(payload) .then(() => diff --git a/apps/admin/app/(all)/(dashboard)/email/page.tsx b/apps/admin/app/(all)/(dashboard)/email/page.tsx index 33fedc052de..b74a7e9201a 100644 --- a/apps/admin/app/(all)/(dashboard)/email/page.tsx +++ b/apps/admin/app/(all)/(dashboard)/email/page.tsx @@ -1,8 +1,9 @@ "use client"; +import { useEffect, useState } from "react"; import { observer } from "mobx-react"; import useSWR from "swr"; -import { Loader } from "@plane/ui"; +import { Loader, setToast, TOAST_TYPE, ToggleSwitch } from "@plane/ui"; // hooks import { useInstance } from "@/hooks/store"; // components @@ -10,36 +11,76 @@ import { InstanceEmailForm } from "./email-config-form"; const InstanceEmailPage = observer(() => { // 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); + await disableEmail(); + setIsSMTPEnabled(false); + setIsSubmitting(false); + setToast({ + title: "Email feature disabled", + message: "Email feature has been disabled", + type: TOAST_TYPE.SUCCESS, + }); + return; + } + setIsSMTPEnabled(true); + setToast({ + title: "Email feature enabled", + message: "Email feature has been enabled", + type: TOAST_TYPE.SUCCESS, + }); + }; + useEffect(() => { + if (formattedConfig) { + setIsSMTPEnabled(formattedConfig.ENABLE_SMTP === "1"); + } + }, [formattedConfig]); return ( <>
-
-
Secure emails from your own instance
-
- Plane can send useful emails to you and your users from your own instance without talking to the Internet. +
+
+
Secure emails from your own instance
- Set it up below and please test your settings before you save them.  - Misconfigs can lead to email bounces and errors. + Plane can send useful emails to you and your users from your own instance without talking to the Internet. +
+ Set it up below and please test your settings before you save them.  + Misconfigs can lead to email bounces and errors. +
-
-
- {formattedConfig ? ( - - ) : ( - - - - - - + {isLoading ? ( + + + ) : ( + )}
+ {isSMTPEnabled && !isLoading && ( +
+ {formattedConfig ? ( + + ) : ( + + + + + + + + )} +
+ )}
); diff --git a/apps/admin/core/store/instance.store.ts b/apps/admin/core/store/instance.store.ts index 1f690f7080b..1179f04d6fd 100644 --- a/apps/admin/core/store/instance.store.ts +++ b/apps/admin/core/store/instance.store.ts @@ -32,6 +32,7 @@ export interface IInstanceStore { fetchInstanceAdmins: () => Promise; fetchInstanceConfigurations: () => Promise; updateInstanceConfigurations: (data: Partial) => Promise; + disableEmail: () => Promise; } export class InstanceStore implements IInstanceStore { @@ -187,4 +188,30 @@ export class InstanceStore implements IInstanceStore { 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; + } + }; } diff --git a/packages/services/src/instance/instance.service.ts b/packages/services/src/instance/instance.service.ts index 637c81cad01..6af821d9b6b 100644 --- a/packages/services/src/instance/instance.service.ts +++ b/packages/services/src/instance/instance.service.ts @@ -122,4 +122,17 @@ export class InstanceService extends APIService { throw error?.response?.data; }); } + + /** + * Disables the email configuration + * @returns {Promise} Promise resolving to void + * @throws {Error} If the API request fails + */ + async disableEmail(): Promise { + return this.delete("/api/instances/configurations/disable-email-feature/") + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } } diff --git a/packages/types/src/instance/email.ts b/packages/types/src/instance/email.ts index d7fe8be3dc7..e5a06152fb4 100644 --- a/packages/types/src/instance/email.ts +++ b/packages/types/src/instance/email.ts @@ -5,4 +5,5 @@ export type TInstanceEmailConfigurationKeys = | "EMAIL_HOST_PASSWORD" | "EMAIL_USE_TLS" | "EMAIL_USE_SSL" - | "EMAIL_FROM"; + | "EMAIL_FROM" + | "ENABALE_SMTP"; From f7217616ca03b3132dbc0744596c26d9b0398309 Mon Sep 17 00:00:00 2001 From: sangeethailango Date: Fri, 11 Jul 2025 19:13:57 +0530 Subject: [PATCH 6/9] fix: error handling --- .../plane/license/api/views/configuration.py | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/apps/api/plane/license/api/views/configuration.py b/apps/api/plane/license/api/views/configuration.py index 6890a165a60..3bf996db929 100644 --- a/apps/api/plane/license/api/views/configuration.py +++ b/apps/api/plane/license/api/views/configuration.py @@ -63,21 +63,27 @@ class DisableEmailFeatureEndpoint(BaseAPIView): @invalidate_cache(path="/api/instances/", user=False) def delete(self, request): - InstanceConfiguration.objects.filter( - Q( - key__in=[ - "EMAIL_HOST", - "EMAIL_HOST_USER", - "EMAIL_HOST_PASSWORD", - "ENABLE_SMTP", - "EMAIL_PORT", - "EMAIL_FROM", - ] + 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, ) - ).update( - value=Case(When(key="ENABLE_SMTP", then=Value("0")), default=Value("")) - ) - return Response(status=status.HTTP_200_OK) class EmailCredentialCheckEndpoint(BaseAPIView): From e249ad0dd5ea6ac128376bd29d275deb042e0177 Mon Sep 17 00:00:00 2001 From: gakshita Date: Mon, 14 Jul 2025 16:55:44 +0530 Subject: [PATCH 7/9] fix: refactor --- .../app/(all)/(dashboard)/email/page.tsx | 25 +++++++++++++------ packages/types/src/instance/email.ts | 2 +- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/apps/admin/app/(all)/(dashboard)/email/page.tsx b/apps/admin/app/(all)/(dashboard)/email/page.tsx index b74a7e9201a..69f72476825 100644 --- a/apps/admin/app/(all)/(dashboard)/email/page.tsx +++ b/apps/admin/app/(all)/(dashboard)/email/page.tsx @@ -21,14 +21,23 @@ const InstanceEmailPage = observer(() => { const handleToggle = async () => { if (isSMTPEnabled) { setIsSubmitting(true); - await disableEmail(); - setIsSMTPEnabled(false); - setIsSubmitting(false); - setToast({ - title: "Email feature disabled", - message: "Email feature has been disabled", - type: TOAST_TYPE.SUCCESS, - }); + 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); diff --git a/packages/types/src/instance/email.ts b/packages/types/src/instance/email.ts index e5a06152fb4..0c15cd230b2 100644 --- a/packages/types/src/instance/email.ts +++ b/packages/types/src/instance/email.ts @@ -6,4 +6,4 @@ export type TInstanceEmailConfigurationKeys = | "EMAIL_USE_TLS" | "EMAIL_USE_SSL" | "EMAIL_FROM" - | "ENABALE_SMTP"; + | "ENABLE_SMTP"; From 8ac524410a565d5cf4596a2d3fd220e61ba70e51 Mon Sep 17 00:00:00 2001 From: gakshita Date: Mon, 14 Jul 2025 17:11:45 +0530 Subject: [PATCH 8/9] fix: removed enabled toast --- apps/admin/app/(all)/(dashboard)/email/page.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/apps/admin/app/(all)/(dashboard)/email/page.tsx b/apps/admin/app/(all)/(dashboard)/email/page.tsx index 69f72476825..445ff2ec72b 100644 --- a/apps/admin/app/(all)/(dashboard)/email/page.tsx +++ b/apps/admin/app/(all)/(dashboard)/email/page.tsx @@ -41,11 +41,6 @@ const InstanceEmailPage = observer(() => { return; } setIsSMTPEnabled(true); - setToast({ - title: "Email feature enabled", - message: "Email feature has been enabled", - type: TOAST_TYPE.SUCCESS, - }); }; useEffect(() => { if (formattedConfig) { From 20a32f3ac04e96c568583be58ec6a2359589af6f Mon Sep 17 00:00:00 2001 From: Akshita Goyal <36129505+gakshita@users.noreply.github.com> Date: Tue, 15 Jul 2025 19:18:37 +0530 Subject: [PATCH 9/9] fix: refactor --- apps/admin/app/(all)/(dashboard)/email/email-config-form.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/admin/app/(all)/(dashboard)/email/email-config-form.tsx b/apps/admin/app/(all)/(dashboard)/email/email-config-form.tsx index cb21dcff6f5..2bfcc428772 100644 --- a/apps/admin/app/(all)/(dashboard)/email/email-config-form.tsx +++ b/apps/admin/app/(all)/(dashboard)/email/email-config-form.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { FC, useEffect, useMemo, useState } from "react"; +import React, { FC, useMemo, useState } from "react"; import { useForm } from "react-hook-form"; // types import { IFormattedInstanceConfiguration, TInstanceEmailConfigurationKeys } from "@plane/types";