##// END OF EJS Templates
Fixed that #find_assignee_from_keyword may return a group with blank keyword (#20454)....
Jean-Philippe Lang -
r14117:b66358ac6c33
parent child
Show More
@@ -1,566 +1,568
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class MailHandler < ActionMailer::Base
18 class MailHandler < ActionMailer::Base
19 include ActionView::Helpers::SanitizeHelper
19 include ActionView::Helpers::SanitizeHelper
20 include Redmine::I18n
20 include Redmine::I18n
21
21
22 class UnauthorizedAction < StandardError; end
22 class UnauthorizedAction < StandardError; end
23 class MissingInformation < StandardError; end
23 class MissingInformation < StandardError; end
24
24
25 attr_reader :email, :user, :handler_options
25 attr_reader :email, :user, :handler_options
26
26
27 def self.receive(raw_mail, options={})
27 def self.receive(raw_mail, options={})
28 options = options.deep_dup
28 options = options.deep_dup
29
29
30 options[:issue] ||= {}
30 options[:issue] ||= {}
31
31
32 if options[:allow_override].is_a?(String)
32 if options[:allow_override].is_a?(String)
33 options[:allow_override] = options[:allow_override].split(',').collect(&:strip)
33 options[:allow_override] = options[:allow_override].split(',').collect(&:strip)
34 end
34 end
35 options[:allow_override] ||= []
35 options[:allow_override] ||= []
36 # Project needs to be overridable if not specified
36 # Project needs to be overridable if not specified
37 options[:allow_override] << 'project' unless options[:issue].has_key?(:project)
37 options[:allow_override] << 'project' unless options[:issue].has_key?(:project)
38 # Status overridable by default
38 # Status overridable by default
39 options[:allow_override] << 'status' unless options[:issue].has_key?(:status)
39 options[:allow_override] << 'status' unless options[:issue].has_key?(:status)
40
40
41 options[:no_account_notice] = (options[:no_account_notice].to_s == '1')
41 options[:no_account_notice] = (options[:no_account_notice].to_s == '1')
42 options[:no_notification] = (options[:no_notification].to_s == '1')
42 options[:no_notification] = (options[:no_notification].to_s == '1')
43 options[:no_permission_check] = (options[:no_permission_check].to_s == '1')
43 options[:no_permission_check] = (options[:no_permission_check].to_s == '1')
44
44
45 raw_mail.force_encoding('ASCII-8BIT')
45 raw_mail.force_encoding('ASCII-8BIT')
46
46
47 ActiveSupport::Notifications.instrument("receive.action_mailer") do |payload|
47 ActiveSupport::Notifications.instrument("receive.action_mailer") do |payload|
48 mail = Mail.new(raw_mail)
48 mail = Mail.new(raw_mail)
49 set_payload_for_mail(payload, mail)
49 set_payload_for_mail(payload, mail)
50 new.receive(mail, options)
50 new.receive(mail, options)
51 end
51 end
52 end
52 end
53
53
54 # Receives an email and rescues any exception
54 # Receives an email and rescues any exception
55 def self.safe_receive(*args)
55 def self.safe_receive(*args)
56 receive(*args)
56 receive(*args)
57 rescue Exception => e
57 rescue Exception => e
58 logger.error "An unexpected error occurred when receiving email: #{e.message}" if logger
58 logger.error "An unexpected error occurred when receiving email: #{e.message}" if logger
59 return false
59 return false
60 end
60 end
61
61
62 # Extracts MailHandler options from environment variables
62 # Extracts MailHandler options from environment variables
63 # Use when receiving emails with rake tasks
63 # Use when receiving emails with rake tasks
64 def self.extract_options_from_env(env)
64 def self.extract_options_from_env(env)
65 options = {:issue => {}}
65 options = {:issue => {}}
66 %w(project status tracker category priority).each do |option|
66 %w(project status tracker category priority).each do |option|
67 options[:issue][option.to_sym] = env[option] if env[option]
67 options[:issue][option.to_sym] = env[option] if env[option]
68 end
68 end
69 %w(allow_override unknown_user no_permission_check no_account_notice default_group).each do |option|
69 %w(allow_override unknown_user no_permission_check no_account_notice default_group).each do |option|
70 options[option.to_sym] = env[option] if env[option]
70 options[option.to_sym] = env[option] if env[option]
71 end
71 end
72 if env['private']
72 if env['private']
73 options[:issue][:is_private] = '1'
73 options[:issue][:is_private] = '1'
74 end
74 end
75 options
75 options
76 end
76 end
77
77
78 def logger
78 def logger
79 Rails.logger
79 Rails.logger
80 end
80 end
81
81
82 cattr_accessor :ignored_emails_headers
82 cattr_accessor :ignored_emails_headers
83 self.ignored_emails_headers = {
83 self.ignored_emails_headers = {
84 'Auto-Submitted' => /\Aauto-(replied|generated)/,
84 'Auto-Submitted' => /\Aauto-(replied|generated)/,
85 'X-Autoreply' => 'yes'
85 'X-Autoreply' => 'yes'
86 }
86 }
87
87
88 # Processes incoming emails
88 # Processes incoming emails
89 # Returns the created object (eg. an issue, a message) or false
89 # Returns the created object (eg. an issue, a message) or false
90 def receive(email, options={})
90 def receive(email, options={})
91 @email = email
91 @email = email
92 @handler_options = options
92 @handler_options = options
93 sender_email = email.from.to_a.first.to_s.strip
93 sender_email = email.from.to_a.first.to_s.strip
94 # Ignore emails received from the application emission address to avoid hell cycles
94 # Ignore emails received from the application emission address to avoid hell cycles
95 if sender_email.casecmp(Setting.mail_from.to_s.strip) == 0
95 if sender_email.casecmp(Setting.mail_from.to_s.strip) == 0
96 if logger
96 if logger
97 logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]"
97 logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]"
98 end
98 end
99 return false
99 return false
100 end
100 end
101 # Ignore auto generated emails
101 # Ignore auto generated emails
102 self.class.ignored_emails_headers.each do |key, ignored_value|
102 self.class.ignored_emails_headers.each do |key, ignored_value|
103 value = email.header[key]
103 value = email.header[key]
104 if value
104 if value
105 value = value.to_s.downcase
105 value = value.to_s.downcase
106 if (ignored_value.is_a?(Regexp) && value.match(ignored_value)) || value == ignored_value
106 if (ignored_value.is_a?(Regexp) && value.match(ignored_value)) || value == ignored_value
107 if logger
107 if logger
108 logger.info "MailHandler: ignoring email with #{key}:#{value} header"
108 logger.info "MailHandler: ignoring email with #{key}:#{value} header"
109 end
109 end
110 return false
110 return false
111 end
111 end
112 end
112 end
113 end
113 end
114 @user = User.find_by_mail(sender_email) if sender_email.present?
114 @user = User.find_by_mail(sender_email) if sender_email.present?
115 if @user && !@user.active?
115 if @user && !@user.active?
116 if logger
116 if logger
117 logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]"
117 logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]"
118 end
118 end
119 return false
119 return false
120 end
120 end
121 if @user.nil?
121 if @user.nil?
122 # Email was submitted by an unknown user
122 # Email was submitted by an unknown user
123 case handler_options[:unknown_user]
123 case handler_options[:unknown_user]
124 when 'accept'
124 when 'accept'
125 @user = User.anonymous
125 @user = User.anonymous
126 when 'create'
126 when 'create'
127 @user = create_user_from_email
127 @user = create_user_from_email
128 if @user
128 if @user
129 if logger
129 if logger
130 logger.info "MailHandler: [#{@user.login}] account created"
130 logger.info "MailHandler: [#{@user.login}] account created"
131 end
131 end
132 add_user_to_group(handler_options[:default_group])
132 add_user_to_group(handler_options[:default_group])
133 unless handler_options[:no_account_notice]
133 unless handler_options[:no_account_notice]
134 Mailer.account_information(@user, @user.password).deliver
134 Mailer.account_information(@user, @user.password).deliver
135 end
135 end
136 else
136 else
137 if logger
137 if logger
138 logger.error "MailHandler: could not create account for [#{sender_email}]"
138 logger.error "MailHandler: could not create account for [#{sender_email}]"
139 end
139 end
140 return false
140 return false
141 end
141 end
142 else
142 else
143 # Default behaviour, emails from unknown users are ignored
143 # Default behaviour, emails from unknown users are ignored
144 if logger
144 if logger
145 logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]"
145 logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]"
146 end
146 end
147 return false
147 return false
148 end
148 end
149 end
149 end
150 User.current = @user
150 User.current = @user
151 dispatch
151 dispatch
152 end
152 end
153
153
154 private
154 private
155
155
156 MESSAGE_ID_RE = %r{^<?redmine\.([a-z0-9_]+)\-(\d+)\.\d+(\.[a-f0-9]+)?@}
156 MESSAGE_ID_RE = %r{^<?redmine\.([a-z0-9_]+)\-(\d+)\.\d+(\.[a-f0-9]+)?@}
157 ISSUE_REPLY_SUBJECT_RE = %r{\[(?:[^\]]*\s+)?#(\d+)\]}
157 ISSUE_REPLY_SUBJECT_RE = %r{\[(?:[^\]]*\s+)?#(\d+)\]}
158 MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
158 MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
159
159
160 def dispatch
160 def dispatch
161 headers = [email.in_reply_to, email.references].flatten.compact
161 headers = [email.in_reply_to, email.references].flatten.compact
162 subject = email.subject.to_s
162 subject = email.subject.to_s
163 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
163 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
164 klass, object_id = $1, $2.to_i
164 klass, object_id = $1, $2.to_i
165 method_name = "receive_#{klass}_reply"
165 method_name = "receive_#{klass}_reply"
166 if self.class.private_instance_methods.collect(&:to_s).include?(method_name)
166 if self.class.private_instance_methods.collect(&:to_s).include?(method_name)
167 send method_name, object_id
167 send method_name, object_id
168 else
168 else
169 # ignoring it
169 # ignoring it
170 end
170 end
171 elsif m = subject.match(ISSUE_REPLY_SUBJECT_RE)
171 elsif m = subject.match(ISSUE_REPLY_SUBJECT_RE)
172 receive_issue_reply(m[1].to_i)
172 receive_issue_reply(m[1].to_i)
173 elsif m = subject.match(MESSAGE_REPLY_SUBJECT_RE)
173 elsif m = subject.match(MESSAGE_REPLY_SUBJECT_RE)
174 receive_message_reply(m[1].to_i)
174 receive_message_reply(m[1].to_i)
175 else
175 else
176 dispatch_to_default
176 dispatch_to_default
177 end
177 end
178 rescue ActiveRecord::RecordInvalid => e
178 rescue ActiveRecord::RecordInvalid => e
179 # TODO: send a email to the user
179 # TODO: send a email to the user
180 logger.error e.message if logger
180 logger.error e.message if logger
181 false
181 false
182 rescue MissingInformation => e
182 rescue MissingInformation => e
183 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
183 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
184 false
184 false
185 rescue UnauthorizedAction => e
185 rescue UnauthorizedAction => e
186 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
186 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
187 false
187 false
188 end
188 end
189
189
190 def dispatch_to_default
190 def dispatch_to_default
191 receive_issue
191 receive_issue
192 end
192 end
193
193
194 # Creates a new issue
194 # Creates a new issue
195 def receive_issue
195 def receive_issue
196 project = target_project
196 project = target_project
197 # check permission
197 # check permission
198 unless handler_options[:no_permission_check]
198 unless handler_options[:no_permission_check]
199 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
199 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
200 end
200 end
201
201
202 issue = Issue.new(:author => user, :project => project)
202 issue = Issue.new(:author => user, :project => project)
203 issue.safe_attributes = issue_attributes_from_keywords(issue)
203 issue.safe_attributes = issue_attributes_from_keywords(issue)
204 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
204 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
205 issue.subject = cleaned_up_subject
205 issue.subject = cleaned_up_subject
206 if issue.subject.blank?
206 if issue.subject.blank?
207 issue.subject = '(no subject)'
207 issue.subject = '(no subject)'
208 end
208 end
209 issue.description = cleaned_up_text_body
209 issue.description = cleaned_up_text_body
210 issue.start_date ||= Date.today if Setting.default_issue_start_date_to_creation_date?
210 issue.start_date ||= Date.today if Setting.default_issue_start_date_to_creation_date?
211 issue.is_private = (handler_options[:issue][:is_private] == '1')
211 issue.is_private = (handler_options[:issue][:is_private] == '1')
212
212
213 # add To and Cc as watchers before saving so the watchers can reply to Redmine
213 # add To and Cc as watchers before saving so the watchers can reply to Redmine
214 add_watchers(issue)
214 add_watchers(issue)
215 issue.save!
215 issue.save!
216 add_attachments(issue)
216 add_attachments(issue)
217 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger
217 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger
218 issue
218 issue
219 end
219 end
220
220
221 # Adds a note to an existing issue
221 # Adds a note to an existing issue
222 def receive_issue_reply(issue_id, from_journal=nil)
222 def receive_issue_reply(issue_id, from_journal=nil)
223 issue = Issue.find_by_id(issue_id)
223 issue = Issue.find_by_id(issue_id)
224 return unless issue
224 return unless issue
225 # check permission
225 # check permission
226 unless handler_options[:no_permission_check]
226 unless handler_options[:no_permission_check]
227 unless user.allowed_to?(:add_issue_notes, issue.project) ||
227 unless user.allowed_to?(:add_issue_notes, issue.project) ||
228 user.allowed_to?(:edit_issues, issue.project)
228 user.allowed_to?(:edit_issues, issue.project)
229 raise UnauthorizedAction
229 raise UnauthorizedAction
230 end
230 end
231 end
231 end
232
232
233 # ignore CLI-supplied defaults for new issues
233 # ignore CLI-supplied defaults for new issues
234 handler_options[:issue].clear
234 handler_options[:issue].clear
235
235
236 journal = issue.init_journal(user)
236 journal = issue.init_journal(user)
237 if from_journal && from_journal.private_notes?
237 if from_journal && from_journal.private_notes?
238 # If the received email was a reply to a private note, make the added note private
238 # If the received email was a reply to a private note, make the added note private
239 issue.private_notes = true
239 issue.private_notes = true
240 end
240 end
241 issue.safe_attributes = issue_attributes_from_keywords(issue)
241 issue.safe_attributes = issue_attributes_from_keywords(issue)
242 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
242 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
243 journal.notes = cleaned_up_text_body
243 journal.notes = cleaned_up_text_body
244 add_attachments(issue)
244 add_attachments(issue)
245 issue.save!
245 issue.save!
246 if logger
246 if logger
247 logger.info "MailHandler: issue ##{issue.id} updated by #{user}"
247 logger.info "MailHandler: issue ##{issue.id} updated by #{user}"
248 end
248 end
249 journal
249 journal
250 end
250 end
251
251
252 # Reply will be added to the issue
252 # Reply will be added to the issue
253 def receive_journal_reply(journal_id)
253 def receive_journal_reply(journal_id)
254 journal = Journal.find_by_id(journal_id)
254 journal = Journal.find_by_id(journal_id)
255 if journal && journal.journalized_type == 'Issue'
255 if journal && journal.journalized_type == 'Issue'
256 receive_issue_reply(journal.journalized_id, journal)
256 receive_issue_reply(journal.journalized_id, journal)
257 end
257 end
258 end
258 end
259
259
260 # Receives a reply to a forum message
260 # Receives a reply to a forum message
261 def receive_message_reply(message_id)
261 def receive_message_reply(message_id)
262 message = Message.find_by_id(message_id)
262 message = Message.find_by_id(message_id)
263 if message
263 if message
264 message = message.root
264 message = message.root
265
265
266 unless handler_options[:no_permission_check]
266 unless handler_options[:no_permission_check]
267 raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
267 raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
268 end
268 end
269
269
270 if !message.locked?
270 if !message.locked?
271 reply = Message.new(:subject => cleaned_up_subject.gsub(%r{^.*msg\d+\]}, '').strip,
271 reply = Message.new(:subject => cleaned_up_subject.gsub(%r{^.*msg\d+\]}, '').strip,
272 :content => cleaned_up_text_body)
272 :content => cleaned_up_text_body)
273 reply.author = user
273 reply.author = user
274 reply.board = message.board
274 reply.board = message.board
275 message.children << reply
275 message.children << reply
276 add_attachments(reply)
276 add_attachments(reply)
277 reply
277 reply
278 else
278 else
279 if logger
279 if logger
280 logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic"
280 logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic"
281 end
281 end
282 end
282 end
283 end
283 end
284 end
284 end
285
285
286 def add_attachments(obj)
286 def add_attachments(obj)
287 if email.attachments && email.attachments.any?
287 if email.attachments && email.attachments.any?
288 email.attachments.each do |attachment|
288 email.attachments.each do |attachment|
289 next unless accept_attachment?(attachment)
289 next unless accept_attachment?(attachment)
290 obj.attachments << Attachment.create(:container => obj,
290 obj.attachments << Attachment.create(:container => obj,
291 :file => attachment.decoded,
291 :file => attachment.decoded,
292 :filename => attachment.filename,
292 :filename => attachment.filename,
293 :author => user,
293 :author => user,
294 :content_type => attachment.mime_type)
294 :content_type => attachment.mime_type)
295 end
295 end
296 end
296 end
297 end
297 end
298
298
299 # Returns false if the +attachment+ of the incoming email should be ignored
299 # Returns false if the +attachment+ of the incoming email should be ignored
300 def accept_attachment?(attachment)
300 def accept_attachment?(attachment)
301 @excluded ||= Setting.mail_handler_excluded_filenames.to_s.split(',').map(&:strip).reject(&:blank?)
301 @excluded ||= Setting.mail_handler_excluded_filenames.to_s.split(',').map(&:strip).reject(&:blank?)
302 @excluded.each do |pattern|
302 @excluded.each do |pattern|
303 regexp = %r{\A#{Regexp.escape(pattern).gsub("\\*", ".*")}\z}i
303 regexp = %r{\A#{Regexp.escape(pattern).gsub("\\*", ".*")}\z}i
304 if attachment.filename.to_s =~ regexp
304 if attachment.filename.to_s =~ regexp
305 logger.info "MailHandler: ignoring attachment #{attachment.filename} matching #{pattern}"
305 logger.info "MailHandler: ignoring attachment #{attachment.filename} matching #{pattern}"
306 return false
306 return false
307 end
307 end
308 end
308 end
309 true
309 true
310 end
310 end
311
311
312 # Adds To and Cc as watchers of the given object if the sender has the
312 # Adds To and Cc as watchers of the given object if the sender has the
313 # appropriate permission
313 # appropriate permission
314 def add_watchers(obj)
314 def add_watchers(obj)
315 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
315 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
316 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
316 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
317 unless addresses.empty?
317 unless addresses.empty?
318 User.active.having_mail(addresses).each do |w|
318 User.active.having_mail(addresses).each do |w|
319 obj.add_watcher(w)
319 obj.add_watcher(w)
320 end
320 end
321 end
321 end
322 end
322 end
323 end
323 end
324
324
325 def get_keyword(attr, options={})
325 def get_keyword(attr, options={})
326 @keywords ||= {}
326 @keywords ||= {}
327 if @keywords.has_key?(attr)
327 if @keywords.has_key?(attr)
328 @keywords[attr]
328 @keywords[attr]
329 else
329 else
330 @keywords[attr] = begin
330 @keywords[attr] = begin
331 if (options[:override] || handler_options[:allow_override].include?(attr.to_s)) &&
331 if (options[:override] || handler_options[:allow_override].include?(attr.to_s)) &&
332 (v = extract_keyword!(cleaned_up_text_body, attr, options[:format]))
332 (v = extract_keyword!(cleaned_up_text_body, attr, options[:format]))
333 v
333 v
334 elsif !handler_options[:issue][attr].blank?
334 elsif !handler_options[:issue][attr].blank?
335 handler_options[:issue][attr]
335 handler_options[:issue][attr]
336 end
336 end
337 end
337 end
338 end
338 end
339 end
339 end
340
340
341 # Destructively extracts the value for +attr+ in +text+
341 # Destructively extracts the value for +attr+ in +text+
342 # Returns nil if no matching keyword found
342 # Returns nil if no matching keyword found
343 def extract_keyword!(text, attr, format=nil)
343 def extract_keyword!(text, attr, format=nil)
344 keys = [attr.to_s.humanize]
344 keys = [attr.to_s.humanize]
345 if attr.is_a?(Symbol)
345 if attr.is_a?(Symbol)
346 if user && user.language.present?
346 if user && user.language.present?
347 keys << l("field_#{attr}", :default => '', :locale => user.language)
347 keys << l("field_#{attr}", :default => '', :locale => user.language)
348 end
348 end
349 if Setting.default_language.present?
349 if Setting.default_language.present?
350 keys << l("field_#{attr}", :default => '', :locale => Setting.default_language)
350 keys << l("field_#{attr}", :default => '', :locale => Setting.default_language)
351 end
351 end
352 end
352 end
353 keys.reject! {|k| k.blank?}
353 keys.reject! {|k| k.blank?}
354 keys.collect! {|k| Regexp.escape(k)}
354 keys.collect! {|k| Regexp.escape(k)}
355 format ||= '.+'
355 format ||= '.+'
356 keyword = nil
356 keyword = nil
357 regexp = /^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i
357 regexp = /^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i
358 if m = text.match(regexp)
358 if m = text.match(regexp)
359 keyword = m[2].strip
359 keyword = m[2].strip
360 text.sub!(regexp, '')
360 text.sub!(regexp, '')
361 end
361 end
362 keyword
362 keyword
363 end
363 end
364
364
365 def target_project
365 def target_project
366 # TODO: other ways to specify project:
366 # TODO: other ways to specify project:
367 # * parse the email To field
367 # * parse the email To field
368 # * specific project (eg. Setting.mail_handler_target_project)
368 # * specific project (eg. Setting.mail_handler_target_project)
369 target = Project.find_by_identifier(get_keyword(:project))
369 target = Project.find_by_identifier(get_keyword(:project))
370 if target.nil?
370 if target.nil?
371 # Invalid project keyword, use the project specified as the default one
371 # Invalid project keyword, use the project specified as the default one
372 default_project = handler_options[:issue][:project]
372 default_project = handler_options[:issue][:project]
373 if default_project.present?
373 if default_project.present?
374 target = Project.find_by_identifier(default_project)
374 target = Project.find_by_identifier(default_project)
375 end
375 end
376 end
376 end
377 raise MissingInformation.new('Unable to determine target project') if target.nil?
377 raise MissingInformation.new('Unable to determine target project') if target.nil?
378 target
378 target
379 end
379 end
380
380
381 # Returns a Hash of issue attributes extracted from keywords in the email body
381 # Returns a Hash of issue attributes extracted from keywords in the email body
382 def issue_attributes_from_keywords(issue)
382 def issue_attributes_from_keywords(issue)
383 assigned_to = (k = get_keyword(:assigned_to, :override => true)) && find_assignee_from_keyword(k, issue)
383 assigned_to = (k = get_keyword(:assigned_to, :override => true)) && find_assignee_from_keyword(k, issue)
384
384
385 attrs = {
385 attrs = {
386 'tracker_id' => (k = get_keyword(:tracker)) && issue.project.trackers.named(k).first.try(:id),
386 'tracker_id' => (k = get_keyword(:tracker)) && issue.project.trackers.named(k).first.try(:id),
387 'status_id' => (k = get_keyword(:status)) && IssueStatus.named(k).first.try(:id),
387 'status_id' => (k = get_keyword(:status)) && IssueStatus.named(k).first.try(:id),
388 'priority_id' => (k = get_keyword(:priority)) && IssuePriority.named(k).first.try(:id),
388 'priority_id' => (k = get_keyword(:priority)) && IssuePriority.named(k).first.try(:id),
389 'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.named(k).first.try(:id),
389 'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.named(k).first.try(:id),
390 'assigned_to_id' => assigned_to.try(:id),
390 'assigned_to_id' => assigned_to.try(:id),
391 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) &&
391 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) &&
392 issue.project.shared_versions.named(k).first.try(:id),
392 issue.project.shared_versions.named(k).first.try(:id),
393 'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
393 'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
394 'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
394 'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
395 'estimated_hours' => get_keyword(:estimated_hours, :override => true),
395 'estimated_hours' => get_keyword(:estimated_hours, :override => true),
396 'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0')
396 'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0')
397 }.delete_if {|k, v| v.blank? }
397 }.delete_if {|k, v| v.blank? }
398
398
399 if issue.new_record? && attrs['tracker_id'].nil?
399 if issue.new_record? && attrs['tracker_id'].nil?
400 attrs['tracker_id'] = issue.project.trackers.first.try(:id)
400 attrs['tracker_id'] = issue.project.trackers.first.try(:id)
401 end
401 end
402
402
403 attrs
403 attrs
404 end
404 end
405
405
406 # Returns a Hash of issue custom field values extracted from keywords in the email body
406 # Returns a Hash of issue custom field values extracted from keywords in the email body
407 def custom_field_values_from_keywords(customized)
407 def custom_field_values_from_keywords(customized)
408 customized.custom_field_values.inject({}) do |h, v|
408 customized.custom_field_values.inject({}) do |h, v|
409 if keyword = get_keyword(v.custom_field.name, :override => true)
409 if keyword = get_keyword(v.custom_field.name, :override => true)
410 h[v.custom_field.id.to_s] = v.custom_field.value_from_keyword(keyword, customized)
410 h[v.custom_field.id.to_s] = v.custom_field.value_from_keyword(keyword, customized)
411 end
411 end
412 h
412 h
413 end
413 end
414 end
414 end
415
415
416 # Returns the text/plain part of the email
416 # Returns the text/plain part of the email
417 # If not found (eg. HTML-only email), returns the body with tags removed
417 # If not found (eg. HTML-only email), returns the body with tags removed
418 def plain_text_body
418 def plain_text_body
419 return @plain_text_body unless @plain_text_body.nil?
419 return @plain_text_body unless @plain_text_body.nil?
420
420
421 parts = if (text_parts = email.all_parts.select {|p| p.mime_type == 'text/plain'}).present?
421 parts = if (text_parts = email.all_parts.select {|p| p.mime_type == 'text/plain'}).present?
422 text_parts
422 text_parts
423 elsif (html_parts = email.all_parts.select {|p| p.mime_type == 'text/html'}).present?
423 elsif (html_parts = email.all_parts.select {|p| p.mime_type == 'text/html'}).present?
424 html_parts
424 html_parts
425 else
425 else
426 [email]
426 [email]
427 end
427 end
428
428
429 parts.reject! do |part|
429 parts.reject! do |part|
430 part.attachment?
430 part.attachment?
431 end
431 end
432
432
433 @plain_text_body = parts.map do |p|
433 @plain_text_body = parts.map do |p|
434 body_charset = Mail::RubyVer.respond_to?(:pick_encoding) ?
434 body_charset = Mail::RubyVer.respond_to?(:pick_encoding) ?
435 Mail::RubyVer.pick_encoding(p.charset).to_s : p.charset
435 Mail::RubyVer.pick_encoding(p.charset).to_s : p.charset
436
436
437 body = Redmine::CodesetUtil.to_utf8(p.body.decoded, body_charset)
437 body = Redmine::CodesetUtil.to_utf8(p.body.decoded, body_charset)
438 # convert html parts to text
438 # convert html parts to text
439 p.mime_type == 'text/html' ? self.class.html_body_to_text(body) : self.class.plain_text_body_to_text(body)
439 p.mime_type == 'text/html' ? self.class.html_body_to_text(body) : self.class.plain_text_body_to_text(body)
440 end.join("\r\n")
440 end.join("\r\n")
441
441
442 @plain_text_body
442 @plain_text_body
443 end
443 end
444
444
445 def cleaned_up_text_body
445 def cleaned_up_text_body
446 @cleaned_up_text_body ||= cleanup_body(plain_text_body)
446 @cleaned_up_text_body ||= cleanup_body(plain_text_body)
447 end
447 end
448
448
449 def cleaned_up_subject
449 def cleaned_up_subject
450 subject = email.subject.to_s
450 subject = email.subject.to_s
451 subject.strip[0,255]
451 subject.strip[0,255]
452 end
452 end
453
453
454 # Converts a HTML email body to text
454 # Converts a HTML email body to text
455 def self.html_body_to_text(html)
455 def self.html_body_to_text(html)
456 Redmine::WikiFormatting.html_parser.to_text(html)
456 Redmine::WikiFormatting.html_parser.to_text(html)
457 end
457 end
458
458
459 # Converts a plain/text email body to text
459 # Converts a plain/text email body to text
460 def self.plain_text_body_to_text(text)
460 def self.plain_text_body_to_text(text)
461 # Removes leading spaces that would cause the line to be rendered as
461 # Removes leading spaces that would cause the line to be rendered as
462 # preformatted text with textile
462 # preformatted text with textile
463 text.gsub(/^ +(?![*#])/, '')
463 text.gsub(/^ +(?![*#])/, '')
464 end
464 end
465
465
466 def self.assign_string_attribute_with_limit(object, attribute, value, limit=nil)
466 def self.assign_string_attribute_with_limit(object, attribute, value, limit=nil)
467 limit ||= object.class.columns_hash[attribute.to_s].limit || 255
467 limit ||= object.class.columns_hash[attribute.to_s].limit || 255
468 value = value.to_s.slice(0, limit)
468 value = value.to_s.slice(0, limit)
469 object.send("#{attribute}=", value)
469 object.send("#{attribute}=", value)
470 end
470 end
471
471
472 # Returns a User from an email address and a full name
472 # Returns a User from an email address and a full name
473 def self.new_user_from_attributes(email_address, fullname=nil)
473 def self.new_user_from_attributes(email_address, fullname=nil)
474 user = User.new
474 user = User.new
475
475
476 # Truncating the email address would result in an invalid format
476 # Truncating the email address would result in an invalid format
477 user.mail = email_address
477 user.mail = email_address
478 assign_string_attribute_with_limit(user, 'login', email_address, User::LOGIN_LENGTH_LIMIT)
478 assign_string_attribute_with_limit(user, 'login', email_address, User::LOGIN_LENGTH_LIMIT)
479
479
480 names = fullname.blank? ? email_address.gsub(/@.*$/, '').split('.') : fullname.split
480 names = fullname.blank? ? email_address.gsub(/@.*$/, '').split('.') : fullname.split
481 assign_string_attribute_with_limit(user, 'firstname', names.shift, 30)
481 assign_string_attribute_with_limit(user, 'firstname', names.shift, 30)
482 assign_string_attribute_with_limit(user, 'lastname', names.join(' '), 30)
482 assign_string_attribute_with_limit(user, 'lastname', names.join(' '), 30)
483 user.lastname = '-' if user.lastname.blank?
483 user.lastname = '-' if user.lastname.blank?
484 user.language = Setting.default_language
484 user.language = Setting.default_language
485 user.generate_password = true
485 user.generate_password = true
486 user.mail_notification = 'only_my_events'
486 user.mail_notification = 'only_my_events'
487
487
488 unless user.valid?
488 unless user.valid?
489 user.login = "user#{Redmine::Utils.random_hex(6)}" unless user.errors[:login].blank?
489 user.login = "user#{Redmine::Utils.random_hex(6)}" unless user.errors[:login].blank?
490 user.firstname = "-" unless user.errors[:firstname].blank?
490 user.firstname = "-" unless user.errors[:firstname].blank?
491 (puts user.errors[:lastname];user.lastname = "-") unless user.errors[:lastname].blank?
491 (puts user.errors[:lastname];user.lastname = "-") unless user.errors[:lastname].blank?
492 end
492 end
493
493
494 user
494 user
495 end
495 end
496
496
497 # Creates a User for the +email+ sender
497 # Creates a User for the +email+ sender
498 # Returns the user or nil if it could not be created
498 # Returns the user or nil if it could not be created
499 def create_user_from_email
499 def create_user_from_email
500 from = email.header['from'].to_s
500 from = email.header['from'].to_s
501 addr, name = from, nil
501 addr, name = from, nil
502 if m = from.match(/^"?(.+?)"?\s+<(.+@.+)>$/)
502 if m = from.match(/^"?(.+?)"?\s+<(.+@.+)>$/)
503 addr, name = m[2], m[1]
503 addr, name = m[2], m[1]
504 end
504 end
505 if addr.present?
505 if addr.present?
506 user = self.class.new_user_from_attributes(addr, name)
506 user = self.class.new_user_from_attributes(addr, name)
507 if handler_options[:no_notification]
507 if handler_options[:no_notification]
508 user.mail_notification = 'none'
508 user.mail_notification = 'none'
509 end
509 end
510 if user.save
510 if user.save
511 user
511 user
512 else
512 else
513 logger.error "MailHandler: failed to create User: #{user.errors.full_messages}" if logger
513 logger.error "MailHandler: failed to create User: #{user.errors.full_messages}" if logger
514 nil
514 nil
515 end
515 end
516 else
516 else
517 logger.error "MailHandler: failed to create User: no FROM address found" if logger
517 logger.error "MailHandler: failed to create User: no FROM address found" if logger
518 nil
518 nil
519 end
519 end
520 end
520 end
521
521
522 # Adds the newly created user to default group
522 # Adds the newly created user to default group
523 def add_user_to_group(default_group)
523 def add_user_to_group(default_group)
524 if default_group.present?
524 if default_group.present?
525 default_group.split(',').each do |group_name|
525 default_group.split(',').each do |group_name|
526 if group = Group.named(group_name).first
526 if group = Group.named(group_name).first
527 group.users << @user
527 group.users << @user
528 elsif logger
528 elsif logger
529 logger.warn "MailHandler: could not add user to [#{group_name}], group not found"
529 logger.warn "MailHandler: could not add user to [#{group_name}], group not found"
530 end
530 end
531 end
531 end
532 end
532 end
533 end
533 end
534
534
535 # Removes the email body of text after the truncation configurations.
535 # Removes the email body of text after the truncation configurations.
536 def cleanup_body(body)
536 def cleanup_body(body)
537 delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
537 delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
538 unless delimiters.empty?
538 unless delimiters.empty?
539 regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
539 regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
540 body = body.gsub(regex, '')
540 body = body.gsub(regex, '')
541 end
541 end
542 body.strip
542 body.strip
543 end
543 end
544
544
545 def find_assignee_from_keyword(keyword, issue)
545 def find_assignee_from_keyword(keyword, issue)
546 keyword = keyword.to_s
546 keyword = keyword.to_s
547 return nil if keyword.blank?
548
547 assignable = issue.assignable_users
549 assignable = issue.assignable_users
548 assignee = nil
550 assignee = nil
549 assignee ||= assignable.detect {|a|
551 assignee ||= assignable.detect {|a|
550 keyword.casecmp(a.mail.to_s) == 0 ||
552 keyword.casecmp(a.mail.to_s) == 0 ||
551 keyword.casecmp(a.login.to_s) == 0
553 keyword.casecmp(a.login.to_s) == 0
552 }
554 }
553 if assignee.nil? && keyword.match(/ /)
555 if assignee.nil? && keyword.match(/ /)
554 firstname, lastname = *(keyword.split) # "First Last Throwaway"
556 firstname, lastname = *(keyword.split) # "First Last Throwaway"
555 assignee ||= assignable.detect {|a|
557 assignee ||= assignable.detect {|a|
556 a.is_a?(User) &&
558 a.is_a?(User) &&
557 firstname.casecmp(a.firstname.to_s) == 0 &&
559 firstname.casecmp(a.firstname.to_s) == 0 &&
558 lastname.casecmp(a.lastname.to_s) == 0
560 lastname.casecmp(a.lastname.to_s) == 0
559 }
561 }
560 end
562 end
561 if assignee.nil?
563 if assignee.nil?
562 assignee ||= assignable.detect {|a| keyword.casecmp(a.name) == 0}
564 assignee ||= assignable.detect {|a| keyword.casecmp(a.name) == 0}
563 end
565 end
564 assignee
566 assignee
565 end
567 end
566 end
568 end
General Comments 0
You need to be logged in to leave comments. Login now