refactor: build Email queue from send mail requestversion-14
@@ -528,16 +528,20 @@ def sendmail(recipients=[], sender="", subject="No Subject", message="No Message | |||||
if not delayed: | if not delayed: | ||||
now = True | now = True | ||||
from frappe.email import queue | |||||
queue.send(recipients=recipients, sender=sender, | |||||
from frappe.email.doctype.email_queue.email_queue import QueueBuilder | |||||
builder = QueueBuilder(recipients=recipients, sender=sender, | |||||
subject=subject, message=message, text_content=text_content, | subject=subject, message=message, text_content=text_content, | ||||
reference_doctype = doctype or reference_doctype, reference_name = name or reference_name, add_unsubscribe_link=add_unsubscribe_link, | reference_doctype = doctype or reference_doctype, reference_name = name or reference_name, add_unsubscribe_link=add_unsubscribe_link, | ||||
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, bcc=bcc, message_id=message_id, in_reply_to=in_reply_to, | attachments=attachments, reply_to=reply_to, cc=cc, bcc=bcc, message_id=message_id, in_reply_to=in_reply_to, | ||||
send_after=send_after, expose_recipients=expose_recipients, send_priority=send_priority, queue_separately=queue_separately, | send_after=send_after, expose_recipients=expose_recipients, send_priority=send_priority, queue_separately=queue_separately, | ||||
communication=communication, now=now, read_receipt=read_receipt, is_notification=is_notification, | |||||
communication=communication, read_receipt=read_receipt, is_notification=is_notification, | |||||
inline_images=inline_images, header=header, print_letterhead=print_letterhead, with_container=with_container) | inline_images=inline_images, header=header, print_letterhead=print_letterhead, with_container=with_container) | ||||
# build email queue and send the email if send_now is True. | |||||
builder.process(send_now=now) | |||||
whitelisted = [] | whitelisted = [] | ||||
guest_methods = [] | guest_methods = [] | ||||
xss_safe_methods = [] | xss_safe_methods = [] | ||||
@@ -9,14 +9,18 @@ from rq.timeouts import JobTimeoutException | |||||
import smtplib | import smtplib | ||||
import quopri | import quopri | ||||
from email.parser import Parser | from email.parser import Parser | ||||
from email.policy import SMTPUTF8 | |||||
from html2text import html2text | |||||
from six.moves import html_parser as HTMLParser | |||||
import frappe | import frappe | ||||
from frappe import _, safe_encode, task | from frappe import _, safe_encode, task | ||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
from frappe.email.queue import get_unsubcribed_url | |||||
from frappe.email.email_body import add_attachment | |||||
from frappe.utils import cint | |||||
from email.policy import SMTPUTF8 | |||||
from frappe.email.queue import get_unsubcribed_url, get_unsubscribe_message | |||||
from frappe.email.email_body import add_attachment, get_formatted_html, get_email | |||||
from frappe.utils import cint, split_emails, add_days, nowdate, cstr | |||||
from frappe.email.doctype.email_account.email_account import EmailAccount | |||||
MAX_RETRY_COUNT = 3 | MAX_RETRY_COUNT = 3 | ||||
class EmailQueue(Document): | class EmailQueue(Document): | ||||
@@ -41,6 +45,19 @@ class EmailQueue(Document): | |||||
duplicate.set_recipients(recipients) | duplicate.set_recipients(recipients) | ||||
return duplicate | return duplicate | ||||
@classmethod | |||||
def new(cls, doc_data, ignore_permissions=False): | |||||
data = doc_data.copy() | |||||
if not data.get('recipients'): | |||||
return | |||||
recipients = data.pop('recipients') | |||||
doc = frappe.new_doc(cls.DOCTYPE) | |||||
doc.update(data) | |||||
doc.set_recipients(recipients) | |||||
doc.insert(ignore_permissions=ignore_permissions) | |||||
return doc | |||||
@classmethod | @classmethod | ||||
def find(cls, name): | def find(cls, name): | ||||
return frappe.get_doc(cls.DOCTYPE, name) | return frappe.get_doc(cls.DOCTYPE, name) | ||||
@@ -74,8 +91,6 @@ class EmailQueue(Document): | |||||
return json.loads(self.attachments) if self.attachments else [] | return json.loads(self.attachments) if self.attachments else [] | ||||
def get_email_account(self): | def get_email_account(self): | ||||
from frappe.email.doctype.email_account.email_account import EmailAccount | |||||
if self.email_account: | if self.email_account: | ||||
return frappe.get_doc('Email Account', self.email_account) | return frappe.get_doc('Email Account', self.email_account) | ||||
@@ -300,3 +315,283 @@ def send_now(name): | |||||
def on_doctype_update(): | def on_doctype_update(): | ||||
"""Add index in `tabCommunication` for `(reference_doctype, reference_name)`""" | """Add index in `tabCommunication` for `(reference_doctype, reference_name)`""" | ||||
frappe.db.add_index('Email Queue', ('status', 'send_after', 'priority', 'creation'), 'index_bulk_flush') | frappe.db.add_index('Email Queue', ('status', 'send_after', 'priority', 'creation'), 'index_bulk_flush') | ||||
class QueueBuilder: | |||||
"""Builds Email Queue from the given data | |||||
""" | |||||
def __init__(self, recipients=None, sender=None, subject=None, message=None, | |||||
text_content=None, reference_doctype=None, reference_name=None, | |||||
unsubscribe_method=None, unsubscribe_params=None, unsubscribe_message=None, | |||||
attachments=None, reply_to=None, cc=None, bcc=None, message_id=None, in_reply_to=None, | |||||
send_after=None, expose_recipients=None, send_priority=1, communication=None, | |||||
read_receipt=None, queue_separately=False, is_notification=False, | |||||
add_unsubscribe_link=1, inline_images=None, header=None, | |||||
print_letterhead=False, with_container=False): | |||||
"""Add email to sending queue (Email Queue) | |||||
:param recipients: List of recipients. | |||||
:param sender: Email sender. | |||||
:param subject: Email subject. | |||||
:param message: Email message. | |||||
:param text_content: Text version of email message. | |||||
:param reference_doctype: Reference DocType of caller document. | |||||
:param reference_name: Reference name of caller document. | |||||
:param send_priority: Priority for Email Queue, default 1. | |||||
:param unsubscribe_method: URL method for unsubscribe. Default is `/api/method/frappe.email.queue.unsubscribe`. | |||||
:param unsubscribe_params: additional params for unsubscribed links. default are name, doctype, email | |||||
:param attachments: Attachments to be sent. | |||||
:param reply_to: Reply to be captured here (default inbox) | |||||
: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 communication: Communication link to be set in Email Queue record | |||||
:param queue_separately: Queue each email separately | |||||
:param is_notification: Marks email as notification so will not trigger notifications from system | |||||
:param add_unsubscribe_link: Send unsubscribe link in the footer of the Email, default 1. | |||||
:param inline_images: List of inline images as {"filename", "filecontent"}. All src properties will be replaced with random Content-Id | |||||
:param header: Append header in email (boolean) | |||||
:param with_container: Wraps email inside styled container | |||||
""" | |||||
self._unsubscribe_method = unsubscribe_method | |||||
self._recipients = recipients | |||||
self._cc = cc | |||||
self._bcc = bcc | |||||
self._send_after = send_after | |||||
self._sender = sender | |||||
self._text_content = text_content | |||||
self._message = message | |||||
self._add_unsubscribe_link = add_unsubscribe_link | |||||
self._unsubscribe_message = unsubscribe_message | |||||
self._attachments = attachments | |||||
self._unsubscribed_user_emails = None | |||||
self._email_account = None | |||||
self.unsubscribe_params = unsubscribe_params | |||||
self.subject = subject | |||||
self.reference_doctype = reference_doctype | |||||
self.reference_name = reference_name | |||||
self.expose_recipients = expose_recipients | |||||
self.with_container = with_container | |||||
self.header = header | |||||
self.reply_to = reply_to | |||||
self.message_id = message_id | |||||
self.in_reply_to = in_reply_to | |||||
self.send_priority = send_priority | |||||
self.communication = communication | |||||
self.read_receipt = read_receipt | |||||
self.queue_separately = queue_separately | |||||
self.is_notification = is_notification | |||||
self.inline_images = inline_images | |||||
self.print_letterhead = print_letterhead | |||||
@property | |||||
def unsubscribe_method(self): | |||||
return self._unsubscribe_method or '/api/method/frappe.email.queue.unsubscribe' | |||||
def _get_emails_list(self, emails=None): | |||||
emails = split_emails(emails) if isinstance(emails, str) else (emails or []) | |||||
return [each for each in set(emails) if each] | |||||
@property | |||||
def recipients(self): | |||||
return self._get_emails_list(self._recipients) | |||||
@property | |||||
def cc(self): | |||||
return self._get_emails_list(self._cc) | |||||
@property | |||||
def bcc(self): | |||||
return self._get_emails_list(self._bcc) | |||||
@property | |||||
def send_after(self): | |||||
if isinstance(self._send_after, int): | |||||
return add_days(nowdate(), self._send_after) | |||||
return self._send_after | |||||
@property | |||||
def sender(self): | |||||
if not self._sender or self._sender == "Administrator": | |||||
email_account = self.get_outgoing_email_account() | |||||
return email_account.default_sender | |||||
return self._sender | |||||
def email_text_content(self): | |||||
unsubscribe_msg = self.unsubscribe_message() | |||||
unsubscribe_text_message = (unsubscribe_msg and unsubscribe_msg.text) or '' | |||||
if self._text_content: | |||||
return self._text_content + unsubscribe_text_message | |||||
try: | |||||
text_content = html2text(self._message) | |||||
except HTMLParser.HTMLParseError: | |||||
text_content = "See html attachment" | |||||
return text_content + unsubscribe_text_message | |||||
def email_html_content(self): | |||||
email_account = self.get_outgoing_email_account() | |||||
return get_formatted_html(self.subject, self._message, header=self.header, | |||||
email_account=email_account, unsubscribe_link=self.unsubscribe_message(), | |||||
with_container=self.with_container) | |||||
def should_include_unsubscribe_link(self): | |||||
return (self._add_unsubscribe_link == 1 | |||||
and self.reference_doctype | |||||
and (self._unsubscribe_message or self.reference_doctype=="Newsletter")) | |||||
def unsubscribe_message(self): | |||||
if self.should_include_unsubscribe_link(): | |||||
return get_unsubscribe_message(self._unsubscribe_message, self.expose_recipients) | |||||
def get_outgoing_email_account(self): | |||||
if self._email_account: | |||||
return self._email_account | |||||
self._email_account = EmailAccount.find_outgoing( | |||||
match_by_doctype=self.reference_doctype, match_by_email=self._sender, _raise_error=True) | |||||
return self._email_account | |||||
def get_unsubscribed_user_emails(self): | |||||
if self._unsubscribed_user_emails is not None: | |||||
return self._unsubscribed_user_emails | |||||
all_ids = tuple(set(self.recipients + self.cc)) | |||||
unsubscribed = frappe.db.sql_list(''' | |||||
SELECT | |||||
distinct email | |||||
from | |||||
`tabEmail Unsubscribe` | |||||
where | |||||
email in %(all_ids)s | |||||
and ( | |||||
( | |||||
reference_doctype = %(reference_doctype)s | |||||
and reference_name = %(reference_name)s | |||||
) | |||||
or global_unsubscribe = 1 | |||||
) | |||||
''', { | |||||
'all_ids': all_ids, | |||||
'reference_doctype': self.reference_doctype, | |||||
'reference_name': self.reference_name, | |||||
}) | |||||
self._unsubscribed_user_emails = unsubscribed or [] | |||||
return self._unsubscribed_user_emails | |||||
def final_recipients(self): | |||||
unsubscribed_emails = self.get_unsubscribed_user_emails() | |||||
return [mail_id for mail_id in self.recipients if mail_id not in unsubscribed_emails] | |||||
def final_cc(self): | |||||
unsubscribed_emails = self.get_unsubscribed_user_emails() | |||||
return [mail_id for mail_id in self.cc if mail_id not in unsubscribed_emails] | |||||
def get_attachments(self): | |||||
attachments = [] | |||||
if self._attachments: | |||||
# store attachments with fid or print format details, to be attached on-demand later | |||||
for att in self._attachments: | |||||
if att.get('fid'): | |||||
attachments.append(att) | |||||
elif att.get("print_format_attachment") == 1: | |||||
if not att.get('lang', None): | |||||
att['lang'] = frappe.local.lang | |||||
att['print_letterhead'] = self.print_letterhead | |||||
attachments.append(att) | |||||
return attachments | |||||
def prepare_email_content(self): | |||||
mail = get_email(recipients=self.final_recipients(), | |||||
sender=self.sender, | |||||
subject=self.subject, | |||||
formatted=self.email_html_content(), | |||||
text_content=self.email_text_content(), | |||||
attachments=self._attachments, | |||||
reply_to=self.reply_to, | |||||
cc=self.final_cc(), | |||||
bcc=self.bcc, | |||||
email_account=self.get_outgoing_email_account(), | |||||
expose_recipients=self.expose_recipients, | |||||
inline_images=self.inline_images, | |||||
header=self.header) | |||||
mail.set_message_id(self.message_id, self.is_notification) | |||||
if self.read_receipt: | |||||
mail.msg_root["Disposition-Notification-To"] = self.sender | |||||
if self.in_reply_to: | |||||
mail.set_in_reply_to(self.in_reply_to) | |||||
return mail | |||||
def process(self, send_now=False): | |||||
"""Build and return the email queues those are created. | |||||
Sends email incase if it is requested to send now. | |||||
""" | |||||
final_recipients = self.final_recipients() | |||||
queue_separately = (final_recipients and self.queue_separately) or len(final_recipients) > 20 | |||||
if not (final_recipients + self.final_cc()): | |||||
return [] | |||||
email_queues = [] | |||||
queue_data = self.as_dict(include_recipients=False) | |||||
if not queue_data: | |||||
return [] | |||||
if not queue_separately: | |||||
recipients = list(set(final_recipients + self.final_cc() + self.bcc)) | |||||
q = EmailQueue.new({**queue_data, **{'recipients': recipients}}, ignore_permissions=True) | |||||
email_queues.append(q) | |||||
else: | |||||
for r in final_recipients: | |||||
recipients = [r] if email_queues else list(set([r] + self.final_cc() + self.bcc)) | |||||
q = EmailQueue.new({**queue_data, **{'recipients': recipients}}, ignore_permissions=True) | |||||
email_queues.append(q) | |||||
if send_now: | |||||
for doc in email_queues: | |||||
doc.send() | |||||
return email_queues | |||||
def as_dict(self, include_recipients=True): | |||||
email_account = self.get_outgoing_email_account() | |||||
email_account_name = email_account and email_account.is_exists_in_db() and email_account.name | |||||
mail = self.prepare_email_content() | |||||
try: | |||||
mail_to_string = cstr(mail.as_string()) | |||||
except frappe.InvalidEmailAddressError: | |||||
# bad Email Address - don't add to queue | |||||
frappe.log_error('Invalid Email ID Sender: {0}, Recipients: {1}, \nTraceback: {2} ' | |||||
.format(self.sender, ', '.join(self.final_recipients()), traceback.format_exc()), | |||||
'Email Not Sent' | |||||
) | |||||
return | |||||
d = { | |||||
'priority': self.send_priority, | |||||
'attachments': json.dumps(self.get_attachments()), | |||||
'message_id': mail.msg_root["Message-Id"].strip(" <>"), | |||||
'message': mail_to_string, | |||||
'sender': self.sender, | |||||
'reference_doctype': self.reference_doctype, | |||||
'reference_name': self.reference_name, | |||||
'add_unsubscribe_link': self._add_unsubscribe_link, | |||||
'unsubscribe_method': self.unsubscribe_method, | |||||
'unsubscribe_params': self.unsubscribe_params, | |||||
'expose_recipients': self.expose_recipients, | |||||
'communication': self.communication, | |||||
'send_after': self.send_after, | |||||
'show_as_cc': ",".join(self.final_cc()), | |||||
'show_as_bcc': ','.join(self.bcc), | |||||
'email_account': email_account_name or None | |||||
} | |||||
if include_recipients: | |||||
d['recipients'] = self.final_recipients() | |||||
return d |
@@ -8,7 +8,6 @@ import frappe.utils | |||||
from frappe import throw, _ | from frappe import throw, _ | ||||
from frappe.website.website_generator import WebsiteGenerator | from frappe.website.website_generator import WebsiteGenerator | ||||
from frappe.utils.verified_command import get_signed_params, verify_request | from frappe.utils.verified_command import get_signed_params, verify_request | ||||
from frappe.email.queue import send | |||||
from frappe.email.doctype.email_group.email_group import add_subscribers | from frappe.email.doctype.email_group.email_group import add_subscribers | ||||
from frappe.utils import parse_addr, now_datetime, markdown, validate_email_address | from frappe.utils import parse_addr, now_datetime, markdown, validate_email_address | ||||
@@ -3,257 +3,9 @@ | |||||
from __future__ import unicode_literals | from __future__ import unicode_literals | ||||
import frappe | import frappe | ||||
import sys | |||||
from six.moves import html_parser as HTMLParser | |||||
import smtplib, quopri, json | |||||
from frappe import msgprint, _, safe_decode, safe_encode, enqueue | |||||
from frappe.email.smtp import SMTPServer | |||||
from frappe.email.doctype.email_account.email_account import EmailAccount | |||||
from frappe.email.email_body import get_email, get_formatted_html, add_attachment | |||||
from frappe import msgprint, _ | |||||
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 frappe.utils import get_url, nowdate, now_datetime, add_days, split_emails, cstr, cint | |||||
from rq.timeouts import JobTimeoutException | |||||
from six import text_type, string_types, PY3 | |||||
from email.parser import Parser | |||||
class EmailLimitCrossedError(frappe.ValidationError): pass | |||||
def send(recipients=None, sender=None, subject=None, message=None, text_content=None, reference_doctype=None, | |||||
reference_name=None, unsubscribe_method=None, unsubscribe_params=None, unsubscribe_message=None, | |||||
attachments=None, reply_to=None, cc=None, bcc=None, message_id=None, in_reply_to=None, send_after=None, | |||||
expose_recipients=None, send_priority=1, communication=None, now=False, read_receipt=None, | |||||
queue_separately=False, is_notification=False, add_unsubscribe_link=1, inline_images=None, | |||||
header=None, print_letterhead=False, with_container=False): | |||||
"""Add email to sending queue (Email Queue) | |||||
:param recipients: List of recipients. | |||||
:param sender: Email sender. | |||||
:param subject: Email subject. | |||||
:param message: Email message. | |||||
:param text_content: Text version of email message. | |||||
:param reference_doctype: Reference DocType of caller document. | |||||
:param reference_name: Reference name of caller document. | |||||
:param send_priority: Priority for Email Queue, default 1. | |||||
:param unsubscribe_method: URL method for unsubscribe. Default is `/api/method/frappe.email.queue.unsubscribe`. | |||||
:param unsubscribe_params: additional params for unsubscribed links. default are name, doctype, email | |||||
:param attachments: Attachments to be sent. | |||||
:param reply_to: Reply to be captured here (default inbox) | |||||
: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 communication: Communication link to be set in Email Queue record | |||||
:param now: Send immediately (don't send in the background) | |||||
:param queue_separately: Queue each email separately | |||||
:param is_notification: Marks email as notification so will not trigger notifications from system | |||||
:param add_unsubscribe_link: Send unsubscribe link in the footer of the Email, default 1. | |||||
:param inline_images: List of inline images as {"filename", "filecontent"}. All src properties will be replaced with random Content-Id | |||||
:param header: Append header in email (boolean) | |||||
:param with_container: Wraps email inside styled container | |||||
""" | |||||
if not unsubscribe_method: | |||||
unsubscribe_method = "/api/method/frappe.email.queue.unsubscribe" | |||||
if not recipients and not cc: | |||||
return | |||||
if not cc: | |||||
cc = [] | |||||
if not bcc: | |||||
bcc = [] | |||||
if isinstance(recipients, string_types): | |||||
recipients = split_emails(recipients) | |||||
if isinstance(cc, string_types): | |||||
cc = split_emails(cc) | |||||
if isinstance(bcc, string_types): | |||||
bcc = split_emails(bcc) | |||||
if isinstance(send_after, int): | |||||
send_after = add_days(nowdate(), send_after) | |||||
email_account = EmailAccount.find_outgoing( | |||||
match_by_doctype=reference_doctype, match_by_email=sender, _raise_error=True) | |||||
if not sender or sender == "Administrator": | |||||
sender = email_account.default_sender | |||||
if not text_content: | |||||
try: | |||||
text_content = html2text(message) | |||||
except HTMLParser.HTMLParseError: | |||||
text_content = "See html attachment" | |||||
recipients = list(set(recipients)) | |||||
cc = list(set(cc)) | |||||
all_ids = tuple(recipients + cc) | |||||
unsubscribed = frappe.db.sql_list(''' | |||||
SELECT | |||||
distinct email | |||||
from | |||||
`tabEmail Unsubscribe` | |||||
where | |||||
email in %(all_ids)s | |||||
and ( | |||||
( | |||||
reference_doctype = %(reference_doctype)s | |||||
and reference_name = %(reference_name)s | |||||
) | |||||
or global_unsubscribe = 1 | |||||
) | |||||
''', { | |||||
'all_ids': all_ids, | |||||
'reference_doctype': reference_doctype, | |||||
'reference_name': reference_name, | |||||
}) | |||||
recipients = [r for r in recipients if r and r not in unsubscribed] | |||||
if cc: | |||||
cc = [r for r in cc if r and r not in unsubscribed] | |||||
if not recipients and not cc: | |||||
# Recipients may have been unsubscribed, exit quietly | |||||
return | |||||
email_text_context = text_content | |||||
should_append_unsubscribe = (add_unsubscribe_link | |||||
and reference_doctype | |||||
and (unsubscribe_message or reference_doctype=="Newsletter") | |||||
and add_unsubscribe_link==1) | |||||
unsubscribe_link = None | |||||
if should_append_unsubscribe: | |||||
unsubscribe_link = get_unsubscribe_message(unsubscribe_message, expose_recipients) | |||||
email_text_context += unsubscribe_link.text | |||||
email_content = get_formatted_html(subject, message, | |||||
email_account=email_account, header=header, | |||||
unsubscribe_link=unsubscribe_link, with_container=with_container) | |||||
# add to queue | |||||
add(recipients, sender, subject, | |||||
formatted=email_content, | |||||
text_content=email_text_context, | |||||
reference_doctype=reference_doctype, | |||||
reference_name=reference_name, | |||||
attachments=attachments, | |||||
reply_to=reply_to, | |||||
cc=cc, | |||||
bcc=bcc, | |||||
message_id=message_id, | |||||
in_reply_to=in_reply_to, | |||||
send_after=send_after, | |||||
send_priority=send_priority, | |||||
email_account=email_account, | |||||
communication=communication, | |||||
add_unsubscribe_link=add_unsubscribe_link, | |||||
unsubscribe_method=unsubscribe_method, | |||||
unsubscribe_params=unsubscribe_params, | |||||
expose_recipients=expose_recipients, | |||||
read_receipt=read_receipt, | |||||
queue_separately=queue_separately, | |||||
is_notification = is_notification, | |||||
inline_images = inline_images, | |||||
header=header, | |||||
now=now, | |||||
print_letterhead=print_letterhead) | |||||
def add(recipients, sender, subject, **kwargs): | |||||
"""Add to Email Queue""" | |||||
if kwargs.get('queue_separately') or len(recipients) > 20: | |||||
email_queue = None | |||||
for r in recipients: | |||||
if not email_queue: | |||||
email_queue = get_email_queue([r], sender, subject, **kwargs) | |||||
if kwargs.get('now'): | |||||
email_queue.send() | |||||
else: | |||||
duplicate = email_queue.get_duplicate([r]) | |||||
duplicate.insert(ignore_permissions=True) | |||||
if kwargs.get('now'): | |||||
duplicate.send() | |||||
frappe.db.commit() | |||||
else: | |||||
email_queue = get_email_queue(recipients, sender, subject, **kwargs) | |||||
if kwargs.get('now'): | |||||
email_queue.send() | |||||
def get_email_queue(recipients, sender, subject, **kwargs): | |||||
'''Make Email Queue object''' | |||||
e = frappe.new_doc('Email Queue') | |||||
e.priority = kwargs.get('send_priority') | |||||
attachments = kwargs.get('attachments') | |||||
if attachments: | |||||
# store attachments with fid or print format details, to be attached on-demand later | |||||
_attachments = [] | |||||
for att in attachments: | |||||
if att.get('fid'): | |||||
_attachments.append(att) | |||||
elif att.get("print_format_attachment") == 1: | |||||
if not att.get('lang', None): | |||||
att['lang'] = frappe.local.lang | |||||
att['print_letterhead'] = kwargs.get('print_letterhead') | |||||
_attachments.append(att) | |||||
e.attachments = json.dumps(_attachments) | |||||
try: | |||||
mail = get_email(recipients, | |||||
sender=sender, | |||||
subject=subject, | |||||
formatted=kwargs.get('formatted'), | |||||
text_content=kwargs.get('text_content'), | |||||
attachments=kwargs.get('attachments'), | |||||
reply_to=kwargs.get('reply_to'), | |||||
cc=kwargs.get('cc'), | |||||
bcc=kwargs.get('bcc'), | |||||
email_account=kwargs.get('email_account'), | |||||
expose_recipients=kwargs.get('expose_recipients'), | |||||
inline_images=kwargs.get('inline_images'), | |||||
header=kwargs.get('header')) | |||||
mail.set_message_id(kwargs.get('message_id'),kwargs.get('is_notification')) | |||||
if kwargs.get('read_receipt'): | |||||
mail.msg_root["Disposition-Notification-To"] = sender | |||||
if kwargs.get('in_reply_to'): | |||||
mail.set_in_reply_to(kwargs.get('in_reply_to')) | |||||
e.message_id = mail.msg_root["Message-Id"].strip(" <>") | |||||
e.message = cstr(mail.as_string()) | |||||
e.sender = mail.sender | |||||
except frappe.InvalidEmailAddressError: | |||||
# bad Email Address - don't add to queue | |||||
import traceback | |||||
frappe.log_error('Invalid Email ID Sender: {0}, Recipients: {1}, \nTraceback: {2} '.format(mail.sender, | |||||
', '.join(mail.recipients), traceback.format_exc()), 'Email Not Sent') | |||||
recipients = list(set(recipients + kwargs.get('cc', []) + kwargs.get('bcc', []))) | |||||
email_account = kwargs.get('email_account') | |||||
email_account_name = email_account and email_account.is_exists_in_db() and email_account.name | |||||
e.set_recipients(recipients) | |||||
e.reference_doctype = kwargs.get('reference_doctype') | |||||
e.reference_name = kwargs.get('reference_name') | |||||
e.add_unsubscribe_link = kwargs.get("add_unsubscribe_link") | |||||
e.unsubscribe_method = kwargs.get('unsubscribe_method') | |||||
e.unsubscribe_params = kwargs.get('unsubscribe_params') | |||||
e.expose_recipients = kwargs.get('expose_recipients') | |||||
e.communication = kwargs.get('communication') | |||||
e.send_after = kwargs.get('send_after') | |||||
e.show_as_cc = ",".join(kwargs.get('cc', [])) | |||||
e.show_as_bcc = ",".join(kwargs.get('bcc', [])) | |||||
e.email_account = email_account_name or None | |||||
e.insert(ignore_permissions=True) | |||||
return e | |||||
from frappe.utils import get_url, now_datetime, cint | |||||
def get_emails_sent_this_month(): | def get_emails_sent_this_month(): | ||||
return frappe.db.sql(""" | return frappe.db.sql(""" | ||||
@@ -85,18 +85,19 @@ class SMTPServer: | |||||
SMTP = smtplib.SMTP_SSL if self.use_ssl else smtplib.SMTP | SMTP = smtplib.SMTP_SSL if self.use_ssl else smtplib.SMTP | ||||
try: | try: | ||||
self._session = SMTP(self.server, self.port) | |||||
if not self._session: | |||||
_session = SMTP(self.server, self.port) | |||||
if not _session: | |||||
frappe.msgprint(CONNECTION_FAILED, raise_exception=frappe.OutgoingEmailError) | frappe.msgprint(CONNECTION_FAILED, raise_exception=frappe.OutgoingEmailError) | ||||
self.secure_session(self._session) | |||||
self.secure_session(_session) | |||||
if self.login and self.password: | if self.login and self.password: | ||||
res = self._session.login(str(self.login or ""), str(self.password or "")) | |||||
res = _session.login(str(self.login or ""), str(self.password or "")) | |||||
# check if logged correctly | # check if logged correctly | ||||
if res[0]!=235: | if res[0]!=235: | ||||
frappe.msgprint(res[1], raise_exception=frappe.OutgoingEmailError) | frappe.msgprint(res[1], raise_exception=frappe.OutgoingEmailError) | ||||
self._session = _session | |||||
return self._session | return self._session | ||||
except smtplib.SMTPAuthenticationError as e: | except smtplib.SMTPAuthenticationError as e: | ||||
@@ -7,8 +7,7 @@ from frappe import safe_decode | |||||
from frappe.email.receive import Email | from frappe.email.receive import Email | ||||
from frappe.email.email_body import (replace_filename_with_cid, | from frappe.email.email_body import (replace_filename_with_cid, | ||||
get_email, inline_style_in_html, get_header) | get_email, inline_style_in_html, get_header) | ||||
from frappe.email.queue import get_email_queue | |||||
from frappe.email.doctype.email_queue.email_queue import SendMailContext | |||||
from frappe.email.doctype.email_queue.email_queue import SendMailContext, QueueBuilder | |||||
from six import PY3 | from six import PY3 | ||||
class TestEmailBody(unittest.TestCase): | class TestEmailBody(unittest.TestCase): | ||||
@@ -50,27 +49,25 @@ This is the text version of this email | |||||
uni_chr1 = unichr(40960) | uni_chr1 = unichr(40960) | ||||
uni_chr2 = unichr(1972) | uni_chr2 = unichr(1972) | ||||
email = get_email_queue( | |||||
queue_doc = QueueBuilder( | |||||
recipients=['test@example.com'], | recipients=['test@example.com'], | ||||
sender='me@example.com', | sender='me@example.com', | ||||
subject='Test Subject', | subject='Test Subject', | ||||
content='<h1>' + uni_chr1 + 'abcd' + uni_chr2 + '</h1>', | |||||
formatted='<h1>' + uni_chr1 + 'abcd' + uni_chr2 + '</h1>', | |||||
text_content='whatever') | |||||
mail_ctx = SendMailContext(queue_doc = email) | |||||
message='<h1>' + uni_chr1 + 'abcd' + uni_chr2 + '</h1>', | |||||
text_content='whatever').process()[0] | |||||
mail_ctx = SendMailContext(queue_doc = queue_doc) | |||||
result = mail_ctx.build_message(recipient_email = 'test@test.com') | result = mail_ctx.build_message(recipient_email = 'test@test.com') | ||||
self.assertTrue(b"<h1>=EA=80=80abcd=DE=B4</h1>" in result) | self.assertTrue(b"<h1>=EA=80=80abcd=DE=B4</h1>" in result) | ||||
def test_prepare_message_returns_cr_lf(self): | def test_prepare_message_returns_cr_lf(self): | ||||
email = get_email_queue( | |||||
queue_doc = QueueBuilder( | |||||
recipients=['test@example.com'], | recipients=['test@example.com'], | ||||
sender='me@example.com', | sender='me@example.com', | ||||
subject='Test Subject', | subject='Test Subject', | ||||
content='<h1>\n this is a test of newlines\n' + '</h1>', | |||||
formatted='<h1>\n this is a test of newlines\n' + '</h1>', | |||||
text_content='whatever') | |||||
message='<h1>\n this is a test of newlines\n' + '</h1>', | |||||
text_content='whatever').process()[0] | |||||
mail_ctx = SendMailContext(queue_doc = email) | |||||
mail_ctx = SendMailContext(queue_doc = queue_doc) | |||||
result = safe_decode(mail_ctx.build_message(recipient_email='test@test.com')) | result = safe_decode(mail_ctx.build_message(recipient_email='test@test.com')) | ||||
if PY3: | if PY3: | ||||
@@ -143,7 +143,8 @@ class TestEmail(unittest.TestCase): | |||||
self.assertEqual(len(queue_recipients), 2) | self.assertEqual(len(queue_recipients), 2) | ||||
def test_unsubscribe(self): | def test_unsubscribe(self): | ||||
from frappe.email.queue import unsubscribe, send | |||||
from frappe.email.queue import unsubscribe | |||||
from frappe.email.doctype.email_queue.email_queue import QueueBuilder | |||||
unsubscribe(doctype="User", name="Administrator", email="test@example.com") | unsubscribe(doctype="User", name="Administrator", email="test@example.com") | ||||
self.assertTrue(frappe.db.get_value("Email Unsubscribe", | self.assertTrue(frappe.db.get_value("Email Unsubscribe", | ||||
@@ -152,11 +153,11 @@ class TestEmail(unittest.TestCase): | |||||
before = frappe.db.sql("""select count(name) from `tabEmail Queue` where status='Not Sent'""")[0][0] | before = frappe.db.sql("""select count(name) from `tabEmail Queue` where status='Not Sent'""")[0][0] | ||||
send(recipients=['test@example.com', 'test1@example.com'], | |||||
sender="admin@example.com", | |||||
reference_doctype='User', reference_name="Administrator", | |||||
subject='Testing Email Queue', message='This is mail is queued!', unsubscribe_message="Unsubscribe") | |||||
builder = QueueBuilder(recipients=['test@example.com', 'test1@example.com'], | |||||
sender="admin@example.com", | |||||
reference_doctype='User', reference_name="Administrator", | |||||
subject='Testing Email Queue', message='This is mail is queued!', unsubscribe_message="Unsubscribe") | |||||
builder.process() | |||||
# this is sent async (?) | # this is sent async (?) | ||||
email_queue = frappe.db.sql("""select name from `tabEmail Queue` where status='Not Sent'""", | email_queue = frappe.db.sql("""select name from `tabEmail Queue` where status='Not Sent'""", | ||||
@@ -215,7 +215,7 @@ def raise_error_on_no_output(error_message, error_type=None, keep_quiet=None): | |||||
>>> @raise_error_on_no_output("Ingradients missing") | >>> @raise_error_on_no_output("Ingradients missing") | ||||
... def get_indradients(_raise_error=1): return | ... def get_indradients(_raise_error=1): return | ||||
... | ... | ||||
>>> get_indradients() | |||||
>>> get_ingradients() | |||||
`Exception Name`: Ingradients missing | `Exception Name`: Ingradients missing | ||||
""" | """ | ||||
def decorator_raise_error_on_no_output(func): | def decorator_raise_error_on_no_output(func): | ||||