[WEB-5917] fix: generate clean plain text from HTML email template #8535

This commit is contained in:
b-saikrishnakanth 2026-02-17 00:44:52 +05:30 committed by GitHub
parent e9b011896d
commit f0dcf66167
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 65 additions and 23 deletions

View file

@ -13,7 +13,6 @@ from celery import shared_task
# Django imports # Django imports
from django.core.mail import EmailMultiAlternatives, get_connection from django.core.mail import EmailMultiAlternatives, get_connection
from django.template.loader import render_to_string 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.models import Q, Case, Value, When
from django.db import models from django.db import models
from django.db.models.functions import Concat from django.db.models.functions import Concat
@ -22,6 +21,7 @@ from django.db.models.functions import Concat
from plane.db.models import Issue from plane.db.models import Issue
from plane.license.utils.instance_value import get_email_configuration from plane.license.utils.instance_value import get_email_configuration
from plane.utils.analytics_plot import build_graph_plot 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.exception_logger import log_exception
from plane.utils.issue_filters import issue_filters from plane.utils.issue_filters import issue_filters
from plane.utils.csv_utils import sanitize_csv_row from plane.utils.csv_utils import sanitize_csv_row
@ -53,7 +53,7 @@ def send_export_email(email, slug, csv_buffer, rows):
"""Helper function to send export email.""" """Helper function to send export email."""
subject = "Your Export is ready" subject = "Your Export is ready"
html_content = render_to_string("emails/exports/analytics.html", {}) 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) csv_buffer.seek(0)

View file

@ -15,12 +15,12 @@ from django.template.loader import render_to_string
# Django imports # Django imports
from django.utils import timezone from django.utils import timezone
from django.utils.html import strip_tags
# Module imports # Module imports
from plane.db.models import EmailNotificationLog, Issue, User from plane.db.models import EmailNotificationLog, Issue, User
from plane.license.utils.instance_value import get_email_configuration from plane.license.utils.instance_value import get_email_configuration
from plane.settings.redis import redis_instance 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 from plane.utils.exception_logger import log_exception
@ -260,7 +260,7 @@ def send_email_notification(issue_id, notification_data, receiver_id, email_noti
"entity_type": "issue", "entity_type": "issue",
} }
html_content = render_to_string("emails/notifications/issue-updates.html", context) 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: try:
connection = get_connection( connection = get_connection(

View file

@ -12,10 +12,10 @@ from celery import shared_task
# Third party imports # Third party imports
from django.core.mail import EmailMultiAlternatives, get_connection from django.core.mail import EmailMultiAlternatives, get_connection
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.html import strip_tags
# Module imports # Module imports
from plane.license.utils.instance_value import get_email_configuration 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.utils.exception_logger import log_exception
@ -45,7 +45,7 @@ def forgot_password(first_name, email, uidb64, token, current_site):
html_content = render_to_string("emails/auth/forgot_password.html", context) 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( connection = get_connection(
host=EMAIL_HOST, host=EMAIL_HOST,

View file

@ -12,10 +12,10 @@ from celery import shared_task
# Third party imports # Third party imports
from django.core.mail import EmailMultiAlternatives, get_connection from django.core.mail import EmailMultiAlternatives, get_connection
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.html import strip_tags
# Module imports # Module imports
from plane.license.utils.instance_value import get_email_configuration 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.utils.exception_logger import log_exception
@ -37,7 +37,7 @@ def magic_link(email, key, token):
context = {"code": token, "email": email} context = {"code": token, "email": email}
html_content = render_to_string("emails/auth/magic_signin.html", context) 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( connection = get_connection(
host=EMAIL_HOST, host=EMAIL_HOST,

View file

@ -11,11 +11,11 @@ from celery import shared_task
# Third party imports # Third party imports
from django.core.mail import EmailMultiAlternatives, get_connection from django.core.mail import EmailMultiAlternatives, get_connection
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.html import strip_tags
# Module imports # Module imports
from plane.license.utils.instance_value import get_email_configuration 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.utils.exception_logger import log_exception
from plane.db.models import ProjectMember from plane.db.models import ProjectMember
from plane.db.models import User from plane.db.models import User
@ -59,7 +59,7 @@ def project_add_user_email(current_site, project_member_id, invitor_id):
# Render the email template # Render the email template
html_content = render_to_string("emails/notifications/project_addition.html", context) 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 # Initialize the connection
connection = get_connection( connection = get_connection(
host=EMAIL_HOST, host=EMAIL_HOST,

View file

@ -12,11 +12,11 @@ from celery import shared_task
# Third party imports # Third party imports
from django.core.mail import EmailMultiAlternatives, get_connection from django.core.mail import EmailMultiAlternatives, get_connection
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.html import strip_tags
# Module imports # Module imports
from plane.db.models import Project, ProjectMemberInvite, User from plane.db.models import Project, ProjectMemberInvite, User
from plane.license.utils.instance_value import get_email_configuration 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.utils.exception_logger import log_exception
@ -41,7 +41,7 @@ def project_invitation(email, project_id, token, current_site, invitor):
html_content = render_to_string("emails/invitations/project_invitation.html", context) 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.message = text_content
project_member_invite.save() project_member_invite.save()

View file

@ -8,7 +8,6 @@ import logging
# Django imports # Django imports
from django.core.mail import EmailMultiAlternatives, get_connection from django.core.mail import EmailMultiAlternatives, get_connection
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.html import strip_tags
# Third party imports # Third party imports
from celery import shared_task from celery import shared_task
@ -16,6 +15,7 @@ from celery import shared_task
# Module imports # Module imports
from plane.db.models import User from plane.db.models import User
from plane.license.utils.instance_value import get_email_configuration 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.utils.exception_logger import log_exception
@ -31,7 +31,7 @@ def user_activation_email(current_site, user_id):
# Send email to user # Send email to user
html_content = render_to_string("emails/user/user_activation.html", context) 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 # Configure email connection from the database
( (
EMAIL_HOST, EMAIL_HOST,

View file

@ -8,7 +8,6 @@ import logging
# Django imports # Django imports
from django.core.mail import EmailMultiAlternatives, get_connection from django.core.mail import EmailMultiAlternatives, get_connection
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.html import strip_tags
# Third party imports # Third party imports
from celery import shared_task from celery import shared_task
@ -16,6 +15,7 @@ from celery import shared_task
# Module imports # Module imports
from plane.db.models import User from plane.db.models import User
from plane.license.utils.instance_value import get_email_configuration 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.utils.exception_logger import log_exception
@ -31,7 +31,7 @@ def user_deactivation_email(current_site, user_id):
# Send email to user # Send email to user
html_content = render_to_string("emails/user/user_deactivation.html", context) 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 # Configure email connection from the database
( (
EMAIL_HOST, EMAIL_HOST,

View file

@ -11,10 +11,10 @@ from celery import shared_task
# Django imports # Django imports
from django.core.mail import EmailMultiAlternatives, get_connection from django.core.mail import EmailMultiAlternatives, get_connection
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.html import strip_tags
# Module imports # Module imports
from plane.license.utils.instance_value import get_email_configuration 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.utils.exception_logger import log_exception
@ -36,7 +36,7 @@ def send_email_update_magic_code(email, token):
context = {"code": token, "email": email} context = {"code": token, "email": email}
html_content = render_to_string("emails/auth/magic_signin.html", context) 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( connection = get_connection(
host=EMAIL_HOST, host=EMAIL_HOST,
@ -87,7 +87,7 @@ def send_email_update_confirmation(email):
context = {"email": email} context = {"email": email}
html_content = render_to_string("emails/user/email_updated.html", context) 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( connection = get_connection(
host=EMAIL_HOST, host=EMAIL_HOST,

View file

@ -20,7 +20,6 @@ from django.db.models import Prefetch
from django.core.mail import EmailMultiAlternatives, get_connection from django.core.mail import EmailMultiAlternatives, get_connection
from django.core.serializers.json import DjangoJSONEncoder from django.core.serializers.json import DjangoJSONEncoder
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.html import strip_tags
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
# Module imports # Module imports
@ -51,6 +50,7 @@ from plane.db.models import (
IssueAssignee, IssueAssignee,
) )
from plane.license.utils.instance_value import get_email_configuration 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.utils.exception_logger import log_exception
from plane.settings.mongo import MongoConnection from plane.settings.mongo import MongoConnection
@ -222,7 +222,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)}", "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) 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 # Set the email connection
connection = get_connection( connection = get_connection(

View file

@ -11,11 +11,11 @@ from celery import shared_task
# Django imports # Django imports
from django.core.mail import EmailMultiAlternatives, get_connection from django.core.mail import EmailMultiAlternatives, get_connection
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.html import strip_tags
# Module imports # Module imports
from plane.db.models import User, Workspace, WorkspaceMemberInvite from plane.db.models import User, Workspace, WorkspaceMemberInvite
from plane.license.utils.instance_value import get_email_configuration 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.utils.exception_logger import log_exception
@ -57,7 +57,7 @@ def workspace_invitation(email, workspace_id, token, current_site, inviter):
html_content = render_to_string("emails/invitations/workspace_invitation.html", context) 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.message = text_content
workspace_member_invite.save() workspace_member_invite.save()

View file

@ -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"<style[^>]*>.*?</style>", "", 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