From 93d1cc6294085008e025bb3032b231a6a81c6480 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?R=C3=A9gis=20Hanol?= <regis@hanol.fr>
Date: Thu, 10 Dec 2015 23:49:16 +0100
Subject: [PATCH] add support for incoming emails in CC/BCC fields

---
 app/jobs/scheduled/poll_mailbox.rb      | 11 +++----
 app/models/email_log.rb                 | 14 ++++++---
 lib/email/receiver.rb                   | 16 +++++-----
 spec/components/email/receiver_spec.rb  | 42 ++++++++++++++++++++-----
 spec/fixtures/emails/valid_incoming.eml |  1 +
 5 files changed, 59 insertions(+), 25 deletions(-)

diff --git a/app/jobs/scheduled/poll_mailbox.rb b/app/jobs/scheduled/poll_mailbox.rb
index 68a1f986322..4dcf3f2882c 100644
--- a/app/jobs/scheduled/poll_mailbox.rb
+++ b/app/jobs/scheduled/poll_mailbox.rb
@@ -14,9 +14,7 @@ module Jobs
 
     def execute(args)
       @args = args
-      if SiteSetting.pop3_polling_enabled?
-        poll_pop3
-      end
+      poll_pop3 if SiteSetting.pop3_polling_enabled?
     end
 
     def handle_mail(mail)
@@ -31,7 +29,6 @@ module Jobs
     end
 
     def handle_failure(mail_string, e)
-
       Rails.logger.warn("Email can not be processed: #{e}\n\n#{mail_string}") if SiteSetting.log_mail_processing_failures
 
       template_args = {}
@@ -90,14 +87,14 @@ module Jobs
 
       connection.start(SiteSetting.pop3_polling_username, SiteSetting.pop3_polling_password) do |pop|
         unless pop.mails.empty?
-          pop.each do |mail|
-            handle_mail(mail)
-          end
+          pop.each { |mail| handle_mail(mail) }
         end
         pop.finish
       end
     rescue Net::POPAuthenticationError => e
       Discourse.handle_job_exception(e, error_context(@args, "Signing in to poll incoming email"))
+    rescue Net::POPError => e
+      Discourse.handle_job_exception(e, error_context(@args, "Generic POP error"))
     end
 
   end
diff --git a/app/models/email_log.rb b/app/models/email_log.rb
index bb324e1a73a..d806f3ef968 100644
--- a/app/models/email_log.rb
+++ b/app/models/email_log.rb
@@ -10,19 +10,25 @@ class EmailLog < ActiveRecord::Base
 
   after_create do
     # Update last_emailed_at if the user_id is present and email was sent
-    User.where(id: user_id).update_all("last_emailed_at = CURRENT_TIMESTAMP") if user_id.present? and !skipped
+    User.where(id: user_id).update_all("last_emailed_at = CURRENT_TIMESTAMP") if user_id.present? && !skipped
   end
 
   def self.count_per_day(start_date, end_date)
-    where('created_at >= ? and created_at < ? AND skipped = false', start_date, end_date).group('date(created_at)').order('date(created_at)').count
+    sent.where("created_at BETWEEN ? AND ?", start_date, end_date)
+        .group("DATE(created_at)")
+        .order("DATE(created_at)")
+        .count
   end
 
   def self.for(reply_key)
-    EmailLog.find_by(reply_key: reply_key)
+    self.find_by(reply_key: reply_key)
   end
 
   def self.last_sent_email_address
-    where(email_type: 'signup').order('created_at DESC').first.try(:to_address)
+    self.where(email_type: "signup")
+        .order(created_at: :desc)
+        .first
+        .try(:to_address)
   end
 
 end
diff --git a/lib/email/receiver.rb b/lib/email/receiver.rb
index 07f89304e3b..3753cadd6c7 100644
--- a/lib/email/receiver.rb
+++ b/lib/email/receiver.rb
@@ -34,7 +34,8 @@ module Email
       body = parse_body(message)
 
       dest_info = { type: :invalid, obj: nil }
-      message.to.each do |to_address|
+      # 'smtp_envelope_to' is a combination of: to, cc and bcc fields
+      message.smtp_envelope_to.each do |to_address|
         dest_info = check_address(to_address)
         break if dest_info[:type] != :invalid
       end
@@ -51,7 +52,6 @@ module Email
 
       case dest_info[:type]
       when :group
-        raise BadDestinationAddress unless SiteSetting.email_in
         group = dest_info[:obj]
 
         if @user.blank?
@@ -68,7 +68,6 @@ module Email
 
         create_new_topic(archetype: Archetype.private_message, target_group_names: [group.name])
       when :category
-        raise BadDestinationAddress unless SiteSetting.email_in
         category = dest_info[:obj]
 
         if @user.blank? && category.email_in_allow_strangers
@@ -108,11 +107,14 @@ module Email
     end
 
     def check_address(address)
-      group = Group.find_by_email(address)
-      return { type: :group, obj: group } if group
+      # only check groups/categories when 'email_in' is enabled
+      if SiteSetting.email_in
+        group = Group.find_by_email(address)
+        return { type: :group, obj: group } if group
 
-      category = Category.find_by_email(address)
-      return { type: :category, obj: category } if category
+        category = Category.find_by_email(address)
+        return { type: :category, obj: category } if category
+      end
 
       regex = Regexp.escape(SiteSetting.reply_by_email_address)
       regex = regex.gsub(Regexp.escape('%{reply_key}'), "(.*)")
diff --git a/spec/components/email/receiver_spec.rb b/spec/components/email/receiver_spec.rb
index b4af327b6a4..da5f792e752 100644
--- a/spec/components/email/receiver_spec.rb
+++ b/spec/components/email/receiver_spec.rb
@@ -474,16 +474,17 @@ This is a link http://example.com"
 
   end
 
-  def fill_email(mail, from, to, body = nil, subject = nil)
+  def fill_email(mail, from, to, body = nil, subject = nil, cc = nil)
     result = mail.gsub("FROM", from).gsub("TO", to)
     result.gsub!(/Hey.*/m, body)  if body
     result.sub!(/We .*/, subject) if subject
+    result.sub!("CC", cc.presence || "")
     result
   end
 
   def process_email(opts)
     incoming_email = fixture_file("emails/valid_incoming.eml")
-    email = fill_email(incoming_email, opts[:from],  opts[:to], opts[:body], opts[:subject])
+    email = fill_email(incoming_email, opts[:from],  opts[:to], opts[:body], opts[:subject], opts[:cc])
     Email::Receiver.new(email).process
   end
 
@@ -532,15 +533,15 @@ greatest show ever created. Everyone should watch it.
   end
 
   describe "processes an email to a category" do
+    let(:to) { "some@email.com" }
+
     before do
       SiteSetting.email_in = true
+      SiteSetting.email_in_min_trust = TrustLevel[4].to_s
     end
 
     it "correctly can target categories" do
-      to = "some@email.com"
-
       Fabricate(:category, email_in_allow_strangers: false, email_in: to)
-      SiteSetting.email_in_min_trust = TrustLevel[4].to_s
 
       # no email in for user
       expect{
@@ -614,7 +615,7 @@ greatest show ever created. Everyone should watch it.
       }.to raise_error(Email::Receiver::UserNotFoundError)
     end
 
-    it "creates a topic for allowed category" do
+    it "creates a topic for matching category" do
       Fabricate(:category, email_in_allow_strangers: true, email_in: email_in)
       process_email(from: user_email, to: email_in, body: body)
 
@@ -636,7 +637,7 @@ greatest show ever created. Everyone should watch it.
       SiteSetting.allow_staged_accounts = true
     end
 
-    it "creates a message for allowed group" do
+    it "creates a message for matching group" do
       Fabricate(:group, incoming_email: incoming_email)
       process_email(from: user_email, to: incoming_email, body: body)
 
@@ -652,4 +653,31 @@ greatest show ever created. Everyone should watch it.
 
   end
 
+  describe "supports incoming mail in CC fields" do
+
+    let(:incoming_email) { "foo@bar.com" }
+    let(:user_email) { "#{SecureRandom.hex(32)}@foobar.com" }
+    let(:body) { "This is a message to\n\na group via CC ;)" }
+
+    before do
+      SiteSetting.email_in = true
+      SiteSetting.allow_staged_accounts = true
+    end
+
+    it "creates a message for matching group" do
+      Fabricate(:group, incoming_email: incoming_email)
+      process_email(from: user_email, to: "some@email.com", body: body, cc: incoming_email)
+
+      staged_account = User.find_by_email(user_email)
+      expect(staged_account).to be
+      expect(staged_account.staged).to be(true)
+
+      post = staged_account.posts.order(id: :desc).first
+      expect(post).to be
+      expect(post.raw).to eq(body)
+      expect(post.topic.private_message?).to eq(true)
+    end
+
+  end
+
 end
diff --git a/spec/fixtures/emails/valid_incoming.eml b/spec/fixtures/emails/valid_incoming.eml
index 5e8db53319e..cad475d662e 100644
--- a/spec/fixtures/emails/valid_incoming.eml
+++ b/spec/fixtures/emails/valid_incoming.eml
@@ -6,6 +6,7 @@ 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 <FROM>
 To: <TO>
+Cc: <CC>
 Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
 Subject: We should have a post-by-email-feature.
 Mime-Version: 1.0