##// END OF EJS Templates
Fix handling multiple text parts in email (#13646)....
Jean-Philippe Lang -
r11604:9b7d312a0e66
parent child
Show More
@@ -0,0 +1,55
1 From JSmith@somenet.foo Fri Mar 22 08:30:28 2013
2 From: John Smith <JSmith@somenet.foo>
3 Content-Type: multipart/mixed; boundary="Apple-Mail=_33C8180A-B097-4B87-A925-441300BDB9C9"
4 Message-Id: <BB533668-3CC8-41CA-A951-0A5D8EA37FB0@somenet.foo>
5 Mime-Version: 1.0 (Mac OS X Mail 6.3 \(1503\))
6 Subject: Test with multiple text parts
7 Date: Fri, 22 Mar 2013 17:30:20 +0200
8 To: redmine@somenet.foo
9 X-Mailer: Apple Mail (2.1503)
10
11
12
13 --Apple-Mail=_33C8180A-B097-4B87-A925-441300BDB9C9
14 Content-Transfer-Encoding: quoted-printable
15 Content-Type: text/plain;
16 charset=us-ascii
17
18 The first text part.
19
20 --Apple-Mail=_33C8180A-B097-4B87-A925-441300BDB9C9
21 Content-Disposition: inline;
22 filename=1st.pdf
23 Content-Type: application/pdf;
24 x-unix-mode=0644;
25 name="1st.pdf"
26 Content-Transfer-Encoding: base64
27
28 JVBERi0xLjMKJcTl8uXrp/Og0MTGCjQgMCBvYmoKPDwgL0xlbmd0aCA1IDAgUiAvRmlsdGVyIC9G
29
30 --Apple-Mail=_33C8180A-B097-4B87-A925-441300BDB9C9
31 Content-Transfer-Encoding: quoted-printable
32 Content-Type: text/plain;
33 charset=us-ascii
34
35 The second text part.
36
37 --Apple-Mail=_33C8180A-B097-4B87-A925-441300BDB9C9
38 Content-Disposition: inline;
39 filename=2nd.pdf
40 Content-Type: application/pdf;
41 x-unix-mode=0644;
42 name="2nd.pdf"
43 Content-Transfer-Encoding: base64
44
45 JVBERi0xLjMKJcTl8uXrp/Og0MTGCjQgMCBvYmoKPDwgL0xlbmd0aCA1IDAgUiAvRmlsdGVyIC9G
46
47
48 --Apple-Mail=_33C8180A-B097-4B87-A925-441300BDB9C9
49 Content-Transfer-Encoding: quoted-printable
50 Content-Type: text/plain;
51 charset=us-ascii
52
53 The third one.
54
55 --Apple-Mail=_33C8180A-B097-4B87-A925-441300BDB9C9--
@@ -1,501 +1,510
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 && logger.info
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 && logger.info
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 && logger.info
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 && logger.info
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 && logger.error
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 && logger.info
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+@}
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 && logger.info
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 && logger.info
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 && logger.info
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 obj.attachments << Attachment.create(:container => obj,
271 271 :file => attachment.decoded,
272 272 :filename => attachment.filename,
273 273 :author => user,
274 274 :content_type => attachment.mime_type)
275 275 end
276 276 end
277 277 end
278 278
279 279 # Adds To and Cc as watchers of the given object if the sender has the
280 280 # appropriate permission
281 281 def add_watchers(obj)
282 282 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
283 283 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
284 284 unless addresses.empty?
285 285 watchers = User.active.where('LOWER(mail) IN (?)', addresses).all
286 286 watchers.each {|w| obj.add_watcher(w)}
287 287 end
288 288 end
289 289 end
290 290
291 291 def get_keyword(attr, options={})
292 292 @keywords ||= {}
293 293 if @keywords.has_key?(attr)
294 294 @keywords[attr]
295 295 else
296 296 @keywords[attr] = begin
297 297 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) &&
298 298 (v = extract_keyword!(plain_text_body, attr, options[:format]))
299 299 v
300 300 elsif !@@handler_options[:issue][attr].blank?
301 301 @@handler_options[:issue][attr]
302 302 end
303 303 end
304 304 end
305 305 end
306 306
307 307 # Destructively extracts the value for +attr+ in +text+
308 308 # Returns nil if no matching keyword found
309 309 def extract_keyword!(text, attr, format=nil)
310 310 keys = [attr.to_s.humanize]
311 311 if attr.is_a?(Symbol)
312 312 if user && user.language.present?
313 313 keys << l("field_#{attr}", :default => '', :locale => user.language)
314 314 end
315 315 if Setting.default_language.present?
316 316 keys << l("field_#{attr}", :default => '', :locale => Setting.default_language)
317 317 end
318 318 end
319 319 keys.reject! {|k| k.blank?}
320 320 keys.collect! {|k| Regexp.escape(k)}
321 321 format ||= '.+'
322 322 keyword = nil
323 323 regexp = /^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i
324 324 if m = text.match(regexp)
325 325 keyword = m[2].strip
326 326 text.gsub!(regexp, '')
327 327 end
328 328 keyword
329 329 end
330 330
331 331 def target_project
332 332 # TODO: other ways to specify project:
333 333 # * parse the email To field
334 334 # * specific project (eg. Setting.mail_handler_target_project)
335 335 target = Project.find_by_identifier(get_keyword(:project))
336 336 raise MissingInformation.new('Unable to determine target project') if target.nil?
337 337 target
338 338 end
339 339
340 340 # Returns a Hash of issue attributes extracted from keywords in the email body
341 341 def issue_attributes_from_keywords(issue)
342 342 assigned_to = (k = get_keyword(:assigned_to, :override => true)) && find_assignee_from_keyword(k, issue)
343 343
344 344 attrs = {
345 345 'tracker_id' => (k = get_keyword(:tracker)) && issue.project.trackers.named(k).first.try(:id),
346 346 'status_id' => (k = get_keyword(:status)) && IssueStatus.named(k).first.try(:id),
347 347 'priority_id' => (k = get_keyword(:priority)) && IssuePriority.named(k).first.try(:id),
348 348 'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.named(k).first.try(:id),
349 349 'assigned_to_id' => assigned_to.try(:id),
350 350 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) &&
351 351 issue.project.shared_versions.named(k).first.try(:id),
352 352 'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
353 353 'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
354 354 'estimated_hours' => get_keyword(:estimated_hours, :override => true),
355 355 'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0')
356 356 }.delete_if {|k, v| v.blank? }
357 357
358 358 if issue.new_record? && attrs['tracker_id'].nil?
359 359 attrs['tracker_id'] = issue.project.trackers.first.try(:id)
360 360 end
361 361
362 362 attrs
363 363 end
364 364
365 365 # Returns a Hash of issue custom field values extracted from keywords in the email body
366 366 def custom_field_values_from_keywords(customized)
367 367 customized.custom_field_values.inject({}) do |h, v|
368 368 if keyword = get_keyword(v.custom_field.name, :override => true)
369 369 h[v.custom_field.id.to_s] = v.custom_field.value_from_keyword(keyword, customized)
370 370 end
371 371 h
372 372 end
373 373 end
374 374
375 375 # Returns the text/plain part of the email
376 376 # If not found (eg. HTML-only email), returns the body with tags removed
377 377 def plain_text_body
378 378 return @plain_text_body unless @plain_text_body.nil?
379 379
380 part = email.text_part || email.html_part || email
381 @plain_text_body = Redmine::CodesetUtil.to_utf8(part.body.decoded, part.charset)
380 parts = if (text_parts = email.all_parts.select {|p| p.mime_type == 'text/plain'}).present?
381 text_parts
382 elsif (html_parts = email.all_parts.select {|p| p.mime_type == 'text/html'}).present?
383 html_parts
384 else
385 [email]
386 end
387 @plain_text_body = parts.map {|p| Redmine::CodesetUtil.to_utf8(p.body.decoded, p.charset)}.join("\r\n")
382 388
383 389 # strip html tags and remove doctype directive
390 if parts.any? {|p| p.mime_type == 'text/html'}
384 391 @plain_text_body = strip_tags(@plain_text_body.strip)
385 392 @plain_text_body.sub! %r{^<!DOCTYPE .*$}, ''
393 end
394
386 395 @plain_text_body
387 396 end
388 397
389 398 def cleaned_up_text_body
390 399 cleanup_body(plain_text_body)
391 400 end
392 401
393 402 def cleaned_up_subject
394 403 subject = email.subject.to_s
395 404 subject.strip[0,255]
396 405 end
397 406
398 407 def self.full_sanitizer
399 408 @full_sanitizer ||= HTML::FullSanitizer.new
400 409 end
401 410
402 411 def self.assign_string_attribute_with_limit(object, attribute, value, limit=nil)
403 412 limit ||= object.class.columns_hash[attribute.to_s].limit || 255
404 413 value = value.to_s.slice(0, limit)
405 414 object.send("#{attribute}=", value)
406 415 end
407 416
408 417 # Returns a User from an email address and a full name
409 418 def self.new_user_from_attributes(email_address, fullname=nil)
410 419 user = User.new
411 420
412 421 # Truncating the email address would result in an invalid format
413 422 user.mail = email_address
414 423 assign_string_attribute_with_limit(user, 'login', email_address, User::LOGIN_LENGTH_LIMIT)
415 424
416 425 names = fullname.blank? ? email_address.gsub(/@.*$/, '').split('.') : fullname.split
417 426 assign_string_attribute_with_limit(user, 'firstname', names.shift, 30)
418 427 assign_string_attribute_with_limit(user, 'lastname', names.join(' '), 30)
419 428 user.lastname = '-' if user.lastname.blank?
420 429 user.language = Setting.default_language
421 430 user.generate_password = true
422 431 user.mail_notification = 'only_my_events'
423 432
424 433 unless user.valid?
425 434 user.login = "user#{Redmine::Utils.random_hex(6)}" unless user.errors[:login].blank?
426 435 user.firstname = "-" unless user.errors[:firstname].blank?
427 436 (puts user.errors[:lastname];user.lastname = "-") unless user.errors[:lastname].blank?
428 437 end
429 438
430 439 user
431 440 end
432 441
433 442 # Creates a User for the +email+ sender
434 443 # Returns the user or nil if it could not be created
435 444 def create_user_from_email
436 445 from = email.header['from'].to_s
437 446 addr, name = from, nil
438 447 if m = from.match(/^"?(.+?)"?\s+<(.+@.+)>$/)
439 448 addr, name = m[2], m[1]
440 449 end
441 450 if addr.present?
442 451 user = self.class.new_user_from_attributes(addr, name)
443 452 if @@handler_options[:no_notification]
444 453 user.mail_notification = 'none'
445 454 end
446 455 if user.save
447 456 user
448 457 else
449 458 logger.error "MailHandler: failed to create User: #{user.errors.full_messages}" if logger
450 459 nil
451 460 end
452 461 else
453 462 logger.error "MailHandler: failed to create User: no FROM address found" if logger
454 463 nil
455 464 end
456 465 end
457 466
458 467 # Adds the newly created user to default group
459 468 def add_user_to_group(default_group)
460 469 if default_group.present?
461 470 default_group.split(',').each do |group_name|
462 471 if group = Group.named(group_name).first
463 472 group.users << @user
464 473 elsif logger
465 474 logger.warn "MailHandler: could not add user to [#{group_name}], group not found"
466 475 end
467 476 end
468 477 end
469 478 end
470 479
471 480 # Removes the email body of text after the truncation configurations.
472 481 def cleanup_body(body)
473 482 delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
474 483 unless delimiters.empty?
475 484 regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
476 485 body = body.gsub(regex, '')
477 486 end
478 487 body.strip
479 488 end
480 489
481 490 def find_assignee_from_keyword(keyword, issue)
482 491 keyword = keyword.to_s.downcase
483 492 assignable = issue.assignable_users
484 493 assignee = nil
485 494 assignee ||= assignable.detect {|a|
486 495 a.mail.to_s.downcase == keyword ||
487 496 a.login.to_s.downcase == keyword
488 497 }
489 498 if assignee.nil? && keyword.match(/ /)
490 499 firstname, lastname = *(keyword.split) # "First Last Throwaway"
491 500 assignee ||= assignable.detect {|a|
492 501 a.is_a?(User) && a.firstname.to_s.downcase == firstname &&
493 502 a.lastname.to_s.downcase == lastname
494 503 }
495 504 end
496 505 if assignee.nil?
497 506 assignee ||= assignable.detect {|a| a.name.downcase == keyword}
498 507 end
499 508 assignee
500 509 end
501 510 end
@@ -1,827 +1,834
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_localized_attributes
374 374 User.find_by_mail('jsmith@somenet.foo').update_attribute 'language', 'fr'
375 375 issue = submit_email(
376 376 'ticket_with_localized_attributes.eml',
377 377 :allow_override => 'tracker,category,priority'
378 378 )
379 379 assert issue.is_a?(Issue)
380 380 assert !issue.new_record?
381 381 issue.reload
382 382 assert_equal 'New ticket on a given project', issue.subject
383 383 assert_equal User.find_by_login('jsmith'), issue.author
384 384 assert_equal Project.find(2), issue.project
385 385 assert_equal 'Feature request', issue.tracker.to_s
386 386 assert_equal 'Stock management', issue.category.to_s
387 387 assert_equal 'Urgent', issue.priority.to_s
388 388 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
389 389 end
390 390
391 391 def test_add_issue_with_japanese_keywords
392 392 ja_dev = "\xe9\x96\x8b\xe7\x99\xba"
393 393 ja_dev.force_encoding('UTF-8') if ja_dev.respond_to?(:force_encoding)
394 394 tracker = Tracker.create!(:name => ja_dev)
395 395 Project.find(1).trackers << tracker
396 396 issue = submit_email(
397 397 'japanese_keywords_iso_2022_jp.eml',
398 398 :issue => {:project => 'ecookbook'},
399 399 :allow_override => 'tracker'
400 400 )
401 401 assert_kind_of Issue, issue
402 402 assert_equal tracker, issue.tracker
403 403 end
404 404
405 405 def test_add_issue_from_apple_mail
406 406 issue = submit_email(
407 407 'apple_mail_with_attachment.eml',
408 408 :issue => {:project => 'ecookbook'}
409 409 )
410 410 assert_kind_of Issue, issue
411 411 assert_equal 1, issue.attachments.size
412 412
413 413 attachment = issue.attachments.first
414 414 assert_equal 'paella.jpg', attachment.filename
415 415 assert_equal 10790, attachment.filesize
416 416 assert File.exist?(attachment.diskfile)
417 417 assert_equal 10790, File.size(attachment.diskfile)
418 418 assert_equal 'caaf384198bcbc9563ab5c058acd73cd', attachment.digest
419 419 end
420 420
421 421 def test_thunderbird_with_attachment_ja
422 422 issue = submit_email(
423 423 'thunderbird_with_attachment_ja.eml',
424 424 :issue => {:project => 'ecookbook'}
425 425 )
426 426 assert_kind_of Issue, issue
427 427 assert_equal 1, issue.attachments.size
428 428 ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88.txt"
429 429 ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding)
430 430 attachment = issue.attachments.first
431 431 assert_equal ja, attachment.filename
432 432 assert_equal 5, attachment.filesize
433 433 assert File.exist?(attachment.diskfile)
434 434 assert_equal 5, File.size(attachment.diskfile)
435 435 assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest
436 436 end
437 437
438 438 def test_gmail_with_attachment_ja
439 439 issue = submit_email(
440 440 'gmail_with_attachment_ja.eml',
441 441 :issue => {:project => 'ecookbook'}
442 442 )
443 443 assert_kind_of Issue, issue
444 444 assert_equal 1, issue.attachments.size
445 445 ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88.txt"
446 446 ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding)
447 447 attachment = issue.attachments.first
448 448 assert_equal ja, attachment.filename
449 449 assert_equal 5, attachment.filesize
450 450 assert File.exist?(attachment.diskfile)
451 451 assert_equal 5, File.size(attachment.diskfile)
452 452 assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest
453 453 end
454 454
455 455 def test_thunderbird_with_attachment_latin1
456 456 issue = submit_email(
457 457 'thunderbird_with_attachment_iso-8859-1.eml',
458 458 :issue => {:project => 'ecookbook'}
459 459 )
460 460 assert_kind_of Issue, issue
461 461 assert_equal 1, issue.attachments.size
462 462 u = ""
463 463 u.force_encoding('UTF-8') if u.respond_to?(:force_encoding)
464 464 u1 = "\xc3\x84\xc3\xa4\xc3\x96\xc3\xb6\xc3\x9c\xc3\xbc"
465 465 u1.force_encoding('UTF-8') if u1.respond_to?(:force_encoding)
466 466 11.times { u << u1 }
467 467 attachment = issue.attachments.first
468 468 assert_equal "#{u}.png", attachment.filename
469 469 assert_equal 130, attachment.filesize
470 470 assert File.exist?(attachment.diskfile)
471 471 assert_equal 130, File.size(attachment.diskfile)
472 472 assert_equal '4d80e667ac37dddfe05502530f152abb', attachment.digest
473 473 end
474 474
475 475 def test_gmail_with_attachment_latin1
476 476 issue = submit_email(
477 477 'gmail_with_attachment_iso-8859-1.eml',
478 478 :issue => {:project => 'ecookbook'}
479 479 )
480 480 assert_kind_of Issue, issue
481 481 assert_equal 1, issue.attachments.size
482 482 u = ""
483 483 u.force_encoding('UTF-8') if u.respond_to?(:force_encoding)
484 484 u1 = "\xc3\x84\xc3\xa4\xc3\x96\xc3\xb6\xc3\x9c\xc3\xbc"
485 485 u1.force_encoding('UTF-8') if u1.respond_to?(:force_encoding)
486 486 11.times { u << u1 }
487 487 attachment = issue.attachments.first
488 488 assert_equal "#{u}.txt", attachment.filename
489 489 assert_equal 5, attachment.filesize
490 490 assert File.exist?(attachment.diskfile)
491 491 assert_equal 5, File.size(attachment.diskfile)
492 492 assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest
493 493 end
494 494
495 def test_multiple_text_parts
496 issue = submit_email('multiple_text_parts.eml', :issue => {:project => 'ecookbook'})
497 assert_include 'first', issue.description
498 assert_include 'second', issue.description
499 assert_include 'third', issue.description
500 end
501
495 502 def test_add_issue_with_iso_8859_1_subject
496 503 issue = submit_email(
497 504 'subject_as_iso-8859-1.eml',
498 505 :issue => {:project => 'ecookbook'}
499 506 )
500 507 str = "Testmail from Webmail: \xc3\xa4 \xc3\xb6 \xc3\xbc..."
501 508 str.force_encoding('UTF-8') if str.respond_to?(:force_encoding)
502 509 assert_kind_of Issue, issue
503 510 assert_equal str, issue.subject
504 511 end
505 512
506 513 def test_add_issue_with_japanese_subject
507 514 issue = submit_email(
508 515 'subject_japanese_1.eml',
509 516 :issue => {:project => 'ecookbook'}
510 517 )
511 518 assert_kind_of Issue, issue
512 519 ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88"
513 520 ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding)
514 521 assert_equal ja, issue.subject
515 522 end
516 523
517 524 def test_add_issue_with_no_subject_header
518 525 issue = submit_email(
519 526 'no_subject_header.eml',
520 527 :issue => {:project => 'ecookbook'}
521 528 )
522 529 assert_kind_of Issue, issue
523 530 assert_equal '(no subject)', issue.subject
524 531 end
525 532
526 533 def test_add_issue_with_mixed_japanese_subject
527 534 issue = submit_email(
528 535 'subject_japanese_2.eml',
529 536 :issue => {:project => 'ecookbook'}
530 537 )
531 538 assert_kind_of Issue, issue
532 539 ja = "Re: \xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88"
533 540 ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding)
534 541 assert_equal ja, issue.subject
535 542 end
536 543
537 544 def test_should_ignore_emails_from_locked_users
538 545 User.find(2).lock!
539 546
540 547 MailHandler.any_instance.expects(:dispatch).never
541 548 assert_no_difference 'Issue.count' do
542 549 assert_equal false, submit_email('ticket_on_given_project.eml')
543 550 end
544 551 end
545 552
546 553 def test_should_ignore_emails_from_emission_address
547 554 Role.anonymous.add_permission!(:add_issues)
548 555 assert_no_difference 'User.count' do
549 556 assert_equal false,
550 557 submit_email(
551 558 'ticket_from_emission_address.eml',
552 559 :issue => {:project => 'ecookbook'},
553 560 :unknown_user => 'create'
554 561 )
555 562 end
556 563 end
557 564
558 565 def test_should_ignore_auto_replied_emails
559 566 MailHandler.any_instance.expects(:dispatch).never
560 567 [
561 568 "X-Auto-Response-Suppress: OOF",
562 569 "Auto-Submitted: auto-replied",
563 570 "Auto-Submitted: Auto-Replied",
564 571 "Auto-Submitted: auto-generated"
565 572 ].each do |header|
566 573 raw = IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml'))
567 574 raw = header + "\n" + raw
568 575
569 576 assert_no_difference 'Issue.count' do
570 577 assert_equal false, MailHandler.receive(raw), "email with #{header} header was not ignored"
571 578 end
572 579 end
573 580 end
574 581
575 582 def test_add_issue_should_send_email_notification
576 583 Setting.notified_events = ['issue_added']
577 584 ActionMailer::Base.deliveries.clear
578 585 # This email contains: 'Project: onlinestore'
579 586 issue = submit_email('ticket_on_given_project.eml')
580 587 assert issue.is_a?(Issue)
581 588 assert_equal 1, ActionMailer::Base.deliveries.size
582 589 end
583 590
584 591 def test_update_issue
585 592 journal = submit_email('ticket_reply.eml')
586 593 assert journal.is_a?(Journal)
587 594 assert_equal User.find_by_login('jsmith'), journal.user
588 595 assert_equal Issue.find(2), journal.journalized
589 596 assert_match /This is reply/, journal.notes
590 597 assert_equal false, journal.private_notes
591 598 assert_equal 'Feature request', journal.issue.tracker.name
592 599 end
593 600
594 601 def test_update_issue_with_attribute_changes
595 602 # This email contains: 'Status: Resolved'
596 603 journal = submit_email('ticket_reply_with_status.eml')
597 604 assert journal.is_a?(Journal)
598 605 issue = Issue.find(journal.issue.id)
599 606 assert_equal User.find_by_login('jsmith'), journal.user
600 607 assert_equal Issue.find(2), journal.journalized
601 608 assert_match /This is reply/, journal.notes
602 609 assert_equal 'Feature request', journal.issue.tracker.name
603 610 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
604 611 assert_equal '2010-01-01', issue.start_date.to_s
605 612 assert_equal '2010-12-31', issue.due_date.to_s
606 613 assert_equal User.find_by_login('jsmith'), issue.assigned_to
607 614 assert_equal "52.6", issue.custom_value_for(CustomField.find_by_name('Float field')).value
608 615 # keywords should be removed from the email body
609 616 assert !journal.notes.match(/^Status:/i)
610 617 assert !journal.notes.match(/^Start Date:/i)
611 618 end
612 619
613 620 def test_update_issue_with_attachment
614 621 assert_difference 'Journal.count' do
615 622 assert_difference 'JournalDetail.count' do
616 623 assert_difference 'Attachment.count' do
617 624 assert_no_difference 'Issue.count' do
618 625 journal = submit_email('ticket_with_attachment.eml') do |raw|
619 626 raw.gsub! /^Subject: .*$/, 'Subject: Re: [Cookbook - Feature #2] (New) Add ingredients categories'
620 627 end
621 628 end
622 629 end
623 630 end
624 631 end
625 632 journal = Journal.first(:order => 'id DESC')
626 633 assert_equal Issue.find(2), journal.journalized
627 634 assert_equal 1, journal.details.size
628 635
629 636 detail = journal.details.first
630 637 assert_equal 'attachment', detail.property
631 638 assert_equal 'Paella.jpg', detail.value
632 639 end
633 640
634 641 def test_update_issue_should_send_email_notification
635 642 ActionMailer::Base.deliveries.clear
636 643 journal = submit_email('ticket_reply.eml')
637 644 assert journal.is_a?(Journal)
638 645 assert_equal 1, ActionMailer::Base.deliveries.size
639 646 end
640 647
641 648 def test_update_issue_should_not_set_defaults
642 649 journal = submit_email(
643 650 'ticket_reply.eml',
644 651 :issue => {:tracker => 'Support request', :priority => 'High'}
645 652 )
646 653 assert journal.is_a?(Journal)
647 654 assert_match /This is reply/, journal.notes
648 655 assert_equal 'Feature request', journal.issue.tracker.name
649 656 assert_equal 'Normal', journal.issue.priority.name
650 657 end
651 658
652 659 def test_replying_to_a_private_note_should_add_reply_as_private
653 660 private_journal = Journal.create!(:notes => 'Private notes', :journalized => Issue.find(1), :private_notes => true, :user_id => 2)
654 661
655 662 assert_difference 'Journal.count' do
656 663 journal = submit_email('ticket_reply.eml') do |email|
657 664 email.sub! %r{^In-Reply-To:.*$}, "In-Reply-To: <redmine.journal-#{private_journal.id}.20060719210421@osiris>"
658 665 end
659 666
660 667 assert_kind_of Journal, journal
661 668 assert_match /This is reply/, journal.notes
662 669 assert_equal true, journal.private_notes
663 670 end
664 671 end
665 672
666 673 def test_reply_to_a_message
667 674 m = submit_email('message_reply.eml')
668 675 assert m.is_a?(Message)
669 676 assert !m.new_record?
670 677 m.reload
671 678 assert_equal 'Reply via email', m.subject
672 679 # The email replies to message #2 which is part of the thread of message #1
673 680 assert_equal Message.find(1), m.parent
674 681 end
675 682
676 683 def test_reply_to_a_message_by_subject
677 684 m = submit_email('message_reply_by_subject.eml')
678 685 assert m.is_a?(Message)
679 686 assert !m.new_record?
680 687 m.reload
681 688 assert_equal 'Reply to the first post', m.subject
682 689 assert_equal Message.find(1), m.parent
683 690 end
684 691
685 692 def test_should_strip_tags_of_html_only_emails
686 693 issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'})
687 694 assert issue.is_a?(Issue)
688 695 assert !issue.new_record?
689 696 issue.reload
690 697 assert_equal 'HTML email', issue.subject
691 698 assert_equal 'This is a html-only email.', issue.description
692 699 end
693 700
694 701 test "truncate emails with no setting should add the entire email into the issue" do
695 702 with_settings :mail_handler_body_delimiters => '' do
696 703 issue = submit_email('ticket_on_given_project.eml')
697 704 assert_issue_created(issue)
698 705 assert issue.description.include?('---')
699 706 assert issue.description.include?('This paragraph is after the delimiter')
700 707 end
701 708 end
702 709
703 710 test "truncate emails with a single string should truncate the email at the delimiter for the issue" do
704 711 with_settings :mail_handler_body_delimiters => '---' do
705 712 issue = submit_email('ticket_on_given_project.eml')
706 713 assert_issue_created(issue)
707 714 assert issue.description.include?('This paragraph is before delimiters')
708 715 assert issue.description.include?('--- This line starts with a delimiter')
709 716 assert !issue.description.match(/^---$/)
710 717 assert !issue.description.include?('This paragraph is after the delimiter')
711 718 end
712 719 end
713 720
714 721 test "truncate emails with a single quoted reply should truncate the email at the delimiter with the quoted reply symbols (>)" do
715 722 with_settings :mail_handler_body_delimiters => '--- Reply above. Do not remove this line. ---' do
716 723 journal = submit_email('issue_update_with_quoted_reply_above.eml')
717 724 assert journal.is_a?(Journal)
718 725 assert journal.notes.include?('An update to the issue by the sender.')
719 726 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
720 727 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
721 728 end
722 729 end
723 730
724 731 test "truncate emails with multiple quoted replies should truncate the email at the delimiter with the quoted reply symbols (>)" do
725 732 with_settings :mail_handler_body_delimiters => '--- Reply above. Do not remove this line. ---' do
726 733 journal = submit_email('issue_update_with_multiple_quoted_reply_above.eml')
727 734 assert journal.is_a?(Journal)
728 735 assert journal.notes.include?('An update to the issue by the sender.')
729 736 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
730 737 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
731 738 end
732 739 end
733 740
734 741 test "truncate emails with multiple strings should truncate the email at the first delimiter found (BREAK)" do
735 742 with_settings :mail_handler_body_delimiters => "---\nBREAK" do
736 743 issue = submit_email('ticket_on_given_project.eml')
737 744 assert_issue_created(issue)
738 745 assert issue.description.include?('This paragraph is before delimiters')
739 746 assert !issue.description.include?('BREAK')
740 747 assert !issue.description.include?('This paragraph is between delimiters')
741 748 assert !issue.description.match(/^---$/)
742 749 assert !issue.description.include?('This paragraph is after the delimiter')
743 750 end
744 751 end
745 752
746 753 def test_email_with_long_subject_line
747 754 issue = submit_email('ticket_with_long_subject.eml')
748 755 assert issue.is_a?(Issue)
749 756 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]
750 757 end
751 758
752 759 def test_new_user_from_attributes_should_return_valid_user
753 760 to_test = {
754 761 # [address, name] => [login, firstname, lastname]
755 762 ['jsmith@example.net', nil] => ['jsmith@example.net', 'jsmith', '-'],
756 763 ['jsmith@example.net', 'John'] => ['jsmith@example.net', 'John', '-'],
757 764 ['jsmith@example.net', 'John Smith'] => ['jsmith@example.net', 'John', 'Smith'],
758 765 ['jsmith@example.net', 'John Paul Smith'] => ['jsmith@example.net', 'John', 'Paul Smith'],
759 766 ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsTheMaximumLength Smith'] => ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsT', 'Smith'],
760 767 ['jsmith@example.net', 'John AVeryLongLastnameThatExceedsTheMaximumLength'] => ['jsmith@example.net', 'John', 'AVeryLongLastnameThatExceedsTh']
761 768 }
762 769
763 770 to_test.each do |attrs, expected|
764 771 user = MailHandler.new_user_from_attributes(attrs.first, attrs.last)
765 772
766 773 assert user.valid?, user.errors.full_messages.to_s
767 774 assert_equal attrs.first, user.mail
768 775 assert_equal expected[0], user.login
769 776 assert_equal expected[1], user.firstname
770 777 assert_equal expected[2], user.lastname
771 778 assert_equal 'only_my_events', user.mail_notification
772 779 end
773 780 end
774 781
775 782 def test_new_user_from_attributes_should_use_default_login_if_invalid
776 783 user = MailHandler.new_user_from_attributes('foo+bar@example.net')
777 784 assert user.valid?
778 785 assert user.login =~ /^user[a-f0-9]+$/
779 786 assert_equal 'foo+bar@example.net', user.mail
780 787 end
781 788
782 789 def test_new_user_with_utf8_encoded_fullname_should_be_decoded
783 790 assert_difference 'User.count' do
784 791 issue = submit_email(
785 792 'fullname_of_sender_as_utf8_encoded.eml',
786 793 :issue => {:project => 'ecookbook'},
787 794 :unknown_user => 'create'
788 795 )
789 796 end
790 797
791 798 user = User.first(:order => 'id DESC')
792 799 assert_equal "foo@example.org", user.mail
793 800 str1 = "\xc3\x84\xc3\xa4"
794 801 str2 = "\xc3\x96\xc3\xb6"
795 802 str1.force_encoding('UTF-8') if str1.respond_to?(:force_encoding)
796 803 str2.force_encoding('UTF-8') if str2.respond_to?(:force_encoding)
797 804 assert_equal str1, user.firstname
798 805 assert_equal str2, user.lastname
799 806 end
800 807
801 808 def test_extract_options_from_env_should_return_options
802 809 options = MailHandler.extract_options_from_env({
803 810 'tracker' => 'defect',
804 811 'project' => 'foo',
805 812 'unknown_user' => 'create'
806 813 })
807 814
808 815 assert_equal({
809 816 :issue => {:tracker => 'defect', :project => 'foo'},
810 817 :unknown_user => 'create'
811 818 }, options)
812 819 end
813 820
814 821 private
815 822
816 823 def submit_email(filename, options={})
817 824 raw = IO.read(File.join(FIXTURES_PATH, filename))
818 825 yield raw if block_given?
819 826 MailHandler.receive(raw, options)
820 827 end
821 828
822 829 def assert_issue_created(issue)
823 830 assert issue.is_a?(Issue)
824 831 assert !issue.new_record?
825 832 issue.reload
826 833 end
827 834 end
General Comments 0
You need to be logged in to leave comments. Login now