Merge pull request #2012 from ligthyear/incoming-emails

Advanced New-Topic via Email Feature
This commit is contained in:
Sam 2014-03-03 10:58:10 +11:00
commit 172e517b31
18 changed files with 671 additions and 58 deletions

View File

@ -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'),

View File

@ -66,6 +66,8 @@ 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'),
email_in_allow_strangers: this.get('email_in_allow_strangers'),
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'

View File

@ -105,6 +105,24 @@
</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>
<label class="checkbox-label">
{{view Ember.Checkbox checkedBinding="email_in_allow_strangers"}}
{{i18n category.email_in_allow_strangers}}
</label>
</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>

View File

@ -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, :email_in_allow_strangers, :parent_category_id, :auto_close_hours, :permissions => [*p.try(:keys)])
end end
end end

View File

@ -3,11 +3,14 @@
# #
require 'net/pop' require 'net/pop'
require_dependency 'email/receiver' require_dependency 'email/receiver'
require_dependency 'email/sender'
require_dependency 'email/message_builder'
module Jobs module Jobs
class PollMailbox < Jobs::Scheduled class PollMailbox < Jobs::Scheduled
every 5.minutes every 5.minutes
sidekiq_options retry: false sidekiq_options retry: false
include Email::BuildEmailHelper
def execute(args) def execute(args)
if SiteSetting.pop3s_polling_enabled? if SiteSetting.pop3s_polling_enabled?
@ -15,6 +18,26 @@ module Jobs
end end
end end
def handle_mail(mail)
begin
Email::Receiver.new(mail).process
rescue Email::Receiver::UserNotSufficientTrustLevelError => e
# inform the user about the rejection
@message = Mail::Message.new(mail)
clientMessage = RejectionMailer.send_trust_level(@message.from, @message.body)
email_sender = Email::Sender.new(clientMessage, :email_reject_trust_level)
email_sender.send
rescue Email::Receiver::ProcessingError
# all other ProcessingErrors are ok to be dropped
rescue StandardError => e
# Inform Admins about error
GroupMessage.create(Group[:admins].name, :email_error_notification,
{limit_once_per: false, message_params: {source: mail, error: e}})
ensure
mail.delete
end
end
def poll_pop3s def poll_pop3s
Net::POP3.enable_ssl(OpenSSL::SSL::VERIFY_NONE) Net::POP3.enable_ssl(OpenSSL::SSL::VERIFY_NONE)
Net::POP3.start(SiteSetting.pop3s_polling_host, Net::POP3.start(SiteSetting.pop3s_polling_host,
@ -23,9 +46,7 @@ module Jobs
SiteSetting.pop3s_polling_password) do |pop| SiteSetting.pop3s_polling_password) do |pop|
unless pop.mails.empty? unless pop.mails.empty?
pop.each do |mail| pop.each do |mail|
if Email::Receiver.new(mail.pop).process == Email::Receiver.results[:processed] handle_mail mail.pop
mail.delete
end
end end
end end
end end

View File

@ -0,0 +1,13 @@
require_dependency 'email/message_builder'
class RejectionMailer < ActionMailer::Base
include Email::BuildEmailHelper
def send_rejection(from, body)
build_email(from, template: 'email_reject_notification', from: from, body: body)
end
def send_trust_level(from, body, to)
build_email(from, template: 'email_reject_trust_level', to: to)
end
end

View File

@ -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

View File

@ -5,6 +5,8 @@ class CategorySerializer < BasicCategorySerializer
:auto_close_hours, :auto_close_hours,
:group_permissions, :group_permissions,
:position, :position,
:email_in,
:email_in_allow_strangers,
:can_delete :can_delete
def group_permissions def group_permissions
@ -35,4 +37,12 @@ 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
def include_email_in_allow_strangers?
scope && scope.can_edit?(object)
end
end end

View File

@ -1081,6 +1081,8 @@ 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:"
email_in_allow_strangers: "Accept emails from non-users (aka strangers)"
edit_permissions: "Edit Permissions" edit_permissions: "Edit Permissions"
add_permission: "Add Permission" add_permission: "Add Permission"
this_year: "this year" this_year: "this year"

View File

@ -791,6 +791,10 @@ en:
pop3s_polling_host: "The host to poll for email via POP3S" pop3s_polling_host: "The host to poll for email via POP3S"
pop3s_polling_username: "The username for the POP3S account to poll for email" pop3s_polling_username: "The username for the POP3S account to poll for email"
pop3s_polling_password: "The password for the POP3S account to poll for email" pop3s_polling_password: "The password for the POP3S account to poll for email"
email_in: "Allow users to post new topics via email"
email_in_address: "The email address the users can post new topics to. None means users can't post globally."
email_in_min_trust: "The minimum trust level an users needs to have to be allowed to post new topics via email"
email_in_category: "The category new emails are posted into"
enable_mailing_list_mode: "Allow users to (optionally) opt-in to mailing list mode via a user preference" enable_mailing_list_mode: "Allow users to (optionally) opt-in to mailing list mode via a user preference"
minimum_topics_similar: "How many topics need to exist in the database before similar topics are presented." minimum_topics_similar: "How many topics need to exist in the database before similar topics are presented."
@ -1107,6 +1111,24 @@ en:
subject_template: "Import completed successfully" subject_template: "Import completed successfully"
text_body_template: "The import was successful." text_body_template: "The import was successful."
email_error_notification:
subject_template: "Error parsing email"
text_body_template: |
This is an automated message to inform you that parsing the following incoming email failed.
Please review the following message.
Error - %{error}
%{source}
email_reject_trust_level:
subject_template: "Message rejected"
text_body_template: |
The message you've send to %{to} was rejected by the system.
You do not have the required trust to post new topics to this email address.
too_many_spam_flags: too_many_spam_flags:
subject_template: "New account blocked" subject_template: "New account blocked"
text_body_template: | text_body_template: |

View File

@ -235,6 +235,14 @@ email:
pop3s_polling_port: 995 pop3s_polling_port: 995
pop3s_polling_username: '' pop3s_polling_username: ''
pop3s_polling_password: '' pop3s_polling_password: ''
email_in:
default: false
client: true
email_in_address: ''
email_in_min_trust:
default: 3
enum: 'MinTrustToCreateTopicSetting'
email_in_category: -1
enable_mailing_list_mode: enable_mailing_list_mode:
default: false default: false
client: true client: true

View File

@ -0,0 +1,12 @@
class AddCustomEmailInToCategories < ActiveRecord::Migration
def up
add_column :categories, :email_in, :string, null: true
add_column :categories, :email_in_allow_strangers, :boolean, default: false
add_index :categories, :email_in, unique: true
end
def down
remove_column :categories, :email_in
remove_column :categories, :email_in_allow_strangers
remove_index :categories, :email_in
end
end

View File

@ -0,0 +1,49 @@
## 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
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":
![enable-reply-by-email](https://f.cloud.github.com/assets/2879972/2242953/97d5dd52-9d17-11e3-915e-037758cc68a7.png)
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.
Additionally, by checking the "accept non-user emails"-checkbox in the category settings, emails to the given email but from unknown email-addresses will be posted in the category by the System-User in a quoted fashion, showing the original email-address and content in the quotes.
### Troubleshooting
You might want to allow users to opt-in to receive all posts via email with the option on the bottom:
![enable-mailing-list-mode](https://f.cloud.github.com/assets/2879972/2242954/994ac2a6-9d17-11e3-8f1f-31e570905394.png)
As there is no way to enforce subject lines, you might want to lower minimum topic length, too
![lower-min-topic-length](https://f.cloud.github.com/assets/2879972/2242956/9b20df84-9d17-11e3-917b-d91c17fd88c3.png)
And as some emails may have the same subject, allow duplicate titles might be another option you want to look at
![allow-duplicate-titles](https://f.cloud.github.com/assets/2879972/2242957/9ce3ed70-9d17-11e3-88ae-b7f9b63145bf.png)
## Suggested User Preferences
![suggested-user-prefs](https://f.cloud.github.com/assets/2879972/2242958/9e866356-9d17-11e3-815d-164c78794b01.png)
## FAQ
Q: Why is this needed?
A: No matter how good a forum is, sometimes members need to ask a question and all they have is their mail client.
Q: What if a message is received from an email address which doesn't belong to an approved, registered user?
A: It will be rejected, and a notification email sent to the moderator. Check your POP mailbox to see the rejected email content.
Q: Who did this?
A: @therealx, @yesthatallen and @ligthyear

View File

@ -5,9 +5,12 @@
module Email module Email
class Receiver class Receiver
def self.results class ProcessingError < StandardError; end
@results ||= Enum.new(:unprocessable, :missing, :processed, :error) class EmailUnparsableError < ProcessingError; end
end class EmptyEmailError < ProcessingError; end
class UserNotFoundError < ProcessingError; end
class UserNotSufficientTrustLevelError < ProcessingError; end
class EmailLogNotFound < ProcessingError; end
attr_reader :body, :reply_key, :email_log attr_reader :body, :reply_key, :email_log
@ -15,43 +18,75 @@ module Email
@raw = raw @raw = raw
end end
def is_in_email?
@allow_strangers = false
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
@allow_strangers = category.email_in_allow_strangers
return true
end
def process def process
return Email::Receiver.results[:unprocessable] if @raw.blank? raise EmptyEmailError if @raw.blank?
@message = Mail::Message.new(@raw) @message = Mail::Message.new(@raw)
# First remove the known discourse stuff. # First remove the known discourse stuff.
parse_body parse_body
return Email::Receiver.results[:unprocessable] if @body.blank? raise EmptyEmailError if @body.blank?
# Then run the github EmailReplyParser on it in case we didn't catch it # Then run the github EmailReplyParser on it in case we didn't catch it
@body = EmailReplyParser.read(@body).visible_text.force_encoding('UTF-8') @body = EmailReplyParser.read(@body).visible_text.force_encoding('UTF-8')
discourse_email_parser discourse_email_parser
return Email::Receiver.results[:unprocessable] if @body.blank? raise EmailUnparsableError if @body.blank?
@reply_key = @message.to.first
# Extract the `reply_key` from the format the site has specified if is_in_email?
tokens = SiteSetting.reply_by_email_address.split("%{reply_key}") @user = User.find_by_email(@message.from.first)
tokens.each do |t| if @user.blank? and @allow_strangers
@reply_key.gsub!(t, "") if t.present? wrap_body_in_quote
@user = Discourse.system_user
end
raise UserNotFoundError if @user.blank?
raise UserNotSufficientTrustLevelError.new @user if not @user.has_trust_level?(TrustLevel.levels[SiteSetting.email_in_min_trust.to_i])
create_new_topic
else
@reply_key = @message.to.first
# Extract the `reply_key` from the format the site has specified
tokens = SiteSetting.reply_by_email_address.split("%{reply_key}")
tokens.each do |t|
@reply_key.gsub!(t, "") if t.present?
end
# Look up the email log for the reply key
@email_log = EmailLog.for(reply_key)
raise EmailLogNotFound if @email_log.blank?
create_reply
end end
# Look up the email log for the reply key
@email_log = EmailLog.for(reply_key)
return Email::Receiver.results[:missing] if @email_log.blank?
create_reply
Email::Receiver.results[:processed]
rescue
Email::Receiver.results[:error]
end end
private private
def wrap_body_in_quote
@body = "[quote=\"#{@message.from.first}\"]
#{@body}
[/quote]"
end
def parse_body def parse_body
html = nil html = nil
@ -131,5 +166,25 @@ module Email
creator.create creator.create
end end
def create_new_topic
# Try to post the body as a reply
topic_creator = TopicCreator.new(@user,
Guardian.new(@user),
category: @category_id,
title: @message.subject)
topic = topic_creator.create
post_creator = PostCreator.new(@user,
raw: @body,
topic_id: topic.id,
cooking_options: {traditional_markdown_linebreaks: true})
post_creator.create
EmailLog.create(email_type: "topic_via_incoming_email",
to_address: @message.to.first,
topic_id: topic.id, user_id: @user.id)
topic
end
end end
end end

View File

@ -0,0 +1,107 @@
# -*- encoding : utf-8 -*-
require 'spec_helper'
require 'email/receiver'
require 'jobs/scheduled/poll_mailbox'
require 'email/message_builder'
describe Jobs::PollMailbox do
describe "processing email" do
let!(:poller) { Jobs::PollMailbox.new }
let!(:receiver) { mock }
let!(:email) { mock }
before do
Email::Receiver.expects(:new).with(email).returns(receiver)
end
describe "all goes fine" do
it "email gets deleted" do
receiver.expects(:process)
email.expects(:delete)
poller.handle_mail(email)
end
end
describe "raises Untrusted error" do
before do
receiver.expects(:process).raises(Email::Receiver::UserNotSufficientTrustLevelError)
email.expects(:delete)
Mail::Message.expects(:new).returns(email)
email.expects(:from)
email.expects(:body)
clientMessage = mock
senderMock = mock
RejectionMailer.expects(:send_trust_level).returns(clientMessage)
Email::Sender.expects(:new).with(
clientMessage, :email_reject_trust_level).returns(senderMock)
senderMock.expects(:send)
end
it "sends a reply and deletes the email" do
poller.handle_mail(email)
end
end
describe "raises error" do
it "deletes email on ProcessingError" do
receiver.expects(:process).raises(Email::Receiver::ProcessingError)
email.expects(:delete)
poller.handle_mail(email)
end
it "deletes email on EmailUnparsableError" do
receiver.expects(:process).raises(Email::Receiver::EmailUnparsableError)
email.expects(:delete)
poller.handle_mail(email)
end
it "deletes email on EmptyEmailError" do
receiver.expects(:process).raises(Email::Receiver::EmptyEmailError)
email.expects(:delete)
poller.handle_mail(email)
end
it "deletes email on UserNotFoundError" do
receiver.expects(:process).raises(Email::Receiver::UserNotFoundError)
email.expects(:delete)
poller.handle_mail(email)
end
it "deletes email on EmailLogNotFound" do
receiver.expects(:process).raises(Email::Receiver::EmailLogNotFound)
email.expects(:delete)
poller.handle_mail(email)
end
it "informs admins on any other error" do
receiver.expects(:process).raises(TypeError)
email.expects(:delete)
GroupMessage.expects(:create) do |args|
args[0].should eq "admins"
args[1].shouled eq :email_error_notification
args[2].message_params.source.should eq email
args[2].message_params.error.should_be instance_of(TypeError)
end
poller.handle_mail(email)
end
end
end
end

View File

@ -7,25 +7,16 @@ describe Email::Receiver do
before do before do
SiteSetting.stubs(:reply_by_email_address).returns("reply+%{reply_key}@appmail.adventuretime.ooo") SiteSetting.stubs(:reply_by_email_address).returns("reply+%{reply_key}@appmail.adventuretime.ooo")
end SiteSetting.stubs(:email_in).returns(false)
describe "exception raised" do
it "returns error if it encountered an error processing" do
receiver = Email::Receiver.new("some email")
def receiver.parse_body
raise "ERROR HAPPENED!"
end
expect(receiver.process).to eq(Email::Receiver.results[:error])
end
end end
describe 'invalid emails' do describe 'invalid emails' do
it "returns unprocessable if the message is blank" do it "raises EmptyEmailError if the message is blank" do
expect(Email::Receiver.new("").process).to eq(Email::Receiver.results[:unprocessable]) expect { Email::Receiver.new("").process }.to raise_error(Email::Receiver::EmptyEmailError)
end end
it "returns unprocessable if the message is not an email" do it "raises EmailUnparsableError if the message is not an email" do
expect(Email::Receiver.new("asdf" * 30).process).to eq(Email::Receiver.results[:unprocessable]) expect { Email::Receiver.new("asdf" * 30).process}.to raise_error(Email::Receiver::EmptyEmailError)
end end
end end
@ -34,7 +25,7 @@ describe Email::Receiver do
let(:receiver) { Email::Receiver.new(reply_below) } let(:receiver) { Email::Receiver.new(reply_below) }
it "processes correctly" do it "processes correctly" do
receiver.process expect { receiver.process}.to raise_error(Email::Receiver::ProcessingError)
expect(receiver.body).to eq( expect(receiver.body).to eq(
"So presumably all the quoted garbage and my (proper) signature will get "So presumably all the quoted garbage and my (proper) signature will get
stripped from my reply?") stripped from my reply?")
@ -46,7 +37,7 @@ stripped from my reply?")
let(:receiver) { Email::Receiver.new(reply_below) } let(:receiver) { Email::Receiver.new(reply_below) }
it "processes correctly" do it "processes correctly" do
receiver.process expect { receiver.process}.to raise_error(Email::Receiver::ProcessingError)
expect(receiver.body).to eq("The EC2 instance - I've seen that there tends to be odd and " + expect(receiver.body).to eq("The EC2 instance - I've seen that there tends to be odd and " +
"unrecommended settings on the Bitnami installs that I've checked out.") "unrecommended settings on the Bitnami installs that I've checked out.")
end end
@ -57,7 +48,7 @@ stripped from my reply?")
let(:receiver) { Email::Receiver.new(attachment) } let(:receiver) { Email::Receiver.new(attachment) }
it "processes correctly" do it "processes correctly" do
expect(receiver.process).to eq(Email::Receiver.results[:unprocessable]) expect { receiver.process}.to raise_error(Email::Receiver::EmptyEmailError)
expect(receiver.body).to be_blank expect(receiver.body).to be_blank
end end
end end
@ -67,7 +58,7 @@ stripped from my reply?")
let(:receiver) { Email::Receiver.new(dutch) } let(:receiver) { Email::Receiver.new(dutch) }
it "processes correctly" do it "processes correctly" do
receiver.process expect { receiver.process}.to raise_error(Email::Receiver::ProcessingError)
expect(receiver.body).to eq("Dit is een antwoord in het Nederlands.") expect(receiver.body).to eq("Dit is een antwoord in het Nederlands.")
end end
end end
@ -78,7 +69,7 @@ stripped from my reply?")
it "processes correctly" do it "processes correctly" do
I18n.expects(:t).with('user_notifications.previous_discussion').returns('כלטוב') I18n.expects(:t).with('user_notifications.previous_discussion').returns('כלטוב')
receiver.process expect { receiver.process}.to raise_error(Email::Receiver::ProcessingError)
expect(receiver.body).to eq("שלום") expect(receiver.body).to eq("שלום")
end end
end end
@ -89,7 +80,7 @@ stripped from my reply?")
it "processes correctly" do it "processes correctly" do
I18n.expects(:t).with('user_notifications.previous_discussion').returns('媽!我上電視了!') I18n.expects(:t).with('user_notifications.previous_discussion').returns('媽!我上電視了!')
receiver.process expect { receiver.process}.to raise_error(Email::Receiver::ProcessingError)
expect(receiver.body).to eq("媽!我上電視了!") expect(receiver.body).to eq("媽!我上電視了!")
end end
end end
@ -99,7 +90,7 @@ stripped from my reply?")
let(:receiver) { Email::Receiver.new(wrote) } let(:receiver) { Email::Receiver.new(wrote) }
it "removes via lines if we know them" do it "removes via lines if we know them" do
receiver.process expect { receiver.process}.to raise_error(Email::Receiver::ProcessingError)
expect(receiver.body).to eq("Hello this email has content!") expect(receiver.body).to eq("Hello this email has content!")
end end
end end
@ -109,7 +100,7 @@ stripped from my reply?")
let(:receiver) { Email::Receiver.new(wrote) } let(:receiver) { Email::Receiver.new(wrote) }
it "processes correctly" do it "processes correctly" do
receiver.process expect { receiver.process}.to raise_error(Email::Receiver::ProcessingError)
expect(receiver.body).to eq("Thanks!") expect(receiver.body).to eq("Thanks!")
end end
end end
@ -119,7 +110,7 @@ stripped from my reply?")
let(:receiver) { Email::Receiver.new(previous) } let(:receiver) { Email::Receiver.new(previous) }
it "processes correctly" do it "processes correctly" do
receiver.process expect { receiver.process}.to raise_error(Email::Receiver::ProcessingError)
expect(receiver.body).to eq("This will not include the previous discussion that is present in this email.") expect(receiver.body).to eq("This will not include the previous discussion that is present in this email.")
end end
end end
@ -129,7 +120,7 @@ stripped from my reply?")
let(:receiver) { Email::Receiver.new(paragraphs) } let(:receiver) { Email::Receiver.new(paragraphs) }
it "processes correctly" do it "processes correctly" do
receiver.process expect { receiver.process}.to raise_error(Email::Receiver::ProcessingError)
expect(receiver.body).to eq( expect(receiver.body).to eq(
"Is there any reason the *old* candy can't be be kept in silos while the new candy "Is there any reason the *old* candy can't be be kept in silos while the new candy
is imported into *new* silos? is imported into *new* silos?
@ -160,10 +151,8 @@ greatest show ever created. Everyone should watch it.
EmailLog.expects(:for).returns(nil) EmailLog.expects(:for).returns(nil)
end end
let!(:result) { receiver.process } it "raises EmailLogNotFoundError" do
expect{ receiver.process }.to raise_error(Email::Receiver::EmailLogNotFound)
it "returns missing" do
expect(result).to eq(Email::Receiver.results[:missing])
end end
end end
@ -184,10 +173,6 @@ greatest show ever created. Everyone should watch it.
let!(:result) { receiver.process } let!(:result) { receiver.process }
it "returns a processed result" do
expect(result).to eq(Email::Receiver.results[:processed])
end
it "extracts the body" do it "extracts the body" do
expect(receiver.body).to eq(reply_body) expect(receiver.body).to eq(reply_body)
end end
@ -204,5 +189,282 @@ greatest show ever created. Everyone should watch it.
end end
describe "processes a valid incoming email" do
before do
SiteSetting.stubs(:email_in_address).returns("discourse-in@appmail.adventuretime.ooo")
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(: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 "email from non user" do
before do
User.expects(:find_by_email).returns(nil)
end
it "raises user not found error" do
expect { receiver.process }.to raise_error(Email::Receiver::UserNotFoundError)
end
end
describe "email from untrusted user" do
before do
User.expects(:find_by_email).with(
"jake@adventuretime.ooo").returns(user)
SiteSetting.stubs(:email_in_min_trust).returns(TrustLevel.levels[:elder].to_s)
end
it "raises untrusted user error" do
expect { receiver.process }.to raise_error(Email::Receiver::UserNotSufficientTrustLevelError)
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)
topic_creator = mock()
TopicCreator.expects(:new).with(instance_of(User),
instance_of(Guardian),
has_entries(title: subject,
category: 42))
.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 "extracts the body" do
expect(receiver.body).to eq(email_body)
end
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
it "raises EmailLogNotFoundError" do
expect{ receiver.process }.to raise_error(Email::Receiver::EmailLogNotFound)
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
it "raises UserNotFoundError" do
expect{ receiver.process }.to raise_error(Email::Receiver::UserNotFoundError)
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
it "raises untrusted user error" do
expect { receiver.process }.to raise_error(Email::Receiver::UserNotSufficientTrustLevelError)
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 "extracts the body" do
expect(receiver.body).to eq(email_body)
end
end
end
describe "processes an unknown email sender to 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(:non_inbox_email_category) { Fabricate.build(:category, id: 20, email_in_allow_strangers: false) }
let(:public_inbox_email_category) { Fabricate.build(:category, id: 25, email_in_allow_strangers: true) }
let(:subject) { "We should have a post-by-email-feature." }
let(:email_body) { "[quote=\"jake@adventuretime.ooo\"]
Hey folks,
I was thinking. Wouldn't it be great if we could post topics via email? Yes it would!
Jakie
[/quote]" }
describe "to disabled category" do
before do
User.expects(:find_by_email).with(
"jake@adventuretime.ooo").returns(nil)
Category.expects(:find_by_email).with(
"discourse-in@appmail.adventuretime.ooo").returns(non_inbox_email_category)
end
it "raises UserNotFoundError" do
expect{ receiver.process }.to raise_error(Email::Receiver::UserNotFoundError)
end
end
describe "to enabled category" do
before do
User.expects(:find_by_email).with(
"jake@adventuretime.ooo").returns(nil)
Category.expects(:find_by_email).with(
"discourse-in@appmail.adventuretime.ooo").returns(public_inbox_email_category)
topic_creator = mock()
TopicCreator.expects(:new).with(Discourse.system_user,
instance_of(Guardian),
has_entries(title: subject,
category: 25)) # 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(135)
post_creator = mock
PostCreator.expects(:new).with(Discourse.system_user,
has_entries(raw: email_body,
topic_id: 135,
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: Discourse.system_user.id,
topic_id: 135
))
end
let!(:result) { receiver.process }
it "extracts the body" do
expect(receiver.body).to eq(email_body)
end
end
end
end end

View File

@ -2,4 +2,3 @@ Fabricator(:category) do
name { sequence(:name) { |n| "Amazing Category #{n}" } } name { sequence(:name) { |n| "Amazing Category #{n}" } }
user user
end end

25
spec/fixtures/emails/valid_incoming.eml vendored Normal file
View File

@ -0,0 +1,25 @@
Return-Path: <jake@adventuretime.ooo>
Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <discourse-in@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400
Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <discourse-in@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700
Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
Date: Thu, 13 Jun 2013 17:03:48 -0400
From: Jake the Dog <jake@adventuretime.ooo>
To: discourse-in@appmail.adventuretime.ooo
Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
Subject: We should have a post-by-email-feature.
Mime-Version: 1.0
Content-Type: text/plain;
charset=ISO-8859-1
Content-Transfer-Encoding: 7bit
X-Sieve: CMU Sieve 2.2
X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
13 Jun 2013 14:03:48 -0700 (PDT)
X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
Hey folks,
I was thinking. Wouldn't it be great if we could post topics via email? Yes it would!
Jakie