summaryrefslogtreecommitdiffstats
path: root/askbot
diff options
context:
space:
mode:
authorEvgeny Fadeev <evgeny.fadeev@gmail.com>2012-03-12 20:42:22 -0500
committerEvgeny Fadeev <evgeny.fadeev@gmail.com>2012-03-12 20:42:22 -0500
commit0d4aafb04accf35ee09017f937490011bfa7418b (patch)
treefcff74892eef50b6dcf7f97c6cb387f273027ea5 /askbot
parentb00797906d29e573be6d200208ccaacdf7118b7c (diff)
parent4c2851f37e2c1504bfce8735544d208158f31d72 (diff)
downloadaskbot-0d4aafb04accf35ee09017f937490011bfa7418b.tar.gz
askbot-0d4aafb04accf35ee09017f937490011bfa7418b.tar.bz2
askbot-0d4aafb04accf35ee09017f937490011bfa7418b.zip
Merge branch 'vasil'
Diffstat (limited to 'askbot')
-rw-r--r--askbot/conf/email.py39
-rw-r--r--askbot/conf/minimum_reputation.py10
-rw-r--r--askbot/doc/source/optional-modules.rst63
-rw-r--r--askbot/lamson_handlers.py87
-rw-r--r--askbot/migrations/0106_add_model_ReplyAddress.py288
-rw-r--r--askbot/models/__init__.py33
-rw-r--r--askbot/models/reply_by_email.py63
-rw-r--r--askbot/skins/default/templates/instant_notification_reply_by_email.html14
-rw-r--r--askbot/skins/default/templates/reply_by_email_error.html4
-rw-r--r--askbot/tests/__init__.py1
-rw-r--r--askbot/tests/reply_by_email_tests.py112
11 files changed, 707 insertions, 7 deletions
diff --git a/askbot/conf/email.py b/askbot/conf/email.py
index 1f60c442..195f36e3 100644
--- a/askbot/conf/email.py
+++ b/askbot/conf/email.py
@@ -273,3 +273,42 @@ settings.register(
)
)
)
+
+
+
+settings.register(
+ livesettings.BooleanValue(
+ EMAIL,
+ 'REPLY_BY_EMAIL',
+ default = False,
+ description=_('Enable posting answers and comments by email'),
+ #TODO give a better explanation depending on lamson startup procedure
+ help_text=_(
+ 'To enable this feature make sure lamson is running'
+
+ )
+ )
+)
+
+settings.register(
+ livesettings.StringValue(
+ EMAIL,
+ 'REPLY_BY_EMAIL_HOSTNAME',
+ default = "",
+ description=_('Reply by email hostname'),
+ #TODO give a better explanation depending on lamson startup procedure
+
+ )
+)
+
+
+
+settings.register(
+ livesettings.IntegerValue(
+ EMAIL,
+ 'MIN_WORDS_FOR_ANSWER_BY_EMAIL',
+ default=14,
+ description=_('Email replies having fewer words than this number will be posted as comments instead of answers')
+ )
+)
+
diff --git a/askbot/conf/minimum_reputation.py b/askbot/conf/minimum_reputation.py
index d184be0b..06f210f2 100644
--- a/askbot/conf/minimum_reputation.py
+++ b/askbot/conf/minimum_reputation.py
@@ -180,3 +180,13 @@ settings.register(
)
)
)
+
+
+settings.register(
+ livesettings.IntegerValue(
+ MIN_REP,
+ 'MIN_REP_TO_POST_BY_EMAIL',
+ default=100,
+ description=_('Post answers and comments by email')
+ )
+) \ No newline at end of file
diff --git a/askbot/doc/source/optional-modules.rst b/askbot/doc/source/optional-modules.rst
index 93f7129a..0c013121 100644
--- a/askbot/doc/source/optional-modules.rst
+++ b/askbot/doc/source/optional-modules.rst
@@ -164,3 +164,66 @@ For `supervisor <http://supervisord.org/>`_: add this sample config file named a
startsecs=10
Then run **supervisorctl update** and it will be started. For more information about job handling with supervisor please visit `this link <http://supervisord.org/>`_.
+
+
+Receiving replies for email notifications
+===========================================
+
+Askbot supports posting replies by email. For this feature to work ``Lamson`` and ``django-lamson`` need to be installed on the system. To install all the necessery dependencies execute the following command:
+
+ pip install django-lamson
+
+The lamson daemon needs a folder to store it's mail queue files and a folder to store log files, create the folders folder named ``run`` and ``logs`` within your project folder by executing the following commands:
+
+ mkdir run
+
+ mkdir logs
+
+The minimum settings required to enable this feature are defining the port and binding address for the lamson SMTP daemon and the email handlers within askbot. Edit your settings.py file to include the following:
+
+ LAMSON_RECEIVER_CONFIG = {'host': 'your.ip.address', 'port': 25}
+
+ LAMSON_HANDLERS = ['askbot.lamson_handlers']
+
+ LAMSON_ROUTER_DEFAULTS = {'host': '.+'}
+
+In the list of ``installed_apps`` add the app ``django-lamson``.
+
+The ``LAMSON_RECEIVER_CONFIG`` parameter defines the binding address/port for the SMTP daemon. To recieve internet email you will need to bind to your external ip address and port 25. If you just want to test the feature by sending eamil from the same system you could bind to 127.0.0.1 and any higher port.
+
+To run the lamson SMTP daemon you will need to execute the following management command:
+
+ python manage.py lamson_start
+
+To stop the daemon issue the following command
+
+ python manage.py lamson_stop
+
+Note that in order to be able to bind the daemon to port 25 you will need to execute the command as a superuser.
+
+Within the askbot admin interface there are 4 significant configuration points for this feature.
+
+* In the email section, the "Enable posting answers and comments by email" controls whether the feature is enabled or disabled.
+* The "reply by email hostname" needs to be set to the email hostname where you want to receive the email replies. If for example this is set to "example.com" the users will post replies to addresses such as "4wffsw345wsf@example.com", you need to point the MX DNS record for that domain to the address where you will run the lamson SMTP daemon.
+* The last setting in this section controls the threshold for minimum length of the reply that is posted as an answer to a question. If the user is replying to a notification for a question and the reply body is shorter than this threshold the reply will be posted as a comment to the question.
+* In the karma thresholds section the "Post answers and comments by email" defines the minimum karma for users to be able to post replies by email.
+
+If the system where lamson is hosted also acts as an email server or you simply want some of the emails to be ignored and sent to another server you can define forward rules. Any emails matching these rules will be sent to another smtp server, bypassing the reply by email function. As an example by adding the following in your settings.py file:
+
+ LAMSON_FORWARD = (
+ {
+ 'pattern': '(.*?)@(.subdomain1|subdomain2)\.example.com',
+ 'host': 'localhost',
+ 'port': 8825
+ },
+ {
+ 'pattern': '(info|support)@example.com',
+ 'host': 'localhost',
+ 'port': 8825
+ },
+
+ )
+
+any email that was sent to anyaddress@sobdomain1.example.com or anyaddress@sobdomain2.example.com or info@example.com will be forwarded to the smtp server listening on port 8825. The pattern parameter is treated as a regular expression that is matched against the ``To`` header of the email message and the ``host`` and ``port`` are the host and port of the smtp server that the message should be forwarded to.
+
+If you want to run the lamson daemon on a port other than 25 you can use a mail proxy server such as ``nginx`` that will listen on port 25 and forward any SMTP requests to lamson. Using nginx you can also setup more complex email handling rules, such as for example if the same server where askbot is installed acts as an email server for other domains you can configure nginx to forward any emails directed to your askbot installation to lamson and any other emails to the mail server you're using, such as ``postfix``. For more information on how to use nginx for this please consult the nginx mail module documentation `nginx mail module documentation <http://wiki.nginx.org/MailCoreModule>`_ .
diff --git a/askbot/lamson_handlers.py b/askbot/lamson_handlers.py
new file mode 100644
index 00000000..c39df01a
--- /dev/null
+++ b/askbot/lamson_handlers.py
@@ -0,0 +1,87 @@
+import re
+import logging
+from lamson.routing import route, route_like, 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
+
+
+
+
+#we might end up needing to use something like this
+#to distinguish the reply text from the quoted original message
+"""
+def _strip_message_qoute(message_text):
+ import re
+ result = message_text
+ pattern = "(?P<qoute>" + \
+ "On ([a-zA-Z0-9, :/<>@\.\"\[\]]* wrote:.*)|" + \
+ "From: [\w@ \.]* \[mailto:[\w\.]*@[\w\.]*\].*|" + \
+ "From: [\w@ \.]*(\n|\r\n)+Sent: [\*\w@ \.,:/]*(\n|\r\n)+To:.*(\n|\r\n)+.*|" + \
+ "[- ]*Forwarded by [\w@ \.,:/]*.*|" + \
+ "From: [\w@ \.<>\-]*(\n|\r\n)To: [\w@ \.<>\-]*(\n|\r\n)Date: [\w@ \.<>\-:,]*\n.*|" + \
+ "From: [\w@ \.<>\-]*(\n|\r\n)To: [\w@ \.<>\-]*(\n|\r\n)Sent: [\*\w@ \.,:/]*(\n|\r\n).*|" + \
+ "From: [\w@ \.<>\-]*(\n|\r\n)To: [\w@ \.<>\-]*(\n|\r\n)Subject:.*|" + \
+ "(-| )*Original Message(-| )*.*)"
+ groups = re.search(pattern, email_text, re.IGNORECASE + re.DOTALL)
+ qoute = None
+ if not groups is None:
+ if groups.groupdict().has_key("qoute"):
+ qoute = groups.groupdict()["qoute"]
+ if qoute:
+ result = reslut.split(qoute)[0]
+ #if the last line contains an email message remove that one too
+ lines = result.splitlines(True)
+ if re.search(r'[\w\.]*@[\w\.]*\].*', lines[-1]):
+ result = '\n'.join(lines[:-1])
+ return result
+"""
+
+
+
+@route("(address)@(host)", address=".+")
+@stateless
+def PROCESS(message, address = None, host = None):
+ try:
+ for rule in settings.LAMSON_FORWARD:
+ if re.match(rule['pattern'], message.base['to']):
+ relay = Relay(host=rule['host'],
+ port=rule['port'], debug=1)
+ relay.deliver(message)
+ return
+ except AttributeError:
+ pass
+
+ error = None
+ try:
+ reply_address = ReplyAddress.objects.get_unused(address, message.From)
+ separator = _("======= Reply above this line. ====-=-=")
+ parts = message.body().split(separator)
+ 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.")
+ else:
+ reply_part = parts[0]
+ reply_part = '\n'.join(reply_part.splitlines(True)[:-3])
+ reply_address.create_reply(reply_part.strip())
+ 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\
+ received the notification.")
+ if error is not None:
+ from askbot.utils import mail
+ from django.template import Context
+ from askbot.skins.loaders import get_template
+
+ template = get_template('reply_by_email_error.html')
+ body_text = template.render(Context({'error':error}))
+ mail.send_mail(
+ subject_line = "Error posting your reply",
+ body_text = body_text,
+ recipient_list = [message.From],
+ )
+
+
+
diff --git a/askbot/migrations/0106_add_model_ReplyAddress.py b/askbot/migrations/0106_add_model_ReplyAddress.py
new file mode 100644
index 00000000..9cc43252
--- /dev/null
+++ b/askbot/migrations/0106_add_model_ReplyAddress.py
@@ -0,0 +1,288 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+ # Adding model 'ReplyAddress'
+ db.create_table('askbot_replyaddress', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('address', self.gf('django.db.models.fields.CharField')(unique=True, max_length=25)),
+ ('post', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['askbot.Post'])),
+ ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
+ ('allowed_from_email', self.gf('django.db.models.fields.EmailField')(max_length=150)),
+ ('used_at', self.gf('django.db.models.fields.DateTimeField')(default=None, null=True)),
+ ))
+ db.send_create_signal('askbot', ['ReplyAddress'])
+
+ def backwards(self, orm):
+ # Deleting model 'ReplyAddress'
+ db.delete_table('askbot_replyaddress')
+
+ models = {
+ 'askbot.activity': {
+ 'Meta': {'object_name': 'Activity', 'db_table': "u'activity'"},
+ 'active_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'activity_type': ('django.db.models.fields.SmallIntegerField', [], {}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_auditted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Post']", 'null': 'True'}),
+ 'receiving_users': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'received_activity'", 'symmetrical': 'False', 'to': "orm['auth.User']"}),
+ 'recipients': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'incoming_activity'", 'symmetrical': 'False', 'through': "orm['askbot.ActivityAuditStatus']", 'to': "orm['auth.User']"}),
+ 'summary': ('django.db.models.fields.TextField', [], {'default': "''"}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'askbot.activityauditstatus': {
+ 'Meta': {'unique_together': "(('user', 'activity'),)", 'object_name': 'ActivityAuditStatus'},
+ 'activity': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Activity']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'status': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'askbot.anonymousanswer': {
+ 'Meta': {'object_name': 'AnonymousAnswer'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ip_addr': ('django.db.models.fields.IPAddressField', [], {'max_length': '15'}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'anonymous_answers'", 'to': "orm['askbot.Post']"}),
+ 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
+ 'summary': ('django.db.models.fields.CharField', [], {'max_length': '180'}),
+ 'text': ('django.db.models.fields.TextField', [], {}),
+ 'wiki': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+ },
+ 'askbot.anonymousquestion': {
+ 'Meta': {'object_name': 'AnonymousQuestion'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ip_addr': ('django.db.models.fields.IPAddressField', [], {'max_length': '15'}),
+ 'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
+ 'summary': ('django.db.models.fields.CharField', [], {'max_length': '180'}),
+ 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '125'}),
+ 'text': ('django.db.models.fields.TextField', [], {}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '300'}),
+ 'wiki': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+ },
+ 'askbot.award': {
+ 'Meta': {'object_name': 'Award', 'db_table': "u'award'"},
+ 'awarded_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'badge': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'award_badge'", 'to': "orm['askbot.BadgeData']"}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'notified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'award_user'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.badgedata': {
+ 'Meta': {'ordering': "('slug',)", 'object_name': 'BadgeData'},
+ 'awarded_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'awarded_to': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'badges'", 'symmetrical': 'False', 'through': "orm['askbot.Award']", 'to': "orm['auth.User']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50'})
+ },
+ 'askbot.emailfeedsetting': {
+ 'Meta': {'object_name': 'EmailFeedSetting'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'feed_type': ('django.db.models.fields.CharField', [], {'max_length': '16'}),
+ 'frequency': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '8'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'reported_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
+ 'subscriber': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'notification_subscriptions'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.favoritequestion': {
+ 'Meta': {'object_name': 'FavoriteQuestion', 'db_table': "u'favorite_question'"},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Thread']"}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'user_favorite_questions'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.markedtag': {
+ 'Meta': {'object_name': 'MarkedTag'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'reason': ('django.db.models.fields.CharField', [], {'max_length': '16'}),
+ 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'user_selections'", 'to': "orm['askbot.Tag']"}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'tag_selections'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.post': {
+ 'Meta': {'object_name': 'Post'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'posts'", 'to': "orm['auth.User']"}),
+ 'comment_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'deleted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'deleted_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'deleted_posts'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'html': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_edited_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'last_edited_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'last_edited_posts'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'locked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'locked_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'locked_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'locked_posts'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'offensive_flag_count': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'old_answer_id': ('django.db.models.fields.PositiveIntegerField', [], {'default': 'None', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
+ 'old_comment_id': ('django.db.models.fields.PositiveIntegerField', [], {'default': 'None', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
+ 'old_question_id': ('django.db.models.fields.PositiveIntegerField', [], {'default': 'None', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
+ 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'comments'", 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'post_type': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'summary': ('django.db.models.fields.CharField', [], {'max_length': '180'}),
+ 'text': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ 'thread': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'posts'", 'to': "orm['askbot.Thread']"}),
+ 'vote_down_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'vote_up_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'wiki': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'wikified_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'})
+ },
+ 'askbot.postrevision': {
+ 'Meta': {'ordering': "('-revision',)", 'unique_together': "(('post', 'revision'),)", 'object_name': 'PostRevision'},
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'postrevisions'", 'to': "orm['auth.User']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'revisions'", 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'revised_at': ('django.db.models.fields.DateTimeField', [], {}),
+ 'revision': ('django.db.models.fields.PositiveIntegerField', [], {}),
+ 'revision_type': ('django.db.models.fields.SmallIntegerField', [], {}),
+ 'summary': ('django.db.models.fields.CharField', [], {'max_length': '300', 'blank': 'True'}),
+ 'tagnames': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '125', 'blank': 'True'}),
+ 'text': ('django.db.models.fields.TextField', [], {}),
+ 'title': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '300', 'blank': 'True'})
+ },
+ 'askbot.questionview': {
+ 'Meta': {'object_name': 'QuestionView'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'viewed'", 'to': "orm['askbot.Post']"}),
+ 'when': ('django.db.models.fields.DateTimeField', [], {}),
+ 'who': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'question_views'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.replyaddress': {
+ 'Meta': {'object_name': 'ReplyAddress'},
+ 'address': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '25'}),
+ 'allowed_from_email': ('django.db.models.fields.EmailField', [], {'max_length': '150'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Post']"}),
+ 'used_at': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'askbot.repute': {
+ 'Meta': {'object_name': 'Repute', 'db_table': "u'repute'"},
+ 'comment': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'negative': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'positive': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Post']", 'null': 'True', 'blank': 'True'}),
+ 'reputation': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'reputation_type': ('django.db.models.fields.SmallIntegerField', [], {}),
+ 'reputed_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'askbot.tag': {
+ 'Meta': {'ordering': "('-used_count', 'name')", 'object_name': 'Tag', 'db_table': "u'tag'"},
+ 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'created_tags'", 'to': "orm['auth.User']"}),
+ 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'deleted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'deleted_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'deleted_tags'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'used_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+ },
+ 'askbot.thread': {
+ 'Meta': {'object_name': 'Thread'},
+ 'accepted_answer': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'answer_accepted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'answer_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'close_reason': ('django.db.models.fields.SmallIntegerField', [], {'null': 'True', 'blank': 'True'}),
+ 'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'closed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'closed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+ 'favorited_by': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'unused_favorite_threads'", 'symmetrical': 'False', 'through': "orm['askbot.FavoriteQuestion']", 'to': "orm['auth.User']"}),
+ 'favourite_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'followed_by': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'followed_threads'", 'symmetrical': 'False', 'to': "orm['auth.User']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'last_activity_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_activity_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'unused_last_active_in_threads'", 'to': "orm['auth.User']"}),
+ 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '125'}),
+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'threads'", 'symmetrical': 'False', 'to': "orm['askbot.Tag']"}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '300'}),
+ 'view_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+ },
+ 'askbot.vote': {
+ 'Meta': {'unique_together': "(('user', 'voted_post'),)", 'object_name': 'Vote', 'db_table': "u'vote'"},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'votes'", 'to': "orm['auth.User']"}),
+ 'vote': ('django.db.models.fields.SmallIntegerField', [], {}),
+ 'voted_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'voted_post': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'votes'", 'to': "orm['askbot.Post']"})
+ },
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}),
+ 'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}),
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
+ 'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
+ 'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
+ 'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}),
+ 'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
+ 'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}),
+ 'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}),
+ 'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ }
+ }
+
+ complete_apps = ['askbot'] \ No newline at end of file
diff --git a/askbot/models/__init__.py b/askbot/models/__init__.py
index 9abb4198..b5d9cf0b 100644
--- a/askbot/models/__init__.py
+++ b/askbot/models/__init__.py
@@ -31,6 +31,7 @@ from askbot.models.tag import Tag, MarkedTag
from askbot.models.meta import Vote
from askbot.models.user import EmailFeedSetting, ActivityAuditStatus, Activity
from askbot.models.post import Post, PostRevision
+from askbot.models.reply_by_email import ReplyAddress
from askbot.models import signals
from askbot.models.badges import award_badges_signal, get_badge, BadgeData
from askbot.models.repute import Award, Repute
@@ -2345,6 +2346,9 @@ def format_instant_notification_email(
update_data = {
'update_author_name': from_user.username,
'receiving_user_name': to_user.username,
+ 'receiving_user_karma': to_user.reputation,
+ 'reply_by_email_karma_threshold': askbot_settings.MIN_REP_TO_POST_BY_EMAIL,
+ 'can_reply': to_user.reputation > askbot_settings.MIN_REP_TO_POST_BY_EMAIL,
'content_preview': content_preview,#post.get_snippet()
'update_type': update_type,
'post_url': strip_path(site_url) + post.get_absolute_url(),
@@ -2383,25 +2387,38 @@ def send_instant_notifications_about_activity_in_post(
origin_post = post.get_origin_post()
for user in recipients:
+ if askbot_settings.REPLY_BY_EMAIL:
+ template = get_template('instant_notification_reply_by_email.html')
+
subject_line, body_text = format_instant_notification_email(
- to_user = user,
- from_user = update_activity.user,
- post = post,
- update_type = update_type,
- template = template,
- )
+ to_user = user,
+ from_user = update_activity.user,
+ post = post,
+ update_type = update_type,
+ template = template,
+ )
+
#todo: this could be packaged as an "action" - a bundle
#of executive function with the activity log recording
+ #TODO check user reputation
+ headers = mail.thread_headers(post, origin_post, update_activity.activity_type)
+ if askbot_settings.REPLY_BY_EMAIL:
+ reply_address = "noreply"
+ if user.reputation >= askbot_settings.MIN_REP_TO_POST_BY_EMAIL:
+ reply_address = ReplyAddress.objects.create_new(post, user).address
+ headers.update({'Reply-To': "%s@%s"%(reply_address, askbot_settings.REPLY_BY_EMAIL_HOSTNAME)})
mail.send_mail(
subject_line = subject_line,
body_text = body_text,
recipient_list = [user.email],
related_object = origin_post,
activity_type = const.TYPE_ACTIVITY_EMAIL_UPDATE_SENT,
- headers = mail.thread_headers(post, origin_post, update_activity.activity_type)
+ headers = headers
)
+
+
#todo: move to utils
def calculate_gravatar_hash(instance, **kwargs):
"""Calculates a User's gravatar hash from their email address."""
@@ -2801,5 +2818,7 @@ __all__ = [
'User',
+ 'ReplyAddress',
+
'get_model'
]
diff --git a/askbot/models/reply_by_email.py b/askbot/models/reply_by_email.py
new file mode 100644
index 00000000..7a575de7
--- /dev/null
+++ b/askbot/models/reply_by_email.py
@@ -0,0 +1,63 @@
+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.conf import settings as askbot_settings
+
+
+
+class ReplyAddressManager(BaseQuerySetManager):
+
+ def get_unused(self, address, allowed_from_email):
+ return self.get(address = address, allowed_from_email = allowed_from_email, used_at__isnull = True)
+
+ def create_new(self, post, user):
+ reply_address = ReplyAddress(post = post, user = user, allowed_from_email = user.email)
+ while True:
+ reply_address.address = ''.join(random.choice(string.letters +
+ string.digits) for i in xrange(random.randint(12, 25))).lower()
+ if self.filter(address = reply_address.address).count() == 0:
+ break
+ reply_address.save()
+ return reply_address
+
+
+class ReplyAddress(models.Model):
+ address = models.CharField(max_length = 25, unique = True)
+ post = models.ForeignKey(Post)
+ user = models.ForeignKey(User)
+ allowed_from_email = models.EmailField(max_length = 150)
+ used_at = models.DateTimeField(null = True, default = None)
+
+ objects = ReplyAddressManager()
+
+
+ class Meta:
+ app_label = 'askbot'
+ db_table = 'askbot_replyaddress'
+
+ def create_reply(self, content):
+ result = None
+ if self.post.post_type == 'answer':
+ result = self.user.post_comment(self.post, content)
+ elif self.post.post_type == 'question':
+ wordcount = len(content)/6
+ if wordcount > askbot_settings.MIN_WORDS_FOR_ANSWER_BY_EMAIL:
+ result = self.user.post_answer(self.post, content)
+ else:
+ result = self.user.post_comment(self.post, content)
+ elif self.post.post_type == 'comment':
+ result = self.user.post_comment(self.post.parent, content)
+ self.used_at = datetime.now()
+ self.save()
+ return result
+
+
+
+
diff --git a/askbot/skins/default/templates/instant_notification_reply_by_email.html b/askbot/skins/default/templates/instant_notification_reply_by_email.html
new file mode 100644
index 00000000..ffb43110
--- /dev/null
+++ b/askbot/skins/default/templates/instant_notification_reply_by_email.html
@@ -0,0 +1,14 @@
+
+{% if can_reply %}
+{% trans %}
+{# Don't change the following line in the template. #}
+======= Reply above this line. ====-=-=
+{% endtrans %}
+{% else %}
+{% trans %}
+You can post an answer or a comment by replying to email notifications. To do that
+you need {{reply_by_email_karma_threshold}} karma, you have {{receiving_user_karma}} karma.
+{% endtrans %}
+{% endif %}
+
+{% include 'instant_notification.html' %} \ No newline at end of file
diff --git a/askbot/skins/default/templates/reply_by_email_error.html b/askbot/skins/default/templates/reply_by_email_error.html
new file mode 100644
index 00000000..6860b75f
--- /dev/null
+++ b/askbot/skins/default/templates/reply_by_email_error.html
@@ -0,0 +1,4 @@
+{%trans%}
+<p>The system was unable to process your message successfully, the reason being:<p>
+{%endtrans%}
+{{error}} \ No newline at end of file
diff --git a/askbot/tests/__init__.py b/askbot/tests/__init__.py
index 49546e8e..7c1baae3 100644
--- a/askbot/tests/__init__.py
+++ b/askbot/tests/__init__.py
@@ -14,3 +14,4 @@ from askbot.tests.templatefilter_tests import *
from askbot.tests.markup_test import *
from askbot.tests.misc_tests import *
from askbot.tests.post_model_tests import *
+from askbot.tests.reply_by_email_tests import * \ No newline at end of file
diff --git a/askbot/tests/reply_by_email_tests.py b/askbot/tests/reply_by_email_tests.py
new file mode 100644
index 00000000..76097362
--- /dev/null
+++ b/askbot/tests/reply_by_email_tests.py
@@ -0,0 +1,112 @@
+from django.utils.translation import ugettext as _
+from askbot.models import ReplyAddress
+from askbot.lamson_handlers import PROCESS
+
+
+from askbot.tests.utils import AskbotTestCase
+from askbot.models import Post, PostRevision
+
+class MockMessage(object):
+
+ def __init__(self, body, from_email):
+ self._body = body
+ self.From= from_email
+
+ def body(self):
+ return self._body
+
+class EmailProcessingTests(AskbotTestCase):
+
+ def setUp(self):
+ self.u1 = self.create_user(username='user1')
+ self.u1.set_status('a')
+ self.u1.email = "user1@domain.com"
+ self.u1.save()
+
+ self.u1.moderate_user_reputation(self.u1, reputation_change = 100, comment= "no comment")
+ self.u2 = self.create_user(username='user2')
+ self.u1.moderate_user_reputation(self.u2, reputation_change = 100, comment= "no comment")
+ self.u3 = self.create_user(username='user3')
+ self.u1.moderate_user_reputation(self.u3, reputation_change = 100, comment= "no comment")
+
+ self.question = self.post_question(
+ user = self.u1,
+ follow = True,
+ )
+ self.answer = self.post_answer(
+ user = self.u2,
+ question = self.question
+ )
+
+ self.comment = self.post_comment(user = self.u2, parent_post = self.answer)
+
+ def test_process_correct_answer_comment(self):
+ addr = ReplyAddress.objects.create_new( self.answer, self.u1).address
+ separator = _("======= Reply above this line. ====-=-=")
+ msg = MockMessage("This is a test reply \n\nOn such and such someone\
+ wrote something \n\n%s\nlorem ipsum "%(separator), "user1@domain.com")
+ PROCESS(msg, addr, '')
+ self.assertEquals(self.answer.comments.count(), 2)
+ self.assertEquals(self.answer.comments.all().order_by('-pk')[0].text.strip(), "This is a test reply")
+
+
+
+class ReplyAddressModelTests(AskbotTestCase):
+
+ def setUp(self):
+ self.u1 = self.create_user(username='user1')
+ self.u1.set_status('a')
+ self.u1.moderate_user_reputation(self.u1, reputation_change = 100, comment= "no comment")
+ self.u2 = self.create_user(username='user2')
+ self.u1.moderate_user_reputation(self.u2, reputation_change = 100, comment= "no comment")
+ self.u3 = self.create_user(username='user3')
+ self.u1.moderate_user_reputation(self.u3, reputation_change = 100, comment= "no comment")
+
+ self.question = self.post_question(
+ user = self.u1,
+ follow = True,
+ )
+ self.answer = self.post_answer(
+ user = self.u2,
+ question = self.question
+ )
+
+ self.comment = self.post_comment(user = self.u2, parent_post = self.answer)
+
+ def test_address_creation(self):
+ self.assertEquals(ReplyAddress.objects.all().count(), 0)
+ result = ReplyAddress.objects.create_new( self.answer, self.u1)
+ self.assertTrue(len(result.address) >= 12 and len(result.address) <= 25)
+ self.assertEquals(ReplyAddress.objects.all().count(), 1)
+
+
+ def test_create_answer_reply(self):
+ result = ReplyAddress.objects.create_new( self.answer, self.u1)
+ post = result.create_reply("A test post")
+ self.assertEquals(post.post_type, "comment")
+ self.assertEquals(post.text, "A test post")
+ self.assertEquals(self.answer.comments.count(), 2)
+
+ def test_create_comment_reply(self):
+ result = ReplyAddress.objects.create_new( self.comment, self.u1)
+ post = result.create_reply("A test reply")
+ self.assertEquals(post.post_type, "comment")
+ self.assertEquals(post.text, "A test reply")
+ self.assertEquals(self.answer.comments.count(), 2)
+
+
+ def test_create_question_comment_reply(self):
+ result = ReplyAddress.objects.create_new( self.question, self.u3)
+ post = result.create_reply("A test post")
+ self.assertEquals(post.post_type, "comment")
+ self.assertEquals(post.text, "A test post")
+
+ def test_create_question_answer_reply(self):
+ result = ReplyAddress.objects.create_new( self.question, self.u3)
+ post = result.create_reply("A test post "* 10)
+ self.assertEquals(post.post_type, "answer")
+ self.assertEquals(post.text, "A test post "* 10)
+
+
+
+