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