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..2bfcc428772 100644 --- a/apps/admin/app/(all)/(dashboard)/email/email-config-form.tsx +++ b/apps/admin/app/(all)/(dashboard)/email/email-config-form.tsx @@ -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..445ff2ec72b 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,80 @@ 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); + 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 ( <>
-
-
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/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..3bf996db929 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 @@ -57,6 +58,34 @@ 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): + 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): def post(self, request): receiver_email = request.data.get("receiver_email", False) 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"), 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(), 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..0c15cd230b2 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" + | "ENABLE_SMTP";