You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

329 rivejä
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, expand_partial_links
  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 = expand_partial_links(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. other_footers = webnotes.get_hooks().mail_footer or []
  90. for f in other_footers:
  91. footer += f
  92. return footer
  93. def attach_file(self, n):
  94. """attach a file from the `FileData` table"""
  95. from webnotes.utils.file_manager import get_file
  96. res = get_file(n)
  97. if not res:
  98. return
  99. self.add_attachment(res[0], res[1])
  100. def add_attachment(self, fname, fcontent, content_type=None):
  101. """add attachment"""
  102. from email.mime.audio import MIMEAudio
  103. from email.mime.base import MIMEBase
  104. from email.mime.image import MIMEImage
  105. from email.mime.text import MIMEText
  106. import mimetypes
  107. if not content_type:
  108. content_type, encoding = mimetypes.guess_type(fname)
  109. if content_type is None:
  110. # No guess could be made, or the file is encoded (compressed), so
  111. # use a generic bag-of-bits type.
  112. content_type = 'application/octet-stream'
  113. maintype, subtype = content_type.split('/', 1)
  114. if maintype == 'text':
  115. # Note: we should handle calculating the charset
  116. if isinstance(fcontent, unicode):
  117. fcontent = fcontent.encode("utf-8")
  118. part = MIMEText(fcontent, _subtype=subtype, _charset="utf-8")
  119. elif maintype == 'image':
  120. part = MIMEImage(fcontent, _subtype=subtype)
  121. elif maintype == 'audio':
  122. part = MIMEAudio(fcontent, _subtype=subtype)
  123. else:
  124. part = MIMEBase(maintype, subtype)
  125. part.set_payload(fcontent)
  126. # Encode the payload using Base64
  127. from email import encoders
  128. encoders.encode_base64(part)
  129. # Set the filename parameter
  130. if fname:
  131. part.add_header(b'Content-Disposition',
  132. ("attachment; filename=%s" % fname).encode('utf-8'))
  133. self.msg_root.attach(part)
  134. def validate(self):
  135. """validate the email ids"""
  136. from webnotes.utils import validate_email_add
  137. def _validate(email):
  138. """validate an email field"""
  139. if email and not validate_email_add(email):
  140. webnotes.msgprint("%s is not a valid email id" % email,
  141. raise_exception = 1)
  142. return email
  143. if not self.sender:
  144. self.sender = webnotes.conn.get_value('Email Settings', None,
  145. 'auto_email_id') or conf.get('auto_email_id') or None
  146. if not self.sender:
  147. webnotes.msgprint("""Please specify 'Auto Email Id' \
  148. in Setup > Email Settings""")
  149. if not "expires_on" in conf:
  150. webnotes.msgprint("""Alternatively, \
  151. you can also specify 'auto_email_id' in conf.py""")
  152. raise webnotes.ValidationError
  153. self.sender = _validate(self.sender)
  154. self.reply_to = _validate(self.reply_to)
  155. for e in self.recipients + (self.cc or []):
  156. _validate(e.strip())
  157. def make(self):
  158. """build into msg_root"""
  159. self.msg_root['Subject'] = self.subject.encode("utf-8")
  160. self.msg_root['From'] = self.sender.encode("utf-8")
  161. self.msg_root['To'] = ', '.join([r.strip() for r in self.recipients]).encode("utf-8")
  162. self.msg_root['Date'] = email.utils.formatdate()
  163. if not self.reply_to:
  164. self.reply_to = self.sender
  165. self.msg_root['Reply-To'] = self.reply_to.encode("utf-8")
  166. if self.cc:
  167. self.msg_root['CC'] = ', '.join([r.strip() for r in self.cc]).encode("utf-8")
  168. def as_string(self):
  169. """validate, build message and convert to string"""
  170. self.validate()
  171. self.make()
  172. return self.msg_root.as_string()
  173. def send(self, as_bulk=False):
  174. """send the message or add it to Outbox Email"""
  175. if webnotes.flags.mute_emails or conf.get("mute_emails") or False:
  176. webnotes.msgprint("Emails are muted")
  177. return
  178. import smtplib
  179. try:
  180. smtpserver = SMTPServer()
  181. if hasattr(smtpserver, "always_use_login_id_as_sender") and \
  182. cint(smtpserver.always_use_login_id_as_sender) and smtpserver.login:
  183. if not self.reply_to:
  184. self.reply_to = self.sender
  185. self.sender = smtpserver.login
  186. smtpserver.sess.sendmail(self.sender, self.recipients + (self.cc or []),
  187. self.as_string())
  188. except smtplib.SMTPSenderRefused:
  189. webnotes.msgprint("""Invalid Outgoing Mail Server's Login Id or Password. \
  190. Please rectify and try again.""")
  191. raise
  192. except smtplib.SMTPRecipientsRefused:
  193. webnotes.msgprint("""Invalid Recipient (To) Email Address. \
  194. Please rectify and try again.""")
  195. raise
  196. class SMTPServer:
  197. def __init__(self, login=None, password=None, server=None, port=None, use_ssl=None):
  198. import webnotes.model.doc
  199. from webnotes.utils import cint
  200. # get defaults from control panel
  201. try:
  202. es = webnotes.model.doc.Document('Email Settings','Email Settings')
  203. except webnotes.DoesNotExistError:
  204. es = None
  205. self._sess = None
  206. if server:
  207. self.server = server
  208. self.port = port
  209. self.use_ssl = cint(use_ssl)
  210. self.login = login
  211. self.password = password
  212. elif es and es.outgoing_mail_server:
  213. self.server = es.outgoing_mail_server
  214. self.port = es.mail_port
  215. self.use_ssl = cint(es.use_ssl)
  216. self.login = es.mail_login
  217. self.password = es.mail_password
  218. self.always_use_login_id_as_sender = es.always_use_login_id_as_sender
  219. else:
  220. self.server = conf.get("mail_server") or ""
  221. self.port = conf.get("mail_port") or None
  222. self.use_ssl = cint(conf.get("use_ssl") or 0)
  223. self.login = conf.get("mail_login") or ""
  224. self.password = conf.get("mail_password") or ""
  225. @property
  226. def sess(self):
  227. """get session"""
  228. if self._sess:
  229. return self._sess
  230. from webnotes.utils import cint
  231. import smtplib
  232. import _socket
  233. # check if email server specified
  234. if not self.server:
  235. err_msg = 'Outgoing Mail Server not specified'
  236. webnotes.msgprint(err_msg)
  237. raise webnotes.OutgoingEmailError, err_msg
  238. try:
  239. if self.use_ssl and not self.port:
  240. self.port = 587
  241. self._sess = smtplib.SMTP((self.server or "").encode('utf-8'),
  242. cint(self.port) or None)
  243. if not self._sess:
  244. err_msg = 'Could not connect to outgoing email server'
  245. webnotes.msgprint(err_msg)
  246. raise webnotes.OutgoingEmailError, err_msg
  247. if self.use_ssl:
  248. self._sess.ehlo()
  249. self._sess.starttls()
  250. self._sess.ehlo()
  251. if self.login:
  252. ret = self._sess.login((self.login or "").encode('utf-8'),
  253. (self.password or "").encode('utf-8'))
  254. # check if logged correctly
  255. if ret[0]!=235:
  256. msgprint(ret[1])
  257. raise webnotes.OutgoingEmailError, ret[1]
  258. return self._sess
  259. except _socket.error:
  260. # Invalid mail server -- due to refusing connection
  261. webnotes.msgprint('Invalid Outgoing Mail Server or Port. Please rectify and try again.')
  262. raise
  263. except smtplib.SMTPAuthenticationError:
  264. webnotes.msgprint("Invalid Outgoing Mail Server's Login Id or Password. \
  265. Please rectify and try again.")
  266. raise
  267. except smtplib.SMTPException:
  268. webnotes.msgprint('There is something wrong with your Outgoing Mail Settings. \
  269. Please contact us at support@erpnext.com')
  270. raise