feat(Communication): Automatic Email Linking in Document (#7601)

Attach an email to a document's timeline by parsing the email of the format `test_email+ToDo+abc1234@example.com`
This commit is contained in:
Himanshu 2019-06-17 11:43:49 +05:30 committed by Faris Ansari
parent f9cd91cca2
commit 646140cf40
7 changed files with 254 additions and 1364 deletions

View file

@ -13,6 +13,7 @@ from frappe.utils.bot import BotReply
from frappe.utils import parse_addr
from frappe.core.doctype.comment.comment import update_comment_in_doc
from email.utils import parseaddr
from six.moves.urllib.parse import unquote
from collections import Counter
exclude_from_linked_with = True
@ -60,6 +61,7 @@ class Communication(Document):
validate_email(self)
if self.communication_medium == "Email":
self.parse_email_for_timeline_links()
self.set_timeline_links()
self.deduplicate_timeline_links()
@ -235,6 +237,9 @@ class Communication(Document):
if commit:
frappe.db.commit()
def parse_email_for_timeline_links(self):
parse_email(self, [self.recipients, self.cc, self.bcc])
# Timeline Links
def set_timeline_links(self):
contacts = get_contacts([self.sender, self.recipients, self.cc, self.bcc])
@ -327,6 +332,7 @@ def get_contacts(email_strings):
contacts = []
for email in email_addrs:
email = get_email_without_link(email)
contact_name = frappe.db.get_value('Contact', {'email_id': email})
if not contact_name:
@ -349,4 +355,35 @@ def add_contact_links_to_communication(communication, contact_name):
if contact_links:
for contact_link in contact_links:
communication.add_link(contact_link.link_doctype, contact_link.link_name)
communication.add_link(contact_link.link_doctype, contact_link.link_name)
def parse_email(communication, email_strings):
"""
Parse email to add timeline links.
When automatic email linking is enabled, an email from email_strings can contain
a doctype and docname ie in the format `admin+doctype+docname@example.com`,
the email is parsed and doctype and docname is extracted and timeline link is added.
"""
delimiter = "+"
for email_string in email_strings:
if email_string:
for email in email_string.split(","):
if delimiter in email:
email = email.split("@")[0]
doctype = unquote(email.split(delimiter)[1])
docname = unquote(email.split(delimiter)[2])
if doctype and docname and frappe.db.exists(doctype, docname):
communication.add_link(doctype, docname)
def get_email_without_link(email):
"""
returns email address without doctype links
returns admin@example.com for email admin+doctype+docname@example.com
"""
email_id = email.split("@")[0].split("+")[0]
email_host = email.split("@")[1]
return "{0}@{1}".format(email_id, email_host)

View file

@ -4,6 +4,7 @@ from __future__ import unicode_literals
import frappe
import unittest
from six.moves.urllib.parse import quote
test_records = frappe.get_test_records('Communication')
@ -170,4 +171,27 @@ class TestCommunication(unittest.TestCase):
data.append(comm.name)
self.assertIn(comm_note_1.name, data)
self.assertIn(comm_note_2.name, data)
self.assertIn(comm_note_2.name, data)
def test_link_in_email(self):
frappe.delete_doc_if_exists("Note", "test document link in email")
note = frappe.get_doc({
"doctype": "Note",
"title": "test document link in email",
"content": "test document link in email"
}).insert(ignore_permissions=True)
comm = frappe.get_doc({
"doctype": "Communication",
"communication_medium": "Email",
"subject": "Document Link in Email",
"sender": "comm_sender@example.com",
"recipients": "comm_recipient+{0}+{1}@example.com".format(quote("Note"), quote(note.name)),
}).insert(ignore_permissions=True)
doc_links = []
for timeline_link in comm.timeline_links:
doc_links.append((timeline_link.link_doctype, timeline_link.link_name))
self.assertIn(("Note", note.name), doc_links)

File diff suppressed because it is too large Load diff

View file

@ -90,6 +90,7 @@ class EmailAccount(Document):
"""Check there is only one default of each type."""
from frappe.core.doctype.user.user import setup_user_email_inbox
self.check_automatic_linking_email_account()
self.there_must_be_only_one_default()
setup_user_email_inbox(email_account=self.name, awaiting_password=self.awaiting_password,
email_id=self.email_id, enable_outgoing=self.enable_outgoing)
@ -639,6 +640,14 @@ class EmailAccount(Document):
frappe.db.sql(""" update `tabCommunication` set seen={seen}
where name in ({docnames})""".format(docnames=docnames, seen=seen))
def check_automatic_linking_email_account(self):
if self.enable_automatic_linking:
if not self.enable_incoming:
frappe.throw(_("Automatic Linking can be activated only if Incoming is enabled."))
if frappe.db.exists("Email Account", {"enable_automatic_linking": 1, "name": ('!=', self.name)}):
frappe.throw(_("Automatic Linking can be activated only for one Email Account."))
@frappe.whitelist()
def get_append_to(doctype=None, txt=None, searchfield=None, start=None, page_len=None, filters=None):
if not txt: txt = ""

View file

@ -2,6 +2,7 @@
// MIT License. See license.txt
frappe.provide('frappe.timeline');
frappe.provide('frappe.email');
frappe.separator_element = '<div>---</div>';
frappe.ui.form.Timeline = class Timeline {
@ -12,10 +13,11 @@ frappe.ui.form.Timeline = class Timeline {
make() {
var me = this;
this.wrapper = $(frappe.render_template("timeline",
{doctype: me.frm.doctype, allow_events_in_timeline: me.frm.meta.allow_events_in_timeline})).appendTo(me.parent);
this.wrapper = $(frappe.render_template("timeline",{doctype: me.frm.doctype,allow_events_in_timeline: me.frm.meta.allow_events_in_timeline})).appendTo(me.parent);
this.set_automatic_link_email();
this.list = this.wrapper.find(".timeline-items");
this.email_link = this.wrapper.find(".timeline-email-import");
this.comment_area = frappe.ui.form.make_control({
parent: this.wrapper.find('.timeline-head'),
@ -72,6 +74,10 @@ frappe.ui.form.Timeline = class Timeline {
});
});
this.email_link.on("click", ".copy-to-clipboard", function() {
let text = $(".copy-to-clipboard").text();
frappe.utils.copy_to_clipboard(text);
});
}
setup_email_button() {
@ -109,6 +115,34 @@ frappe.ui.form.Timeline = class Timeline {
});
}
set_automatic_link_email() {
if (!frappe.email.automatic_link_email){
frappe.db.get_value("Email Account", {"enable_incoming": 1, "enable_automatic_linking": 1}, "email_id", (r) => {
if (r && r.email_id) {
frappe.email.automatic_link_email = r.email_id;
} else {
frappe.email.automatic_link_email = null;
}
this.display_automatic_link_email();
});
} else {
this.display_automatic_link_email();
}
}
display_automatic_link_email() {
var me = this;
if (frappe.email.automatic_link_email){
let email_id = frappe.email.automatic_link_email;
email_id = email_id.split("@")[0] +"+"+ encodeURIComponent(me.frm.doctype) +"+"+ encodeURIComponent(me.frm.docname)
+"@"+ email_id.split("@")[1];
$(".timeline-email-import-link").text(email_id);
} else {
$('.timeline-email-import').addClass("hide");
}
}
setup_interaction_button() {
var me = this;
var selector = ".btn-new-interaction";

View file

@ -17,7 +17,12 @@
{% } %}
{% } %}
</div>
<div class="timeline-items">
<div class="timeline-new-email timeline-email-import text-muted small">
<p>
{%= __("Send an email to {0} to link it here.", [`<b><a class="timeline-email-import-link copy-to-clipboard"></a></b>`]) %}
</p>
</div>
<div class="timeline-items">
</div>
</div>
</div>

View file

@ -703,6 +703,19 @@ Object.assign(frappe.utils, {
let parts = dataURI.split(',');
const encoded_data = parts[1];
return decodeURIComponent(escape(atob(encoded_data)));
},
copy_to_clipboard(string) {
let input = $("<input>");
$("body").append(input);
input.val(string).select();
document.execCommand("copy");
input.remove();
frappe.show_alert({
indicator: 'green',
message: __('Copied to clipboard.')
});
}
});