##// END OF EJS Templates
rdm-mailhandler with project-from-subaddress fails (#21531)....
Jean-Philippe Lang -
r14604:cf59a9b62e44
parent child
Show More
@@ -1,212 +1,212
1 #!/usr/bin/env ruby
1 #!/usr/bin/env ruby
2 # Redmine - project management software
2 # Redmine - project management software
3 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 #
4 #
5 # This program is free software; you can redistribute it and/or
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; either version 2
7 # as published by the Free Software Foundation; either version 2
8 # of the License, or (at your option) any later version.
8 # of the License, or (at your option) any later version.
9 #
9 #
10 # This program is distributed in the hope that it will be useful,
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
13 # GNU General Public License for more details.
14 #
14 #
15 # You should have received a copy of the GNU General Public License
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
18
19 require 'net/http'
19 require 'net/http'
20 require 'net/https'
20 require 'net/https'
21 require 'uri'
21 require 'uri'
22 require 'optparse'
22 require 'optparse'
23
23
24 module Net
24 module Net
25 class HTTPS < HTTP
25 class HTTPS < HTTP
26 def self.post_form(url, params, headers, options={})
26 def self.post_form(url, params, headers, options={})
27 request = Post.new(url.path)
27 request = Post.new(url.path)
28 request.form_data = params
28 request.form_data = params
29 request.initialize_http_header(headers)
29 request.initialize_http_header(headers)
30 request.basic_auth url.user, url.password if url.user
30 request.basic_auth url.user, url.password if url.user
31 http = new(url.host, url.port)
31 http = new(url.host, url.port)
32 http.use_ssl = (url.scheme == 'https')
32 http.use_ssl = (url.scheme == 'https')
33 if options[:certificate_bundle]
33 if options[:certificate_bundle]
34 http.ca_file = options[:certificate_bundle]
34 http.ca_file = options[:certificate_bundle]
35 end
35 end
36 if options[:no_check_certificate]
36 if options[:no_check_certificate]
37 http.verify_mode = OpenSSL::SSL::VERIFY_NONE
37 http.verify_mode = OpenSSL::SSL::VERIFY_NONE
38 end
38 end
39 http.start {|h| h.request(request) }
39 http.start {|h| h.request(request) }
40 end
40 end
41 end
41 end
42 end
42 end
43
43
44 class RedmineMailHandler
44 class RedmineMailHandler
45 VERSION = '0.2.3'
45 VERSION = '0.2.3'
46
46
47 attr_accessor :verbose, :issue_attributes, :allow_override, :unknown_user, :default_group, :no_permission_check,
47 attr_accessor :verbose, :issue_attributes, :allow_override, :unknown_user, :default_group, :no_permission_check,
48 :url, :key, :no_check_certificate, :certificate_bundle, :no_account_notice, :no_notification, :project_from_subaddress
48 :url, :key, :no_check_certificate, :certificate_bundle, :no_account_notice, :no_notification, :project_from_subaddress
49
49
50 def initialize
50 def initialize
51 self.issue_attributes = {}
51 self.issue_attributes = {}
52
52
53 optparse = OptionParser.new do |opts|
53 optparse = OptionParser.new do |opts|
54 opts.banner = "Usage: rdm-mailhandler.rb [options] --url=<Redmine URL> --key=<API key>"
54 opts.banner = "Usage: rdm-mailhandler.rb [options] --url=<Redmine URL> --key=<API key>"
55 opts.separator("")
55 opts.separator("")
56 opts.separator("Reads an email from standard input and forwards it to a Redmine server through a HTTP request.")
56 opts.separator("Reads an email from standard input and forwards it to a Redmine server through a HTTP request.")
57 opts.separator("")
57 opts.separator("")
58 opts.separator("Required arguments:")
58 opts.separator("Required arguments:")
59 opts.on("-u", "--url URL", "URL of the Redmine server") {|v| self.url = v}
59 opts.on("-u", "--url URL", "URL of the Redmine server") {|v| self.url = v}
60 opts.on("-k", "--key KEY", "Redmine API key") {|v| self.key = v}
60 opts.on("-k", "--key KEY", "Redmine API key") {|v| self.key = v}
61 opts.separator("")
61 opts.separator("")
62 opts.separator("General options:")
62 opts.separator("General options:")
63 opts.on("--key-file FILE", "full path to a file that contains your Redmine",
63 opts.on("--key-file FILE", "full path to a file that contains your Redmine",
64 "API key (use this option instead of --key if",
64 "API key (use this option instead of --key if",
65 "you don't want the key to appear in the command",
65 "you don't want the key to appear in the command",
66 "line)") {|v| read_key_from_file(v)}
66 "line)") {|v| read_key_from_file(v)}
67 opts.on("--no-check-certificate", "do not check server certificate") {self.no_check_certificate = true}
67 opts.on("--no-check-certificate", "do not check server certificate") {self.no_check_certificate = true}
68 opts.on("--certificate-bundle FILE", "certificate bundle to use") {|v| self.certificate_bundle = v}
68 opts.on("--certificate-bundle FILE", "certificate bundle to use") {|v| self.certificate_bundle = v}
69 opts.on("-h", "--help", "show this help") {puts opts; exit 1}
69 opts.on("-h", "--help", "show this help") {puts opts; exit 1}
70 opts.on("-v", "--verbose", "show extra information") {self.verbose = true}
70 opts.on("-v", "--verbose", "show extra information") {self.verbose = true}
71 opts.on("-V", "--version", "show version information and exit") {puts VERSION; exit}
71 opts.on("-V", "--version", "show version information and exit") {puts VERSION; exit}
72 opts.separator("")
72 opts.separator("")
73 opts.separator("User and permissions options:")
73 opts.separator("User and permissions options:")
74 opts.on("--unknown-user ACTION", "how to handle emails from an unknown user",
74 opts.on("--unknown-user ACTION", "how to handle emails from an unknown user",
75 "ACTION can be one of the following values:",
75 "ACTION can be one of the following values:",
76 "* ignore: email is ignored (default)",
76 "* ignore: email is ignored (default)",
77 "* accept: accept as anonymous user",
77 "* accept: accept as anonymous user",
78 "* create: create a user account") {|v| self.unknown_user = v}
78 "* create: create a user account") {|v| self.unknown_user = v}
79 opts.on("--no-permission-check", "disable permission checking when receiving",
79 opts.on("--no-permission-check", "disable permission checking when receiving",
80 "the email") {self.no_permission_check = '1'}
80 "the email") {self.no_permission_check = '1'}
81 opts.on("--default-group GROUP", "add created user to GROUP (none by default)",
81 opts.on("--default-group GROUP", "add created user to GROUP (none by default)",
82 "GROUP can be a comma separated list of groups") { |v| self.default_group = v}
82 "GROUP can be a comma separated list of groups") { |v| self.default_group = v}
83 opts.on("--no-account-notice", "don't send account information to the newly",
83 opts.on("--no-account-notice", "don't send account information to the newly",
84 "created user") { |v| self.no_account_notice = '1'}
84 "created user") { |v| self.no_account_notice = '1'}
85 opts.on("--no-notification", "disable email notifications for the created",
85 opts.on("--no-notification", "disable email notifications for the created",
86 "user") { |v| self.no_notification = '1'}
86 "user") { |v| self.no_notification = '1'}
87 opts.separator("")
87 opts.separator("")
88 opts.separator("Issue attributes control options:")
88 opts.separator("Issue attributes control options:")
89 opts.on( "--project-from-subaddress ADDR", "select project from subadress of ADDR found",
89 opts.on( "--project-from-subaddress ADDR", "select project from subadress of ADDR found",
90 "in To, Cc, Bcc headers") {|v| self.project_from_subaddress['project'] = v}
90 "in To, Cc, Bcc headers") {|v| self.project_from_subaddress = v}
91 opts.on("-p", "--project PROJECT", "identifier of the target project") {|v| self.issue_attributes['project'] = v}
91 opts.on("-p", "--project PROJECT", "identifier of the target project") {|v| self.issue_attributes['project'] = v}
92 opts.on("-s", "--status STATUS", "name of the target status") {|v| self.issue_attributes['status'] = v}
92 opts.on("-s", "--status STATUS", "name of the target status") {|v| self.issue_attributes['status'] = v}
93 opts.on("-t", "--tracker TRACKER", "name of the target tracker") {|v| self.issue_attributes['tracker'] = v}
93 opts.on("-t", "--tracker TRACKER", "name of the target tracker") {|v| self.issue_attributes['tracker'] = v}
94 opts.on( "--category CATEGORY", "name of the target category") {|v| self.issue_attributes['category'] = v}
94 opts.on( "--category CATEGORY", "name of the target category") {|v| self.issue_attributes['category'] = v}
95 opts.on( "--priority PRIORITY", "name of the target priority") {|v| self.issue_attributes['priority'] = v}
95 opts.on( "--priority PRIORITY", "name of the target priority") {|v| self.issue_attributes['priority'] = v}
96 opts.on( "--fixed-version VERSION","name of the target version") {|v| self.issue_attributes['fixed_version'] = v}
96 opts.on( "--fixed-version VERSION","name of the target version") {|v| self.issue_attributes['fixed_version'] = v}
97 opts.on( "--private", "create new issues as private") {|v| self.issue_attributes['is_private'] = '1'}
97 opts.on( "--private", "create new issues as private") {|v| self.issue_attributes['is_private'] = '1'}
98 opts.on("-o", "--allow-override ATTRS", "allow email content to set attributes values",
98 opts.on("-o", "--allow-override ATTRS", "allow email content to set attributes values",
99 "ATTRS is a comma separated list of attributes",
99 "ATTRS is a comma separated list of attributes",
100 "or 'all' to allow all attributes to be",
100 "or 'all' to allow all attributes to be",
101 "overridable (see below for details)") {|v| self.allow_override = v}
101 "overridable (see below for details)") {|v| self.allow_override = v}
102
102
103 opts.separator <<-END_DESC
103 opts.separator <<-END_DESC
104
104
105 Overrides:
105 Overrides:
106 ATTRS is a comma separated list of attributes among:
106 ATTRS is a comma separated list of attributes among:
107 * project, tracker, status, priority, category, assigned_to, fixed_version,
107 * project, tracker, status, priority, category, assigned_to, fixed_version,
108 start_date, due_date, estimated_hours, done_ratio
108 start_date, due_date, estimated_hours, done_ratio
109 * custom fields names with underscores instead of spaces (case insensitive)
109 * custom fields names with underscores instead of spaces (case insensitive)
110 Example: --allow_override=project,priority,my_custom_field
110 Example: --allow_override=project,priority,my_custom_field
111
111
112 If the --project option is not set, project is overridable by default for
112 If the --project option is not set, project is overridable by default for
113 emails that create new issues.
113 emails that create new issues.
114
114
115 You can use --allow_override=all to allow all attributes to be overridable.
115 You can use --allow_override=all to allow all attributes to be overridable.
116
116
117 Examples:
117 Examples:
118 No project specified, emails MUST contain the 'Project' keyword, otherwise
118 No project specified, emails MUST contain the 'Project' keyword, otherwise
119 they will be dropped (not recommanded):
119 they will be dropped (not recommanded):
120
120
121 rdm-mailhandler.rb --url http://redmine.domain.foo --key secret
121 rdm-mailhandler.rb --url http://redmine.domain.foo --key secret
122
122
123 Fixed project and default tracker specified, but emails can override
123 Fixed project and default tracker specified, but emails can override
124 both tracker and priority attributes using keywords:
124 both tracker and priority attributes using keywords:
125
125
126 rdm-mailhandler.rb --url https://domain.foo/redmine --key secret \\
126 rdm-mailhandler.rb --url https://domain.foo/redmine --key secret \\
127 --project myproject \\
127 --project myproject \\
128 --tracker bug \\
128 --tracker bug \\
129 --allow-override tracker,priority
129 --allow-override tracker,priority
130
130
131 Project selected by subaddress of redmine@example.net. Sending the email
131 Project selected by subaddress of redmine@example.net. Sending the email
132 to redmine+myproject@example.net will add the issue to myproject:
132 to redmine+myproject@example.net will add the issue to myproject:
133
133
134 rdm-mailhandler.rb --url http://redmine.domain.foo --key secret \\
134 rdm-mailhandler.rb --url http://redmine.domain.foo --key secret \\
135 --project-from-subaddress redmine@example.net
135 --project-from-subaddress redmine@example.net
136 END_DESC
136 END_DESC
137
137
138 opts.summary_width = 27
138 opts.summary_width = 27
139 end
139 end
140 optparse.parse!
140 optparse.parse!
141
141
142 unless url && key
142 unless url && key
143 puts "Some arguments are missing. Use `rdm-mailhandler.rb --help` for getting help."
143 puts "Some arguments are missing. Use `rdm-mailhandler.rb --help` for getting help."
144 exit 1
144 exit 1
145 end
145 end
146 end
146 end
147
147
148 def submit(email)
148 def submit(email)
149 uri = url.gsub(%r{/*$}, '') + '/mail_handler'
149 uri = url.gsub(%r{/*$}, '') + '/mail_handler'
150
150
151 headers = { 'User-Agent' => "Redmine mail handler/#{VERSION}" }
151 headers = { 'User-Agent' => "Redmine mail handler/#{VERSION}" }
152
152
153 data = { 'key' => key, 'email' => email,
153 data = { 'key' => key, 'email' => email,
154 'allow_override' => allow_override,
154 'allow_override' => allow_override,
155 'unknown_user' => unknown_user,
155 'unknown_user' => unknown_user,
156 'default_group' => default_group,
156 'default_group' => default_group,
157 'no_account_notice' => no_account_notice,
157 'no_account_notice' => no_account_notice,
158 'no_notification' => no_notification,
158 'no_notification' => no_notification,
159 'no_permission_check' => no_permission_check,
159 'no_permission_check' => no_permission_check,
160 'project_from_subaddress' => project_from_subaddress}
160 'project_from_subaddress' => project_from_subaddress}
161 issue_attributes.each { |attr, value| data["issue[#{attr}]"] = value }
161 issue_attributes.each { |attr, value| data["issue[#{attr}]"] = value }
162
162
163 debug "Posting to #{uri}..."
163 debug "Posting to #{uri}..."
164 begin
164 begin
165 response = Net::HTTPS.post_form(URI.parse(uri), data, headers, :no_check_certificate => no_check_certificate, :certificate_bundle => certificate_bundle)
165 response = Net::HTTPS.post_form(URI.parse(uri), data, headers, :no_check_certificate => no_check_certificate, :certificate_bundle => certificate_bundle)
166 rescue SystemCallError, IOError => e # connection refused, etc.
166 rescue SystemCallError, IOError => e # connection refused, etc.
167 warn "An error occured while contacting your Redmine server: #{e.message}"
167 warn "An error occured while contacting your Redmine server: #{e.message}"
168 return 75 # temporary failure
168 return 75 # temporary failure
169 end
169 end
170 debug "Response received: #{response.code}"
170 debug "Response received: #{response.code}"
171
171
172 case response.code.to_i
172 case response.code.to_i
173 when 403
173 when 403
174 warn "Request was denied by your Redmine server. " +
174 warn "Request was denied by your Redmine server. " +
175 "Make sure that 'WS for incoming emails' is enabled in application settings and that you provided the correct API key."
175 "Make sure that 'WS for incoming emails' is enabled in application settings and that you provided the correct API key."
176 return 77
176 return 77
177 when 422
177 when 422
178 warn "Request was denied by your Redmine server. " +
178 warn "Request was denied by your Redmine server. " +
179 "Possible reasons: email is sent from an invalid email address or is missing some information."
179 "Possible reasons: email is sent from an invalid email address or is missing some information."
180 return 77
180 return 77
181 when 400..499
181 when 400..499
182 warn "Request was denied by your Redmine server (#{response.code})."
182 warn "Request was denied by your Redmine server (#{response.code})."
183 return 77
183 return 77
184 when 500..599
184 when 500..599
185 warn "Failed to contact your Redmine server (#{response.code})."
185 warn "Failed to contact your Redmine server (#{response.code})."
186 return 75
186 return 75
187 when 201
187 when 201
188 debug "Proccessed successfully"
188 debug "Proccessed successfully"
189 return 0
189 return 0
190 else
190 else
191 return 1
191 return 1
192 end
192 end
193 end
193 end
194
194
195 private
195 private
196
196
197 def debug(msg)
197 def debug(msg)
198 puts msg if verbose
198 puts msg if verbose
199 end
199 end
200
200
201 def read_key_from_file(filename)
201 def read_key_from_file(filename)
202 begin
202 begin
203 self.key = File.read(filename).strip
203 self.key = File.read(filename).strip
204 rescue Exception => e
204 rescue Exception => e
205 $stderr.puts "Unable to read the key from #{filename}:\n#{e.message}"
205 $stderr.puts "Unable to read the key from #{filename}:\n#{e.message}"
206 exit 1
206 exit 1
207 end
207 end
208 end
208 end
209 end
209 end
210
210
211 handler = RedmineMailHandler.new
211 handler = RedmineMailHandler.new
212 exit(handler.submit(STDIN.read))
212 exit(handler.submit(STDIN.read))
General Comments 0
You need to be logged in to leave comments. Login now