Mail handler: more control over issue attributes (#1110).

Tracker, category and priority attributes can be specified in command line arguments and/or individually specified as overridable by email body keywords.

git-svn-id: http://redmine.rubyforge.org/svn/trunk@1643 e93f8b46-1217-0410-a6f0-8f06a7374b81
This commit is contained in:
Jean-Philippe Lang 2008-07-06 16:26:25 +00:00
parent bfba84d526
commit 40efaae6d5
6 changed files with 214 additions and 49 deletions

View File

@ -23,7 +23,14 @@ class MailHandler < ActionMailer::Base
attr_reader :email, :user attr_reader :email, :user
def self.receive(email, options={}) def self.receive(email, options={})
@@handler_options = options @@handler_options = options.dup
@@handler_options[:issue] ||= {}
@@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip) if @@handler_options[:allow_override].is_a?(String)
@@handler_options[:allow_override] ||= []
# Project needs to be overridable if not specified
@@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
super email super email
end end
@ -66,11 +73,13 @@ class MailHandler < ActionMailer::Base
# Creates a new issue # Creates a new issue
def receive_issue def receive_issue
project = target_project project = target_project
# TODO: make the tracker configurable tracker = (get_keyword(:tracker) && project.trackers.find_by_name(get_keyword(:tracker))) || project.trackers.find(:first)
tracker = project.trackers.find(:first) category = (get_keyword(:category) && project.issue_categories.find_by_name(get_keyword(:category)))
priority = (get_keyword(:priority) && Enumeration.find_by_opt_and_name('IPRI', get_keyword(:priority)))
# check permission # check permission
raise UnauthorizedAction unless user.allowed_to?(:add_issues, project) raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
issue = Issue.new(:author => user, :project => project, :tracker => tracker) issue = Issue.new(:author => user, :project => project, :tracker => tracker, :category => category, :priority => priority)
issue.subject = email.subject.chomp issue.subject = email.subject.chomp
issue.description = email.plain_text_body.chomp issue.description = email.plain_text_body.chomp
issue.save! issue.save!
@ -84,13 +93,7 @@ class MailHandler < ActionMailer::Base
# TODO: other ways to specify project: # TODO: other ways to specify project:
# * parse the email To field # * parse the email To field
# * specific project (eg. Setting.mail_handler_target_project) # * specific project (eg. Setting.mail_handler_target_project)
identifier = if !@@handler_options[:project].blank? target = Project.find_by_identifier(get_keyword(: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? raise MissingInformation.new('Unable to determine target project') if target.nil?
target target
end end
@ -120,6 +123,14 @@ class MailHandler < ActionMailer::Base
end end
end end
end end
def get_keyword(attr)
if @@handler_options[:allow_override].include?(attr.to_s) && email.plain_text_body =~ /^#{attr}:[ \t]*(.+)$/i
$1.strip
elsif !@@handler_options[:issue][attr].blank?
@@handler_options[:issue][attr]
end
end
end end
class TMail::Mail class TMail::Mail

View File

@ -12,16 +12,22 @@ require 'getoptlong'
class RedmineMailHandler class RedmineMailHandler
VERSION = '0.1' VERSION = '0.1'
attr_accessor :verbose, :project, :url, :key attr_accessor :verbose, :issue_attributes, :allow_override, :url, :key
def initialize def initialize
self.issue_attributes = {}
opts = GetoptLong.new( opts = GetoptLong.new(
[ '--help', '-h', GetoptLong::NO_ARGUMENT ], [ '--help', '-h', GetoptLong::NO_ARGUMENT ],
[ '--version', '-V', GetoptLong::NO_ARGUMENT ], [ '--version', '-V', GetoptLong::NO_ARGUMENT ],
[ '--verbose', '-v', GetoptLong::NO_ARGUMENT ], [ '--verbose', '-v', GetoptLong::NO_ARGUMENT ],
[ '--url', '-u', GetoptLong::REQUIRED_ARGUMENT ], [ '--url', '-u', GetoptLong::REQUIRED_ARGUMENT ],
[ '--key', '-k', GetoptLong::REQUIRED_ARGUMENT], [ '--key', '-k', GetoptLong::REQUIRED_ARGUMENT],
[ '--project', '-p', GetoptLong::REQUIRED_ARGUMENT ] [ '--project', '-p', GetoptLong::REQUIRED_ARGUMENT ],
[ '--tracker', '-t', GetoptLong::REQUIRED_ARGUMENT],
[ '--category', GetoptLong::REQUIRED_ARGUMENT],
[ '--priority', GetoptLong::REQUIRED_ARGUMENT],
[ '--allow-override', '-o', GetoptLong::REQUIRED_ARGUMENT]
) )
opts.each do |opt, arg| opts.each do |opt, arg|
@ -36,8 +42,10 @@ class RedmineMailHandler
self.verbose = true self.verbose = true
when '--version' when '--version'
puts VERSION; exit puts VERSION; exit
when '--project' when '--project', '--tracker', '--category', '--priority'
self.project = arg.dup self.issue_attributes[opt.gsub(%r{^\-\-}, '')] = arg.dup
when '--allow-override'
self.allow_override = arg.dup
end end
end end
@ -46,8 +54,11 @@ class RedmineMailHandler
def submit(email) def submit(email)
uri = url.gsub(%r{/*$}, '') + '/mail_handler' uri = url.gsub(%r{/*$}, '') + '/mail_handler'
data = { 'key' => key, 'email' => email, 'allow_override' => allow_override }
issue_attributes.each { |attr, value| data["issue[#{attr}]"] = value }
debug "Posting to #{uri}..." debug "Posting to #{uri}..."
data = { 'key' => key, 'project' => project, 'email' => email }
response = Net::HTTP.post_form(URI.parse(uri), data) response = Net::HTTP.post_form(URI.parse(uri), data)
debug "Response received: #{response.code}" debug "Response received: #{response.code}"
response.code == 201 ? 0 : 1 response.code == 201 ? 0 : 1
@ -56,17 +67,39 @@ class RedmineMailHandler
private private
def usage def usage
puts "Usage: rdm-mailhandler [options] --url=<Redmine URL> --key=<API key>" puts <<-USAGE
puts "Reads an email from standard input and forward it to a Redmine server" Usage: rdm-mailhandler [options] --url=<Redmine URL> --key=<API key>
puts Reads an email from standard input and forward it to a Redmine server
puts "Options:"
puts " --help show this help" Required:
puts " --verbose show extra information" -u, --url URL of the Redmine server
puts " --project identifier of the target project" -k, --key Redmine API key
puts
puts "Examples:" General options:
puts " rdm-mailhandler --url http://redmine.domain.foo --key secret" -h, --help show this help
puts " rdm-mailhandler --url https://redmine.domain.foo --key secret --project foo" -v, --verbose show extra information
-V, --version show version information and exit
Issue attributes control options:
-p, --project=PROJECT identifier of the target project
-t, --tracker=TRACKER name of the target tracker
--category=CATEGORY name of the target category
--priority=PRIORITY name of the target priority
-o, --allow-override=ATTRS allow email content to override attributes
specified by previous options
ATTRS is a comma separated list of attributes
Examples:
# No project specified. Emails MUST contain the 'Project' keyword:
rdm-mailhandler --url http://redmine.domain.foo --key secret
# Fixed project and default tracker specified, but emails can override
# both tracker and priority attributes:
rdm-mailhandler --url https://domain.foo/redmine --key secret \\
--project foo \\
--tracker bug \\
--allow-override tracker,priority
USAGE
exit exit
end end

View File

@ -21,16 +21,31 @@ namespace :redmine do
desc <<-END_DESC desc <<-END_DESC
Read an email from standard input. Read an email from standard input.
Available options: Issue attributes control options:
* project => identifier of the project the issue should be added to project=PROJECT identifier of the target project
tracker=TRACKER name of the target tracker
Example: category=CATEGORY name of the target category
rake redmine:email:receive project=foo RAILS_ENV="production" priority=PRIORITY name of the target priority
allow_override=ATTRS allow email content to override attributes
specified by previous options
ATTRS is a comma separated list of attributes
Examples:
# No project specified. Emails MUST contain the 'Project' keyword:
rake redmine:email:read RAILS_ENV="production" < raw_email
# Fixed project and default tracker specified, but emails can override
# both tracker and priority attributes:
rake redmine:email:read RAILS_ENV="production" \\
project=foo \\
tracker=bug \\
allow_override=tracker,priority < raw_email
END_DESC END_DESC
task :receive => :environment do task :read => :environment do
options = {} options = { :issue => {} }
options[:project] = ENV['project'] if ENV['project'] %w(project tracker category priority).each { |a| options[:issue][a.to_sym] = ENV[a] if ENV[a] }
options[:allow_override] = ENV['allow_override'] if ENV['allow_override']
MailHandler.receive(STDIN.read, options) MailHandler.receive(STDIN.read, options)
end end
@ -39,17 +54,37 @@ END_DESC
Read emails from an IMAP server. Read emails from an IMAP server.
Available IMAP options: Available IMAP options:
* host => IMAP server host (default: 127.0.0.1) host=HOST IMAP server host (default: 127.0.0.1)
* port => IMAP server port (default: 143) port=PORT IMAP server port (default: 143)
* ssl => Use SSL? (default: false) ssl=SSL Use SSL? (default: false)
* username => IMAP account username=USERNAME IMAP account
* password => IMAP password password=PASSWORD IMAP password
* folder => IMAP folder to read (default: INBOX) folder=FOLDER IMAP folder to read (default: INBOX)
Other options:
* project => identifier of the project the issue should be added to Issue attributes control options:
project=PROJECT identifier of the target project
tracker=TRACKER name of the target tracker
category=CATEGORY name of the target category
priority=PRIORITY name of the target priority
allow_override=ATTRS allow email content to override attributes
specified by previous options
ATTRS is a comma separated list of attributes
Example: Examples:
rake redmine:email:receive_iamp host=imap.foo.bar username=redmine@somenet.foo password=xxx project=foo RAILS_ENV="production" # No project specified. Emails MUST contain the 'Project' keyword:
rake redmine:email:receive_iamp RAILS_ENV="production" \\
host=imap.foo.bar username=redmine@somenet.foo password=xxx
# Fixed project and default tracker specified, but emails can override
# both tracker and priority attributes:
rake redmine:email:receive_iamp RAILS_ENV="production" \\
host=imap.foo.bar username=redmine@somenet.foo password=xxx ssl=1 \\
project=foo \\
tracker=bug \\
allow_override=tracker,priority
END_DESC END_DESC
task :receive_imap => :environment do task :receive_imap => :environment do
@ -60,8 +95,9 @@ END_DESC
:password => ENV['password'], :password => ENV['password'],
:folder => ENV['folder']} :folder => ENV['folder']}
options = {} options = { :issue => {} }
options[:project] = ENV['project'] if ENV['project'] %w(project tracker category priority).each { |a| options[:issue][a.to_sym] = ENV[a] if ENV[a] }
options[:allow_override] = ENV['allow_override'] if ENV['allow_override']
Redmine::IMAP.check(imap_options, options) Redmine::IMAP.check(imap_options, options)
end end

View File

@ -9,3 +9,9 @@ issue_categories_002:
project_id: 1 project_id: 1
assigned_to_id: assigned_to_id:
id: 2 id: 2
issue_categories_003:
name: Stock management
project_id: 2
assigned_to_id:
id: 3

View File

@ -0,0 +1,43 @@
Return-Path: <jsmith@somenet.foo>
Received: from osiris ([127.0.0.1])
by OSIRIS
with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200
Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris>
From: "John Smith" <jsmith@somenet.foo>
To: <redmine@somenet.foo>
Subject: New ticket on a given project
Date: Sun, 22 Jun 2008 12:28:07 +0200
MIME-Version: 1.0
Content-Type: text/plain;
format=flowed;
charset="iso-8859-1";
reply-type=original
Content-Transfer-Encoding: 7bit
X-Priority: 3
X-MSMail-Priority: Normal
X-Mailer: Microsoft Outlook Express 6.00.2900.2869
X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869
Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet
turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus
blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti
sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In
in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras
sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum
id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus
eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique
sed, mauris. Pellentesque habitant morbi tristique senectus et netus et
malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse
platea dictumst.
Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque
sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem.
Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et,
dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed,
massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo
pulvinar dui, a gravida orci mi eget odio. Nunc a lacus.
Project: onlinestore
Tracker: Feature request
category: Stock management
priority: Urgent

View File

@ -18,7 +18,15 @@
require File.dirname(__FILE__) + '/../test_helper' require File.dirname(__FILE__) + '/../test_helper'
class MailHandlerTest < Test::Unit::TestCase class MailHandlerTest < Test::Unit::TestCase
fixtures :users, :projects, :enabled_modules, :roles, :members, :issues, :trackers, :enumerations fixtures :users, :projects,
:enabled_modules,
:roles,
:members,
:issues,
:trackers,
:projects_trackers,
:enumerations,
:issue_categories
FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler' FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler'
@ -38,8 +46,36 @@ class MailHandlerTest < Test::Unit::TestCase
assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.') assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
end end
def test_add_issue_with_attributes_override
issue = submit_email('ticket_with_attributes.eml', :allow_override => 'tracker,category,priority')
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_equal 'Feature request', issue.tracker.to_s
assert_equal 'Stock management', issue.category.to_s
assert_equal 'Urgent', issue.priority.to_s
assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
end
def test_add_issue_with_partial_attributes_override
issue = submit_email('ticket_with_attributes.eml', :issue => {:priority => 'High'}, :allow_override => ['tracker'])
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_equal 'Feature request', issue.tracker.to_s
assert_nil issue.category
assert_equal 'High', issue.priority.to_s
assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
end
def test_add_issue_with_attachment_to_specific_project def test_add_issue_with_attachment_to_specific_project
issue = submit_email('ticket_with_attachment.eml', :project => 'onlinestore') issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
assert issue.is_a?(Issue) assert issue.is_a?(Issue)
assert !issue.new_record? assert !issue.new_record?
issue.reload issue.reload