From 1346f75c096c0995d2698ed3f3c4803015aa8d0f Mon Sep 17 00:00:00 2001 From: Vasil Vangelovski Date: Sun, 15 Jan 2012 18:56:37 +0100 Subject: Defined live settings options for the reply by email feature --- askbot/conf/email.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/askbot/conf/email.py b/askbot/conf/email.py index 3db80e7a..13c079ef 100644 --- a/askbot/conf/email.py +++ b/askbot/conf/email.py @@ -264,3 +264,30 @@ 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.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') + ) +) + -- cgit v1.2.3-1-g7c22 From 78ae887a6c082fd418bcefa675fd51919fab6d5a Mon Sep 17 00:00:00 2001 From: Vasil Vangelovski Date: Sun, 15 Jan 2012 18:57:29 +0100 Subject: Defined live settings options for the reply by email feature --- askbot/conf/minimum_reputation.py | 10 ++++++++++ 1 file changed, 10 insertions(+) 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 -- cgit v1.2.3-1-g7c22 From c631028e26ec9c203d0a4e6195fa9b138d858ede Mon Sep 17 00:00:00 2001 From: Vasil Vangelovski Date: Sun, 15 Jan 2012 21:40:19 +0100 Subject: Added model for tracking generated addresses for reply by email feature --- askbot/migrations/0106_add_model_ReplyAddress.py | 288 +++++++++++++++++++++++ askbot/models/__init__.py | 3 + askbot/models/reply_by_email.py | 34 +++ askbot/tests/__init__.py | 1 + askbot/tests/reply_by_email_tests.py | 26 ++ 5 files changed, 352 insertions(+) create mode 100644 askbot/migrations/0106_add_model_ReplyAddress.py create mode 100644 askbot/models/reply_by_email.py create mode 100644 askbot/tests/reply_by_email_tests.py 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 e7feecce..3d90dfae 100644 --- a/askbot/models/__init__.py +++ b/askbot/models/__init__.py @@ -29,6 +29,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 @@ -2720,5 +2721,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..74c7b666 --- /dev/null +++ b/askbot/models/reply_by_email.py @@ -0,0 +1,34 @@ +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 + +class ReplyAddressManager(BaseQuerySetManager): + + 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))) + if ReplyAddress.objects.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' \ 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..c40d425d --- /dev/null +++ b/askbot/tests/reply_by_email_tests.py @@ -0,0 +1,26 @@ +from askbot.models import ReplyAddress + +from askbot.tests.utils import AskbotTestCase +from askbot.models import Post, PostRevision + + +class ReplyAddressModelTests(AskbotTestCase): + + def setUp(self): + self.u1 = self.create_user(username='user1') + self.u2 = self.create_user(username='user2') + self.question = self.post_question( + user = self.u1, + follow = True, + ) + self.answer = self.post_answer( + user = self.u2, + question = self.question + ) + + def test_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) + -- cgit v1.2.3-1-g7c22 From 430d42d193ecdde6ebcd642133e392dca60a3d41 Mon Sep 17 00:00:00 2001 From: Vasil Vangelovski Date: Tue, 17 Jan 2012 01:23:14 +0100 Subject: Posting reply by email - saving post --- askbot/conf/email.py | 12 ++++++++ askbot/models/__init__.py | 14 +++++++-- askbot/models/reply_by_email.py | 20 ++++++++++++- .../instant_notification_reply_by_email.html | 7 +++++ askbot/tests/reply_by_email_tests.py | 35 +++++++++++++++++++++- 5 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 askbot/skins/default/templates/instant_notification_reply_by_email.html diff --git a/askbot/conf/email.py b/askbot/conf/email.py index 13c079ef..ebfab0d7 100644 --- a/askbot/conf/email.py +++ b/askbot/conf/email.py @@ -281,6 +281,18 @@ settings.register( ) ) +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( diff --git a/askbot/models/__init__.py b/askbot/models/__init__.py index 3d90dfae..656fa6f3 100644 --- a/askbot/models/__init__.py +++ b/askbot/models/__init__.py @@ -2278,6 +2278,8 @@ 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, 'content_preview': content_preview,#post.get_snippet() 'update_type': update_type, 'post_url': site_url + post.get_absolute_url(), @@ -2315,7 +2317,8 @@ 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, @@ -2325,13 +2328,20 @@ def send_instant_notifications_about_activity_in_post( ) #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 ) diff --git a/askbot/models/reply_by_email.py b/askbot/models/reply_by_email.py index 74c7b666..124fce4b 100644 --- a/askbot/models/reply_by_email.py +++ b/askbot/models/reply_by_email.py @@ -1,3 +1,4 @@ +from datetime import datetime import random import string @@ -6,6 +7,7 @@ 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): @@ -29,6 +31,22 @@ class ReplyAddress(models.Model): objects = ReplyAddressManager() + class Meta: app_label = 'askbot' - db_table = 'askbot_replyaddress' \ No newline at end of file + db_table = 'askbot_replyaddress' + + def create_reply(self, content): + result = None + if self.post.post_type == 'answer' or self.post.post_type == 'comment': + result = self.user.post_comment(self.post, content) + elif self.post.post_type == 'question': + wordcount = len(content.rsplit()) + 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) + 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..84506143 --- /dev/null +++ b/askbot/skins/default/templates/instant_notification_reply_by_email.html @@ -0,0 +1,7 @@ +{% trans %} +{# Don't change the following line in the template. #} +======= Reply above this line. ====-=-= +You can post an answer or a comment by replying to this email. To reply to this email +you need {{reply_by_email_karma_threshold}} karma, you have {{receiving_user_karma}} karma. +{% endtrans %} +{% include 'instant_notification.html' %} \ No newline at end of file diff --git a/askbot/tests/reply_by_email_tests.py b/askbot/tests/reply_by_email_tests.py index c40d425d..c4d70c0b 100644 --- a/askbot/tests/reply_by_email_tests.py +++ b/askbot/tests/reply_by_email_tests.py @@ -8,7 +8,13 @@ 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, @@ -17,10 +23,37 @@ class ReplyAddressModelTests(AskbotTestCase): user = self.u2, question = self.question ) + + self.comment = self.post_comment(user = self.u2, parent_post = self.answer) - def test_creation(self): + 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") + + + 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") + + + 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) \ No newline at end of file -- cgit v1.2.3-1-g7c22 From e725455c9f6ce499f9cf9ab8d7c67b846ccccdd9 Mon Sep 17 00:00:00 2001 From: Vasil Vangelovski Date: Tue, 17 Jan 2012 02:53:24 +0100 Subject: Added basic handler for processing replies by email --- askbot/models/reply_by_email.py | 15 +++++++++- askbot/tests/reply_by_email_tests.py | 55 ++++++++++++++++++++++++++++++++++-- 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/askbot/models/reply_by_email.py b/askbot/models/reply_by_email.py index 124fce4b..0d187b46 100644 --- a/askbot/models/reply_by_email.py +++ b/askbot/models/reply_by_email.py @@ -4,19 +4,25 @@ import string from django.db import models from django.contrib.auth.models import User +from django.utils.translation import ugettext as _ 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): + return self.get(address = address, 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))) - if ReplyAddress.objects.filter(address = reply_address.address).count() == 0: + if self.filter(address = reply_address.address).count() == 0: break reply_address.save() return reply_address @@ -50,3 +56,10 @@ class ReplyAddress(models.Model): self.save() return result + +#TODO move this function to a more appropriate module +def process_reply_email(message, address, host): + reply_address = ReplyAddress.objects.get_unused(address) + separator = _("======= Reply above this line. ====-=-=") + reply_part = message.body().split(separator)[0] + reply_address.create_reply(reply_part) diff --git a/askbot/tests/reply_by_email_tests.py b/askbot/tests/reply_by_email_tests.py index c4d70c0b..f391827b 100644 --- a/askbot/tests/reply_by_email_tests.py +++ b/askbot/tests/reply_by_email_tests.py @@ -1,8 +1,54 @@ +from django.utils.translation import ugettext as _ from askbot.models import ReplyAddress +from askbot.models.reply_by_email import process_reply_email + 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_email = 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%s\nlorem ipsum"%(separator), "user1@domain.com") + process_reply_email(msg, addr, '') + self.assertEquals(self.answer.comments.count(), 2) + self.assertEquals(self.answer.comments.all().order_by('-timestamp')[0].text, "This is a test reply") + + class ReplyAddressModelTests(AskbotTestCase): @@ -32,12 +78,13 @@ class ReplyAddressModelTests(AskbotTestCase): 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) @@ -56,4 +103,8 @@ class ReplyAddressModelTests(AskbotTestCase): 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) \ No newline at end of file + self.assertEquals(post.text, "A test post "* 10) + + + + -- cgit v1.2.3-1-g7c22 From def1cb19113690a1dac93b42d457ec4279dd61f3 Mon Sep 17 00:00:00 2001 From: Vasil Vangelovski Date: Wed, 18 Jan 2012 00:52:24 +0100 Subject: Reply by email basic use case working with some edge-case handling missing --- askbot/models/reply_by_email.py | 40 ++++++++++++++++++---- .../default/templates/reply_by_email_error.html | 4 +++ askbot/tests/reply_by_email_tests.py | 4 +-- 3 files changed, 39 insertions(+), 9 deletions(-) create mode 100644 askbot/skins/default/templates/reply_by_email_error.html diff --git a/askbot/models/reply_by_email.py b/askbot/models/reply_by_email.py index 0d187b46..8ec43974 100644 --- a/askbot/models/reply_by_email.py +++ b/askbot/models/reply_by_email.py @@ -14,14 +14,14 @@ from askbot.conf import settings as askbot_settings class ReplyAddressManager(BaseQuerySetManager): - def get_unused(self, address): - return self.get(address = address, used_at__isnull = True) + 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))) + string.digits) for i in xrange(random.randint(12, 25))).lower() if self.filter(address = reply_address.address).count() == 0: break reply_address.save() @@ -59,7 +59,33 @@ class ReplyAddress(models.Model): #TODO move this function to a more appropriate module def process_reply_email(message, address, host): - reply_address = ReplyAddress.objects.get_unused(address) - separator = _("======= Reply above this line. ====-=-=") - reply_part = message.body().split(separator)[0] - reply_address.create_reply(reply_part) + + 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_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/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%} +

The system was unable to process your message successfully, the reason being:

+{%endtrans%} +{{error}} \ No newline at end of file diff --git a/askbot/tests/reply_by_email_tests.py b/askbot/tests/reply_by_email_tests.py index f391827b..4efa440b 100644 --- a/askbot/tests/reply_by_email_tests.py +++ b/askbot/tests/reply_by_email_tests.py @@ -10,7 +10,7 @@ class MockMessage(object): def __init__(self, body, from_email): self._body = body - self.from_email = from_email + self.From= from_email def body(self): return self._body @@ -46,7 +46,7 @@ class EmailProcessingTests(AskbotTestCase): msg = MockMessage("This is a test reply \n%s\nlorem ipsum"%(separator), "user1@domain.com") process_reply_email(msg, addr, '') self.assertEquals(self.answer.comments.count(), 2) - self.assertEquals(self.answer.comments.all().order_by('-timestamp')[0].text, "This is a test reply") + self.assertEquals(self.answer.comments.all().order_by('-pk')[0].text, "This is a test reply") -- cgit v1.2.3-1-g7c22 From b8e6e0f93ff70f7d846604969c38c54697ad448a Mon Sep 17 00:00:00 2001 From: Vasil Vangelovski Date: Wed, 18 Jan 2012 23:13:58 +0100 Subject: Separating quoted text in reply by email feature --- askbot/models/reply_by_email.py | 31 +++++++++++++++++++++++++++++++ askbot/tests/reply_by_email_tests.py | 5 +++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/askbot/models/reply_by_email.py b/askbot/models/reply_by_email.py index 8ec43974..43d5905d 100644 --- a/askbot/models/reply_by_email.py +++ b/askbot/models/reply_by_email.py @@ -57,6 +57,36 @@ class ReplyAddress(models.Model): return result +#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" + \ + "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 +""" + + #TODO move this function to a more appropriate module def process_reply_email(message, address, host): @@ -70,6 +100,7 @@ def process_reply_email(message, address, host): 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\ diff --git a/askbot/tests/reply_by_email_tests.py b/askbot/tests/reply_by_email_tests.py index 4efa440b..68aeda35 100644 --- a/askbot/tests/reply_by_email_tests.py +++ b/askbot/tests/reply_by_email_tests.py @@ -43,10 +43,11 @@ class EmailProcessingTests(AskbotTestCase): 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%s\nlorem ipsum"%(separator), "user1@domain.com") + 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_reply_email(msg, addr, '') self.assertEquals(self.answer.comments.count(), 2) - self.assertEquals(self.answer.comments.all().order_by('-pk')[0].text, "This is a test reply") + self.assertEquals(self.answer.comments.all().order_by('-pk')[0].text.strip(), "This is a test reply") -- cgit v1.2.3-1-g7c22 From d9958a6244e587996cec0f698141c432ee9ee7fa Mon Sep 17 00:00:00 2001 From: Vasil Vangelovski Date: Thu, 19 Jan 2012 00:08:53 +0100 Subject: Fixed posting replies to comments by email --- askbot/models/reply_by_email.py | 4 +++- askbot/tests/reply_by_email_tests.py | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/askbot/models/reply_by_email.py b/askbot/models/reply_by_email.py index 43d5905d..5c92f0a6 100644 --- a/askbot/models/reply_by_email.py +++ b/askbot/models/reply_by_email.py @@ -44,7 +44,7 @@ class ReplyAddress(models.Model): def create_reply(self, content): result = None - if self.post.post_type == 'answer' or self.post.post_type == 'comment': + if self.post.post_type == 'answer': result = self.user.post_comment(self.post, content) elif self.post.post_type == 'question': wordcount = len(content.rsplit()) @@ -52,6 +52,8 @@ class ReplyAddress(models.Model): 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/tests/reply_by_email_tests.py b/askbot/tests/reply_by_email_tests.py index 68aeda35..6aca55ec 100644 --- a/askbot/tests/reply_by_email_tests.py +++ b/askbot/tests/reply_by_email_tests.py @@ -92,6 +92,7 @@ class ReplyAddressModelTests(AskbotTestCase): 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): -- cgit v1.2.3-1-g7c22 From 414af13390777cea80accecf3fc8580e3d6d1dd6 Mon Sep 17 00:00:00 2001 From: Vasil Vangelovski Date: Wed, 25 Jan 2012 14:25:47 +0100 Subject: Work on documentation for reply by email feature --- askbot/doc/source/optional-modules.rst | 43 ++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/askbot/doc/source/optional-modules.rst b/askbot/doc/source/optional-modules.rst index 93f7129a..8f568e60 100644 --- a/askbot/doc/source/optional-modules.rst +++ b/askbot/doc/source/optional-modules.rst @@ -164,3 +164,46 @@ For `supervisor `_: 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 `_. + + +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 files, create a folder named ``run`` within your project folder by executing the following command: + + mkdir run + +The minimum settings required to enable this feature are defining the port and binding address for the lamson SMTP daemon and the +and the email handlers withing 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': '.+'} + +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 "myaskbot.com" the users will post replies to addresses such as "4wffsw345wsf@myaskbot.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. -- cgit v1.2.3-1-g7c22 From fb4522c75780562df3abfaf16d1cea5c81ffe5e7 Mon Sep 17 00:00:00 2001 From: Vasil Vangelovski Date: Wed, 25 Jan 2012 14:45:26 +0100 Subject: Work on documentation for reply by email feature --- askbot/doc/source/optional-modules.rst | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/askbot/doc/source/optional-modules.rst b/askbot/doc/source/optional-modules.rst index 8f568e60..6ab381ba 100644 --- a/askbot/doc/source/optional-modules.rst +++ b/askbot/doc/source/optional-modules.rst @@ -201,9 +201,6 @@ Note that in order to be able to bind the daemon to port 25 you will need to exe 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 "myaskbot.com" the users will post replies to addresses such as "4wffsw345wsf@myaskbot.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. +* 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 "myaskbot.com" the users will post replies to addresses such as "4wffsw345wsf@myaskbot.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. -- cgit v1.2.3-1-g7c22 From d23553c24c4a8f71d130ac5d5d78f335a49a2ccd Mon Sep 17 00:00:00 2001 From: Vasil Vangelovski Date: Wed, 25 Jan 2012 16:25:05 +0100 Subject: Adapted reply by email feature to work with django lamson --- askbot/doc/source/optional-modules.rst | 14 ++++--- askbot/lamson_handlers.py | 75 ++++++++++++++++++++++++++++++++++ askbot/models/reply_by_email.py | 63 +--------------------------- askbot/tests/reply_by_email_tests.py | 4 +- 4 files changed, 87 insertions(+), 69 deletions(-) create mode 100644 askbot/lamson_handlers.py diff --git a/askbot/doc/source/optional-modules.rst b/askbot/doc/source/optional-modules.rst index 6ab381ba..b574aeae 100644 --- a/askbot/doc/source/optional-modules.rst +++ b/askbot/doc/source/optional-modules.rst @@ -169,12 +169,11 @@ Then run **supervisorctl update** and it will be started. For more information a 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: +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 files, create a folder named ``run`` within your project folder by executing the following command: +The lamson daemon needs a folder to store it's mail queue files, create a folder named ``run`` within your project folder by executing the following command: mkdir run @@ -182,11 +181,14 @@ The minimum settings required to enable this feature are defining the port and b and the email handlers withing 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': '.+'} -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. +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: @@ -204,3 +206,5 @@ Within the askbot admin interface there are 4 significant configuration points f * 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 "myaskbot.com" the users will post replies to addresses such as "4wffsw345wsf@myaskbot.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 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 `_ . diff --git a/askbot/lamson_handlers.py b/askbot/lamson_handlers.py new file mode 100644 index 00000000..d91749c8 --- /dev/null +++ b/askbot/lamson_handlers.py @@ -0,0 +1,75 @@ +import re +import logging +from lamson.routing import route, route_like, stateless +from django.utils.translation import ugettext as _ +from askbot.models import ReplyAddress +from askbot.conf import settings as askbot_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" + \ + "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): + + 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/models/reply_by_email.py b/askbot/models/reply_by_email.py index 5c92f0a6..98f86caf 100644 --- a/askbot/models/reply_by_email.py +++ b/askbot/models/reply_by_email.py @@ -4,7 +4,7 @@ import string from django.db import models from django.contrib.auth.models import User -from django.utils.translation import ugettext as _ + from askbot.models.post import Post from askbot.models.base import BaseQuerySetManager @@ -59,66 +59,5 @@ class ReplyAddress(models.Model): return result -#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" + \ - "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 -""" - - -#TODO move this function to a more appropriate module -def process_reply_email(message, address, host): - - 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/tests/reply_by_email_tests.py b/askbot/tests/reply_by_email_tests.py index 6aca55ec..76097362 100644 --- a/askbot/tests/reply_by_email_tests.py +++ b/askbot/tests/reply_by_email_tests.py @@ -1,6 +1,6 @@ from django.utils.translation import ugettext as _ from askbot.models import ReplyAddress -from askbot.models.reply_by_email import process_reply_email +from askbot.lamson_handlers import PROCESS from askbot.tests.utils import AskbotTestCase @@ -45,7 +45,7 @@ class EmailProcessingTests(AskbotTestCase): 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_reply_email(msg, addr, '') + 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") -- cgit v1.2.3-1-g7c22 From e31c96c8bc4d4a521725e2d3fc66e5febc88c7a1 Mon Sep 17 00:00:00 2001 From: Vasil Vangelovski Date: Mon, 30 Jan 2012 10:57:15 +0100 Subject: Smarter message in instant notification when reply by email is enabled --- askbot/models/__init__.py | 18 ++++++++++++------ .../templates/instant_notification_reply_by_email.html | 9 ++++++++- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/askbot/models/__init__.py b/askbot/models/__init__.py index 656fa6f3..d6f84a57 100644 --- a/askbot/models/__init__.py +++ b/askbot/models/__init__.py @@ -2280,6 +2280,7 @@ def format_instant_notification_email( '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': site_url + post.get_absolute_url(), @@ -2317,15 +2318,18 @@ 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 @@ -2345,6 +2349,8 @@ def send_instant_notifications_about_activity_in_post( ) + + #todo: move to utils def calculate_gravatar_hash(instance, **kwargs): """Calculates a User's gravatar hash from their email address.""" diff --git a/askbot/skins/default/templates/instant_notification_reply_by_email.html b/askbot/skins/default/templates/instant_notification_reply_by_email.html index 84506143..ffb43110 100644 --- a/askbot/skins/default/templates/instant_notification_reply_by_email.html +++ b/askbot/skins/default/templates/instant_notification_reply_by_email.html @@ -1,7 +1,14 @@ + +{% if can_reply %} {% trans %} {# Don't change the following line in the template. #} ======= Reply above this line. ====-=-= -You can post an answer or a comment by replying to this email. To reply to this email +{% 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 -- cgit v1.2.3-1-g7c22 From d946e0b55332d96f3ba6281db1d5218e80c0f0d2 Mon Sep 17 00:00:00 2001 From: Vasil Vangelovski Date: Mon, 30 Jan 2012 11:34:03 +0100 Subject: Changed word counting method --- askbot/models/reply_by_email.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/askbot/models/reply_by_email.py b/askbot/models/reply_by_email.py index 98f86caf..7a575de7 100644 --- a/askbot/models/reply_by_email.py +++ b/askbot/models/reply_by_email.py @@ -47,7 +47,7 @@ class ReplyAddress(models.Model): if self.post.post_type == 'answer': result = self.user.post_comment(self.post, content) elif self.post.post_type == 'question': - wordcount = len(content.rsplit()) + wordcount = len(content)/6 if wordcount > askbot_settings.MIN_WORDS_FOR_ANSWER_BY_EMAIL: result = self.user.post_answer(self.post, content) else: -- cgit v1.2.3-1-g7c22 From 4c2851f37e2c1504bfce8735544d208158f31d72 Mon Sep 17 00:00:00 2001 From: Vasil Vangelovski Date: Tue, 31 Jan 2012 19:38:36 +0100 Subject: Forward rules for reply by email --- askbot/doc/source/optional-modules.rst | 27 +++++++++++++++++++++++---- askbot/lamson_handlers.py | 12 ++++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/askbot/doc/source/optional-modules.rst b/askbot/doc/source/optional-modules.rst index b574aeae..0c013121 100644 --- a/askbot/doc/source/optional-modules.rst +++ b/askbot/doc/source/optional-modules.rst @@ -173,12 +173,13 @@ Askbot supports posting replies by email. For this feature to work ``Lamson`` a pip install django-lamson -The lamson daemon needs a folder to store it's mail queue files, create a folder named ``run`` within your project folder by executing the following command: +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 -The minimum settings required to enable this feature are defining the port and binding address for the lamson SMTP daemon and the -and the email handlers withing askbot. Edit your settings.py file to include the following: + 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} @@ -203,8 +204,26 @@ Note that in order to be able to bind the daemon to port 25 you will need to exe 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 "myaskbot.com" the users will post replies to addresses such as "4wffsw345wsf@myaskbot.com", you need to point the MX DNS record for that domain to the address where you will run the lamson SMTP daemon. +* 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 `_ . diff --git a/askbot/lamson_handlers.py b/askbot/lamson_handlers.py index d91749c8..c39df01a 100644 --- a/askbot/lamson_handlers.py +++ b/askbot/lamson_handlers.py @@ -1,9 +1,11 @@ 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 @@ -42,6 +44,15 @@ def _strip_message_qoute(message_text): @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: @@ -73,3 +84,4 @@ def PROCESS(message, address = None, host = None): ) + -- cgit v1.2.3-1-g7c22