OSDN Git Service

Adds basic support for issue creation via email (#1110).
authorJean-Philippe Lang <jp_lang@yahoo.fr>
Sun, 22 Jun 2008 10:45:03 +0000 (10:45 +0000)
committerJean-Philippe Lang <jp_lang@yahoo.fr>
Sun, 22 Jun 2008 10:45:03 +0000 (10:45 +0000)
git-svn-id: http://redmine.rubyforge.org/svn/trunk@1568 e93f8b46-1217-0410-a6f0-8f06a7374b81

app/models/mail_handler.rb
lib/tasks/email.rake [new file with mode: 0644]
test/fixtures/enabled_modules.yml
test/fixtures/enumerations.yml
test/fixtures/mail_handler/add_note_to_issue.txt [deleted file]
test/fixtures/mail_handler/ticket_on_given_project.eml [new file with mode: 0644]
test/fixtures/mail_handler/ticket_reply.eml [new file with mode: 0644]
test/fixtures/mail_handler/ticket_with_attachment.eml [new file with mode: 0644]
test/unit/mail_handler_test.rb

index 7a1d732..03e48e1 100644 (file)
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 
 class MailHandler < ActionMailer::Base
+
+  class UnauthorizedAction < StandardError; end
+  class MissingInformation < StandardError; end
+  
+  attr_reader :email, :user
+
+  def self.receive(email, options={})
+    @@handler_options = options
+    super email
+  end
   
   # Processes incoming emails
-  # Currently, it only supports adding a note to an existing issue
-  # by replying to the initial notification message
   def receive(email)
-    # find related issue by parsing the subject
-    m = email.subject.match %r{\[.*#(\d+)\]}
-    return unless m
-    issue = Issue.find_by_id(m[1])
+    @email = email
+    @user = User.find_active(:first, :conditions => {:mail => email.from.first})
+    unless @user
+      # Unknown user => the email is ignored
+      # TODO: ability to create the user's account
+      logger.info "MailHandler: email submitted by unknown user [#{email.from.first}]" if logger && logger.info
+      return false
+    end
+    User.current = @user
+    dispatch
+  end
+  
+  private
+
+  ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]+#(\d+)\]}
+  
+  def dispatch
+    if m = email.subject.match(ISSUE_REPLY_SUBJECT_RE)
+      receive_issue_update(m[1].to_i)
+    else
+      receive_issue
+    end
+  rescue ActiveRecord::RecordInvalid => e
+    # TODO: send a email to the user
+    logger.error e.message if logger
+    false
+  rescue MissingInformation => e
+    logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
+    false
+  rescue UnauthorizedAction => e
+    logger.error "MailHandler: unauthorized attempt from #{user}" if logger
+    false
+  end
+  
+  # Creates a new issue
+  def receive_issue
+    project = target_project
+    # TODO: make the tracker configurable
+    tracker = project.trackers.find(:first)
+    # check permission
+    raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
+    issue = Issue.new(:author => user, :project => project, :tracker => tracker)
+    issue.subject = email.subject.chomp
+    issue.description = email.plain_text_body.chomp
+    issue.save!
+    add_attachments(issue)
+    logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
+    Mailer.deliver_issue_add(issue) if Setting.notified_events.include?('issue_added')
+    issue
+  end
+  
+  def target_project
+    # TODO: other ways to specify project:
+    # * parse the email To field
+    # * specific project (eg. Setting.mail_handler_target_project)
+    identifier = if @@handler_options[:project]
+                   @@handler_options[:project]
+                 elsif email.plain_text_body =~ %r{^Project:[ \t]*(.+)$}i
+                    $1
+                 end
+                 
+    target = Project.find_by_identifier(identifier.to_s)
+    raise MissingInformation.new('Unable to determine target project') if target.nil?
+    target
+  end
+  
+  # Adds a note to an existing issue
+  def receive_issue_update(issue_id)
+    issue = Issue.find_by_id(issue_id)
     return unless issue
-    
-    # find user
-    user = User.find_active(:first, :conditions => {:mail => email.from.first})
-    return unless user
     # check permission
-    return unless user.allowed_to?(:add_issue_notes, issue.project)
-    
+    raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project)
     # add the note
-    issue.init_journal(user, email.body.chomp)
-    issue.save
+    journal = issue.init_journal(user, email.plain_text_body.chomp)
+    add_attachments(journal)
+    issue.save!
+    logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info
+    Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
+    journal
+  end
+  
+  def add_attachments(obj)
+    if email.has_attachments?
+      email.attachments.each do |attachment|
+        Attachment.create(:container => obj,
+                          :file => attachment,
+                          :author => user,
+                          :content_type => attachment.content_type)
+      end
+    end
   end
 end
+
+class TMail::Mail
+  # Returns body of the first plain text part found if any
+  def plain_text_body
+    return @plain_text_body unless @plain_text_body.nil?
+    p = self.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten
+    plain = p.detect {|c| c.content_type == 'text/plain'}
+    @plain_text_body = plain.nil? ? self.body : plain.body
+  end
+end
+
diff --git a/lib/tasks/email.rake b/lib/tasks/email.rake
new file mode 100644 (file)
index 0000000..dbdbe71
--- /dev/null
@@ -0,0 +1,37 @@
+# redMine - project management software\r
+# Copyright (C) 2006-2008  Jean-Philippe Lang\r
+#\r
+# This program is free software; you can redistribute it and/or\r
+# modify it under the terms of the GNU General Public License\r
+# as published by the Free Software Foundation; either version 2\r
+# of the License, or (at your option) any later version.\r
+# \r
+# This program is distributed in the hope that it will be useful,\r
+# but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
+# GNU General Public License for more details.\r
+# \r
+# You should have received a copy of the GNU General Public License\r
+# along with this program; if not, write to the Free Software\r
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.\r
+\r
+desc <<-END_DESC\r
+Read an email from standard input.\r
+\r
+Available options:\r
+  * project => identifier of the project the issue should be added to\r
+  \r
+Example:\r
+  rake redmine:email:receive project=foo RAILS_ENV="production"\r
+END_DESC\r
+\r
+namespace :redmine do\r
+  namespace :email do\r
+    task :receive => :environment do\r
+      options = {}\r
+      options[:project] = ENV['project'] if ENV['project']\r
+      \r
+      MailHandler.receive(STDIN.read, options)\r
+    end\r
+  end\r
+end\r
index 8d15655..da63bad 100644 (file)
@@ -39,4 +39,8 @@ enabled_modules_010:
   name: wiki
   project_id: 3
   id: 10
+enabled_modules_011: 
+  name: issue_tracking
+  project_id: 2
+  id: 11
   
\ No newline at end of file
index 5e26154..22a581a 100644 (file)
@@ -19,6 +19,7 @@ enumerations_005:
   name: Normal\r
   id: 5\r
   opt: IPRI\r
+  is_default: true\r
 enumerations_006: \r
   name: High\r
   id: 6\r
diff --git a/test/fixtures/mail_handler/add_note_to_issue.txt b/test/fixtures/mail_handler/add_note_to_issue.txt
deleted file mode 100644 (file)
index 4fc6b68..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-x-sender: <jsmith@somenet.foo>\r
-x-receiver: <redmine@somenet.foo>\r
-Received: from somenet.foo ([127.0.0.1]) by somenet.foo;\r
-       Sun, 25 Feb 2007 09:57:56 GMT\r
-Date: Sun, 25 Feb 2007 10:57:56 +0100\r
-From: jsmith@somenet.foo\r
-To: redmine@somenet.foo\r
-Message-Id: <45e15df440c00_b90238570a27b@osiris.tmail>\r
-In-Reply-To: <45e15df440c29_b90238570a27b@osiris.tmail>\r
-Subject: [Cookbook - Feature #2]\r
-Mime-Version: 1.0\r
-Content-Type: text/plain; charset=utf-8\r
-\r
-Note added by mail\r
diff --git a/test/fixtures/mail_handler/ticket_on_given_project.eml b/test/fixtures/mail_handler/ticket_on_given_project.eml
new file mode 100644 (file)
index 0000000..07c7f16
--- /dev/null
@@ -0,0 +1,41 @@
+Return-Path: <jsmith@somenet.foo>\r
+Received: from osiris ([127.0.0.1])\r
+       by OSIRIS\r
+       with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200\r
+Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris>\r
+From: "John Smith" <jsmith@somenet.foo>\r
+To: <redmine@somenet.foo>\r
+Subject: New ticket on a given project\r
+Date: Sun, 22 Jun 2008 12:28:07 +0200\r
+MIME-Version: 1.0\r
+Content-Type: text/plain;\r
+       format=flowed;\r
+       charset="iso-8859-1";\r
+       reply-type=original\r
+Content-Transfer-Encoding: 7bit\r
+X-Priority: 3\r
+X-MSMail-Priority: Normal\r
+X-Mailer: Microsoft Outlook Express 6.00.2900.2869\r
+X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869\r
+\r
+Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet \r
+turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus \r
+blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti \r
+sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In \r
+in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras \r
+sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum \r
+id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus \r
+eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique \r
+sed, mauris. Pellentesque habitant morbi tristique senectus et netus et \r
+malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse \r
+platea dictumst.\r
+\r
+Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque \r
+sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem. \r
+Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et, \r
+dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed, \r
+massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo \r
+pulvinar dui, a gravida orci mi eget odio. Nunc a lacus.\r
+\r
+Project: onlinestore\r
+\r
diff --git a/test/fixtures/mail_handler/ticket_reply.eml b/test/fixtures/mail_handler/ticket_reply.eml
new file mode 100644 (file)
index 0000000..99fcfa0
--- /dev/null
@@ -0,0 +1,73 @@
+Return-Path: <jsmith@somenet.foo>\r
+Received: from osiris ([127.0.0.1])\r
+       by OSIRIS\r
+       with hMailServer ; Sat, 21 Jun 2008 18:41:39 +0200\r
+Message-ID: <006a01c8d3bd$ad9baec0$0a00a8c0@osiris>\r
+From: "John Smith" <jsmith@somenet.foo>\r
+To: <redmine@somenet.foo>\r
+References: <485d0ad366c88_d7014663a025f@osiris.tmail>\r
+Subject: Re: [Cookbook - Feature #2] (New) Add ingredients categories\r
+Date: Sat, 21 Jun 2008 18:41:39 +0200\r
+MIME-Version: 1.0\r
+Content-Type: multipart/alternative;\r
+       boundary="----=_NextPart_000_0067_01C8D3CE.711F9CC0"\r
+X-Priority: 3\r
+X-MSMail-Priority: Normal\r
+X-Mailer: Microsoft Outlook Express 6.00.2900.2869\r
+X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869\r
+\r
+This is a multi-part message in MIME format.\r
+\r
+------=_NextPart_000_0067_01C8D3CE.711F9CC0\r
+Content-Type: text/plain;\r
+       charset="utf-8"\r
+Content-Transfer-Encoding: quoted-printable\r
+\r
+This is reply\r
+------=_NextPart_000_0067_01C8D3CE.711F9CC0\r
+Content-Type: text/html;\r
+       charset="utf-8"\r
+Content-Transfer-Encoding: quoted-printable\r
+\r
+=EF=BB=BF<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">\r
+<HTML><HEAD>\r
+<META http-equiv=3DContent-Type content=3D"text/html; charset=3Dutf-8">\r
+<STYLE>BODY {\r
+       FONT-SIZE: 0.8em; COLOR: #484848; FONT-FAMILY: Verdana, sans-serif\r
+}\r
+BODY H1 {\r
+       FONT-SIZE: 1.2em; MARGIN: 0px; FONT-FAMILY: "Trebuchet MS", Verdana, =\r
+sans-serif\r
+}\r
+A {\r
+       COLOR: #2a5685\r
+}\r
+A:link {\r
+       COLOR: #2a5685\r
+}\r
+A:visited {\r
+       COLOR: #2a5685\r
+}\r
+A:hover {\r
+       COLOR: #c61a1a\r
+}\r
+A:active {\r
+       COLOR: #c61a1a\r
+}\r
+HR {\r
+       BORDER-RIGHT: 0px; BORDER-TOP: 0px; BACKGROUND: #ccc; BORDER-LEFT: 0px; =\r
+WIDTH: 100%; BORDER-BOTTOM: 0px; HEIGHT: 1px\r
+}\r
+.footer {\r
+       FONT-SIZE: 0.8em; FONT-STYLE: italic\r
+}\r
+</STYLE>\r
+\r
+<META content=3D"MSHTML 6.00.2900.2883" name=3DGENERATOR></HEAD>\r
+<BODY bgColor=3D#ffffff>\r
+<DIV><SPAN class=3Dfooter><FONT face=3DArial color=3D#000000 =\r
+size=3D2>This is=20\r
+reply</FONT></DIV></SPAN></BODY></HTML>\r
+\r
+------=_NextPart_000_0067_01C8D3CE.711F9CC0--\r
+\r
diff --git a/test/fixtures/mail_handler/ticket_with_attachment.eml b/test/fixtures/mail_handler/ticket_with_attachment.eml
new file mode 100644 (file)
index 0000000..c85f6b4
--- /dev/null
@@ -0,0 +1,248 @@
+Return-Path: <jsmith@somenet.foo>\r
+Received: from osiris ([127.0.0.1])\r
+       by OSIRIS\r
+       with hMailServer ; Sat, 21 Jun 2008 15:53:25 +0200\r
+Message-ID: <002301c8d3a6$2cdf6950$0a00a8c0@osiris>\r
+From: "John Smith" <jsmith@somenet.foo>\r
+To: <redmine@somenet.foo>\r
+Subject: Ticket created by email with attachment\r
+Date: Sat, 21 Jun 2008 15:53:25 +0200\r
+MIME-Version: 1.0\r
+Content-Type: multipart/mixed;\r
+       boundary="----=_NextPart_000_001F_01C8D3B6.F05C5270"\r
+X-Priority: 3\r
+X-MSMail-Priority: Normal\r
+X-Mailer: Microsoft Outlook Express 6.00.2900.2869\r
+X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869\r
+\r
+This is a multi-part message in MIME format.\r
+\r
+------=_NextPart_000_001F_01C8D3B6.F05C5270\r
+Content-Type: multipart/alternative;\r
+       boundary="----=_NextPart_001_0020_01C8D3B6.F05C5270"\r
+\r
+\r
+------=_NextPart_001_0020_01C8D3B6.F05C5270\r
+Content-Type: text/plain;\r
+       charset="iso-8859-1"\r
+Content-Transfer-Encoding: quoted-printable\r
+\r
+This is  a new ticket with attachments\r
+------=_NextPart_001_0020_01C8D3B6.F05C5270\r
+Content-Type: text/html;\r
+       charset="iso-8859-1"\r
+Content-Transfer-Encoding: quoted-printable\r
+\r
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">\r
+<HTML><HEAD>\r
+<META http-equiv=3DContent-Type content=3D"text/html; =\r
+charset=3Diso-8859-1">\r
+<META content=3D"MSHTML 6.00.2900.2883" name=3DGENERATOR>\r
+<STYLE></STYLE>\r
+</HEAD>\r
+<BODY bgColor=3D#ffffff>\r
+<DIV><FONT face=3DArial size=3D2>This is&nbsp; a new ticket with=20\r
+attachments</FONT></DIV></BODY></HTML>\r
+\r
+------=_NextPart_001_0020_01C8D3B6.F05C5270--\r
+\r
+------=_NextPart_000_001F_01C8D3B6.F05C5270\r
+Content-Type: image/jpeg;\r
+       name="Paella.jpg"\r
+Content-Transfer-Encoding: base64\r
+Content-Disposition: attachment;\r
+       filename="Paella.jpg"\r
+\r
+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\r
+cbis+/WpUqUcMZKdF44n/9k=\r
+\r
+------=_NextPart_000_001F_01C8D3B6.F05C5270--\r
+\r
index d0fc68d..6bb638f 100644 (file)
@@ -20,38 +20,52 @@ require File.dirname(__FILE__) + '/../test_helper'
 class MailHandlerTest < Test::Unit::TestCase
   fixtures :users, :projects, :enabled_modules, :roles, :members, :issues, :trackers, :enumerations
   
-  FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures'
-  CHARSET = "utf-8"
-
-  include ActionMailer::Quoting
-
+  FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler'
+  
   def setup
-    ActionMailer::Base.delivery_method = :test
-    ActionMailer::Base.perform_deliveries = true
-    ActionMailer::Base.deliveries = []
-
-    @expected = TMail::Mail.new
-    @expected.set_content_type "text", "plain", { "charset" => CHARSET }
-    @expected.mime_version = '1.0'
+    ActionMailer::Base.deliveries.clear
   end
   
-  def test_add_note_to_issue
-    raw = read_fixture("add_note_to_issue.txt").join
-    MailHandler.receive(raw)
-
-    issue = Issue.find(2)
-    journal = issue.journals.find(:first, :order => "created_on DESC")
-    assert journal
-    assert_equal User.find_by_mail("jsmith@somenet.foo"), journal.user
-    assert_equal "Note added by mail", journal.notes
+  def test_add_issue
+    # This email contains: 'Project: onlinestore'
+    issue = submit_email('ticket_on_given_project.eml')
+    assert issue.is_a?(Issue)
+    assert !issue.new_record?
+    issue.reload
+    assert_equal 'New ticket on a given project', issue.subject
+    assert_equal User.find_by_login('jsmith'), issue.author
+    assert_equal Project.find(2), issue.project
+    assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
+  end
+  
+  def test_add_issue_with_attachment_to_specific_project
+    issue = submit_email('ticket_with_attachment.eml', :project => 'onlinestore')
+    assert issue.is_a?(Issue)
+    assert !issue.new_record?
+    issue.reload
+    assert_equal 'Ticket created by email with attachment', issue.subject
+    assert_equal User.find_by_login('jsmith'), issue.author
+    assert_equal Project.find(2), issue.project
+    assert_equal 'This is  a new ticket with attachments', issue.description
+    # Attachment properties
+    assert_equal 1, issue.attachments.size
+    assert_equal 'Paella.jpg', issue.attachments.first.filename
+    assert_equal 'image/jpeg', issue.attachments.first.content_type
+    assert_equal 10790, issue.attachments.first.filesize
+  end
+  
+  def test_add_issue_note
+    journal = submit_email('ticket_reply.eml')
+    assert journal.is_a?(Journal)
+    assert_equal User.find_by_login('jsmith'), journal.user
+    assert_equal Issue.find(2), journal.journalized
+    assert_equal 'This is reply', journal.notes
   end
 
   private
-    def read_fixture(action)
-      IO.readlines("#{FIXTURES_PATH}/mail_handler/#{action}")
-    end
-
-    def encode(subject)
-      quoted_printable(subject, CHARSET)
-    end
+  
+  def submit_email(filename, options={})
+    raw = IO.read(File.join(FIXTURES_PATH, filename))
+    MailHandler.receive(raw, options)
+  end
 end