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