comparison app/models/mail_handler.rb @ 1464:261b3d9a4903 redmine-2.4

Update to Redmine 2.4 branch rev 12663
author Chris Cannam
date Tue, 14 Jan 2014 14:37:42 +0000
parents 3e4c3460b6ca
children e248c7af89ec
comparison
equal deleted inserted replaced
1296:038ba2d95de8 1464:261b3d9a4903
1 # Redmine - project management software 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 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.
36 # Project needs to be overridable if not specified 36 # Project needs to be overridable if not specified
37 @@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)
38 # Status overridable by default 38 # Status overridable by default
39 @@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)
40 40
41 @@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1' ? true : false) 41 @@handler_options[:no_account_notice] = (@@handler_options[:no_account_notice].to_s == '1')
42 @@handler_options[:no_notification] = (@@handler_options[:no_notification].to_s == '1')
43 @@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1')
42 44
43 email.force_encoding('ASCII-8BIT') if email.respond_to?(:force_encoding) 45 email.force_encoding('ASCII-8BIT') if email.respond_to?(:force_encoding)
44 super(email) 46 super(email)
47 end
48
49 # Extracts MailHandler options from environment variables
50 # Use when receiving emails with rake tasks
51 def self.extract_options_from_env(env)
52 options = {:issue => {}}
53 %w(project status tracker category priority).each do |option|
54 options[:issue][option.to_sym] = env[option] if env[option]
55 end
56 %w(allow_override unknown_user no_permission_check no_account_notice default_group).each do |option|
57 options[option.to_sym] = env[option] if env[option]
58 end
59 options
45 end 60 end
46 61
47 def logger 62 def logger
48 Rails.logger 63 Rails.logger
49 end 64 end
59 def receive(email) 74 def receive(email)
60 @email = email 75 @email = email
61 sender_email = email.from.to_a.first.to_s.strip 76 sender_email = email.from.to_a.first.to_s.strip
62 # 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
63 if sender_email.downcase == Setting.mail_from.to_s.strip.downcase 78 if sender_email.downcase == Setting.mail_from.to_s.strip.downcase
64 if logger && logger.info 79 if logger
65 logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]" 80 logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]"
66 end 81 end
67 return false 82 return false
68 end 83 end
69 # Ignore auto generated emails 84 # Ignore auto generated emails
70 self.class.ignored_emails_headers.each do |key, ignored_value| 85 self.class.ignored_emails_headers.each do |key, ignored_value|
71 value = email.header[key] 86 value = email.header[key]
72 if value 87 if value
73 value = value.to_s.downcase 88 value = value.to_s.downcase
74 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
75 if logger && logger.info 90 if logger
76 logger.info "MailHandler: ignoring email with #{key}:#{value} header" 91 logger.info "MailHandler: ignoring email with #{key}:#{value} header"
77 end 92 end
78 return false 93 return false
79 end 94 end
80 end 95 end
81 end 96 end
82 @user = User.find_by_mail(sender_email) if sender_email.present? 97 @user = User.find_by_mail(sender_email) if sender_email.present?
83 if @user && !@user.active? 98 if @user && !@user.active?
84 if logger && logger.info 99 if logger
85 logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]" 100 logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]"
86 end 101 end
87 return false 102 return false
88 end 103 end
89 if @user.nil? 104 if @user.nil?
92 when 'accept' 107 when 'accept'
93 @user = User.anonymous 108 @user = User.anonymous
94 when 'create' 109 when 'create'
95 @user = create_user_from_email 110 @user = create_user_from_email
96 if @user 111 if @user
97 if logger && logger.info 112 if logger
98 logger.info "MailHandler: [#{@user.login}] account created" 113 logger.info "MailHandler: [#{@user.login}] account created"
99 end 114 end
100 Mailer.account_information(@user, @user.password).deliver 115 add_user_to_group(@@handler_options[:default_group])
116 unless @@handler_options[:no_account_notice]
117 Mailer.account_information(@user, @user.password).deliver
118 end
101 else 119 else
102 if logger && logger.error 120 if logger
103 logger.error "MailHandler: could not create account for [#{sender_email}]" 121 logger.error "MailHandler: could not create account for [#{sender_email}]"
104 end 122 end
105 return false 123 return false
106 end 124 end
107 else 125 else
108 # Default behaviour, emails from unknown users are ignored 126 # Default behaviour, emails from unknown users are ignored
109 if logger && logger.info 127 if logger
110 logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]" 128 logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]"
111 end 129 end
112 return false 130 return false
113 end 131 end
114 end 132 end
115 User.current = @user 133 User.current = @user
116 dispatch 134 dispatch
117 end 135 end
118 136
119 private 137 private
120 138
121 MESSAGE_ID_RE = %r{^<?redmine\.([a-z0-9_]+)\-(\d+)\.\d+@} 139 MESSAGE_ID_RE = %r{^<?redmine\.([a-z0-9_]+)\-(\d+)\.\d+(\.[a-f0-9]+)?@}
122 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]*#(\d+)\]} 140 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]*#(\d+)\]}
123 MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]} 141 MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
124 142
125 def dispatch 143 def dispatch
126 headers = [email.in_reply_to, email.references].flatten.compact 144 headers = [email.in_reply_to, email.references].flatten.compact
175 193
176 # 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
177 add_watchers(issue) 195 add_watchers(issue)
178 issue.save! 196 issue.save!
179 add_attachments(issue) 197 add_attachments(issue)
180 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
181 issue 199 issue
182 end 200 end
183 201
184 # Adds a note to an existing issue 202 # Adds a note to an existing issue
185 def receive_issue_reply(issue_id, from_journal=nil) 203 def receive_issue_reply(issue_id, from_journal=nil)
204 issue.safe_attributes = issue_attributes_from_keywords(issue) 222 issue.safe_attributes = issue_attributes_from_keywords(issue)
205 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)}
206 journal.notes = cleaned_up_text_body 224 journal.notes = cleaned_up_text_body
207 add_attachments(issue) 225 add_attachments(issue)
208 issue.save! 226 issue.save!
209 if logger && logger.info 227 if logger
210 logger.info "MailHandler: issue ##{issue.id} updated by #{user}" 228 logger.info "MailHandler: issue ##{issue.id} updated by #{user}"
211 end 229 end
212 journal 230 journal
213 end 231 end
214 232
237 reply.board = message.board 255 reply.board = message.board
238 message.children << reply 256 message.children << reply
239 add_attachments(reply) 257 add_attachments(reply)
240 reply 258 reply
241 else 259 else
242 if logger && logger.info 260 if logger
243 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"
244 end 262 end
245 end 263 end
246 end 264 end
247 end 265 end
248 266
249 def add_attachments(obj) 267 def add_attachments(obj)
250 if email.attachments && email.attachments.any? 268 if email.attachments && email.attachments.any?
251 email.attachments.each do |attachment| 269 email.attachments.each do |attachment|
270 next unless accept_attachment?(attachment)
252 obj.attachments << Attachment.create(:container => obj, 271 obj.attachments << Attachment.create(:container => obj,
253 :file => attachment.decoded, 272 :file => attachment.decoded,
254 :filename => attachment.filename, 273 :filename => attachment.filename,
255 :author => user, 274 :author => user,
256 :content_type => attachment.mime_type) 275 :content_type => attachment.mime_type)
257 end 276 end
258 end 277 end
278 end
279
280 # Returns false if the +attachment+ of the incoming email should be ignored
281 def accept_attachment?(attachment)
282 @excluded ||= Setting.mail_handler_excluded_filenames.to_s.split(',').map(&:strip).reject(&:blank?)
283 @excluded.each do |pattern|
284 regexp = %r{\A#{Regexp.escape(pattern).gsub("\\*", ".*")}\z}i
285 if attachment.filename.to_s =~ regexp
286 logger.info "MailHandler: ignoring attachment #{attachment.filename} matching #{pattern}"
287 return false
288 end
289 end
290 true
259 end 291 end
260 292
261 # Adds To and Cc as watchers of the given object if the sender has the 293 # Adds To and Cc as watchers of the given object if the sender has the
262 # appropriate permission 294 # appropriate permission
263 def add_watchers(obj) 295 def add_watchers(obj)
264 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project) 296 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
265 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase} 297 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
266 unless addresses.empty? 298 unless addresses.empty?
267 watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses]) 299 watchers = User.active.where('LOWER(mail) IN (?)', addresses).all
268 watchers.each {|w| obj.add_watcher(w)} 300 watchers.each {|w| obj.add_watcher(w)}
269 end 301 end
270 end 302 end
271 end 303 end
272 304
313 def target_project 345 def target_project
314 # TODO: other ways to specify project: 346 # TODO: other ways to specify project:
315 # * parse the email To field 347 # * parse the email To field
316 # * specific project (eg. Setting.mail_handler_target_project) 348 # * specific project (eg. Setting.mail_handler_target_project)
317 target = Project.find_by_identifier(get_keyword(:project)) 349 target = Project.find_by_identifier(get_keyword(:project))
350 if target.nil?
351 # Invalid project keyword, use the project specified as the default one
352 default_project = @@handler_options[:issue][:project]
353 if default_project.present?
354 target = Project.find_by_identifier(default_project)
355 end
356 end
318 raise MissingInformation.new('Unable to determine target project') if target.nil? 357 raise MissingInformation.new('Unable to determine target project') if target.nil?
319 target 358 target
320 end 359 end
321 360
322 # Returns a Hash of issue attributes extracted from keywords in the email body 361 # Returns a Hash of issue attributes extracted from keywords in the email body
336 'estimated_hours' => get_keyword(:estimated_hours, :override => true), 375 'estimated_hours' => get_keyword(:estimated_hours, :override => true),
337 'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0') 376 'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0')
338 }.delete_if {|k, v| v.blank? } 377 }.delete_if {|k, v| v.blank? }
339 378
340 if issue.new_record? && attrs['tracker_id'].nil? 379 if issue.new_record? && attrs['tracker_id'].nil?
341 attrs['tracker_id'] = issue.project.trackers.find(:first).try(:id) 380 attrs['tracker_id'] = issue.project.trackers.first.try(:id)
342 end 381 end
343 382
344 attrs 383 attrs
345 end 384 end
346 385
357 # Returns the text/plain part of the email 396 # Returns the text/plain part of the email
358 # If not found (eg. HTML-only email), returns the body with tags removed 397 # If not found (eg. HTML-only email), returns the body with tags removed
359 def plain_text_body 398 def plain_text_body
360 return @plain_text_body unless @plain_text_body.nil? 399 return @plain_text_body unless @plain_text_body.nil?
361 400
362 part = email.text_part || email.html_part || email 401 parts = if (text_parts = email.all_parts.select {|p| p.mime_type == 'text/plain'}).present?
363 @plain_text_body = Redmine::CodesetUtil.to_utf8(part.body.decoded, part.charset) 402 text_parts
403 elsif (html_parts = email.all_parts.select {|p| p.mime_type == 'text/html'}).present?
404 html_parts
405 else
406 [email]
407 end
408
409 parts.reject! do |part|
410 part.header[:content_disposition].try(:disposition_type) == 'attachment'
411 end
412
413 @plain_text_body = parts.map {|p| Redmine::CodesetUtil.to_utf8(p.body.decoded, p.charset)}.join("\r\n")
364 414
365 # strip html tags and remove doctype directive 415 # strip html tags and remove doctype directive
366 @plain_text_body = strip_tags(@plain_text_body.strip) 416 if parts.any? {|p| p.mime_type == 'text/html'}
367 @plain_text_body.sub! %r{^<!DOCTYPE .*$}, '' 417 @plain_text_body = strip_tags(@plain_text_body.strip)
418 @plain_text_body.sub! %r{^<!DOCTYPE .*$}, ''
419 end
420
368 @plain_text_body 421 @plain_text_body
369 end 422 end
370 423
371 def cleaned_up_text_body 424 def cleaned_up_text_body
372 cleanup_body(plain_text_body) 425 cleanup_body(plain_text_body)
394 # Truncating the email address would result in an invalid format 447 # Truncating the email address would result in an invalid format
395 user.mail = email_address 448 user.mail = email_address
396 assign_string_attribute_with_limit(user, 'login', email_address, User::LOGIN_LENGTH_LIMIT) 449 assign_string_attribute_with_limit(user, 'login', email_address, User::LOGIN_LENGTH_LIMIT)
397 450
398 names = fullname.blank? ? email_address.gsub(/@.*$/, '').split('.') : fullname.split 451 names = fullname.blank? ? email_address.gsub(/@.*$/, '').split('.') : fullname.split
399 assign_string_attribute_with_limit(user, 'firstname', names.shift) 452 assign_string_attribute_with_limit(user, 'firstname', names.shift, 30)
400 assign_string_attribute_with_limit(user, 'lastname', names.join(' ')) 453 assign_string_attribute_with_limit(user, 'lastname', names.join(' '), 30)
401 user.lastname = '-' if user.lastname.blank? 454 user.lastname = '-' if user.lastname.blank?
402
403 password_length = [Setting.password_min_length.to_i, 10].max
404 user.password = Redmine::Utils.random_hex(password_length / 2 + 1)
405 user.language = Setting.default_language 455 user.language = Setting.default_language
456 user.generate_password = true
457 user.mail_notification = 'only_my_events'
406 458
407 unless user.valid? 459 unless user.valid?
408 user.login = "user#{Redmine::Utils.random_hex(6)}" unless user.errors[:login].blank? 460 user.login = "user#{Redmine::Utils.random_hex(6)}" unless user.errors[:login].blank?
409 user.firstname = "-" unless user.errors[:firstname].blank? 461 user.firstname = "-" unless user.errors[:firstname].blank?
410 user.lastname = "-" unless user.errors[:lastname].blank? 462 (puts user.errors[:lastname];user.lastname = "-") unless user.errors[:lastname].blank?
411 end 463 end
412 464
413 user 465 user
414 end 466 end
415 467
421 if m = from.match(/^"?(.+?)"?\s+<(.+@.+)>$/) 473 if m = from.match(/^"?(.+?)"?\s+<(.+@.+)>$/)
422 addr, name = m[2], m[1] 474 addr, name = m[2], m[1]
423 end 475 end
424 if addr.present? 476 if addr.present?
425 user = self.class.new_user_from_attributes(addr, name) 477 user = self.class.new_user_from_attributes(addr, name)
478 if @@handler_options[:no_notification]
479 user.mail_notification = 'none'
480 end
426 if user.save 481 if user.save
427 user 482 user
428 else 483 else
429 logger.error "MailHandler: failed to create User: #{user.errors.full_messages}" if logger 484 logger.error "MailHandler: failed to create User: #{user.errors.full_messages}" if logger
430 nil 485 nil
431 end 486 end
432 else 487 else
433 logger.error "MailHandler: failed to create User: no FROM address found" if logger 488 logger.error "MailHandler: failed to create User: no FROM address found" if logger
434 nil 489 nil
490 end
491 end
492
493 # Adds the newly created user to default group
494 def add_user_to_group(default_group)
495 if default_group.present?
496 default_group.split(',').each do |group_name|
497 if group = Group.named(group_name).first
498 group.users << @user
499 elsif logger
500 logger.warn "MailHandler: could not add user to [#{group_name}], group not found"
501 end
502 end
435 end 503 end
436 end 504 end
437 505
438 # Removes the email body of text after the truncation configurations. 506 # Removes the email body of text after the truncation configurations.
439 def cleanup_body(body) 507 def cleanup_body(body)
453 a.mail.to_s.downcase == keyword || 521 a.mail.to_s.downcase == keyword ||
454 a.login.to_s.downcase == keyword 522 a.login.to_s.downcase == keyword
455 } 523 }
456 if assignee.nil? && keyword.match(/ /) 524 if assignee.nil? && keyword.match(/ /)
457 firstname, lastname = *(keyword.split) # "First Last Throwaway" 525 firstname, lastname = *(keyword.split) # "First Last Throwaway"
458 assignee ||= assignable.detect {|a| 526 assignee ||= assignable.detect {|a|
459 a.is_a?(User) && a.firstname.to_s.downcase == firstname && 527 a.is_a?(User) && a.firstname.to_s.downcase == firstname &&
460 a.lastname.to_s.downcase == lastname 528 a.lastname.to_s.downcase == lastname
461 } 529 }
462 end 530 end
463 if assignee.nil? 531 if assignee.nil?