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