You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@allura.apache.org by gc...@apache.org on 2026/03/19 22:44:01 UTC

(allura) branch gc/8596 updated (ed32cbaf2 -> 27dbdf7f4)

This is an automated email from the ASF dual-hosted git repository.

gcruz pushed a change to branch gc/8596
in repository https://gitbox.apache.org/repos/asf/allura.git


 discard ed32cbaf2 fixup! [#8596] add test for email notification that send markdown content
 discard ff91998da [#8596] add test for email notification that send markdown content
 discard 0e9324b3c [#8596] escape markdown and also html in notification email templates
     add f69fd5240 bump pillow 12.1.0 -> 12.1.1
     add 3a6df67e3 updated Markdown syntax guide with a new example to make use of text rendering and updated the exmaple for lists
     add 6bc3f7275 strengthen /nf/markdown_syntax test, also avoid errors showing up on that page in dev mode
     add d16207a22 remove unneeded warning ignore
     add e3e637c2f update config notes and pytest.ini for new forgehg version
     add 3379bfd2c if MR is calculating commits, avoid saying "0 commits"
     add 7f5e7c462 [#8595] add new function to hide/redact email addresses and also shorten the acces tokens that appear printed in the logs
     add 43ce95f72 [#8595] added tests and fixed hide_email function
     add 975982a0c bump werkzeug 3.1.5 -> 3.1.6
     add 4b4a116eb [#8597] improve a test, add a failing test that check perf
     add 9ef7a9cce [#8597] add a test for link references (pure markdown feature, not related to our artifact references)
     add 6c740d212 [#8597] Fix perf by using markdown's own regexes and inheriting from existing InlineProcessors which process more incrementally with simple regexes.
     add 8030c580c [#8597] remove empty class=""
     add aa0dcd952 [#8597] minor cleanup
     add deedb0625 dependency-check upgrade joserfc 1.6.1 -> 1.6.3
     add c442bc81e [#8598] Upgrade underscore from 1.13.6 -> 1.13.8
     add 44297b597 fixup CHANGES note on underscore library upgrade
     new 2f5d536c4 [#8596] escape markdown and also html in notification email templates
     new 27dbdf7f4 [#8596] add test for email notification that send markdown content

This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version.  This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:

 * -- * -- B -- O -- O -- O   (ed32cbaf2)
            \
             N -- N -- N   refs/heads/gc/8596 (27dbdf7f4)

You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.

Any revisions marked "omit" are not gone; other references still
refer to them.  Any revisions marked "discard" are gone forever.

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .eslintignore-es5                                  |   2 +-
 Allura/LICENSE                                     |   2 +-
 Allura/allura/controllers/rest.py                  |   4 +-
 Allura/allura/lib/markdown_extensions.py           | 178 ++++++++++-----------
 Allura/allura/lib/utils.py                         |  55 +++++++
 Allura/allura/model/auth.py                        |   8 +-
 .../allura/public/nf/js/underscore-1.13.6.min.js   |   6 -
 .../allura/public/nf/js/underscore-1.13.8.min.js   |   6 +
 Allura/allura/templates/jinja_master/lib.html      |  27 +++-
 Allura/allura/templates/jinja_master/top_nav.html  |   2 +-
 Allura/allura/templates/repo/merge_request.html    |   7 +-
 .../templates_responsive/jinja_master/top_nav.html |   2 +-
 Allura/allura/tests/functional/test_newforge.py    |   2 +
 Allura/allura/tests/model/test_notification.py     |   8 +-
 Allura/allura/tests/test_globals.py                |  59 +++++--
 Allura/allura/tests/test_utils.py                  |   6 +
 Allura/development.ini                             |   3 +
 Brocfile.js                                        |   2 +-
 CHANGES                                            |   1 +
 ForgeBlog/forgeblog/tests/unit/test_blog_post.py   |   2 +-
 ForgeWiki/forgewiki/wiki_main.py                   |   4 +-
 LICENSE                                            |   2 +-
 pytest.ini                                         |   4 -
 rat-excludes.txt                                   |   2 +-
 requirements.txt                                   |   6 +-
 25 files changed, 260 insertions(+), 140 deletions(-)
 delete mode 100644 Allura/allura/public/nf/js/underscore-1.13.6.min.js
 create mode 100644 Allura/allura/public/nf/js/underscore-1.13.8.min.js


(allura) 01/02: [#8596] escape markdown and also html in notification email templates

Posted by gc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

gcruz pushed a commit to branch gc/8596
in repository https://gitbox.apache.org/repos/asf/allura.git

commit 2f5d536c492e66999e4e54009054cb9627421ce0
Author: Guillermo Cruz <gu...@slashdotmedia.com>
AuthorDate: Fri Feb 13 10:12:34 2026 -0700

    [#8596] escape markdown and also html in notification email templates
---
 Allura/allura/config/app_cfg.py                         |  1 +
 Allura/allura/lib/helpers.py                            | 17 +++++++++++++++++
 Allura/allura/model/notification.py                     |  2 ++
 Allura/allura/tasks/mail_tasks.py                       |  1 -
 Allura/allura/templates/mail/Discussion.txt             |  2 +-
 Allura/allura/templates/mail/MergeRequest.txt           |  2 +-
 Allura/allura/templates/mail/Ticket.txt                 | 14 +++++++-------
 Allura/allura/templates/mail/bulk_export.html           |  6 +++---
 Allura/allura/templates/mail/claimed_existing_email.txt |  2 +-
 Allura/allura/templates/mail/commits.md                 |  4 ++--
 Allura/allura/templates/mail/email_added.md             |  4 ++--
 Allura/allura/templates/mail/email_removed.md           |  4 ++--
 Allura/allura/templates/mail/footer.txt                 |  2 +-
 Allura/allura/templates/mail/monitor_email_footer.txt   |  2 +-
 Allura/allura/templates/mail/password_changed.md        |  4 ++--
 Allura/allura/templates/mail/primary_email_changed.md   |  4 ++--
 Allura/allura/templates/mail/twofactor_disabled.md      |  2 +-
 Allura/allura/templates/mail/twofactor_enabled.md       |  2 +-
 Allura/allura/templates/mail/usermentions_email.md      |  2 +-
 19 files changed, 48 insertions(+), 29 deletions(-)

diff --git a/Allura/allura/config/app_cfg.py b/Allura/allura/config/app_cfg.py
index 01df3da77..db663dbbf 100644
--- a/Allura/allura/config/app_cfg.py
+++ b/Allura/allura/config/app_cfg.py
@@ -190,6 +190,7 @@ def create(cls, config, app_globals):
         jinja2_env.filters['nl2br'] = helpers.nl2br_jinja_filter
         jinja2_env.filters['subrender'] = helpers.subrender_jinja_filter
         jinja2_env.filters['safe_html'] = helpers.clean_html
+        jinja2_env.filters['escape_markdown'] = helpers.escape_markdown
         jinja2_env.globals.update({
             'hasattr': hasattr,
             'h': helpers,
diff --git a/Allura/allura/lib/helpers.py b/Allura/allura/lib/helpers.py
index bc2f8cee2..8e1890168 100644
--- a/Allura/allura/lib/helpers.py
+++ b/Allura/allura/lib/helpers.py
@@ -74,6 +74,7 @@
 import urllib.parse as urlparse
 from urllib.parse import urlencode
 import math
+import markdown
 
 # import to make available to templates, don't delete:
 from .security import has_access, is_allowed_by_role, is_site_admin  # noqa: F401 RUF100
@@ -1434,3 +1435,19 @@ def parse_fediverse_address(username: str):
 def clean_html(value: str) -> Markup:
     from allura.lib.markdown_extensions import HTMLSanitizer
     return Markup(HTMLSanitizer().run(value))  # noqa: S704
+
+
+def escape_markdown(content: str) -> str:
+    if content is None:
+        return ''
+    md = markdown.Markdown()
+    escaped_chars = md.ESCAPED_CHARS
+    if isinstance(escaped_chars, (list, tuple, set)):
+        escaped_chars = "".join(escaped_chars)
+    else:
+        escaped_chars = str(escaped_chars)
+    # escape html tag like <b> or <img>
+    html_escaped = html.escape(content, quote=True)
+    pattern = re.compile(rf"([{re.escape(escaped_chars)}])")
+    # Escape markdown special characters to prevent unintended formatting
+    return pattern.sub(r"\\\1", html_escaped)
diff --git a/Allura/allura/model/notification.py b/Allura/allura/model/notification.py
index 337411c3d..0cfe7ee93 100644
--- a/Allura/allura/model/notification.py
+++ b/Allura/allura/model/notification.py
@@ -111,6 +111,7 @@ class __mongometa__:
         loader=jinja2.PackageLoader('allura', 'templates'),
         auto_reload=asbool(config.get('auto_reload_templates', True)),
     )
+    view.filters['escape_markdown'] = h.escape_markdown
 
     @classmethod
     def post(cls, artifact, topic, additional_artifacts_to_match_subscriptions=None, **kw):
@@ -689,6 +690,7 @@ class MailFooter:
         loader=jinja2.PackageLoader('allura', 'templates'),
         auto_reload=asbool(config.get('auto_reload_templates', True)),
     )
+    view.filters['escape_markdown'] = h.escape_markdown
 
     @classmethod
     def _render(cls, template, **kw):
diff --git a/Allura/allura/tasks/mail_tasks.py b/Allura/allura/tasks/mail_tasks.py
index 75523b444..ea97f06b2 100644
--- a/Allura/allura/tasks/mail_tasks.py
+++ b/Allura/allura/tasks/mail_tasks.py
@@ -128,7 +128,6 @@ def replace_html(matchobj):
                         )
     plain_text = html.unescape(plain_text)  # put literal HTML tags back into plaintext
     plain_msg = mail_util.encode_email_part(plain_text, 'plain')
-
     html_text = ForgeMarkdown(email=True).convert(text)
     if metalink:
         html_text = html_text + mail_meta_content(metalink)
diff --git a/Allura/allura/templates/mail/Discussion.txt b/Allura/allura/templates/mail/Discussion.txt
index 21bfb51f2..f9e66b219 100644
--- a/Allura/allura/templates/mail/Discussion.txt
+++ b/Allura/allura/templates/mail/Discussion.txt
@@ -20,4 +20,4 @@
 
 ---
 
-[{{post.thread.subject|e}}]({{h.absurl(post.url_paginated())}})
+[{{post.thread.subject|escape_markdown}}]({{h.absurl(post.url_paginated())}})
diff --git a/Allura/allura/templates/mail/MergeRequest.txt b/Allura/allura/templates/mail/MergeRequest.txt
index 3302abaa1..3ade1bdaa 100644
--- a/Allura/allura/templates/mail/MergeRequest.txt
+++ b/Allura/allura/templates/mail/MergeRequest.txt
@@ -20,6 +20,6 @@
 
 ---
 
-{{ data.creator_name }} has requested to merge changes from `{{ data.downstream_repo.clone_url_first(anon=False) }}` at commit `{{data.downstream_repo.shorthand_for_commit(data.downstream.commit_id)}}` into the branch `{{ data.target_branch }}`
+{{ data.creator_name|escape_markdown }} has requested to merge changes from `{{ data.downstream_repo.clone_url_first(anon=False) }}` at commit `{{data.downstream_repo.shorthand_for_commit(data.downstream.commit_id)}}` into the branch `{{ data.target_branch|escape_markdown }}`
 
 {{ data.description }}
diff --git a/Allura/allura/templates/mail/Ticket.txt b/Allura/allura/templates/mail/Ticket.txt
index fc5b4ae30..e73d9e5cc 100644
--- a/Allura/allura/templates/mail/Ticket.txt
+++ b/Allura/allura/templates/mail/Ticket.txt
@@ -20,25 +20,25 @@
 
 ---
 
-**[{{data.app_config.options.mount_point}}:#{{data.ticket_num}}] {{data.summary|e}}**
+**[{{data.app_config.options.mount_point}}:#{{data.ticket_num}}] {{data.summary|escape_markdown}}**
 
-**Status:** {{data.status}}
+**Status:** {{data.status|escape_markdown}}
 {% for f in data.globals.milestone_fields -%}
-  **{{ f.label }}:** {{ data.custom_fields.get(f.name, '') }}
+  **{{ f.label|escape_markdown }}:** {{ data.custom_fields.get(f.name, '')|escape_markdown }}
 {% endfor -%}
 {% if data.labels.__len__() -%}
-    **Labels:** {% for label in data.labels %}{{label}} {% else %}None{% endfor %}
+    **Labels:** {% for label in data.labels %}{{label|escape_markdown}} {% else %}None{% endfor %}
 {% endif -%}
-**Created:** {{data.created_date.strftime('%a %b %d, %Y %I:%M %p UTC')}} by {{data.reported_by.display_name}}
+**Created:** {{data.created_date.strftime('%a %b %d, %Y %I:%M %p UTC')}} by {{data.reported_by.display_name|escape_markdown}}
 {% if (data.mod_date - data.created_date).days >= 0 -%}
     **Last Updated:** {{data.mod_date.strftime('%a %b %d, %Y %I:%M %p UTC')}}
 {% endif -%}
-**Owner:** {{data.assigned_to_name()}}
+**Owner:** {{data.assigned_to_name()|escape_markdown}}
 {% if data.attachments -%}
     **Attachments:**
 
 {% for att in data.attachments -%}
-    - [{{att.filename}}]({{h.absurl(att.url())}}) ({{h.do_filesizeformat(att.length)}}; {{att.content_type}})
+    - [{{att.filename|escape_markdown}}]({{h.absurl(att.url())}}) ({{h.do_filesizeformat(att.length)}}; {{att.content_type}})
 {% endfor -%}
 {% endif %}
 
diff --git a/Allura/allura/templates/mail/bulk_export.html b/Allura/allura/templates/mail/bulk_export.html
index e8aded10f..b135041ac 100644
--- a/Allura/allura/templates/mail/bulk_export.html
+++ b/Allura/allura/templates/mail/bulk_export.html
@@ -17,19 +17,19 @@
        under the License.
 #}
 
-The bulk export for project {{ project.shortname }} is completed.
+The bulk export for project {{ project.shortname|escape_markdown }} is completed.
 
 {% if tools %}
 The following tools were exported:
 {% for tool in tools -%}
-- {{ tool }}
+- {{ tool|escape_markdown }}
 {% endfor %}
 {% endif %}
 
 {% if not_exported_tools %}
 The following tools were not exported:
 {% for tool in not_exported_tools -%}
-- {{ tool }}
+- {{ tool|escape_markdown }}
 {% endfor %}
 {% endif %}
 
diff --git a/Allura/allura/templates/mail/claimed_existing_email.txt b/Allura/allura/templates/mail/claimed_existing_email.txt
index ead38a808..cda4ceca9 100644
--- a/Allura/allura/templates/mail/claimed_existing_email.txt
+++ b/Allura/allura/templates/mail/claimed_existing_email.txt
@@ -17,6 +17,6 @@
        under the License.
 #}
 
-You tried to add {{ email.email }} to your {{ config['site_name'] }} account, but it is already claimed by your {{ user.username }} account.
+You tried to add {{ email.email }} to your {{ config['site_name'] }} account, but it is already claimed by your {{ user.username|escape_markdown }} account.
 You should use that account instead, or remove that address from that account.
 If this was not you who attempted this, you can safely ignore this email.
diff --git a/Allura/allura/templates/mail/commits.md b/Allura/allura/templates/mail/commits.md
index 1b1d043c4..e61b5b3a7 100644
--- a/Allura/allura/templates/mail/commits.md
+++ b/Allura/allura/templates/mail/commits.md
@@ -18,11 +18,11 @@
 --> #}
 {%- for cm in commit_msgs[:max_num_commits] %}
 {%- if cm.branches and cm.show_branch_name|default %}
-## Branch: {% for b in cm.branches %}{{ b }}  {% endfor%}
+## Branch: {% for b in cm.branches %}{{ b|escape_markdown }}  {% endfor%}
 {% endif %}
 {{ cm.summary }}
 
-By {{ cm.author }} on {{ cm.date }}
+By {{ cm.author|escape_markdown }} on {{ cm.date }}
 [**View Changes**]({{ cm.commit_url }})
 
 {% if not loop.last %}-----{% endif %}
diff --git a/Allura/allura/templates/mail/email_added.md b/Allura/allura/templates/mail/email_added.md
index eace71880..3090c43ff 100644
--- a/Allura/allura/templates/mail/email_added.md
+++ b/Allura/allura/templates/mail/email_added.md
@@ -17,9 +17,9 @@
        under the License.
 -#}
 
-Hello {{ user.display_name }},
+Hello {{ user.display_name|escape_markdown }},
 
-A new email address was added to your {{ config['site_name'] }} account "{{ user.username }}":  
+A new email address was added to your {{ config['site_name'] }} account "{{ user.username|escape_markdown }}":  
 
 * {{ addr }}
 
diff --git a/Allura/allura/templates/mail/email_removed.md b/Allura/allura/templates/mail/email_removed.md
index 323ea17e2..2758eca70 100644
--- a/Allura/allura/templates/mail/email_removed.md
+++ b/Allura/allura/templates/mail/email_removed.md
@@ -17,9 +17,9 @@
        under the License.
 -#}
 
-Hello {{ user.display_name }},
+Hello {{ user.display_name|escape_markdown }},
 
-An email address was removed from your {{ config['site_name'] }} account "{{ user.username }}":  
+An email address was removed from your {{ config['site_name'] }} account "{{ user.username|escape_markdown }}":  
 
 * {{ addr }} 
 
diff --git a/Allura/allura/templates/mail/footer.txt b/Allura/allura/templates/mail/footer.txt
index 62de8623f..08a7e08b8 100644
--- a/Allura/allura/templates/mail/footer.txt
+++ b/Allura/allura/templates/mail/footer.txt
@@ -20,7 +20,7 @@
 
 ---
 
-Sent from {{domain}} because you indicated interest in <{{ prefix }}{{ notification.link }}>
+Sent from {{domain|escape_markdown}} because you indicated interest in <{{ prefix }}{{ notification.link }}>
 
 {% if discussion_disabled %}
 Please do not reply to this message. Discussion is disabled for the <{{ prefix }}{{ notification.link }}>
diff --git a/Allura/allura/templates/mail/monitor_email_footer.txt b/Allura/allura/templates/mail/monitor_email_footer.txt
index d3492ca37..f2290e12c 100644
--- a/Allura/allura/templates/mail/monitor_email_footer.txt
+++ b/Allura/allura/templates/mail/monitor_email_footer.txt
@@ -20,6 +20,6 @@
 
 ---
 
-Sent from {{domain}} because {{email}} is subscribed to {{app_url}}
+Sent from {{domain}} because {{email|escape_markdown}} is subscribed to {{app_url}}
 
 To unsubscribe from further messages, a project admin can change settings at {{setting_url}}.  Or, if this is a mailing list, you can unsubscribe from the mailing list.
diff --git a/Allura/allura/templates/mail/password_changed.md b/Allura/allura/templates/mail/password_changed.md
index 10e062ff0..db6cc4059 100644
--- a/Allura/allura/templates/mail/password_changed.md
+++ b/Allura/allura/templates/mail/password_changed.md
@@ -17,9 +17,9 @@
        under the License.
 -#}
 
-Hello {{ user.display_name }},
+Hello {{ user.display_name|escape_markdown }},
 
-The password for your {{ config['site_name'] }} account "{{ user.username }}" has been changed.  This is a confirmation email, you are all set.
+The password for your {{ config['site_name'] }} account "{{ user.username|escape_markdown }}" has been changed.  This is a confirmation email, you are all set.
 
 {% block footer %}
 If you did not do this, please contact us immediately.
diff --git a/Allura/allura/templates/mail/primary_email_changed.md b/Allura/allura/templates/mail/primary_email_changed.md
index 2830b1c2f..fd6dec35b 100644
--- a/Allura/allura/templates/mail/primary_email_changed.md
+++ b/Allura/allura/templates/mail/primary_email_changed.md
@@ -17,9 +17,9 @@
        under the License.
 -#}
 
-Hello {{ user.display_name }},
+Hello {{ user.display_name|escape_markdown }},
 
-The primary email address on your {{ config['site_name'] }} account "{{ user.username }}" was changed to:  
+The primary email address on your {{ config['site_name'] }} account "{{ user.username|escape_markdown }}" was changed to:  
 
 * {{ addr }}
 
diff --git a/Allura/allura/templates/mail/twofactor_disabled.md b/Allura/allura/templates/mail/twofactor_disabled.md
index 3e4e619b0..e7e0a868f 100644
--- a/Allura/allura/templates/mail/twofactor_disabled.md
+++ b/Allura/allura/templates/mail/twofactor_disabled.md
@@ -17,7 +17,7 @@
        under the License.
 -#}
 
-Hello {{ user.username }},
+Hello {{ user.username|escape_markdown }},
 
 You have recently disabled two-factor authentication on {{ config['site_name'] }}.  :(
 
diff --git a/Allura/allura/templates/mail/twofactor_enabled.md b/Allura/allura/templates/mail/twofactor_enabled.md
index e2d0ca06f..6e0734349 100644
--- a/Allura/allura/templates/mail/twofactor_enabled.md
+++ b/Allura/allura/templates/mail/twofactor_enabled.md
@@ -17,7 +17,7 @@
        under the License.
 -#}
 
-Hello {{ user.username }},
+Hello {{ user.username|escape_markdown }},
 
 You have recently set up new two-factor authentication on {{ config['site_name'] }}.  This is a confirmation email, you are all set.
 
diff --git a/Allura/allura/templates/mail/usermentions_email.md b/Allura/allura/templates/mail/usermentions_email.md
index 98763ca69..836c315a1 100644
--- a/Allura/allura/templates/mail/usermentions_email.md
+++ b/Allura/allura/templates/mail/usermentions_email.md
@@ -16,7 +16,7 @@
        specific language governing permissions and limitations
        under the License.
 -#}
-Your name was mentioned at [{{artifact_linktext}}]({{artifact_link}}) by {{mentioned_by.display_name}}
+Your name was mentioned at [{{artifact_linktext|escape_markdown}}]({{artifact_link}}) by {{mentioned_by.display_name|escape_markdown}}
 
 It can be viewed using the link below:
 {{artifact_link}}


(allura) 02/02: [#8596] add test for email notification that send markdown content

Posted by gc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

gcruz pushed a commit to branch gc/8596
in repository https://gitbox.apache.org/repos/asf/allura.git

commit 27dbdf7f4a22ffe13703315adccb8e9ad81b6f78
Author: Guillermo Cruz <gu...@slashdotmedia.com>
AuthorDate: Thu Mar 12 19:36:41 2026 -0600

    [#8596] add test for email notification that send markdown content
---
 Allura/allura/lib/helpers.py                       |  9 +--
 .../templates/repo/merge_request_changed.html      |  2 +-
 Allura/allura/tests/model/test_notification.py     | 81 +++++++++++++++++++++-
 ForgeTracker/forgetracker/data/mass_report.html    |  2 +-
 ForgeTracker/forgetracker/data/ticket_changed.html |  6 +-
 5 files changed, 85 insertions(+), 15 deletions(-)

diff --git a/Allura/allura/lib/helpers.py b/Allura/allura/lib/helpers.py
index 8e1890168..ecb6d063c 100644
--- a/Allura/allura/lib/helpers.py
+++ b/Allura/allura/lib/helpers.py
@@ -1440,14 +1440,9 @@ def clean_html(value: str) -> Markup:
 def escape_markdown(content: str) -> str:
     if content is None:
         return ''
-    md = markdown.Markdown()
-    escaped_chars = md.ESCAPED_CHARS
-    if isinstance(escaped_chars, (list, tuple, set)):
-        escaped_chars = "".join(escaped_chars)
-    else:
-        escaped_chars = str(escaped_chars)
+    escaped_chars = r"\\`*_{}[]()#!"
     # escape html tag like <b> or <img>
-    html_escaped = html.escape(content, quote=True)
+    html_escaped = html.escape(content, quote=False)
     pattern = re.compile(rf"([{re.escape(escaped_chars)}])")
     # Escape markdown special characters to prevent unintended formatting
     return pattern.sub(r"\\\1", html_escaped)
diff --git a/Allura/allura/templates/repo/merge_request_changed.html b/Allura/allura/templates/repo/merge_request_changed.html
index 479bab316..2bba7a005 100644
--- a/Allura/allura/templates/repo/merge_request_changed.html
+++ b/Allura/allura/templates/repo/merge_request_changed.html
@@ -27,6 +27,6 @@ Diff:
 ~~~~
 
 {% else %}
-- **{{ field }}**: {{ values[0] }} --> {{ values[1] }}
+- **{{ field }}**: {{ values[0] |escape_markdown }} --> {{ values[1] |escape_markdown }}
 {% endif %}
 {% endfor %}
diff --git a/Allura/allura/tests/model/test_notification.py b/Allura/allura/tests/model/test_notification.py
index 42b14728c..22c2557ce 100644
--- a/Allura/allura/tests/model/test_notification.py
+++ b/Allura/allura/tests/model/test_notification.py
@@ -17,7 +17,7 @@
 
 from datetime import timedelta
 import collections
-
+import email.iterators
 import pytest
 from tg import tmpl_context as c, app_globals as g
 from ming.odm import ThreadLocalODMSession
@@ -30,6 +30,7 @@
 from allura.lib import helpers as h
 from allura.tests import decorators as td
 from forgewiki import model as WM
+from forgetracker import model as TM
 
 
 class TestNotification:
@@ -284,13 +285,87 @@ def test_footer(self):
         footer = MailFooter.standard(M.Notification())
         assert 'Sent from localhost because you indicated interest' in footer
 
+    @pytest.mark.parametrize('markdown, result',
+                             [('![img](https://static.vecteezy.com/system/resources/thumbnails/012/042/301/small_2x/warning-sign-icon-transparent-background-free-png.png)',
+                               'src="https://static.vecteezy.com/system/'),
+                                 ('[link text](https://example.com)', 'href="https://example.com"'),
+                             ])
+    def test_email_markdown_not_parsed(self, markdown, result):
+        """
+        strings like `![img](url)` in Wiki notification body should be converted to HTML
+        (e.g., `<img ...>`).
+        """
+        self._subscribe()  # current user: test-admin
+        self._post_notification(text=markdown, subject=markdown, link='https://example.com')
+        M.MonQTask.run_ready()
+        ThreadLocalODMSession.flush_all()
+
+        with mock.patch('allura.tasks.mail_tasks.smtp_client.sendmail') as sendmail:
+            while M.MonQTask.run_ready():
+                ThreadLocalODMSession.flush_all()
+                # have to run them all multiple times since one task creates another
+                pass
+
+        multipart_msg = sendmail.call_args_list[0][0][6]
+        html_body = next(email.iterators.typed_subpart_iterator(multipart_msg, 'text', 'html'))\
+            .get_payload(decode=True).decode('utf-8')
+        assert result in html_body
+
     def _subscribe(self, **kw):
         self.pg.subscribe(type='direct', **kw)
         ThreadLocalODMSession.flush_all()
         ThreadLocalODMSession.close_all()
 
-    def _post_notification(self):
-        return M.Notification.post(self.pg, 'metadata')
+    def _post_notification(self, **kwargs):
+        return M.Notification.post(self.pg, 'metadata', **kwargs)
+
+
+class TestTicketNotifications(TestPostNotifications):
+
+    def setup_method(self, method):
+        super().setup_method(method)
+        self.setup_with_tools()
+
+    @td.with_tracker
+    def setup_with_tools(self):
+        setup_global_objects()
+        g.set_app('bugs')
+        ticket = TM.Ticket.new()
+        ticket.summary = 'test ticket'
+        ThreadLocalODMSession.flush_all()
+        self.pg = ticket
+        M.notification.MAILBOX_QUIESCENT = None  # disable message combining
+        while M.MonQTask.run_ready('setup'):
+            ThreadLocalODMSession.flush_all()
+
+    @pytest.mark.parametrize('markdown, rendered_snippet',
+        [('![img](https://static.vecteezy.com/system/resources/thumbnails/012/042/301/small_2x/warning-sign-icon-transparent-background-free-png.png)', '<img',), ('[link text](https://example.com)', '<a',), ])
+    def test_ticket_email_markdown_not_parsed(self, markdown, rendered_snippet):
+        self._subscribe()
+        body_link = '[main link](https://wesbite.com)'
+        body_link_rendered = '<a class="" href="https://wesbite.com" rel="nofollow">main link</a>'
+        self.pg.summary = markdown  # should not render to html
+        self.pg.status = markdown  # should not render to html
+        self.pg.ticket_num = getattr(self.pg, 'ticket_num', 1) or 1
+        self.pg.description = body_link  # should render to html
+        self.pg.app_config.options.mount_point = getattr(self.pg.app_config.options, 'mount_point', 'bugs') or 'bugs'
+        ThreadLocalODMSession.flush_all()
+        self._post_notification(text=markdown, subject=markdown, link='https://example.com')
+        while M.MonQTask.run_ready():
+            ThreadLocalODMSession.flush_all()
+
+        with mock.patch('allura.tasks.mail_tasks.smtp_client.sendmail') as sendmail:
+            while M.MonQTask.run_ready():
+                ThreadLocalODMSession.flush_all()
+            assert sendmail.call_count >= 1, "Expected at least one outbound email"
+            multipart_msg = sendmail.call_args_list[0][0][6]
+
+        html_body = next(email.iterators.typed_subpart_iterator(multipart_msg, 'text', 'html')).get_payload(
+            decode=True).decode('utf-8')
+        # Raw markdown must appear in some parts the html body
+        assert markdown in html_body
+        # And also the rendered html should appear in the body
+        assert body_link_rendered in html_body
 
 
 class TestSubscriptionTypes:
diff --git a/ForgeTracker/forgetracker/data/mass_report.html b/ForgeTracker/forgetracker/data/mass_report.html
index 70fe1f705..8aac69b45 100644
--- a/ForgeTracker/forgetracker/data/mass_report.html
+++ b/ForgeTracker/forgetracker/data/mass_report.html
@@ -19,7 +19,7 @@
 {{ data.header }}
 
 {% for ticket, change_text in data.changes %}
-ticket: {{ context.app.config.options.mount_point }}:#{{ ticket.ticket_num }} {{ ticket.summary }}
+ticket: {{ context.app.config.options.mount_point }}:#{{ ticket.ticket_num }} {{ ticket.summary |escape_markdown }}
 
 {{ change_text }}
 {% endfor %}
diff --git a/ForgeTracker/forgetracker/data/ticket_changed.html b/ForgeTracker/forgetracker/data/ticket_changed.html
index 668b08dea..8b922149b 100644
--- a/ForgeTracker/forgetracker/data/ticket_changed.html
+++ b/ForgeTracker/forgetracker/data/ticket_changed.html
@@ -31,11 +31,11 @@ Diff:
 {% else -%}
 {% set oldv, newv = values -%}
 {% if key == 'assigned_to' or oldv.type_s == 'User' or newv.type_s == 'User' -%}
-- **{{key}}**: {% if oldv %}{{oldv.get_pref('display_name')}} --> {% endif %}{% if newv %}{{newv.get_pref('display_name')}}{% else %} nobody {% endif %}
+- **{{key}}**: {% if oldv %}{{oldv.get_pref('display_name') |escape_markdown }} --> {% endif %}{% if newv %}{{newv.get_pref('display_name') |escape_markdown }}{% else %} nobody {% endif %}
 {% elif key == 'labels' -%}
-- **{{key}}**: {{', '.join(oldv)}} --> {{', '.join(newv)}}
+- **{{key}}**: {{', '.join(oldv) |escape_markdown }} --> {{', '.join(newv) |escape_markdown }}
 {% else -%}
-- **{{key}}**: {{oldv if oldv is not none else ''}} --> {{newv if newv is not none else ''}}
+- **{{key}}**: {{oldv |escape_markdown if oldv is not none else ''}} --> {{newv |escape_markdown if newv is not none else ''}}
 {% endif -%}
 {% endif -%}
 {% endfor -%}