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:
parent
f9cd91cca2
commit
646140cf40
7 changed files with 254 additions and 1364 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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 = ""
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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.')
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue