Added CC in Communication to manually specify whom to notify. frappe/erpnext#3697

This commit is contained in:
Anand Doshi 2015-09-15 19:25:09 +05:30
parent b28bda4beb
commit 2b9cb67e1f
12 changed files with 669 additions and 432 deletions

View file

@ -309,7 +309,7 @@ def sendmail(recipients=(), sender="", subject="No Subject", message="No Message
as_markdown=False, bulk=False, reference_doctype=None, reference_name=None,
unsubscribe_method=None, unsubscribe_params=None, unsubscribe_message=None,
attachments=None, content=None, doctype=None, name=None, reply_to=None,
cc=(), message_id=None, as_bulk=False, send_after=None):
cc=(), message_id=None, as_bulk=False, send_after=None, expose_recipients=False):
"""Send email using user's default **Email Account** or global default **Email Account**.
@ -327,6 +327,7 @@ def sendmail(recipients=(), sender="", subject="No Subject", message="No Message
:param reply_to: Reply-To email id.
:param message_id: Used for threading. If a reply is received to this email, Message-Id is sent back as In-Reply-To in received email.
:param send_after: Send after the given datetime.
:param expose_recipients: Display all recipients in the footer message - "This email was sent to"
"""
if bulk or as_bulk:
@ -335,7 +336,8 @@ def sendmail(recipients=(), sender="", subject="No Subject", message="No Message
subject=subject, message=content or message,
reference_doctype = doctype or reference_doctype, reference_name = name or reference_name,
unsubscribe_method=unsubscribe_method, unsubscribe_params=unsubscribe_params, unsubscribe_message=unsubscribe_message,
attachments=attachments, reply_to=reply_to, cc=cc, message_id=message_id, send_after=send_after)
attachments=attachments, reply_to=reply_to, cc=cc, message_id=message_id, send_after=send_after,
expose_recipients=expose_recipients)
else:
import frappe.email
if as_markdown:

View file

@ -37,20 +37,108 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "sent_or_received",
"depends_on": "",
"fieldname": "communication_medium",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Sent or Received",
"label": "Communication Medium",
"no_copy": 0,
"options": "Sent\nReceived",
"options": "\nChat\nPhone\nEmail\nSMS\nVisit\nOther",
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 1,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "recipients",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Recipients",
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"depends_on": "eval:doc.communication_medium===\"Email\"",
"fieldname": "cc",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "CC",
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"depends_on": "eval:doc.communication_medium!==\"Email\"",
"fieldname": "phone_no",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Phone No.",
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "column_break_5",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
@ -78,6 +166,28 @@
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "sent_or_received",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Sent or Received",
"no_copy": 0,
"options": "Sent\nReceived",
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
@ -102,6 +212,27 @@
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "section_break_10",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
@ -127,7 +258,136 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "column_break_5",
"fieldname": "section_break_8",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "content",
"fieldtype": "Text Editor",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Content",
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"width": "400"
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 1,
"fieldname": "additional_info",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "More Information",
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "sender",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Sender",
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "sender_full_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Sender Full Name",
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"default": "Today",
"fieldname": "communication_date",
"fieldtype": "Datetime",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Date",
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "column_break_14",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
@ -194,230 +454,19 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "section_break_8",
"fieldtype": "Section Break",
"fieldname": "in_reply_to",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "In Reply To",
"no_copy": 0,
"options": "Communication",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "content",
"fieldtype": "Text Editor",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Content",
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"width": "400"
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "additional_info",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Additional Info",
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "recipients",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Recipients",
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "phone_no",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Phone No.",
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "communication_medium",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Communication Medium",
"no_copy": 0,
"options": "\nChat\nPhone\nEmail\nSMS\nVisit\nOther",
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "column_break_14",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "sender",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Sender",
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "sender_full_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Sender Full Name",
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "section_break2",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"no_copy": 0,
"options": "simple",
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "column_break4",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "By",
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
@ -474,39 +523,19 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "column_break5",
"fieldtype": "Column Break",
"default": "0",
"fieldname": "unread_notification_sent",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "On",
"label": "Unread Notification Sent",
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"default": "Today",
"fieldname": "communication_date",
"fieldtype": "Datetime",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Date",
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
@ -533,29 +562,6 @@
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"default": "0",
"fieldname": "unread_notification_sent",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Unread Notification Sent",
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}
],
"hide_heading": 0,
@ -567,7 +573,7 @@
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"modified": "2015-08-14 17:46:20.902296",
"modified": "2015-09-15 05:51:16.112080",
"modified_by": "Administrator",
"module": "Core",
"name": "Communication",

View file

@ -5,7 +5,7 @@ from __future__ import unicode_literals, absolute_import
import frappe
import json
from email.utils import formataddr, parseaddr
from frappe.utils import get_url, get_formatted_email, cstr, cint
from frappe.utils import get_url, get_formatted_email, cstr, cint, validate_email_add, split_emails
from frappe.utils.file_manager import get_file
import frappe.email.smtp
from frappe import _
@ -33,6 +33,14 @@ class Communication(Document):
else:
self.status = "Open"
# validate recipients
for email in split_emails(self.recipients):
validate_email_add(email, throw=True)
# validate CC
for email in split_emails(self.cc):
validate_email_add(email, throw=True)
def after_insert(self):
# send new comment to listening clients
comment = self.as_dict()
@ -73,51 +81,41 @@ class Communication(Document):
self.send_me_a_copy = send_me_a_copy
self.notify(print_html, print_format, attachments, recipients)
def set_incoming_outgoing_accounts(self):
self.incoming_email_account = self.outgoing_email_account = None
if self.reference_doctype:
self.incoming_email_account = frappe.db.get_value("Email Account",
{"append_to": self.reference_doctype, "enable_incoming": 1}, "email_id")
self.outgoing_email_account = frappe.db.get_value("Email Account",
{"append_to": self.reference_doctype, "enable_outgoing": 1},
["email_id", "always_use_account_email_id_as_sender"], as_dict=True)
if not self.incoming_email_account:
self.incoming_email_account = frappe.db.get_value("Email Account", {"default_incoming": 1}, "email_id")
if not self.outgoing_email_account:
self.outgoing_email_account = frappe.db.get_value("Email Account", {"default_outgoing": 1},
["email_id", "always_use_account_email_id_as_sender"], as_dict=True) or frappe._dict()
def notify(self, print_html=None, print_format=None, attachments=None, recipients=None, except_recipient=False):
def notify(self, print_html=None, print_format=None, attachments=None,
recipients=None, cc=None, fetched_from_email_account=False):
"""Calls a delayed celery task 'sendmail' that enqueus email in Bulk Email queue
:param print_html: Send given value as HTML attachment
:param print_format: Attach print format of parent document
:param attachments: A list of filenames that should be attached when sending this email
:param recipients: Email recipients
:param except_recipient: True when pulling email, the notification shouldn't go to the main recipient
:param cc: Send email as CC to
:param fetched_from_email_account: True when pulling email, the notification shouldn't go to the main recipient
"""
recipients, cc = self.get_recipients_and_cc(recipients, cc,
fetched_from_email_account=fetched_from_email_account)
self.emails_not_sent_to = set(self.all_email_addresses) - set(recipients) - set(cc)
if frappe.flags.in_test:
# for test cases, run synchronously
self._notify(print_html=print_html, print_format=print_format, attachments=attachments,
recipients=recipients, except_recipient=except_recipient)
recipients=recipients, cc=cc)
else:
from frappe.tasks import sendmail
sendmail.delay(frappe.local.site, self.name,
print_html=print_html, print_format=print_format, attachments=attachments,
recipients=recipients, except_recipient=except_recipient)
recipients=recipients, cc=cc)
def _notify(self, print_html=None, print_format=None, attachments=None,
recipients=None, cc=None):
def _notify(self, print_html=None, print_format=None, attachments=None, recipients=None, except_recipient=False):
self.prepare_to_notify(print_html, print_format, attachments)
if not recipients:
recipients = self.get_recipients(except_recipient=except_recipient)
frappe.sendmail(
recipients=recipients,
recipients=(recipients or []) + (cc or []),
expose_recipients=True,
sender=self.sender,
reply_to=self.incoming_email_account,
subject=self.subject,
@ -130,6 +128,27 @@ class Communication(Document):
bulk=True
)
def get_recipients_and_cc(self, recipients, cc, fetched_from_email_account=False):
self.all_email_addresses = []
if not recipients:
recipients = self.get_recipients()
if not cc:
cc = self.get_cc(recipients, fetched_from_email_account=fetched_from_email_account)
if fetched_from_email_account:
# email was already sent to the original recipient by the sender's email service
original_recipients, recipients = recipients, []
# cc that was received in the email
original_cc = split_emails(self.cc)
# don't cc to people who already received the mail from sender's email service
cc = list(set(cc) - set(original_cc) - set(original_recipients))
return recipients, cc
def prepare_to_notify(self, print_html=None, print_format=None, attachments=None):
"""Prepare to make multipart MIME Email
@ -165,78 +184,129 @@ class Communication(Document):
else:
self.attachments.append(a)
def get_recipients(self, except_recipient=False):
"""Build a list of users to which this email should go to"""
def set_incoming_outgoing_accounts(self):
self.incoming_email_account = self.outgoing_email_account = None
if self.reference_doctype:
self.incoming_email_account = frappe.db.get_value("Email Account",
{"append_to": self.reference_doctype, "enable_incoming": 1}, "email_id")
self.outgoing_email_account = frappe.db.get_value("Email Account",
{"append_to": self.reference_doctype, "enable_outgoing": 1},
["email_id", "always_use_account_email_id_as_sender"], as_dict=True)
if not self.incoming_email_account:
self.incoming_email_account = frappe.db.get_value("Email Account", {"default_incoming": 1}, "email_id")
if not self.outgoing_email_account:
self.outgoing_email_account = frappe.db.get_value("Email Account", {"default_outgoing": 1},
["email_id", "always_use_account_email_id_as_sender"], as_dict=True) or frappe._dict()
def get_recipients(self):
"""Build a list of email addresses for To"""
# [EDGE CASE] self.recipients can be None when an email is sent as BCC
original_recipients = [s.strip() for s in cstr(self.recipients).split(",")]
recipients = original_recipients[:]
recipients = split_emails(self.recipients)
if recipients:
# this will be used to eventually find email addresses that aren't sent to
self.all_email_addresses.extend(recipients)
# exclude email accounts
exclude = [d[0] for d in
frappe.db.get_all("Email Account", ["email_id"], {"enable_incoming": 1}, as_list=True)]
exclude += [d[0] for d in
frappe.db.get_all("Email Account", ["login_id"], {"enable_incoming": 1}, as_list=True)
if d[0]]
recipients = self.filter_email_list(recipients, exclude)
return recipients
def get_cc(self, recipients=None, fetched_from_email_account=False):
"""Build a list of email addresses for CC"""
# get a copy of CC list
cc = split_emails(self.cc)
if self.reference_doctype and self.reference_name:
recipients += self.get_earlier_participants()
recipients += self.get_commentors()
recipients += self.get_assignees()
recipients += self.get_starrers()
if not cc or fetched_from_email_account:
# if CC is not mentioned from the UI or is a fetched email, add follows to CC
cc.append(self.get_owner_email())
cc += self.get_assignees()
cc += self.get_starrers()
# remove unsubscribed recipients
unsubscribed = [d[0] for d in frappe.db.get_all("User", ["name"], {"thread_notify": 0}, as_list=True)]
email_accounts = [d[0] for d in frappe.db.get_all("Email Account", ["email_id"], {"enable_incoming": 1}, as_list=True)]
sender = parseaddr(self.sender)[1]
if fetched_from_email_account and self.in_reply_to:
# add sender of previous reply
cc.append(frappe.db.get_value("Communication", self.in_reply_to, "sender"))
if cc:
# this will be used to eventually find email addresses that aren't sent to
self.all_email_addresses.extend(cc)
# exclude email accounts, unfollows, recipients and unsubscribes
exclude = [d[0] for d in
frappe.db.get_all("Email Account", ["email_id"], {"enable_incoming": 1}, as_list=True)]
exclude += [d[0] for d in
frappe.db.get_all("Email Account", ["login_id"], {"enable_incoming": 1}, as_list=True)
if d[0]]
exclude += [d[0] for d in frappe.db.get_all("User", ["name"], {"thread_notify": 0}, as_list=True)]
exclude += [parseaddr(email)[1] for email in recipients]
if fetched_from_email_account:
# exclude sender when pulling email
exclude += [parseaddr(self.sender)[1]]
if self.reference_doctype and self.reference_name:
exclude += [d[0] for d in frappe.db.get_all("Email Unsubscribe", ["email"],
{"reference_doctype": self.reference_doctype, "reference_name": self.reference_name}, as_list=True)]
cc = self.filter_email_list(cc, exclude)
if getattr(self, "send_me_a_copy", False) and self.sender not in cc:
self.all_email_addresses.append(self.sender)
cc.append(self.sender)
return cc
def filter_email_list(self, email_list, exclude):
# temp variables
filtered = []
email_addresses = []
for e in list(set(recipients)):
if (e=="Administrator") or ((e==self.sender) and (e not in original_recipients)) or \
(e in unsubscribed) or (e in email_accounts):
email_address_list = []
for email in list(set(email_list)):
if email in exclude:
continue
email_id = parseaddr(e)[1]
if not email_id:
email_address = (parseaddr(email)[1] or "").lower()
if not email_address:
continue
if email_id==sender or email_id in unsubscribed or email_id in email_accounts:
continue
if except_recipient and (e==self.recipients or email_id==self.recipients):
# while pulling email, don't send email to current recipient
if email_address in exclude:
continue
# make sure of case-insensitive uniqueness of email address
if email_id.lower() not in email_addresses:
if email_address not in email_address_list:
# append the full email i.e. "Human <human@example.com>"
filtered.append(e)
email_addresses.append(email_id.lower())
if getattr(self, "send_me_a_copy", False):
filtered.append(self.sender)
filtered.append(email)
email_address_list.append(email_address)
return filtered
def get_starrers(self):
"""Return list of users who have starred this document."""
if self.reference_doctype and self.reference_name:
return self.get_parent_doc().get_starred_by()
else:
return []
return [( get_formatted_email(user) or user ) for user in self.get_parent_doc().get_starred_by()]
def get_earlier_participants(self):
return frappe.db.sql_list("""
select distinct sender
from tabCommunication where
reference_doctype=%s and reference_name=%s""",
(self.reference_doctype, self.reference_name))
def get_commentors(self):
return frappe.db.sql_list("""
select distinct comment_by
from tabComment where
comment_doctype=%s and comment_docname=%s and
ifnull(unsubscribed, 0)=0 and comment_by!='Administrator'""",
(self.reference_doctype, self.reference_name))
def get_owner_email(self):
owner = self.get_parent_doc().owner
return get_formatted_email(owner) or owner
def get_assignees(self):
return [d.owner for d in frappe.db.get_all("ToDo", filters={"reference_type": self.reference_doctype,
"reference_name": self.reference_name, "status": "Open"}, fields=["owner"])]
return [( get_formatted_email(d.owner) or d.owner ) for d in
frappe.db.get_all("ToDo", filters={
"reference_type": self.reference_doctype,
"reference_name": self.reference_name,
"status": "Open"
}, fields=["owner"])
]
def get_attach_link(self, print_format):
"""Returns public link for the attachment via `templates/emails/print_link.html`."""
@ -256,7 +326,7 @@ def on_doctype_update():
def make(doctype=None, name=None, content=None, subject=None, sent_or_received = "Sent",
sender=None, recipients=None, communication_medium="Email", send_email=False,
print_html=None, print_format=None, attachments='[]', ignore_doctype_permissions=False,
send_me_a_copy=False):
send_me_a_copy=False, cc=None):
"""Make a new communication.
:param doctype: Reference DocType.
@ -289,6 +359,7 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received =
"content": content,
"sender": sender,
"recipients": recipients,
"cc": cc or None,
"communication_medium": "Email",
"sent_or_received": sent_or_received,
"reference_doctype": doctype,
@ -300,15 +371,13 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received =
# if not committed, delayed task doesn't find the communication
frappe.db.commit()
recipients = None
if send_email:
comm.send_me_a_copy = send_me_a_copy
recipients = comm.get_recipients()
comm.send(print_html, print_format, attachments, send_me_a_copy=send_me_a_copy, recipients=recipients)
comm.send(print_html, print_format, attachments, send_me_a_copy=send_me_a_copy)
return {
"name": comm.name,
"recipients": ", ".join(recipients) if recipients else None
"emails_not_sent_to": ", ".join(comm.emails_not_sent_to) if hasattr(comm, "emails_not_sent_to") else None
}
@frappe.whitelist()

View file

@ -10,13 +10,14 @@ from frappe.email.smtp import SMTPServer, get_outgoing_email_account
from frappe.email.email_body import get_email, get_formatted_html
from frappe.utils.verified_command import get_signed_params, verify_request
from html2text import html2text
from frappe.utils import get_url, nowdate, encode, now_datetime, add_days
from frappe.utils import get_url, nowdate, encode, now_datetime, add_days, split_emails
class BulkLimitCrossedError(frappe.ValidationError): pass
def send(recipients=None, sender=None, subject=None, message=None, reference_doctype=None,
reference_name=None, unsubscribe_method=None, unsubscribe_params=None, unsubscribe_message=None,
attachments=None, reply_to=None, cc=(), message_id=None, send_after=None):
attachments=None, reply_to=None, cc=(), message_id=None, send_after=None,
expose_recipients=False):
"""Add email to sending queue (Bulk Email)
:param recipients: List of recipients.
@ -39,7 +40,7 @@ def send(recipients=None, sender=None, subject=None, message=None, reference_doc
return
if isinstance(recipients, basestring):
recipients = recipients.split(",")
recipients = split_emails(recipients)
if isinstance(send_after, int):
send_after = add_days(nowdate(), send_after)
@ -66,23 +67,30 @@ def send(recipients=None, sender=None, subject=None, message=None, reference_doc
else:
unsubscribed = []
for email in filter(None, list(set(recipients))):
if email not in unsubscribed:
email_content = formatted
email_text_context = text_content
recipients = [r for r in list(set(recipients)) if r and r not in unsubscribed]
if reference_doctype:
unsubscribe_url = get_unsubcribed_url(reference_doctype, reference_name, email,
unsubscribe_method, unsubscribe_params)
for email in recipients:
email_content = formatted
email_text_context = text_content
# add to queue
email_content = add_unsubscribe_link(email_content, email, reference_doctype,
reference_name, unsubscribe_url, unsubscribe_message)
if reference_doctype:
unsubscribe_link = get_unsubscribe_link(
reference_doctype=reference_doctype,
reference_name=reference_name,
email=email,
recipients=recipients,
expose_recipients=expose_recipients,
unsubscribe_method=unsubscribe_method,
unsubscribe_params=unsubscribe_params,
unsubscribe_message=unsubscribe_message
)
email_text_context += "\n" + _("This email was sent to {0}. To unsubscribe click on this link: {1}").format(email, unsubscribe_url)
email_content = email_content.replace("<!--unsubscribe link here-->", unsubscribe_link.html)
email_text_context += unsubscribe_link.text
add(email, sender, subject, email_content, email_text_context, reference_doctype,
reference_name, attachments, reply_to, cc, message_id, send_after)
# add to queue
add(email, sender, subject, email_content, email_text_context, reference_doctype,
reference_name, attachments, reply_to, cc, message_id, send_after)
def add(email, sender, subject, formatted, text_content=None,
reference_doctype=None, reference_name=None, attachments=None, reply_to=None,
@ -129,18 +137,41 @@ def check_bulk_limit(recipients):
throw(_("Email limit {0} crossed").format(monthly_bulk_mail_limit),
BulkLimitCrossedError)
def add_unsubscribe_link(message, email, reference_doctype, reference_name, unsubscribe_url, unsubscribe_message):
unsubscribe_link = """<div style="padding: 7px; text-align: center; color: #8D99A6;">
{email}. <a href="{unsubscribe_url}" style="color: #8D99A6; text-decoration: underline;
target="_blank">{unsubscribe_message}.
</a>
</div>""".format(unsubscribe_url = unsubscribe_url,
email= _("This email was sent to {0}").format(email),
unsubscribe_message = unsubscribe_message or _("Unsubscribe from this list"))
def get_unsubscribe_link(reference_doctype, reference_name,
email, recipients, expose_recipients, unsubscribe_method, unsubscribe_params, unsubscribe_message):
message = message.replace("<!--unsubscribe link here-->", unsubscribe_link)
unsubscribe_email = recipients if expose_recipients else [email]
unsubscribe_email = _("This email was sent to {0}").format(", ".join(unsubscribe_email))
return message
if not unsubscribe_message:
unsubscribe_message = _("Unsubscribe from this list")
unsubscribe_url = get_unsubcribed_url(reference_doctype, reference_name, email,
unsubscribe_method, unsubscribe_params)
html = """<div style="margin: 15px auto; padding: 0px 7px; text-align: center; color: #8d99a6;">
{email}
<p style="margin: 15px auto;">
<a href="{unsubscribe_url}" style="color: #8d99a6; text-decoration: underline;
target="_blank">{unsubscribe_message}
</a>
</p>
</div>""".format(
unsubscribe_url = unsubscribe_url,
email=unsubscribe_email,
unsubscribe_message=unsubscribe_message
)
text = "\n{email}\n\n{unsubscribe_message}: {unsubscribe_url}".format(
email=unsubscribe_email,
unsubscribe_message=unsubscribe_message,
unsubscribe_url=unsubscribe_url
)
return frappe._dict({
"html": html,
"text": text
})
def get_unsubcribed_url(reference_doctype, reference_name, email, unsubscribe_method, unsubscribe_params):
params = {"email": email.encode("utf-8"),

View file

@ -137,7 +137,7 @@ class EmailAccount(Document):
else:
frappe.db.commit()
communication.notify(attachments=communication._attachments, except_recipient=True)
communication.notify(attachments=communication._attachments, fetched_from_email_account=True)
if exceptions:
raise Exception, frappe.as_json(exceptions)
@ -158,6 +158,7 @@ class EmailAccount(Document):
"sender_full_name": email.from_real_name,
"sender": email.from_email,
"recipients": email.mail.get("To"),
"cc": email.mail.get("CC"),
"email_account": self.name,
"communication_medium": "Email"
})
@ -208,6 +209,9 @@ class EmailAccount(Document):
if frappe.db.exists("Communication", in_reply_to):
parent = frappe.get_doc("Communication", in_reply_to)
# set in_reply_to of current communication
communication.in_reply_to = in_reply_to
if parent.reference_name:
parent = frappe.get_doc(parent.reference_doctype,
parent.reference_name)

View file

@ -5,7 +5,7 @@ from __future__ import unicode_literals
import frappe
from frappe.utils.pdf import get_pdf
from frappe.email.smtp import get_outgoing_email_account
from frappe.utils import get_url, scrub_urls, strip, expand_relative_urls, cint
from frappe.utils import get_url, scrub_urls, strip, expand_relative_urls, cint, split_emails
import email.utils
from markdown2 import markdown
@ -42,7 +42,7 @@ class EMail:
if isinstance(recipients, basestring):
recipients = recipients.replace(';', ',').replace('\n', '')
recipients = recipients.split(',')
recipients = split_emails(recipients)
# remove null
recipients = filter(None, (strip(r) for r in recipients))
@ -238,18 +238,18 @@ def get_footer(email_account, footer=None):
footer = footer or ""
if email_account and email_account.footer:
footer += email_account.footer
footer += '<div style="margin: 15px auto;">{0}</div>'.format(email_account.footer)
footer += "<!--unsubscribe link here-->"
company_address = frappe.db.get_default("email_footer_address")
if company_address:
footer += '<div style="text-align: center; color: #8d99a6">{0}</div>'\
footer += '<div style="margin: 15px auto; text-align: center; color: #8d99a6">{0}</div>'\
.format(company_address.replace("\n", "<br>"))
if not cint(frappe.db.get_default("disable_standard_email_footer")):
for default_mail_footer in frappe.get_hooks("default_mail_footer"):
footer += default_mail_footer
footer += '<div style="margin: 15px auto;">{0}</div>'.format(default_mail_footer)
return footer

View file

@ -193,6 +193,29 @@ $.extend(frappe.user, {
is_report_manager: function() {
return frappe.user.has_role(['Administrator', 'System Manager', 'Report Manager']);
},
get_formatted_email: function(email) {
var fullname = frappe.user.full_name(email);
if (!fullname) {
return email;
} else {
// to quote or to not
var quote = '';
// only if these special characters are found
// why? To make the output same as that in python!
if (fullname.search(/[\[\]\\()<>@,:;".]/) !== -1) {
quote = '"';
}
return repl('%(quote)s%(fullname)s%(quote)s <%(email)s>', {
fullname: fullname,
email: email,
quote: quote
});
}
}
});
frappe.session_alive = true;

View file

@ -43,6 +43,9 @@ frappe.utils = {
});
return out.join(newline);
},
escape_html: function(txt) {
return $("<div></div>").text(txt || "").html();
},
is_url: function(txt) {
return txt.toLowerCase().substr(0,7)=='http://'
|| txt.toLowerCase().substr(0,8)=='https://'

View file

@ -2,12 +2,17 @@
// MIT License. See license.txt
frappe.ui.is_starred = function(doc) {
var starred = frappe.ui.get_starred_by(doc);
return starred.indexOf(user)===-1 ? false : true;
}
frappe.ui.get_starred_by = function(doc) {
var starred = doc._starred_by;
if(starred) {
starred = JSON.parse(starred);
return starred.indexOf(user)===-1 ? false : true;
}
return false;
return starred || [];
}
frappe.ui.toggle_star = function($btn, doctype, name) {

View file

@ -14,41 +14,7 @@ frappe.views.CommunicationComposer = Class.extend({
this.dialog = new frappe.ui.Dialog({
title: __("Add Reply") + ": " + (this.subject || ""),
no_submit_on_enter: true,
fields: [
{label:__("To"), fieldtype:"Data", reqd: 1, fieldname:"recipients"},
{fieldtype: "Section Break"},
{fieldtype: "Column Break"},
{label:__("Subject"), fieldtype:"Data", reqd: 1,
fieldname:"subject"},
{fieldtype: "Column Break"},
{label:__("Standard Reply"), fieldtype:"Link", options:"Standard Reply",
fieldname:"standard_reply"},
{fieldtype: "Section Break"},
{label:__("Message"), fieldtype:"Text Editor", reqd: 1,
fieldname:"content"},
{fieldtype: "Section Break"},
{fieldtype: "Column Break"},
{label:__("Send As Email"), fieldtype:"Check",
fieldname:"send_email"},
{label:__("Send me a copy"), fieldtype:"Check",
fieldname:"send_me_a_copy"},
{label:__("Communication Medium"), fieldtype:"Select",
options: ["Phone", "Chat", "Email", "SMS", "Visit", "Other"],
fieldname:"communication_medium"},
{label:__("Sent or Received"), fieldtype:"Select",
options: ["Received", "Sent"],
fieldname:"sent_or_received"},
{label:__("Attach Document Print"), fieldtype:"Check",
fieldname:"attach_document_print"},
{label:__("Select Print Format"), fieldtype:"Select",
fieldname:"select_print_format"},
{fieldtype: "Column Break"},
{label:__("Select Attachments"), fieldtype:"HTML",
fieldname:"select_attachments"}
],
fields: this.get_fields(),
primary_action_label: "Send",
primary_action: function() {
me.send_action();
@ -79,6 +45,100 @@ frappe.views.CommunicationComposer = Class.extend({
this.dialog.show();
},
get_fields: function() {
var cc_fields = this.get_cc_fields();
var fields_before_cc = [
{fieldtype: "Section Break"},
{label:__("To"), fieldtype:"Data", reqd: 1, fieldname:"recipients"},
{fieldtype: "Section Break", collapsible: 1, label: "CC & Standard Reply"},
{label:__("CC"), fieldtype:"Data", fieldname:"cc"},
];
var fields_after_cc = [
{label:__("Standard Reply"), fieldtype:"Link", options:"Standard Reply",
fieldname:"standard_reply"},
{fieldtype: "Section Break"},
{label:__("Subject"), fieldtype:"Data", reqd: 1,
fieldname:"subject"},
{fieldtype: "Section Break"},
{label:__("Message"), fieldtype:"Text Editor", reqd: 1,
fieldname:"content"},
{fieldtype: "Section Break"},
{fieldtype: "Column Break"},
{label:__("Send As Email"), fieldtype:"Check",
fieldname:"send_email"},
{label:__("Send me a copy"), fieldtype:"Check",
fieldname:"send_me_a_copy"},
{label:__("Communication Medium"), fieldtype:"Select",
options: ["Phone", "Chat", "Email", "SMS", "Visit", "Other"],
fieldname:"communication_medium"},
{label:__("Sent or Received"), fieldtype:"Select",
options: ["Received", "Sent"],
fieldname:"sent_or_received"},
{label:__("Attach Document Print"), fieldtype:"Check",
fieldname:"attach_document_print"},
{label:__("Select Print Format"), fieldtype:"Select",
fieldname:"select_print_format"},
{fieldtype: "Column Break"},
{label:__("Select Attachments"), fieldtype:"HTML",
fieldname:"select_attachments"}
];
return fields_before_cc.concat(cc_fields).concat(fields_after_cc);
},
get_cc_fields: function() {
var cc = [ [this.frm.doc.owner, 1] ];
var starred_by = frappe.ui.get_starred_by(this.frm.doc);
if (starred_by) {
for ( var i=0, l=starred_by.length; i<l; i++ ) {
cc.push( [starred_by[i], 1] );
}
}
var assignments = this.frm.get_docinfo().assignments;
if (assignments) {
for ( var i=0, l=assignments.length; i<l; i++ ) {
cc.push( [assignments[i].owner, 1] );
}
}
var comments = this.frm.get_docinfo().comments;
if (comments) {
for ( var i=0, l=comments.length; i<l; i++ ) {
cc.push( [comments[i].comment_by, 0] );
}
}
var added = [];
var cc_fields = [];
for ( var i=0, l=cc.length; i<l; i++ ) {
var email = cc[i][0];
var default_value = cc[i][1];
if ( !email || added.indexOf(email)!==-1 || email.indexOf("@")===-1 ) {
continue;
}
// for deduplication
added.push(email);
email = frappe.user.get_formatted_email(email);
cc_fields.push({
"label": frappe.utils.escape_html(email),
"fieldtype": "Check",
"fieldname": email,
"is_cc_checkbox": 1,
"default": default_value
});
}
return cc_fields;
},
prepare: function() {
this.setup_subject_and_recipients();
this.setup_print();
@ -284,10 +344,10 @@ frappe.views.CommunicationComposer = Class.extend({
},
send_action: function() {
var me = this,
form_values = me.dialog.get_values(),
btn = me.dialog.get_primary_btn();
var me = this;
var btn = me.dialog.get_primary_btn();
var form_values = this.get_values();
if(!form_values) return;
var selected_attachments = $.map($(me.dialog.wrapper)
@ -312,6 +372,26 @@ frappe.views.CommunicationComposer = Class.extend({
}
},
get_values: function() {
var form_values = this.dialog.get_values();
// cc
for ( var i=0, l=this.dialog.fields.length; i < l; i++ ) {
var df = this.dialog.fields[i];
if ( df.is_cc_checkbox ) {
// concat in cc
if ( form_values[df.fieldname] ) {
form_values.cc = ( form_values.cc ? (form_values.cc + ", ") : "" ) + df.fieldname;
}
delete form_values[df.fieldname];
}
}
return form_values;
},
send_email: function(btn, form_values, selected_attachments, print_html, print_format) {
var me = this;
@ -334,6 +414,7 @@ frappe.views.CommunicationComposer = Class.extend({
method:"frappe.core.doctype.communication.communication.make",
args: {
recipients: form_values.recipients,
cc: form_values.cc,
subject: form_values.subject,
content: form_values.content,
doctype: me.doc.doctype,
@ -349,8 +430,11 @@ frappe.views.CommunicationComposer = Class.extend({
btn: btn,
callback: function(r) {
if(!r.exc) {
if(form_values.send_email && r.message["recipients"])
msgprint(__("Email sent to {0}", [r.message["recipients"]]));
if(form_values.send_email && r.message["emails_not_sent_to"]) {
msgprint( __("Email not sent to {0}",
[ frappe.utils.escape_html(r.message["emails_not_sent_to"]) ]) );
}
me.dialog.hide();
if (cur_frm) {

View file

@ -185,7 +185,8 @@ def run_async_task(self, site=None, user=None, cmd=None, form_dict=None, hijack_
@celery_task()
def sendmail(site, communication_name, print_html=None, print_format=None, attachments=None, recipients=None, except_recipient=False):
def sendmail(site, communication_name, print_html=None, print_format=None, attachments=None,
recipients=None, cc=None):
try:
frappe.connect(site=site)
@ -193,7 +194,8 @@ def sendmail(site, communication_name, print_html=None, print_format=None, attac
for i in xrange(3):
try:
communication = frappe.get_doc("Communication", communication_name)
communication._notify(print_html=print_html, print_format=print_format, attachments=attachments, recipients=recipients, except_recipient=except_recipient)
communication._notify(print_html=print_html, print_format=print_format, attachments=attachments,
recipients=recipients, cc=cc)
except MySQLdb.OperationalError, e:
# deadlock, try again
if e.args[0]==1213:

View file

@ -9,7 +9,6 @@ import os, sys, re, urllib
import frappe
import requests
# utility functions like cint, int, flt, etc.
from frappe.utils.data import *
@ -89,6 +88,15 @@ def validate_email_add(email_str, throw=False):
return matched
def split_emails(txt):
email_list = []
for email in re.split(''',(?=(?:[^"]|"[^"]*")*$)''', cstr(txt)):
email = strip(cstr(email))
if email:
email_list.append(email)
return email_list
def random_string(length):
"""generate a random string"""
import string