您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
 
 
 
 
 
 

561 行
19 KiB

  1. # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
  2. # MIT License. See license.txt
  3. from __future__ import unicode_literals
  4. from six.moves import range
  5. import frappe
  6. from six.moves import html_parser as HTMLParser
  7. import smtplib, quopri, json
  8. from frappe import msgprint, throw, _
  9. from frappe.email.smtp import SMTPServer, get_outgoing_email_account
  10. from frappe.email.email_body import get_email, get_formatted_html, add_attachment
  11. from frappe.utils.verified_command import get_signed_params, verify_request
  12. from html2text import html2text
  13. from frappe.utils import get_url, nowdate, encode, now_datetime, add_days, split_emails, cstr, cint
  14. from frappe.utils.file_manager import get_file
  15. from rq.timeouts import JobTimeoutException
  16. from frappe.utils.scheduler import log
  17. from six import text_type, string_types
  18. class EmailLimitCrossedError(frappe.ValidationError): pass
  19. def send(recipients=None, sender=None, subject=None, message=None, text_content=None, reference_doctype=None,
  20. reference_name=None, unsubscribe_method=None, unsubscribe_params=None, unsubscribe_message=None,
  21. attachments=None, reply_to=None, cc=[], bcc=[], message_id=None, in_reply_to=None, send_after=None,
  22. expose_recipients=None, send_priority=1, communication=None, now=False, read_receipt=None,
  23. queue_separately=False, is_notification=False, add_unsubscribe_link=1, inline_images=None,
  24. header=None):
  25. """Add email to sending queue (Email Queue)
  26. :param recipients: List of recipients.
  27. :param sender: Email sender.
  28. :param subject: Email subject.
  29. :param message: Email message.
  30. :param text_content: Text version of email message.
  31. :param reference_doctype: Reference DocType of caller document.
  32. :param reference_name: Reference name of caller document.
  33. :param send_priority: Priority for Email Queue, default 1.
  34. :param unsubscribe_method: URL method for unsubscribe. Default is `/api/method/frappe.email.queue.unsubscribe`.
  35. :param unsubscribe_params: additional params for unsubscribed links. default are name, doctype, email
  36. :param attachments: Attachments to be sent.
  37. :param reply_to: Reply to be captured here (default inbox)
  38. :param in_reply_to: Used to send the Message-Id of a received email back as In-Reply-To.
  39. :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.
  40. :param communication: Communication link to be set in Email Queue record
  41. :param now: Send immediately (don't send in the background)
  42. :param queue_separately: Queue each email separately
  43. :param is_notification: Marks email as notification so will not trigger notifications from system
  44. :param add_unsubscribe_link: Send unsubscribe link in the footer of the Email, default 1.
  45. :param inline_images: List of inline images as {"filename", "filecontent"}. All src properties will be replaced with random Content-Id
  46. :param header: Append header in email (boolean)
  47. """
  48. if not unsubscribe_method:
  49. unsubscribe_method = "/api/method/frappe.email.queue.unsubscribe"
  50. if not recipients and not cc:
  51. return
  52. if isinstance(recipients, string_types):
  53. recipients = split_emails(recipients)
  54. if isinstance(cc, string_types):
  55. cc = split_emails(cc)
  56. if isinstance(bcc, string_types):
  57. bcc = split_emails(bcc)
  58. if isinstance(send_after, int):
  59. send_after = add_days(nowdate(), send_after)
  60. email_account = get_outgoing_email_account(True, append_to=reference_doctype, sender=sender)
  61. if not sender or sender == "Administrator":
  62. sender = email_account.default_sender
  63. check_email_limit(recipients)
  64. if not text_content:
  65. try:
  66. text_content = html2text(message)
  67. except HTMLParser.HTMLParseError:
  68. text_content = "See html attachment"
  69. if reference_doctype and reference_name:
  70. unsubscribed = [d.email for d in frappe.db.get_all("Email Unsubscribe", "email",
  71. {"reference_doctype": reference_doctype, "reference_name": reference_name})]
  72. unsubscribed += [d.email for d in frappe.db.get_all("Email Unsubscribe", "email",
  73. {"global_unsubscribe": 1})]
  74. else:
  75. unsubscribed = []
  76. recipients = [r for r in list(set(recipients)) if r and r not in unsubscribed]
  77. email_text_context = text_content
  78. should_append_unsubscribe = (add_unsubscribe_link
  79. and reference_doctype
  80. and (unsubscribe_message or reference_doctype=="Newsletter")
  81. and add_unsubscribe_link==1)
  82. unsubscribe_link = None
  83. if should_append_unsubscribe:
  84. unsubscribe_link = get_unsubscribe_message(unsubscribe_message, expose_recipients)
  85. email_text_context += unsubscribe_link.text
  86. email_content = get_formatted_html(subject, message,
  87. email_account=email_account, header=header,
  88. unsubscribe_link=unsubscribe_link)
  89. # add to queue
  90. add(recipients, sender, subject,
  91. formatted=email_content,
  92. text_content=email_text_context,
  93. reference_doctype=reference_doctype,
  94. reference_name=reference_name,
  95. attachments=attachments,
  96. reply_to=reply_to,
  97. cc=cc,
  98. bcc=bcc,
  99. message_id=message_id,
  100. in_reply_to=in_reply_to,
  101. send_after=send_after,
  102. send_priority=send_priority,
  103. email_account=email_account,
  104. communication=communication,
  105. add_unsubscribe_link=add_unsubscribe_link,
  106. unsubscribe_method=unsubscribe_method,
  107. unsubscribe_params=unsubscribe_params,
  108. expose_recipients=expose_recipients,
  109. read_receipt=read_receipt,
  110. queue_separately=queue_separately,
  111. is_notification = is_notification,
  112. inline_images = inline_images,
  113. header=header,
  114. now=now)
  115. def add(recipients, sender, subject, **kwargs):
  116. """Add to Email Queue"""
  117. if kwargs.get('queue_separately') or len(recipients) > 20:
  118. email_queue = None
  119. for r in recipients:
  120. if not email_queue:
  121. email_queue = get_email_queue([r], sender, subject, **kwargs)
  122. if kwargs.get('now'):
  123. email_queue(email_queue.name, now=True)
  124. else:
  125. duplicate = email_queue.get_duplicate([r])
  126. duplicate.insert(ignore_permissions=True)
  127. if kwargs.get('now'):
  128. send_one(duplicate.name, now=True)
  129. frappe.db.commit()
  130. else:
  131. email_queue = get_email_queue(recipients, sender, subject, **kwargs)
  132. if kwargs.get('now'):
  133. send_one(email_queue.name, now=True)
  134. def get_email_queue(recipients, sender, subject, **kwargs):
  135. '''Make Email Queue object'''
  136. e = frappe.new_doc('Email Queue')
  137. e.priority = kwargs.get('send_priority')
  138. attachments = kwargs.get('attachments')
  139. if attachments:
  140. # store attachments with fid or print format details, to be attached on-demand later
  141. _attachments = []
  142. for att in attachments:
  143. if att.get('fid'):
  144. _attachments.append(att)
  145. elif att.get("print_format_attachment") == 1:
  146. _attachments.append(att)
  147. e.attachments = json.dumps(_attachments)
  148. try:
  149. mail = get_email(recipients,
  150. sender=sender,
  151. subject=subject,
  152. formatted=kwargs.get('formatted'),
  153. text_content=kwargs.get('text_content'),
  154. attachments=kwargs.get('attachments'),
  155. reply_to=kwargs.get('reply_to'),
  156. cc=kwargs.get('cc'),
  157. bcc=kwargs.get('bcc'),
  158. email_account=kwargs.get('email_account'),
  159. expose_recipients=kwargs.get('expose_recipients'),
  160. inline_images=kwargs.get('inline_images'),
  161. header=kwargs.get('header'))
  162. mail.set_message_id(kwargs.get('message_id'),kwargs.get('is_notification'))
  163. if kwargs.get('read_receipt'):
  164. mail.msg_root["Disposition-Notification-To"] = sender
  165. if kwargs.get('in_reply_to'):
  166. mail.set_in_reply_to(kwargs.get('in_reply_to'))
  167. e.message_id = mail.msg_root["Message-Id"].strip(" <>")
  168. e.message = cstr(mail.as_string())
  169. e.sender = mail.sender
  170. except frappe.InvalidEmailAddressError:
  171. # bad Email Address - don't add to queue
  172. frappe.log_error('Invalid Email ID Sender: {0}, Recipients: {1}'.format(mail.sender,
  173. ', '.join(mail.recipients)), 'Email Not Sent')
  174. e.set_recipients(recipients + kwargs.get('cc', []) + kwargs.get('bcc', []))
  175. e.reference_doctype = kwargs.get('reference_doctype')
  176. e.reference_name = kwargs.get('reference_name')
  177. e.add_unsubscribe_link = kwargs.get("add_unsubscribe_link")
  178. e.unsubscribe_method = kwargs.get('unsubscribe_method')
  179. e.unsubscribe_params = kwargs.get('unsubscribe_params')
  180. e.expose_recipients = kwargs.get('expose_recipients')
  181. e.communication = kwargs.get('communication')
  182. e.send_after = kwargs.get('send_after')
  183. e.show_as_cc = ",".join(kwargs.get('cc', []))
  184. e.show_as_bcc = ",".join(kwargs.get('bcc', []))
  185. e.insert(ignore_permissions=True)
  186. return e
  187. def check_email_limit(recipients):
  188. # if using settings from site_config.json, check email limit
  189. # No limit for own email settings
  190. smtp_server = SMTPServer()
  191. if (smtp_server.email_account
  192. and getattr(smtp_server.email_account, "from_site_config", False)
  193. or frappe.flags.in_test):
  194. monthly_email_limit = frappe.conf.get('limits', {}).get('emails')
  195. daily_email_limit = cint(frappe.conf.get('limits', {}).get('daily_emails'))
  196. if frappe.flags.in_test:
  197. monthly_email_limit = 500
  198. daily_email_limit = 50
  199. if daily_email_limit:
  200. # get count of sent mails in last 24 hours
  201. today = get_emails_sent_today()
  202. if (today + len(recipients)) > daily_email_limit:
  203. throw(_("Cannot send this email. You have crossed the sending limit of {0} emails for this day.").format(daily_email_limit),
  204. EmailLimitCrossedError)
  205. if not monthly_email_limit:
  206. return
  207. # get count of mails sent this month
  208. this_month = get_emails_sent_this_month()
  209. if (this_month + len(recipients)) > monthly_email_limit:
  210. throw(_("Cannot send this email. You have crossed the sending limit of {0} emails for this month.").format(monthly_email_limit),
  211. EmailLimitCrossedError)
  212. def get_emails_sent_this_month():
  213. return frappe.db.sql("""select count(name) from `tabEmail Queue` where
  214. status='Sent' and MONTH(creation)=MONTH(CURDATE())""")[0][0]
  215. def get_emails_sent_today():
  216. return frappe.db.sql("""select count(name) from `tabEmail Queue` where
  217. status='Sent' and creation>DATE_SUB(NOW(), INTERVAL 24 HOUR)""")[0][0]
  218. def get_unsubscribe_message(unsubscribe_message, expose_recipients):
  219. if unsubscribe_message:
  220. unsubscribe_html = '''<a href="<!--unsubscribe url-->"
  221. target="_blank">{0}</a>'''.format(unsubscribe_message)
  222. else:
  223. unsubscribe_link = '''<a href="<!--unsubscribe url-->"
  224. target="_blank">{0}</a>'''.format(_('Unsubscribe'))
  225. unsubscribe_html = _("{0} to stop receiving emails of this type").format(unsubscribe_link)
  226. html = """<div class="email-unsubscribe">
  227. <!--cc message-->
  228. <div>
  229. {0}
  230. </div>
  231. </div>""".format(unsubscribe_html)
  232. if expose_recipients == "footer":
  233. text = "\n<!--cc message-->"
  234. else:
  235. text = ""
  236. text += "\n\n{unsubscribe_message}: <!--unsubscribe url-->\n".format(unsubscribe_message=unsubscribe_message)
  237. return frappe._dict({
  238. "html": html,
  239. "text": text
  240. })
  241. def get_unsubcribed_url(reference_doctype, reference_name, email, unsubscribe_method, unsubscribe_params):
  242. params = {"email": email.encode("utf-8"),
  243. "doctype": reference_doctype.encode("utf-8"),
  244. "name": reference_name.encode("utf-8")}
  245. if unsubscribe_params:
  246. params.update(unsubscribe_params)
  247. query_string = get_signed_params(params)
  248. # for test
  249. frappe.local.flags.signed_query_string = query_string
  250. return get_url(unsubscribe_method + "?" + get_signed_params(params))
  251. @frappe.whitelist(allow_guest=True)
  252. def unsubscribe(doctype, name, email):
  253. # unsubsribe from comments and communications
  254. if not verify_request():
  255. return
  256. try:
  257. frappe.get_doc({
  258. "doctype": "Email Unsubscribe",
  259. "email": email,
  260. "reference_doctype": doctype,
  261. "reference_name": name
  262. }).insert(ignore_permissions=True)
  263. except frappe.DuplicateEntryError:
  264. frappe.db.rollback()
  265. else:
  266. frappe.db.commit()
  267. return_unsubscribed_page(email, doctype, name)
  268. def return_unsubscribed_page(email, doctype, name):
  269. frappe.respond_as_web_page(_("Unsubscribed"),
  270. _("{0} has left the conversation in {1} {2}").format(email, _(doctype), name),
  271. indicator_color='green')
  272. def flush(from_test=False):
  273. """flush email queue, every time: called from scheduler"""
  274. # additional check
  275. cache = frappe.cache()
  276. check_email_limit([])
  277. auto_commit = not from_test
  278. if frappe.are_emails_muted():
  279. msgprint(_("Emails are muted"))
  280. from_test = True
  281. smtpserver = SMTPServer()
  282. make_cache_queue()
  283. for i in range(cache.llen('cache_email_queue')):
  284. email = cache.lpop('cache_email_queue')
  285. if cint(frappe.defaults.get_defaults().get("hold_queue"))==1:
  286. break
  287. if email:
  288. send_one(email, smtpserver, auto_commit, from_test=from_test)
  289. # NOTE: removing commit here because we pass auto_commit
  290. # finally:
  291. # frappe.db.commit()
  292. def make_cache_queue():
  293. '''cache values in queue before sendign'''
  294. cache = frappe.cache()
  295. emails = frappe.db.sql('''select
  296. name
  297. from
  298. `tabEmail Queue`
  299. where
  300. (status='Not Sent' or status='Partially Sent') and
  301. (send_after is null or send_after < %(now)s)
  302. order
  303. by priority desc, creation asc
  304. limit 500''', { 'now': now_datetime() })
  305. # reset value
  306. cache.delete_value('cache_email_queue')
  307. for e in emails:
  308. cache.rpush('cache_email_queue', e[0])
  309. def send_one(email, smtpserver=None, auto_commit=True, now=False, from_test=False):
  310. '''Send Email Queue with given smtpserver'''
  311. email = frappe.db.sql('''select
  312. name, status, communication, message, sender, reference_doctype,
  313. reference_name, unsubscribe_param, unsubscribe_method, expose_recipients,
  314. show_as_cc, add_unsubscribe_link, attachments
  315. from
  316. `tabEmail Queue`
  317. where
  318. name=%s
  319. for update''', email, as_dict=True)[0]
  320. recipients_list = frappe.db.sql('''select name, recipient, status from
  321. `tabEmail Queue Recipient` where parent=%s''',email.name,as_dict=1)
  322. if frappe.are_emails_muted():
  323. frappe.msgprint(_("Emails are muted"))
  324. return
  325. if cint(frappe.defaults.get_defaults().get("hold_queue"))==1 :
  326. return
  327. if email.status not in ('Not Sent','Partially Sent') :
  328. # rollback to release lock and return
  329. frappe.db.rollback()
  330. return
  331. frappe.db.sql("""update `tabEmail Queue` set status='Sending', modified=%s where name=%s""",
  332. (now_datetime(), email.name), auto_commit=auto_commit)
  333. if email.communication:
  334. frappe.get_doc('Communication', email.communication).set_delivery_status(commit=auto_commit)
  335. try:
  336. if not frappe.flags.in_test:
  337. if not smtpserver: smtpserver = SMTPServer()
  338. smtpserver.setup_email_account(email.reference_doctype, sender=email.sender)
  339. for recipient in recipients_list:
  340. if recipient.status != "Not Sent":
  341. continue
  342. message = prepare_message(email, recipient.recipient, recipients_list)
  343. if not frappe.flags.in_test:
  344. smtpserver.sess.sendmail(email.sender, recipient.recipient, encode(message))
  345. recipient.status = "Sent"
  346. frappe.db.sql("""update `tabEmail Queue Recipient` set status='Sent', modified=%s where name=%s""",
  347. (now_datetime(), recipient.name), auto_commit=auto_commit)
  348. #if all are sent set status
  349. if any("Sent" == s.status for s in recipients_list):
  350. frappe.db.sql("""update `tabEmail Queue` set status='Sent', modified=%s where name=%s""",
  351. (now_datetime(), email.name), auto_commit=auto_commit)
  352. else:
  353. frappe.db.sql("""update `tabEmail Queue` set status='Error', error=%s
  354. where name=%s""", ("No recipients to send to", email.name), auto_commit=auto_commit)
  355. if frappe.flags.in_test:
  356. frappe.flags.sent_mail = message
  357. return
  358. if email.communication:
  359. frappe.get_doc('Communication', email.communication).set_delivery_status(commit=auto_commit)
  360. except (smtplib.SMTPServerDisconnected,
  361. smtplib.SMTPConnectError,
  362. smtplib.SMTPHeloError,
  363. smtplib.SMTPAuthenticationError,
  364. JobTimeoutException):
  365. # bad connection/timeout, retry later
  366. if any("Sent" == s.status for s in recipients_list):
  367. frappe.db.sql("""update `tabEmail Queue` set status='Partially Sent', modified=%s where name=%s""",
  368. (now_datetime(), email.name), auto_commit=auto_commit)
  369. else:
  370. frappe.db.sql("""update `tabEmail Queue` set status='Not Sent', modified=%s where name=%s""",
  371. (now_datetime(), email.name), auto_commit=auto_commit)
  372. if email.communication:
  373. frappe.get_doc('Communication', email.communication).set_delivery_status(commit=auto_commit)
  374. # no need to attempt further
  375. return
  376. except Exception as e:
  377. frappe.db.rollback()
  378. if any("Sent" == s.status for s in recipients_list):
  379. frappe.db.sql("""update `tabEmail Queue` set status='Partially Errored', error=%s where name=%s""",
  380. (text_type(e), email.name), auto_commit=auto_commit)
  381. else:
  382. frappe.db.sql("""update `tabEmail Queue` set status='Error', error=%s
  383. where name=%s""", (text_type(e), email.name), auto_commit=auto_commit)
  384. if email.communication:
  385. frappe.get_doc('Communication', email.communication).set_delivery_status(commit=auto_commit)
  386. if now:
  387. print(frappe.get_traceback())
  388. raise e
  389. else:
  390. # log to Error Log
  391. log('frappe.email.queue.flush', text_type(e))
  392. def prepare_message(email, recipient, recipients_list):
  393. message = email.message
  394. if not message:
  395. return ""
  396. if email.add_unsubscribe_link and email.reference_doctype: # is missing the check for unsubscribe message but will not add as there will be no unsubscribe url
  397. unsubscribe_url = get_unsubcribed_url(email.reference_doctype, email.reference_name, recipient,
  398. email.unsubscribe_method, email.unsubscribe_params)
  399. message = message.replace("<!--unsubscribe url-->", quopri.encodestring(unsubscribe_url.encode()).decode())
  400. if email.expose_recipients == "header":
  401. pass
  402. else:
  403. if email.expose_recipients == "footer":
  404. if isinstance(email.show_as_cc, string_types):
  405. email.show_as_cc = email.show_as_cc.split(",")
  406. email_sent_to = [r.recipient for r in recipients_list]
  407. email_sent_cc = ", ".join([e for e in email_sent_to if e in email.show_as_cc])
  408. email_sent_to = ", ".join([e for e in email_sent_to if e not in email.show_as_cc])
  409. if email_sent_cc:
  410. email_sent_message = _("This email was sent to {0} and copied to {1}").format(email_sent_to,email_sent_cc)
  411. else:
  412. email_sent_message = _("This email was sent to {0}").format(email_sent_to)
  413. message = message.replace("<!--cc message-->", quopri.encodestring(email_sent_message.encode()).decode())
  414. message = message.replace("<!--recipient-->", recipient)
  415. message = (message and message.encode('utf8')) or ''
  416. if not email.attachments:
  417. return message
  418. # On-demand attachments
  419. from email.parser import Parser
  420. msg_obj = Parser().parsestr(message)
  421. attachments = json.loads(email.attachments)
  422. for attachment in attachments:
  423. if attachment.get('fcontent'): continue
  424. fid = attachment.get("fid")
  425. if fid:
  426. fname, fcontent = get_file(fid)
  427. attachment.update({
  428. 'fname': fname,
  429. 'fcontent': fcontent,
  430. 'parent': msg_obj
  431. })
  432. attachment.pop("fid", None)
  433. add_attachment(**attachment)
  434. elif attachment.get("print_format_attachment") == 1:
  435. attachment.pop("print_format_attachment", None)
  436. print_format_file = frappe.attach_print(**attachment)
  437. print_format_file.update({"parent": msg_obj})
  438. add_attachment(**print_format_file)
  439. return msg_obj.as_string()
  440. def clear_outbox():
  441. """Remove low priority older than 31 days in Outbox and expire mails not sent for 7 days.
  442. Called daily via scheduler.
  443. Note: Used separate query to avoid deadlock
  444. """
  445. email_queues = frappe.db.sql_list("""select name from `tabEmail Queue`
  446. where priority=0 and datediff(now(), modified) > 31""")
  447. if email_queues:
  448. frappe.db.sql("""delete from `tabEmail Queue` where name in (%s)"""
  449. % ','.join(['%s']*len(email_queues)), tuple(email_queues))
  450. frappe.db.sql("""delete from `tabEmail Queue Recipient` where parent in (%s)"""
  451. % ','.join(['%s']*len(email_queues)), tuple(email_queues))
  452. frappe.db.sql("""
  453. update `tabEmail Queue`
  454. set status='Expired'
  455. where datediff(curdate(), modified) > 7 and status='Not Sent' and (send_after is null or send_after < %(now)s)""", { 'now': now_datetime() })