##// END OF EJS Templates
Ignore emails with "Auto-Submitted: auto-*" header (#11338)....
Jean-Philippe Lang -
r9741:232edc623727
parent child
Show More
@@ -1,472 +1,475
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class MailHandler < ActionMailer::Base
19 19 include ActionView::Helpers::SanitizeHelper
20 20 include Redmine::I18n
21 21
22 22 class UnauthorizedAction < StandardError; end
23 23 class MissingInformation < StandardError; end
24 24
25 25 attr_reader :email, :user
26 26
27 27 def self.receive(email, options={})
28 28 @@handler_options = options.dup
29 29
30 30 @@handler_options[:issue] ||= {}
31 31
32 32 if @@handler_options[:allow_override].is_a?(String)
33 33 @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip)
34 34 end
35 35 @@handler_options[:allow_override] ||= []
36 36 # Project needs to be overridable if not specified
37 37 @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
38 38 # Status overridable by default
39 39 @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
40 40
41 41 @@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1' ? true : false)
42 42
43 43 email.force_encoding('ASCII-8BIT') if email.respond_to?(:force_encoding)
44 44 super(email)
45 45 end
46 46
47 47 def logger
48 48 Rails.logger
49 49 end
50 50
51 51 cattr_accessor :ignored_emails_headers
52 52 @@ignored_emails_headers = {
53 'X-Auto-Response-Suppress' => 'OOF',
54 'Auto-Submitted' => 'auto-replied'
53 'X-Auto-Response-Suppress' => 'oof',
54 'Auto-Submitted' => /^auto-/
55 55 }
56 56
57 57 # Processes incoming emails
58 58 # Returns the created object (eg. an issue, a message) or false
59 59 def receive(email)
60 60 @email = email
61 61 sender_email = email.from.to_a.first.to_s.strip
62 62 # Ignore emails received from the application emission address to avoid hell cycles
63 63 if sender_email.downcase == Setting.mail_from.to_s.strip.downcase
64 64 if logger && logger.info
65 65 logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]"
66 66 end
67 67 return false
68 68 end
69 69 # Ignore auto generated emails
70 70 self.class.ignored_emails_headers.each do |key, ignored_value|
71 71 value = email.header[key]
72 if value && value.to_s.downcase == ignored_value.downcase
72 if value
73 value = value.to_s.downcase
74 if (ignored_value.is_a?(Regexp) && value.match(ignored_value)) || value == ignored_value
73 75 if logger && logger.info
74 76 logger.info "MailHandler: ignoring email with #{key}:#{value} header"
75 77 end
76 78 return false
77 79 end
78 80 end
81 end
79 82 @user = User.find_by_mail(sender_email) if sender_email.present?
80 83 if @user && !@user.active?
81 84 if logger && logger.info
82 85 logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]"
83 86 end
84 87 return false
85 88 end
86 89 if @user.nil?
87 90 # Email was submitted by an unknown user
88 91 case @@handler_options[:unknown_user]
89 92 when 'accept'
90 93 @user = User.anonymous
91 94 when 'create'
92 95 @user = create_user_from_email
93 96 if @user
94 97 if logger && logger.info
95 98 logger.info "MailHandler: [#{@user.login}] account created"
96 99 end
97 100 Mailer.account_information(@user, @user.password).deliver
98 101 else
99 102 if logger && logger.error
100 103 logger.error "MailHandler: could not create account for [#{sender_email}]"
101 104 end
102 105 return false
103 106 end
104 107 else
105 108 # Default behaviour, emails from unknown users are ignored
106 109 if logger && logger.info
107 110 logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]"
108 111 end
109 112 return false
110 113 end
111 114 end
112 115 User.current = @user
113 116 dispatch
114 117 end
115 118
116 119 private
117 120
118 121 MESSAGE_ID_RE = %r{^<?redmine\.([a-z0-9_]+)\-(\d+)\.\d+@}
119 122 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]*#(\d+)\]}
120 123 MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
121 124
122 125 def dispatch
123 126 headers = [email.in_reply_to, email.references].flatten.compact
124 127 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
125 128 klass, object_id = $1, $2.to_i
126 129 method_name = "receive_#{klass}_reply"
127 130 if self.class.private_instance_methods.collect(&:to_s).include?(method_name)
128 131 send method_name, object_id
129 132 else
130 133 # ignoring it
131 134 end
132 135 elsif m = email.subject.match(ISSUE_REPLY_SUBJECT_RE)
133 136 receive_issue_reply(m[1].to_i)
134 137 elsif m = email.subject.match(MESSAGE_REPLY_SUBJECT_RE)
135 138 receive_message_reply(m[1].to_i)
136 139 else
137 140 dispatch_to_default
138 141 end
139 142 rescue ActiveRecord::RecordInvalid => e
140 143 # TODO: send a email to the user
141 144 logger.error e.message if logger
142 145 false
143 146 rescue MissingInformation => e
144 147 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
145 148 false
146 149 rescue UnauthorizedAction => e
147 150 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
148 151 false
149 152 end
150 153
151 154 def dispatch_to_default
152 155 receive_issue
153 156 end
154 157
155 158 # Creates a new issue
156 159 def receive_issue
157 160 project = target_project
158 161 # check permission
159 162 unless @@handler_options[:no_permission_check]
160 163 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
161 164 end
162 165
163 166 issue = Issue.new(:author => user, :project => project)
164 167 issue.safe_attributes = issue_attributes_from_keywords(issue)
165 168 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
166 169 issue.subject = cleaned_up_subject
167 170 if issue.subject.blank?
168 171 issue.subject = '(no subject)'
169 172 end
170 173 issue.description = cleaned_up_text_body
171 174
172 175 # add To and Cc as watchers before saving so the watchers can reply to Redmine
173 176 add_watchers(issue)
174 177 issue.save!
175 178 add_attachments(issue)
176 179 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
177 180 issue
178 181 end
179 182
180 183 # Adds a note to an existing issue
181 184 def receive_issue_reply(issue_id)
182 185 issue = Issue.find_by_id(issue_id)
183 186 return unless issue
184 187 # check permission
185 188 unless @@handler_options[:no_permission_check]
186 189 unless user.allowed_to?(:add_issue_notes, issue.project) ||
187 190 user.allowed_to?(:edit_issues, issue.project)
188 191 raise UnauthorizedAction
189 192 end
190 193 end
191 194
192 195 # ignore CLI-supplied defaults for new issues
193 196 @@handler_options[:issue].clear
194 197
195 198 journal = issue.init_journal(user)
196 199 issue.safe_attributes = issue_attributes_from_keywords(issue)
197 200 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
198 201 journal.notes = cleaned_up_text_body
199 202 add_attachments(issue)
200 203 issue.save!
201 204 if logger && logger.info
202 205 logger.info "MailHandler: issue ##{issue.id} updated by #{user}"
203 206 end
204 207 journal
205 208 end
206 209
207 210 # Reply will be added to the issue
208 211 def receive_journal_reply(journal_id)
209 212 journal = Journal.find_by_id(journal_id)
210 213 if journal && journal.journalized_type == 'Issue'
211 214 receive_issue_reply(journal.journalized_id)
212 215 end
213 216 end
214 217
215 218 # Receives a reply to a forum message
216 219 def receive_message_reply(message_id)
217 220 message = Message.find_by_id(message_id)
218 221 if message
219 222 message = message.root
220 223
221 224 unless @@handler_options[:no_permission_check]
222 225 raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
223 226 end
224 227
225 228 if !message.locked?
226 229 reply = Message.new(:subject => cleaned_up_subject.gsub(%r{^.*msg\d+\]}, '').strip,
227 230 :content => cleaned_up_text_body)
228 231 reply.author = user
229 232 reply.board = message.board
230 233 message.children << reply
231 234 add_attachments(reply)
232 235 reply
233 236 else
234 237 if logger && logger.info
235 238 logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic"
236 239 end
237 240 end
238 241 end
239 242 end
240 243
241 244 def add_attachments(obj)
242 245 if email.attachments && email.attachments.any?
243 246 email.attachments.each do |attachment|
244 247 obj.attachments << Attachment.create(:container => obj,
245 248 :file => attachment.decoded,
246 249 :filename => attachment.filename,
247 250 :author => user,
248 251 :content_type => attachment.mime_type)
249 252 end
250 253 end
251 254 end
252 255
253 256 # Adds To and Cc as watchers of the given object if the sender has the
254 257 # appropriate permission
255 258 def add_watchers(obj)
256 259 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
257 260 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
258 261 unless addresses.empty?
259 262 watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
260 263 watchers.each {|w| obj.add_watcher(w)}
261 264 end
262 265 end
263 266 end
264 267
265 268 def get_keyword(attr, options={})
266 269 @keywords ||= {}
267 270 if @keywords.has_key?(attr)
268 271 @keywords[attr]
269 272 else
270 273 @keywords[attr] = begin
271 274 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) &&
272 275 (v = extract_keyword!(plain_text_body, attr, options[:format]))
273 276 v
274 277 elsif !@@handler_options[:issue][attr].blank?
275 278 @@handler_options[:issue][attr]
276 279 end
277 280 end
278 281 end
279 282 end
280 283
281 284 # Destructively extracts the value for +attr+ in +text+
282 285 # Returns nil if no matching keyword found
283 286 def extract_keyword!(text, attr, format=nil)
284 287 keys = [attr.to_s.humanize]
285 288 if attr.is_a?(Symbol)
286 289 if user && user.language.present?
287 290 keys << l("field_#{attr}", :default => '', :locale => user.language)
288 291 end
289 292 if Setting.default_language.present?
290 293 keys << l("field_#{attr}", :default => '', :locale => Setting.default_language)
291 294 end
292 295 end
293 296 keys.reject! {|k| k.blank?}
294 297 keys.collect! {|k| Regexp.escape(k)}
295 298 format ||= '.+'
296 299 keyword = nil
297 300 regexp = /^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i
298 301 if m = text.match(regexp)
299 302 keyword = m[2].strip
300 303 text.gsub!(regexp, '')
301 304 end
302 305 keyword
303 306 end
304 307
305 308 def target_project
306 309 # TODO: other ways to specify project:
307 310 # * parse the email To field
308 311 # * specific project (eg. Setting.mail_handler_target_project)
309 312 target = Project.find_by_identifier(get_keyword(:project))
310 313 raise MissingInformation.new('Unable to determine target project') if target.nil?
311 314 target
312 315 end
313 316
314 317 # Returns a Hash of issue attributes extracted from keywords in the email body
315 318 def issue_attributes_from_keywords(issue)
316 319 assigned_to = (k = get_keyword(:assigned_to, :override => true)) && find_assignee_from_keyword(k, issue)
317 320
318 321 attrs = {
319 322 'tracker_id' => (k = get_keyword(:tracker)) && issue.project.trackers.named(k).first.try(:id),
320 323 'status_id' => (k = get_keyword(:status)) && IssueStatus.named(k).first.try(:id),
321 324 'priority_id' => (k = get_keyword(:priority)) && IssuePriority.named(k).first.try(:id),
322 325 'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.named(k).first.try(:id),
323 326 'assigned_to_id' => assigned_to.try(:id),
324 327 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) &&
325 328 issue.project.shared_versions.named(k).first.try(:id),
326 329 'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
327 330 'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
328 331 'estimated_hours' => get_keyword(:estimated_hours, :override => true),
329 332 'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0')
330 333 }.delete_if {|k, v| v.blank? }
331 334
332 335 if issue.new_record? && attrs['tracker_id'].nil?
333 336 attrs['tracker_id'] = issue.project.trackers.find(:first).try(:id)
334 337 end
335 338
336 339 attrs
337 340 end
338 341
339 342 # Returns a Hash of issue custom field values extracted from keywords in the email body
340 343 def custom_field_values_from_keywords(customized)
341 344 customized.custom_field_values.inject({}) do |h, v|
342 345 if value = get_keyword(v.custom_field.name, :override => true)
343 346 h[v.custom_field.id.to_s] = value
344 347 end
345 348 h
346 349 end
347 350 end
348 351
349 352 # Returns the text/plain part of the email
350 353 # If not found (eg. HTML-only email), returns the body with tags removed
351 354 def plain_text_body
352 355 return @plain_text_body unless @plain_text_body.nil?
353 356
354 357 part = email.text_part || email.html_part || email
355 358 @plain_text_body = Redmine::CodesetUtil.to_utf8(part.body.decoded, part.charset)
356 359
357 360 # strip html tags and remove doctype directive
358 361 @plain_text_body = strip_tags(@plain_text_body.strip)
359 362 @plain_text_body.sub! %r{^<!DOCTYPE .*$}, ''
360 363 @plain_text_body
361 364 end
362 365
363 366 def cleaned_up_text_body
364 367 cleanup_body(plain_text_body)
365 368 end
366 369
367 370 def cleaned_up_subject
368 371 subject = email.subject.to_s
369 372 unless subject.respond_to?(:encoding)
370 373 # try to reencode to utf8 manually with ruby1.8
371 374 begin
372 375 if h = email.header[:subject]
373 376 if m = h.value.match(/^=\?([^\?]+)\?/)
374 377 subject = Redmine::CodesetUtil.to_utf8(subject, m[1])
375 378 end
376 379 end
377 380 rescue
378 381 # nop
379 382 end
380 383 end
381 384 subject.strip[0,255]
382 385 end
383 386
384 387 def self.full_sanitizer
385 388 @full_sanitizer ||= HTML::FullSanitizer.new
386 389 end
387 390
388 391 def self.assign_string_attribute_with_limit(object, attribute, value, limit=nil)
389 392 limit ||= object.class.columns_hash[attribute.to_s].limit || 255
390 393 value = value.to_s.slice(0, limit)
391 394 object.send("#{attribute}=", value)
392 395 end
393 396
394 397 # Returns a User from an email address and a full name
395 398 def self.new_user_from_attributes(email_address, fullname=nil)
396 399 user = User.new
397 400
398 401 # Truncating the email address would result in an invalid format
399 402 user.mail = email_address
400 403 assign_string_attribute_with_limit(user, 'login', email_address, User::LOGIN_LENGTH_LIMIT)
401 404
402 405 names = fullname.blank? ? email_address.gsub(/@.*$/, '').split('.') : fullname.split
403 406 assign_string_attribute_with_limit(user, 'firstname', names.shift)
404 407 assign_string_attribute_with_limit(user, 'lastname', names.join(' '))
405 408 user.lastname = '-' if user.lastname.blank?
406 409
407 410 password_length = [Setting.password_min_length.to_i, 10].max
408 411 user.password = Redmine::Utils.random_hex(password_length / 2 + 1)
409 412 user.language = Setting.default_language
410 413
411 414 unless user.valid?
412 415 user.login = "user#{Redmine::Utils.random_hex(6)}" unless user.errors[:login].blank?
413 416 user.firstname = "-" unless user.errors[:firstname].blank?
414 417 user.lastname = "-" unless user.errors[:lastname].blank?
415 418 end
416 419
417 420 user
418 421 end
419 422
420 423 # Creates a User for the +email+ sender
421 424 # Returns the user or nil if it could not be created
422 425 def create_user_from_email
423 426 from = email.header['from'].to_s
424 427 addr, name = from, nil
425 428 if m = from.match(/^"?(.+?)"?\s+<(.+@.+)>$/)
426 429 addr, name = m[2], m[1]
427 430 end
428 431 if addr.present?
429 432 user = self.class.new_user_from_attributes(addr, name)
430 433 if user.save
431 434 user
432 435 else
433 436 logger.error "MailHandler: failed to create User: #{user.errors.full_messages}" if logger
434 437 nil
435 438 end
436 439 else
437 440 logger.error "MailHandler: failed to create User: no FROM address found" if logger
438 441 nil
439 442 end
440 443 end
441 444
442 445 # Removes the email body of text after the truncation configurations.
443 446 def cleanup_body(body)
444 447 delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
445 448 unless delimiters.empty?
446 449 regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
447 450 body = body.gsub(regex, '')
448 451 end
449 452 body.strip
450 453 end
451 454
452 455 def find_assignee_from_keyword(keyword, issue)
453 456 keyword = keyword.to_s.downcase
454 457 assignable = issue.assignable_users
455 458 assignee = nil
456 459 assignee ||= assignable.detect {|a|
457 460 a.mail.to_s.downcase == keyword ||
458 461 a.login.to_s.downcase == keyword
459 462 }
460 463 if assignee.nil? && keyword.match(/ /)
461 464 firstname, lastname = *(keyword.split) # "First Last Throwaway"
462 465 assignee ||= assignable.detect {|a|
463 466 a.is_a?(User) && a.firstname.to_s.downcase == firstname &&
464 467 a.lastname.to_s.downcase == lastname
465 468 }
466 469 end
467 470 if assignee.nil?
468 471 assignee ||= assignable.detect {|a| a.is_a?(Group) && a.name.downcase == keyword}
469 472 end
470 473 assignee
471 474 end
472 475 end
@@ -1,645 +1,646
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2012 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 test_add_issue
39 39 ActionMailer::Base.deliveries.clear
40 40 # This email contains: 'Project: onlinestore'
41 41 issue = submit_email('ticket_on_given_project.eml')
42 42 assert issue.is_a?(Issue)
43 43 assert !issue.new_record?
44 44 issue.reload
45 45 assert_equal Project.find(2), issue.project
46 46 assert_equal issue.project.trackers.first, issue.tracker
47 47 assert_equal 'New ticket on a given project', issue.subject
48 48 assert_equal User.find_by_login('jsmith'), issue.author
49 49 assert_equal IssueStatus.find_by_name('Resolved'), issue.status
50 50 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
51 51 assert_equal '2010-01-01', issue.start_date.to_s
52 52 assert_equal '2010-12-31', issue.due_date.to_s
53 53 assert_equal User.find_by_login('jsmith'), issue.assigned_to
54 54 assert_equal Version.find_by_name('Alpha'), issue.fixed_version
55 55 assert_equal 2.5, issue.estimated_hours
56 56 assert_equal 30, issue.done_ratio
57 57 assert_equal [issue.id, 1, 2], [issue.root_id, issue.lft, issue.rgt]
58 58 # keywords should be removed from the email body
59 59 assert !issue.description.match(/^Project:/i)
60 60 assert !issue.description.match(/^Status:/i)
61 61 assert !issue.description.match(/^Start Date:/i)
62 62 # Email notification should be sent
63 63 mail = ActionMailer::Base.deliveries.last
64 64 assert_not_nil mail
65 65 assert mail.subject.include?('New ticket on a given project')
66 66 end
67 67
68 68 def test_add_issue_with_default_tracker
69 69 # This email contains: 'Project: onlinestore'
70 70 issue = submit_email(
71 71 'ticket_on_given_project.eml',
72 72 :issue => {:tracker => 'Support request'}
73 73 )
74 74 assert issue.is_a?(Issue)
75 75 assert !issue.new_record?
76 76 issue.reload
77 77 assert_equal 'Support request', issue.tracker.name
78 78 end
79 79
80 80 def test_add_issue_with_status
81 81 # This email contains: 'Project: onlinestore' and 'Status: Resolved'
82 82 issue = submit_email('ticket_on_given_project.eml')
83 83 assert issue.is_a?(Issue)
84 84 assert !issue.new_record?
85 85 issue.reload
86 86 assert_equal Project.find(2), issue.project
87 87 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
88 88 end
89 89
90 90 def test_add_issue_with_attributes_override
91 91 issue = submit_email(
92 92 'ticket_with_attributes.eml',
93 93 :allow_override => 'tracker,category,priority'
94 94 )
95 95 assert issue.is_a?(Issue)
96 96 assert !issue.new_record?
97 97 issue.reload
98 98 assert_equal 'New ticket on a given project', issue.subject
99 99 assert_equal User.find_by_login('jsmith'), issue.author
100 100 assert_equal Project.find(2), issue.project
101 101 assert_equal 'Feature request', issue.tracker.to_s
102 102 assert_equal 'Stock management', issue.category.to_s
103 103 assert_equal 'Urgent', issue.priority.to_s
104 104 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
105 105 end
106 106
107 107 def test_add_issue_with_group_assignment
108 108 with_settings :issue_group_assignment => '1' do
109 109 issue = submit_email('ticket_on_given_project.eml') do |email|
110 110 email.gsub!('Assigned to: John Smith', 'Assigned to: B Team')
111 111 end
112 112 assert issue.is_a?(Issue)
113 113 assert !issue.new_record?
114 114 issue.reload
115 115 assert_equal Group.find(11), issue.assigned_to
116 116 end
117 117 end
118 118
119 119 def test_add_issue_with_partial_attributes_override
120 120 issue = submit_email(
121 121 'ticket_with_attributes.eml',
122 122 :issue => {:priority => 'High'},
123 123 :allow_override => ['tracker']
124 124 )
125 125 assert issue.is_a?(Issue)
126 126 assert !issue.new_record?
127 127 issue.reload
128 128 assert_equal 'New ticket on a given project', issue.subject
129 129 assert_equal User.find_by_login('jsmith'), issue.author
130 130 assert_equal Project.find(2), issue.project
131 131 assert_equal 'Feature request', issue.tracker.to_s
132 132 assert_nil issue.category
133 133 assert_equal 'High', issue.priority.to_s
134 134 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
135 135 end
136 136
137 137 def test_add_issue_with_spaces_between_attribute_and_separator
138 138 issue = submit_email(
139 139 'ticket_with_spaces_between_attribute_and_separator.eml',
140 140 :allow_override => 'tracker,category,priority'
141 141 )
142 142 assert issue.is_a?(Issue)
143 143 assert !issue.new_record?
144 144 issue.reload
145 145 assert_equal 'New ticket on a given project', issue.subject
146 146 assert_equal User.find_by_login('jsmith'), issue.author
147 147 assert_equal Project.find(2), issue.project
148 148 assert_equal 'Feature request', issue.tracker.to_s
149 149 assert_equal 'Stock management', issue.category.to_s
150 150 assert_equal 'Urgent', issue.priority.to_s
151 151 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
152 152 end
153 153
154 154 def test_add_issue_with_attachment_to_specific_project
155 155 issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
156 156 assert issue.is_a?(Issue)
157 157 assert !issue.new_record?
158 158 issue.reload
159 159 assert_equal 'Ticket created by email with attachment', issue.subject
160 160 assert_equal User.find_by_login('jsmith'), issue.author
161 161 assert_equal Project.find(2), issue.project
162 162 assert_equal 'This is a new ticket with attachments', issue.description
163 163 # Attachment properties
164 164 assert_equal 1, issue.attachments.size
165 165 assert_equal 'Paella.jpg', issue.attachments.first.filename
166 166 assert_equal 'image/jpeg', issue.attachments.first.content_type
167 167 assert_equal 10790, issue.attachments.first.filesize
168 168 end
169 169
170 170 def test_add_issue_with_custom_fields
171 171 issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'onlinestore'})
172 172 assert issue.is_a?(Issue)
173 173 assert !issue.new_record?
174 174 issue.reload
175 175 assert_equal 'New ticket with custom field values', issue.subject
176 176 assert_equal 'Value for a custom field',
177 177 issue.custom_value_for(CustomField.find_by_name('Searchable field')).value
178 178 assert !issue.description.match(/^searchable field:/i)
179 179 end
180 180
181 181 def test_add_issue_with_cc
182 182 issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'})
183 183 assert issue.is_a?(Issue)
184 184 assert !issue.new_record?
185 185 issue.reload
186 186 assert issue.watched_by?(User.find_by_mail('dlopper@somenet.foo'))
187 187 assert_equal 1, issue.watcher_user_ids.size
188 188 end
189 189
190 190 def test_add_issue_by_unknown_user
191 191 assert_no_difference 'User.count' do
192 192 assert_equal false,
193 193 submit_email(
194 194 'ticket_by_unknown_user.eml',
195 195 :issue => {:project => 'ecookbook'}
196 196 )
197 197 end
198 198 end
199 199
200 200 def test_add_issue_by_anonymous_user
201 201 Role.anonymous.add_permission!(:add_issues)
202 202 assert_no_difference 'User.count' do
203 203 issue = submit_email(
204 204 'ticket_by_unknown_user.eml',
205 205 :issue => {:project => 'ecookbook'},
206 206 :unknown_user => 'accept'
207 207 )
208 208 assert issue.is_a?(Issue)
209 209 assert issue.author.anonymous?
210 210 end
211 211 end
212 212
213 213 def test_add_issue_by_anonymous_user_with_no_from_address
214 214 Role.anonymous.add_permission!(:add_issues)
215 215 assert_no_difference 'User.count' do
216 216 issue = submit_email(
217 217 'ticket_by_empty_user.eml',
218 218 :issue => {:project => 'ecookbook'},
219 219 :unknown_user => 'accept'
220 220 )
221 221 assert issue.is_a?(Issue)
222 222 assert issue.author.anonymous?
223 223 end
224 224 end
225 225
226 226 def test_add_issue_by_anonymous_user_on_private_project
227 227 Role.anonymous.add_permission!(:add_issues)
228 228 assert_no_difference 'User.count' do
229 229 assert_no_difference 'Issue.count' do
230 230 assert_equal false,
231 231 submit_email(
232 232 'ticket_by_unknown_user.eml',
233 233 :issue => {:project => 'onlinestore'},
234 234 :unknown_user => 'accept'
235 235 )
236 236 end
237 237 end
238 238 end
239 239
240 240 def test_add_issue_by_anonymous_user_on_private_project_without_permission_check
241 241 assert_no_difference 'User.count' do
242 242 assert_difference 'Issue.count' do
243 243 issue = submit_email(
244 244 'ticket_by_unknown_user.eml',
245 245 :issue => {:project => 'onlinestore'},
246 246 :no_permission_check => '1',
247 247 :unknown_user => 'accept'
248 248 )
249 249 assert issue.is_a?(Issue)
250 250 assert issue.author.anonymous?
251 251 assert !issue.project.is_public?
252 252 assert_equal [issue.id, 1, 2], [issue.root_id, issue.lft, issue.rgt]
253 253 end
254 254 end
255 255 end
256 256
257 257 def test_add_issue_by_created_user
258 258 Setting.default_language = 'en'
259 259 assert_difference 'User.count' do
260 260 issue = submit_email(
261 261 'ticket_by_unknown_user.eml',
262 262 :issue => {:project => 'ecookbook'},
263 263 :unknown_user => 'create'
264 264 )
265 265 assert issue.is_a?(Issue)
266 266 assert issue.author.active?
267 267 assert_equal 'john.doe@somenet.foo', issue.author.mail
268 268 assert_equal 'John', issue.author.firstname
269 269 assert_equal 'Doe', issue.author.lastname
270 270
271 271 # account information
272 272 email = ActionMailer::Base.deliveries.first
273 273 assert_not_nil email
274 274 assert email.subject.include?('account activation')
275 275 login = mail_body(email).match(/\* Login: (.*)$/)[1].strip
276 276 password = mail_body(email).match(/\* Password: (.*)$/)[1].strip
277 277 assert_equal issue.author, User.try_to_login(login, password)
278 278 end
279 279 end
280 280
281 281 def test_add_issue_without_from_header
282 282 Role.anonymous.add_permission!(:add_issues)
283 283 assert_equal false, submit_email('ticket_without_from_header.eml')
284 284 end
285 285
286 286 def test_add_issue_with_invalid_attributes
287 287 issue = submit_email(
288 288 'ticket_with_invalid_attributes.eml',
289 289 :allow_override => 'tracker,category,priority'
290 290 )
291 291 assert issue.is_a?(Issue)
292 292 assert !issue.new_record?
293 293 issue.reload
294 294 assert_nil issue.assigned_to
295 295 assert_nil issue.start_date
296 296 assert_nil issue.due_date
297 297 assert_equal 0, issue.done_ratio
298 298 assert_equal 'Normal', issue.priority.to_s
299 299 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
300 300 end
301 301
302 302 def test_add_issue_with_localized_attributes
303 303 User.find_by_mail('jsmith@somenet.foo').update_attribute 'language', 'fr'
304 304 issue = submit_email(
305 305 'ticket_with_localized_attributes.eml',
306 306 :allow_override => 'tracker,category,priority'
307 307 )
308 308 assert issue.is_a?(Issue)
309 309 assert !issue.new_record?
310 310 issue.reload
311 311 assert_equal 'New ticket on a given project', issue.subject
312 312 assert_equal User.find_by_login('jsmith'), issue.author
313 313 assert_equal Project.find(2), issue.project
314 314 assert_equal 'Feature request', issue.tracker.to_s
315 315 assert_equal 'Stock management', issue.category.to_s
316 316 assert_equal 'Urgent', issue.priority.to_s
317 317 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
318 318 end
319 319
320 320 def test_add_issue_with_japanese_keywords
321 321 ja_dev = "\xe9\x96\x8b\xe7\x99\xba"
322 322 ja_dev.force_encoding('UTF-8') if ja_dev.respond_to?(:force_encoding)
323 323 tracker = Tracker.create!(:name => ja_dev)
324 324 Project.find(1).trackers << tracker
325 325 issue = submit_email(
326 326 'japanese_keywords_iso_2022_jp.eml',
327 327 :issue => {:project => 'ecookbook'},
328 328 :allow_override => 'tracker'
329 329 )
330 330 assert_kind_of Issue, issue
331 331 assert_equal tracker, issue.tracker
332 332 end
333 333
334 334 def test_add_issue_from_apple_mail
335 335 issue = submit_email(
336 336 'apple_mail_with_attachment.eml',
337 337 :issue => {:project => 'ecookbook'}
338 338 )
339 339 assert_kind_of Issue, issue
340 340 assert_equal 1, issue.attachments.size
341 341
342 342 attachment = issue.attachments.first
343 343 assert_equal 'paella.jpg', attachment.filename
344 344 assert_equal 10790, attachment.filesize
345 345 assert File.exist?(attachment.diskfile)
346 346 assert_equal 10790, File.size(attachment.diskfile)
347 347 assert_equal 'caaf384198bcbc9563ab5c058acd73cd', attachment.digest
348 348 end
349 349
350 350 def test_add_issue_with_iso_8859_1_subject
351 351 issue = submit_email(
352 352 'subject_as_iso-8859-1.eml',
353 353 :issue => {:project => 'ecookbook'}
354 354 )
355 355 assert_kind_of Issue, issue
356 356 assert_equal 'Testmail from Webmail: Γ€ ΓΆ ΓΌ...', issue.subject
357 357 end
358 358
359 359 def test_should_ignore_emails_from_locked_users
360 360 User.find(2).lock!
361 361
362 362 MailHandler.any_instance.expects(:dispatch).never
363 363 assert_no_difference 'Issue.count' do
364 364 assert_equal false, submit_email('ticket_on_given_project.eml')
365 365 end
366 366 end
367 367
368 368 def test_should_ignore_emails_from_emission_address
369 369 Role.anonymous.add_permission!(:add_issues)
370 370 assert_no_difference 'User.count' do
371 371 assert_equal false,
372 372 submit_email(
373 373 'ticket_from_emission_address.eml',
374 374 :issue => {:project => 'ecookbook'},
375 375 :unknown_user => 'create'
376 376 )
377 377 end
378 378 end
379 379
380 380 def test_should_ignore_auto_replied_emails
381 381 MailHandler.any_instance.expects(:dispatch).never
382 382 [
383 383 "X-Auto-Response-Suppress: OOF",
384 384 "Auto-Submitted: auto-replied",
385 "Auto-Submitted: Auto-Replied"
385 "Auto-Submitted: Auto-Replied",
386 "Auto-Submitted: auto-generated"
386 387 ].each do |header|
387 388 raw = IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml'))
388 389 raw = header + "\n" + raw
389 390
390 391 assert_no_difference 'Issue.count' do
391 392 assert_equal false, MailHandler.receive(raw), "email with #{header} header was not ignored"
392 393 end
393 394 end
394 395 end
395 396
396 397 def test_add_issue_should_send_email_notification
397 398 Setting.notified_events = ['issue_added']
398 399 ActionMailer::Base.deliveries.clear
399 400 # This email contains: 'Project: onlinestore'
400 401 issue = submit_email('ticket_on_given_project.eml')
401 402 assert issue.is_a?(Issue)
402 403 assert_equal 1, ActionMailer::Base.deliveries.size
403 404 end
404 405
405 406 def test_update_issue
406 407 journal = submit_email('ticket_reply.eml')
407 408 assert journal.is_a?(Journal)
408 409 assert_equal User.find_by_login('jsmith'), journal.user
409 410 assert_equal Issue.find(2), journal.journalized
410 411 assert_match /This is reply/, journal.notes
411 412 assert_equal 'Feature request', journal.issue.tracker.name
412 413 end
413 414
414 415 def test_update_issue_with_attribute_changes
415 416 # This email contains: 'Status: Resolved'
416 417 journal = submit_email('ticket_reply_with_status.eml')
417 418 assert journal.is_a?(Journal)
418 419 issue = Issue.find(journal.issue.id)
419 420 assert_equal User.find_by_login('jsmith'), journal.user
420 421 assert_equal Issue.find(2), journal.journalized
421 422 assert_match /This is reply/, journal.notes
422 423 assert_equal 'Feature request', journal.issue.tracker.name
423 424 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
424 425 assert_equal '2010-01-01', issue.start_date.to_s
425 426 assert_equal '2010-12-31', issue.due_date.to_s
426 427 assert_equal User.find_by_login('jsmith'), issue.assigned_to
427 428 assert_equal "52.6", issue.custom_value_for(CustomField.find_by_name('Float field')).value
428 429 # keywords should be removed from the email body
429 430 assert !journal.notes.match(/^Status:/i)
430 431 assert !journal.notes.match(/^Start Date:/i)
431 432 end
432 433
433 434 def test_update_issue_with_attachment
434 435 assert_difference 'Journal.count' do
435 436 assert_difference 'JournalDetail.count' do
436 437 assert_difference 'Attachment.count' do
437 438 assert_no_difference 'Issue.count' do
438 439 journal = submit_email('ticket_with_attachment.eml') do |raw|
439 440 raw.gsub! /^Subject: .*$/, 'Subject: Re: [Cookbook - Feature #2] (New) Add ingredients categories'
440 441 end
441 442 end
442 443 end
443 444 end
444 445 end
445 446 journal = Journal.first(:order => 'id DESC')
446 447 assert_equal Issue.find(2), journal.journalized
447 448 assert_equal 1, journal.details.size
448 449
449 450 detail = journal.details.first
450 451 assert_equal 'attachment', detail.property
451 452 assert_equal 'Paella.jpg', detail.value
452 453 end
453 454
454 455 def test_update_issue_should_send_email_notification
455 456 ActionMailer::Base.deliveries.clear
456 457 journal = submit_email('ticket_reply.eml')
457 458 assert journal.is_a?(Journal)
458 459 assert_equal 1, ActionMailer::Base.deliveries.size
459 460 end
460 461
461 462 def test_update_issue_should_not_set_defaults
462 463 journal = submit_email(
463 464 'ticket_reply.eml',
464 465 :issue => {:tracker => 'Support request', :priority => 'High'}
465 466 )
466 467 assert journal.is_a?(Journal)
467 468 assert_match /This is reply/, journal.notes
468 469 assert_equal 'Feature request', journal.issue.tracker.name
469 470 assert_equal 'Normal', journal.issue.priority.name
470 471 end
471 472
472 473 def test_reply_to_a_message
473 474 m = submit_email('message_reply.eml')
474 475 assert m.is_a?(Message)
475 476 assert !m.new_record?
476 477 m.reload
477 478 assert_equal 'Reply via email', m.subject
478 479 # The email replies to message #2 which is part of the thread of message #1
479 480 assert_equal Message.find(1), m.parent
480 481 end
481 482
482 483 def test_reply_to_a_message_by_subject
483 484 m = submit_email('message_reply_by_subject.eml')
484 485 assert m.is_a?(Message)
485 486 assert !m.new_record?
486 487 m.reload
487 488 assert_equal 'Reply to the first post', m.subject
488 489 assert_equal Message.find(1), m.parent
489 490 end
490 491
491 492 def test_should_strip_tags_of_html_only_emails
492 493 issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'})
493 494 assert issue.is_a?(Issue)
494 495 assert !issue.new_record?
495 496 issue.reload
496 497 assert_equal 'HTML email', issue.subject
497 498 assert_equal 'This is a html-only email.', issue.description
498 499 end
499 500
500 501 context "truncate emails based on the Setting" do
501 502 context "with no setting" do
502 503 setup do
503 504 Setting.mail_handler_body_delimiters = ''
504 505 end
505 506
506 507 should "add the entire email into the issue" do
507 508 issue = submit_email('ticket_on_given_project.eml')
508 509 assert_issue_created(issue)
509 510 assert issue.description.include?('---')
510 511 assert issue.description.include?('This paragraph is after the delimiter')
511 512 end
512 513 end
513 514
514 515 context "with a single string" do
515 516 setup do
516 517 Setting.mail_handler_body_delimiters = '---'
517 518 end
518 519 should "truncate the email at the delimiter for the issue" do
519 520 issue = submit_email('ticket_on_given_project.eml')
520 521 assert_issue_created(issue)
521 522 assert issue.description.include?('This paragraph is before delimiters')
522 523 assert issue.description.include?('--- This line starts with a delimiter')
523 524 assert !issue.description.match(/^---$/)
524 525 assert !issue.description.include?('This paragraph is after the delimiter')
525 526 end
526 527 end
527 528
528 529 context "with a single quoted reply (e.g. reply to a Redmine email notification)" do
529 530 setup do
530 531 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
531 532 end
532 533 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
533 534 journal = submit_email('issue_update_with_quoted_reply_above.eml')
534 535 assert journal.is_a?(Journal)
535 536 assert journal.notes.include?('An update to the issue by the sender.')
536 537 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
537 538 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
538 539 end
539 540 end
540 541
541 542 context "with multiple quoted replies (e.g. reply to a reply of a Redmine email notification)" do
542 543 setup do
543 544 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
544 545 end
545 546 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
546 547 journal = submit_email('issue_update_with_multiple_quoted_reply_above.eml')
547 548 assert journal.is_a?(Journal)
548 549 assert journal.notes.include?('An update to the issue by the sender.')
549 550 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
550 551 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
551 552 end
552 553 end
553 554
554 555 context "with multiple strings" do
555 556 setup do
556 557 Setting.mail_handler_body_delimiters = "---\nBREAK"
557 558 end
558 559 should "truncate the email at the first delimiter found (BREAK)" do
559 560 issue = submit_email('ticket_on_given_project.eml')
560 561 assert_issue_created(issue)
561 562 assert issue.description.include?('This paragraph is before delimiters')
562 563 assert !issue.description.include?('BREAK')
563 564 assert !issue.description.include?('This paragraph is between delimiters')
564 565 assert !issue.description.match(/^---$/)
565 566 assert !issue.description.include?('This paragraph is after the delimiter')
566 567 end
567 568 end
568 569 end
569 570
570 571 def test_email_with_long_subject_line
571 572 issue = submit_email('ticket_with_long_subject.eml')
572 573 assert issue.is_a?(Issue)
573 574 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]
574 575 end
575 576
576 577 def test_new_user_from_attributes_should_return_valid_user
577 578 to_test = {
578 579 # [address, name] => [login, firstname, lastname]
579 580 ['jsmith@example.net', nil] => ['jsmith@example.net', 'jsmith', '-'],
580 581 ['jsmith@example.net', 'John'] => ['jsmith@example.net', 'John', '-'],
581 582 ['jsmith@example.net', 'John Smith'] => ['jsmith@example.net', 'John', 'Smith'],
582 583 ['jsmith@example.net', 'John Paul Smith'] => ['jsmith@example.net', 'John', 'Paul Smith'],
583 584 ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsTheMaximumLength Smith'] => ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsT', 'Smith'],
584 585 ['jsmith@example.net', 'John AVeryLongLastnameThatExceedsTheMaximumLength'] => ['jsmith@example.net', 'John', 'AVeryLongLastnameThatExceedsTh']
585 586 }
586 587
587 588 to_test.each do |attrs, expected|
588 589 user = MailHandler.new_user_from_attributes(attrs.first, attrs.last)
589 590
590 591 assert user.valid?, user.errors.full_messages.to_s
591 592 assert_equal attrs.first, user.mail
592 593 assert_equal expected[0], user.login
593 594 assert_equal expected[1], user.firstname
594 595 assert_equal expected[2], user.lastname
595 596 end
596 597 end
597 598
598 599 def test_new_user_from_attributes_should_respect_minimum_password_length
599 600 with_settings :password_min_length => 15 do
600 601 user = MailHandler.new_user_from_attributes('jsmith@example.net')
601 602 assert user.valid?
602 603 assert user.password.length >= 15
603 604 end
604 605 end
605 606
606 607 def test_new_user_from_attributes_should_use_default_login_if_invalid
607 608 user = MailHandler.new_user_from_attributes('foo+bar@example.net')
608 609 assert user.valid?
609 610 assert user.login =~ /^user[a-f0-9]+$/
610 611 assert_equal 'foo+bar@example.net', user.mail
611 612 end
612 613
613 614 def test_new_user_with_utf8_encoded_fullname_should_be_decoded
614 615 assert_difference 'User.count' do
615 616 issue = submit_email(
616 617 'fullname_of_sender_as_utf8_encoded.eml',
617 618 :issue => {:project => 'ecookbook'},
618 619 :unknown_user => 'create'
619 620 )
620 621 end
621 622
622 623 user = User.first(:order => 'id DESC')
623 624 assert_equal "foo@example.org", user.mail
624 625 str1 = "\xc3\x84\xc3\xa4"
625 626 str2 = "\xc3\x96\xc3\xb6"
626 627 str1.force_encoding('UTF-8') if str1.respond_to?(:force_encoding)
627 628 str2.force_encoding('UTF-8') if str2.respond_to?(:force_encoding)
628 629 assert_equal str1, user.firstname
629 630 assert_equal str2, user.lastname
630 631 end
631 632
632 633 private
633 634
634 635 def submit_email(filename, options={})
635 636 raw = IO.read(File.join(FIXTURES_PATH, filename))
636 637 yield raw if block_given?
637 638 MailHandler.receive(raw, options)
638 639 end
639 640
640 641 def assert_issue_created(issue)
641 642 assert issue.is_a?(Issue)
642 643 assert !issue.new_record?
643 644 issue.reload
644 645 end
645 646 end
General Comments 0
You need to be logged in to leave comments. Login now