diff --git a/apps/api/plane/bgtasks/analytic_plot_export.py b/apps/api/plane/bgtasks/analytic_plot_export.py index 845fb50dd24..5a9b83eee3d 100644 --- a/apps/api/plane/bgtasks/analytic_plot_export.py +++ b/apps/api/plane/bgtasks/analytic_plot_export.py @@ -9,7 +9,6 @@ # Django imports from django.core.mail import EmailMultiAlternatives, get_connection from django.template.loader import render_to_string -from django.utils.html import strip_tags from django.db.models import Q, Case, Value, When from django.db import models from django.db.models.functions import Concat @@ -18,6 +17,7 @@ from plane.db.models import Issue from plane.license.utils.instance_value import get_email_configuration from plane.utils.analytics_plot import build_graph_plot +from plane.utils.email import generate_plain_text_from_html from plane.utils.exception_logger import log_exception from plane.utils.issue_filters import issue_filters @@ -48,7 +48,7 @@ def send_export_email(email, slug, csv_buffer, rows): """Helper function to send export email.""" subject = "Your Export is ready" html_content = render_to_string("emails/exports/analytics.html", {}) - text_content = strip_tags(html_content) + text_content = generate_plain_text_from_html(html_content) csv_buffer.seek(0) diff --git a/apps/api/plane/bgtasks/email_notification_task.py b/apps/api/plane/bgtasks/email_notification_task.py index 1402adc41f8..3269569a165 100644 --- a/apps/api/plane/bgtasks/email_notification_task.py +++ b/apps/api/plane/bgtasks/email_notification_task.py @@ -11,12 +11,12 @@ # Django imports from django.utils import timezone -from django.utils.html import strip_tags # Module imports from plane.db.models import EmailNotificationLog, Issue, User from plane.license.utils.instance_value import get_email_configuration from plane.settings.redis import redis_instance +from plane.utils.email import generate_plain_text_from_html from plane.utils.exception_logger import log_exception @@ -256,7 +256,7 @@ def send_email_notification(issue_id, notification_data, receiver_id, email_noti "entity_type": "issue", } html_content = render_to_string("emails/notifications/issue-updates.html", context) - text_content = strip_tags(html_content) + text_content = generate_plain_text_from_html(html_content) try: connection = get_connection( diff --git a/apps/api/plane/bgtasks/forgot_password_task.py b/apps/api/plane/bgtasks/forgot_password_task.py index ffaba9937f0..d996efbf044 100644 --- a/apps/api/plane/bgtasks/forgot_password_task.py +++ b/apps/api/plane/bgtasks/forgot_password_task.py @@ -8,10 +8,10 @@ # Third party imports from django.core.mail import EmailMultiAlternatives, get_connection from django.template.loader import render_to_string -from django.utils.html import strip_tags # Module imports from plane.license.utils.instance_value import get_email_configuration +from plane.utils.email import generate_plain_text_from_html from plane.utils.exception_logger import log_exception @@ -41,7 +41,7 @@ def forgot_password(first_name, email, uidb64, token, current_site): html_content = render_to_string("emails/auth/forgot_password.html", context) - text_content = strip_tags(html_content) + text_content = generate_plain_text_from_html(html_content) connection = get_connection( host=EMAIL_HOST, diff --git a/apps/api/plane/bgtasks/magic_link_code_task.py b/apps/api/plane/bgtasks/magic_link_code_task.py index d8267e69716..8735a7bd1b7 100644 --- a/apps/api/plane/bgtasks/magic_link_code_task.py +++ b/apps/api/plane/bgtasks/magic_link_code_task.py @@ -8,10 +8,10 @@ # Third party imports from django.core.mail import EmailMultiAlternatives, get_connection from django.template.loader import render_to_string -from django.utils.html import strip_tags # Module imports from plane.license.utils.instance_value import get_email_configuration +from plane.utils.email import generate_plain_text_from_html from plane.utils.exception_logger import log_exception @@ -33,7 +33,7 @@ def magic_link(email, key, token): context = {"code": token, "email": email} html_content = render_to_string("emails/auth/magic_signin.html", context) - text_content = strip_tags(html_content) + text_content = generate_plain_text_from_html(html_content) connection = get_connection( host=EMAIL_HOST, diff --git a/apps/api/plane/bgtasks/project_add_user_email_task.py b/apps/api/plane/bgtasks/project_add_user_email_task.py index af6014695d1..a2a1b0049a2 100644 --- a/apps/api/plane/bgtasks/project_add_user_email_task.py +++ b/apps/api/plane/bgtasks/project_add_user_email_task.py @@ -7,11 +7,11 @@ # Third party imports from django.core.mail import EmailMultiAlternatives, get_connection from django.template.loader import render_to_string -from django.utils.html import strip_tags # Module imports from plane.license.utils.instance_value import get_email_configuration +from plane.utils.email import generate_plain_text_from_html from plane.utils.exception_logger import log_exception from plane.db.models import ProjectMember from plane.db.models import User @@ -55,7 +55,7 @@ def project_add_user_email(current_site, project_member_id, invitor_id): # Render the email template html_content = render_to_string("emails/notifications/project_addition.html", context) - text_content = strip_tags(html_content) + text_content = generate_plain_text_from_html(html_content) # Initialize the connection connection = get_connection( host=EMAIL_HOST, diff --git a/apps/api/plane/bgtasks/project_invitation_task.py b/apps/api/plane/bgtasks/project_invitation_task.py index b8eed5e45a9..2169449d73a 100644 --- a/apps/api/plane/bgtasks/project_invitation_task.py +++ b/apps/api/plane/bgtasks/project_invitation_task.py @@ -8,11 +8,11 @@ # Third party imports from django.core.mail import EmailMultiAlternatives, get_connection from django.template.loader import render_to_string -from django.utils.html import strip_tags # Module imports from plane.db.models import Project, ProjectMemberInvite, User from plane.license.utils.instance_value import get_email_configuration +from plane.utils.email import generate_plain_text_from_html from plane.utils.exception_logger import log_exception @@ -37,7 +37,7 @@ def project_invitation(email, project_id, token, current_site, invitor): html_content = render_to_string("emails/invitations/project_invitation.html", context) - text_content = strip_tags(html_content) + text_content = generate_plain_text_from_html(html_content) project_member_invite.message = text_content project_member_invite.save() diff --git a/apps/api/plane/bgtasks/user_activation_email_task.py b/apps/api/plane/bgtasks/user_activation_email_task.py index 492564b3cec..44da0421c9c 100644 --- a/apps/api/plane/bgtasks/user_activation_email_task.py +++ b/apps/api/plane/bgtasks/user_activation_email_task.py @@ -4,7 +4,6 @@ # Django imports from django.core.mail import EmailMultiAlternatives, get_connection from django.template.loader import render_to_string -from django.utils.html import strip_tags # Third party imports from celery import shared_task @@ -12,6 +11,7 @@ # Module imports from plane.db.models import User from plane.license.utils.instance_value import get_email_configuration +from plane.utils.email import generate_plain_text_from_html from plane.utils.exception_logger import log_exception @@ -27,7 +27,7 @@ def user_activation_email(current_site, user_id): # Send email to user html_content = render_to_string("emails/user/user_activation.html", context) - text_content = strip_tags(html_content) + text_content = generate_plain_text_from_html(html_content) # Configure email connection from the database ( EMAIL_HOST, diff --git a/apps/api/plane/bgtasks/user_deactivation_email_task.py b/apps/api/plane/bgtasks/user_deactivation_email_task.py index 2595d8055b2..b07c1ff179e 100644 --- a/apps/api/plane/bgtasks/user_deactivation_email_task.py +++ b/apps/api/plane/bgtasks/user_deactivation_email_task.py @@ -4,7 +4,6 @@ # Django imports from django.core.mail import EmailMultiAlternatives, get_connection from django.template.loader import render_to_string -from django.utils.html import strip_tags # Third party imports from celery import shared_task @@ -12,6 +11,7 @@ # Module imports from plane.db.models import User from plane.license.utils.instance_value import get_email_configuration +from plane.utils.email import generate_plain_text_from_html from plane.utils.exception_logger import log_exception @@ -27,7 +27,7 @@ def user_deactivation_email(current_site, user_id): # Send email to user html_content = render_to_string("emails/user/user_deactivation.html", context) - text_content = strip_tags(html_content) + text_content = generate_plain_text_from_html(html_content) # Configure email connection from the database ( EMAIL_HOST, diff --git a/apps/api/plane/bgtasks/user_email_update_task.py b/apps/api/plane/bgtasks/user_email_update_task.py index 667de368c79..1a77f74177c 100644 --- a/apps/api/plane/bgtasks/user_email_update_task.py +++ b/apps/api/plane/bgtasks/user_email_update_task.py @@ -7,10 +7,10 @@ # Django imports from django.core.mail import EmailMultiAlternatives, get_connection from django.template.loader import render_to_string -from django.utils.html import strip_tags # Module imports from plane.license.utils.instance_value import get_email_configuration +from plane.utils.email import generate_plain_text_from_html from plane.utils.exception_logger import log_exception @@ -32,7 +32,7 @@ def send_email_update_magic_code(email, token): context = {"code": token, "email": email} html_content = render_to_string("emails/auth/magic_signin.html", context) - text_content = strip_tags(html_content) + text_content = generate_plain_text_from_html(html_content) connection = get_connection( host=EMAIL_HOST, @@ -83,7 +83,7 @@ def send_email_update_confirmation(email): context = {"email": email} html_content = render_to_string("emails/user/email_updated.html", context) - text_content = strip_tags(html_content) + text_content = generate_plain_text_from_html(html_content) connection = get_connection( host=EMAIL_HOST, diff --git a/apps/api/plane/bgtasks/webhook_task.py b/apps/api/plane/bgtasks/webhook_task.py index 3d04a65b71b..a5d3cb7a91e 100644 --- a/apps/api/plane/bgtasks/webhook_task.py +++ b/apps/api/plane/bgtasks/webhook_task.py @@ -16,7 +16,6 @@ from django.core.mail import EmailMultiAlternatives, get_connection from django.core.serializers.json import DjangoJSONEncoder from django.template.loader import render_to_string -from django.utils.html import strip_tags from django.core.exceptions import ObjectDoesNotExist # Module imports @@ -47,6 +46,7 @@ IssueAssignee, ) from plane.license.utils.instance_value import get_email_configuration +from plane.utils.email import generate_plain_text_from_html from plane.utils.exception_logger import log_exception from plane.settings.mongo import MongoConnection @@ -218,7 +218,7 @@ def send_webhook_deactivation_email(webhook_id: str, receiver_id: str, current_s "webhook_url": f"{current_site}/{str(webhook.workspace.slug)}/settings/webhooks/{str(webhook.id)}", } html_content = render_to_string("emails/notifications/webhook-deactivate.html", context) - text_content = strip_tags(html_content) + text_content = generate_plain_text_from_html(html_content) # Set the email connection connection = get_connection( diff --git a/apps/api/plane/bgtasks/workspace_invitation_task.py b/apps/api/plane/bgtasks/workspace_invitation_task.py index f7480b36a69..f3dab9b8eea 100644 --- a/apps/api/plane/bgtasks/workspace_invitation_task.py +++ b/apps/api/plane/bgtasks/workspace_invitation_task.py @@ -7,11 +7,11 @@ # Django imports from django.core.mail import EmailMultiAlternatives, get_connection from django.template.loader import render_to_string -from django.utils.html import strip_tags # Module imports from plane.db.models import User, Workspace, WorkspaceMemberInvite from plane.license.utils.instance_value import get_email_configuration +from plane.utils.email import generate_plain_text_from_html from plane.utils.exception_logger import log_exception @@ -53,7 +53,7 @@ def workspace_invitation(email, workspace_id, token, current_site, inviter): html_content = render_to_string("emails/invitations/workspace_invitation.html", context) - text_content = strip_tags(html_content) + text_content = generate_plain_text_from_html(html_content) workspace_member_invite.message = text_content workspace_member_invite.save() diff --git a/apps/api/plane/utils/email.py b/apps/api/plane/utils/email.py new file mode 100644 index 00000000000..f950e94515c --- /dev/null +++ b/apps/api/plane/utils/email.py @@ -0,0 +1,42 @@ +# SPDX-FileCopyrightText: 2023-present Plane Software, Inc. +# SPDX-License-Identifier: LicenseRef-Plane-Commercial +# +# Licensed under the Plane Commercial License (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# https://plane.so/legals/eula +# +# DO NOT remove or modify this notice. +# NOTICE: Proprietary and confidential. Unauthorized use or distribution is prohibited. + +# Python imports +import re + +# Django imports +from django.utils.html import strip_tags + + +def generate_plain_text_from_html(html_content): + """ + Generate clean plain text from HTML email template. + Removes all HTML tags, CSS styles, and excessive whitespace. + + Args: + html_content (str): The HTML content to convert to plain text + + Returns: + str: Clean plain text without HTML tags, styles, or excessive whitespace + """ + # Remove style tags and their content + html_content = re.sub(r"]*>.*?", "", html_content, flags=re.DOTALL | re.IGNORECASE) + + # Strip HTML tags + text_content = strip_tags(html_content) + + # Remove excessive empty lines + text_content = re.sub(r"\n\s*\n\s*\n+", "\n\n", text_content) + + # Ensure there's a leading and trailing whitespace + text_content = "\n\n" + text_content.lstrip().rstrip() + "\n\n" + + return text_content