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