##// END OF EJS Templates
Mail handler: more control over issue attributes (#1110)....
Jean-Philippe Lang -
r1629:40efaae6d5a2
parent child
Show More
@@ -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
@@ -1,134 +1,145
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class MailHandler < ActionMailer::Base
19 19
20 20 class UnauthorizedAction < StandardError; end
21 21 class MissingInformation < StandardError; end
22 22
23 23 attr_reader :email, :user
24 24
25 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 34 super email
28 35 end
29 36
30 37 # Processes incoming emails
31 38 def receive(email)
32 39 @email = email
33 40 @user = User.find_active(:first, :conditions => {:mail => email.from.first})
34 41 unless @user
35 42 # Unknown user => the email is ignored
36 43 # TODO: ability to create the user's account
37 44 logger.info "MailHandler: email submitted by unknown user [#{email.from.first}]" if logger && logger.info
38 45 return false
39 46 end
40 47 User.current = @user
41 48 dispatch
42 49 end
43 50
44 51 private
45 52
46 53 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]+#(\d+)\]}
47 54
48 55 def dispatch
49 56 if m = email.subject.match(ISSUE_REPLY_SUBJECT_RE)
50 57 receive_issue_update(m[1].to_i)
51 58 else
52 59 receive_issue
53 60 end
54 61 rescue ActiveRecord::RecordInvalid => e
55 62 # TODO: send a email to the user
56 63 logger.error e.message if logger
57 64 false
58 65 rescue MissingInformation => e
59 66 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
60 67 false
61 68 rescue UnauthorizedAction => e
62 69 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
63 70 false
64 71 end
65 72
66 73 # Creates a new issue
67 74 def receive_issue
68 75 project = target_project
69 # TODO: make the tracker configurable
70 tracker = project.trackers.find(:first)
76 tracker = (get_keyword(:tracker) && project.trackers.find_by_name(get_keyword(: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 80 # check permission
72 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 83 issue.subject = email.subject.chomp
75 84 issue.description = email.plain_text_body.chomp
76 85 issue.save!
77 86 add_attachments(issue)
78 87 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
79 88 Mailer.deliver_issue_add(issue) if Setting.notified_events.include?('issue_added')
80 89 issue
81 90 end
82 91
83 92 def target_project
84 93 # TODO: other ways to specify project:
85 94 # * parse the email To field
86 95 # * specific project (eg. Setting.mail_handler_target_project)
87 identifier = if !@@handler_options[:project].blank?
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)
96 target = Project.find_by_identifier(get_keyword(:project))
94 97 raise MissingInformation.new('Unable to determine target project') if target.nil?
95 98 target
96 99 end
97 100
98 101 # Adds a note to an existing issue
99 102 def receive_issue_update(issue_id)
100 103 issue = Issue.find_by_id(issue_id)
101 104 return unless issue
102 105 # check permission
103 106 raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project)
104 107 # add the note
105 108 journal = issue.init_journal(user, email.plain_text_body.chomp)
106 109 add_attachments(issue)
107 110 issue.save!
108 111 logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info
109 112 Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
110 113 journal
111 114 end
112 115
113 116 def add_attachments(obj)
114 117 if email.has_attachments?
115 118 email.attachments.each do |attachment|
116 119 Attachment.create(:container => obj,
117 120 :file => attachment,
118 121 :author => user,
119 122 :content_type => attachment.content_type)
120 123 end
121 124 end
122 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 134 end
124 135
125 136 class TMail::Mail
126 137 # Returns body of the first plain text part found if any
127 138 def plain_text_body
128 139 return @plain_text_body unless @plain_text_body.nil?
129 140 p = self.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten
130 141 plain = p.detect {|c| c.content_type == 'text/plain'}
131 142 @plain_text_body = plain.nil? ? self.body : plain.body
132 143 end
133 144 end
134 145
@@ -1,79 +1,112
1 1 #!/usr/bin/ruby
2 2
3 3 # rdm-mailhandler
4 4 # Reads an email from standard input and forward it to a Redmine server
5 5 # Can be used from a remote mail server
6 6
7 7 require 'net/http'
8 8 require 'net/https'
9 9 require 'uri'
10 10 require 'getoptlong'
11 11
12 12 class RedmineMailHandler
13 13 VERSION = '0.1'
14 14
15 attr_accessor :verbose, :project, :url, :key
15 attr_accessor :verbose, :issue_attributes, :allow_override, :url, :key
16 16
17 17 def initialize
18 self.issue_attributes = {}
19
18 20 opts = GetoptLong.new(
19 21 [ '--help', '-h', GetoptLong::NO_ARGUMENT ],
20 22 [ '--version', '-V', GetoptLong::NO_ARGUMENT ],
21 23 [ '--verbose', '-v', GetoptLong::NO_ARGUMENT ],
22 24 [ '--url', '-u', GetoptLong::REQUIRED_ARGUMENT ],
23 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 33 opts.each do |opt, arg|
28 34 case opt
29 35 when '--url'
30 36 self.url = arg.dup
31 37 when '--key'
32 38 self.key = arg.dup
33 39 when '--help'
34 40 usage
35 41 when '--verbose'
36 42 self.verbose = true
37 43 when '--version'
38 44 puts VERSION; exit
39 when '--project'
40 self.project = arg.dup
45 when '--project', '--tracker', '--category', '--priority'
46 self.issue_attributes[opt.gsub(%r{^\-\-}, '')] = arg.dup
47 when '--allow-override'
48 self.allow_override = arg.dup
41 49 end
42 50 end
43 51
44 52 usage if url.nil?
45 53 end
46 54
47 55 def submit(email)
48 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 61 debug "Posting to #{uri}..."
50 data = { 'key' => key, 'project' => project, 'email' => email }
51 62 response = Net::HTTP.post_form(URI.parse(uri), data)
52 63 debug "Response received: #{response.code}"
53 64 response.code == 201 ? 0 : 1
54 65 end
55 66
56 67 private
57 68
58 69 def usage
59 puts "Usage: rdm-mailhandler [options] --url=<Redmine URL> --key=<API key>"
60 puts "Reads an email from standard input and forward it to a Redmine server"
61 puts
62 puts "Options:"
63 puts " --help show this help"
64 puts " --verbose show extra information"
65 puts " --project identifier of the target project"
66 puts
67 puts "Examples:"
68 puts " rdm-mailhandler --url http://redmine.domain.foo --key secret"
69 puts " rdm-mailhandler --url https://redmine.domain.foo --key secret --project foo"
70 puts <<-USAGE
71 Usage: rdm-mailhandler [options] --url=<Redmine URL> --key=<API key>
72 Reads an email from standard input and forward it to a Redmine server
73
74 Required:
75 -u, --url URL of the Redmine server
76 -k, --key Redmine API key
77
78 General options:
79 -h, --help show this help
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 103 exit
71 104 end
72 105
73 106 def debug(msg)
74 107 puts msg if verbose
75 108 end
76 109 end
77 110
78 111 handler = RedmineMailHandler.new
79 112 handler.submit(STDIN.read)
@@ -1,69 +1,105
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 namespace :redmine do
19 19 namespace :email do
20 20
21 21 desc <<-END_DESC
22 22 Read an email from standard input.
23 23
24 Available options:
25 * project => identifier of the project the issue should be added to
26
27 Example:
28 rake redmine:email:receive project=foo RAILS_ENV="production"
24 Issue attributes control options:
25 project=PROJECT identifier of the target project
26 tracker=TRACKER name of the target tracker
27 category=CATEGORY name of the target category
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 43 END_DESC
30 44
31 task :receive => :environment do
32 options = {}
33 options[:project] = ENV['project'] if ENV['project']
45 task :read => :environment do
46 options = { :issue => {} }
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 50 MailHandler.receive(STDIN.read, options)
36 51 end
37 52
38 53 desc <<-END_DESC
39 54 Read emails from an IMAP server.
40 55
41 56 Available IMAP options:
42 * host => IMAP server host (default: 127.0.0.1)
43 * port => IMAP server port (default: 143)
44 * ssl => Use SSL? (default: false)
45 * username => IMAP account
46 * password => IMAP password
47 * folder => IMAP folder to read (default: INBOX)
48 Other options:
49 * project => identifier of the project the issue should be added to
57 host=HOST IMAP server host (default: 127.0.0.1)
58 port=PORT IMAP server port (default: 143)
59 ssl=SSL Use SSL? (default: false)
60 username=USERNAME IMAP account
61 password=PASSWORD IMAP password
62 folder=FOLDER IMAP folder to read (default: INBOX)
63
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:
52 rake redmine:email:receive_iamp host=imap.foo.bar username=redmine@somenet.foo password=xxx project=foo RAILS_ENV="production"
83 rake redmine:email:receive_iamp RAILS_ENV="production" \\
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 88 END_DESC
54 89
55 90 task :receive_imap => :environment do
56 91 imap_options = {:host => ENV['host'],
57 92 :port => ENV['port'],
58 93 :ssl => ENV['ssl'],
59 94 :username => ENV['username'],
60 95 :password => ENV['password'],
61 96 :folder => ENV['folder']}
62 97
63 options = {}
64 options[:project] = ENV['project'] if ENV['project']
98 options = { :issue => {} }
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 102 Redmine::IMAP.check(imap_options, options)
67 103 end
68 104 end
69 105 end
@@ -1,11 +1,17
1 1 ---
2 2 issue_categories_001:
3 3 name: Printing
4 4 project_id: 1
5 5 assigned_to_id: 2
6 6 id: 1
7 7 issue_categories_002:
8 8 name: Recipes
9 9 project_id: 1
10 10 assigned_to_id:
11 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
@@ -1,71 +1,107
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19
20 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 31 FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler'
24 32
25 33 def setup
26 34 ActionMailer::Base.deliveries.clear
27 35 end
28 36
29 37 def test_add_issue
30 38 # This email contains: 'Project: onlinestore'
31 39 issue = submit_email('ticket_on_given_project.eml')
32 40 assert issue.is_a?(Issue)
33 41 assert !issue.new_record?
34 42 issue.reload
35 43 assert_equal 'New ticket on a given project', issue.subject
36 44 assert_equal User.find_by_login('jsmith'), issue.author
37 45 assert_equal Project.find(2), issue.project
38 46 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
39 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 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 79 assert issue.is_a?(Issue)
44 80 assert !issue.new_record?
45 81 issue.reload
46 82 assert_equal 'Ticket created by email with attachment', issue.subject
47 83 assert_equal User.find_by_login('jsmith'), issue.author
48 84 assert_equal Project.find(2), issue.project
49 85 assert_equal 'This is a new ticket with attachments', issue.description
50 86 # Attachment properties
51 87 assert_equal 1, issue.attachments.size
52 88 assert_equal 'Paella.jpg', issue.attachments.first.filename
53 89 assert_equal 'image/jpeg', issue.attachments.first.content_type
54 90 assert_equal 10790, issue.attachments.first.filesize
55 91 end
56 92
57 93 def test_add_issue_note
58 94 journal = submit_email('ticket_reply.eml')
59 95 assert journal.is_a?(Journal)
60 96 assert_equal User.find_by_login('jsmith'), journal.user
61 97 assert_equal Issue.find(2), journal.journalized
62 98 assert_equal 'This is reply', journal.notes
63 99 end
64 100
65 101 private
66 102
67 103 def submit_email(filename, options={})
68 104 raw = IO.read(File.join(FIXTURES_PATH, filename))
69 105 MailHandler.receive(raw, options)
70 106 end
71 107 end
General Comments 0
You need to be logged in to leave comments. Login now