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