25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

223 lines
6.9 KiB

  1. # Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors
  2. # MIT License. See license.txt
  3. from __future__ import unicode_literals
  4. import frappe
  5. from frappe import msgprint, throw, _
  6. from frappe.utils import scrub_urls, cstr
  7. import email.utils
  8. from markdown2 import markdown
  9. def get_email(recipients, sender='', msg='', subject='[No Subject]',
  10. text_content = None, footer=None, print_html=None, formatted=None):
  11. """send an html email as multipart with attachments and all"""
  12. emailobj = EMail(sender, recipients, subject)
  13. msg = markdown(msg)
  14. emailobj.set_html(msg, text_content, footer=footer, print_html=print_html, formatted=formatted)
  15. return emailobj
  16. class EMail:
  17. """
  18. Wrapper on the email module. Email object represents emails to be sent to the client.
  19. Also provides a clean way to add binary `FileData` attachments
  20. Also sets all messages as multipart/alternative for cleaner reading in text-only clients
  21. """
  22. def __init__(self, sender='', recipients=[], subject='', alternative=0, reply_to=None):
  23. from email.mime.multipart import MIMEMultipart
  24. from email import Charset
  25. Charset.add_charset('utf-8', Charset.QP, Charset.QP, 'utf-8')
  26. if isinstance(recipients, basestring):
  27. recipients = recipients.replace(';', ',').replace('\n', '')
  28. recipients = recipients.split(',')
  29. # remove null
  30. recipients = filter(None, (r.strip() for r in recipients))
  31. self.sender = sender
  32. self.reply_to = reply_to or sender
  33. self.recipients = recipients
  34. self.subject = subject
  35. self.msg_root = MIMEMultipart('mixed')
  36. self.msg_multipart = MIMEMultipart('alternative')
  37. self.msg_root.attach(self.msg_multipart)
  38. self.cc = []
  39. self.html_set = False
  40. def set_html(self, message, text_content = None, footer=None, print_html=None, formatted=None):
  41. """Attach message in the html portion of multipart/alternative"""
  42. if not formatted:
  43. formatted = get_formatted_html(self.subject, message, footer, print_html)
  44. # this is the first html part of a multi-part message,
  45. # convert to text well
  46. if not self.html_set:
  47. if text_content:
  48. self.set_text(text_content)
  49. else:
  50. self.set_html_as_text(formatted)
  51. self.set_part_html(formatted)
  52. self.html_set = True
  53. def set_text(self, message):
  54. """
  55. Attach message in the text portion of multipart/alternative
  56. """
  57. from email.mime.text import MIMEText
  58. part = MIMEText(message, 'plain', 'utf-8')
  59. self.msg_multipart.attach(part)
  60. def set_part_html(self, message):
  61. from email.mime.text import MIMEText
  62. part = MIMEText(message, 'html', 'utf-8')
  63. self.msg_multipart.attach(part)
  64. def set_html_as_text(self, html):
  65. """return html2text"""
  66. import HTMLParser
  67. from frappe.utils.email_lib.html2text import html2text
  68. try:
  69. self.set_text(html2text(html))
  70. except HTMLParser.HTMLParseError:
  71. pass
  72. def set_message(self, message, mime_type='text/html', as_attachment=0, filename='attachment.html'):
  73. """Append the message with MIME content to the root node (as attachment)"""
  74. from email.mime.text import MIMEText
  75. maintype, subtype = mime_type.split('/')
  76. part = MIMEText(message, _subtype = subtype)
  77. if as_attachment:
  78. part.add_header('Content-Disposition', 'attachment', filename=filename)
  79. self.msg_root.attach(part)
  80. def attach_file(self, n):
  81. """attach a file from the `FileData` table"""
  82. from frappe.utils.file_manager import get_file
  83. res = get_file(n)
  84. if not res:
  85. return
  86. self.add_attachment(res[0], res[1])
  87. def add_attachment(self, fname, fcontent, content_type=None):
  88. """add attachment"""
  89. from email.mime.audio import MIMEAudio
  90. from email.mime.base import MIMEBase
  91. from email.mime.image import MIMEImage
  92. from email.mime.text import MIMEText
  93. import mimetypes
  94. if not content_type:
  95. content_type, encoding = mimetypes.guess_type(fname)
  96. if content_type is None:
  97. # No guess could be made, or the file is encoded (compressed), so
  98. # use a generic bag-of-bits type.
  99. content_type = 'application/octet-stream'
  100. maintype, subtype = content_type.split('/', 1)
  101. if maintype == 'text':
  102. # Note: we should handle calculating the charset
  103. if isinstance(fcontent, unicode):
  104. fcontent = fcontent.encode("utf-8")
  105. part = MIMEText(fcontent, _subtype=subtype, _charset="utf-8")
  106. elif maintype == 'image':
  107. part = MIMEImage(fcontent, _subtype=subtype)
  108. elif maintype == 'audio':
  109. part = MIMEAudio(fcontent, _subtype=subtype)
  110. else:
  111. part = MIMEBase(maintype, subtype)
  112. part.set_payload(fcontent)
  113. # Encode the payload using Base64
  114. from email import encoders
  115. encoders.encode_base64(part)
  116. # Set the filename parameter
  117. if fname:
  118. part.add_header(b'Content-Disposition',
  119. ("attachment; filename=%s" % fname).encode('utf-8'))
  120. self.msg_root.attach(part)
  121. def validate(self):
  122. """validate the email ids"""
  123. from frappe.utils import validate_email_add
  124. def _validate(email):
  125. """validate an email field"""
  126. if email and not validate_email_add(email):
  127. throw(_("{0} is not a valid email id").format(email))
  128. return email
  129. if not self.sender:
  130. self.sender = frappe.db.get_value('Outgoing Email Settings', None,
  131. 'auto_email_id') or frappe.conf.get('auto_email_id') or None
  132. if not self.sender:
  133. msgprint(_("Please specify 'Auto Email Id' in Setup > Outgoing Email Settings"))
  134. if not "expires_on" in frappe.conf:
  135. msgprint(_("Alternatively, you can also specify 'auto_email_id' in site_config.json"))
  136. raise frappe.ValidationError
  137. self.sender = _validate(self.sender)
  138. self.reply_to = _validate(self.reply_to)
  139. for e in self.recipients + (self.cc or []):
  140. _validate(e.strip())
  141. def make(self):
  142. """build into msg_root"""
  143. self.msg_root['Subject'] = self.subject.encode("utf-8")
  144. self.msg_root['From'] = self.sender.encode("utf-8")
  145. self.msg_root['To'] = ', '.join([r.strip() for r in self.recipients]).encode("utf-8")
  146. self.msg_root['Date'] = email.utils.formatdate()
  147. if not self.reply_to:
  148. self.reply_to = self.sender
  149. self.msg_root['Reply-To'] = self.reply_to.encode("utf-8")
  150. if self.cc:
  151. self.msg_root['CC'] = ', '.join([r.strip() for r in self.cc]).encode("utf-8")
  152. def as_string(self):
  153. """validate, build message and convert to string"""
  154. self.validate()
  155. self.make()
  156. return self.msg_root.as_string()
  157. def get_formatted_html(subject, message, footer=None, print_html=None):
  158. # imported here to avoid cyclic import
  159. message = scrub_urls(message)
  160. rendered_email = frappe.get_template("templates/emails/standard.html").render({
  161. "content": message,
  162. "footer": get_footer(footer),
  163. "title": subject,
  164. "print_html": print_html,
  165. "subject": subject
  166. })
  167. # if in a test case, do not inline css
  168. if frappe.local.flags.in_test:
  169. return rendered_email
  170. return rendered_email
  171. def get_footer(footer=None):
  172. """append a footer (signature)"""
  173. footer = footer or ""
  174. # control panel
  175. footer += frappe.db.get_default('mail_footer') or ''
  176. # hooks
  177. for f in frappe.get_hooks("mail_footer"):
  178. footer += frappe.get_attr(f)
  179. footer += "<!--unsubscribe link here-->"
  180. return footer