# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # MIT License. See license.txt from __future__ import unicode_literals import frappe import HTMLParser import smtplib from frappe import msgprint, throw, _ from frappe.email.smtp import SMTPServer, get_outgoing_email_account from frappe.email.email_body import get_email, get_formatted_html from frappe.utils.verified_command import get_signed_params, verify_request from html2text import html2text from frappe.utils import get_url, nowdate, encode, now_datetime, add_days class BulkLimitCrossedError(frappe.ValidationError): pass 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, attachments=None, reply_to=None, cc=(), message_id=None, send_after=None): """Add email to sending queue (Bulk Email) :param recipients: List of recipients. :param sender: Email sender. :param subject: Email subject. :param message: Email message. :param reference_doctype: Reference DocType of caller document. :param reference_name: Reference name of caller document. :param unsubscribe_method: URL method for unsubscribe. Default is `/api/method/frappe.email.bulk.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 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 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. """ if not unsubscribe_method: unsubscribe_method = "/api/method/frappe.email.bulk.unsubscribe" if not recipients: return if isinstance(recipients, basestring): recipients = recipients.split(",") if isinstance(send_after, int): send_after = add_days(nowdate(), send_after) if not sender or sender == "Administrator": email_account = get_outgoing_email_account() sender = email_account.get("sender") or email_account.email_id check_bulk_limit(recipients) formatted = get_formatted_html(subject, message) try: text_content = html2text(formatted) except HTMLParser.HTMLParseError: text_content = "See html attachment" if reference_doctype and reference_name: unsubscribed = [d.email for d in frappe.db.get_all("Email Unsubscribe", "email", {"reference_doctype": reference_doctype, "reference_name": reference_name})] unsubscribed += [d.email for d in frappe.db.get_all("Email Unsubscribe", "email", {"global_unsubscribe": 1})] else: unsubscribed = [] for email in filter(None, list(set(recipients))): if email not in unsubscribed: email_content = formatted email_text_context = text_content if reference_doctype: unsubscribe_url = get_unsubcribed_url(reference_doctype, reference_name, email, unsubscribe_method, unsubscribe_params) # add to queue email_content = add_unsubscribe_link(email_content, email, reference_doctype, reference_name, unsubscribe_url, unsubscribe_message) email_text_context += "\n" + _("This email was sent to {0}. To unsubscribe click on this link: {1}").format(email, unsubscribe_url) add(email, sender, subject, email_content, email_text_context, reference_doctype, reference_name, attachments, reply_to, cc, message_id, send_after) def add(email, sender, subject, formatted, text_content=None, reference_doctype=None, reference_name=None, attachments=None, reply_to=None, cc=(), message_id=None, send_after=None): """add to bulk mail queue""" e = frappe.new_doc('Bulk Email') e.sender = sender e.recipient = email try: mail = get_email(email, sender=e.sender, formatted=formatted, subject=subject, text_content=text_content, attachments=attachments, reply_to=reply_to, cc=cc) if message_id: mail.set_message_id(message_id) e.message = mail.as_string() except frappe.InvalidEmailAddressError: # bad email id - don't add to queue return e.reference_doctype = reference_doctype e.reference_name = reference_name e.send_after = send_after e.insert(ignore_permissions=True) def check_bulk_limit(recipients): # get count of mails sent this month this_month = frappe.db.sql("""select count(*) from `tabBulk Email` where status='Sent' and MONTH(creation)=MONTH(CURDATE())""")[0][0] # if using settings from site_config.json, check bulk limit # No limit for own email settings smtp_server = SMTPServer() if (smtp_server.email_account and getattr(smtp_server.email_account, "from_site_config", False) or frappe.flags.in_test): monthly_bulk_mail_limit = frappe.conf.get('monthly_bulk_mail_limit') or 500 if (this_month + len(recipients)) > monthly_bulk_mail_limit: throw(_("Email limit {0} crossed").format(monthly_bulk_mail_limit), BulkLimitCrossedError) def add_unsubscribe_link(message, email, reference_doctype, reference_name, unsubscribe_url, unsubscribe_message): unsubscribe_link = """
{email}. {unsubscribe_message}.
""".format(unsubscribe_url = unsubscribe_url, email= _("This email was sent to {0}").format(email), unsubscribe_message = unsubscribe_message or _("Unsubscribe from this list")) message = message.replace("", unsubscribe_link) return message def get_unsubcribed_url(reference_doctype, reference_name, email, unsubscribe_method, unsubscribe_params): params = {"email": email.encode("utf-8"), "doctype": reference_doctype.encode("utf-8"), "name": reference_name.encode("utf-8")} if unsubscribe_params: params.update(unsubscribe_params) query_string = get_signed_params(params) # for test frappe.local.flags.signed_query_string = query_string return get_url(unsubscribe_method + "?" + get_signed_params(params)) @frappe.whitelist(allow_guest=True) def unsubscribe(doctype, name, email): # unsubsribe from comments and communications if not verify_request(): return try: frappe.get_doc({ "doctype": "Email Unsubscribe", "email": email, "reference_doctype": doctype, "reference_name": name }).insert(ignore_permissions=True) except frappe.DuplicateEntryError: frappe.db.rollback() else: frappe.db.commit() return_unsubscribed_page(email, doctype, name) def return_unsubscribed_page(email, doctype, name): frappe.respond_as_web_page(_("Unsubscribed"), _("{0} has left the conversation in {1} {2}").format(email, _(doctype), name)) def flush(from_test=False): """flush email queue, every time: called from scheduler""" smtpserver = SMTPServer() auto_commit = not from_test # additional check check_bulk_limit([]) if frappe.are_emails_muted(): msgprint(_("Emails are muted")) from_test = True frappe.db.sql("""update `tabBulk Email` set status='Expired' where datediff(curdate(), creation) > 3""", auto_commit=auto_commit) for i in xrange(500): email = frappe.db.sql("""select * from `tabBulk Email` where status='Not Sent' and ifnull(send_after, "2000-01-01 00:00:00") < %s order by creation asc limit 1 for update""", now_datetime(), as_dict=1) if email: email = email[0] else: break frappe.db.sql("""update `tabBulk Email` set status='Sending' where name=%s""", (email["name"],), auto_commit=auto_commit) try: if not from_test: smtpserver.setup_email_account(email.reference_doctype) smtpserver.sess.sendmail(email["sender"], email["recipient"], encode(email["message"])) frappe.db.sql("""update `tabBulk Email` set status='Sent' where name=%s""", (email["name"],), auto_commit=auto_commit) except (smtplib.SMTPServerDisconnected, smtplib.SMTPConnectError, smtplib.SMTPHeloError, smtplib.SMTPAuthenticationError): # bad connection, retry later frappe.db.sql("""update `tabBulk Email` set status='Not Sent' where name=%s""", (email["name"],), auto_commit=auto_commit) # no need to attempt further return except Exception, e: frappe.db.sql("""update `tabBulk Email` set status='Error', error=%s where name=%s""", (unicode(e), email["name"]), auto_commit=auto_commit) def clear_outbox(): """Remove mails older than 31 days in Outbox. Called daily via scheduler.""" frappe.db.sql("""delete from `tabBulk Email` where datediff(now(), creation) > 31""")