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