PageRenderTime 236ms CodeModel.GetById 36ms RepoModel.GetById 0ms app.codeStats 0ms

/askbot/mail/__init__.py

https://github.com/rtnpro/askbot-devel
Python | 393 lines | 368 code | 3 blank | 22 comment | 5 complexity | 9aacedc95cc706749f4fed998137dfe1 MD5 | raw file
  1. """functions that send email in askbot
  2. these automatically catch email-related exceptions
  3. """
  4. import os
  5. import smtplib
  6. import logging
  7. from django.core import mail
  8. from django.conf import settings as django_settings
  9. from django.core.exceptions import PermissionDenied
  10. from django.forms import ValidationError
  11. from django.utils.translation import ugettext_lazy as _
  12. from django.utils.translation import string_concat
  13. from django.template import Context
  14. from askbot import exceptions
  15. from askbot import const
  16. from askbot.conf import settings as askbot_settings
  17. from askbot.utils import url_utils
  18. from askbot.utils.file_utils import store_file
  19. #todo: maybe send_mail functions belong to models
  20. #or the future API
  21. def prefix_the_subject_line(subject):
  22. """prefixes the subject line with the
  23. EMAIL_SUBJECT_LINE_PREFIX either from
  24. from live settings, which take default from django
  25. """
  26. prefix = askbot_settings.EMAIL_SUBJECT_PREFIX
  27. if prefix != '':
  28. subject = prefix.strip() + ' ' + subject.strip()
  29. return subject
  30. def extract_first_email_address(text):
  31. """extract first matching email address
  32. from text string
  33. returns ``None`` if there are no matches
  34. """
  35. match = const.EMAIL_REGEX.search(text)
  36. if match:
  37. return match.group(0)
  38. else:
  39. return None
  40. def thread_headers(post, orig_post, update):
  41. """modify headers for email messages, so
  42. that emails appear as threaded conversations in gmail"""
  43. suffix_id = django_settings.SERVER_EMAIL
  44. if update == const.TYPE_ACTIVITY_ASK_QUESTION:
  45. msg_id = "NQ-%s-%s" % (post.id, suffix_id)
  46. headers = {'Message-ID': msg_id}
  47. elif update == const.TYPE_ACTIVITY_ANSWER:
  48. msg_id = "NA-%s-%s" % (post.id, suffix_id)
  49. orig_id = "NQ-%s-%s" % (orig_post.id, suffix_id)
  50. headers = {'Message-ID': msg_id,
  51. 'In-Reply-To': orig_id}
  52. elif update == const.TYPE_ACTIVITY_UPDATE_QUESTION:
  53. msg_id = "UQ-%s-%s-%s" % (post.id, post.last_edited_at, suffix_id)
  54. orig_id = "NQ-%s-%s" % (orig_post.id, suffix_id)
  55. headers = {'Message-ID': msg_id,
  56. 'In-Reply-To': orig_id}
  57. elif update == const.TYPE_ACTIVITY_COMMENT_QUESTION:
  58. msg_id = "CQ-%s-%s" % (post.id, suffix_id)
  59. orig_id = "NQ-%s-%s" % (orig_post.id, suffix_id)
  60. headers = {'Message-ID': msg_id,
  61. 'In-Reply-To': orig_id}
  62. elif update == const.TYPE_ACTIVITY_UPDATE_ANSWER:
  63. msg_id = "UA-%s-%s-%s" % (post.id, post.last_edited_at, suffix_id)
  64. orig_id = "NQ-%s-%s" % (orig_post.id, suffix_id)
  65. headers = {'Message-ID': msg_id,
  66. 'In-Reply-To': orig_id}
  67. elif update == const.TYPE_ACTIVITY_COMMENT_ANSWER:
  68. msg_id = "CA-%s-%s" % (post.id, suffix_id)
  69. orig_id = "NQ-%s-%s" % (orig_post.id, suffix_id)
  70. headers = {'Message-ID': msg_id,
  71. 'In-Reply-To': orig_id}
  72. else:
  73. # Unknown type -> Can't set headers
  74. return {}
  75. return headers
  76. def send_mail(
  77. subject_line = None,
  78. body_text = None,
  79. from_email = django_settings.DEFAULT_FROM_EMAIL,
  80. recipient_list = None,
  81. activity_type = None,
  82. related_object = None,
  83. headers = None,
  84. raise_on_failure = False,
  85. ):
  86. """
  87. todo: remove parameters not relevant to the function
  88. sends email message
  89. logs email sending activity
  90. and any errors are reported as critical
  91. in the main log file
  92. related_object is not mandatory, other arguments
  93. are. related_object (if given, will be saved in
  94. the activity record)
  95. if raise_on_failure is True, exceptions.EmailNotSent is raised
  96. """
  97. try:
  98. assert(subject_line is not None)
  99. subject_line = prefix_the_subject_line(subject_line)
  100. msg = mail.EmailMessage(
  101. subject_line,
  102. body_text,
  103. from_email,
  104. recipient_list,
  105. headers = headers
  106. )
  107. msg.content_subtype = 'html'
  108. msg.send()
  109. if related_object is not None:
  110. assert(activity_type is not None)
  111. except Exception, error:
  112. logging.critical(unicode(error))
  113. if raise_on_failure == True:
  114. raise exceptions.EmailNotSent(unicode(error))
  115. def mail_moderators(
  116. subject_line = '',
  117. body_text = '',
  118. raise_on_failure = False):
  119. """sends email to forum moderators and admins
  120. """
  121. from django.db.models import Q
  122. from askbot.models import User
  123. recipient_list = User.objects.filter(
  124. Q(status='m') | Q(is_superuser=True)
  125. ).filter(
  126. is_active = True
  127. ).values_list('email', flat=True)
  128. recipient_list = set(recipient_list)
  129. from_email = ''
  130. if hasattr(django_settings, 'DEFAULT_FROM_EMAIL'):
  131. from_email = django_settings.DEFAULT_FROM_EMAIL
  132. try:
  133. mail.send_mail(subject_line, body_text, from_email, recipient_list)
  134. except smtplib.SMTPException, error:
  135. logging.critical(unicode(error))
  136. if raise_on_failure == True:
  137. raise exceptions.EmailNotSent(unicode(error))
  138. INSTRUCTIONS_PREAMBLE = _('<p>To ask by email, please:</p>')
  139. QUESTION_TITLE_INSTRUCTION = _(
  140. '<li>Type title in the subject line</li>'
  141. )
  142. QUESTION_DETAILS_INSTRUCTION = _(
  143. '<li>Type details of your question into the email body</li>'
  144. )
  145. OPTIONAL_TAGS_INSTRUCTION = _(
  146. """<li>The beginning of the subject line can contain tags,
  147. <em>enclosed in the square brackets</em> like so: [Tag1; Tag2]</li>"""
  148. )
  149. REQUIRED_TAGS_INSTRUCTION = _(
  150. """<li>In the beginning of the subject add at least one tag
  151. <em>enclosed in the brackets</em> like so: [Tag1; Tag2].</li>"""
  152. )
  153. TAGS_INSTRUCTION_FOOTNOTE = _(
  154. """<p>Note that a tag may consist of more than one word, to separate
  155. the tags, use a semicolon or a comma, for example, [One tag; Other tag]</p>"""
  156. )
  157. def bounce_email(
  158. email, subject, reason = None, body_text = None, reply_to = None
  159. ):
  160. """sends a bounce email at address ``email``, with the subject
  161. line ``subject``, accepts several reasons for the bounce:
  162. * ``'problem_posting'``, ``unknown_user`` and ``permission_denied``
  163. * ``body_text`` in an optional parameter that allows to append
  164. extra text to the message
  165. """
  166. if reason == 'problem_posting':
  167. error_message = _(
  168. '<p>Sorry, there was an error posting your question '
  169. 'please contact the %(site)s administrator</p>'
  170. ) % {'site': askbot_settings.APP_SHORT_NAME}
  171. if askbot_settings.TAGS_ARE_REQUIRED:
  172. error_message = string_concat(
  173. INSTRUCTIONS_PREAMBLE,
  174. '<ul>',
  175. QUESTION_TITLE_INSTRUCTION,
  176. REQUIRED_TAGS_INSTRUCTION,
  177. QUESTION_DETAILS_INSTRUCTION,
  178. '</ul>',
  179. TAGS_INSTRUCTION_FOOTNOTE
  180. )
  181. else:
  182. error_message = string_concat(
  183. INSTRUCTIONS_PREAMBLE,
  184. '<ul>',
  185. QUESTION_TITLE_INSTRUCTION,
  186. QUESTION_DETAILS_INSTRUCTION,
  187. OPTIONAL_TAGS_INSTRUCTION,
  188. '</ul>',
  189. TAGS_INSTRUCTION_FOOTNOTE
  190. )
  191. elif reason == 'unknown_user':
  192. error_message = _(
  193. '<p>Sorry, in order to post questions on %(site)s '
  194. 'by email, please <a href="%(url)s">register first</a></p>'
  195. ) % {
  196. 'site': askbot_settings.APP_SHORT_NAME,
  197. 'url': url_utils.get_login_url()
  198. }
  199. elif reason == 'permission_denied' and body_text is None:
  200. error_message = _(
  201. '<p>Sorry, your question could not be posted '
  202. 'due to insufficient privileges of your user account</p>'
  203. )
  204. elif body_text:
  205. error_message = body_text
  206. else:
  207. raise ValueError('unknown reason to bounce an email: "%s"' % reason)
  208. #print 'sending email'
  209. #print email
  210. #print subject
  211. #print error_message
  212. headers = {}
  213. if reply_to:
  214. headers['Reply-To'] = reply_to
  215. send_mail(
  216. recipient_list = (email,),
  217. subject_line = 'Re: ' + subject,
  218. body_text = error_message,
  219. headers = headers
  220. )
  221. def extract_reply(text):
  222. """take the part above the separator
  223. and discard the last line above the separator"""
  224. if const.REPLY_SEPARATOR_REGEX.search(text):
  225. text = const.REPLY_SEPARATOR_REGEX.split(text)[0]
  226. return '\n'.join(text.splitlines(True)[:-3])
  227. else:
  228. return text
  229. def process_attachment(attachment):
  230. """will save a single
  231. attachment and return
  232. link to file in the markdown format and the
  233. file storage object
  234. """
  235. file_storage, file_name, file_url = store_file(attachment)
  236. markdown_link = '[%s](%s) ' % (attachment.name, file_url)
  237. file_extension = os.path.splitext(attachment.name)[1]
  238. #todo: this is a hack - use content type
  239. if file_extension.lower() in ('.png', '.jpg', '.jpeg', '.gif'):
  240. markdown_link = '!' + markdown_link
  241. return markdown_link, file_storage
  242. def extract_user_signature(text, reply_code):
  243. """extracts email signature as text trailing
  244. the reply code"""
  245. if reply_code in text:
  246. #extract the signature
  247. tail = list()
  248. for line in reversed(text.splitlines()):
  249. #scan backwards from the end until the magic line
  250. if reply_code in line:
  251. break
  252. tail.insert(0, line)
  253. #strip off the leading quoted lines, there could be one or two
  254. #also strip empty lines
  255. while tail and (tail[0].startswith('>') or tail[0].strip() == ''):
  256. tail.pop(0)
  257. return '\n'.join(tail)
  258. else:
  259. return ''
  260. def process_parts(parts, reply_code = None):
  261. """Process parts will upload the attachments and parse out the
  262. body, if body is multipart. Secondly - links to attachments
  263. will be added to the body of the question.
  264. Returns ready to post body of the message and the list
  265. of uploaded files.
  266. """
  267. body_markdown = ''
  268. stored_files = list()
  269. attachments_markdown = ''
  270. for (part_type, content) in parts:
  271. if part_type == 'attachment':
  272. markdown, stored_file = process_attachment(content)
  273. stored_files.append(stored_file)
  274. attachments_markdown += '\n\n' + markdown
  275. elif part_type == 'body':
  276. body_markdown += '\n\n' + content.strip('\n\t ')
  277. elif part_type == 'inline':
  278. markdown, stored_file = process_attachment(content)
  279. stored_files.append(stored_file)
  280. body_markdown += markdown
  281. #if the response separator is present -
  282. #split the body with it, and discard the "so and so wrote:" part
  283. if reply_code:
  284. signature = extract_user_signature(body_markdown, reply_code)
  285. else:
  286. signature = None
  287. body_markdown = extract_reply(body_markdown)
  288. body_markdown += attachments_markdown
  289. return body_markdown.strip(), stored_files, signature
  290. def process_emailed_question(
  291. from_address, subject, body_text, stored_files, tags = None
  292. ):
  293. """posts question received by email or bounces the message"""
  294. #a bunch of imports here, to avoid potential circular import issues
  295. from askbot.forms import AskByEmailForm
  296. from askbot.models import ReplyAddress, User
  297. from askbot.mail import messages
  298. reply_to = None
  299. try:
  300. #todo: delete uploaded files when posting by email fails!!!
  301. data = {
  302. 'sender': from_address,
  303. 'subject': subject,
  304. 'body_text': body_text
  305. }
  306. form = AskByEmailForm(data)
  307. if form.is_valid():
  308. email_address = form.cleaned_data['email']
  309. user = User.objects.get(
  310. email__iexact = email_address
  311. )
  312. if user.can_post_by_email() == False:
  313. raise PermissionDenied(messages.insufficient_reputation(user))
  314. if user.email_isvalid == False:
  315. reply_to = ReplyAddress.objects.create_new(
  316. user = user,
  317. reply_action = 'validate_email'
  318. ).as_email_address()
  319. message = messages.ask_for_signature(user, footer_code = reply_to)
  320. raise PermissionDenied(message)
  321. tagnames = form.cleaned_data['tagnames']
  322. title = form.cleaned_data['title']
  323. body_text = form.cleaned_data['body_text']
  324. #defect - here we might get "too many tags" issue
  325. if tags:
  326. tagnames += ' ' + ' '.join(tags)
  327. stripped_body_text = user.strip_email_signature(body_text)
  328. if stripped_body_text == body_text and user.email_signature:
  329. #todo: send an email asking to update the signature
  330. raise ValueError('email signature changed')
  331. user.post_question(
  332. title = title,
  333. tags = tagnames.strip(),
  334. body_text = stripped_body_text,
  335. by_email = True,
  336. email_address = from_address
  337. )
  338. else:
  339. raise ValidationError()
  340. except User.DoesNotExist:
  341. bounce_email(email_address, subject, reason = 'unknown_user')
  342. except User.MultipleObjectsReturned:
  343. bounce_email(email_address, subject, reason = 'problem_posting')
  344. except PermissionDenied, error:
  345. bounce_email(
  346. email_address,
  347. subject,
  348. reason = 'permission_denied',
  349. body_text = unicode(error),
  350. reply_to = reply_to
  351. )
  352. except ValidationError:
  353. if from_address:
  354. bounce_email(
  355. from_address,
  356. subject,
  357. reason = 'problem_posting',
  358. )