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):
This is embedded image you asked for
-{{_("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 }}