浏览代码

[fix] set Message-Id in email based on reference and tag replies to the same object

version-14
Rushabh Mehta 8 年前
父节点
当前提交
2fe0e2acad
共有 8 个文件被更改,包括 107 次插入75 次删除
  1. +6
    -5
      frappe/__init__.py
  2. +1
    -1
      frappe/commands/scheduler.py
  3. +0
    -1
      frappe/core/doctype/communication/email.py
  4. +2
    -5
      frappe/email/__init__.py
  5. +85
    -51
      frappe/email/doctype/email_account/email_account.py
  6. +4
    -3
      frappe/email/email_body.py
  7. +8
    -7
      frappe/email/queue.py
  8. +1
    -2
      frappe/hooks.py

+ 6
- 5
frappe/__init__.py 查看文件

@@ -359,8 +359,8 @@ def sendmail(recipients=(), sender="", subject="No Subject", message="No Message
as_markdown=False, delayed=True, reference_doctype=None, reference_name=None, as_markdown=False, delayed=True, 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=(), show_as_cc=(), message_id=None, in_reply_to=None, send_after=None, expose_recipients=False,
send_priority=1, communication=None, retry=1):
cc=(), show_as_cc=(), in_reply_to=None, send_after=None, expose_recipients=False,
send_priority=1, communication=None, retry=1, now=None):
"""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**.




@@ -377,7 +377,6 @@ def sendmail(recipients=(), sender="", subject="No Subject", message="No Message
:param unsubscribe_params: Unsubscribe paramaters to be loaded on the unsubscribe_method [optional] (dict). :param unsubscribe_params: Unsubscribe paramaters to be loaded on the unsubscribe_method [optional] (dict).
:param attachments: List of attachments. :param attachments: List of attachments.
: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 in_reply_to: Used to send the Message-Id of a received email back as In-Reply-To. :param in_reply_to: Used to send the Message-Id of a received email back as In-Reply-To.
: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" :param expose_recipients: Display all recipients in the footer message - "This email was sent to"
@@ -389,6 +388,8 @@ def sendmail(recipients=(), sender="", subject="No Subject", message="No Message
from markdown2 import markdown from markdown2 import markdown
message = markdown(message) message = markdown(message)


if now!=None:
delayed = not now


import email import email
if delayed: if delayed:
@@ -397,12 +398,12 @@ def sendmail(recipients=(), sender="", subject="No Subject", message="No Message
subject=subject, message=message, subject=subject, message=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, show_as_cc=show_as_cc, message_id=message_id, in_reply_to=in_reply_to,
attachments=attachments, reply_to=reply_to, cc=cc, show_as_cc=show_as_cc, in_reply_to=in_reply_to,
send_after=send_after, expose_recipients=expose_recipients, send_priority=send_priority, communication=communication) send_after=send_after, expose_recipients=expose_recipients, send_priority=send_priority, communication=communication)
else: else:
email.sendmail(recipients, sender=sender, email.sendmail(recipients, sender=sender,
subject=subject, msg=content or message, attachments=attachments, reply_to=reply_to, subject=subject, msg=content or message, attachments=attachments, reply_to=reply_to,
cc=cc, message_id=message_id, in_reply_to=in_reply_to, retry=retry)
cc=cc, in_reply_to=in_reply_to, retry=retry)


whitelisted = [] whitelisted = []
guest_methods = [] guest_methods = []


+ 1
- 1
frappe/commands/scheduler.py 查看文件

@@ -27,7 +27,7 @@ def trigger_scheduler_event(context, event):
try: try:
frappe.init(site=site) frappe.init(site=site)
frappe.connect() frappe.connect()
frappe.utils.scheduler.trigger(site, event, now=context.force)
frappe.utils.scheduler.trigger(site, event, now=True)
finally: finally:
frappe.destroy() frappe.destroy()




+ 0
- 1
frappe/core/doctype/communication/email.py 查看文件

@@ -136,7 +136,6 @@ def _notify(doc, print_html=None, print_format=None, attachments=None,
reference_doctype=doc.reference_doctype, reference_doctype=doc.reference_doctype,
reference_name=doc.reference_name, reference_name=doc.reference_name,
attachments=doc.attachments, attachments=doc.attachments,
message_id=doc.name,
unsubscribe_message=unsubscribe_message, unsubscribe_message=unsubscribe_message,
delayed=True, delayed=True,
communication=doc.name communication=doc.name


+ 2
- 5
frappe/email/__init__.py 查看文件

@@ -6,15 +6,12 @@ import frappe


from frappe.email.email_body import get_email from frappe.email.email_body import get_email
from frappe.email.smtp import send from frappe.email.smtp import send
from frappe.utils import markdown


def sendmail(recipients, sender='', msg='', subject='[No Subject]', attachments=None, content=None,
reply_to=None, cc=(), message_id=None, in_reply_to=None, retry=1):
def sendmail(recipients, sender='', msg='', subject='[No Subject]', attachments=None,
content=None, reply_to=None, cc=(), in_reply_to=None, retry=1):
"""send an html email as multipart with attachments and all""" """send an html email as multipart with attachments and all"""
mail = get_email(recipients, sender, content or msg, subject, attachments=attachments, mail = get_email(recipients, sender, content or msg, subject, attachments=attachments,
reply_to=reply_to, cc=cc) reply_to=reply_to, cc=cc)
if message_id:
mail.set_message_id(message_id)
if in_reply_to: if in_reply_to:
mail.set_in_reply_to(in_reply_to) mail.set_in_reply_to(in_reply_to)




+ 85
- 51
frappe/email/doctype/email_account/email_account.py 查看文件

@@ -270,46 +270,50 @@ class EmailAccount(Document):


If no thread id is found and `append_to` is set for the email account, If no thread id is found and `append_to` is set for the email account,
it will create a new parent transaction (e.g. Issue)""" it will create a new parent transaction (e.g. Issue)"""
in_reply_to = (email.mail.get("In-Reply-To") or "").strip(" <>")
parent = None parent = None


if self.append_to:
# set subject_field and sender_field
meta_module = frappe.get_meta_module(self.append_to)
meta = frappe.get_meta(self.append_to)
subject_field = getattr(meta_module, "subject_field", "subject")
if not meta.get_field(subject_field):
subject_field = None
sender_field = getattr(meta_module, "sender_field", "sender")
if not meta.get_field(sender_field):
sender_field = None
parent = self.find_parent_from_in_reply_to(email, communication)


if in_reply_to:
if "@{0}".format(frappe.local.site) in in_reply_to:
if not parent:
self.set_sender_field_and_subject_field()


# reply to a communication sent from the system
in_reply_to, domain = in_reply_to.split("@", 1)
if not parent and self.append_to:
parent = self.find_parent_based_on_subject_and_sender(email, communication)


if frappe.db.exists("Communication", in_reply_to):
parent = frappe.get_doc("Communication", in_reply_to)
if not parent and self.append_to and self.append_to!="Communication":
parent = self.create_new_parent(email, communication)


# set in_reply_to of current communication
communication.in_reply_to = in_reply_to
if parent:
communication.reference_doctype = parent.doctype
communication.reference_name = parent.name

def set_sender_field_and_subject_field(self):
'''Identify the sender and subject fields from the `append_to` DocType'''
# set subject_field and sender_field
meta_module = frappe.get_meta_module(self.append_to)
meta = frappe.get_meta(self.append_to)

self.subject_field = getattr(meta_module, "subject_field", "subject")
if not meta.get_field(self.subject_field):
self.subject_field = None


if parent.reference_name:
parent = frappe.get_doc(parent.reference_doctype,
parent.reference_name)
self.sender_field = getattr(meta_module, "sender_field", "sender")
if not meta.get_field(self.sender_field):
self.sender_field = None


if not parent and self.append_to and sender_field:
if subject_field:
def find_parent_based_on_subject_and_sender(self, email, communication):
'''Find parent document based on subject and sender match'''

if self.append_to and self.sender_field:
if self.subject_field:
# try and match by subject and sender # try and match by subject and sender
# if sent by same sender with same subject, # if sent by same sender with same subject,
# append it to old coversation # append it to old coversation
subject = strip(re.sub("^\s*(Re|RE)[^:]*:\s*", "", email.subject)) subject = strip(re.sub("^\s*(Re|RE)[^:]*:\s*", "", email.subject))


parent = frappe.db.get_all(self.append_to, filters={ parent = frappe.db.get_all(self.append_to, filters={
sender_field: email.from_email,
subject_field: ("like", "%{0}%".format(subject)),
self.sender_field: email.from_email,
self.subject_field: ("like", "%{0}%".format(subject)),
"creation": (">", (get_datetime() - relativedelta(days=10)).strftime(DATE_FORMAT)) "creation": (">", (get_datetime() - relativedelta(days=10)).strftime(DATE_FORMAT))
}, fields="name") }, fields="name")


@@ -318,42 +322,73 @@ class EmailAccount(Document):
# and subject is atleast 10 chars long # and subject is atleast 10 chars long
if not parent and len(subject) > 10 and is_system_user(email.from_email): if not parent and len(subject) > 10 and is_system_user(email.from_email):
parent = frappe.db.get_all(self.append_to, filters={ parent = frappe.db.get_all(self.append_to, filters={
subject_field: ("like", "%{0}%".format(subject)),
self.subject_field: ("like", "%{0}%".format(subject)),
"creation": (">", (get_datetime() - relativedelta(days=10)).strftime(DATE_FORMAT)) "creation": (">", (get_datetime() - relativedelta(days=10)).strftime(DATE_FORMAT))
}, fields="name") }, fields="name")


if parent: if parent:
parent = frappe.get_doc(self.append_to, parent[0].name) parent = frappe.get_doc(self.append_to, parent[0].name)
return parent


if not parent and self.append_to and self.append_to!="Communication":
# no parent found, but must be tagged
# insert parent type doc
parent = frappe.new_doc(self.append_to)


if subject_field:
parent.set(subject_field, email.subject)
def create_new_parent(self, email, communication):
'''If no parent found, create a new reference document'''


if sender_field:
parent.set(sender_field, email.from_email)
# no parent found, but must be tagged
# insert parent type doc
parent = frappe.new_doc(self.append_to)


parent.flags.ignore_mandatory = True
if self.subject_field:
parent.set(self.subject_field, email.subject)


try:
parent.insert(ignore_permissions=True)
except frappe.DuplicateEntryError:
# try and find matching parent
parent_name = frappe.db.get_value(self.append_to, {sender_field: email.from_email})
if parent_name:
parent.name = parent_name
else:
parent = None
if self.sender_field:
parent.set(self.sender_field, email.from_email)


# NOTE if parent isn't found and there's no subject match, it is likely that it is a new conversation thread and hence is_first = True
communication.is_first = True
parent.flags.ignore_mandatory = True


if parent:
communication.reference_doctype = parent.doctype
communication.reference_name = parent.name
try:
parent.insert(ignore_permissions=True)
except frappe.DuplicateEntryError:
# try and find matching parent
parent_name = frappe.db.get_value(self.append_to, {self.sender_field: email.from_email})
if parent_name:
parent.name = parent_name
else:
parent = None

# NOTE if parent isn't found and there's no subject match, it is likely that it is a new conversation thread and hence is_first = True
communication.is_first = True

return parent

def find_parent_from_in_reply_to(self, communication, email):
'''Returns parent reference if embedded in In-Reply-To header

Message-ID is formatted as `{random}.{doctype}.{name}@{site}`'''
parent = None
in_reply_to = (email.mail.get("In-Reply-To") or "").strip(" <>")

if in_reply_to and "@{0}".format(frappe.local.site) in in_reply_to:

# reply to a communication sent from the system
reference, domain = in_reply_to.split("@", 1)
if '.' in reference:
t, parent_doctype, parent_name = reference.split('.', 2)
else:
parent_doctype, parent_name = 'Communication', reference

if frappe.db.exists(parent_doctype, parent_name):
parent = frappe.get_doc(parent_doctype, parent_name)

# set in_reply_to of current communication
if parent_doctype=='Communication':
communication.in_reply_to = in_reply_to

if parent.reference_name:
parent = frappe.get_doc(parent.reference_doctype,
parent.reference_name)

return parent


def send_auto_reply(self, communication, email): def send_auto_reply(self, communication, email):
"""Send auto reply if set.""" """Send auto reply if set."""
@@ -373,7 +408,6 @@ class EmailAccount(Document):
frappe.get_template("templates/emails/auto_reply.html").render(communication.as_dict()), frappe.get_template("templates/emails/auto_reply.html").render(communication.as_dict()),
reference_doctype = communication.reference_doctype, reference_doctype = communication.reference_doctype,
reference_name = communication.reference_name, reference_name = communication.reference_name,
message_id = communication.name,
in_reply_to = email.mail.get("Message-Id"), # send back the Message-Id as In-Reply-To in_reply_to = email.mail.get("Message-Id"), # send back the Message-Id as In-Reply-To
unsubscribe_message = unsubscribe_message) unsubscribe_message = unsubscribe_message)




+ 4
- 3
frappe/email/email_body.py 查看文件

@@ -6,7 +6,7 @@ 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, to_markdown, markdown, encode)
split_emails, to_markdown, markdown, encode, random_string)
import email.utils import email.utils


def get_email(recipients, sender='', msg='', subject='[No Subject]', def get_email(recipients, sender='', msg='', subject='[No Subject]',
@@ -182,8 +182,9 @@ class EMail:
sender_name, sender_email = email.utils.parseaddr(self.sender) sender_name, sender_email = email.utils.parseaddr(self.sender)
self.sender = email.utils.formataddr((sender_name or self.email_account.name, self.email_account.email_id)) self.sender = email.utils.formataddr((sender_name or self.email_account.name, self.email_account.email_id))


def set_message_id(self, message_id):
self.msg_root["Message-Id"] = "<{0}@{1}>".format(message_id, frappe.local.site)
def set_message_id(self, doctype, name):
self.msg_root["Message-Id"] = "<{random}.{doctype}.{name}@{site}>".format(
doctype=doctype, name=name, site=frappe.local.site, random=random_string(10))


def set_in_reply_to(self, in_reply_to): def set_in_reply_to(self, in_reply_to):
"""Used to send the Message-Id of a received email back as In-Reply-To""" """Used to send the Message-Id of a received email back as In-Reply-To"""


+ 8
- 7
frappe/email/queue.py 查看文件

@@ -18,7 +18,7 @@ class EmailLimitCrossedError(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=(), show_as_cc=(), message_id=None, in_reply_to=None, send_after=None,
attachments=None, reply_to=None, cc=(), show_as_cc=(), in_reply_to=None, send_after=None,
expose_recipients=False, send_priority=1, communication=None): expose_recipients=False, send_priority=1, communication=None):
"""Add email to sending queue (Email Queue) """Add email to sending queue (Email Queue)


@@ -33,7 +33,6 @@ def send(recipients=None, sender=None, subject=None, message=None, reference_doc
:param unsubscribe_params: additional params for unsubscribed links. default are name, doctype, email :param unsubscribe_params: additional params for unsubscribed links. default are name, doctype, email
:param attachments: Attachments to be sent. :param attachments: Attachments to be sent.
:param reply_to: Reply to be captured here (default inbox) :param reply_to: Reply to be captured here (default inbox)
: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 in_reply_to: Used to send the Message-Id of a received email back as In-Reply-To. :param in_reply_to: Used to send the Message-Id of a received email back as In-Reply-To.
:param send_after: Send this email after the given datetime. If value is in integer, then `send_after` will be the automatically set to no of days from current date. :param send_after: Send this email after the given datetime. If value is in integer, then `send_after` will be the automatically set to no of days from current date.
:param communication: Communication link to be set in Email Queue record :param communication: Communication link to be set in Email Queue record
@@ -103,11 +102,12 @@ def send(recipients=None, sender=None, subject=None, message=None, reference_doc
email_text_context = cc_message + "\n" + email_text_context email_text_context = cc_message + "\n" + email_text_context
# add to queue # add to queue
add(email, sender, subject, email_content, email_text_context, reference_doctype, add(email, sender, subject, email_content, email_text_context, reference_doctype,
reference_name, attachments, reply_to, cc, message_id, in_reply_to, send_after, send_priority, email_account=email_account, communication=communication)
reference_name, attachments, reply_to, cc, in_reply_to, send_after, send_priority, email_account=email_account, communication=communication)


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,
cc=(), message_id=None, in_reply_to=None, send_after=None, send_priority=1, email_account=None, communication=None):
cc=(), in_reply_to=None, send_after=None, send_priority=1, email_account=None,
communication=None):
"""Add to Email Queue""" """Add to Email Queue"""
e = frappe.new_doc('Email Queue') e = frappe.new_doc('Email Queue')
e.recipient = email e.recipient = email
@@ -115,10 +115,11 @@ def add(email, sender, subject, formatted, text_content=None,


try: try:
mail = get_email(email, sender=sender, formatted=formatted, subject=subject, mail = get_email(email, sender=sender, formatted=formatted, subject=subject,
text_content=text_content, attachments=attachments, reply_to=reply_to, cc=cc, email_account=email_account)
text_content=text_content, attachments=attachments, reply_to=reply_to,
cc=cc, email_account=email_account)


if message_id:
mail.set_message_id(message_id)
if reference_doctype and reference_name:
mail.set_message_id(reference_doctype, reference_name)


if in_reply_to: if in_reply_to:
mail.set_in_reply_to(in_reply_to) mail.set_in_reply_to(in_reply_to)


+ 1
- 2
frappe/hooks.py 查看文件

@@ -121,8 +121,7 @@ scheduler_events = {
"hourly": [ "hourly": [
"frappe.model.utils.link_count.update_link_count", "frappe.model.utils.link_count.update_link_count",
'frappe.model.utils.list_settings.sync_list_settings', 'frappe.model.utils.list_settings.sync_list_settings',
"frappe.utils.error.collect_error_snapshots",
"frappe.integration_broker.doctype.integration_service.integration_service.trigger_integration_service_events"
"frappe.utils.error.collect_error_snapshots"
], ],
"daily": [ "daily": [
"frappe.email.queue.clear_outbox", "frappe.email.queue.clear_outbox",


正在加载...
取消
保存