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