From f6d265b2efb5535cb5ce31fd7cf6d00a22564f59 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Thu, 13 Jul 2017 18:37:28 +0530 Subject: [PATCH] Email inline_images enhancement, header (#3682) * minor refactor * update user.py to use new sendmail api * On-demand attachments in email * Replace inline_images by just specifying path * Add header flag in frappe.sendmail * Inline images can now be attached from assets/, files/ and private/files/ * Update tests * Fix email_account test --- frappe/__init__.py | 5 +- frappe/core/doctype/user/user.py | 12 +- .../doctype/email_queue/email_queue.json | 51 ++++- frappe/email/doctype/newsletter/newsletter.py | 5 +- frappe/email/email_body.py | 205 ++++++++++++------ frappe/email/queue.py | 52 ++++- frappe/email/test_email_body.py | 24 +- frappe/templates/emails/password_update.html | 2 +- 8 files changed, 260 insertions(+), 96 deletions(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index 7fa302f763..9b31e15fee 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -380,7 +380,7 @@ def sendmail(recipients=[], sender="", subject="No Subject", message="No Message attachments=None, content=None, doctype=None, name=None, reply_to=None, cc=[], message_id=None, in_reply_to=None, send_after=None, expose_recipients=None, send_priority=1, communication=None, retry=1, now=None, read_receipt=None, is_notification=False, - inline_images=None, template=None, args=None): + inline_images=None, template=None, args=None, header=False): """Send email using user's default **Email Account** or global default **Email Account**. @@ -405,6 +405,7 @@ def sendmail(recipients=[], sender="", subject="No Subject", message="No Message :param inline_images: List of inline images as {"filename", "filecontent"}. All src properties will be replaced with random Content-Id :param template: Name of html template from templates/emails folder :param args: Arguments for rendering the template + :param header: Append header in email """ text_content = None @@ -428,7 +429,7 @@ def sendmail(recipients=[], sender="", subject="No Subject", message="No Message attachments=attachments, reply_to=reply_to, cc=cc, message_id=message_id, in_reply_to=in_reply_to, send_after=send_after, expose_recipients=expose_recipients, send_priority=send_priority, communication=communication, now=now, read_receipt=read_receipt, is_notification=is_notification, - inline_images=inline_images) + inline_images=inline_images, header=header) whitelisted = [] guest_methods = [] diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index c91c876680..487cb3fb11 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -225,11 +225,11 @@ class User(Document): def password_reset_mail(self, link): self.send_login_mail(_("Password Reset"), - "templates/emails/password_reset.html", {"link": link}, now=True) + "password_reset", {"link": link}, now=True) def password_update_mail(self, password): self.send_login_mail(_("Password Update"), - "templates/emails/password_update.html", {"new_password": password}, now=True) + "password_update", {"new_password": password}, now=True) def send_welcome_mail_to_user(self): from frappe.utils import get_url @@ -248,7 +248,7 @@ class User(Document): else: subject = _("Complete Registration") - self.send_login_mail(subject, "templates/emails/new_user.html", + self.send_login_mail(subject, "new_user", dict( link=link, site_url=get_url(), @@ -279,7 +279,7 @@ class User(Document): sender = frappe.session.user not in STANDARD_USERS and get_formatted_email(frappe.session.user) or None frappe.sendmail(recipients=self.email, sender=sender, subject=subject, - message=frappe.get_template(template).render(args), + template=template, args=args, delayed=(not now) if now!=None else self.flags.delay_emails, retry=3) def a_system_manager_should_exist(self): @@ -547,7 +547,7 @@ def update_password(new_password, key=None, old_password=None): def test_password_strength(new_password, key=None, old_password=None, user_data=[]): from frappe.utils.password_strength import test_password_strength as _test_password_strength - password_policy = frappe.db.get_value("System Settings", None, + password_policy = frappe.db.get_value("System Settings", None, ["enable_password_policy", "minimum_password_score"], as_dict=True) or {} enable_password_policy = cint(password_policy.get("enable_password_policy", 0)) @@ -557,7 +557,7 @@ def test_password_strength(new_password, key=None, old_password=None, user_data= return {} if not user_data: - user_data = frappe.db.get_value('User', frappe.session.user, + user_data = frappe.db.get_value('User', frappe.session.user, ['first_name', 'middle_name', 'last_name', 'email', 'birth_date']) if new_password: diff --git a/frappe/email/doctype/email_queue/email_queue.json b/frappe/email/doctype/email_queue/email_queue.json index ff09b44f36..4445f60a02 100644 --- a/frappe/email/doctype/email_queue/email_queue.json +++ b/frappe/email/doctype/email_queue/email_queue.json @@ -1,5 +1,6 @@ { "allow_copy": 0, + "allow_guest_to_view": 0, "allow_import": 0, "allow_rename": 0, "autoname": "hash", @@ -14,6 +15,7 @@ "engine": "InnoDB", "fields": [ { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -43,6 +45,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -72,6 +75,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -101,6 +105,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -129,6 +134,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -159,6 +165,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -187,6 +194,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -216,6 +224,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -245,6 +254,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -273,6 +283,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -303,6 +314,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -332,6 +344,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -362,6 +375,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -392,6 +406,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -421,6 +436,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -450,6 +466,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -477,20 +494,50 @@ "search_index": 0, "set_only_once": 0, "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "attachments", + "fieldtype": "Code", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Attachments", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 } ], + "has_web_view": 0, "hide_heading": 0, "hide_toolbar": 0, "icon": "fa fa-envelope", "idx": 1, "image_view": 0, "in_create": 1, - "in_dialog": 0, "is_submittable": 0, "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2017-02-24 17:42:10.878546", + "modified": "2017-07-07 16:29:15.780393", "modified_by": "Administrator", "module": "Email", "name": "Email Queue", diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py index a54ab28c8d..29ecbee853 100755 --- a/frappe/email/doctype/newsletter/newsletter.py +++ b/frappe/email/doctype/newsletter/newsletter.py @@ -70,8 +70,9 @@ class Newsletter(Document): for file in files: try: - file = get_file(file.name) - attachments.append({"fname": file[0], "fcontent": file[1]}) + # these attachments will be attached on-demand + # and won't be stored in the message + attachments.append({"fid": file.name}) except IOError: frappe.throw(_("Unable to find attachment {0}").format(a)) diff --git a/frappe/email/email_body.py b/frappe/email/email_body.py index 290735361d..41753dbaf7 100755 --- a/frappe/email/email_body.py +++ b/frappe/email/email_body.py @@ -2,7 +2,7 @@ # MIT License. See license.txt from __future__ import unicode_literals -import frappe, re +import frappe, re, os from frappe.utils.pdf import get_pdf from frappe.email.smtp import get_outgoing_email_account from frappe.utils import (get_url, scrub_urls, strip, expand_relative_urls, cint, @@ -15,7 +15,7 @@ from email.mime.multipart import MIMEMultipart def get_email(recipients, sender='', msg='', subject='[No Subject]', text_content = None, footer=None, print_html=None, formatted=None, attachments=None, content=None, reply_to=None, cc=[], email_account=None, expose_recipients=None, - inline_images=[]): + inline_images=[], header=False): """ Prepare an email with the following format: - multipart/mixed - multipart/alternative @@ -31,13 +31,15 @@ def get_email(recipients, sender='', msg='', subject='[No Subject]', if not content.strip().startswith("<"): content = markdown(content) - emailobj.set_html(content, text_content, footer=footer, + emailobj.set_html(content, text_content, footer=footer, header=header, print_html=print_html, formatted=formatted, inline_images=inline_images) if isinstance(attachments, dict): attachments = [attachments] for attach in (attachments or []): + # cannot attach if no filecontent + if attach.get('fcontent') is None: continue emailobj.add_attachment(**attach) return emailobj @@ -74,10 +76,11 @@ class EMail: self.email_account = email_account or get_outgoing_email_account() def set_html(self, message, text_content = None, footer=None, print_html=None, - formatted=None, inline_images=None): + formatted=None, inline_images=None, header=False): """Attach message in the html portion of multipart/alternative""" if not formatted: - formatted = get_formatted_html(self.subject, message, footer, print_html, email_account=self.email_account) + formatted = get_formatted_html(self.subject, message, footer, print_html, + email_account=self.email_account, header=header) # this is the first html part of a multi-part message, # convert to text well @@ -100,21 +103,12 @@ class EMail: def set_part_html(self, message, inline_images): from email.mime.text import MIMEText - if inline_images: + + has_inline_images = re.search('''embed=['"].*?['"]''', message) + + if has_inline_images: # process inline images - _inline_images = [] - for image in inline_images: - # images in dict like {filename:'', filecontent:'raw'} - - content_id = random_string(10) - message = replace_filename_with_cid(message, - image.get('filename'), content_id) - - _inline_images.append({ - 'filename': image.get('filename'), - 'filecontent': image.get('filecontent'), - 'content_id': content_id - }) + message, _inline_images = replace_filename_with_cid(message) # prepare parts msg_related = MIMEMultipart('related') @@ -158,48 +152,11 @@ class EMail: def add_attachment(self, fname, fcontent, content_type=None, parent=None, content_id=None, inline=False): """add attachment""" - from email.mime.audio import MIMEAudio - from email.mime.base import MIMEBase - from email.mime.image import MIMEImage - from email.mime.text import MIMEText - - import mimetypes - if not content_type: - content_type, encoding = mimetypes.guess_type(fname) - - if content_type is None: - # No guess could be made, or the file is encoded (compressed), so - # use a generic bag-of-bits type. - content_type = 'application/octet-stream' - - maintype, subtype = content_type.split('/', 1) - if maintype == 'text': - # Note: we should handle calculating the charset - if isinstance(fcontent, unicode): - fcontent = fcontent.encode("utf-8") - part = MIMEText(fcontent, _subtype=subtype, _charset="utf-8") - elif maintype == 'image': - part = MIMEImage(fcontent, _subtype=subtype) - elif maintype == 'audio': - part = MIMEAudio(fcontent, _subtype=subtype) - else: - part = MIMEBase(maintype, subtype) - part.set_payload(fcontent) - # Encode the payload using Base64 - from email import encoders - encoders.encode_base64(part) - - # Set the filename parameter - if fname: - attachment_type = 'inline' if inline else 'attachment' - part.add_header(b'Content-Disposition', attachment_type, filename=fname.encode('utf=8')) - if content_id: - part.add_header(b'Content-ID', '<{0}>'.format(content_id)) if not parent: parent = self.msg_root - parent.attach(part) + add_attachment(fname, fcontent, content_type, parent, content_id, inline) def add_pdf_attachment(self, name, html, options=None): self.add_attachment(name, get_pdf(html, options), 'application/octet-stream') @@ -276,11 +233,12 @@ class EMail: self.make() return self.msg_root.as_string() -def get_formatted_html(subject, message, footer=None, print_html=None, email_account=None): +def get_formatted_html(subject, message, footer=None, print_html=None, email_account=None, header=False): if not email_account: email_account = get_outgoing_email_account(False) rendered_email = frappe.get_template("templates/emails/standard.html").render({ + "header": get_header() if header else None, "content": message, "signature": get_signature(email_account), "footer": get_footer(email_account, footer), @@ -291,6 +249,52 @@ def get_formatted_html(subject, message, footer=None, print_html=None, email_acc return scrub_urls(rendered_email) +def add_attachment(fname, fcontent, content_type=None, + parent=None, content_id=None, inline=False): + """Add attachment to parent which must an email object""" + from email.mime.audio import MIMEAudio + from email.mime.base import MIMEBase + from email.mime.image import MIMEImage + from email.mime.text import MIMEText + + import mimetypes + if not content_type: + content_type, encoding = mimetypes.guess_type(fname) + + if not parent: + return + + if content_type is None: + # No guess could be made, or the file is encoded (compressed), so + # use a generic bag-of-bits type. + content_type = 'application/octet-stream' + + maintype, subtype = content_type.split('/', 1) + if maintype == 'text': + # Note: we should handle calculating the charset + if isinstance(fcontent, unicode): + fcontent = fcontent.encode("utf-8") + part = MIMEText(fcontent, _subtype=subtype, _charset="utf-8") + elif maintype == 'image': + part = MIMEImage(fcontent, _subtype=subtype) + elif maintype == 'audio': + part = MIMEAudio(fcontent, _subtype=subtype) + else: + part = MIMEBase(maintype, subtype) + part.set_payload(fcontent) + # Encode the payload using Base64 + from email import encoders + encoders.encode_base64(part) + + # Set the filename parameter + if fname: + attachment_type = 'inline' if inline else 'attachment' + part.add_header(b'Content-Disposition', attachment_type, filename=fname.encode('utf=8')) + if content_id: + part.add_header(b'Content-ID', '<{0}>'.format(content_id)) + + parent.attach(part) + def get_message_id(): '''Returns Message ID created from doctype and name''' return "<{unique}@{site}>".format( @@ -329,11 +333,86 @@ def get_footer(email_account, footer=None): return footer -def replace_filename_with_cid(message, filename, content_id): - """ Replaces with - +def replace_filename_with_cid(message): + """ Replaces with + and return the modified message and + a list of inline_images with {filename, filecontent, content_id} """ - message = re.sub('''embed=['"]{0}['"]'''.format(filename), + + inline_images = [] + + while True: + matches = re.search('''embed=["'](.*?)["']''', message) + if not matches: break + groups = matches.groups() + + # found match + img_path = groups[0] + filename = img_path.rsplit('/')[-1] + + filecontent = get_filecontent_from_path(img_path) + if not filecontent: + message = re.sub('''embed=['"]{0}['"]'''.format(img_path), '', message) + continue + + content_id = random_string(10) + + inline_images.append({ + 'filename': filename, + 'filecontent': filecontent, + 'content_id': content_id + }) + + message = re.sub('''embed=['"]{0}['"]'''.format(img_path), 'src="cid:{0}"'.format(content_id), message) - return message + return (message, inline_images) + +def get_filecontent_from_path(path): + if not path: return + + if path.startswith('/'): + path = path[1:] + + if path.startswith('assets/'): + # from public folder + full_path = os.path.abspath(path) + elif path.startswith('files/'): + # public file + full_path = frappe.get_site_path('public', path) + elif path.startswith('private/files/'): + # private file + full_path = frappe.get_site_path(path) + else: + full_path = path + + if os.path.exists(full_path): + with open(full_path) as f: + filecontent = f.read() + + return filecontent + else: + print(full_path + ' doesn\'t exists') + return None + + +def get_header(): + """ Build header from template """ + from frappe.utils.jinja import get_email_from_template + + default_brand_image = 'assets/frappe/images/favicon.png' # svg doesn't work in email + email_brand_image = frappe.get_hooks('email_brand_image') + if len(email_brand_image): + email_brand_image = email_brand_image[-1] + else: + email_brand_image = default_brand_image + + email_brand_image = default_brand_image + brand_text = frappe.get_hooks('app_title')[-1] + + email_header, text = get_email_from_template('email_header', { + 'brand_image': email_brand_image, + 'brand_text': brand_text + }) + + return email_header diff --git a/frappe/email/queue.py b/frappe/email/queue.py index 9abb9dccd2..dbbec7bd12 100755 --- a/frappe/email/queue.py +++ b/frappe/email/queue.py @@ -5,13 +5,14 @@ from __future__ import unicode_literals from six.moves import range import frappe import HTMLParser -import smtplib, quopri +import smtplib, quopri, json from frappe import msgprint, throw, _ from frappe.email.smtp import SMTPServer, get_outgoing_email_account -from frappe.email.email_body import get_email, get_formatted_html +from frappe.email.email_body import get_email, get_formatted_html, add_attachment from frappe.utils.verified_command import get_signed_params, verify_request from html2text import html2text from frappe.utils import get_url, nowdate, encode, now_datetime, add_days, split_emails, cstr, cint +from frappe.utils.file_manager import get_file from rq.timeouts import JobTimeoutException from frappe.utils.scheduler import log @@ -21,7 +22,8 @@ def send(recipients=None, sender=None, subject=None, message=None, text_content= reference_name=None, unsubscribe_method=None, unsubscribe_params=None, unsubscribe_message=None, attachments=None, reply_to=None, cc=[], message_id=None, in_reply_to=None, send_after=None, expose_recipients=None, send_priority=1, communication=None, now=False, read_receipt=None, - queue_separately=False, is_notification=False, add_unsubscribe_link=1, inline_images=None): + queue_separately=False, is_notification=False, add_unsubscribe_link=1, inline_images=None, + header=False): """Add email to sending queue (Email Queue) :param recipients: List of recipients. @@ -44,6 +46,7 @@ def send(recipients=None, sender=None, subject=None, message=None, text_content= :param is_notification: Marks email as notification so will not trigger notifications from system :param add_unsubscribe_link: Send unsubscribe link in the footer of the Email, default 1. :param inline_images: List of inline images as {"filename", "filecontent"}. All src properties will be replaced with random Content-Id + :param header: Append header in email (boolean) """ if not unsubscribe_method: unsubscribe_method = "/api/method/frappe.email.queue.unsubscribe" @@ -72,7 +75,7 @@ def send(recipients=None, sender=None, subject=None, message=None, text_content= except HTMLParser.HTMLParseError: text_content = "See html attachment" - formatted = get_formatted_html(subject, message, email_account=email_account) + formatted = get_formatted_html(subject, message, email_account=email_account, header=header) if reference_doctype and reference_name: unsubscribed = [d.email for d in frappe.db.get_all("Email Unsubscribe", "email", @@ -116,6 +119,7 @@ def send(recipients=None, sender=None, subject=None, message=None, text_content= queue_separately=queue_separately, is_notification = is_notification, inline_images = inline_images, + header=header, now=now) @@ -145,6 +149,14 @@ def get_email_queue(recipients, sender, subject, **kwargs): '''Make Email Queue object''' e = frappe.new_doc('Email Queue') e.priority = kwargs.get('send_priority') + attachments = kwargs.get('attachments') + if attachments: + # store attachments with fid, to be attached on-demand later + _attachments = [] + for att in attachments: + if att.get('fid'): + _attachments.append(att) + e.attachments = json.dumps(_attachments) try: mail = get_email(recipients, @@ -157,7 +169,8 @@ def get_email_queue(recipients, sender, subject, **kwargs): cc=kwargs.get('cc'), email_account=kwargs.get('email_account'), expose_recipients=kwargs.get('expose_recipients'), - inline_images=kwargs.get('inline_images')) + inline_images=kwargs.get('inline_images'), + header=kwargs.get('header')) mail.set_message_id(kwargs.get('message_id'),kwargs.get('is_notification')) if kwargs.get('read_receipt'): @@ -333,7 +346,7 @@ def send_one(email, smtpserver=None, auto_commit=True, now=False, from_test=Fals email = frappe.db.sql('''select name, status, communication, message, sender, reference_doctype, reference_name, unsubscribe_param, unsubscribe_method, expose_recipients, - show_as_cc, add_unsubscribe_link + show_as_cc, add_unsubscribe_link, attachments from `tabEmail Queue` where @@ -426,6 +439,7 @@ where name=%s""", (unicode(e), email.name), auto_commit=auto_commit) frappe.get_doc('Communication', email.communication).set_delivery_status(commit=auto_commit) if now: + print(frappe.get_traceback()) raise e else: @@ -459,7 +473,31 @@ def prepare_message(email, recipient, recipients_list): message = message.replace("", quopri.encodestring(email_sent_message)) message = message.replace("", recipient) - return message + + if not email.attachments: + return message + + # On-demand attachments + from email.parser import Parser + + msg_obj = Parser().parsestr(message) + attachments = json.loads(email.attachments) + + for attachment in attachments: + if attachment.get('fcontent'): continue + + fid = attachment.get('fid') + if not fid: continue + + fname, fcontent = get_file(fid) + attachment.update({ + 'fname': fname, + 'fcontent': fcontent, + 'parent': msg_obj + }) + add_attachment(**attachment) + + return msg_obj.as_string() def clear_outbox(): """Remove low priority older than 31 days in Outbox and expire mails not sent for 7 days. diff --git a/frappe/email/test_email_body.py b/frappe/email/test_email_body.py index 1ca56dce5d..fda4646b68 100644 --- a/frappe/email/test_email_body.py +++ b/frappe/email/test_email_body.py @@ -2,7 +2,7 @@ # License: GNU General Public License v3. See license.txt from __future__ import unicode_literals -import unittest, os, base64 +import frappe, unittest, os, base64 from frappe.email.email_body import replace_filename_with_cid, get_email class TestEmailBody(unittest.TestCase): @@ -11,16 +11,15 @@ class TestEmailBody(unittest.TestCase):

Hey John Doe!

This is embedded image you asked for

- +
''' email_text = ''' Hey John Doe! This is the text version of this email ''' - frappe_app_path = os.path.join('..', 'apps', 'frappe') - img_path = os.path.join(frappe_app_path, 'frappe', 'public', 'images', 'favicon.png') + img_path = os.path.abspath('assets/frappe/images/favicon.png') with open(img_path) as f: img_content = f.read() img_base64 = base64.b64encode(img_content) @@ -33,11 +32,7 @@ This is the text version of this email sender='me@example.com', subject='Test Subject', content=email_html, - text_content=email_text, - inline_images=[{ - 'filename': 'favicon.png', - 'filecontent': img_content - }] + text_content=email_text ).as_string() @@ -86,15 +81,18 @@ w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> def test_replace_filename_with_cid(self): original_message = '''
- test + test +
''' + message, inline_images = replace_filename_with_cid(original_message) + processed_message = '''
- test + test +
- ''' - message = replace_filename_with_cid(original_message, 'test.jpg', 'abcdefghij') + '''.format(inline_images[0].get('content_id')) self.assertEquals(message, processed_message) diff --git a/frappe/templates/emails/password_update.html b/frappe/templates/emails/password_update.html index f7741977a1..d87e706d20 100644 --- a/frappe/templates/emails/password_update.html +++ b/frappe/templates/emails/password_update.html @@ -1,5 +1,5 @@

{{_("Password Update Notification")}}

{{_("Dear")}} {{ first_name }}{% if last_name %} {{ last_name}}{% endif %},

-

{{_("Your password has been updated. Here is your new password")}}: {{ new_password }}

+

{{_("Your password has been updated. Here is your new password")}}: {{ new_password }}

{{_("Thank you")}},
{{ user_fullname }}

\ No newline at end of file