Merge branch 'develop' of github.com:frappe/frappe into drop-py2-code
This commit is contained in:
commit
2ad9d202cb
14 changed files with 410 additions and 303 deletions
|
|
@ -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)))
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,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("""
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@
|
|||
margin: 0;
|
||||
padding: var(--padding-xs);
|
||||
z-index: 1;
|
||||
min-width: 250px;
|
||||
|
||||
&> li {
|
||||
cursor: pointer;
|
||||
|
|
|
|||
|
|
@ -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'""",
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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…
Add table
Reference in a new issue