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