Mercurial > hg > soundsoftware-site
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? |