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