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