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:
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,
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,
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,
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)

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


whitelisted = []
guest_methods = []
xss_safe_methods = []
@@ -1692,6 +1696,23 @@ def safe_eval(code, eval_globals=None, eval_locals=None):
"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:
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"
}
],
"modified": "2020-08-06 12:39:30.740379",
"modified": "2021-06-02 13:04:53.118716",
"modified_by": "Administrator",
"module": "Core",
"name": "Module Def",
@@ -69,6 +69,7 @@
"read": 1,
"report": 1,
"role": "Administrator",
"select": 1,
"share": 1,
"write": 1
},
@@ -78,7 +79,14 @@
"read": 1,
"report": 1,
"role": "System Manager",
"select": 1,
"write": 1
},
{
"read": 1,
"report": 1,
"role": "All",
"select": 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",
"fieldtype": "Data",
"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,
@@ -306,7 +296,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2021-04-29 21:21:06.476372",
"modified": "2021-06-02 06:49:16.782806",
"modified_by": "Administrator",
"module": "Custom",
"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 quopri
from email.parser import Parser
from email.policy import SMTPUTF8
from html2text import html2text
from six.moves import html_parser as HTMLParser

import frappe
from frappe import _, safe_encode, task
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
class EmailQueue(Document):
@@ -41,6 +45,19 @@ class EmailQueue(Document):
duplicate.set_recipients(recipients)
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
def find(cls, name):
return frappe.get_doc(cls.DOCTYPE, name)
@@ -74,8 +91,6 @@ class EmailQueue(Document):
return json.loads(self.attachments) if self.attachments else []

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

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

@@ -300,3 +315,283 @@ def send_now(name):
def on_doctype_update():
"""Add index in `tabCommunication` for `(reference_doctype, reference_name)`"""
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.website.website_generator import WebsiteGenerator
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.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

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 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():
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

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)

self.secure_session(self._session)
self.secure_session(_session)
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
if res[0]!=235:
frappe.msgprint(res[1], raise_exception=frappe.OutgoingEmailError)

self._session = _session
return self._session

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.email_body import (replace_filename_with_cid,
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):
@@ -43,27 +42,25 @@ This is the text version of this email
uni_chr1 = chr(40960)
uni_chr2 = chr(1972)

email = get_email_queue(
queue_doc = QueueBuilder(
recipients=['test@example.com'],
sender='me@example.com',
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')
self.assertTrue(b"<h1>=EA=80=80abcd=DE=B4</h1>" in result)

def test_prepare_message_returns_cr_lf(self):
email = get_email_queue(
queue_doc = QueueBuilder(
recipients=['test@example.com'],
sender='me@example.com',
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'))

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


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

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

&> li {
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)

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")

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]

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 (?)

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
import frappe
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):
def setUp(self):
frappe.delete_doc('Email Account', 'Test Email Account Password')
@@ -104,6 +104,16 @@ class TestPassword(unittest.TestCase):
doc.save()
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):
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")
... def get_indradients(_raise_error=1): return
...
>>> get_indradients()
>>> get_ingradients()
`Exception Name`: Ingradients missing
"""
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()


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


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

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
except InvalidToken:
# 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():


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

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

# allow iterators and list comprehension
out._getiter_ = iter
@@ -176,6 +177,27 @@ def _getitem(obj, key):
raise SyntaxError('Key starts with _')
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):
# guard function for RestrictedPython
# allow writing to any object


Loading…
Cancel
Save