@@ -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: | ||||
@@ -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", | ||||
@@ -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() | ||||
@@ -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"), | ||||
@@ -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,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 |
@@ -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; | ||||
@@ -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://' | ||||
@@ -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) { | ||||
@@ -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) { | ||||
@@ -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,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 | ||||