CAT-SOOP is a flexible, programmable learning management system based on the Python programming language. https://catsoop.mit.edu
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.
 
 
 

238 lines
8.3 KiB

  1. # This file is part of CAT-SOOP
  2. # Copyright (c) 2011-2020 by The CAT-SOOP Developers <catsoop-dev@mit.edu>
  3. #
  4. # This program is free software: you can redistribute it and/or modify it under
  5. # the terms of the GNU Affero General Public License as published by the Free
  6. # Software Foundation, either version 3 of the License, or (at your option) any
  7. # later version.
  8. #
  9. # This program is distributed in the hope that it will be useful, but WITHOUT
  10. # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  11. # FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
  12. # details.
  13. #
  14. # You should have received a copy of the GNU Affero General Public License
  15. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  16. """
  17. Methods for sending e-mail from within CAT-SOOP
  18. """
  19. import re
  20. import smtplib
  21. from email.mime.text import MIMEText
  22. from email.mime.multipart import MIMEMultipart
  23. _nodoc = {"MIMEText", "MIMEMultipart"}
  24. RE_URL = re.compile(r"([^:]*:\/\/)?(([^\/]*\.)*([^\/\.]+\.[^\/]+)+)")
  25. """
  26. Regular expression to match a URL, to give a chance at guessing a reasonable
  27. default "from" address
  28. """
  29. def get_from_address(context):
  30. """
  31. Get the address that should be used for the "From" field in sent e-mails
  32. If `cs_email_from address` is sent, use that. Otherwise, if `cs_url_root`
  33. is a sensible URL, then use `"no-reply@%s" % cs_url_root`. Otherwise,
  34. return `None`.
  35. **Parameters:**
  36. * `context`: the context associated with this request
  37. **Returns:** a string containing a default "From" address, or `None` in the
  38. case of an error.
  39. """
  40. from_addr = context.get("cs_email_from_address", None)
  41. if from_addr is not None:
  42. return from_addr
  43. # no address specified.
  44. # try to figure out a reasonable guess from cs_url_root.
  45. m = RE_URL.match(context.get("cs_url_root", ""))
  46. if m is None:
  47. # cs_url_root not set, or didn't match RE_URL; return None
  48. # (will error out later)
  49. return None
  50. return "no-reply@%s" % m.group(2)
  51. def get_smtp_config_vars(context):
  52. """
  53. Helper function. Get the values of e-mail-related configuration variables
  54. from the given context.
  55. In particular, this function looks for `cs_smtp_host`, `cs_smtp_port`,
  56. `cs_smtp_user`, and `cs_smtp_password`. If those values are not provided,
  57. the function assumes it is connecting to localhost on port 25, and that no
  58. username or password are required.
  59. **Parameters:**
  60. * `context`: the context associated with this request
  61. **Returns:** a 4-tuple `(hostname, port, username, password)`
  62. """
  63. host = context.get("cs_smtp_host", "localhost")
  64. port = context.get("cs_smtp_port", 25)
  65. user = context.get("cs_smtp_user", None)
  66. passwd = context.get("cs_smtp_password", None)
  67. return host, port, user, passwd
  68. def get_smtp_object(context):
  69. """
  70. Return an SMTP object for sending e-mails, or `None` if not configured to
  71. send e-mail.
  72. This function is actually also used as the primary means of detecting
  73. whether this instance is capable of sending e-mails (which controls certain
  74. behaviors, such as whether e-mail confirmations are required when signing
  75. up for an account under the `"login"` authentication type.
  76. **Parameters:**
  77. * `context`: the context associated with this request
  78. **Returns:** an `smtplib.SMTP` object to use for sending e-mail, or `None`
  79. if this instance is not capable of sending e-mail.
  80. """
  81. host, port, user, passwd = get_smtp_config_vars(context)
  82. try:
  83. smtp = setup_smtp_object(smtplib.SMTP(host, port), user, passwd)
  84. return smtp
  85. except:
  86. return None
  87. def setup_smtp_object(smtp, user, passwd):
  88. """
  89. Helper function. Set up an `smtplib.SMTP` object for use with CAT-SOOP,
  90. enabling TLS if possible and logging in a user if information is specified.
  91. **Parameters**:
  92. * `smtp`: the `smtplib.SMTP` object to configure
  93. * `user`: the username to use when logging in
  94. * `password`: the password to use when logging in
  95. **Returns:** the same `smtplib.SMTP` object that was passed in, after
  96. configuring it.
  97. """
  98. smtp.set_debuglevel(False)
  99. smtp.ehlo()
  100. if user is not None and passwd is not None:
  101. if smtp.has_extn("STARTTLS"):
  102. smtp.starttls()
  103. smtp.ehlo()
  104. smtp.login(user, passwd)
  105. return smtp
  106. def can_send_email(context, smtp=-1):
  107. """
  108. Test whether CAT-SOOP can send e-mail as currently configured
  109. **Parameters**:
  110. * `context`: the context associated with this request
  111. **Optional Parameters:**
  112. * `smtp` (default `-1`): the `smtplib.SMTP` object to use; if none is
  113. provided, `catsoop.mail.get_smtp_object` is invoked to create one
  114. **Returns:** `True` if this instance is capable of sending e-mails (if it
  115. properly configured an `smtplib.SMTP` object and has a valid "From"
  116. address), and `False` otherwise
  117. """
  118. if smtp == -1:
  119. smtp = get_smtp_object(context)
  120. return smtp is not None and get_from_address(context) is not None
  121. def send_email(context, to_addr, subject, body, html_body=None, from_addr=None):
  122. """
  123. Helper function. Send an e-mail.
  124. **Parameters**:
  125. * `context`: the context associated with this request
  126. * `to_addr`: A string containing a single e-mail address for the recipient,
  127. or an iterable containing multiple recipient addresses.
  128. * `subject`: A string representing the subject of the e-mail message
  129. * `body`: A string representing the contents of the e-mail (plain text)
  130. **Optional Parameters:**
  131. * `html_body` (default `None`): A string representing the contents of the
  132. e-mail in HTML mode, or `None` to send only a plain-text message
  133. * `from_addr` (default `None`): the "From" address to use; if none is
  134. provided, the result of `catsoop.mail.get_from_address` is used
  135. **Returns:** a dictionary containing error information
  136. """
  137. if not isinstance(to_addr, (list, tuple, set)):
  138. to_addr = [to_addr]
  139. if not can_send_email(context):
  140. return dict((a, None) for a in to_addr)
  141. msg = MIMEText(body, "plain")
  142. if html_body is not None:
  143. _m = msg
  144. msg = MIMEMultipart("alternative")
  145. msg.attach(_m)
  146. msg.attach(MIMEText(html_body, "html"))
  147. msg["To"] = ", ".join(to_addr)
  148. msg["From"] = _from = get_from_address(context) if from_addr is None else from_addr
  149. msg["Subject"] = subject
  150. smtp = get_smtp_object(context)
  151. try:
  152. smtp.sendmail(_from, to_addr, msg.as_string())
  153. out = {}
  154. smtp.close()
  155. except:
  156. out = dict((a, None) for a in to_addr)
  157. return out
  158. def internal_message(context, course, recipient, subject, body, from_addr=None):
  159. """
  160. Send an e-mail to a member of a course.
  161. This function will send a multipart message. The HTML portion will consist
  162. of the result of interpreting the given `body` as Markdown, and the
  163. plain-text portion will contain `body` verbatim.
  164. **Parameters**:
  165. * `context`: the context associated with this request
  166. * `course`: the course associated with this request (where to look for user
  167. information for the recipient)
  168. * `recipient`: the CAT-SOOP username (not e-mail address) of the indented
  169. recipient
  170. * `subject`: A string representing the subject of the e-mail message
  171. * `body`: A string representing the contents of the e-mail (plain text)
  172. **Optional Parameters:**
  173. * `from_addr` (default `None`): the "From" address to use; if none is
  174. provided, the result of `catsoop.mail.get_from_address` is used
  175. **Returns:** a dictionary containing error information (empty on success),
  176. or a string containing an error message.
  177. """
  178. if recipient not in context["csm_user"].list_all_users(context, course):
  179. return "%s is not a user in %s." % (recipient, course)
  180. into = {"username": recipient}
  181. ctx = context["csm_loader"].generate_context([course])
  182. uinfo = context["csm_auth"]._get_user_information(ctx, into, course, recipient)
  183. if "email" not in uinfo:
  184. return "No e-mail address found for %s" % recipient
  185. email = uinfo["email"]
  186. lang = context["csm_language"]
  187. html_body = lang._md_format_string(context, body, False)
  188. return send_email(context, email, subject, body, html_body, from_addr)