Parcourir la source

[email] threading based on message_id in Email Queue and test case

version-14
Rushabh Mehta il y a 8 ans
Parent
révision
43c6c47513
8 fichiers modifiés avec 183 ajouts et 37 suppressions
  1. +11
    -16
      frappe/__init__.py
  2. +4
    -1
      frappe/desk/doctype/event/event.py
  3. +5
    -8
      frappe/email/doctype/email_account/email_account.py
  4. +25
    -0
      frappe/email/doctype/email_account/test_email_account.py
  5. +75
    -0
      frappe/email/doctype/email_account/test_mails/reply-4.raw
  6. +40
    -1
      frappe/email/doctype/email_queue/email_queue.json
  7. +9
    -4
      frappe/email/email_body.py
  8. +14
    -7
      frappe/email/queue.py

+ 11
- 16
frappe/__init__.py Voir le fichier

@@ -388,22 +388,17 @@ 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
if delayed:
import email.queue
email.queue.send(recipients=recipients, sender=sender,
subject=subject, message=message,
reference_doctype = doctype or reference_doctype, reference_name = name or reference_name,
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, in_reply_to=in_reply_to,
send_after=send_after, expose_recipients=expose_recipients, send_priority=send_priority, communication=communication)
else:
email.sendmail(recipients, sender=sender,
subject=subject, msg=content or message, attachments=attachments, reply_to=reply_to,
cc=cc, in_reply_to=in_reply_to, retry=retry)
if not delayed:
now = True

import email.queue
email.queue.send(recipients=recipients, sender=sender,
subject=subject, message=message,
reference_doctype = doctype or reference_doctype, reference_name = name or reference_name,
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, in_reply_to=in_reply_to,
send_after=send_after, expose_recipients=expose_recipients, send_priority=send_priority,
communication=communication, now=now)


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


+ 4
- 1
frappe/desk/doctype/event/event.py Voir le fichier

@@ -5,7 +5,7 @@ from __future__ import unicode_literals
import frappe import frappe


from frappe.utils import (getdate, cint, add_months, date_diff, add_days, from frappe.utils import (getdate, cint, add_months, date_diff, add_days,
nowdate, get_datetime_str, cstr, get_datetime)
nowdate, get_datetime_str, cstr, get_datetime, now_datetime)
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils.user import get_enabled_system_users from frappe.utils.user import get_enabled_system_users


@@ -13,6 +13,9 @@ weekdays = ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday",


class Event(Document): class Event(Document):
def validate(self): def validate(self):
if not self.starts_on:
self.starts_on = now_datetime()

if self.starts_on and self.ends_on and get_datetime(self.starts_on) > get_datetime(self.ends_on): if self.starts_on and self.ends_on and get_datetime(self.starts_on) > get_datetime(self.ends_on):
frappe.msgprint(frappe._("Event end must be after start"), raise_exception=True) frappe.msgprint(frappe._("Event end must be after start"), raise_exception=True)




+ 5
- 8
frappe/email/doctype/email_account/email_account.py Voir le fichier

@@ -365,20 +365,17 @@ class EmailAccount(Document):
def find_parent_from_in_reply_to(self, communication, email): def find_parent_from_in_reply_to(self, communication, email):
'''Returns parent reference if embedded in In-Reply-To header '''Returns parent reference if embedded in In-Reply-To header


Message-ID is formatted as `{random}.{doctype}.{name}@{site}`'''
Message-ID is formatted as `{message_id}@{site}`'''
parent = None parent = None
in_reply_to = (email.mail.get("In-Reply-To") or "").strip(" <>") 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: if in_reply_to and "@{0}".format(frappe.local.site) in in_reply_to:

# reply to a communication sent from the system # 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)

# parent doctype has '-' instead of ' ' and '--' instead of '-'
parent_doctype = parent_doctype.replace('--', '%').replace('-', ' ').replace('%', '-')
email_queue = frappe.db.get_value('Email Queue', dict(message_id=in_reply_to), ['reference_doctype', 'reference_name'])
if email_queue:
parent_doctype, parent_name = email_queue
else: else:
reference, domain = in_reply_to.split("@", 1)
parent_doctype, parent_name = 'Communication', reference parent_doctype, parent_name = 'Communication', reference


if frappe.db.exists(parent_doctype, parent_name): if frappe.db.exists(parent_doctype, parent_name):


+ 25
- 0
frappe/email/doctype/email_account/test_email_account.py Voir le fichier

@@ -170,5 +170,30 @@ class TestEmailAccount(unittest.TestCase):
self.assertEquals(comm_list[0].reference_doctype, comm_list[1].reference_doctype) self.assertEquals(comm_list[0].reference_doctype, comm_list[1].reference_doctype)
self.assertEquals(comm_list[0].reference_name, comm_list[1].reference_name) self.assertEquals(comm_list[0].reference_name, comm_list[1].reference_name)


def test_threading_by_message_id(self):
frappe.db.sql("""delete from tabCommunication""")
frappe.db.sql("""delete from `tabEmail Queue`""")


# reference document for testing
event = frappe.get_doc(dict(doctype='Event', subject='test-message')).insert()


# send a mail against this
frappe.sendmail(recipients='test@example.com', subject='test message for threading',
message='testing', reference_doctype=event.doctype, reference_name=event.name)

last_mail = frappe.get_doc('Email Queue', dict(reference_name=event.name))

# get test mail with message-id as in-reply-to
with open(os.path.join(os.path.dirname(__file__), "test_mails", "reply-4.raw"), "r") as f:
test_mails = [f.read().replace('{{ message_id }}', last_mail.message_id)]

# pull the mail
email_account = frappe.get_doc("Email Account", "_Test Email Account 1")
email_account.receive(test_mails=test_mails)

comm_list = frappe.get_all("Communication", filters={"sender":"test_sender@example.com"},
fields=["name", "reference_doctype", "reference_name"])

# check if threaded correctly
self.assertEquals(comm_list[0].reference_doctype, event.doctype)
self.assertEquals(comm_list[0].reference_name, event.name)

+ 75
- 0
frappe/email/doctype/email_account/test_mails/reply-4.raw Voir le fichier

@@ -0,0 +1,75 @@
From: <test_sender@example.com>
Content-Type: multipart/alternative;
boundary="Apple-Mail=_29597CF7-20DD-4184-B3FA-85582C5C4361"
Message-Id: <07D687F6-10AA-4B9F-82DE-27753096164E@gmail.com>
Mime-Version: 1.0 (Mac OS X Mail 9.3 \(3124\))
X-Smtp-Server: 73CC8281-7E8F-4B47-8324-D5DA86EEDD4F
Subject: Re: What did you work on today?
Date: Thu, 10 Nov 2016 16:04:43 +0530
X-Universally-Unique-Identifier: A4D9669F-179C-42D8-A3D3-AA6A8C49A6F2
References: {{ message_id }}
To: test_in@iwebnotes.com
In-Reply-To: {{ message_id }}


--Apple-Mail=_29597CF7-20DD-4184-B3FA-85582C5C4361
Content-Transfer-Encoding: quoted-printable
Content-Type: text/plain;
charset=us-ascii

Testing another reply!

> On 10-Nov-2016, at 3:20 PM, Frappe <test@erpnext.com> wrote:
>=20
> Please share what did you do today. If you reply by midnight, your =
response will be recorded!
>=20
> This email was sent to rmehta@gmail.com
> Unsubscribe from this list =
<http://demo-test.erpnext.com.dev/api/method/frappe.email.queue.unsubscrib=
e?email=3Drmehta%40gmail.com&name=3D26cc3e5a5d&doctype=3DDaily+Work+Summar=
y&_signature=3D2c7ab37e6d775e5a481e9b4376154a41>
> Sent via ERPNext <https://erpnext.com/?source=3Dvia_email_footer>


--Apple-Mail=_29597CF7-20DD-4184-B3FA-85582C5C4361
Content-Transfer-Encoding: 7bit
Content-Type: text/html;
charset=us-ascii

<html><head><meta http-equiv="Content-Type" content="text/html charset=us-ascii"></head><body style="word-wrap: break-word; -webkit-nbsp-mode: space; -webkit-line-break: after-white-space;" class="">Testing another reply!<div class=""><br class=""><div><blockquote type="cite" class=""><div class="">On 10-Nov-2016, at 3:20 PM, Frappe &lt;<a href="mailto:test@erpnext.com" class="">test@erpnext.com</a>&gt; wrote:</div><br class="Apple-interchange-newline"><div class="">


<meta name="viewport" content="width=device-width" class="">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" class="">
<title class="">What did you work on today?</title>

<div style="line-height: 1.5; color: #36414C;" class="">
<!-- body -->
<div style="font-family: -apple-system, BlinkMacSystemFont,
" segoe="" ui",="" "roboto",="" "oxygen",="" "ubuntu",="" "cantarell",="" "fira="" sans",="" "droid="" "helvetica="" neue",="" sans-serif;="" font-size:="" 14px;="" padding:="" 10px;"="" class=""><p class="">Please share what did you do today. If you reply by midnight, your response will be recorded!</p>

</div>

<!-- footer -->
<div style="margin-top: 30px; font-family: Helvetica, Arial, sans-serif; font-size: 11px;
margin-bottom: 15px; border-top: 1px solid #d1d8dd;" data-email-footer="true" class="">
<div style="margin: 15px auto; padding: 0px 7px; text-align: center; color: #8d99a6;" class="">
This email was sent to <a href="mailto:rmehta@gmail.com" class="">rmehta@gmail.com</a>
<p style="margin: 15px auto;" class="">
<a href="http://demo-test.erpnext.com.dev/api/method/frappe.email.queue.unsubscribe?email=rmehta%40gmail.com&amp;name=26cc3e5a5d&amp;doctype=Daily+Work+Summary&amp;_signature=2c7ab37e6d775e5a481e9b4376154a41" style="color: #8d99a6; text-decoration: underline;
target=" _blank"="" class="">Unsubscribe from this list
</a>
</p>
</div><div style="margin: 15px auto;" class=""><div style="text-align: center;" class="">
<a href="https://erpnext.com/?source=via_email_footer" target="_blank" style="color: #8d99a6;" class="">
Sent via ERPNext
</a>
</div></div>
</div>
<!-- /footer -->

<div class="print-html"></div>
</div>
</div></blockquote></div><br class=""></div></body></html>
--Apple-Mail=_29597CF7-20DD-4184-B3FA-85582C5C4361--

+ 40
- 1
frappe/email/doctype/email_queue/email_queue.json Voir le fichier

@@ -24,6 +24,7 @@
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"ignore_xss_filter": 0, "ignore_xss_filter": 0,
"in_filter": 0, "in_filter": 0,
"in_global_search": 0,
"in_list_view": 0, "in_list_view": 0,
"in_standard_filter": 0, "in_standard_filter": 0,
"label": "Sender", "label": "Sender",
@@ -52,6 +53,7 @@
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"ignore_xss_filter": 0, "ignore_xss_filter": 0,
"in_filter": 0, "in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0, "in_standard_filter": 0,
"label": "Recipient", "label": "Recipient",
@@ -80,6 +82,7 @@
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"ignore_xss_filter": 0, "ignore_xss_filter": 0,
"in_filter": 0, "in_filter": 0,
"in_global_search": 0,
"in_list_view": 0, "in_list_view": 0,
"in_standard_filter": 0, "in_standard_filter": 0,
"label": "Message", "label": "Message",
@@ -108,6 +111,7 @@
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"ignore_xss_filter": 0, "ignore_xss_filter": 0,
"in_filter": 0, "in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Status", "label": "Status",
@@ -136,6 +140,7 @@
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"ignore_xss_filter": 0, "ignore_xss_filter": 0,
"in_filter": 0, "in_filter": 0,
"in_global_search": 0,
"in_list_view": 0, "in_list_view": 0,
"in_standard_filter": 0, "in_standard_filter": 0,
"label": "Error", "label": "Error",
@@ -152,6 +157,35 @@
"set_only_once": 0, "set_only_once": 0,
"unique": 0 "unique": 0
}, },
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "message_id",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Message ID",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 1,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
@@ -163,6 +197,7 @@
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"ignore_xss_filter": 0, "ignore_xss_filter": 0,
"in_filter": 0, "in_filter": 0,
"in_global_search": 0,
"in_list_view": 0, "in_list_view": 0,
"in_standard_filter": 0, "in_standard_filter": 0,
"label": "Reference DocType", "label": "Reference DocType",
@@ -191,6 +226,7 @@
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"ignore_xss_filter": 0, "ignore_xss_filter": 0,
"in_filter": 0, "in_filter": 0,
"in_global_search": 0,
"in_list_view": 0, "in_list_view": 0,
"in_standard_filter": 0, "in_standard_filter": 0,
"label": "Reference DocName", "label": "Reference DocName",
@@ -218,6 +254,7 @@
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"ignore_xss_filter": 0, "ignore_xss_filter": 0,
"in_filter": 0, "in_filter": 0,
"in_global_search": 0,
"in_list_view": 0, "in_list_view": 0,
"in_standard_filter": 0, "in_standard_filter": 0,
"label": "Communication", "label": "Communication",
@@ -247,6 +284,7 @@
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"ignore_xss_filter": 0, "ignore_xss_filter": 0,
"in_filter": 0, "in_filter": 0,
"in_global_search": 0,
"in_list_view": 0, "in_list_view": 0,
"in_standard_filter": 0, "in_standard_filter": 0,
"label": "Send After", "label": "Send After",
@@ -276,6 +314,7 @@
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"ignore_xss_filter": 0, "ignore_xss_filter": 0,
"in_filter": 0, "in_filter": 0,
"in_global_search": 0,
"in_list_view": 0, "in_list_view": 0,
"in_standard_filter": 0, "in_standard_filter": 0,
"label": "Priority", "label": "Priority",
@@ -305,7 +344,7 @@
"issingle": 0, "issingle": 0,
"istable": 0, "istable": 0,
"max_attachments": 0, "max_attachments": 0,
"modified": "2016-11-07 05:29:46.316408",
"modified": "2016-11-17 16:45:02.296617",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Email", "module": "Email",
"name": "Email Queue", "name": "Email Queue",


+ 9
- 4
frappe/email/email_body.py Voir le fichier

@@ -59,6 +59,7 @@ class EMail:
self.html_set = False self.html_set = False


self.email_account = email_account or get_outgoing_email_account() self.email_account = email_account or get_outgoing_email_account()
self.set_message_id()


def set_html(self, message, text_content = None, footer=None, print_html=None, formatted=None): def set_html(self, message, text_content = None, footer=None, print_html=None, formatted=None):
"""Attach message in the html portion of multipart/alternative""" """Attach message in the html portion of multipart/alternative"""
@@ -182,10 +183,8 @@ 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, doctype, name):
message_id = "<{random}.{doctype}.{name}@{site}>".format(
doctype=doctype.replace('-', '--').replace(' ', '-'), name=name, site=frappe.local.site, random=random_string(10))
self.msg_root["Message-Id"] = message_id
def set_message_id(self):
self.msg_root["Message-Id"] = get_message_id()


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"""
@@ -241,6 +240,12 @@ def get_formatted_html(subject, message, footer=None, print_html=None, email_acc


return scrub_urls(rendered_email) return scrub_urls(rendered_email)


def get_message_id():
'''Returns Message ID created from doctype and name'''
return "<{unique}@{site}>".format(
site=frappe.local.site,
unique=email.utils.make_msgid(random_string(10)).split('@')[0].split('<')[1])

def get_signature(email_account): def get_signature(email_account):
if email_account and email_account.add_signature and email_account.signature: if email_account and email_account.add_signature and email_account.signature:
return "<br><br>" + email_account.signature return "<br><br>" + email_account.signature


+ 14
- 7
frappe/email/queue.py Voir le fichier

@@ -19,7 +19,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=(), 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, now=False):
"""Add email to sending queue (Email Queue) """Add email to sending queue (Email Queue)


:param recipients: List of recipients. :param recipients: List of recipients.
@@ -36,6 +36,7 @@ def send(recipients=None, sender=None, subject=None, message=None, reference_doc
: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
:param now: Send immediately (don't send in the background)
""" """
if not unsubscribe_method: if not unsubscribe_method:
unsubscribe_method = "/api/method/frappe.email.queue.unsubscribe" unsubscribe_method = "/api/method/frappe.email.queue.unsubscribe"
@@ -101,9 +102,13 @@ def send(recipients=None, sender=None, subject=None, message=None, reference_doc
email_content = email_content.replace("<!-- cc message -->", cc_message) email_content = email_content.replace("<!-- cc message -->", cc_message)
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,
email_queue = add(email, sender, subject, email_content, email_text_context, reference_doctype,
reference_name, attachments, reply_to, cc, 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)


if now:
send_one(email_queue.name, now=True)


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=(), in_reply_to=None, send_after=None, send_priority=1, email_account=None, cc=(), in_reply_to=None, send_after=None, send_priority=1, email_account=None,
@@ -118,12 +123,10 @@ def add(email, sender, subject, formatted, text_content=None,
text_content=text_content, attachments=attachments, reply_to=reply_to, text_content=text_content, attachments=attachments, reply_to=reply_to,
cc=cc, email_account=email_account) cc=cc, email_account=email_account)


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)


e.message_id = mail.msg_root["Message-Id"].strip(" <>")
e.message = cstr(mail.as_string()) e.message = cstr(mail.as_string())
e.sender = mail.sender e.sender = mail.sender


@@ -137,6 +140,8 @@ def add(email, sender, subject, formatted, text_content=None,
e.send_after = send_after e.send_after = send_after
e.db_insert() e.db_insert()


return e

def check_email_limit(recipients): def check_email_limit(recipients):
# if using settings from site_config.json, check email limit # if using settings from site_config.json, check email limit
# No limit for own email settings # No limit for own email settings
@@ -347,8 +352,10 @@ def send_one(email, smtpserver=None, auto_commit=True, now=False):
log('frappe.email.queue.flush', unicode(e)) log('frappe.email.queue.flush', unicode(e))


def clear_outbox(): def clear_outbox():
"""Remove mails older than 31 days in Outbox. Called daily via scheduler."""
frappe.db.sql("""delete from `tabEmail Queue` where
"""Remove low priority older than 31 days in Outbox and expire mails not sent for 7 days.

Called daily via scheduler."""
frappe.db.sql("""delete from `tabEmail Queue` where priority=0
datediff(now(), modified) > 31""") datediff(now(), modified) > 31""")


frappe.db.sql("""update `tabEmail Queue` set status='Expired' frappe.db.sql("""update `tabEmail Queue` set status='Expired'


Chargement…
Annuler
Enregistrer