Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.
 
 
 
 
 
 

309 wiersze
10 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, re
  5. from frappe.utils.pdf import get_pdf
  6. from frappe.email.smtp import get_outgoing_email_account
  7. from frappe.utils import (get_url, scrub_urls, strip, expand_relative_urls, cint,
  8. split_emails, to_markdown, markdown, encode, random_string, parse_addr)
  9. import email.utils
  10. from six import iteritems
  11. from email.mime.multipart import MIMEMultipart
  12. def get_email(recipients, sender='', msg='', subject='[No Subject]',
  13. text_content = None, footer=None, print_html=None, formatted=None, attachments=None,
  14. content=None, reply_to=None, cc=[], email_account=None, expose_recipients=None,
  15. inline_images=[]):
  16. """send an html email as multipart with attachments and all"""
  17. content = content or msg
  18. emailobj = EMail(sender, recipients, subject, reply_to=reply_to, cc=cc, email_account=email_account, expose_recipients=expose_recipients)
  19. if not content.strip().startswith("<"):
  20. content = markdown(content)
  21. emailobj.set_html(content, text_content, footer=footer,
  22. print_html=print_html, formatted=formatted, inline_images=inline_images)
  23. if isinstance(attachments, dict):
  24. attachments = [attachments]
  25. for attach in (attachments or []):
  26. emailobj.add_attachment(**attach)
  27. return emailobj
  28. class EMail:
  29. """
  30. Wrapper on the email module. Email object represents emails to be sent to the client.
  31. Also provides a clean way to add binary `FileData` attachments
  32. Also sets all messages as multipart/alternative for cleaner reading in text-only clients
  33. """
  34. def __init__(self, sender='', recipients=(), subject='', alternative=0, reply_to=None, cc=(), email_account=None, expose_recipients=None):
  35. from email import Charset
  36. Charset.add_charset('utf-8', Charset.QP, Charset.QP, 'utf-8')
  37. if isinstance(recipients, basestring):
  38. recipients = recipients.replace(';', ',').replace('\n', '')
  39. recipients = split_emails(recipients)
  40. # remove null
  41. recipients = filter(None, (strip(r) for r in recipients))
  42. self.sender = sender
  43. self.reply_to = reply_to or sender
  44. self.recipients = recipients
  45. self.subject = subject
  46. self.expose_recipients = expose_recipients
  47. self.msg_root = MIMEMultipart('mixed')
  48. self.msg_multipart = MIMEMultipart('alternative')
  49. self.msg_root.attach(self.msg_multipart)
  50. self.cc = cc or []
  51. self.html_set = False
  52. self.email_account = email_account or get_outgoing_email_account()
  53. def set_html(self, message, text_content = None, footer=None, print_html=None,
  54. formatted=None, inline_images=None):
  55. """Attach message in the html portion of multipart/alternative"""
  56. if not formatted:
  57. formatted = get_formatted_html(self.subject, message, footer, print_html, email_account=self.email_account)
  58. # this is the first html part of a multi-part message,
  59. # convert to text well
  60. if not self.html_set:
  61. if text_content:
  62. self.set_text(expand_relative_urls(text_content))
  63. else:
  64. self.set_html_as_text(expand_relative_urls(formatted))
  65. self.set_part_html(formatted, inline_images)
  66. self.html_set = True
  67. def set_text(self, message):
  68. """
  69. Attach message in the text portion of multipart/alternative
  70. """
  71. from email.mime.text import MIMEText
  72. part = MIMEText(message, 'plain', 'utf-8')
  73. self.msg_multipart.attach(part)
  74. def set_part_html(self, message, inline_images):
  75. from email.mime.text import MIMEText
  76. if inline_images:
  77. related = MIMEMultipart('related')
  78. for image in inline_images:
  79. # images in dict like {filename:'', filecontent:'raw'}
  80. content_id = random_string(10)
  81. # replace filename in message with CID
  82. message = re.sub('''src=['"]{0}['"]'''.format(image.get('filename')),
  83. 'src="cid:{0}"'.format(content_id), message)
  84. self.add_attachment(image.get('filename'), image.get('filecontent'),
  85. None, content_id=content_id, parent=related)
  86. html_part = MIMEText(message, 'html', 'utf-8')
  87. related.attach(html_part)
  88. self.msg_multipart.attach(related)
  89. else:
  90. self.msg_multipart.attach(MIMEText(message, 'html', 'utf-8'))
  91. def set_html_as_text(self, html):
  92. """return html2text"""
  93. self.set_text(to_markdown(html))
  94. def set_message(self, message, mime_type='text/html', as_attachment=0, filename='attachment.html'):
  95. """Append the message with MIME content to the root node (as attachment)"""
  96. from email.mime.text import MIMEText
  97. maintype, subtype = mime_type.split('/')
  98. part = MIMEText(message, _subtype = subtype)
  99. if as_attachment:
  100. part.add_header('Content-Disposition', 'attachment', filename=filename)
  101. self.msg_root.attach(part)
  102. def attach_file(self, n):
  103. """attach a file from the `FileData` table"""
  104. from frappe.utils.file_manager import get_file
  105. res = get_file(n)
  106. if not res:
  107. return
  108. self.add_attachment(res[0], res[1])
  109. def add_attachment(self, fname, fcontent, content_type=None,
  110. parent=None, content_id=None):
  111. """add attachment"""
  112. from email.mime.audio import MIMEAudio
  113. from email.mime.base import MIMEBase
  114. from email.mime.image import MIMEImage
  115. from email.mime.text import MIMEText
  116. import mimetypes
  117. if not content_type:
  118. content_type, encoding = mimetypes.guess_type(fname)
  119. if content_type is None:
  120. # No guess could be made, or the file is encoded (compressed), so
  121. # use a generic bag-of-bits type.
  122. content_type = 'application/octet-stream'
  123. maintype, subtype = content_type.split('/', 1)
  124. if maintype == 'text':
  125. # Note: we should handle calculating the charset
  126. if isinstance(fcontent, unicode):
  127. fcontent = fcontent.encode("utf-8")
  128. part = MIMEText(fcontent, _subtype=subtype, _charset="utf-8")
  129. elif maintype == 'image':
  130. part = MIMEImage(fcontent, _subtype=subtype)
  131. elif maintype == 'audio':
  132. part = MIMEAudio(fcontent, _subtype=subtype)
  133. else:
  134. part = MIMEBase(maintype, subtype)
  135. part.set_payload(fcontent)
  136. # Encode the payload using Base64
  137. from email import encoders
  138. encoders.encode_base64(part)
  139. # Set the filename parameter
  140. if fname:
  141. part.add_header(b'Content-Disposition',
  142. ("attachment; filename=\"%s\"" % fname).encode('utf-8'))
  143. if content_id:
  144. part.add_header(b'Content-ID', '<{0}>'.format(content_id))
  145. if not parent:
  146. parent = self.msg_root
  147. parent.attach(part)
  148. def add_pdf_attachment(self, name, html, options=None):
  149. self.add_attachment(name, get_pdf(html, options), 'application/octet-stream')
  150. def validate(self):
  151. """validate the Email Addresses"""
  152. from frappe.utils import validate_email_add
  153. if not self.sender:
  154. self.sender = self.email_account.default_sender
  155. validate_email_add(strip(self.sender), True)
  156. self.reply_to = validate_email_add(strip(self.reply_to) or self.sender, True)
  157. self.replace_sender()
  158. self.recipients = [strip(r) for r in self.recipients]
  159. self.cc = [strip(r) for r in self.cc]
  160. for e in self.recipients + (self.cc or []):
  161. validate_email_add(e, True)
  162. def replace_sender(self):
  163. if cint(self.email_account.always_use_account_email_id_as_sender):
  164. self.set_header('X-Original-From', self.sender)
  165. sender_name, sender_email = parse_addr(self.sender)
  166. self.sender = email.utils.formataddr((sender_name or self.email_account.name, self.email_account.email_id))
  167. def set_message_id(self, message_id, is_notification=False):
  168. if message_id:
  169. self.msg_root["Message-Id"] = '<' + message_id + '>'
  170. else:
  171. self.msg_root["Message-Id"] = get_message_id()
  172. self.msg_root["isnotification"] = '<notification>'
  173. if is_notification:
  174. self.msg_root["isnotification"] = '<notification>'
  175. def set_in_reply_to(self, in_reply_to):
  176. """Used to send the Message-Id of a received email back as In-Reply-To"""
  177. self.msg_root["In-Reply-To"] = in_reply_to
  178. def make(self):
  179. """build into msg_root"""
  180. headers = {
  181. "Subject": strip(self.subject),
  182. "From": self.sender,
  183. "To": ', '.join(self.recipients) if self.expose_recipients=="header" else "<!--recipient-->",
  184. "Date": email.utils.formatdate(),
  185. "Reply-To": self.reply_to if self.reply_to else None,
  186. "CC": ', '.join(self.cc) if self.cc and self.expose_recipients=="header" else None,
  187. 'X-Frappe-Site': get_url(),
  188. }
  189. # reset headers as values may be changed.
  190. for key, val in iteritems(headers):
  191. self.set_header(key, val)
  192. # call hook to enable apps to modify msg_root before sending
  193. for hook in frappe.get_hooks("make_email_body_message"):
  194. frappe.get_attr(hook)(self)
  195. def set_header(self, key, value):
  196. key = encode(key)
  197. value = encode(value)
  198. if self.msg_root.has_key(key):
  199. del self.msg_root[key]
  200. self.msg_root[key] = value
  201. def as_string(self):
  202. """validate, build message and convert to string"""
  203. self.validate()
  204. self.make()
  205. return self.msg_root.as_string()
  206. def get_formatted_html(subject, message, footer=None, print_html=None, email_account=None):
  207. if not email_account:
  208. email_account = get_outgoing_email_account(False)
  209. rendered_email = frappe.get_template("templates/emails/standard.html").render({
  210. "content": message,
  211. "signature": get_signature(email_account),
  212. "footer": get_footer(email_account, footer),
  213. "title": subject,
  214. "print_html": print_html,
  215. "subject": subject
  216. })
  217. return scrub_urls(rendered_email)
  218. def get_message_id():
  219. '''Returns Message ID created from doctype and name'''
  220. return "<{unique}@{site}>".format(
  221. site=frappe.local.site,
  222. unique=email.utils.make_msgid(random_string(10)).split('@')[0].split('<')[1])
  223. def get_signature(email_account):
  224. if email_account and email_account.add_signature and email_account.signature:
  225. return "<br><br>" + email_account.signature
  226. else:
  227. return ""
  228. def get_footer(email_account, footer=None):
  229. """append a footer (signature)"""
  230. footer = footer or ""
  231. if email_account and email_account.footer:
  232. footer += '<div style="margin: 15px auto;">{0}</div>'.format(email_account.footer)
  233. footer += "<!--unsubscribe link here-->"
  234. company_address = frappe.db.get_default("email_footer_address")
  235. if company_address:
  236. footer += '<div style="margin: 15px auto; text-align: center; color: #8d99a6">{0}</div>'\
  237. .format(company_address.replace("\n", "<br>"))
  238. if not cint(frappe.db.get_default("disable_standard_email_footer")):
  239. for default_mail_footer in frappe.get_hooks("default_mail_footer"):
  240. footer += '<div style="margin: 15px auto;">{0}</div>'.format(default_mail_footer)
  241. return footer