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
This commit is contained in:
parent
ad7911fb96
commit
f6d265b2ef
8 changed files with 260 additions and 96 deletions
|
|
@ -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 = []
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <img embed="filename.jpg" ...> with
|
||||
<img src="cid:content_id" ...>
|
||||
def replace_filename_with_cid(message):
|
||||
""" Replaces <img embed="assets/frappe/images/filename.jpg" ...> with
|
||||
<img src="cid:content_id" ...> 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
|
||||
|
|
|
|||
|
|
@ -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("<!--cc message-->", quopri.encodestring(email_sent_message))
|
||||
|
||||
message = message.replace("<!--recipient-->", 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.
|
||||
|
|
|
|||
|
|
@ -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):
|
|||
<div>
|
||||
<h3>Hey John Doe!</h3>
|
||||
<p>This is embedded image you asked for</p>
|
||||
<img embed="favicon.png" />
|
||||
<img embed="assets/frappe/images/favicon.png" />
|
||||
</div>
|
||||
'''
|
||||
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 = '''
|
||||
<div>
|
||||
<img embed="test.jpg" alt="test" />
|
||||
<img embed="assets/frappe/images/favicon.png" alt="test" />
|
||||
<img embed="notexists.jpg" />
|
||||
</div>
|
||||
'''
|
||||
message, inline_images = replace_filename_with_cid(original_message)
|
||||
|
||||
processed_message = '''
|
||||
<div>
|
||||
<img src="cid:abcdefghij" alt="test" />
|
||||
<img src="cid:{0}" alt="test" />
|
||||
<img />
|
||||
</div>
|
||||
'''
|
||||
message = replace_filename_with_cid(original_message, 'test.jpg', 'abcdefghij')
|
||||
'''.format(inline_images[0].get('content_id'))
|
||||
self.assertEquals(message, processed_message)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<h3>{{_("Password Update Notification")}}</h3>
|
||||
<p>{{_("Dear")}} {{ first_name }}{% if last_name %} {{ last_name}}{% endif %},</p>
|
||||
<p>{{_("Your password has been updated. Here is your new password")}}: {{ new_password }}</p>
|
||||
<p>{{_("Your password has been updated. Here is your new password")}}: <b>{{ new_password }}</b></p>
|
||||
<p>{{_("Thank you")}},<br>
|
||||
{{ user_fullname }}</p>
|
||||
Loading…
Add table
Reference in a new issue