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