Browse Source

Merge branch 'develop' of github.com:frappe/frappe into drop-py2-code

version-14
Gavin D'souza 4 years ago
parent
commit
2ad9d202cb
14 changed files with 410 additions and 303 deletions
  1. +24
    -3
      frappe/__init__.py
  2. +9
    -1
      frappe/core/doctype/module_def/module_def.json
  3. +1
    -11
      frappe/custom/doctype/customize_form/customize_form.json
  4. +301
    -6
      frappe/email/doctype/email_queue/email_queue.py
  5. +0
    -1
      frappe/email/doctype/newsletter/newsletter.py
  6. +2
    -249
      frappe/email/queue.py
  7. +5
    -4
      frappe/email/smtp.py
  8. +9
    -12
      frappe/email/test_email_body.py
  9. +1
    -0
      frappe/public/scss/common/awesomeplete.scss
  10. +7
    -6
      frappe/tests/test_email.py
  11. +12
    -2
      frappe/tests/test_password.py
  12. +1
    -1
      frappe/utils/error.py
  13. +16
    -7
      frappe/utils/password.py
  14. +22
    -0
      frappe/utils/safe_exec.py

+ 24
- 3
frappe/__init__.py View File

@@ -527,16 +527,20 @@ def sendmail(recipients=[], sender="", subject="No Subject", message="No Message
if not delayed: if not delayed:
now = True now = True


from frappe.email import queue
queue.send(recipients=recipients, sender=sender,
from frappe.email.doctype.email_queue.email_queue import QueueBuilder
builder = QueueBuilder(recipients=recipients, sender=sender,
subject=subject, message=message, text_content=text_content, subject=subject, message=message, text_content=text_content,
reference_doctype = doctype or reference_doctype, reference_name = name or reference_name, add_unsubscribe_link=add_unsubscribe_link, reference_doctype = doctype or reference_doctype, reference_name = name or reference_name, add_unsubscribe_link=add_unsubscribe_link,
unsubscribe_method=unsubscribe_method, unsubscribe_params=unsubscribe_params, unsubscribe_message=unsubscribe_message, unsubscribe_method=unsubscribe_method, unsubscribe_params=unsubscribe_params, unsubscribe_message=unsubscribe_message,
attachments=attachments, reply_to=reply_to, cc=cc, bcc=bcc, message_id=message_id, in_reply_to=in_reply_to, attachments=attachments, reply_to=reply_to, cc=cc, bcc=bcc, message_id=message_id, in_reply_to=in_reply_to,
send_after=send_after, expose_recipients=expose_recipients, send_priority=send_priority, queue_separately=queue_separately, send_after=send_after, expose_recipients=expose_recipients, send_priority=send_priority, queue_separately=queue_separately,
communication=communication, now=now, read_receipt=read_receipt, is_notification=is_notification,
communication=communication, read_receipt=read_receipt, is_notification=is_notification,
inline_images=inline_images, header=header, print_letterhead=print_letterhead, with_container=with_container) inline_images=inline_images, header=header, print_letterhead=print_letterhead, with_container=with_container)


# build email queue and send the email if send_now is True.
builder.process(send_now=now)


whitelisted = [] whitelisted = []
guest_methods = [] guest_methods = []
xss_safe_methods = [] xss_safe_methods = []
@@ -1692,6 +1696,23 @@ def safe_eval(code, eval_globals=None, eval_locals=None):
"round": round "round": round
} }


UNSAFE_ATTRIBUTES = {
# Generator Attributes
"gi_frame", "gi_code",
# Coroutine Attributes
"cr_frame", "cr_code", "cr_origin",
# Async Generator Attributes
"ag_code", "ag_frame",
# Traceback Attributes
"tb_frame", "tb_next",
# Format Attributes
"format", "format_map",
}

for attribute in UNSAFE_ATTRIBUTES:
if attribute in code:
throw('Illegal rule {0}. Cannot use "{1}"'.format(bold(code), attribute))

if '__' in code: if '__' in code:
throw('Illegal rule {0}. Cannot use "__"'.format(bold(code))) throw('Illegal rule {0}. Cannot use "__"'.format(bold(code)))




+ 9
- 1
frappe/core/doctype/module_def/module_def.json View File

@@ -55,7 +55,7 @@
"link_fieldname": "module" "link_fieldname": "module"
} }
], ],
"modified": "2020-08-06 12:39:30.740379",
"modified": "2021-06-02 13:04:53.118716",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Core", "module": "Core",
"name": "Module Def", "name": "Module Def",
@@ -69,6 +69,7 @@
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "Administrator", "role": "Administrator",
"select": 1,
"share": 1, "share": 1,
"write": 1 "write": 1
}, },
@@ -78,7 +79,14 @@
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "System Manager", "role": "System Manager",
"select": 1,
"write": 1 "write": 1
},
{
"read": 1,
"report": 1,
"role": "All",
"select": 1
} }
], ],
"show_name_in_global_search": 1, "show_name_in_global_search": 1,


+ 1
- 11
frappe/custom/doctype/customize_form/customize_form.json View File

@@ -288,16 +288,6 @@
"fieldname": "autoname", "fieldname": "autoname",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Auto Name" "label": "Auto Name"
},
{
"fieldname": "default_email_template",
"fieldtype": "Link",
"label": "Default Email Template",
"options": "Email Template"
},
{
"fieldname": "column_break_26",
"fieldtype": "Column Break"
} }
], ],
"hide_toolbar": 1, "hide_toolbar": 1,
@@ -306,7 +296,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2021-04-29 21:21:06.476372",
"modified": "2021-06-02 06:49:16.782806",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Custom", "module": "Custom",
"name": "Customize Form", "name": "Customize Form",


+ 301
- 6
frappe/email/doctype/email_queue/email_queue.py View File

@@ -9,14 +9,18 @@ from rq.timeouts import JobTimeoutException
import smtplib import smtplib
import quopri import quopri
from email.parser import Parser from email.parser import Parser
from email.policy import SMTPUTF8
from html2text import html2text
from six.moves import html_parser as HTMLParser


import frappe import frappe
from frappe import _, safe_encode, task from frappe import _, safe_encode, task
from frappe.model.document import Document from frappe.model.document import Document
from frappe.email.queue import get_unsubcribed_url
from frappe.email.email_body import add_attachment
from frappe.utils import cint
from email.policy import SMTPUTF8
from frappe.email.queue import get_unsubcribed_url, get_unsubscribe_message
from frappe.email.email_body import add_attachment, get_formatted_html, get_email
from frappe.utils import cint, split_emails, add_days, nowdate, cstr
from frappe.email.doctype.email_account.email_account import EmailAccount



MAX_RETRY_COUNT = 3 MAX_RETRY_COUNT = 3
class EmailQueue(Document): class EmailQueue(Document):
@@ -41,6 +45,19 @@ class EmailQueue(Document):
duplicate.set_recipients(recipients) duplicate.set_recipients(recipients)
return duplicate return duplicate


@classmethod
def new(cls, doc_data, ignore_permissions=False):
data = doc_data.copy()
if not data.get('recipients'):
return

recipients = data.pop('recipients')
doc = frappe.new_doc(cls.DOCTYPE)
doc.update(data)
doc.set_recipients(recipients)
doc.insert(ignore_permissions=ignore_permissions)
return doc

@classmethod @classmethod
def find(cls, name): def find(cls, name):
return frappe.get_doc(cls.DOCTYPE, name) return frappe.get_doc(cls.DOCTYPE, name)
@@ -74,8 +91,6 @@ class EmailQueue(Document):
return json.loads(self.attachments) if self.attachments else [] return json.loads(self.attachments) if self.attachments else []


def get_email_account(self): def get_email_account(self):
from frappe.email.doctype.email_account.email_account import EmailAccount

if self.email_account: if self.email_account:
return frappe.get_doc('Email Account', self.email_account) return frappe.get_doc('Email Account', self.email_account)


@@ -300,3 +315,283 @@ def send_now(name):
def on_doctype_update(): def on_doctype_update():
"""Add index in `tabCommunication` for `(reference_doctype, reference_name)`""" """Add index in `tabCommunication` for `(reference_doctype, reference_name)`"""
frappe.db.add_index('Email Queue', ('status', 'send_after', 'priority', 'creation'), 'index_bulk_flush') frappe.db.add_index('Email Queue', ('status', 'send_after', 'priority', 'creation'), 'index_bulk_flush')

class QueueBuilder:
"""Builds Email Queue from the given data
"""
def __init__(self, recipients=None, sender=None, subject=None, message=None,
text_content=None, reference_doctype=None, reference_name=None,
unsubscribe_method=None, unsubscribe_params=None, unsubscribe_message=None,
attachments=None, reply_to=None, cc=None, bcc=None, message_id=None, in_reply_to=None,
send_after=None, expose_recipients=None, send_priority=1, communication=None,
read_receipt=None, queue_separately=False, is_notification=False,
add_unsubscribe_link=1, inline_images=None, header=None,
print_letterhead=False, with_container=False):
"""Add email to sending queue (Email Queue)

:param recipients: List of recipients.
:param sender: Email sender.
:param subject: Email subject.
:param message: Email message.
:param text_content: Text version of email message.
:param reference_doctype: Reference DocType of caller document.
:param reference_name: Reference name of caller document.
:param send_priority: Priority for Email Queue, default 1.
:param unsubscribe_method: URL method for unsubscribe. Default is `/api/method/frappe.email.queue.unsubscribe`.
:param unsubscribe_params: additional params for unsubscribed links. default are name, doctype, email
:param attachments: Attachments to be sent.
:param reply_to: Reply to be captured here (default inbox)
:param in_reply_to: Used to send the Message-Id of a received email back as In-Reply-To.
:param send_after: Send this email after the given datetime. If value is in integer, then `send_after` will be the automatically set to no of days from current date.
:param communication: Communication link to be set in Email Queue record
:param queue_separately: Queue each email separately
: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)
:param with_container: Wraps email inside styled container
"""

self._unsubscribe_method = unsubscribe_method
self._recipients = recipients
self._cc = cc
self._bcc = bcc
self._send_after = send_after
self._sender = sender
self._text_content = text_content
self._message = message
self._add_unsubscribe_link = add_unsubscribe_link
self._unsubscribe_message = unsubscribe_message
self._attachments = attachments

self._unsubscribed_user_emails = None
self._email_account = None

self.unsubscribe_params = unsubscribe_params
self.subject = subject
self.reference_doctype = reference_doctype
self.reference_name = reference_name
self.expose_recipients = expose_recipients
self.with_container = with_container
self.header = header
self.reply_to = reply_to
self.message_id = message_id
self.in_reply_to = in_reply_to
self.send_priority = send_priority
self.communication = communication
self.read_receipt = read_receipt
self.queue_separately = queue_separately
self.is_notification = is_notification
self.inline_images = inline_images
self.print_letterhead = print_letterhead

@property
def unsubscribe_method(self):
return self._unsubscribe_method or '/api/method/frappe.email.queue.unsubscribe'

def _get_emails_list(self, emails=None):
emails = split_emails(emails) if isinstance(emails, str) else (emails or [])
return [each for each in set(emails) if each]

@property
def recipients(self):
return self._get_emails_list(self._recipients)

@property
def cc(self):
return self._get_emails_list(self._cc)

@property
def bcc(self):
return self._get_emails_list(self._bcc)

@property
def send_after(self):
if isinstance(self._send_after, int):
return add_days(nowdate(), self._send_after)
return self._send_after

@property
def sender(self):
if not self._sender or self._sender == "Administrator":
email_account = self.get_outgoing_email_account()
return email_account.default_sender
return self._sender

def email_text_content(self):
unsubscribe_msg = self.unsubscribe_message()
unsubscribe_text_message = (unsubscribe_msg and unsubscribe_msg.text) or ''

if self._text_content:
return self._text_content + unsubscribe_text_message

try:
text_content = html2text(self._message)
except HTMLParser.HTMLParseError:
text_content = "See html attachment"
return text_content + unsubscribe_text_message

def email_html_content(self):
email_account = self.get_outgoing_email_account()
return get_formatted_html(self.subject, self._message, header=self.header,
email_account=email_account, unsubscribe_link=self.unsubscribe_message(),
with_container=self.with_container)

def should_include_unsubscribe_link(self):
return (self._add_unsubscribe_link == 1
and self.reference_doctype
and (self._unsubscribe_message or self.reference_doctype=="Newsletter"))

def unsubscribe_message(self):
if self.should_include_unsubscribe_link():
return get_unsubscribe_message(self._unsubscribe_message, self.expose_recipients)

def get_outgoing_email_account(self):
if self._email_account:
return self._email_account

self._email_account = EmailAccount.find_outgoing(
match_by_doctype=self.reference_doctype, match_by_email=self._sender, _raise_error=True)
return self._email_account

def get_unsubscribed_user_emails(self):
if self._unsubscribed_user_emails is not None:
return self._unsubscribed_user_emails

all_ids = tuple(set(self.recipients + self.cc))

unsubscribed = frappe.db.sql_list('''
SELECT
distinct email
from
`tabEmail Unsubscribe`
where
email in %(all_ids)s
and (
(
reference_doctype = %(reference_doctype)s
and reference_name = %(reference_name)s
)
or global_unsubscribe = 1
)
''', {
'all_ids': all_ids,
'reference_doctype': self.reference_doctype,
'reference_name': self.reference_name,
})

self._unsubscribed_user_emails = unsubscribed or []
return self._unsubscribed_user_emails

def final_recipients(self):
unsubscribed_emails = self.get_unsubscribed_user_emails()
return [mail_id for mail_id in self.recipients if mail_id not in unsubscribed_emails]

def final_cc(self):
unsubscribed_emails = self.get_unsubscribed_user_emails()
return [mail_id for mail_id in self.cc if mail_id not in unsubscribed_emails]

def get_attachments(self):
attachments = []
if self._attachments:
# store attachments with fid or print format details, to be attached on-demand later
for att in self._attachments:
if att.get('fid'):
attachments.append(att)
elif att.get("print_format_attachment") == 1:
if not att.get('lang', None):
att['lang'] = frappe.local.lang
att['print_letterhead'] = self.print_letterhead
attachments.append(att)
return attachments

def prepare_email_content(self):
mail = get_email(recipients=self.final_recipients(),
sender=self.sender,
subject=self.subject,
formatted=self.email_html_content(),
text_content=self.email_text_content(),
attachments=self._attachments,
reply_to=self.reply_to,
cc=self.final_cc(),
bcc=self.bcc,
email_account=self.get_outgoing_email_account(),
expose_recipients=self.expose_recipients,
inline_images=self.inline_images,
header=self.header)

mail.set_message_id(self.message_id, self.is_notification)
if self.read_receipt:
mail.msg_root["Disposition-Notification-To"] = self.sender
if self.in_reply_to:
mail.set_in_reply_to(self.in_reply_to)
return mail

def process(self, send_now=False):
"""Build and return the email queues those are created.

Sends email incase if it is requested to send now.
"""
final_recipients = self.final_recipients()
queue_separately = (final_recipients and self.queue_separately) or len(final_recipients) > 20
if not (final_recipients + self.final_cc()):
return []

email_queues = []
queue_data = self.as_dict(include_recipients=False)
if not queue_data:
return []

if not queue_separately:
recipients = list(set(final_recipients + self.final_cc() + self.bcc))
q = EmailQueue.new({**queue_data, **{'recipients': recipients}}, ignore_permissions=True)
email_queues.append(q)
else:
for r in final_recipients:
recipients = [r] if email_queues else list(set([r] + self.final_cc() + self.bcc))
q = EmailQueue.new({**queue_data, **{'recipients': recipients}}, ignore_permissions=True)
email_queues.append(q)

if send_now:
for doc in email_queues:
doc.send()
return email_queues

def as_dict(self, include_recipients=True):
email_account = self.get_outgoing_email_account()
email_account_name = email_account and email_account.is_exists_in_db() and email_account.name

mail = self.prepare_email_content()
try:
mail_to_string = cstr(mail.as_string())
except frappe.InvalidEmailAddressError:
# bad Email Address - don't add to queue
frappe.log_error('Invalid Email ID Sender: {0}, Recipients: {1}, \nTraceback: {2} '
.format(self.sender, ', '.join(self.final_recipients()), traceback.format_exc()),
'Email Not Sent'
)
return

d = {
'priority': self.send_priority,
'attachments': json.dumps(self.get_attachments()),
'message_id': mail.msg_root["Message-Id"].strip(" <>"),
'message': mail_to_string,
'sender': self.sender,
'reference_doctype': self.reference_doctype,
'reference_name': self.reference_name,
'add_unsubscribe_link': self._add_unsubscribe_link,
'unsubscribe_method': self.unsubscribe_method,
'unsubscribe_params': self.unsubscribe_params,
'expose_recipients': self.expose_recipients,
'communication': self.communication,
'send_after': self.send_after,
'show_as_cc': ",".join(self.final_cc()),
'show_as_bcc': ','.join(self.bcc),
'email_account': email_account_name or None
}

if include_recipients:
d['recipients'] = self.final_recipients()

return d

+ 0
- 1
frappe/email/doctype/newsletter/newsletter.py View File

@@ -6,7 +6,6 @@ import frappe.utils
from frappe import throw, _ from frappe import throw, _
from frappe.website.website_generator import WebsiteGenerator from frappe.website.website_generator import WebsiteGenerator
from frappe.utils.verified_command import get_signed_params, verify_request from frappe.utils.verified_command import get_signed_params, verify_request
from frappe.email.queue import send
from frappe.email.doctype.email_group.email_group import add_subscribers from frappe.email.doctype.email_group.email_group import add_subscribers
from frappe.utils import parse_addr, now_datetime, markdown, validate_email_address from frappe.utils import parse_addr, now_datetime, markdown, validate_email_address




+ 2
- 249
frappe/email/queue.py View File

@@ -2,256 +2,9 @@
# MIT License. See license.txt # MIT License. See license.txt


import frappe import frappe
import sys
from html.parser import HTMLParser
import smtplib, quopri, json
from frappe import msgprint, _, safe_decode, safe_encode, enqueue
from frappe.email.smtp import SMTPServer
from frappe.email.doctype.email_account.email_account import EmailAccount
from frappe.email.email_body import get_email, get_formatted_html, add_attachment
from frappe import msgprint, _
from frappe.utils.verified_command import get_signed_params, verify_request from frappe.utils.verified_command import get_signed_params, verify_request
from html2text import html2text
from frappe.utils import get_url, nowdate, now_datetime, add_days, split_emails, cstr, cint
from rq.timeouts import JobTimeoutException
from email.parser import Parser


class EmailLimitCrossedError(frappe.ValidationError): pass

def send(recipients=None, sender=None, subject=None, message=None, text_content=None, reference_doctype=None,
reference_name=None, unsubscribe_method=None, unsubscribe_params=None, unsubscribe_message=None,
attachments=None, reply_to=None, cc=None, bcc=None, 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,
header=None, print_letterhead=False, with_container=False):
"""Add email to sending queue (Email Queue)

:param recipients: List of recipients.
:param sender: Email sender.
:param subject: Email subject.
:param message: Email message.
:param text_content: Text version of email message.
:param reference_doctype: Reference DocType of caller document.
:param reference_name: Reference name of caller document.
:param send_priority: Priority for Email Queue, default 1.
:param unsubscribe_method: URL method for unsubscribe. Default is `/api/method/frappe.email.queue.unsubscribe`.
:param unsubscribe_params: additional params for unsubscribed links. default are name, doctype, email
:param attachments: Attachments to be sent.
:param reply_to: Reply to be captured here (default inbox)
:param in_reply_to: Used to send the Message-Id of a received email back as In-Reply-To.
:param send_after: Send this email after the given datetime. If value is in integer, then `send_after` will be the automatically set to no of days from current date.
:param communication: Communication link to be set in Email Queue record
:param now: Send immediately (don't send in the background)
:param queue_separately: Queue each email separately
: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)
:param with_container: Wraps email inside styled container
"""
if not unsubscribe_method:
unsubscribe_method = "/api/method/frappe.email.queue.unsubscribe"

if not recipients and not cc:
return

if not cc:
cc = []
if not bcc:
bcc = []

if isinstance(recipients, str):
recipients = split_emails(recipients)

if isinstance(cc, str):
cc = split_emails(cc)

if isinstance(bcc, str):
bcc = split_emails(bcc)

if isinstance(send_after, int):
send_after = add_days(nowdate(), send_after)

email_account = EmailAccount.find_outgoing(
match_by_doctype=reference_doctype, match_by_email=sender, _raise_error=True)

if not sender or sender == "Administrator":
sender = email_account.default_sender

if not text_content:
try:
text_content = html2text(message)
except HTMLParser.HTMLParseError:
text_content = "See html attachment"

recipients = list(set(recipients))
cc = list(set(cc))

all_ids = tuple(recipients + cc)

unsubscribed = frappe.db.sql_list('''
SELECT
distinct email
from
`tabEmail Unsubscribe`
where
email in %(all_ids)s
and (
(
reference_doctype = %(reference_doctype)s
and reference_name = %(reference_name)s
)
or global_unsubscribe = 1
)
''', {
'all_ids': all_ids,
'reference_doctype': reference_doctype,
'reference_name': reference_name,
})

recipients = [r for r in recipients if r and r not in unsubscribed]

if cc:
cc = [r for r in cc if r and r not in unsubscribed]

if not recipients and not cc:
# Recipients may have been unsubscribed, exit quietly
return

email_text_context = text_content

should_append_unsubscribe = (add_unsubscribe_link
and reference_doctype
and (unsubscribe_message or reference_doctype=="Newsletter")
and add_unsubscribe_link==1)

unsubscribe_link = None
if should_append_unsubscribe:
unsubscribe_link = get_unsubscribe_message(unsubscribe_message, expose_recipients)
email_text_context += unsubscribe_link.text

email_content = get_formatted_html(subject, message,
email_account=email_account, header=header,
unsubscribe_link=unsubscribe_link, with_container=with_container)

# add to queue
add(recipients, sender, subject,
formatted=email_content,
text_content=email_text_context,
reference_doctype=reference_doctype,
reference_name=reference_name,
attachments=attachments,
reply_to=reply_to,
cc=cc,
bcc=bcc,
message_id=message_id,
in_reply_to=in_reply_to,
send_after=send_after,
send_priority=send_priority,
email_account=email_account,
communication=communication,
add_unsubscribe_link=add_unsubscribe_link,
unsubscribe_method=unsubscribe_method,
unsubscribe_params=unsubscribe_params,
expose_recipients=expose_recipients,
read_receipt=read_receipt,
queue_separately=queue_separately,
is_notification = is_notification,
inline_images = inline_images,
header=header,
now=now,
print_letterhead=print_letterhead)


def add(recipients, sender, subject, **kwargs):
"""Add to Email Queue"""
if kwargs.get('queue_separately') or len(recipients) > 20:
email_queue = None
for r in recipients:
if not email_queue:
email_queue = get_email_queue([r], sender, subject, **kwargs)
if kwargs.get('now'):
email_queue.send()
else:
duplicate = email_queue.get_duplicate([r])
duplicate.insert(ignore_permissions=True)

if kwargs.get('now'):
duplicate.send()

frappe.db.commit()
else:
email_queue = get_email_queue(recipients, sender, subject, **kwargs)
if kwargs.get('now'):
email_queue.send()

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 or print format details, to be attached on-demand later
_attachments = []
for att in attachments:
if att.get('fid'):
_attachments.append(att)
elif att.get("print_format_attachment") == 1:
if not att.get('lang', None):
att['lang'] = frappe.local.lang
att['print_letterhead'] = kwargs.get('print_letterhead')
_attachments.append(att)
e.attachments = json.dumps(_attachments)

try:
mail = get_email(recipients,
sender=sender,
subject=subject,
formatted=kwargs.get('formatted'),
text_content=kwargs.get('text_content'),
attachments=kwargs.get('attachments'),
reply_to=kwargs.get('reply_to'),
cc=kwargs.get('cc'),
bcc=kwargs.get('bcc'),
email_account=kwargs.get('email_account'),
expose_recipients=kwargs.get('expose_recipients'),
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'):
mail.msg_root["Disposition-Notification-To"] = sender
if kwargs.get('in_reply_to'):
mail.set_in_reply_to(kwargs.get('in_reply_to'))

e.message_id = mail.msg_root["Message-Id"].strip(" <>")
e.message = cstr(mail.as_string())
e.sender = mail.sender

except frappe.InvalidEmailAddressError:
# bad Email Address - don't add to queue
import traceback
frappe.log_error('Invalid Email ID Sender: {0}, Recipients: {1}, \nTraceback: {2} '.format(mail.sender,
', '.join(mail.recipients), traceback.format_exc()), 'Email Not Sent')

recipients = list(set(recipients + kwargs.get('cc', []) + kwargs.get('bcc', [])))
email_account = kwargs.get('email_account')
email_account_name = email_account and email_account.is_exists_in_db() and email_account.name

e.set_recipients(recipients)
e.reference_doctype = kwargs.get('reference_doctype')
e.reference_name = kwargs.get('reference_name')
e.add_unsubscribe_link = kwargs.get("add_unsubscribe_link")
e.unsubscribe_method = kwargs.get('unsubscribe_method')
e.unsubscribe_params = kwargs.get('unsubscribe_params')
e.expose_recipients = kwargs.get('expose_recipients')
e.communication = kwargs.get('communication')
e.send_after = kwargs.get('send_after')
e.show_as_cc = ",".join(kwargs.get('cc', []))
e.show_as_bcc = ",".join(kwargs.get('bcc', []))
e.email_account = email_account_name or None
e.insert(ignore_permissions=True)
return e
from frappe.utils import get_url, now_datetime, cint


def get_emails_sent_this_month(): def get_emails_sent_this_month():
return frappe.db.sql(""" return frappe.db.sql("""


+ 5
- 4
frappe/email/smtp.py View File

@@ -84,18 +84,19 @@ class SMTPServer:
SMTP = smtplib.SMTP_SSL if self.use_ssl else smtplib.SMTP SMTP = smtplib.SMTP_SSL if self.use_ssl else smtplib.SMTP


try: try:
self._session = SMTP(self.server, self.port)
if not self._session:
_session = SMTP(self.server, self.port)
if not _session:
frappe.msgprint(CONNECTION_FAILED, raise_exception=frappe.OutgoingEmailError) frappe.msgprint(CONNECTION_FAILED, raise_exception=frappe.OutgoingEmailError)


self.secure_session(self._session)
self.secure_session(_session)
if self.login and self.password: if self.login and self.password:
res = self._session.login(str(self.login or ""), str(self.password or ""))
res = _session.login(str(self.login or ""), str(self.password or ""))


# check if logged correctly # check if logged correctly
if res[0]!=235: if res[0]!=235:
frappe.msgprint(res[1], raise_exception=frappe.OutgoingEmailError) frappe.msgprint(res[1], raise_exception=frappe.OutgoingEmailError)


self._session = _session
return self._session return self._session


except smtplib.SMTPAuthenticationError as e: except smtplib.SMTPAuthenticationError as e:


+ 9
- 12
frappe/email/test_email_body.py View File

@@ -5,8 +5,7 @@ from frappe import safe_decode
from frappe.email.receive import Email from frappe.email.receive import Email
from frappe.email.email_body import (replace_filename_with_cid, from frappe.email.email_body import (replace_filename_with_cid,
get_email, inline_style_in_html, get_header) get_email, inline_style_in_html, get_header)
from frappe.email.queue import get_email_queue
from frappe.email.doctype.email_queue.email_queue import SendMailContext
from frappe.email.doctype.email_queue.email_queue import SendMailContext, QueueBuilder




class TestEmailBody(unittest.TestCase): class TestEmailBody(unittest.TestCase):
@@ -43,27 +42,25 @@ This is the text version of this email
uni_chr1 = chr(40960) uni_chr1 = chr(40960)
uni_chr2 = chr(1972) uni_chr2 = chr(1972)


email = get_email_queue(
queue_doc = QueueBuilder(
recipients=['test@example.com'], recipients=['test@example.com'],
sender='me@example.com', sender='me@example.com',
subject='Test Subject', subject='Test Subject',
content='<h1>' + uni_chr1 + 'abcd' + uni_chr2 + '</h1>',
formatted='<h1>' + uni_chr1 + 'abcd' + uni_chr2 + '</h1>',
text_content='whatever')
mail_ctx = SendMailContext(queue_doc = email)
message='<h1>' + uni_chr1 + 'abcd' + uni_chr2 + '</h1>',
text_content='whatever').process()[0]
mail_ctx = SendMailContext(queue_doc = queue_doc)
result = mail_ctx.build_message(recipient_email = 'test@test.com') result = mail_ctx.build_message(recipient_email = 'test@test.com')
self.assertTrue(b"<h1>=EA=80=80abcd=DE=B4</h1>" in result) self.assertTrue(b"<h1>=EA=80=80abcd=DE=B4</h1>" in result)


def test_prepare_message_returns_cr_lf(self): def test_prepare_message_returns_cr_lf(self):
email = get_email_queue(
queue_doc = QueueBuilder(
recipients=['test@example.com'], recipients=['test@example.com'],
sender='me@example.com', sender='me@example.com',
subject='Test Subject', subject='Test Subject',
content='<h1>\n this is a test of newlines\n' + '</h1>',
formatted='<h1>\n this is a test of newlines\n' + '</h1>',
text_content='whatever')
message='<h1>\n this is a test of newlines\n' + '</h1>',
text_content='whatever').process()[0]


mail_ctx = SendMailContext(queue_doc = email)
mail_ctx = SendMailContext(queue_doc = queue_doc)
result = safe_decode(mail_ctx.build_message(recipient_email='test@test.com')) result = safe_decode(mail_ctx.build_message(recipient_email='test@test.com'))


self.assertTrue(result.count('\n') == result.count("\r")) self.assertTrue(result.count('\n') == result.count("\r"))


+ 1
- 0
frappe/public/scss/common/awesomeplete.scss View File

@@ -31,6 +31,7 @@
margin: 0; margin: 0;
padding: var(--padding-xs); padding: var(--padding-xs);
z-index: 1; z-index: 1;
min-width: 250px;


&> li { &> li {
cursor: pointer; cursor: pointer;


+ 7
- 6
frappe/tests/test_email.py View File

@@ -139,7 +139,8 @@ class TestEmail(unittest.TestCase):
self.assertEqual(len(queue_recipients), 2) self.assertEqual(len(queue_recipients), 2)


def test_unsubscribe(self): def test_unsubscribe(self):
from frappe.email.queue import unsubscribe, send
from frappe.email.queue import unsubscribe
from frappe.email.doctype.email_queue.email_queue import QueueBuilder
unsubscribe(doctype="User", name="Administrator", email="test@example.com") unsubscribe(doctype="User", name="Administrator", email="test@example.com")


self.assertTrue(frappe.db.get_value("Email Unsubscribe", self.assertTrue(frappe.db.get_value("Email Unsubscribe",
@@ -148,11 +149,11 @@ class TestEmail(unittest.TestCase):


before = frappe.db.sql("""select count(name) from `tabEmail Queue` where status='Not Sent'""")[0][0] before = frappe.db.sql("""select count(name) from `tabEmail Queue` where status='Not Sent'""")[0][0]


send(recipients=['test@example.com', 'test1@example.com'],
sender="admin@example.com",
reference_doctype='User', reference_name="Administrator",
subject='Testing Email Queue', message='This is mail is queued!', unsubscribe_message="Unsubscribe")
builder = QueueBuilder(recipients=['test@example.com', 'test1@example.com'],
sender="admin@example.com",
reference_doctype='User', reference_name="Administrator",
subject='Testing Email Queue', message='This is mail is queued!', unsubscribe_message="Unsubscribe")
builder.process()
# this is sent async (?) # this is sent async (?)


email_queue = frappe.db.sql("""select name from `tabEmail Queue` where status='Not Sent'""", email_queue = frappe.db.sql("""select name from `tabEmail Queue` where status='Not Sent'""",


+ 12
- 2
frappe/tests/test_password.py View File

@@ -2,8 +2,8 @@
# MIT License. See license.txt # MIT License. See license.txt
import frappe import frappe
import unittest import unittest
from frappe.utils.password import update_password, check_password, passlibctx
from frappe.utils.password import update_password, check_password, passlibctx, encrypt, decrypt
from cryptography.fernet import Fernet
class TestPassword(unittest.TestCase): class TestPassword(unittest.TestCase):
def setUp(self): def setUp(self):
frappe.delete_doc('Email Account', 'Test Email Account Password') frappe.delete_doc('Email Account', 'Test Email Account Password')
@@ -104,6 +104,16 @@ class TestPassword(unittest.TestCase):
doc.save() doc.save()
self.assertEqual(doc.get_password(raise_exception=False), None) self.assertEqual(doc.get_password(raise_exception=False), None)


def test_custom_encryption_key(self):
text = 'Frappe Framework'
custom_encryption_key = Fernet.generate_key().decode()

encrypted_text = encrypt(text, encryption_key=custom_encryption_key)
decrypted_text = decrypt(encrypted_text, encryption_key=custom_encryption_key)
self.assertEqual(text, decrypted_text)

pass


def get_password_list(doc): def get_password_list(doc):
return frappe.db.sql("""SELECT `password` return frappe.db.sql("""SELECT `password`


+ 1
- 1
frappe/utils/error.py View File

@@ -213,7 +213,7 @@ def raise_error_on_no_output(error_message, error_type=None, keep_quiet=None):
>>> @raise_error_on_no_output("Ingradients missing") >>> @raise_error_on_no_output("Ingradients missing")
... def get_indradients(_raise_error=1): return ... def get_indradients(_raise_error=1): return
... ...
>>> get_indradients()
>>> get_ingradients()
`Exception Name`: Ingradients missing `Exception Name`: Ingradients missing
""" """
def decorator_raise_error_on_no_output(func): def decorator_raise_error_on_no_output(func):


+ 16
- 7
frappe/utils/password.py View File

@@ -156,20 +156,29 @@ def create_auth_table():
frappe.db.create_auth_table() frappe.db.create_auth_table()




def encrypt(pwd):
cipher_suite = Fernet(encode(get_encryption_key()))
cipher_text = cstr(cipher_suite.encrypt(encode(pwd)))
def encrypt(txt, encryption_key=None):
# Only use Fernet.generate_key().decode() to enter encyption_key value

try:
cipher_suite = Fernet(encode(encryption_key or get_encryption_key()))
except Exception:
# encryption_key is not in 32 url-safe base64-encoded format
frappe.throw(_('Encryption key is in invalid format!'))

cipher_text = cstr(cipher_suite.encrypt(encode(txt)))
return cipher_text return cipher_text




def decrypt(pwd):
def decrypt(txt, encryption_key=None):
# Only use encryption_key value generated with Fernet.generate_key().decode()

try: try:
cipher_suite = Fernet(encode(get_encryption_key()))
plain_text = cstr(cipher_suite.decrypt(encode(pwd)))
cipher_suite = Fernet(encode(encryption_key or get_encryption_key()))
plain_text = cstr(cipher_suite.decrypt(encode(txt)))
return plain_text return plain_text
except InvalidToken: except InvalidToken:
# encryption_key in site_config is changed and not valid # encryption_key in site_config is changed and not valid
frappe.throw(_('Encryption key is invalid, Please check site_config.json'))
frappe.throw(_('Encryption key is invalid' + '!' if encryption_key else ', please check site_config.json.'))




def get_encryption_key(): def get_encryption_key():


+ 22
- 0
frappe/utils/safe_exec.py View File

@@ -150,6 +150,7 @@ def get_safe_globals():
# default writer allows write access # default writer allows write access
out._write_ = _write out._write_ = _write
out._getitem_ = _getitem out._getitem_ = _getitem
out._getattr_ = _getattr


# allow iterators and list comprehension # allow iterators and list comprehension
out._getiter_ = iter out._getiter_ = iter
@@ -176,6 +177,27 @@ def _getitem(obj, key):
raise SyntaxError('Key starts with _') raise SyntaxError('Key starts with _')
return obj[key] return obj[key]


def _getattr(object, name, default=None):
# guard function for RestrictedPython
# allow any key to be accessed as long as
# 1. it does not start with an underscore (safer_getattr)
# 2. it is not an UNSAFE_ATTRIBUTES

UNSAFE_ATTRIBUTES = {
# Generator Attributes
"gi_frame", "gi_code",
# Coroutine Attributes
"cr_frame", "cr_code", "cr_origin",
# Async Generator Attributes
"ag_code", "ag_frame",
# Traceback Attributes
"tb_frame", "tb_next",
}

if isinstance(name, str) and (name in UNSAFE_ATTRIBUTES):
raise SyntaxError("{name} is an unsafe attribute".format(name=name))
return RestrictedPython.Guards.safer_getattr(object, name, default=default)

def _write(obj): def _write(obj):
# guard function for RestrictedPython # guard function for RestrictedPython
# allow writing to any object # allow writing to any object


Loading…
Cancel
Save