You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

353 lines
12 KiB

  1. # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
  2. # MIT License. See license.txt
  3. from __future__ import unicode_literals
  4. import frappe
  5. import HTMLParser
  6. import smtplib
  7. from frappe import msgprint, throw, _
  8. from frappe.email.smtp import SMTPServer, get_outgoing_email_account
  9. from frappe.email.email_body import get_email, get_formatted_html
  10. from frappe.utils.verified_command import get_signed_params, verify_request
  11. from html2text import html2text
  12. from frappe.utils import get_url, nowdate, encode, now_datetime, add_days, split_emails, cstr
  13. from rq.timeouts import JobTimeoutException
  14. from frappe.utils.scheduler import log
  15. class EmailLimitCrossedError(frappe.ValidationError): pass
  16. def send(recipients=None, sender=None, subject=None, message=None, reference_doctype=None,
  17. reference_name=None, unsubscribe_method=None, unsubscribe_params=None, unsubscribe_message=None,
  18. attachments=None, reply_to=None, cc=(), show_as_cc=(), message_id=None, in_reply_to=None, send_after=None,
  19. expose_recipients=False, send_priority=1, communication=None):
  20. """Add email to sending queue (Email Queue)
  21. :param recipients: List of recipients.
  22. :param sender: Email sender.
  23. :param subject: Email subject.
  24. :param message: Email message.
  25. :param reference_doctype: Reference DocType of caller document.
  26. :param reference_name: Reference name of caller document.
  27. :param send_priority: Priority for Email Queue, default 1.
  28. :param unsubscribe_method: URL method for unsubscribe. Default is `/api/method/frappe.email.queue.unsubscribe`.
  29. :param unsubscribe_params: additional params for unsubscribed links. default are name, doctype, email
  30. :param attachments: Attachments to be sent.
  31. :param reply_to: Reply to be captured here (default inbox)
  32. :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.
  33. :param in_reply_to: Used to send the Message-Id of a received email back as In-Reply-To.
  34. :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.
  35. :param communication: Communication link to be set in Email Queue record
  36. """
  37. if not unsubscribe_method:
  38. unsubscribe_method = "/api/method/frappe.email.queue.unsubscribe"
  39. if not recipients:
  40. return
  41. if isinstance(recipients, basestring):
  42. recipients = split_emails(recipients)
  43. if isinstance(send_after, int):
  44. send_after = add_days(nowdate(), send_after)
  45. email_account = get_outgoing_email_account(True, append_to=reference_doctype)
  46. if not sender or sender == "Administrator":
  47. sender = email_account.default_sender
  48. check_email_limit(recipients)
  49. formatted = get_formatted_html(subject, message, email_account=email_account)
  50. try:
  51. text_content = html2text(formatted)
  52. except HTMLParser.HTMLParseError:
  53. text_content = "See html attachment"
  54. if reference_doctype and reference_name:
  55. unsubscribed = [d.email for d in frappe.db.get_all("Email Unsubscribe", "email",
  56. {"reference_doctype": reference_doctype, "reference_name": reference_name})]
  57. unsubscribed += [d.email for d in frappe.db.get_all("Email Unsubscribe", "email",
  58. {"global_unsubscribe": 1})]
  59. else:
  60. unsubscribed = []
  61. recipients = [r for r in list(set(recipients)) if r and r not in unsubscribed]
  62. for email in recipients:
  63. email_content = formatted
  64. email_text_context = text_content
  65. if reference_doctype:
  66. unsubscribe_link = get_unsubscribe_link(
  67. reference_doctype=reference_doctype,
  68. reference_name=reference_name,
  69. email=email,
  70. recipients=recipients,
  71. expose_recipients=expose_recipients,
  72. unsubscribe_method=unsubscribe_method,
  73. unsubscribe_params=unsubscribe_params,
  74. unsubscribe_message=unsubscribe_message,
  75. show_as_cc=show_as_cc
  76. )
  77. email_content = email_content.replace("<!--unsubscribe link here-->", unsubscribe_link.html)
  78. email_text_context += unsubscribe_link.text
  79. # show as cc
  80. cc_message = ""
  81. if email in show_as_cc:
  82. cc_message = _("This email was sent to you as CC")
  83. email_content = email_content.replace("<!-- cc message -->", cc_message)
  84. email_text_context = cc_message + "\n" + email_text_context
  85. # add to queue
  86. add(email, sender, subject, email_content, email_text_context, reference_doctype,
  87. reference_name, attachments, reply_to, cc, message_id, in_reply_to, send_after, send_priority, email_account=email_account, communication=communication)
  88. def add(email, sender, subject, formatted, text_content=None,
  89. reference_doctype=None, reference_name=None, attachments=None, reply_to=None,
  90. cc=(), message_id=None, in_reply_to=None, send_after=None, send_priority=1, email_account=None, communication=None):
  91. """Add to Email Queue"""
  92. e = frappe.new_doc('Email Queue')
  93. e.recipient = email
  94. e.priority = send_priority
  95. try:
  96. mail = get_email(email, sender=sender, formatted=formatted, subject=subject,
  97. text_content=text_content, attachments=attachments, reply_to=reply_to, cc=cc, email_account=email_account)
  98. if message_id:
  99. mail.set_message_id(message_id)
  100. if in_reply_to:
  101. mail.set_in_reply_to(in_reply_to)
  102. e.message = cstr(mail.as_string())
  103. e.sender = mail.sender
  104. except frappe.InvalidEmailAddressError:
  105. # bad email id - don't add to queue
  106. return
  107. e.reference_doctype = reference_doctype
  108. e.reference_name = reference_name
  109. e.communication = communication
  110. e.send_after = send_after
  111. e.db_insert()
  112. def check_email_limit(recipients):
  113. # if using settings from site_config.json, check email limit
  114. # No limit for own email settings
  115. smtp_server = SMTPServer()
  116. if (smtp_server.email_account
  117. and getattr(smtp_server.email_account, "from_site_config", False)
  118. or frappe.flags.in_test):
  119. # get count of mails sent this month
  120. this_month = get_emails_sent_this_month()
  121. monthly_email_limit = frappe.conf.get('limits', {}).get('emails') or 500
  122. if frappe.flags.in_test:
  123. monthly_email_limit = 500
  124. if (this_month + len(recipients)) > monthly_email_limit:
  125. throw(_("Cannot send this email. You have crossed the sending limit of {0} emails for this month.").format(monthly_email_limit),
  126. EmailLimitCrossedError)
  127. def get_emails_sent_this_month():
  128. return frappe.db.sql("""select count(name) from `tabEmail Queue` where
  129. status='Sent' and MONTH(creation)=MONTH(CURDATE())""")[0][0]
  130. def get_unsubscribe_link(reference_doctype, reference_name,
  131. email, recipients, expose_recipients, show_as_cc,
  132. unsubscribe_method, unsubscribe_params, unsubscribe_message):
  133. email_sent_to = recipients if expose_recipients else [email]
  134. email_sent_cc = ", ".join([e for e in email_sent_to if e in show_as_cc])
  135. email_sent_to = ", ".join([e for e in email_sent_to if e not in show_as_cc])
  136. if email_sent_cc:
  137. email_sent_message = _("This email was sent to {0} and copied to {1}").format(email_sent_to, email_sent_cc)
  138. else:
  139. email_sent_message = _("This email was sent to {0}").format(email_sent_to)
  140. if not unsubscribe_message:
  141. unsubscribe_message = _("Unsubscribe from this list")
  142. unsubscribe_url = get_unsubcribed_url(reference_doctype, reference_name, email,
  143. unsubscribe_method, unsubscribe_params)
  144. html = """<div style="margin: 15px auto; padding: 0px 7px; text-align: center; color: #8d99a6;">
  145. {email}
  146. <p style="margin: 15px auto;">
  147. <a href="{unsubscribe_url}" style="color: #8d99a6; text-decoration: underline;
  148. target="_blank">{unsubscribe_message}
  149. </a>
  150. </p>
  151. </div>""".format(
  152. unsubscribe_url = unsubscribe_url,
  153. email=email_sent_message,
  154. unsubscribe_message=unsubscribe_message
  155. )
  156. text = "\n{email}\n\n{unsubscribe_message}: {unsubscribe_url}".format(
  157. email=email_sent_message,
  158. unsubscribe_message=unsubscribe_message,
  159. unsubscribe_url=unsubscribe_url
  160. )
  161. return frappe._dict({
  162. "html": html,
  163. "text": text
  164. })
  165. def get_unsubcribed_url(reference_doctype, reference_name, email, unsubscribe_method, unsubscribe_params):
  166. params = {"email": email.encode("utf-8"),
  167. "doctype": reference_doctype.encode("utf-8"),
  168. "name": reference_name.encode("utf-8")}
  169. if unsubscribe_params:
  170. params.update(unsubscribe_params)
  171. query_string = get_signed_params(params)
  172. # for test
  173. frappe.local.flags.signed_query_string = query_string
  174. return get_url(unsubscribe_method + "?" + get_signed_params(params))
  175. @frappe.whitelist(allow_guest=True)
  176. def unsubscribe(doctype, name, email):
  177. # unsubsribe from comments and communications
  178. if not verify_request():
  179. return
  180. try:
  181. frappe.get_doc({
  182. "doctype": "Email Unsubscribe",
  183. "email": email,
  184. "reference_doctype": doctype,
  185. "reference_name": name
  186. }).insert(ignore_permissions=True)
  187. except frappe.DuplicateEntryError:
  188. frappe.db.rollback()
  189. else:
  190. frappe.db.commit()
  191. return_unsubscribed_page(email, doctype, name)
  192. def return_unsubscribed_page(email, doctype, name):
  193. frappe.respond_as_web_page(_("Unsubscribed"), _("{0} has left the conversation in {1} {2}").format(email, _(doctype), name))
  194. def flush(from_test=False):
  195. """flush email queue, every time: called from scheduler"""
  196. # additional check
  197. cache = frappe.cache()
  198. check_email_limit([])
  199. auto_commit = not from_test
  200. if frappe.are_emails_muted():
  201. msgprint(_("Emails are muted"))
  202. from_test = True
  203. smtpserver = SMTPServer()
  204. make_cache_queue()
  205. for i in xrange(cache.llen('cache_email_queue')):
  206. email = cache.lpop('cache_email_queue')
  207. if email:
  208. send_one(email, smtpserver, auto_commit)
  209. # NOTE: removing commit here because we pass auto_commit
  210. # finally:
  211. # frappe.db.commit()
  212. def make_cache_queue():
  213. '''cache values in queue before sendign'''
  214. cache = frappe.cache()
  215. emails = frappe.db.sql('''select name from `tabEmail Queue`
  216. where status='Not Sent' and (send_after is null or send_after < %(now)s)
  217. order by priority desc, creation asc
  218. limit 500''', { 'now': now_datetime() })
  219. # reset value
  220. cache.delete_value('cache_email_queue')
  221. for e in emails:
  222. cache.rpush('cache_email_queue', e[0])
  223. def send_one(email, smtpserver=None, auto_commit=True, now=False):
  224. '''Send Email Queue with given smtpserver'''
  225. email = frappe.db.sql('''select name, status, communication,
  226. message, sender, recipient, reference_doctype
  227. from `tabEmail Queue` where name=%s for update''', email, as_dict=True)[0]
  228. if email.status != 'Not Sent':
  229. # rollback to release lock and return
  230. frappe.db.rollback()
  231. return
  232. frappe.db.sql("""update `tabEmail Queue` set status='Sending', modified=%s where name=%s""",
  233. (now_datetime(), email.name), auto_commit=auto_commit)
  234. if email.communication:
  235. frappe.get_doc('Communication', email.communication).set_delivery_status(commit=auto_commit)
  236. try:
  237. if auto_commit:
  238. if not smtpserver: smtpserver = SMTPServer()
  239. smtpserver.setup_email_account(email.reference_doctype)
  240. smtpserver.sess.sendmail(email.sender, email.recipient, encode(email.message))
  241. frappe.db.sql("""update `tabEmail Queue` set status='Sent', modified=%s where name=%s""",
  242. (now_datetime(), email.name), auto_commit=auto_commit)
  243. if email.communication:
  244. frappe.get_doc('Communication', email.communication).set_delivery_status(commit=auto_commit)
  245. except (smtplib.SMTPServerDisconnected,
  246. smtplib.SMTPConnectError,
  247. smtplib.SMTPHeloError,
  248. smtplib.SMTPAuthenticationError,
  249. JobTimeoutException):
  250. # bad connection/timeout, retry later
  251. frappe.db.sql("""update `tabEmail Queue` set status='Not Sent', modified=%s where name=%s""",
  252. (now_datetime(), email.name), auto_commit=auto_commit)
  253. if email.communication:
  254. frappe.get_doc('Communication', email.communication).set_delivery_status(commit=auto_commit)
  255. # no need to attempt further
  256. return
  257. except Exception, e:
  258. frappe.db.rollback()
  259. frappe.db.sql("""update `tabEmail Queue` set status='Error', error=%s
  260. where name=%s""", (unicode(e), email.name), auto_commit=auto_commit)
  261. if email.communication:
  262. frappe.get_doc('Communication', email.communication).set_delivery_status(commit=auto_commit)
  263. if now:
  264. raise e
  265. else:
  266. # log to Error Log
  267. log('frappe.email.queue.flush', unicode(e))
  268. def clear_outbox():
  269. """Remove mails older than 31 days in Outbox. Called daily via scheduler."""
  270. frappe.db.sql("""delete from `tabEmail Queue` where
  271. datediff(now(), modified) > 31""")
  272. frappe.db.sql("""update `tabEmail Queue` set status='Expired'
  273. where datediff(curdate(), modified) > 7 and status='Not Sent'""")