mirror of
https://github.com/codeninjasllc/discourse.git
synced 2025-02-25 07:54:11 -05:00
Add Email-In-Per-Category
- allow the configuration of an inbox-email-address per category - post emails to that email into that category instead of global - Adds UI for configuration - Adds Documentation for configuration - Adds Tests for new feature
This commit is contained in:
parent
4af2cf3f23
commit
37cea49459
12 changed files with 193 additions and 14 deletions
|
@ -58,6 +58,10 @@ Discourse.EditCategoryController = Discourse.ObjectController.extend(Discourse.M
|
||||||
return false;
|
return false;
|
||||||
}.property('saving', 'name', 'color', 'deleting'),
|
}.property('saving', 'name', 'color', 'deleting'),
|
||||||
|
|
||||||
|
emailInEnabled: function() {
|
||||||
|
return Discourse.SiteSettings.email_in;
|
||||||
|
},
|
||||||
|
|
||||||
deleteDisabled: function() {
|
deleteDisabled: function() {
|
||||||
return (this.get('deleting') || this.get('saving') || false);
|
return (this.get('deleting') || this.get('saving') || false);
|
||||||
}.property('disabled', 'saving', 'deleting'),
|
}.property('disabled', 'saving', 'deleting'),
|
||||||
|
|
|
@ -66,6 +66,7 @@ Discourse.Category = Discourse.Model.extend({
|
||||||
permissions: this.get('permissionsForUpdate'),
|
permissions: this.get('permissionsForUpdate'),
|
||||||
auto_close_hours: this.get('auto_close_hours'),
|
auto_close_hours: this.get('auto_close_hours'),
|
||||||
position: this.get('position'),
|
position: this.get('position'),
|
||||||
|
email_in: this.get('email_in'),
|
||||||
parent_category_id: this.get('parent_category_id')
|
parent_category_id: this.get('parent_category_id')
|
||||||
},
|
},
|
||||||
type: this.get('id') ? 'PUT' : 'POST'
|
type: this.get('id') ? 'PUT' : 'POST'
|
||||||
|
|
|
@ -105,6 +105,18 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{{#if controller.emailInEnabled}}
|
||||||
|
<section class='field'>
|
||||||
|
<div class="email-in-fields">
|
||||||
|
<div>
|
||||||
|
<i class="fa fa-envelope-o"></i>
|
||||||
|
{{i18n category.email_in}}
|
||||||
|
{{textField value=email_in}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
<section class='field'>
|
<section class='field'>
|
||||||
<label>{{i18n category.position}}</label>
|
<label>{{i18n category.position}}</label>
|
||||||
<span {{action disableDefaultPosition}}>{{textField value=position disabled=defaultPosition class="position-input"}}</span>
|
<span {{action disableDefaultPosition}}>{{textField value=position disabled=defaultPosition class="position-input"}}</span>
|
||||||
|
|
|
@ -90,7 +90,7 @@ class CategoriesController < ApplicationController
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
params.permit(*required_param_keys, :position, :parent_category_id, :auto_close_hours, :permissions => [*p.try(:keys)])
|
params.permit(*required_param_keys, :position, :email_in, :parent_category_id, :auto_close_hours, :permissions => [*p.try(:keys)])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -335,6 +335,10 @@ SQL
|
||||||
self.where(id: slug.to_i, parent_category_id: parent_category_id).includes(:featured_users).first
|
self.where(id: slug.to_i, parent_category_id: parent_category_id).includes(:featured_users).first
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.find_by_email(email)
|
||||||
|
self.where(email_in: Email.downcase(email)).first
|
||||||
|
end
|
||||||
|
|
||||||
def has_children?
|
def has_children?
|
||||||
id && Category.where(parent_category_id: id).exists?
|
id && Category.where(parent_category_id: id).exists?
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,6 +5,7 @@ class CategorySerializer < BasicCategorySerializer
|
||||||
:auto_close_hours,
|
:auto_close_hours,
|
||||||
:group_permissions,
|
:group_permissions,
|
||||||
:position,
|
:position,
|
||||||
|
:email_in,
|
||||||
:can_delete
|
:can_delete
|
||||||
|
|
||||||
def group_permissions
|
def group_permissions
|
||||||
|
@ -35,4 +36,8 @@ class CategorySerializer < BasicCategorySerializer
|
||||||
scope && scope.can_delete?(object)
|
scope && scope.can_delete?(object)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def include_email_in?
|
||||||
|
scope && scope.can_edit?(object)
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -1078,6 +1078,7 @@ en:
|
||||||
security: "Security"
|
security: "Security"
|
||||||
auto_close_label: "Auto-close topics after:"
|
auto_close_label: "Auto-close topics after:"
|
||||||
auto_close_units: "hours"
|
auto_close_units: "hours"
|
||||||
|
email_in: "Custom incoming email address:"
|
||||||
edit_permissions: "Edit Permissions"
|
edit_permissions: "Edit Permissions"
|
||||||
add_permission: "Add Permission"
|
add_permission: "Add Permission"
|
||||||
this_year: "this year"
|
this_year: "this year"
|
||||||
|
|
|
@ -235,7 +235,9 @@ email:
|
||||||
pop3s_polling_port: 995
|
pop3s_polling_port: 995
|
||||||
pop3s_polling_username: ''
|
pop3s_polling_username: ''
|
||||||
pop3s_polling_password: ''
|
pop3s_polling_password: ''
|
||||||
email_in: false
|
email_in:
|
||||||
|
default: false
|
||||||
|
client: true
|
||||||
email_in_address: ''
|
email_in_address: ''
|
||||||
email_in_min_trust:
|
email_in_min_trust:
|
||||||
default: 3
|
default: 3
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
class AddCustomEmailInToCategories < ActiveRecord::Migration
|
||||||
|
def up
|
||||||
|
add_column :categories, :email_in, :string, null: true
|
||||||
|
add_index :categories, :email_in, unique: true
|
||||||
|
end
|
||||||
|
def down
|
||||||
|
remove_column :categories, :email_in
|
||||||
|
remove_index :categories, :email_in
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,19 +1,27 @@
|
||||||
## App Setup
|
## App Setup
|
||||||
|
|
||||||
|
Acting like a Mailing list is disabled per default in Discourse. This guide shows you through the way to enable and configure it.
|
||||||
|
|
||||||
## Admin UI Setup
|
## Admin UI Setup
|
||||||
|
|
||||||
Let's tell discourse to check for emails
|
First of, you need a POP3s enabled server receiving your email. Then make sure to enable "reply_by_email_enabled" and configured the server appropriately in your Admin-Settings under "Email":
|
||||||

|

|
||||||
Be sure to setup email as you would for POP3 based replies.
|
|
||||||
|
|
||||||
If users will be using discourse as a mailing list, allow them to opt-in
|
Once that is in place, you can enable the "email_in"-feature globally in the same email-section. If you provide another "email_in_address" all emails arriving in the inbox to that address will be handeled and posted to the "email_in_category" (defaults to "uncategorised"). For spam protection only users of a high trust level can post via email per default. You can change this via the "email_in_min_trust" setting.
|
||||||
|
|
||||||
|
### Per category email address
|
||||||
|
|
||||||
|
Once "email_in" is enabled globally a new configuration option appears in your category settings dialog allowing you to specify an email-address for that category. Emails going to the previously configured inbox to that email-address will be posted in this category instead of the default configuration. **Attention** User-Permissions and the minimum trust levels still apply.
|
||||||
|
|
||||||
|
### Troubleshooting
|
||||||
|
|
||||||
|
You might want to allow users to opt-in to receive all posts via email with the option on the bottom:
|
||||||

|

|
||||||
#TODO Document how to set this true by default
|
|
||||||
|
|
||||||
No way to enforce subject lines, so lower minimum topic length
|
As there is no way to enforce subject lines, you might want to lower minimum topic length, too
|
||||||

|

|
||||||
|
|
||||||
Emails may have the same subject, allow duplicate titles
|
And as some emails may have the same subject, allow duplicate titles might be another option you want to look at
|
||||||

|

|
||||||
|
|
||||||
## Suggested User Preferences
|
## Suggested User Preferences
|
||||||
|
@ -33,8 +41,7 @@ A: It will be rejected, and a notification email sent to the moderator. Check yo
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Q: Who did this?
|
Q: Who did this?
|
||||||
|
|
||||||
A: @therealx and @yesthatallen
|
A: @therealx, @yesthatallen and @ligthyear
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,20 @@ module Email
|
||||||
@raw = raw
|
@raw = raw
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def is_in_email?
|
||||||
|
if SiteSetting.email_in and SiteSetting.email_in_address == @message.to.first
|
||||||
|
@category_id = SiteSetting.email_in_category.to_i
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
category = Category.find_by_email(@message.to.first)
|
||||||
|
return false if not category
|
||||||
|
|
||||||
|
@category_id = category.id
|
||||||
|
return true
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
def process
|
def process
|
||||||
return Email::Receiver.results[:unprocessable] if @raw.blank?
|
return Email::Receiver.results[:unprocessable] if @raw.blank?
|
||||||
|
|
||||||
|
@ -32,7 +46,7 @@ module Email
|
||||||
|
|
||||||
return Email::Receiver.results[:unprocessable] if @body.blank?
|
return Email::Receiver.results[:unprocessable] if @body.blank?
|
||||||
|
|
||||||
if SiteSetting.email_in and @message.to.first == SiteSetting.email_in_address
|
if is_in_email?
|
||||||
@user = User.find_by_email(@message.from.first)
|
@user = User.find_by_email(@message.from.first)
|
||||||
return Email::Receiver.results[:unprocessable] if @user.blank? or not @user.has_trust_level?(TrustLevel.levels[SiteSetting.email_in_min_trust.to_i])
|
return Email::Receiver.results[:unprocessable] if @user.blank? or not @user.has_trust_level?(TrustLevel.levels[SiteSetting.email_in_min_trust.to_i])
|
||||||
|
|
||||||
|
@ -51,7 +65,7 @@ module Email
|
||||||
# Look up the email log for the reply key
|
# Look up the email log for the reply key
|
||||||
@email_log = EmailLog.for(reply_key)
|
@email_log = EmailLog.for(reply_key)
|
||||||
return Email::Receiver.results[:missing] if @email_log.blank?
|
return Email::Receiver.results[:missing] if @email_log.blank?
|
||||||
|
|
||||||
create_reply
|
create_reply
|
||||||
|
|
||||||
Email::Receiver.results[:processed]
|
Email::Receiver.results[:processed]
|
||||||
|
@ -144,7 +158,7 @@ module Email
|
||||||
# Try to post the body as a reply
|
# Try to post the body as a reply
|
||||||
topic_creator = TopicCreator.new(@user,
|
topic_creator = TopicCreator.new(@user,
|
||||||
Guardian.new(@user),
|
Guardian.new(@user),
|
||||||
category: SiteSetting.email_in_category.to_i,
|
category: @category_id,
|
||||||
title: @message.subject)
|
title: @message.subject)
|
||||||
|
|
||||||
topic = topic_creator.create
|
topic = topic_creator.create
|
||||||
|
@ -155,7 +169,7 @@ module Email
|
||||||
|
|
||||||
post_creator.create
|
post_creator.create
|
||||||
EmailLog.create(email_type: "topic_via_incoming_email",
|
EmailLog.create(email_type: "topic_via_incoming_email",
|
||||||
to_address: SiteSetting.email_in_address,
|
to_address: @message.to.first,
|
||||||
topic_id: topic.id, user_id: @user.id)
|
topic_id: topic.id, user_id: @user.id)
|
||||||
topic
|
topic
|
||||||
end
|
end
|
||||||
|
|
|
@ -301,4 +301,123 @@ Jakie" }
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
describe "processes an email to a category" do
|
||||||
|
before do
|
||||||
|
SiteSetting.stubs(:email_in_address).returns("")
|
||||||
|
SiteSetting.stubs(:email_in_category).returns("42")
|
||||||
|
SiteSetting.stubs(:email_in).returns(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:incoming_email) { File.read("#{Rails.root}/spec/fixtures/emails/valid_incoming.eml") }
|
||||||
|
let(:receiver) { Email::Receiver.new(incoming_email) }
|
||||||
|
let(:user) { Fabricate.build(:user, id: 3456) }
|
||||||
|
let(:category) { Fabricate.build(:category, id: 10) }
|
||||||
|
let(:subject) { "We should have a post-by-email-feature." }
|
||||||
|
let(:email_body) {
|
||||||
|
"Hey folks,
|
||||||
|
|
||||||
|
I was thinking. Wouldn't it be great if we could post topics via email? Yes it would!
|
||||||
|
|
||||||
|
Jakie" }
|
||||||
|
|
||||||
|
describe "category not found" do
|
||||||
|
|
||||||
|
before do
|
||||||
|
Category.expects(:find_by_email).returns(nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
let!(:result) { receiver.process }
|
||||||
|
|
||||||
|
it "returns missing" do
|
||||||
|
expect(result).to eq(Email::Receiver.results[:missing])
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "email from non user" do
|
||||||
|
|
||||||
|
before do
|
||||||
|
User.expects(:find_by_email).returns(nil)
|
||||||
|
Category.expects(:find_by_email).with(
|
||||||
|
"discourse-in@appmail.adventuretime.ooo").returns(category)
|
||||||
|
end
|
||||||
|
|
||||||
|
let!(:result) { receiver.process }
|
||||||
|
|
||||||
|
it "returns unprocessable" do
|
||||||
|
expect(result).to eq(Email::Receiver.results[:unprocessable])
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "email from untrusted user" do
|
||||||
|
before do
|
||||||
|
User.expects(:find_by_email).with(
|
||||||
|
"jake@adventuretime.ooo").returns(user)
|
||||||
|
Category.expects(:find_by_email).with(
|
||||||
|
"discourse-in@appmail.adventuretime.ooo").returns(category)
|
||||||
|
SiteSetting.stubs(:email_in_min_trust).returns(TrustLevel.levels[:elder].to_s)
|
||||||
|
end
|
||||||
|
|
||||||
|
let!(:result) { receiver.process }
|
||||||
|
|
||||||
|
it "returns unprocessable" do
|
||||||
|
expect(result).to eq(Email::Receiver.results[:unprocessable])
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "with proper user" do
|
||||||
|
|
||||||
|
before do
|
||||||
|
SiteSetting.stubs(:email_in_min_trust).returns(
|
||||||
|
TrustLevel.levels[:newuser].to_s)
|
||||||
|
User.expects(:find_by_email).with(
|
||||||
|
"jake@adventuretime.ooo").returns(user)
|
||||||
|
Category.expects(:find_by_email).with(
|
||||||
|
"discourse-in@appmail.adventuretime.ooo").returns(category)
|
||||||
|
|
||||||
|
topic_creator = mock()
|
||||||
|
TopicCreator.expects(:new).with(instance_of(User),
|
||||||
|
instance_of(Guardian),
|
||||||
|
has_entries(title: subject,
|
||||||
|
category: 10)) # Make sure it is posted to the right category
|
||||||
|
.returns(topic_creator)
|
||||||
|
|
||||||
|
topic_creator.expects(:create).returns(topic_creator)
|
||||||
|
topic_creator.expects(:id).twice.returns(12345)
|
||||||
|
|
||||||
|
|
||||||
|
post_creator = mock
|
||||||
|
PostCreator.expects(:new).with(instance_of(User),
|
||||||
|
has_entries(raw: email_body,
|
||||||
|
topic_id: 12345,
|
||||||
|
cooking_options: {traditional_markdown_linebreaks: true}))
|
||||||
|
.returns(post_creator)
|
||||||
|
|
||||||
|
post_creator.expects(:create)
|
||||||
|
|
||||||
|
EmailLog.expects(:create).with(has_entries(
|
||||||
|
email_type: 'topic_via_incoming_email',
|
||||||
|
to_address: "discourse-in@appmail.adventuretime.ooo",
|
||||||
|
user_id: 3456,
|
||||||
|
topic_id: 12345
|
||||||
|
))
|
||||||
|
end
|
||||||
|
|
||||||
|
let!(:result) { receiver.process }
|
||||||
|
|
||||||
|
it "returns a processed result" do
|
||||||
|
expect(result).to eq(Email::Receiver.results[:processed])
|
||||||
|
end
|
||||||
|
|
||||||
|
it "extracts the body" do
|
||||||
|
expect(receiver.body).to eq(email_body)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue