From 6e44222a68bc9edb506ab6598621532cbe6f5ced Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Thu, 15 Mar 2012 01:31:29 -0400 Subject: a first pass on accepting file attachments in email responses --- askbot/lamson_handlers.py | 73 +++++++++++++++++++++++++++++++++--- askbot/models/reply_by_email.py | 20 ++++++---- askbot/tests/reply_by_email_tests.py | 4 ++ askbot/utils/file_utils.py | 35 +++++++++++++++++ askbot/views/writers.py | 28 +++----------- 5 files changed, 125 insertions(+), 35 deletions(-) create mode 100644 askbot/utils/file_utils.py diff --git a/askbot/lamson_handlers.py b/askbot/lamson_handlers.py index 899ee47a..8e2d2ce6 100644 --- a/askbot/lamson_handlers.py +++ b/askbot/lamson_handlers.py @@ -1,13 +1,10 @@ import re -import logging -from lamson.routing import route, route_like, stateless +from lamson.routing import route, stateless from lamson.server import Relay from django.utils.translation import ugettext as _ from askbot.models import ReplyAddress -from askbot.conf import settings as askbot_settings from django.conf import settings - - +from StringIO import StringIO #we might end up needing to use something like this @@ -39,11 +36,71 @@ def _strip_message_qoute(message_text): return result """ +def get_dispositions(part): + """return list of part's content dispositions + or an empty list + """ + disposition_hdr = part.get('Content-Disposition', None) + if disposition_hdr: + dispositions = disposition_hdr.strip().split(';') + return [disp.lower() for disp in dispositions] + else: + return list() + +def is_attachment(part): + """True if part content disposition is + attachment""" + dispositions = get_dispositions(part) + if len(dispositions) == 0: + return False + + if dispositions[0] == 'attachment': + return True + + return False + +def process_attachment(part): + """takes message part and turns it into StringIO object""" + file_data = part.get_payload(decode = True) + att = StringIO(file_data) + att.content_type = part.get_content_type() + att.size = len(file_data) + att.name = None#todo figure out file name + att.create_date = None + att.mod_date = None + att.read_date = None + + dispositions = get_dispositions(part)[:1] + for disp in dispositions: + name, value = disp.split('=') + if name == 'filename': + att.name = value + elif name == 'create-date': + att.create_date = value + elif name == 'modification-date': + att.modification_date = value + elif name == 'read-date': + att.read_date = value + + return att + +def get_attachments(message): + """returns a list of file attachments + represented by StringIO objects""" + attachments = list() + for part in message.walk(): + if is_attachment(part): + attachments.append(process_attachment(part)) + return attachments + @route("(address)@(host)", address=".+") @stateless def PROCESS(message, address = None, host = None): + """handler to process the emailed message + and make a post to askbot based on the contents of + the email, including the text body and the file attachments""" try: for rule in settings.LAMSON_FORWARD: if re.match(rule['pattern'], message.base['to']): @@ -59,6 +116,7 @@ def PROCESS(message, address = None, host = None): reply_address = ReplyAddress.objects.get_unused(address, message.From) separator = _("======= Reply above this line. ====-=-=") parts = message.body().split(separator) + attachments = get_attachments(message) if len(parts) != 2 : error = _("Your message was malformed. Please make sure to qoute \ the original notification you received at the end of your reply.") @@ -66,7 +124,10 @@ def PROCESS(message, address = None, host = None): reply_part = parts[0] reply_part = '\n'.join(reply_part.splitlines(True)[:-3]) #the function below actually posts to the forum - reply_address.create_reply(reply_part.strip()) + reply_address.create_reply( + reply_part.strip(), + attachments = attachments + ) except ReplyAddress.DoesNotExist: error = _("You were replying to an email address\ unknown to the system or you were replying from a different address from the one where you\ diff --git a/askbot/models/reply_by_email.py b/askbot/models/reply_by_email.py index 0653f684..936c3ded 100644 --- a/askbot/models/reply_by_email.py +++ b/askbot/models/reply_by_email.py @@ -1,17 +1,13 @@ from datetime import datetime import random import string - from django.db import models from django.contrib.auth.models import User - - from askbot.models.post import Post from askbot.models.base import BaseQuerySetManager +from askbot.utils.file_utils import store_file from askbot.conf import settings as askbot_settings - - class ReplyAddressManager(BaseQuerySetManager): def get_unused(self, address, allowed_from_email): @@ -42,12 +38,22 @@ class ReplyAddress(models.Model): app_label = 'askbot' db_table = 'askbot_replyaddress' - def create_reply(self, content): + def create_reply(self, content, attachments = None): result = None + + if attachments: + #cheap way of dealing with the attachments + #just insert them inline, however it might + #be useful to keep track of the uploaded files separately + #and deal with them as with resources of their own value + for att in attachments: + file_storage, file_name, file_url = store_file(att) + result += '[%s](%s) ' % (att.name, file_url) + if self.post.post_type == 'answer': result = self.user.post_comment(self.post, content) elif self.post.post_type == 'question': - wordcount = len(content)/6 + wordcount = len(content)/6#this is a simplistic hack if wordcount > askbot_settings.MIN_WORDS_FOR_ANSWER_BY_EMAIL: result = self.user.post_answer(self.post, content) else: diff --git a/askbot/tests/reply_by_email_tests.py b/askbot/tests/reply_by_email_tests.py index 76097362..5128c9e7 100644 --- a/askbot/tests/reply_by_email_tests.py +++ b/askbot/tests/reply_by_email_tests.py @@ -15,6 +15,10 @@ class MockMessage(object): def body(self): return self._body + def walk(self): + """todo: add real file attachment""" + return list() + class EmailProcessingTests(AskbotTestCase): def setUp(self): diff --git a/askbot/utils/file_utils.py b/askbot/utils/file_utils.py new file mode 100644 index 00000000..daca1522 --- /dev/null +++ b/askbot/utils/file_utils.py @@ -0,0 +1,35 @@ +"""file utilities for askbot""" +import os +import random +import time +import urlparse +from django.core.files.storage import get_storage_class + +def store_file(file_object): + """Creates an instance of django's file storage + object based on the file-like object, + returns the storage object, file name, file url + """ + file_name = str( + time.time() + ).replace( + '.', + str(random.randint(0,100000)) + ) + os.path.splitext(file_object.name)[1].lower() + + file_storage = get_storage_class()() + # use default storage to store file + file_storage.save(file_name, file_object) + + file_url = file_storage.url(file_name) + parsed_url = urlparse.urlparse(file_url) + file_url = urlparse.urlunparse( + urlparse.ParseResult( + parsed_url.scheme, + parsed_url.netloc, + parsed_url.path, + '', '', '' + ) + ) + + return file_storage, file_name, file_url diff --git a/askbot/views/writers.py b/askbot/views/writers.py index 0ee4b7ef..4b1ae744 100644 --- a/askbot/views/writers.py +++ b/askbot/views/writers.py @@ -13,7 +13,6 @@ import sys import tempfile import time import urlparse -from django.core.files.storage import get_storage_class from django.shortcuts import get_object_or_404 from django.contrib.auth.decorators import login_required from django.http import HttpResponseRedirect, HttpResponse, HttpResponseForbidden, Http404 @@ -31,6 +30,7 @@ from askbot.skins.loaders import render_into_skin from askbot.utils import decorators from askbot.utils.functions import diff_date from askbot.utils import url_utils +from askbot.utils.file_utils import store_file from askbot.templatetags import extra_filters_jinja as template_filters from askbot.importers.stackexchange import management as stackexchange#todo: may change @@ -64,6 +64,9 @@ def upload(request):#ajax upload file to a question or answer # check file type f = request.FILES['file-upload'] + + #todo: extension checking should be replaced with mimetype checking + #and this must be part of the form validation file_extension = os.path.splitext(f.name)[1].lower() if not file_extension in settings.ASKBOT_ALLOWED_UPLOAD_FILE_TYPES: file_types = "', '".join(settings.ASKBOT_ALLOWED_UPLOAD_FILE_TYPES) @@ -71,17 +74,8 @@ def upload(request):#ajax upload file to a question or answer {'file_types': file_types} raise exceptions.PermissionDenied(msg) - # generate new file name - new_file_name = str( - time.time() - ).replace( - '.', - str(random.randint(0,100000)) - ) + file_extension - - file_storage = get_storage_class()() - # use default storage to store file - file_storage.save(new_file_name, f) + # generate new file name and storage object + file_storage, new_file_name, file_url = store_file(f) # check file size # byte size = file_storage.size(new_file_name) @@ -99,16 +93,6 @@ def upload(request):#ajax upload file to a question or answer if error == '': result = 'Good' - file_url = file_storage.url(new_file_name) - parsed_url = urlparse.urlparse(file_url) - file_url = urlparse.urlunparse( - urlparse.ParseResult( - parsed_url.scheme, - parsed_url.netloc, - parsed_url.path, - '', '', '' - ) - ) else: result = '' file_url = '' -- cgit v1.2.3-1-g7c22