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

330 行
10 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. """
  5. Sends email via outgoing server specified in "Control Panel"
  6. Allows easy adding of Attachments of "File" objects
  7. """
  8. import webnotes
  9. from webnotes import conf
  10. from webnotes import msgprint
  11. from webnotes.utils import cint, scrub_urls
  12. import email.utils
  13. class OutgoingEmailError(webnotes.ValidationError): pass
  14. def get_email(recipients, sender='', msg='', subject='[No Subject]', text_content = None, footer=None):
  15. """send an html email as multipart with attachments and all"""
  16. email = EMail(sender, recipients, subject)
  17. if (not '<br>' in msg) and (not '<p>' in msg) and (not '<div' in msg):
  18. msg = msg.replace('\n', '<br>')
  19. email.set_html(msg, text_content, footer=footer)
  20. return email
  21. class EMail:
  22. """
  23. Wrapper on the email module. Email object represents emails to be sent to the client.
  24. Also provides a clean way to add binary `FileData` attachments
  25. Also sets all messages as multipart/alternative for cleaner reading in text-only clients
  26. """
  27. def __init__(self, sender='', recipients=[], subject='', alternative=0, reply_to=None):
  28. from email.mime.multipart import MIMEMultipart
  29. from email import Charset
  30. Charset.add_charset('utf-8', Charset.QP, Charset.QP, 'utf-8')
  31. if isinstance(recipients, basestring):
  32. recipients = recipients.replace(';', ',').replace('\n', '')
  33. recipients = recipients.split(',')
  34. # remove null
  35. recipients = filter(None, (r.strip() for r in recipients))
  36. self.sender = sender
  37. self.reply_to = reply_to or sender
  38. self.recipients = recipients
  39. self.subject = subject
  40. self.msg_root = MIMEMultipart('mixed')
  41. self.msg_multipart = MIMEMultipart('alternative')
  42. self.msg_root.attach(self.msg_multipart)
  43. self.cc = []
  44. self.html_set = False
  45. def set_html(self, message, text_content = None, footer=None):
  46. """Attach message in the html portion of multipart/alternative"""
  47. message = message + self.get_footer(footer)
  48. message = scrub_urls(message)
  49. # this is the first html part of a multi-part message,
  50. # convert to text well
  51. if not self.html_set:
  52. if text_content:
  53. self.set_text(text_content)
  54. else:
  55. self.set_html_as_text(message)
  56. self.set_part_html(message)
  57. self.html_set = True
  58. def set_text(self, message):
  59. """
  60. Attach message in the text portion of multipart/alternative
  61. """
  62. from email.mime.text import MIMEText
  63. part = MIMEText(message.encode('utf-8'), 'plain', 'utf-8')
  64. self.msg_multipart.attach(part)
  65. def set_part_html(self, message):
  66. from email.mime.text import MIMEText
  67. part = MIMEText(message.encode('utf-8'), 'html', 'utf-8')
  68. self.msg_multipart.attach(part)
  69. def set_html_as_text(self, html):
  70. """return html2text"""
  71. import HTMLParser
  72. from webnotes.utils.email_lib.html2text import html2text
  73. try:
  74. self.set_text(html2text(html))
  75. except HTMLParser.HTMLParseError:
  76. pass
  77. def set_message(self, message, mime_type='text/html', as_attachment=0, filename='attachment.html'):
  78. """Append the message with MIME content to the root node (as attachment)"""
  79. from email.mime.text import MIMEText
  80. maintype, subtype = mime_type.split('/')
  81. part = MIMEText(message, _subtype = subtype)
  82. if as_attachment:
  83. part.add_header('Content-Disposition', 'attachment', filename=filename)
  84. self.msg_root.attach(part)
  85. def get_footer(self, footer=None):
  86. """append a footer (signature)"""
  87. footer = footer or ""
  88. footer += webnotes.conn.get_value('Control Panel',None,'mail_footer') or ''
  89. try:
  90. import startup
  91. footer += getattr(startup, 'mail_footer', '')
  92. except ImportError:
  93. pass
  94. return footer
  95. def attach_file(self, n):
  96. """attach a file from the `FileData` table"""
  97. from webnotes.utils.file_manager import get_file
  98. res = get_file(n)
  99. if not res:
  100. return
  101. self.add_attachment(res[0], res[1])
  102. def add_attachment(self, fname, fcontent, content_type=None):
  103. """add attachment"""
  104. from email.mime.audio import MIMEAudio
  105. from email.mime.base import MIMEBase
  106. from email.mime.image import MIMEImage
  107. from email.mime.text import MIMEText
  108. import mimetypes
  109. if not content_type:
  110. content_type, encoding = mimetypes.guess_type(fname)
  111. if content_type is None:
  112. # No guess could be made, or the file is encoded (compressed), so
  113. # use a generic bag-of-bits type.
  114. content_type = 'application/octet-stream'
  115. maintype, subtype = content_type.split('/', 1)
  116. if maintype == 'text':
  117. # Note: we should handle calculating the charset
  118. if isinstance(fcontent, unicode):
  119. fcontent = fcontent.encode("utf-8")
  120. part = MIMEText(fcontent, _subtype=subtype, _charset="utf-8")
  121. elif maintype == 'image':
  122. part = MIMEImage(fcontent, _subtype=subtype)
  123. elif maintype == 'audio':
  124. part = MIMEAudio(fcontent, _subtype=subtype)
  125. else:
  126. part = MIMEBase(maintype, subtype)
  127. part.set_payload(fcontent)
  128. # Encode the payload using Base64
  129. from email import encoders
  130. encoders.encode_base64(part)
  131. # Set the filename parameter
  132. if fname:
  133. part.add_header(b'Content-Disposition',
  134. ("attachment; filename=%s" % fname).encode('utf-8'))
  135. self.msg_root.attach(part)
  136. def validate(self):
  137. """validate the email ids"""
  138. from webnotes.utils import validate_email_add
  139. def _validate(email):
  140. """validate an email field"""
  141. if email and not validate_email_add(email):
  142. webnotes.msgprint("%s is not a valid email id" % email,
  143. raise_exception = 1)
  144. return email
  145. if not self.sender:
  146. self.sender = webnotes.conn.get_value('Email Settings', None,
  147. 'auto_email_id') or conf.get('auto_email_id') or None
  148. if not self.sender:
  149. webnotes.msgprint("""Please specify 'Auto Email Id' \
  150. in Setup > Email Settings""")
  151. if not "expires_on" in conf:
  152. webnotes.msgprint("""Alternatively, \
  153. you can also specify 'auto_email_id' in conf.py""")
  154. raise webnotes.ValidationError
  155. self.sender = _validate(self.sender)
  156. self.reply_to = _validate(self.reply_to)
  157. for e in self.recipients + (self.cc or []):
  158. _validate(e.strip())
  159. def make(self):
  160. """build into msg_root"""
  161. self.msg_root['Subject'] = self.subject.encode("utf-8")
  162. self.msg_root['From'] = self.sender.encode("utf-8")
  163. self.msg_root['To'] = ', '.join([r.strip() for r in self.recipients]).encode("utf-8")
  164. self.msg_root['Date'] = email.utils.formatdate()
  165. if not self.reply_to:
  166. self.reply_to = self.sender
  167. self.msg_root['Reply-To'] = self.reply_to.encode("utf-8")
  168. if self.cc:
  169. self.msg_root['CC'] = ', '.join([r.strip() for r in self.cc]).encode("utf-8")
  170. def as_string(self):
  171. """validate, build message and convert to string"""
  172. self.validate()
  173. self.make()
  174. return self.msg_root.as_string()
  175. def send(self, as_bulk=False):
  176. """send the message or add it to Outbox Email"""
  177. if webnotes.flags.mute_emails or conf.get("mute_emails") or False:
  178. webnotes.msgprint("Emails are muted")
  179. return
  180. import smtplib
  181. try:
  182. smtpserver = SMTPServer()
  183. if hasattr(smtpserver, "always_use_login_id_as_sender") and \
  184. cint(smtpserver.always_use_login_id_as_sender) and smtpserver.login:
  185. if not self.reply_to:
  186. self.reply_to = self.sender
  187. self.sender = smtpserver.login
  188. smtpserver.sess.sendmail(self.sender, self.recipients + (self.cc or []),
  189. self.as_string())
  190. except smtplib.SMTPSenderRefused:
  191. webnotes.msgprint("""Invalid Outgoing Mail Server's Login Id or Password. \
  192. Please rectify and try again.""")
  193. raise
  194. except smtplib.SMTPRecipientsRefused:
  195. webnotes.msgprint("""Invalid Recipient (To) Email Address. \
  196. Please rectify and try again.""")
  197. raise
  198. class SMTPServer:
  199. def __init__(self, login=None, password=None, server=None, port=None, use_ssl=None):
  200. import webnotes.model.doc
  201. from webnotes.utils import cint
  202. # get defaults from control panel
  203. try:
  204. es = webnotes.model.doc.Document('Email Settings','Email Settings')
  205. except webnotes.DoesNotExistError:
  206. es = None
  207. self._sess = None
  208. if server:
  209. self.server = server
  210. self.port = port
  211. self.use_ssl = cint(use_ssl)
  212. self.login = login
  213. self.password = password
  214. elif es and es.outgoing_mail_server:
  215. self.server = es.outgoing_mail_server
  216. self.port = es.mail_port
  217. self.use_ssl = cint(es.use_ssl)
  218. self.login = es.mail_login
  219. self.password = es.mail_password
  220. self.always_use_login_id_as_sender = es.always_use_login_id_as_sender
  221. else:
  222. self.server = conf.get("mail_server") or ""
  223. self.port = conf.get("mail_port") or None
  224. self.use_ssl = cint(conf.get("use_ssl") or 0)
  225. self.login = conf.get("mail_login") or ""
  226. self.password = conf.get("mail_password") or ""
  227. @property
  228. def sess(self):
  229. """get session"""
  230. if self._sess:
  231. return self._sess
  232. from webnotes.utils import cint
  233. import smtplib
  234. import _socket
  235. # check if email server specified
  236. if not self.server:
  237. err_msg = 'Outgoing Mail Server not specified'
  238. webnotes.msgprint(err_msg)
  239. raise webnotes.OutgoingEmailError, err_msg
  240. try:
  241. if self.use_ssl and not self.port:
  242. self.port = 587
  243. self._sess = smtplib.SMTP((self.server or "").encode('utf-8'),
  244. cint(self.port) or None)
  245. if not self._sess:
  246. err_msg = 'Could not connect to outgoing email server'
  247. webnotes.msgprint(err_msg)
  248. raise webnotes.OutgoingEmailError, err_msg
  249. if self.use_ssl:
  250. self._sess.ehlo()
  251. self._sess.starttls()
  252. self._sess.ehlo()
  253. if self.login:
  254. ret = self._sess.login((self.login or "").encode('utf-8'),
  255. (self.password or "").encode('utf-8'))
  256. # check if logged correctly
  257. if ret[0]!=235:
  258. msgprint(ret[1])
  259. raise webnotes.OutgoingEmailError, ret[1]
  260. return self._sess
  261. except _socket.error:
  262. # Invalid mail server -- due to refusing connection
  263. webnotes.msgprint('Invalid Outgoing Mail Server or Port. Please rectify and try again.')
  264. raise
  265. except smtplib.SMTPAuthenticationError:
  266. webnotes.msgprint("Invalid Outgoing Mail Server's Login Id or Password. \
  267. Please rectify and try again.")
  268. raise
  269. except smtplib.SMTPException:
  270. webnotes.msgprint('There is something wrong with your Outgoing Mail Settings. \
  271. Please contact us at support@erpnext.com')
  272. raise