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:
parent
bfba84d526
commit
40efaae6d5
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue