##// END OF EJS Templates
ignore X-Autoreply mails (#15999, #13425)...
Toshi MARUYAMA -
r13356:963719042a12
parent child
Show More
@@ -1,550 +1,551
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2014 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class MailHandler < ActionMailer::Base
19 19 include ActionView::Helpers::SanitizeHelper
20 20 include Redmine::I18n
21 21
22 22 class UnauthorizedAction < StandardError; end
23 23 class MissingInformation < StandardError; end
24 24
25 25 attr_reader :email, :user
26 26
27 27 def self.receive(email, options={})
28 28 @@handler_options = options.deep_dup
29 29
30 30 @@handler_options[:issue] ||= {}
31 31
32 32 if @@handler_options[:allow_override].is_a?(String)
33 33 @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip)
34 34 end
35 35 @@handler_options[:allow_override] ||= []
36 36 # Project needs to be overridable if not specified
37 37 @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
38 38 # Status overridable by default
39 39 @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
40 40
41 41 @@handler_options[:no_account_notice] = (@@handler_options[:no_account_notice].to_s == '1')
42 42 @@handler_options[:no_notification] = (@@handler_options[:no_notification].to_s == '1')
43 43 @@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1')
44 44
45 45 email.force_encoding('ASCII-8BIT')
46 46 super(email)
47 47 end
48 48
49 49 # Receives an email and rescues any exception
50 50 def self.safe_receive(*args)
51 51 receive(*args)
52 52 rescue Exception => e
53 53 logger.error "An unexpected error occurred when receiving email: #{e.message}" if logger
54 54 return false
55 55 end
56 56
57 57 # Extracts MailHandler options from environment variables
58 58 # Use when receiving emails with rake tasks
59 59 def self.extract_options_from_env(env)
60 60 options = {:issue => {}}
61 61 %w(project status tracker category priority).each do |option|
62 62 options[:issue][option.to_sym] = env[option] if env[option]
63 63 end
64 64 %w(allow_override unknown_user no_permission_check no_account_notice default_group).each do |option|
65 65 options[option.to_sym] = env[option] if env[option]
66 66 end
67 67 options
68 68 end
69 69
70 70 def logger
71 71 Rails.logger
72 72 end
73 73
74 74 cattr_accessor :ignored_emails_headers
75 75 @@ignored_emails_headers = {
76 76 'X-Auto-Response-Suppress' => 'oof',
77 'Auto-Submitted' => /\Aauto-(replied|generated)/
77 'Auto-Submitted' => /\Aauto-(replied|generated)/,
78 'X-Autoreply' => 'yes'
78 79 }
79 80
80 81 # Processes incoming emails
81 82 # Returns the created object (eg. an issue, a message) or false
82 83 def receive(email)
83 84 @email = email
84 85 sender_email = email.from.to_a.first.to_s.strip
85 86 # Ignore emails received from the application emission address to avoid hell cycles
86 87 if sender_email.downcase == Setting.mail_from.to_s.strip.downcase
87 88 if logger
88 89 logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]"
89 90 end
90 91 return false
91 92 end
92 93 # Ignore auto generated emails
93 94 self.class.ignored_emails_headers.each do |key, ignored_value|
94 95 value = email.header[key]
95 96 if value
96 97 value = value.to_s.downcase
97 98 if (ignored_value.is_a?(Regexp) && value.match(ignored_value)) || value == ignored_value
98 99 if logger
99 100 logger.info "MailHandler: ignoring email with #{key}:#{value} header"
100 101 end
101 102 return false
102 103 end
103 104 end
104 105 end
105 106 @user = User.find_by_mail(sender_email) if sender_email.present?
106 107 if @user && !@user.active?
107 108 if logger
108 109 logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]"
109 110 end
110 111 return false
111 112 end
112 113 if @user.nil?
113 114 # Email was submitted by an unknown user
114 115 case @@handler_options[:unknown_user]
115 116 when 'accept'
116 117 @user = User.anonymous
117 118 when 'create'
118 119 @user = create_user_from_email
119 120 if @user
120 121 if logger
121 122 logger.info "MailHandler: [#{@user.login}] account created"
122 123 end
123 124 add_user_to_group(@@handler_options[:default_group])
124 125 unless @@handler_options[:no_account_notice]
125 126 Mailer.account_information(@user, @user.password).deliver
126 127 end
127 128 else
128 129 if logger
129 130 logger.error "MailHandler: could not create account for [#{sender_email}]"
130 131 end
131 132 return false
132 133 end
133 134 else
134 135 # Default behaviour, emails from unknown users are ignored
135 136 if logger
136 137 logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]"
137 138 end
138 139 return false
139 140 end
140 141 end
141 142 User.current = @user
142 143 dispatch
143 144 end
144 145
145 146 private
146 147
147 148 MESSAGE_ID_RE = %r{^<?redmine\.([a-z0-9_]+)\-(\d+)\.\d+(\.[a-f0-9]+)?@}
148 149 ISSUE_REPLY_SUBJECT_RE = %r{\[(?:[^\]]*\s+)?#(\d+)\]}
149 150 MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
150 151
151 152 def dispatch
152 153 headers = [email.in_reply_to, email.references].flatten.compact
153 154 subject = email.subject.to_s
154 155 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
155 156 klass, object_id = $1, $2.to_i
156 157 method_name = "receive_#{klass}_reply"
157 158 if self.class.private_instance_methods.collect(&:to_s).include?(method_name)
158 159 send method_name, object_id
159 160 else
160 161 # ignoring it
161 162 end
162 163 elsif m = subject.match(ISSUE_REPLY_SUBJECT_RE)
163 164 receive_issue_reply(m[1].to_i)
164 165 elsif m = subject.match(MESSAGE_REPLY_SUBJECT_RE)
165 166 receive_message_reply(m[1].to_i)
166 167 else
167 168 dispatch_to_default
168 169 end
169 170 rescue ActiveRecord::RecordInvalid => e
170 171 # TODO: send a email to the user
171 172 logger.error e.message if logger
172 173 false
173 174 rescue MissingInformation => e
174 175 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
175 176 false
176 177 rescue UnauthorizedAction => e
177 178 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
178 179 false
179 180 end
180 181
181 182 def dispatch_to_default
182 183 receive_issue
183 184 end
184 185
185 186 # Creates a new issue
186 187 def receive_issue
187 188 project = target_project
188 189 # check permission
189 190 unless @@handler_options[:no_permission_check]
190 191 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
191 192 end
192 193
193 194 issue = Issue.new(:author => user, :project => project)
194 195 issue.safe_attributes = issue_attributes_from_keywords(issue)
195 196 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
196 197 issue.subject = cleaned_up_subject
197 198 if issue.subject.blank?
198 199 issue.subject = '(no subject)'
199 200 end
200 201 issue.description = cleaned_up_text_body
201 202 issue.start_date ||= Date.today if Setting.default_issue_start_date_to_creation_date?
202 203
203 204 # add To and Cc as watchers before saving so the watchers can reply to Redmine
204 205 add_watchers(issue)
205 206 issue.save!
206 207 add_attachments(issue)
207 208 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger
208 209 issue
209 210 end
210 211
211 212 # Adds a note to an existing issue
212 213 def receive_issue_reply(issue_id, from_journal=nil)
213 214 issue = Issue.find_by_id(issue_id)
214 215 return unless issue
215 216 # check permission
216 217 unless @@handler_options[:no_permission_check]
217 218 unless user.allowed_to?(:add_issue_notes, issue.project) ||
218 219 user.allowed_to?(:edit_issues, issue.project)
219 220 raise UnauthorizedAction
220 221 end
221 222 end
222 223
223 224 # ignore CLI-supplied defaults for new issues
224 225 @@handler_options[:issue].clear
225 226
226 227 journal = issue.init_journal(user)
227 228 if from_journal && from_journal.private_notes?
228 229 # If the received email was a reply to a private note, make the added note private
229 230 issue.private_notes = true
230 231 end
231 232 issue.safe_attributes = issue_attributes_from_keywords(issue)
232 233 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
233 234 journal.notes = cleaned_up_text_body
234 235 add_attachments(issue)
235 236 issue.save!
236 237 if logger
237 238 logger.info "MailHandler: issue ##{issue.id} updated by #{user}"
238 239 end
239 240 journal
240 241 end
241 242
242 243 # Reply will be added to the issue
243 244 def receive_journal_reply(journal_id)
244 245 journal = Journal.find_by_id(journal_id)
245 246 if journal && journal.journalized_type == 'Issue'
246 247 receive_issue_reply(journal.journalized_id, journal)
247 248 end
248 249 end
249 250
250 251 # Receives a reply to a forum message
251 252 def receive_message_reply(message_id)
252 253 message = Message.find_by_id(message_id)
253 254 if message
254 255 message = message.root
255 256
256 257 unless @@handler_options[:no_permission_check]
257 258 raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
258 259 end
259 260
260 261 if !message.locked?
261 262 reply = Message.new(:subject => cleaned_up_subject.gsub(%r{^.*msg\d+\]}, '').strip,
262 263 :content => cleaned_up_text_body)
263 264 reply.author = user
264 265 reply.board = message.board
265 266 message.children << reply
266 267 add_attachments(reply)
267 268 reply
268 269 else
269 270 if logger
270 271 logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic"
271 272 end
272 273 end
273 274 end
274 275 end
275 276
276 277 def add_attachments(obj)
277 278 if email.attachments && email.attachments.any?
278 279 email.attachments.each do |attachment|
279 280 next unless accept_attachment?(attachment)
280 281 obj.attachments << Attachment.create(:container => obj,
281 282 :file => attachment.decoded,
282 283 :filename => attachment.filename,
283 284 :author => user,
284 285 :content_type => attachment.mime_type)
285 286 end
286 287 end
287 288 end
288 289
289 290 # Returns false if the +attachment+ of the incoming email should be ignored
290 291 def accept_attachment?(attachment)
291 292 @excluded ||= Setting.mail_handler_excluded_filenames.to_s.split(',').map(&:strip).reject(&:blank?)
292 293 @excluded.each do |pattern|
293 294 regexp = %r{\A#{Regexp.escape(pattern).gsub("\\*", ".*")}\z}i
294 295 if attachment.filename.to_s =~ regexp
295 296 logger.info "MailHandler: ignoring attachment #{attachment.filename} matching #{pattern}"
296 297 return false
297 298 end
298 299 end
299 300 true
300 301 end
301 302
302 303 # Adds To and Cc as watchers of the given object if the sender has the
303 304 # appropriate permission
304 305 def add_watchers(obj)
305 306 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
306 307 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
307 308 unless addresses.empty?
308 309 User.active.where('LOWER(mail) IN (?)', addresses).each do |w|
309 310 obj.add_watcher(w)
310 311 end
311 312 end
312 313 end
313 314 end
314 315
315 316 def get_keyword(attr, options={})
316 317 @keywords ||= {}
317 318 if @keywords.has_key?(attr)
318 319 @keywords[attr]
319 320 else
320 321 @keywords[attr] = begin
321 322 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) &&
322 323 (v = extract_keyword!(cleaned_up_text_body, attr, options[:format]))
323 324 v
324 325 elsif !@@handler_options[:issue][attr].blank?
325 326 @@handler_options[:issue][attr]
326 327 end
327 328 end
328 329 end
329 330 end
330 331
331 332 # Destructively extracts the value for +attr+ in +text+
332 333 # Returns nil if no matching keyword found
333 334 def extract_keyword!(text, attr, format=nil)
334 335 keys = [attr.to_s.humanize]
335 336 if attr.is_a?(Symbol)
336 337 if user && user.language.present?
337 338 keys << l("field_#{attr}", :default => '', :locale => user.language)
338 339 end
339 340 if Setting.default_language.present?
340 341 keys << l("field_#{attr}", :default => '', :locale => Setting.default_language)
341 342 end
342 343 end
343 344 keys.reject! {|k| k.blank?}
344 345 keys.collect! {|k| Regexp.escape(k)}
345 346 format ||= '.+'
346 347 keyword = nil
347 348 regexp = /^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i
348 349 if m = text.match(regexp)
349 350 keyword = m[2].strip
350 351 text.sub!(regexp, '')
351 352 end
352 353 keyword
353 354 end
354 355
355 356 def target_project
356 357 # TODO: other ways to specify project:
357 358 # * parse the email To field
358 359 # * specific project (eg. Setting.mail_handler_target_project)
359 360 target = Project.find_by_identifier(get_keyword(:project))
360 361 if target.nil?
361 362 # Invalid project keyword, use the project specified as the default one
362 363 default_project = @@handler_options[:issue][:project]
363 364 if default_project.present?
364 365 target = Project.find_by_identifier(default_project)
365 366 end
366 367 end
367 368 raise MissingInformation.new('Unable to determine target project') if target.nil?
368 369 target
369 370 end
370 371
371 372 # Returns a Hash of issue attributes extracted from keywords in the email body
372 373 def issue_attributes_from_keywords(issue)
373 374 assigned_to = (k = get_keyword(:assigned_to, :override => true)) && find_assignee_from_keyword(k, issue)
374 375
375 376 attrs = {
376 377 'tracker_id' => (k = get_keyword(:tracker)) && issue.project.trackers.named(k).first.try(:id),
377 378 'status_id' => (k = get_keyword(:status)) && IssueStatus.named(k).first.try(:id),
378 379 'priority_id' => (k = get_keyword(:priority)) && IssuePriority.named(k).first.try(:id),
379 380 'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.named(k).first.try(:id),
380 381 'assigned_to_id' => assigned_to.try(:id),
381 382 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) &&
382 383 issue.project.shared_versions.named(k).first.try(:id),
383 384 'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
384 385 'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
385 386 'estimated_hours' => get_keyword(:estimated_hours, :override => true),
386 387 'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0')
387 388 }.delete_if {|k, v| v.blank? }
388 389
389 390 if issue.new_record? && attrs['tracker_id'].nil?
390 391 attrs['tracker_id'] = issue.project.trackers.first.try(:id)
391 392 end
392 393
393 394 attrs
394 395 end
395 396
396 397 # Returns a Hash of issue custom field values extracted from keywords in the email body
397 398 def custom_field_values_from_keywords(customized)
398 399 customized.custom_field_values.inject({}) do |h, v|
399 400 if keyword = get_keyword(v.custom_field.name, :override => true)
400 401 h[v.custom_field.id.to_s] = v.custom_field.value_from_keyword(keyword, customized)
401 402 end
402 403 h
403 404 end
404 405 end
405 406
406 407 # Returns the text/plain part of the email
407 408 # If not found (eg. HTML-only email), returns the body with tags removed
408 409 def plain_text_body
409 410 return @plain_text_body unless @plain_text_body.nil?
410 411
411 412 parts = if (text_parts = email.all_parts.select {|p| p.mime_type == 'text/plain'}).present?
412 413 text_parts
413 414 elsif (html_parts = email.all_parts.select {|p| p.mime_type == 'text/html'}).present?
414 415 html_parts
415 416 else
416 417 [email]
417 418 end
418 419
419 420 parts.reject! do |part|
420 421 part.attachment?
421 422 end
422 423
423 424 @plain_text_body = parts.map do |p|
424 425 body_charset = Mail::RubyVer.respond_to?(:pick_encoding) ?
425 426 Mail::RubyVer.pick_encoding(p.charset).to_s : p.charset
426 427 Redmine::CodesetUtil.to_utf8(p.body.decoded, body_charset)
427 428 end.join("\r\n")
428 429
429 430 # strip html tags and remove doctype directive
430 431 if parts.any? {|p| p.mime_type == 'text/html'}
431 432 @plain_text_body = strip_tags(@plain_text_body.strip)
432 433 @plain_text_body.sub! %r{^<!DOCTYPE .*$}, ''
433 434 end
434 435
435 436 @plain_text_body
436 437 end
437 438
438 439 def cleaned_up_text_body
439 440 @cleaned_up_text_body ||= cleanup_body(plain_text_body)
440 441 end
441 442
442 443 def cleaned_up_subject
443 444 subject = email.subject.to_s
444 445 subject.strip[0,255]
445 446 end
446 447
447 448 def self.full_sanitizer
448 449 @full_sanitizer ||= HTML::FullSanitizer.new
449 450 end
450 451
451 452 def self.assign_string_attribute_with_limit(object, attribute, value, limit=nil)
452 453 limit ||= object.class.columns_hash[attribute.to_s].limit || 255
453 454 value = value.to_s.slice(0, limit)
454 455 object.send("#{attribute}=", value)
455 456 end
456 457
457 458 # Returns a User from an email address and a full name
458 459 def self.new_user_from_attributes(email_address, fullname=nil)
459 460 user = User.new
460 461
461 462 # Truncating the email address would result in an invalid format
462 463 user.mail = email_address
463 464 assign_string_attribute_with_limit(user, 'login', email_address, User::LOGIN_LENGTH_LIMIT)
464 465
465 466 names = fullname.blank? ? email_address.gsub(/@.*$/, '').split('.') : fullname.split
466 467 assign_string_attribute_with_limit(user, 'firstname', names.shift, 30)
467 468 assign_string_attribute_with_limit(user, 'lastname', names.join(' '), 30)
468 469 user.lastname = '-' if user.lastname.blank?
469 470 user.language = Setting.default_language
470 471 user.generate_password = true
471 472 user.mail_notification = 'only_my_events'
472 473
473 474 unless user.valid?
474 475 user.login = "user#{Redmine::Utils.random_hex(6)}" unless user.errors[:login].blank?
475 476 user.firstname = "-" unless user.errors[:firstname].blank?
476 477 (puts user.errors[:lastname];user.lastname = "-") unless user.errors[:lastname].blank?
477 478 end
478 479
479 480 user
480 481 end
481 482
482 483 # Creates a User for the +email+ sender
483 484 # Returns the user or nil if it could not be created
484 485 def create_user_from_email
485 486 from = email.header['from'].to_s
486 487 addr, name = from, nil
487 488 if m = from.match(/^"?(.+?)"?\s+<(.+@.+)>$/)
488 489 addr, name = m[2], m[1]
489 490 end
490 491 if addr.present?
491 492 user = self.class.new_user_from_attributes(addr, name)
492 493 if @@handler_options[:no_notification]
493 494 user.mail_notification = 'none'
494 495 end
495 496 if user.save
496 497 user
497 498 else
498 499 logger.error "MailHandler: failed to create User: #{user.errors.full_messages}" if logger
499 500 nil
500 501 end
501 502 else
502 503 logger.error "MailHandler: failed to create User: no FROM address found" if logger
503 504 nil
504 505 end
505 506 end
506 507
507 508 # Adds the newly created user to default group
508 509 def add_user_to_group(default_group)
509 510 if default_group.present?
510 511 default_group.split(',').each do |group_name|
511 512 if group = Group.named(group_name).first
512 513 group.users << @user
513 514 elsif logger
514 515 logger.warn "MailHandler: could not add user to [#{group_name}], group not found"
515 516 end
516 517 end
517 518 end
518 519 end
519 520
520 521 # Removes the email body of text after the truncation configurations.
521 522 def cleanup_body(body)
522 523 delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
523 524 unless delimiters.empty?
524 525 regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
525 526 body = body.gsub(regex, '')
526 527 end
527 528 body.strip
528 529 end
529 530
530 531 def find_assignee_from_keyword(keyword, issue)
531 532 keyword = keyword.to_s.downcase
532 533 assignable = issue.assignable_users
533 534 assignee = nil
534 535 assignee ||= assignable.detect {|a|
535 536 a.mail.to_s.downcase == keyword ||
536 537 a.login.to_s.downcase == keyword
537 538 }
538 539 if assignee.nil? && keyword.match(/ /)
539 540 firstname, lastname = *(keyword.split) # "First Last Throwaway"
540 541 assignee ||= assignable.detect {|a|
541 542 a.is_a?(User) && a.firstname.to_s.downcase == firstname &&
542 543 a.lastname.to_s.downcase == lastname
543 544 }
544 545 end
545 546 if assignee.nil?
546 547 assignee ||= assignable.detect {|a| a.name.downcase == keyword}
547 548 end
548 549 assignee
549 550 end
550 551 end
@@ -1,958 +1,959
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2014 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 require File.expand_path('../../test_helper', __FILE__)
21 21
22 22 class MailHandlerTest < ActiveSupport::TestCase
23 23 fixtures :users, :projects, :enabled_modules, :roles,
24 24 :members, :member_roles, :users,
25 25 :issues, :issue_statuses,
26 26 :workflows, :trackers, :projects_trackers,
27 27 :versions, :enumerations, :issue_categories,
28 28 :custom_fields, :custom_fields_trackers, :custom_fields_projects,
29 29 :boards, :messages
30 30
31 31 FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler'
32 32
33 33 def setup
34 34 ActionMailer::Base.deliveries.clear
35 35 Setting.notified_events = Redmine::Notifiable.all.collect(&:name)
36 36 end
37 37
38 38 def teardown
39 39 Setting.clear_cache
40 40 end
41 41
42 42 def test_add_issue
43 43 ActionMailer::Base.deliveries.clear
44 44 lft1 = new_issue_lft
45 45 # This email contains: 'Project: onlinestore'
46 46 issue = submit_email('ticket_on_given_project.eml')
47 47 assert issue.is_a?(Issue)
48 48 assert !issue.new_record?
49 49 issue.reload
50 50 assert_equal Project.find(2), issue.project
51 51 assert_equal issue.project.trackers.first, issue.tracker
52 52 assert_equal 'New ticket on a given project', issue.subject
53 53 assert_equal User.find_by_login('jsmith'), issue.author
54 54 assert_equal IssueStatus.find_by_name('Resolved'), issue.status
55 55 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
56 56 assert_equal '2010-01-01', issue.start_date.to_s
57 57 assert_equal '2010-12-31', issue.due_date.to_s
58 58 assert_equal User.find_by_login('jsmith'), issue.assigned_to
59 59 assert_equal Version.find_by_name('Alpha'), issue.fixed_version
60 60 assert_equal 2.5, issue.estimated_hours
61 61 assert_equal 30, issue.done_ratio
62 62 assert_equal [issue.id, lft1, lft1 + 1], [issue.root_id, issue.lft, issue.rgt]
63 63 # keywords should be removed from the email body
64 64 assert !issue.description.match(/^Project:/i)
65 65 assert !issue.description.match(/^Status:/i)
66 66 assert !issue.description.match(/^Start Date:/i)
67 67 # Email notification should be sent
68 68 mail = ActionMailer::Base.deliveries.last
69 69 assert_not_nil mail
70 70 assert mail.subject.include?("##{issue.id}")
71 71 assert mail.subject.include?('New ticket on a given project')
72 72 end
73 73
74 74 def test_add_issue_with_default_tracker
75 75 # This email contains: 'Project: onlinestore'
76 76 issue = submit_email(
77 77 'ticket_on_given_project.eml',
78 78 :issue => {:tracker => 'Support request'}
79 79 )
80 80 assert issue.is_a?(Issue)
81 81 assert !issue.new_record?
82 82 issue.reload
83 83 assert_equal 'Support request', issue.tracker.name
84 84 end
85 85
86 86 def test_add_issue_with_status
87 87 # This email contains: 'Project: onlinestore' and 'Status: Resolved'
88 88 issue = submit_email('ticket_on_given_project.eml')
89 89 assert issue.is_a?(Issue)
90 90 assert !issue.new_record?
91 91 issue.reload
92 92 assert_equal Project.find(2), issue.project
93 93 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
94 94 end
95 95
96 96 def test_add_issue_with_attributes_override
97 97 issue = submit_email(
98 98 'ticket_with_attributes.eml',
99 99 :allow_override => 'tracker,category,priority'
100 100 )
101 101 assert issue.is_a?(Issue)
102 102 assert !issue.new_record?
103 103 issue.reload
104 104 assert_equal 'New ticket on a given project', issue.subject
105 105 assert_equal User.find_by_login('jsmith'), issue.author
106 106 assert_equal Project.find(2), issue.project
107 107 assert_equal 'Feature request', issue.tracker.to_s
108 108 assert_equal 'Stock management', issue.category.to_s
109 109 assert_equal 'Urgent', issue.priority.to_s
110 110 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
111 111 end
112 112
113 113 def test_add_issue_with_group_assignment
114 114 with_settings :issue_group_assignment => '1' do
115 115 issue = submit_email('ticket_on_given_project.eml') do |email|
116 116 email.gsub!('Assigned to: John Smith', 'Assigned to: B Team')
117 117 end
118 118 assert issue.is_a?(Issue)
119 119 assert !issue.new_record?
120 120 issue.reload
121 121 assert_equal Group.find(11), issue.assigned_to
122 122 end
123 123 end
124 124
125 125 def test_add_issue_with_partial_attributes_override
126 126 issue = submit_email(
127 127 'ticket_with_attributes.eml',
128 128 :issue => {:priority => 'High'},
129 129 :allow_override => ['tracker']
130 130 )
131 131 assert issue.is_a?(Issue)
132 132 assert !issue.new_record?
133 133 issue.reload
134 134 assert_equal 'New ticket on a given project', issue.subject
135 135 assert_equal User.find_by_login('jsmith'), issue.author
136 136 assert_equal Project.find(2), issue.project
137 137 assert_equal 'Feature request', issue.tracker.to_s
138 138 assert_nil issue.category
139 139 assert_equal 'High', issue.priority.to_s
140 140 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
141 141 end
142 142
143 143 def test_add_issue_with_spaces_between_attribute_and_separator
144 144 issue = submit_email(
145 145 'ticket_with_spaces_between_attribute_and_separator.eml',
146 146 :allow_override => 'tracker,category,priority'
147 147 )
148 148 assert issue.is_a?(Issue)
149 149 assert !issue.new_record?
150 150 issue.reload
151 151 assert_equal 'New ticket on a given project', issue.subject
152 152 assert_equal User.find_by_login('jsmith'), issue.author
153 153 assert_equal Project.find(2), issue.project
154 154 assert_equal 'Feature request', issue.tracker.to_s
155 155 assert_equal 'Stock management', issue.category.to_s
156 156 assert_equal 'Urgent', issue.priority.to_s
157 157 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
158 158 end
159 159
160 160 def test_add_issue_with_attachment_to_specific_project
161 161 issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
162 162 assert issue.is_a?(Issue)
163 163 assert !issue.new_record?
164 164 issue.reload
165 165 assert_equal 'Ticket created by email with attachment', issue.subject
166 166 assert_equal User.find_by_login('jsmith'), issue.author
167 167 assert_equal Project.find(2), issue.project
168 168 assert_equal 'This is a new ticket with attachments', issue.description
169 169 # Attachment properties
170 170 assert_equal 1, issue.attachments.size
171 171 assert_equal 'Paella.jpg', issue.attachments.first.filename
172 172 assert_equal 'image/jpeg', issue.attachments.first.content_type
173 173 assert_equal 10790, issue.attachments.first.filesize
174 174 end
175 175
176 176 def test_add_issue_with_custom_fields
177 177 issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'onlinestore'})
178 178 assert issue.is_a?(Issue)
179 179 assert !issue.new_record?
180 180 issue.reload
181 181 assert_equal 'New ticket with custom field values', issue.subject
182 182 assert_equal 'PostgreSQL', issue.custom_field_value(1)
183 183 assert_equal 'Value for a custom field', issue.custom_field_value(2)
184 184 assert !issue.description.match(/^searchable field:/i)
185 185 end
186 186
187 187 def test_add_issue_with_version_custom_fields
188 188 field = IssueCustomField.create!(:name => 'Affected version', :field_format => 'version', :is_for_all => true, :tracker_ids => [1,2,3])
189 189
190 190 issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'ecookbook'}) do |email|
191 191 email << "Affected version: 1.0\n"
192 192 end
193 193 assert issue.is_a?(Issue)
194 194 assert !issue.new_record?
195 195 issue.reload
196 196 assert_equal '2', issue.custom_field_value(field)
197 197 end
198 198
199 199 def test_add_issue_should_match_assignee_on_display_name
200 200 user = User.generate!(:firstname => 'Foo Bar', :lastname => 'Foo Baz')
201 201 User.add_to_project(user, Project.find(2))
202 202 issue = submit_email('ticket_on_given_project.eml') do |email|
203 203 email.sub!(/^Assigned to.*$/, 'Assigned to: Foo Bar Foo baz')
204 204 end
205 205 assert issue.is_a?(Issue)
206 206 assert_equal user, issue.assigned_to
207 207 end
208 208
209 209 def test_add_issue_should_set_default_start_date
210 210 with_settings :default_issue_start_date_to_creation_date => '1' do
211 211 issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'})
212 212 assert issue.is_a?(Issue)
213 213 assert_equal Date.today, issue.start_date
214 214 end
215 215 end
216 216
217 217 def test_add_issue_with_cc
218 218 issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'})
219 219 assert issue.is_a?(Issue)
220 220 assert !issue.new_record?
221 221 issue.reload
222 222 assert issue.watched_by?(User.find_by_mail('dlopper@somenet.foo'))
223 223 assert_equal 1, issue.watcher_user_ids.size
224 224 end
225 225
226 226 def test_add_issue_by_unknown_user
227 227 assert_no_difference 'User.count' do
228 228 assert_equal false,
229 229 submit_email(
230 230 'ticket_by_unknown_user.eml',
231 231 :issue => {:project => 'ecookbook'}
232 232 )
233 233 end
234 234 end
235 235
236 236 def test_add_issue_by_anonymous_user
237 237 Role.anonymous.add_permission!(:add_issues)
238 238 assert_no_difference 'User.count' do
239 239 issue = submit_email(
240 240 'ticket_by_unknown_user.eml',
241 241 :issue => {:project => 'ecookbook'},
242 242 :unknown_user => 'accept'
243 243 )
244 244 assert issue.is_a?(Issue)
245 245 assert issue.author.anonymous?
246 246 end
247 247 end
248 248
249 249 def test_add_issue_by_anonymous_user_with_no_from_address
250 250 Role.anonymous.add_permission!(:add_issues)
251 251 assert_no_difference 'User.count' do
252 252 issue = submit_email(
253 253 'ticket_by_empty_user.eml',
254 254 :issue => {:project => 'ecookbook'},
255 255 :unknown_user => 'accept'
256 256 )
257 257 assert issue.is_a?(Issue)
258 258 assert issue.author.anonymous?
259 259 end
260 260 end
261 261
262 262 def test_add_issue_by_anonymous_user_on_private_project
263 263 Role.anonymous.add_permission!(:add_issues)
264 264 assert_no_difference 'User.count' do
265 265 assert_no_difference 'Issue.count' do
266 266 assert_equal false,
267 267 submit_email(
268 268 'ticket_by_unknown_user.eml',
269 269 :issue => {:project => 'onlinestore'},
270 270 :unknown_user => 'accept'
271 271 )
272 272 end
273 273 end
274 274 end
275 275
276 276 def test_add_issue_by_anonymous_user_on_private_project_without_permission_check
277 277 lft1 = new_issue_lft
278 278 assert_no_difference 'User.count' do
279 279 assert_difference 'Issue.count' do
280 280 issue = submit_email(
281 281 'ticket_by_unknown_user.eml',
282 282 :issue => {:project => 'onlinestore'},
283 283 :no_permission_check => '1',
284 284 :unknown_user => 'accept'
285 285 )
286 286 assert issue.is_a?(Issue)
287 287 assert issue.author.anonymous?
288 288 assert !issue.project.is_public?
289 289 assert_equal [issue.id, lft1, lft1 + 1], [issue.root_id, issue.lft, issue.rgt]
290 290 end
291 291 end
292 292 end
293 293
294 294 def test_add_issue_by_created_user
295 295 Setting.default_language = 'en'
296 296 assert_difference 'User.count' do
297 297 issue = submit_email(
298 298 'ticket_by_unknown_user.eml',
299 299 :issue => {:project => 'ecookbook'},
300 300 :unknown_user => 'create'
301 301 )
302 302 assert issue.is_a?(Issue)
303 303 assert issue.author.active?
304 304 assert_equal 'john.doe@somenet.foo', issue.author.mail
305 305 assert_equal 'John', issue.author.firstname
306 306 assert_equal 'Doe', issue.author.lastname
307 307
308 308 # account information
309 309 email = ActionMailer::Base.deliveries.first
310 310 assert_not_nil email
311 311 assert email.subject.include?('account activation')
312 312 login = mail_body(email).match(/\* Login: (.*)$/)[1].strip
313 313 password = mail_body(email).match(/\* Password: (.*)$/)[1].strip
314 314 assert_equal issue.author, User.try_to_login(login, password)
315 315 end
316 316 end
317 317
318 318 def test_created_user_should_be_added_to_groups
319 319 group1 = Group.generate!
320 320 group2 = Group.generate!
321 321
322 322 assert_difference 'User.count' do
323 323 submit_email(
324 324 'ticket_by_unknown_user.eml',
325 325 :issue => {:project => 'ecookbook'},
326 326 :unknown_user => 'create',
327 327 :default_group => "#{group1.name},#{group2.name}"
328 328 )
329 329 end
330 330 user = User.order('id DESC').first
331 331 assert_equal [group1, group2].sort, user.groups.sort
332 332 end
333 333
334 334 def test_created_user_should_not_receive_account_information_with_no_account_info_option
335 335 assert_difference 'User.count' do
336 336 submit_email(
337 337 'ticket_by_unknown_user.eml',
338 338 :issue => {:project => 'ecookbook'},
339 339 :unknown_user => 'create',
340 340 :no_account_notice => '1'
341 341 )
342 342 end
343 343
344 344 # only 1 email for the new issue notification
345 345 assert_equal 1, ActionMailer::Base.deliveries.size
346 346 email = ActionMailer::Base.deliveries.first
347 347 assert_include 'Ticket by unknown user', email.subject
348 348 end
349 349
350 350 def test_created_user_should_have_mail_notification_to_none_with_no_notification_option
351 351 assert_difference 'User.count' do
352 352 submit_email(
353 353 'ticket_by_unknown_user.eml',
354 354 :issue => {:project => 'ecookbook'},
355 355 :unknown_user => 'create',
356 356 :no_notification => '1'
357 357 )
358 358 end
359 359 user = User.order('id DESC').first
360 360 assert_equal 'none', user.mail_notification
361 361 end
362 362
363 363 def test_add_issue_without_from_header
364 364 Role.anonymous.add_permission!(:add_issues)
365 365 assert_equal false, submit_email('ticket_without_from_header.eml')
366 366 end
367 367
368 368 def test_add_issue_with_invalid_attributes
369 369 with_settings :default_issue_start_date_to_creation_date => '0' do
370 370 issue = submit_email(
371 371 'ticket_with_invalid_attributes.eml',
372 372 :allow_override => 'tracker,category,priority'
373 373 )
374 374 assert issue.is_a?(Issue)
375 375 assert !issue.new_record?
376 376 issue.reload
377 377 assert_nil issue.assigned_to
378 378 assert_nil issue.start_date
379 379 assert_nil issue.due_date
380 380 assert_equal 0, issue.done_ratio
381 381 assert_equal 'Normal', issue.priority.to_s
382 382 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
383 383 end
384 384 end
385 385
386 386 def test_add_issue_with_invalid_project_should_be_assigned_to_default_project
387 387 issue = submit_email('ticket_on_given_project.eml', :issue => {:project => 'ecookbook'}, :allow_override => 'project') do |email|
388 388 email.gsub!(/^Project:.+$/, 'Project: invalid')
389 389 end
390 390 assert issue.is_a?(Issue)
391 391 assert !issue.new_record?
392 392 assert_equal 'ecookbook', issue.project.identifier
393 393 end
394 394
395 395 def test_add_issue_with_localized_attributes
396 396 User.find_by_mail('jsmith@somenet.foo').update_attribute 'language', 'fr'
397 397 issue = submit_email(
398 398 'ticket_with_localized_attributes.eml',
399 399 :allow_override => 'tracker,category,priority'
400 400 )
401 401 assert issue.is_a?(Issue)
402 402 assert !issue.new_record?
403 403 issue.reload
404 404 assert_equal 'New ticket on a given project', issue.subject
405 405 assert_equal User.find_by_login('jsmith'), issue.author
406 406 assert_equal Project.find(2), issue.project
407 407 assert_equal 'Feature request', issue.tracker.to_s
408 408 assert_equal 'Stock management', issue.category.to_s
409 409 assert_equal 'Urgent', issue.priority.to_s
410 410 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
411 411 end
412 412
413 413 def test_add_issue_with_japanese_keywords
414 414 ja_dev = "\xe9\x96\x8b\xe7\x99\xba".force_encoding('UTF-8')
415 415 tracker = Tracker.generate!(:name => ja_dev)
416 416 Project.find(1).trackers << tracker
417 417 issue = submit_email(
418 418 'japanese_keywords_iso_2022_jp.eml',
419 419 :issue => {:project => 'ecookbook'},
420 420 :allow_override => 'tracker'
421 421 )
422 422 assert_kind_of Issue, issue
423 423 assert_equal tracker, issue.tracker
424 424 end
425 425
426 426 def test_add_issue_from_apple_mail
427 427 issue = submit_email(
428 428 'apple_mail_with_attachment.eml',
429 429 :issue => {:project => 'ecookbook'}
430 430 )
431 431 assert_kind_of Issue, issue
432 432 assert_equal 1, issue.attachments.size
433 433
434 434 attachment = issue.attachments.first
435 435 assert_equal 'paella.jpg', attachment.filename
436 436 assert_equal 10790, attachment.filesize
437 437 assert File.exist?(attachment.diskfile)
438 438 assert_equal 10790, File.size(attachment.diskfile)
439 439 assert_equal 'caaf384198bcbc9563ab5c058acd73cd', attachment.digest
440 440 end
441 441
442 442 def test_thunderbird_with_attachment_ja
443 443 issue = submit_email(
444 444 'thunderbird_with_attachment_ja.eml',
445 445 :issue => {:project => 'ecookbook'}
446 446 )
447 447 assert_kind_of Issue, issue
448 448 assert_equal 1, issue.attachments.size
449 449 ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88.txt".force_encoding('UTF-8')
450 450 attachment = issue.attachments.first
451 451 assert_equal ja, attachment.filename
452 452 assert_equal 5, attachment.filesize
453 453 assert File.exist?(attachment.diskfile)
454 454 assert_equal 5, File.size(attachment.diskfile)
455 455 assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest
456 456 end
457 457
458 458 def test_gmail_with_attachment_ja
459 459 issue = submit_email(
460 460 'gmail_with_attachment_ja.eml',
461 461 :issue => {:project => 'ecookbook'}
462 462 )
463 463 assert_kind_of Issue, issue
464 464 assert_equal 1, issue.attachments.size
465 465 ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88.txt".force_encoding('UTF-8')
466 466 attachment = issue.attachments.first
467 467 assert_equal ja, attachment.filename
468 468 assert_equal 5, attachment.filesize
469 469 assert File.exist?(attachment.diskfile)
470 470 assert_equal 5, File.size(attachment.diskfile)
471 471 assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest
472 472 end
473 473
474 474 def test_thunderbird_with_attachment_latin1
475 475 issue = submit_email(
476 476 'thunderbird_with_attachment_iso-8859-1.eml',
477 477 :issue => {:project => 'ecookbook'}
478 478 )
479 479 assert_kind_of Issue, issue
480 480 assert_equal 1, issue.attachments.size
481 481 u = "".force_encoding('UTF-8')
482 482 u1 = "\xc3\x84\xc3\xa4\xc3\x96\xc3\xb6\xc3\x9c\xc3\xbc".force_encoding('UTF-8')
483 483 11.times { u << u1 }
484 484 attachment = issue.attachments.first
485 485 assert_equal "#{u}.png", attachment.filename
486 486 assert_equal 130, attachment.filesize
487 487 assert File.exist?(attachment.diskfile)
488 488 assert_equal 130, File.size(attachment.diskfile)
489 489 assert_equal '4d80e667ac37dddfe05502530f152abb', attachment.digest
490 490 end
491 491
492 492 def test_gmail_with_attachment_latin1
493 493 issue = submit_email(
494 494 'gmail_with_attachment_iso-8859-1.eml',
495 495 :issue => {:project => 'ecookbook'}
496 496 )
497 497 assert_kind_of Issue, issue
498 498 assert_equal 1, issue.attachments.size
499 499 u = "".force_encoding('UTF-8')
500 500 u1 = "\xc3\x84\xc3\xa4\xc3\x96\xc3\xb6\xc3\x9c\xc3\xbc".force_encoding('UTF-8')
501 501 11.times { u << u1 }
502 502 attachment = issue.attachments.first
503 503 assert_equal "#{u}.txt", attachment.filename
504 504 assert_equal 5, attachment.filesize
505 505 assert File.exist?(attachment.diskfile)
506 506 assert_equal 5, File.size(attachment.diskfile)
507 507 assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest
508 508 end
509 509
510 510 def test_multiple_inline_text_parts_should_be_appended_to_issue_description
511 511 issue = submit_email('multiple_text_parts.eml', :issue => {:project => 'ecookbook'})
512 512 assert_include 'first', issue.description
513 513 assert_include 'second', issue.description
514 514 assert_include 'third', issue.description
515 515 end
516 516
517 517 def test_attachment_text_part_should_be_added_as_issue_attachment
518 518 issue = submit_email('multiple_text_parts.eml', :issue => {:project => 'ecookbook'})
519 519 assert_not_include 'Plain text attachment', issue.description
520 520 attachment = issue.attachments.detect {|a| a.filename == 'textfile.txt'}
521 521 assert_not_nil attachment
522 522 assert_include 'Plain text attachment', File.read(attachment.diskfile)
523 523 end
524 524
525 525 def test_add_issue_with_iso_8859_1_subject
526 526 issue = submit_email(
527 527 'subject_as_iso-8859-1.eml',
528 528 :issue => {:project => 'ecookbook'}
529 529 )
530 530 str = "Testmail from Webmail: \xc3\xa4 \xc3\xb6 \xc3\xbc...".force_encoding('UTF-8')
531 531 assert_kind_of Issue, issue
532 532 assert_equal str, issue.subject
533 533 end
534 534
535 535 def test_quoted_printable_utf8
536 536 issue = submit_email(
537 537 'quoted_printable_utf8.eml',
538 538 :issue => {:project => 'ecookbook'}
539 539 )
540 540 assert_kind_of Issue, issue
541 541 str = "Freundliche Gr\xc3\xbcsse".force_encoding('UTF-8')
542 542 assert_equal str, issue.description
543 543 end
544 544
545 545 def test_gmail_iso8859_2
546 546 issue = submit_email(
547 547 'gmail-iso8859-2.eml',
548 548 :issue => {:project => 'ecookbook'}
549 549 )
550 550 assert_kind_of Issue, issue
551 551 str = "Na \xc5\xa1triku se su\xc5\xa1i \xc5\xa1osi\xc4\x87.".force_encoding('UTF-8')
552 552 assert issue.description.include?(str)
553 553 end
554 554
555 555 def test_add_issue_with_japanese_subject
556 556 issue = submit_email(
557 557 'subject_japanese_1.eml',
558 558 :issue => {:project => 'ecookbook'}
559 559 )
560 560 assert_kind_of Issue, issue
561 561 ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88".force_encoding('UTF-8')
562 562 assert_equal ja, issue.subject
563 563 end
564 564
565 565 def test_add_issue_with_korean_body
566 566 # Make sure mail bodies with a charset unknown to Ruby
567 567 # but known to the Mail gem 2.5.4 are handled correctly
568 568 kr = "\xEA\xB3\xA0\xEB\xA7\x99\xEC\x8A\xB5\xEB\x8B\x88\xEB\x8B\xA4.".force_encoding('UTF-8')
569 569 issue = submit_email(
570 570 'body_ks_c_5601-1987.eml',
571 571 :issue => {:project => 'ecookbook'}
572 572 )
573 573 assert_kind_of Issue, issue
574 574 assert_equal kr, issue.description
575 575 end
576 576
577 577 def test_add_issue_with_no_subject_header
578 578 issue = submit_email(
579 579 'no_subject_header.eml',
580 580 :issue => {:project => 'ecookbook'}
581 581 )
582 582 assert_kind_of Issue, issue
583 583 assert_equal '(no subject)', issue.subject
584 584 end
585 585
586 586 def test_add_issue_with_mixed_japanese_subject
587 587 issue = submit_email(
588 588 'subject_japanese_2.eml',
589 589 :issue => {:project => 'ecookbook'}
590 590 )
591 591 assert_kind_of Issue, issue
592 592 ja = "Re: \xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88".force_encoding('UTF-8')
593 593 assert_equal ja, issue.subject
594 594 end
595 595
596 596 def test_should_ignore_emails_from_locked_users
597 597 User.find(2).lock!
598 598
599 599 MailHandler.any_instance.expects(:dispatch).never
600 600 assert_no_difference 'Issue.count' do
601 601 assert_equal false, submit_email('ticket_on_given_project.eml')
602 602 end
603 603 end
604 604
605 605 def test_should_ignore_emails_from_emission_address
606 606 Role.anonymous.add_permission!(:add_issues)
607 607 assert_no_difference 'User.count' do
608 608 assert_equal false,
609 609 submit_email(
610 610 'ticket_from_emission_address.eml',
611 611 :issue => {:project => 'ecookbook'},
612 612 :unknown_user => 'create'
613 613 )
614 614 end
615 615 end
616 616
617 617 def test_should_ignore_auto_replied_emails
618 618 MailHandler.any_instance.expects(:dispatch).never
619 619 [
620 620 "X-Auto-Response-Suppress: OOF",
621 621 "Auto-Submitted: auto-replied",
622 622 "Auto-Submitted: Auto-Replied",
623 "Auto-Submitted: auto-generated"
623 "Auto-Submitted: auto-generated",
624 'X-Autoreply: yes'
624 625 ].each do |header|
625 626 raw = IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml'))
626 627 raw = header + "\n" + raw
627 628
628 629 assert_no_difference 'Issue.count' do
629 630 assert_equal false, MailHandler.receive(raw), "email with #{header} header was not ignored"
630 631 end
631 632 end
632 633 end
633 634
634 635 test "should not ignore Auto-Submitted headers not defined in RFC3834" do
635 636 [
636 637 "Auto-Submitted: auto-forwarded"
637 638 ].each do |header|
638 639 raw = IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml'))
639 640 raw = header + "\n" + raw
640 641
641 642 assert_difference 'Issue.count', 1 do
642 643 assert_not_nil MailHandler.receive(raw), "email with #{header} header was ignored"
643 644 end
644 645 end
645 646 end
646 647
647 648 def test_add_issue_should_send_email_notification
648 649 Setting.notified_events = ['issue_added']
649 650 ActionMailer::Base.deliveries.clear
650 651 # This email contains: 'Project: onlinestore'
651 652 issue = submit_email('ticket_on_given_project.eml')
652 653 assert issue.is_a?(Issue)
653 654 assert_equal 1, ActionMailer::Base.deliveries.size
654 655 end
655 656
656 657 def test_update_issue
657 658 journal = submit_email('ticket_reply.eml')
658 659 assert journal.is_a?(Journal)
659 660 assert_equal User.find_by_login('jsmith'), journal.user
660 661 assert_equal Issue.find(2), journal.journalized
661 662 assert_match /This is reply/, journal.notes
662 663 assert_equal false, journal.private_notes
663 664 assert_equal 'Feature request', journal.issue.tracker.name
664 665 end
665 666
666 667 def test_update_issue_should_accept_issue_id_after_space_inside_brackets
667 668 journal = submit_email('ticket_reply_with_status.eml') do |email|
668 669 assert email.sub!(/^Subject:.*$/, "Subject: Re: [Feature request #2] Add ingredients categories")
669 670 end
670 671 assert journal.is_a?(Journal)
671 672 assert_equal Issue.find(2), journal.journalized
672 673 end
673 674
674 675 def test_update_issue_should_accept_issue_id_inside_brackets
675 676 journal = submit_email('ticket_reply_with_status.eml') do |email|
676 677 assert email.sub!(/^Subject:.*$/, "Subject: Re: [#2] Add ingredients categories")
677 678 end
678 679 assert journal.is_a?(Journal)
679 680 assert_equal Issue.find(2), journal.journalized
680 681 end
681 682
682 683 def test_update_issue_should_ignore_bogus_issue_ids_in_subject
683 684 journal = submit_email('ticket_reply_with_status.eml') do |email|
684 685 assert email.sub!(/^Subject:.*$/, "Subject: Re: [12345#1][bogus#1][Feature request #2] Add ingredients categories")
685 686 end
686 687 assert journal.is_a?(Journal)
687 688 assert_equal Issue.find(2), journal.journalized
688 689 end
689 690
690 691 def test_update_issue_with_attribute_changes
691 692 # This email contains: 'Status: Resolved'
692 693 journal = submit_email('ticket_reply_with_status.eml')
693 694 assert journal.is_a?(Journal)
694 695 issue = Issue.find(journal.issue.id)
695 696 assert_equal User.find_by_login('jsmith'), journal.user
696 697 assert_equal Issue.find(2), journal.journalized
697 698 assert_match /This is reply/, journal.notes
698 699 assert_equal 'Feature request', journal.issue.tracker.name
699 700 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
700 701 assert_equal '2010-01-01', issue.start_date.to_s
701 702 assert_equal '2010-12-31', issue.due_date.to_s
702 703 assert_equal User.find_by_login('jsmith'), issue.assigned_to
703 704 assert_equal "52.6", issue.custom_value_for(CustomField.find_by_name('Float field')).value
704 705 # keywords should be removed from the email body
705 706 assert !journal.notes.match(/^Status:/i)
706 707 assert !journal.notes.match(/^Start Date:/i)
707 708 end
708 709
709 710 def test_update_issue_with_attachment
710 711 assert_difference 'Journal.count' do
711 712 assert_difference 'JournalDetail.count' do
712 713 assert_difference 'Attachment.count' do
713 714 assert_no_difference 'Issue.count' do
714 715 journal = submit_email('ticket_with_attachment.eml') do |raw|
715 716 raw.gsub! /^Subject: .*$/, 'Subject: Re: [Cookbook - Feature #2] (New) Add ingredients categories'
716 717 end
717 718 end
718 719 end
719 720 end
720 721 end
721 722 journal = Journal.order('id DESC').first
722 723 assert_equal Issue.find(2), journal.journalized
723 724 assert_equal 1, journal.details.size
724 725
725 726 detail = journal.details.first
726 727 assert_equal 'attachment', detail.property
727 728 assert_equal 'Paella.jpg', detail.value
728 729 end
729 730
730 731 def test_update_issue_should_send_email_notification
731 732 ActionMailer::Base.deliveries.clear
732 733 journal = submit_email('ticket_reply.eml')
733 734 assert journal.is_a?(Journal)
734 735 assert_equal 1, ActionMailer::Base.deliveries.size
735 736 end
736 737
737 738 def test_update_issue_should_not_set_defaults
738 739 journal = submit_email(
739 740 'ticket_reply.eml',
740 741 :issue => {:tracker => 'Support request', :priority => 'High'}
741 742 )
742 743 assert journal.is_a?(Journal)
743 744 assert_match /This is reply/, journal.notes
744 745 assert_equal 'Feature request', journal.issue.tracker.name
745 746 assert_equal 'Normal', journal.issue.priority.name
746 747 end
747 748
748 749 def test_replying_to_a_private_note_should_add_reply_as_private
749 750 private_journal = Journal.create!(:notes => 'Private notes', :journalized => Issue.find(1), :private_notes => true, :user_id => 2)
750 751
751 752 assert_difference 'Journal.count' do
752 753 journal = submit_email('ticket_reply.eml') do |email|
753 754 email.sub! %r{^In-Reply-To:.*$}, "In-Reply-To: <redmine.journal-#{private_journal.id}.20060719210421@osiris>"
754 755 end
755 756
756 757 assert_kind_of Journal, journal
757 758 assert_match /This is reply/, journal.notes
758 759 assert_equal true, journal.private_notes
759 760 end
760 761 end
761 762
762 763 def test_reply_to_a_message
763 764 m = submit_email('message_reply.eml')
764 765 assert m.is_a?(Message)
765 766 assert !m.new_record?
766 767 m.reload
767 768 assert_equal 'Reply via email', m.subject
768 769 # The email replies to message #2 which is part of the thread of message #1
769 770 assert_equal Message.find(1), m.parent
770 771 end
771 772
772 773 def test_reply_to_a_message_by_subject
773 774 m = submit_email('message_reply_by_subject.eml')
774 775 assert m.is_a?(Message)
775 776 assert !m.new_record?
776 777 m.reload
777 778 assert_equal 'Reply to the first post', m.subject
778 779 assert_equal Message.find(1), m.parent
779 780 end
780 781
781 782 def test_should_strip_tags_of_html_only_emails
782 783 issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'})
783 784 assert issue.is_a?(Issue)
784 785 assert !issue.new_record?
785 786 issue.reload
786 787 assert_equal 'HTML email', issue.subject
787 788 assert_equal 'This is a html-only email.', issue.description
788 789 end
789 790
790 791 test "truncate emails with no setting should add the entire email into the issue" do
791 792 with_settings :mail_handler_body_delimiters => '' do
792 793 issue = submit_email('ticket_on_given_project.eml')
793 794 assert_issue_created(issue)
794 795 assert issue.description.include?('---')
795 796 assert issue.description.include?('This paragraph is after the delimiter')
796 797 end
797 798 end
798 799
799 800 test "truncate emails with a single string should truncate the email at the delimiter for the issue" do
800 801 with_settings :mail_handler_body_delimiters => '---' do
801 802 issue = submit_email('ticket_on_given_project.eml')
802 803 assert_issue_created(issue)
803 804 assert issue.description.include?('This paragraph is before delimiters')
804 805 assert issue.description.include?('--- This line starts with a delimiter')
805 806 assert !issue.description.match(/^---$/)
806 807 assert !issue.description.include?('This paragraph is after the delimiter')
807 808 end
808 809 end
809 810
810 811 test "truncate emails with a single quoted reply should truncate the email at the delimiter with the quoted reply symbols (>)" do
811 812 with_settings :mail_handler_body_delimiters => '--- Reply above. Do not remove this line. ---' do
812 813 journal = submit_email('issue_update_with_quoted_reply_above.eml')
813 814 assert journal.is_a?(Journal)
814 815 assert journal.notes.include?('An update to the issue by the sender.')
815 816 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
816 817 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
817 818 end
818 819 end
819 820
820 821 test "truncate emails with multiple quoted replies should truncate the email at the delimiter with the quoted reply symbols (>)" do
821 822 with_settings :mail_handler_body_delimiters => '--- Reply above. Do not remove this line. ---' do
822 823 journal = submit_email('issue_update_with_multiple_quoted_reply_above.eml')
823 824 assert journal.is_a?(Journal)
824 825 assert journal.notes.include?('An update to the issue by the sender.')
825 826 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
826 827 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
827 828 end
828 829 end
829 830
830 831 test "truncate emails with multiple strings should truncate the email at the first delimiter found (BREAK)" do
831 832 with_settings :mail_handler_body_delimiters => "---\nBREAK" do
832 833 issue = submit_email('ticket_on_given_project.eml')
833 834 assert_issue_created(issue)
834 835 assert issue.description.include?('This paragraph is before delimiters')
835 836 assert !issue.description.include?('BREAK')
836 837 assert !issue.description.include?('This paragraph is between delimiters')
837 838 assert !issue.description.match(/^---$/)
838 839 assert !issue.description.include?('This paragraph is after the delimiter')
839 840 end
840 841 end
841 842
842 843 def test_attachments_that_match_mail_handler_excluded_filenames_should_be_ignored
843 844 with_settings :mail_handler_excluded_filenames => '*.vcf, *.jpg' do
844 845 issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
845 846 assert issue.is_a?(Issue)
846 847 assert !issue.new_record?
847 848 assert_equal 0, issue.reload.attachments.size
848 849 end
849 850 end
850 851
851 852 def test_attachments_that_do_not_match_mail_handler_excluded_filenames_should_be_attached
852 853 with_settings :mail_handler_excluded_filenames => '*.vcf, *.gif' do
853 854 issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
854 855 assert issue.is_a?(Issue)
855 856 assert !issue.new_record?
856 857 assert_equal 1, issue.reload.attachments.size
857 858 end
858 859 end
859 860
860 861 def test_email_with_long_subject_line
861 862 issue = submit_email('ticket_with_long_subject.eml')
862 863 assert issue.is_a?(Issue)
863 864 assert_equal issue.subject, 'New ticket on a given project with a very long subject line which exceeds 255 chars and should not be ignored but chopped off. And if the subject line is still not long enough, we just add more text. And more text. Wow, this is really annoying. Especially, if you have nothing to say...'[0,255]
864 865 end
865 866
866 867 def test_first_keyword_should_be_matched
867 868 issue = submit_email('ticket_with_duplicate_keyword.eml', :allow_override => 'priority')
868 869 assert issue.is_a?(Issue)
869 870 assert_equal 'High', issue.priority.name
870 871 end
871 872
872 873 def test_keyword_after_delimiter_should_be_ignored
873 874 with_settings :mail_handler_body_delimiters => "== DELIMITER ==" do
874 875 issue = submit_email('ticket_with_keyword_after_delimiter.eml', :allow_override => 'priority')
875 876 assert issue.is_a?(Issue)
876 877 assert_equal 'Normal', issue.priority.name
877 878 end
878 879 end
879 880
880 881 def test_new_user_from_attributes_should_return_valid_user
881 882 to_test = {
882 883 # [address, name] => [login, firstname, lastname]
883 884 ['jsmith@example.net', nil] => ['jsmith@example.net', 'jsmith', '-'],
884 885 ['jsmith@example.net', 'John'] => ['jsmith@example.net', 'John', '-'],
885 886 ['jsmith@example.net', 'John Smith'] => ['jsmith@example.net', 'John', 'Smith'],
886 887 ['jsmith@example.net', 'John Paul Smith'] => ['jsmith@example.net', 'John', 'Paul Smith'],
887 888 ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsTheMaximumLength Smith'] => ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsT', 'Smith'],
888 889 ['jsmith@example.net', 'John AVeryLongLastnameThatExceedsTheMaximumLength'] => ['jsmith@example.net', 'John', 'AVeryLongLastnameThatExceedsTh']
889 890 }
890 891
891 892 to_test.each do |attrs, expected|
892 893 user = MailHandler.new_user_from_attributes(attrs.first, attrs.last)
893 894
894 895 assert user.valid?, user.errors.full_messages.to_s
895 896 assert_equal attrs.first, user.mail
896 897 assert_equal expected[0], user.login
897 898 assert_equal expected[1], user.firstname
898 899 assert_equal expected[2], user.lastname
899 900 assert_equal 'only_my_events', user.mail_notification
900 901 end
901 902 end
902 903
903 904 def test_new_user_from_attributes_should_use_default_login_if_invalid
904 905 user = MailHandler.new_user_from_attributes('foo+bar@example.net')
905 906 assert user.valid?
906 907 assert user.login =~ /^user[a-f0-9]+$/
907 908 assert_equal 'foo+bar@example.net', user.mail
908 909 end
909 910
910 911 def test_new_user_with_utf8_encoded_fullname_should_be_decoded
911 912 assert_difference 'User.count' do
912 913 issue = submit_email(
913 914 'fullname_of_sender_as_utf8_encoded.eml',
914 915 :issue => {:project => 'ecookbook'},
915 916 :unknown_user => 'create'
916 917 )
917 918 end
918 919 user = User.order('id DESC').first
919 920 assert_equal "foo@example.org", user.mail
920 921 str1 = "\xc3\x84\xc3\xa4".force_encoding('UTF-8')
921 922 str2 = "\xc3\x96\xc3\xb6".force_encoding('UTF-8')
922 923 assert_equal str1, user.firstname
923 924 assert_equal str2, user.lastname
924 925 end
925 926
926 927 def test_extract_options_from_env_should_return_options
927 928 options = MailHandler.extract_options_from_env({
928 929 'tracker' => 'defect',
929 930 'project' => 'foo',
930 931 'unknown_user' => 'create'
931 932 })
932 933
933 934 assert_equal({
934 935 :issue => {:tracker => 'defect', :project => 'foo'},
935 936 :unknown_user => 'create'
936 937 }, options)
937 938 end
938 939
939 940 def test_safe_receive_should_rescue_exceptions_and_return_false
940 941 MailHandler.stubs(:receive).raises(Exception.new "Something went wrong")
941 942
942 943 assert_equal false, MailHandler.safe_receive
943 944 end
944 945
945 946 private
946 947
947 948 def submit_email(filename, options={})
948 949 raw = IO.read(File.join(FIXTURES_PATH, filename))
949 950 yield raw if block_given?
950 951 MailHandler.receive(raw, options)
951 952 end
952 953
953 954 def assert_issue_created(issue)
954 955 assert issue.is_a?(Issue)
955 956 assert !issue.new_record?
956 957 issue.reload
957 958 end
958 959 end
General Comments 0
You need to be logged in to leave comments. Login now