Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

11 лет назад
11 лет назад
11 лет назад
10 лет назад
10 лет назад
11 лет назад
10 лет назад
10 лет назад
10 лет назад
11 лет назад
9 лет назад
10 лет назад
10 лет назад
10 лет назад
10 лет назад
9 лет назад
9 лет назад
9 лет назад
9 лет назад
11 лет назад
12 лет назад
10 лет назад
10 лет назад
12 лет назад
10 лет назад
9 лет назад
10 лет назад
11 лет назад
9 лет назад
9 лет назад
9 лет назад
9 лет назад
9 лет назад
9 лет назад
12 лет назад
9 лет назад
9 лет назад
10 лет назад
8 лет назад
8 лет назад
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  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'""")