Selaa lähdekoodia

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

version-14
Anand Doshi 9 vuotta sitten
vanhempi
commit
2b9cb67e1f
12 muutettua tiedostoa jossa 506 lisäystä ja 269 poistoa
  1. +4
    -2
      frappe/__init__.py
  2. +112
    -106
      frappe/core/doctype/communication/communication.json
  3. +151
    -82
      frappe/core/doctype/communication/communication.py
  4. +59
    -28
      frappe/email/bulk.py
  5. +5
    -1
      frappe/email/doctype/email_account/email_account.py
  6. +5
    -5
      frappe/email/email_body.py
  7. +23
    -0
      frappe/public/js/frappe/misc/user.js
  8. +3
    -0
      frappe/public/js/frappe/misc/utils.js
  9. +7
    -2
      frappe/public/js/frappe/ui/star.js
  10. +124
    -40
      frappe/public/js/frappe/views/communication.js
  11. +4
    -2
      frappe/tasks.py
  12. +9
    -1
      frappe/utils/__init__.py

+ 4
- 2
frappe/__init__.py Näytä tiedosto

@@ -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, as_markdown=False, bulk=False, reference_doctype=None, reference_name=None,
unsubscribe_method=None, unsubscribe_params=None, unsubscribe_message=None, unsubscribe_method=None, unsubscribe_params=None, unsubscribe_message=None,
attachments=None, content=None, doctype=None, name=None, reply_to=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**. """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 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 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 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: 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, subject=subject, message=content or message,
reference_doctype = doctype or reference_doctype, reference_name = name or reference_name, reference_doctype = doctype or reference_doctype, reference_name = name or reference_name,
unsubscribe_method=unsubscribe_method, unsubscribe_params=unsubscribe_params, unsubscribe_message=unsubscribe_message, unsubscribe_method=unsubscribe_method, unsubscribe_params=unsubscribe_params, unsubscribe_message=unsubscribe_message,
attachments=attachments, reply_to=reply_to, cc=cc, 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: else:
import frappe.email import frappe.email
if as_markdown: if as_markdown:


+ 112
- 106
frappe/core/doctype/communication/communication.json Näytä tiedosto

@@ -37,20 +37,21 @@
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"fieldname": "sent_or_received",
"depends_on": "",
"fieldname": "communication_medium",
"fieldtype": "Select", "fieldtype": "Select",
"hidden": 0, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"in_filter": 0, "in_filter": 0,
"in_list_view": 1, "in_list_view": 1,
"label": "Sent or Received",
"label": "Communication Medium",
"no_copy": 0, "no_copy": 0,
"options": "Sent\nReceived",
"options": "\nChat\nPhone\nEmail\nSMS\nVisit\nOther",
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 1,
"reqd": 0,
"search_index": 0, "search_index": 0,
"set_only_once": 0, "set_only_once": 0,
"unique": 0 "unique": 0
@@ -59,17 +60,15 @@
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"fieldname": "status",
"fieldtype": "Select",
"fieldname": "recipients",
"fieldtype": "Data",
"hidden": 0, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"in_filter": 0, "in_filter": 0,
"in_list_view": 1,
"label": "Status",
"in_list_view": 0,
"label": "Recipients",
"no_copy": 0, "no_copy": 0,
"options": "Open\nReplied\nClosed\nLinked",
"permlevel": 0, "permlevel": 0,
"precision": "",
"print_hide": 0, "print_hide": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
@@ -82,16 +81,15 @@
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"description": "Integrations can use this field to set email delivery status",
"fieldname": "delivery_status",
"fieldtype": "Select",
"hidden": 1,
"depends_on": "eval:doc.communication_medium===\"Email\"",
"fieldname": "cc",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"in_filter": 0, "in_filter": 0,
"in_list_view": 0, "in_list_view": 0,
"label": "Delivery Status",
"label": "CC",
"no_copy": 0, "no_copy": 0,
"options": "\nSent\nBounced\nOpened\nMarked As Spam\nRejected\nDelayed\nSoft-Bounced\nClicked\nRecipient Unsubscribed",
"permlevel": 0, "permlevel": 0,
"precision": "", "precision": "",
"print_hide": 0, "print_hide": 0,
@@ -106,19 +104,20 @@
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"fieldname": "subject",
"depends_on": "eval:doc.communication_medium!==\"Email\"",
"fieldname": "phone_no",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"in_filter": 0, "in_filter": 0,
"in_list_view": 0, "in_list_view": 0,
"label": "Subject",
"label": "Phone No.",
"no_copy": 0, "no_copy": 0,
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 1,
"reqd": 0,
"search_index": 0, "search_index": 0,
"set_only_once": 0, "set_only_once": 0,
"unique": 0 "unique": 0
@@ -148,15 +147,15 @@
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"fieldname": "reference_doctype",
"fieldtype": "Link",
"fieldname": "status",
"fieldtype": "Select",
"hidden": 0, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"in_filter": 0, "in_filter": 0,
"in_list_view": 0,
"label": "Reference DocType",
"in_list_view": 1,
"label": "Status",
"no_copy": 0, "no_copy": 0,
"options": "DocType",
"options": "Open\nReplied\nClosed\nLinked",
"permlevel": 0, "permlevel": 0,
"precision": "", "precision": "",
"print_hide": 0, "print_hide": 0,
@@ -171,21 +170,20 @@
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"fieldname": "reference_name",
"fieldtype": "Dynamic Link",
"fieldname": "sent_or_received",
"fieldtype": "Select",
"hidden": 0, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"in_filter": 0, "in_filter": 0,
"in_list_view": 0,
"label": "Reference Name",
"in_list_view": 1,
"label": "Sent or Received",
"no_copy": 0, "no_copy": 0,
"options": "reference_doctype",
"options": "Sent\nReceived",
"permlevel": 0, "permlevel": 0,
"precision": "",
"print_hide": 0, "print_hide": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0,
"reqd": 1,
"search_index": 0, "search_index": 0,
"set_only_once": 0, "set_only_once": 0,
"unique": 0 "unique": 0
@@ -194,13 +192,16 @@
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"fieldname": "section_break_8",
"fieldtype": "Section Break",
"hidden": 0,
"description": "Integrations can use this field to set email delivery status",
"fieldname": "delivery_status",
"fieldtype": "Select",
"hidden": 1,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"in_filter": 0, "in_filter": 0,
"in_list_view": 0, "in_list_view": 0,
"label": "Delivery Status",
"no_copy": 0, "no_copy": 0,
"options": "\nSent\nBounced\nOpened\nMarked As Spam\nRejected\nDelayed\nSoft-Bounced\nClicked\nRecipient Unsubscribed",
"permlevel": 0, "permlevel": 0,
"precision": "", "precision": "",
"print_hide": 0, "print_hide": 0,
@@ -215,37 +216,15 @@
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 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",
"fieldname": "section_break_10",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"in_filter": 0, "in_filter": 0,
"in_list_view": 0, "in_list_view": 0,
"label": "Additional Info",
"no_copy": 0, "no_copy": 0,
"permlevel": 0, "permlevel": 0,
"precision": "",
"print_hide": 0, "print_hide": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
@@ -258,19 +237,19 @@
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"fieldname": "recipients",
"fieldname": "subject",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"in_filter": 0, "in_filter": 0,
"in_list_view": 0, "in_list_view": 0,
"label": "Recipients",
"label": "Subject",
"no_copy": 0, "no_copy": 0,
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0,
"reqd": 1,
"search_index": 0, "search_index": 0,
"set_only_once": 0, "set_only_once": 0,
"unique": 0 "unique": 0
@@ -279,15 +258,15 @@
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"fieldname": "phone_no",
"fieldtype": "Data",
"fieldname": "section_break_8",
"fieldtype": "Section Break",
"hidden": 0, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"in_filter": 0, "in_filter": 0,
"in_list_view": 0, "in_list_view": 0,
"label": "Phone No.",
"no_copy": 0, "no_copy": 0,
"permlevel": 0, "permlevel": 0,
"precision": "",
"print_hide": 0, "print_hide": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
@@ -300,15 +279,14 @@
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"fieldname": "communication_medium",
"fieldtype": "Select",
"fieldname": "content",
"fieldtype": "Text Editor",
"hidden": 0, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"in_filter": 0, "in_filter": 0,
"in_list_view": 1,
"label": "Communication Medium",
"in_list_view": 0,
"label": "Content",
"no_copy": 0, "no_copy": 0,
"options": "\nChat\nPhone\nEmail\nSMS\nVisit\nOther",
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"read_only": 0, "read_only": 0,
@@ -316,21 +294,22 @@
"reqd": 0, "reqd": 0,
"search_index": 0, "search_index": 0,
"set_only_once": 0, "set_only_once": 0,
"unique": 0
"unique": 0,
"width": "400"
}, },
{ {
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0,
"fieldname": "column_break_14",
"fieldtype": "Column Break",
"collapsible": 1,
"fieldname": "additional_info",
"fieldtype": "Section Break",
"hidden": 0, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"in_filter": 0, "in_filter": 0,
"in_list_view": 0, "in_list_view": 0,
"label": "More Information",
"no_copy": 0, "no_copy": 0,
"permlevel": 0, "permlevel": 0,
"precision": "",
"print_hide": 0, "print_hide": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
@@ -386,14 +365,15 @@
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"fieldname": "section_break2",
"fieldtype": "Section Break",
"default": "Today",
"fieldname": "communication_date",
"fieldtype": "Datetime",
"hidden": 0, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"in_filter": 0, "in_filter": 0,
"in_list_view": 0, "in_list_view": 0,
"label": "Date",
"no_copy": 0, "no_copy": 0,
"options": "simple",
"permlevel": 0, "permlevel": 0,
"print_hide": 0, "print_hide": 0,
"read_only": 0, "read_only": 0,
@@ -407,15 +387,15 @@
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"fieldname": "column_break4",
"fieldname": "column_break_14",
"fieldtype": "Column Break", "fieldtype": "Column Break",
"hidden": 0, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"in_filter": 0, "in_filter": 0,
"in_list_view": 0, "in_list_view": 0,
"label": "By",
"no_copy": 0, "no_copy": 0,
"permlevel": 0, "permlevel": 0,
"precision": "",
"print_hide": 0, "print_hide": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
@@ -428,15 +408,15 @@
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"fieldname": "email_account",
"fieldname": "reference_doctype",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"in_filter": 0, "in_filter": 0,
"in_list_view": 0, "in_list_view": 0,
"label": "Email Account",
"label": "Reference DocType",
"no_copy": 0, "no_copy": 0,
"options": "Email Account",
"options": "DocType",
"permlevel": 0, "permlevel": 0,
"precision": "", "precision": "",
"print_hide": 0, "print_hide": 0,
@@ -451,19 +431,19 @@
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"default": "__user",
"fieldname": "user",
"fieldtype": "Link",
"fieldname": "reference_name",
"fieldtype": "Dynamic Link",
"hidden": 0, "hidden": 0,
"ignore_user_permissions": 1,
"ignore_user_permissions": 0,
"in_filter": 0, "in_filter": 0,
"in_list_view": 0, "in_list_view": 0,
"label": "User",
"label": "Reference Name",
"no_copy": 0, "no_copy": 0,
"options": "User",
"options": "reference_doctype",
"permlevel": 0, "permlevel": 0,
"precision": "",
"print_hide": 0, "print_hide": 0,
"read_only": 1,
"read_only": 0,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
"search_index": 0, "search_index": 0,
@@ -474,17 +454,19 @@
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"fieldname": "column_break5",
"fieldtype": "Column Break",
"fieldname": "in_reply_to",
"fieldtype": "Link",
"hidden": 0, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"in_filter": 0, "in_filter": 0,
"in_list_view": 0, "in_list_view": 0,
"label": "On",
"label": "In Reply To",
"no_copy": 0, "no_copy": 0,
"options": "Communication",
"permlevel": 0, "permlevel": 0,
"precision": "",
"print_hide": 0, "print_hide": 0,
"read_only": 0,
"read_only": 1,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
"search_index": 0, "search_index": 0,
@@ -495,16 +477,17 @@
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"default": "Today",
"fieldname": "communication_date",
"fieldtype": "Datetime",
"fieldname": "email_account",
"fieldtype": "Link",
"hidden": 0, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"in_filter": 0, "in_filter": 0,
"in_list_view": 0, "in_list_view": 0,
"label": "Date",
"label": "Email Account",
"no_copy": 0, "no_copy": 0,
"options": "Email Account",
"permlevel": 0, "permlevel": 0,
"precision": "",
"print_hide": 0, "print_hide": 0,
"read_only": 0, "read_only": 0,
"report_hide": 0, "report_hide": 0,
@@ -517,17 +500,19 @@
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"fieldname": "_user_tags",
"fieldtype": "Data",
"hidden": 1,
"ignore_user_permissions": 0,
"default": "__user",
"fieldname": "user",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 1,
"in_filter": 0, "in_filter": 0,
"in_list_view": 0, "in_list_view": 0,
"label": "User Tags",
"no_copy": 1,
"label": "User",
"no_copy": 0,
"options": "User",
"permlevel": 0, "permlevel": 0,
"print_hide": 1,
"read_only": 0,
"print_hide": 0,
"read_only": 1,
"report_hide": 0, "report_hide": 0,
"reqd": 0, "reqd": 0,
"search_index": 0, "search_index": 0,
@@ -556,6 +541,27 @@
"search_index": 0, "search_index": 0,
"set_only_once": 0, "set_only_once": 0,
"unique": 0 "unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "_user_tags",
"fieldtype": "Data",
"hidden": 1,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "User Tags",
"no_copy": 1,
"permlevel": 0,
"print_hide": 1,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
} }
], ],
"hide_heading": 0, "hide_heading": 0,
@@ -567,7 +573,7 @@
"is_submittable": 0, "is_submittable": 0,
"issingle": 0, "issingle": 0,
"istable": 0, "istable": 0,
"modified": "2015-08-14 17:46:20.902296",
"modified": "2015-09-15 05:51:16.112080",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Core", "module": "Core",
"name": "Communication", "name": "Communication",


+ 151
- 82
frappe/core/doctype/communication/communication.py Näytä tiedosto

@@ -5,7 +5,7 @@ from __future__ import unicode_literals, absolute_import
import frappe import frappe
import json import json
from email.utils import formataddr, parseaddr 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 from frappe.utils.file_manager import get_file
import frappe.email.smtp import frappe.email.smtp
from frappe import _ from frappe import _
@@ -33,6 +33,14 @@ class Communication(Document):
else: else:
self.status = "Open" 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): def after_insert(self):
# send new comment to listening clients # send new comment to listening clients
comment = self.as_dict() comment = self.as_dict()
@@ -73,51 +81,41 @@ class Communication(Document):
self.send_me_a_copy = send_me_a_copy self.send_me_a_copy = send_me_a_copy
self.notify(print_html, print_format, attachments, recipients) 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 """Calls a delayed celery task 'sendmail' that enqueus email in Bulk Email queue


:param print_html: Send given value as HTML attachment :param print_html: Send given value as HTML attachment
:param print_format: Attach print format of parent document :param print_format: Attach print format of parent document
:param attachments: A list of filenames that should be attached when sending this email :param attachments: A list of filenames that should be attached when sending this email
:param recipients: Email recipients :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: if frappe.flags.in_test:
# for test cases, run synchronously # for test cases, run synchronously
self._notify(print_html=print_html, print_format=print_format, attachments=attachments, self._notify(print_html=print_html, print_format=print_format, attachments=attachments,
recipients=recipients, except_recipient=except_recipient)
recipients=recipients, cc=cc)
else: else:
from frappe.tasks import sendmail from frappe.tasks import sendmail
sendmail.delay(frappe.local.site, self.name, sendmail.delay(frappe.local.site, self.name,
print_html=print_html, print_format=print_format, attachments=attachments, 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) self.prepare_to_notify(print_html, print_format, attachments)
if not recipients:
recipients = self.get_recipients(except_recipient=except_recipient)


frappe.sendmail( frappe.sendmail(
recipients=recipients,
recipients=(recipients or []) + (cc or []),
expose_recipients=True,
sender=self.sender, sender=self.sender,
reply_to=self.incoming_email_account, reply_to=self.incoming_email_account,
subject=self.subject, subject=self.subject,
@@ -130,6 +128,27 @@ class Communication(Document):
bulk=True 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): def prepare_to_notify(self, print_html=None, print_format=None, attachments=None):
"""Prepare to make multipart MIME Email """Prepare to make multipart MIME Email


@@ -165,78 +184,129 @@ class Communication(Document):
else: else:
self.attachments.append(a) 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 # [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: 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()

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]]


# 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 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)]


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


email_id = parseaddr(e)[1]
return cc

def filter_email_list(self, email_list, exclude):
# temp variables
filtered = []
email_address_list = []


if not email_id:
for email in list(set(email_list)):
if email in exclude:
continue continue


if email_id==sender or email_id in unsubscribed or email_id in email_accounts:
email_address = (parseaddr(email)[1] or "").lower()
if not email_address:
continue 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 continue


# make sure of case-insensitive uniqueness of email address # 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>" # 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 return filtered


def get_starrers(self): def get_starrers(self):
"""Return list of users who have starred this document.""" """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 []

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))
return [( get_formatted_email(user) or user ) for user in self.get_parent_doc().get_starred_by()]

def get_owner_email(self):
owner = self.get_parent_doc().owner
return get_formatted_email(owner) or owner


def get_assignees(self): 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): def get_attach_link(self, print_format):
"""Returns public link for the attachment via `templates/emails/print_link.html`.""" """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", def make(doctype=None, name=None, content=None, subject=None, sent_or_received = "Sent",
sender=None, recipients=None, communication_medium="Email", send_email=False, sender=None, recipients=None, communication_medium="Email", send_email=False,
print_html=None, print_format=None, attachments='[]', ignore_doctype_permissions=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. """Make a new communication.


:param doctype: Reference DocType. :param doctype: Reference DocType.
@@ -289,6 +359,7 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received =
"content": content, "content": content,
"sender": sender, "sender": sender,
"recipients": recipients, "recipients": recipients,
"cc": cc or None,
"communication_medium": "Email", "communication_medium": "Email",
"sent_or_received": sent_or_received, "sent_or_received": sent_or_received,
"reference_doctype": doctype, "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 # if not committed, delayed task doesn't find the communication
frappe.db.commit() frappe.db.commit()


recipients = None
if send_email: if send_email:
comm.send_me_a_copy = send_me_a_copy 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 { return {
"name": comm.name, "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() @frappe.whitelist()


+ 59
- 28
frappe/email/bulk.py Näytä tiedosto

@@ -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.email.email_body import get_email, get_formatted_html
from frappe.utils.verified_command import get_signed_params, verify_request from frappe.utils.verified_command import get_signed_params, verify_request
from html2text import html2text from 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 class BulkLimitCrossedError(frappe.ValidationError): pass


def send(recipients=None, sender=None, subject=None, message=None, reference_doctype=None, 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, 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) """Add email to sending queue (Bulk Email)


:param recipients: List of recipients. :param recipients: List of recipients.
@@ -39,7 +40,7 @@ def send(recipients=None, sender=None, subject=None, message=None, reference_doc
return return


if isinstance(recipients, basestring): if isinstance(recipients, basestring):
recipients = recipients.split(",")
recipients = split_emails(recipients)


if isinstance(send_after, int): if isinstance(send_after, int):
send_after = add_days(nowdate(), send_after) send_after = add_days(nowdate(), send_after)
@@ -66,23 +67,30 @@ def send(recipients=None, sender=None, subject=None, message=None, reference_doc
else: else:
unsubscribed = [] 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, def add(email, sender, subject, formatted, text_content=None,
reference_doctype=None, reference_name=None, attachments=None, reply_to=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), throw(_("Email limit {0} crossed").format(monthly_bulk_mail_limit),
BulkLimitCrossedError) 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"))

message = message.replace("<!--unsubscribe link here-->", unsubscribe_link)

return message
def get_unsubscribe_link(reference_doctype, reference_name,
email, recipients, expose_recipients, unsubscribe_method, unsubscribe_params, unsubscribe_message):

unsubscribe_email = recipients if expose_recipients else [email]
unsubscribe_email = _("This email was sent to {0}").format(", ".join(unsubscribe_email))

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): def get_unsubcribed_url(reference_doctype, reference_name, email, unsubscribe_method, unsubscribe_params):
params = {"email": email.encode("utf-8"), params = {"email": email.encode("utf-8"),


+ 5
- 1
frappe/email/doctype/email_account/email_account.py Näytä tiedosto

@@ -137,7 +137,7 @@ class EmailAccount(Document):


else: else:
frappe.db.commit() frappe.db.commit()
communication.notify(attachments=communication._attachments, except_recipient=True)
communication.notify(attachments=communication._attachments, fetched_from_email_account=True)


if exceptions: if exceptions:
raise Exception, frappe.as_json(exceptions) raise Exception, frappe.as_json(exceptions)
@@ -158,6 +158,7 @@ class EmailAccount(Document):
"sender_full_name": email.from_real_name, "sender_full_name": email.from_real_name,
"sender": email.from_email, "sender": email.from_email,
"recipients": email.mail.get("To"), "recipients": email.mail.get("To"),
"cc": email.mail.get("CC"),
"email_account": self.name, "email_account": self.name,
"communication_medium": "Email" "communication_medium": "Email"
}) })
@@ -208,6 +209,9 @@ class EmailAccount(Document):
if frappe.db.exists("Communication", in_reply_to): if frappe.db.exists("Communication", in_reply_to):
parent = frappe.get_doc("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: if parent.reference_name:
parent = frappe.get_doc(parent.reference_doctype, parent = frappe.get_doc(parent.reference_doctype,
parent.reference_name) parent.reference_name)


+ 5
- 5
frappe/email/email_body.py Näytä tiedosto

@@ -5,7 +5,7 @@ from __future__ import unicode_literals
import frappe import frappe
from frappe.utils.pdf import get_pdf from frappe.utils.pdf import get_pdf
from frappe.email.smtp import get_outgoing_email_account 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 import email.utils
from markdown2 import markdown from markdown2 import markdown


@@ -42,7 +42,7 @@ class EMail:


if isinstance(recipients, basestring): if isinstance(recipients, basestring):
recipients = recipients.replace(';', ',').replace('\n', '') recipients = recipients.replace(';', ',').replace('\n', '')
recipients = recipients.split(',')
recipients = split_emails(recipients)


# remove null # remove null
recipients = filter(None, (strip(r) for r in recipients)) recipients = filter(None, (strip(r) for r in recipients))
@@ -238,18 +238,18 @@ def get_footer(email_account, footer=None):
footer = footer or "" footer = footer or ""


if email_account and email_account.footer: 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-->" footer += "<!--unsubscribe link here-->"


company_address = frappe.db.get_default("email_footer_address") company_address = frappe.db.get_default("email_footer_address")


if company_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>")) .format(company_address.replace("\n", "<br>"))


if not cint(frappe.db.get_default("disable_standard_email_footer")): if not cint(frappe.db.get_default("disable_standard_email_footer")):
for default_mail_footer in frappe.get_hooks("default_mail_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 return footer

+ 23
- 0
frappe/public/js/frappe/misc/user.js Näytä tiedosto

@@ -193,6 +193,29 @@ $.extend(frappe.user, {
is_report_manager: function() { is_report_manager: function() {
return frappe.user.has_role(['Administrator', 'System Manager', 'Report Manager']); 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; frappe.session_alive = true;


+ 3
- 0
frappe/public/js/frappe/misc/utils.js Näytä tiedosto

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


+ 7
- 2
frappe/public/js/frappe/ui/star.js Näytä tiedosto

@@ -2,12 +2,17 @@
// MIT License. See license.txt // MIT License. See license.txt


frappe.ui.is_starred = function(doc) { 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; var starred = doc._starred_by;
if(starred) { if(starred) {
starred = JSON.parse(starred); starred = JSON.parse(starred);
return starred.indexOf(user)===-1 ? false : true;
} }
return false;

return starred || [];
} }


frappe.ui.toggle_star = function($btn, doctype, name) { frappe.ui.toggle_star = function($btn, doctype, name) {


+ 124
- 40
frappe/public/js/frappe/views/communication.js Näytä tiedosto

@@ -14,41 +14,7 @@ frappe.views.CommunicationComposer = Class.extend({
this.dialog = new frappe.ui.Dialog({ this.dialog = new frappe.ui.Dialog({
title: __("Add Reply") + ": " + (this.subject || ""), title: __("Add Reply") + ": " + (this.subject || ""),
no_submit_on_enter: true, 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_label: "Send",
primary_action: function() { primary_action: function() {
me.send_action(); me.send_action();
@@ -79,6 +45,100 @@ frappe.views.CommunicationComposer = Class.extend({
this.dialog.show(); 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() { prepare: function() {
this.setup_subject_and_recipients(); this.setup_subject_and_recipients();
this.setup_print(); this.setup_print();
@@ -284,10 +344,10 @@ frappe.views.CommunicationComposer = Class.extend({
}, },


send_action: function() { 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; if(!form_values) return;


var selected_attachments = $.map($(me.dialog.wrapper) 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) { send_email: function(btn, form_values, selected_attachments, print_html, print_format) {
var me = this; var me = this;


@@ -334,6 +414,7 @@ frappe.views.CommunicationComposer = Class.extend({
method:"frappe.core.doctype.communication.communication.make", method:"frappe.core.doctype.communication.communication.make",
args: { args: {
recipients: form_values.recipients, recipients: form_values.recipients,
cc: form_values.cc,
subject: form_values.subject, subject: form_values.subject,
content: form_values.content, content: form_values.content,
doctype: me.doc.doctype, doctype: me.doc.doctype,
@@ -349,8 +430,11 @@ frappe.views.CommunicationComposer = Class.extend({
btn: btn, btn: btn,
callback: function(r) { callback: function(r) {
if(!r.exc) { 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(); me.dialog.hide();


if (cur_frm) { if (cur_frm) {


+ 4
- 2
frappe/tasks.py Näytä tiedosto

@@ -185,7 +185,8 @@ def run_async_task(self, site=None, user=None, cmd=None, form_dict=None, hijack_




@celery_task() @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: try:
frappe.connect(site=site) 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): for i in xrange(3):
try: try:
communication = frappe.get_doc("Communication", communication_name) 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: except MySQLdb.OperationalError, e:
# deadlock, try again # deadlock, try again
if e.args[0]==1213: if e.args[0]==1213:


+ 9
- 1
frappe/utils/__init__.py Näytä tiedosto

@@ -9,7 +9,6 @@ import os, sys, re, urllib
import frappe import frappe
import requests import requests



# utility functions like cint, int, flt, etc. # utility functions like cint, int, flt, etc.
from frappe.utils.data import * from frappe.utils.data import *


@@ -89,6 +88,15 @@ def validate_email_add(email_str, throw=False):


return matched 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): def random_string(length):
"""generate a random string""" """generate a random string"""
import string import string


Ladataan…
Peruuta
Tallenna