@@ -0,0 +1,43 | |||||
|
1 | Return-Path: <jsmith@somenet.foo> | |||
|
2 | Received: from osiris ([127.0.0.1]) | |||
|
3 | by OSIRIS | |||
|
4 | with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200 | |||
|
5 | Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris> | |||
|
6 | From: "John Smith" <jsmith@somenet.foo> | |||
|
7 | To: <redmine@somenet.foo> | |||
|
8 | Subject: New ticket on a given project | |||
|
9 | Date: Sun, 22 Jun 2008 12:28:07 +0200 | |||
|
10 | MIME-Version: 1.0 | |||
|
11 | Content-Type: text/plain; | |||
|
12 | format=flowed; | |||
|
13 | charset="iso-8859-1"; | |||
|
14 | reply-type=original | |||
|
15 | Content-Transfer-Encoding: 7bit | |||
|
16 | X-Priority: 3 | |||
|
17 | X-MSMail-Priority: Normal | |||
|
18 | X-Mailer: Microsoft Outlook Express 6.00.2900.2869 | |||
|
19 | X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869 | |||
|
20 | ||||
|
21 | Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet | |||
|
22 | turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus | |||
|
23 | blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti | |||
|
24 | sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In | |||
|
25 | in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras | |||
|
26 | sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum | |||
|
27 | id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus | |||
|
28 | eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique | |||
|
29 | sed, mauris. Pellentesque habitant morbi tristique senectus et netus et | |||
|
30 | malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse | |||
|
31 | platea dictumst. | |||
|
32 | ||||
|
33 | Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque | |||
|
34 | sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem. | |||
|
35 | Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et, | |||
|
36 | dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed, | |||
|
37 | massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo | |||
|
38 | pulvinar dui, a gravida orci mi eget odio. Nunc a lacus. | |||
|
39 | ||||
|
40 | Project: onlinestore | |||
|
41 | Tracker: Feature request | |||
|
42 | category: Stock management | |||
|
43 | priority: Urgent |
@@ -23,7 +23,14 class MailHandler < ActionMailer::Base | |||||
23 | attr_reader :email, :user |
|
23 | attr_reader :email, :user | |
24 |
|
24 | |||
25 | def self.receive(email, options={}) |
|
25 | def self.receive(email, options={}) | |
26 | @@handler_options = options |
|
26 | @@handler_options = options.dup | |
|
27 | ||||
|
28 | @@handler_options[:issue] ||= {} | |||
|
29 | ||||
|
30 | @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip) if @@handler_options[:allow_override].is_a?(String) | |||
|
31 | @@handler_options[:allow_override] ||= [] | |||
|
32 | # Project needs to be overridable if not specified | |||
|
33 | @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project) | |||
27 | super email |
|
34 | super email | |
28 | end |
|
35 | end | |
29 |
|
36 | |||
@@ -66,11 +73,13 class MailHandler < ActionMailer::Base | |||||
66 | # Creates a new issue |
|
73 | # Creates a new issue | |
67 | def receive_issue |
|
74 | def receive_issue | |
68 | project = target_project |
|
75 | project = target_project | |
69 | # TODO: make the tracker configurable |
|
76 | tracker = (get_keyword(:tracker) && project.trackers.find_by_name(get_keyword(:tracker))) || project.trackers.find(:first) | |
70 | tracker = project.trackers.find(:first) |
|
77 | category = (get_keyword(:category) && project.issue_categories.find_by_name(get_keyword(:category))) | |
|
78 | priority = (get_keyword(:priority) && Enumeration.find_by_opt_and_name('IPRI', get_keyword(:priority))) | |||
|
79 | ||||
71 | # check permission |
|
80 | # check permission | |
72 | raise UnauthorizedAction unless user.allowed_to?(:add_issues, project) |
|
81 | raise UnauthorizedAction unless user.allowed_to?(:add_issues, project) | |
73 | issue = Issue.new(:author => user, :project => project, :tracker => tracker) |
|
82 | issue = Issue.new(:author => user, :project => project, :tracker => tracker, :category => category, :priority => priority) | |
74 | issue.subject = email.subject.chomp |
|
83 | issue.subject = email.subject.chomp | |
75 | issue.description = email.plain_text_body.chomp |
|
84 | issue.description = email.plain_text_body.chomp | |
76 | issue.save! |
|
85 | issue.save! | |
@@ -84,13 +93,7 class MailHandler < ActionMailer::Base | |||||
84 | # TODO: other ways to specify project: |
|
93 | # TODO: other ways to specify project: | |
85 | # * parse the email To field |
|
94 | # * parse the email To field | |
86 | # * specific project (eg. Setting.mail_handler_target_project) |
|
95 | # * specific project (eg. Setting.mail_handler_target_project) | |
87 | identifier = if !@@handler_options[:project].blank? |
|
96 | target = Project.find_by_identifier(get_keyword(:project)) | |
88 | @@handler_options[:project] |
|
|||
89 | elsif email.plain_text_body =~ %r{^Project:[ \t]*(.+)$}i |
|
|||
90 | $1 |
|
|||
91 | end |
|
|||
92 |
|
||||
93 | target = Project.find_by_identifier(identifier.to_s) |
|
|||
94 | raise MissingInformation.new('Unable to determine target project') if target.nil? |
|
97 | raise MissingInformation.new('Unable to determine target project') if target.nil? | |
95 | target |
|
98 | target | |
96 | end |
|
99 | end | |
@@ -120,6 +123,14 class MailHandler < ActionMailer::Base | |||||
120 | end |
|
123 | end | |
121 | end |
|
124 | end | |
122 | end |
|
125 | end | |
|
126 | ||||
|
127 | def get_keyword(attr) | |||
|
128 | if @@handler_options[:allow_override].include?(attr.to_s) && email.plain_text_body =~ /^#{attr}:[ \t]*(.+)$/i | |||
|
129 | $1.strip | |||
|
130 | elsif !@@handler_options[:issue][attr].blank? | |||
|
131 | @@handler_options[:issue][attr] | |||
|
132 | end | |||
|
133 | end | |||
123 | end |
|
134 | end | |
124 |
|
135 | |||
125 | class TMail::Mail |
|
136 | class TMail::Mail |
@@ -12,16 +12,22 require 'getoptlong' | |||||
12 | class RedmineMailHandler |
|
12 | class RedmineMailHandler | |
13 | VERSION = '0.1' |
|
13 | VERSION = '0.1' | |
14 |
|
14 | |||
15 |
attr_accessor :verbose, : |
|
15 | attr_accessor :verbose, :issue_attributes, :allow_override, :url, :key | |
16 |
|
16 | |||
17 | def initialize |
|
17 | def initialize | |
|
18 | self.issue_attributes = {} | |||
|
19 | ||||
18 | opts = GetoptLong.new( |
|
20 | opts = GetoptLong.new( | |
19 | [ '--help', '-h', GetoptLong::NO_ARGUMENT ], |
|
21 | [ '--help', '-h', GetoptLong::NO_ARGUMENT ], | |
20 | [ '--version', '-V', GetoptLong::NO_ARGUMENT ], |
|
22 | [ '--version', '-V', GetoptLong::NO_ARGUMENT ], | |
21 | [ '--verbose', '-v', GetoptLong::NO_ARGUMENT ], |
|
23 | [ '--verbose', '-v', GetoptLong::NO_ARGUMENT ], | |
22 | [ '--url', '-u', GetoptLong::REQUIRED_ARGUMENT ], |
|
24 | [ '--url', '-u', GetoptLong::REQUIRED_ARGUMENT ], | |
23 | [ '--key', '-k', GetoptLong::REQUIRED_ARGUMENT], |
|
25 | [ '--key', '-k', GetoptLong::REQUIRED_ARGUMENT], | |
24 | [ '--project', '-p', GetoptLong::REQUIRED_ARGUMENT ] |
|
26 | [ '--project', '-p', GetoptLong::REQUIRED_ARGUMENT ], | |
|
27 | [ '--tracker', '-t', GetoptLong::REQUIRED_ARGUMENT], | |||
|
28 | [ '--category', GetoptLong::REQUIRED_ARGUMENT], | |||
|
29 | [ '--priority', GetoptLong::REQUIRED_ARGUMENT], | |||
|
30 | [ '--allow-override', '-o', GetoptLong::REQUIRED_ARGUMENT] | |||
25 | ) |
|
31 | ) | |
26 |
|
32 | |||
27 | opts.each do |opt, arg| |
|
33 | opts.each do |opt, arg| | |
@@ -36,8 +42,10 class RedmineMailHandler | |||||
36 | self.verbose = true |
|
42 | self.verbose = true | |
37 | when '--version' |
|
43 | when '--version' | |
38 | puts VERSION; exit |
|
44 | puts VERSION; exit | |
39 | when '--project' |
|
45 | when '--project', '--tracker', '--category', '--priority' | |
40 | self.project = arg.dup |
|
46 | self.issue_attributes[opt.gsub(%r{^\-\-}, '')] = arg.dup | |
|
47 | when '--allow-override' | |||
|
48 | self.allow_override = arg.dup | |||
41 | end |
|
49 | end | |
42 | end |
|
50 | end | |
43 |
|
51 | |||
@@ -46,8 +54,11 class RedmineMailHandler | |||||
46 |
|
54 | |||
47 | def submit(email) |
|
55 | def submit(email) | |
48 | uri = url.gsub(%r{/*$}, '') + '/mail_handler' |
|
56 | uri = url.gsub(%r{/*$}, '') + '/mail_handler' | |
|
57 | ||||
|
58 | data = { 'key' => key, 'email' => email, 'allow_override' => allow_override } | |||
|
59 | issue_attributes.each { |attr, value| data["issue[#{attr}]"] = value } | |||
|
60 | ||||
49 | debug "Posting to #{uri}..." |
|
61 | debug "Posting to #{uri}..." | |
50 | data = { 'key' => key, 'project' => project, 'email' => email } |
|
|||
51 | response = Net::HTTP.post_form(URI.parse(uri), data) |
|
62 | response = Net::HTTP.post_form(URI.parse(uri), data) | |
52 | debug "Response received: #{response.code}" |
|
63 | debug "Response received: #{response.code}" | |
53 | response.code == 201 ? 0 : 1 |
|
64 | response.code == 201 ? 0 : 1 | |
@@ -56,17 +67,39 class RedmineMailHandler | |||||
56 | private |
|
67 | private | |
57 |
|
68 | |||
58 | def usage |
|
69 | def usage | |
59 | puts "Usage: rdm-mailhandler [options] --url=<Redmine URL> --key=<API key>" |
|
70 | puts <<-USAGE | |
60 | puts "Reads an email from standard input and forward it to a Redmine server" |
|
71 | Usage: rdm-mailhandler [options] --url=<Redmine URL> --key=<API key> | |
61 | puts |
|
72 | Reads an email from standard input and forward it to a Redmine server | |
62 | puts "Options:" |
|
73 | ||
63 | puts " --help show this help" |
|
74 | Required: | |
64 | puts " --verbose show extra information" |
|
75 | -u, --url URL of the Redmine server | |
65 | puts " --project identifier of the target project" |
|
76 | -k, --key Redmine API key | |
66 | puts |
|
77 | ||
67 | puts "Examples:" |
|
78 | General options: | |
68 | puts " rdm-mailhandler --url http://redmine.domain.foo --key secret" |
|
79 | -h, --help show this help | |
69 | puts " rdm-mailhandler --url https://redmine.domain.foo --key secret --project foo" |
|
80 | -v, --verbose show extra information | |
|
81 | -V, --version show version information and exit | |||
|
82 | ||||
|
83 | Issue attributes control options: | |||
|
84 | -p, --project=PROJECT identifier of the target project | |||
|
85 | -t, --tracker=TRACKER name of the target tracker | |||
|
86 | --category=CATEGORY name of the target category | |||
|
87 | --priority=PRIORITY name of the target priority | |||
|
88 | -o, --allow-override=ATTRS allow email content to override attributes | |||
|
89 | specified by previous options | |||
|
90 | ATTRS is a comma separated list of attributes | |||
|
91 | ||||
|
92 | Examples: | |||
|
93 | # No project specified. Emails MUST contain the 'Project' keyword: | |||
|
94 | rdm-mailhandler --url http://redmine.domain.foo --key secret | |||
|
95 | ||||
|
96 | # Fixed project and default tracker specified, but emails can override | |||
|
97 | # both tracker and priority attributes: | |||
|
98 | rdm-mailhandler --url https://domain.foo/redmine --key secret \\ | |||
|
99 | --project foo \\ | |||
|
100 | --tracker bug \\ | |||
|
101 | --allow-override tracker,priority | |||
|
102 | USAGE | |||
70 | exit |
|
103 | exit | |
71 | end |
|
104 | end | |
72 |
|
105 |
@@ -21,16 +21,31 namespace :redmine do | |||||
21 | desc <<-END_DESC |
|
21 | desc <<-END_DESC | |
22 | Read an email from standard input. |
|
22 | Read an email from standard input. | |
23 |
|
23 | |||
24 | Available options: |
|
24 | Issue attributes control options: | |
25 | * project => identifier of the project the issue should be added to |
|
25 | project=PROJECT identifier of the target project | |
26 |
|
26 | tracker=TRACKER name of the target tracker | ||
27 | Example: |
|
27 | category=CATEGORY name of the target category | |
28 | rake redmine:email:receive project=foo RAILS_ENV="production" |
|
28 | priority=PRIORITY name of the target priority | |
|
29 | allow_override=ATTRS allow email content to override attributes | |||
|
30 | specified by previous options | |||
|
31 | ATTRS is a comma separated list of attributes | |||
|
32 | ||||
|
33 | Examples: | |||
|
34 | # No project specified. Emails MUST contain the 'Project' keyword: | |||
|
35 | rake redmine:email:read RAILS_ENV="production" < raw_email | |||
|
36 | ||||
|
37 | # Fixed project and default tracker specified, but emails can override | |||
|
38 | # both tracker and priority attributes: | |||
|
39 | rake redmine:email:read RAILS_ENV="production" \\ | |||
|
40 | project=foo \\ | |||
|
41 | tracker=bug \\ | |||
|
42 | allow_override=tracker,priority < raw_email | |||
29 | END_DESC |
|
43 | END_DESC | |
30 |
|
44 | |||
31 |
task :re |
|
45 | task :read => :environment do | |
32 | options = {} |
|
46 | options = { :issue => {} } | |
33 | options[:project] = ENV['project'] if ENV['project'] |
|
47 | %w(project tracker category priority).each { |a| options[:issue][a.to_sym] = ENV[a] if ENV[a] } | |
|
48 | options[:allow_override] = ENV['allow_override'] if ENV['allow_override'] | |||
34 |
|
49 | |||
35 | MailHandler.receive(STDIN.read, options) |
|
50 | MailHandler.receive(STDIN.read, options) | |
36 | end |
|
51 | end | |
@@ -39,17 +54,37 END_DESC | |||||
39 | Read emails from an IMAP server. |
|
54 | Read emails from an IMAP server. | |
40 |
|
55 | |||
41 | Available IMAP options: |
|
56 | Available IMAP options: | |
42 |
|
|
57 | host=HOST IMAP server host (default: 127.0.0.1) | |
43 |
|
|
58 | port=PORT IMAP server port (default: 143) | |
44 |
|
|
59 | ssl=SSL Use SSL? (default: false) | |
45 |
|
|
60 | username=USERNAME IMAP account | |
46 |
|
|
61 | password=PASSWORD IMAP password | |
47 |
|
|
62 | folder=FOLDER IMAP folder to read (default: INBOX) | |
48 | Other options: |
|
63 | ||
49 | * project => identifier of the project the issue should be added to |
|
64 | Issue attributes control options: | |
|
65 | project=PROJECT identifier of the target project | |||
|
66 | tracker=TRACKER name of the target tracker | |||
|
67 | category=CATEGORY name of the target category | |||
|
68 | priority=PRIORITY name of the target priority | |||
|
69 | allow_override=ATTRS allow email content to override attributes | |||
|
70 | specified by previous options | |||
|
71 | ATTRS is a comma separated list of attributes | |||
|
72 | ||||
|
73 | Examples: | |||
|
74 | # No project specified. Emails MUST contain the 'Project' keyword: | |||
|
75 | ||||
|
76 | rake redmine:email:receive_iamp RAILS_ENV="production" \\ | |||
|
77 | host=imap.foo.bar username=redmine@somenet.foo password=xxx | |||
|
78 | ||||
|
79 | ||||
|
80 | # Fixed project and default tracker specified, but emails can override | |||
|
81 | # both tracker and priority attributes: | |||
50 |
|
82 | |||
51 | Example: |
|
83 | rake redmine:email:receive_iamp RAILS_ENV="production" \\ | |
52 |
|
|
84 | host=imap.foo.bar username=redmine@somenet.foo password=xxx ssl=1 \\ | |
|
85 | project=foo \\ | |||
|
86 | tracker=bug \\ | |||
|
87 | allow_override=tracker,priority | |||
53 | END_DESC |
|
88 | END_DESC | |
54 |
|
89 | |||
55 | task :receive_imap => :environment do |
|
90 | task :receive_imap => :environment do | |
@@ -60,8 +95,9 END_DESC | |||||
60 | :password => ENV['password'], |
|
95 | :password => ENV['password'], | |
61 | :folder => ENV['folder']} |
|
96 | :folder => ENV['folder']} | |
62 |
|
97 | |||
63 | options = {} |
|
98 | options = { :issue => {} } | |
64 | options[:project] = ENV['project'] if ENV['project'] |
|
99 | %w(project tracker category priority).each { |a| options[:issue][a.to_sym] = ENV[a] if ENV[a] } | |
|
100 | options[:allow_override] = ENV['allow_override'] if ENV['allow_override'] | |||
65 |
|
101 | |||
66 | Redmine::IMAP.check(imap_options, options) |
|
102 | Redmine::IMAP.check(imap_options, options) | |
67 | end |
|
103 | end |
@@ -9,3 +9,9 issue_categories_002: | |||||
9 | project_id: 1 |
|
9 | project_id: 1 | |
10 | assigned_to_id: |
|
10 | assigned_to_id: | |
11 | id: 2 |
|
11 | id: 2 | |
|
12 | issue_categories_003: | |||
|
13 | name: Stock management | |||
|
14 | project_id: 2 | |||
|
15 | assigned_to_id: | |||
|
16 | id: 3 | |||
|
17 | No newline at end of file |
@@ -18,7 +18,15 | |||||
18 | require File.dirname(__FILE__) + '/../test_helper' |
|
18 | require File.dirname(__FILE__) + '/../test_helper' | |
19 |
|
19 | |||
20 | class MailHandlerTest < Test::Unit::TestCase |
|
20 | class MailHandlerTest < Test::Unit::TestCase | |
21 | fixtures :users, :projects, :enabled_modules, :roles, :members, :issues, :trackers, :enumerations |
|
21 | fixtures :users, :projects, | |
|
22 | :enabled_modules, | |||
|
23 | :roles, | |||
|
24 | :members, | |||
|
25 | :issues, | |||
|
26 | :trackers, | |||
|
27 | :projects_trackers, | |||
|
28 | :enumerations, | |||
|
29 | :issue_categories | |||
22 |
|
30 | |||
23 | FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler' |
|
31 | FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler' | |
24 |
|
32 | |||
@@ -38,8 +46,36 class MailHandlerTest < Test::Unit::TestCase | |||||
38 | assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.') |
|
46 | assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.') | |
39 | end |
|
47 | end | |
40 |
|
48 | |||
|
49 | def test_add_issue_with_attributes_override | |||
|
50 | issue = submit_email('ticket_with_attributes.eml', :allow_override => 'tracker,category,priority') | |||
|
51 | assert issue.is_a?(Issue) | |||
|
52 | assert !issue.new_record? | |||
|
53 | issue.reload | |||
|
54 | assert_equal 'New ticket on a given project', issue.subject | |||
|
55 | assert_equal User.find_by_login('jsmith'), issue.author | |||
|
56 | assert_equal Project.find(2), issue.project | |||
|
57 | assert_equal 'Feature request', issue.tracker.to_s | |||
|
58 | assert_equal 'Stock management', issue.category.to_s | |||
|
59 | assert_equal 'Urgent', issue.priority.to_s | |||
|
60 | assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.') | |||
|
61 | end | |||
|
62 | ||||
|
63 | def test_add_issue_with_partial_attributes_override | |||
|
64 | issue = submit_email('ticket_with_attributes.eml', :issue => {:priority => 'High'}, :allow_override => ['tracker']) | |||
|
65 | assert issue.is_a?(Issue) | |||
|
66 | assert !issue.new_record? | |||
|
67 | issue.reload | |||
|
68 | assert_equal 'New ticket on a given project', issue.subject | |||
|
69 | assert_equal User.find_by_login('jsmith'), issue.author | |||
|
70 | assert_equal Project.find(2), issue.project | |||
|
71 | assert_equal 'Feature request', issue.tracker.to_s | |||
|
72 | assert_nil issue.category | |||
|
73 | assert_equal 'High', issue.priority.to_s | |||
|
74 | assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.') | |||
|
75 | end | |||
|
76 | ||||
41 | def test_add_issue_with_attachment_to_specific_project |
|
77 | def test_add_issue_with_attachment_to_specific_project | |
42 | issue = submit_email('ticket_with_attachment.eml', :project => 'onlinestore') |
|
78 | issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'}) | |
43 | assert issue.is_a?(Issue) |
|
79 | assert issue.is_a?(Issue) | |
44 | assert !issue.new_record? |
|
80 | assert !issue.new_record? | |
45 | issue.reload |
|
81 | issue.reload |
General Comments 0
You need to be logged in to leave comments.
Login now