##// END OF EJS Templates
Makes the mail handler ignore out-of-office emails (#10607)....
Jean-Philippe Lang -
r9224:dd1163b58a30
parent child
Show More
@@ -1,430 +1,437
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 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_permission_check] = (@@handler_options[:no_permission_check].to_s == '1' ? true : false)
42 42 super email
43 43 end
44 44
45 45 # Processes incoming emails
46 46 # Returns the created object (eg. an issue, a message) or false
47 47 def receive(email)
48 48 @email = email
49 49 sender_email = email.from.to_a.first.to_s.strip
50 50 # Ignore emails received from the application emission address to avoid hell cycles
51 51 if sender_email.downcase == Setting.mail_from.to_s.strip.downcase
52 52 if logger && logger.info
53 53 logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]"
54 54 end
55 55 return false
56 56 end
57 # Ignore out-of-office emails
58 if email.header_string("X-Auto-Response-Suppress") == 'OOF'
59 if logger && logger.info
60 logger.info "MailHandler: ignoring out-of-office email"
61 end
62 return false
63 end
57 64 @user = User.find_by_mail(sender_email) if sender_email.present?
58 65 if @user && !@user.active?
59 66 if logger && logger.info
60 67 logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]"
61 68 end
62 69 return false
63 70 end
64 71 if @user.nil?
65 72 # Email was submitted by an unknown user
66 73 case @@handler_options[:unknown_user]
67 74 when 'accept'
68 75 @user = User.anonymous
69 76 when 'create'
70 77 @user = create_user_from_email
71 78 if @user
72 79 if logger && logger.info
73 80 logger.info "MailHandler: [#{@user.login}] account created"
74 81 end
75 82 Mailer.deliver_account_information(@user, @user.password)
76 83 else
77 84 if logger && logger.error
78 85 logger.error "MailHandler: could not create account for [#{sender_email}]"
79 86 end
80 87 return false
81 88 end
82 89 else
83 90 # Default behaviour, emails from unknown users are ignored
84 91 if logger && logger.info
85 92 logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]"
86 93 end
87 94 return false
88 95 end
89 96 end
90 97 User.current = @user
91 98 dispatch
92 99 end
93 100
94 101 private
95 102
96 103 MESSAGE_ID_RE = %r{^<redmine\.([a-z0-9_]+)\-(\d+)\.\d+@}
97 104 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]*#(\d+)\]}
98 105 MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
99 106
100 107 def dispatch
101 108 headers = [email.in_reply_to, email.references].flatten.compact
102 109 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
103 110 klass, object_id = $1, $2.to_i
104 111 method_name = "receive_#{klass}_reply"
105 112 if self.class.private_instance_methods.collect(&:to_s).include?(method_name)
106 113 send method_name, object_id
107 114 else
108 115 # ignoring it
109 116 end
110 117 elsif m = email.subject.match(ISSUE_REPLY_SUBJECT_RE)
111 118 receive_issue_reply(m[1].to_i)
112 119 elsif m = email.subject.match(MESSAGE_REPLY_SUBJECT_RE)
113 120 receive_message_reply(m[1].to_i)
114 121 else
115 122 dispatch_to_default
116 123 end
117 124 rescue ActiveRecord::RecordInvalid => e
118 125 # TODO: send a email to the user
119 126 logger.error e.message if logger
120 127 false
121 128 rescue MissingInformation => e
122 129 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
123 130 false
124 131 rescue UnauthorizedAction => e
125 132 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
126 133 false
127 134 end
128 135
129 136 def dispatch_to_default
130 137 receive_issue
131 138 end
132 139
133 140 # Creates a new issue
134 141 def receive_issue
135 142 project = target_project
136 143 # check permission
137 144 unless @@handler_options[:no_permission_check]
138 145 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
139 146 end
140 147
141 148 issue = Issue.new(:author => user, :project => project)
142 149 issue.safe_attributes = issue_attributes_from_keywords(issue)
143 150 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
144 151 issue.subject = email.subject.to_s.chomp[0,255]
145 152 if issue.subject.blank?
146 153 issue.subject = '(no subject)'
147 154 end
148 155 issue.description = cleaned_up_text_body
149 156
150 157 # add To and Cc as watchers before saving so the watchers can reply to Redmine
151 158 add_watchers(issue)
152 159 issue.save!
153 160 add_attachments(issue)
154 161 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
155 162 issue
156 163 end
157 164
158 165 # Adds a note to an existing issue
159 166 def receive_issue_reply(issue_id)
160 167 issue = Issue.find_by_id(issue_id)
161 168 return unless issue
162 169 # check permission
163 170 unless @@handler_options[:no_permission_check]
164 171 unless user.allowed_to?(:add_issue_notes, issue.project) ||
165 172 user.allowed_to?(:edit_issues, issue.project)
166 173 raise UnauthorizedAction
167 174 end
168 175 end
169 176
170 177 # ignore CLI-supplied defaults for new issues
171 178 @@handler_options[:issue].clear
172 179
173 180 journal = issue.init_journal(user)
174 181 issue.safe_attributes = issue_attributes_from_keywords(issue)
175 182 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
176 183 journal.notes = cleaned_up_text_body
177 184 add_attachments(issue)
178 185 issue.save!
179 186 if logger && logger.info
180 187 logger.info "MailHandler: issue ##{issue.id} updated by #{user}"
181 188 end
182 189 journal
183 190 end
184 191
185 192 # Reply will be added to the issue
186 193 def receive_journal_reply(journal_id)
187 194 journal = Journal.find_by_id(journal_id)
188 195 if journal && journal.journalized_type == 'Issue'
189 196 receive_issue_reply(journal.journalized_id)
190 197 end
191 198 end
192 199
193 200 # Receives a reply to a forum message
194 201 def receive_message_reply(message_id)
195 202 message = Message.find_by_id(message_id)
196 203 if message
197 204 message = message.root
198 205
199 206 unless @@handler_options[:no_permission_check]
200 207 raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
201 208 end
202 209
203 210 if !message.locked?
204 211 reply = Message.new(:subject => email.subject.gsub(%r{^.*msg\d+\]}, '').strip,
205 212 :content => cleaned_up_text_body)
206 213 reply.author = user
207 214 reply.board = message.board
208 215 message.children << reply
209 216 add_attachments(reply)
210 217 reply
211 218 else
212 219 if logger && logger.info
213 220 logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic"
214 221 end
215 222 end
216 223 end
217 224 end
218 225
219 226 def add_attachments(obj)
220 227 if email.attachments && email.attachments.any?
221 228 email.attachments.each do |attachment|
222 229 obj.attachments << Attachment.create(:container => obj,
223 230 :file => attachment,
224 231 :author => user,
225 232 :content_type => attachment.content_type)
226 233 end
227 234 end
228 235 end
229 236
230 237 # Adds To and Cc as watchers of the given object if the sender has the
231 238 # appropriate permission
232 239 def add_watchers(obj)
233 240 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
234 241 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
235 242 unless addresses.empty?
236 243 watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
237 244 watchers.each {|w| obj.add_watcher(w)}
238 245 end
239 246 end
240 247 end
241 248
242 249 def get_keyword(attr, options={})
243 250 @keywords ||= {}
244 251 if @keywords.has_key?(attr)
245 252 @keywords[attr]
246 253 else
247 254 @keywords[attr] = begin
248 255 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) &&
249 256 (v = extract_keyword!(plain_text_body, attr, options[:format]))
250 257 v
251 258 elsif !@@handler_options[:issue][attr].blank?
252 259 @@handler_options[:issue][attr]
253 260 end
254 261 end
255 262 end
256 263 end
257 264
258 265 # Destructively extracts the value for +attr+ in +text+
259 266 # Returns nil if no matching keyword found
260 267 def extract_keyword!(text, attr, format=nil)
261 268 keys = [attr.to_s.humanize]
262 269 if attr.is_a?(Symbol)
263 270 if user && user.language.present?
264 271 keys << l("field_#{attr}", :default => '', :locale => user.language)
265 272 end
266 273 if Setting.default_language.present?
267 274 keys << l("field_#{attr}", :default => '', :locale => Setting.default_language)
268 275 end
269 276 end
270 277 keys.reject! {|k| k.blank?}
271 278 keys.collect! {|k| Regexp.escape(k)}
272 279 format ||= '.+'
273 280 text.gsub!(/^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i, '')
274 281 $2 && $2.strip
275 282 end
276 283
277 284 def target_project
278 285 # TODO: other ways to specify project:
279 286 # * parse the email To field
280 287 # * specific project (eg. Setting.mail_handler_target_project)
281 288 target = Project.find_by_identifier(get_keyword(:project))
282 289 raise MissingInformation.new('Unable to determine target project') if target.nil?
283 290 target
284 291 end
285 292
286 293 # Returns a Hash of issue attributes extracted from keywords in the email body
287 294 def issue_attributes_from_keywords(issue)
288 295 assigned_to = (k = get_keyword(:assigned_to, :override => true)) && find_assignee_from_keyword(k, issue)
289 296
290 297 attrs = {
291 298 'tracker_id' => (k = get_keyword(:tracker)) && issue.project.trackers.named(k).first.try(:id),
292 299 'status_id' => (k = get_keyword(:status)) && IssueStatus.named(k).first.try(:id),
293 300 'priority_id' => (k = get_keyword(:priority)) && IssuePriority.named(k).first.try(:id),
294 301 'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.named(k).first.try(:id),
295 302 'assigned_to_id' => assigned_to.try(:id),
296 303 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) &&
297 304 issue.project.shared_versions.named(k).first.try(:id),
298 305 'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
299 306 'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
300 307 'estimated_hours' => get_keyword(:estimated_hours, :override => true),
301 308 'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0')
302 309 }.delete_if {|k, v| v.blank? }
303 310
304 311 if issue.new_record? && attrs['tracker_id'].nil?
305 312 attrs['tracker_id'] = issue.project.trackers.find(:first).try(:id)
306 313 end
307 314
308 315 attrs
309 316 end
310 317
311 318 # Returns a Hash of issue custom field values extracted from keywords in the email body
312 319 def custom_field_values_from_keywords(customized)
313 320 customized.custom_field_values.inject({}) do |h, v|
314 321 if value = get_keyword(v.custom_field.name, :override => true)
315 322 h[v.custom_field.id.to_s] = value
316 323 end
317 324 h
318 325 end
319 326 end
320 327
321 328 # Returns the text/plain part of the email
322 329 # If not found (eg. HTML-only email), returns the body with tags removed
323 330 def plain_text_body
324 331 return @plain_text_body unless @plain_text_body.nil?
325 332 parts = @email.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten
326 333 if parts.empty?
327 334 parts << @email
328 335 end
329 336 plain_text_part = parts.detect {|p| p.content_type == 'text/plain'}
330 337 if plain_text_part.nil?
331 338 # no text/plain part found, assuming html-only email
332 339 # strip html tags and remove doctype directive
333 340 @plain_text_body = strip_tags(@email.body.to_s)
334 341 @plain_text_body.gsub! %r{^<!DOCTYPE .*$}, ''
335 342 else
336 343 @plain_text_body = plain_text_part.body.to_s
337 344 end
338 345 @plain_text_body.strip!
339 346 @plain_text_body
340 347 end
341 348
342 349 def cleaned_up_text_body
343 350 cleanup_body(plain_text_body)
344 351 end
345 352
346 353 def self.full_sanitizer
347 354 @full_sanitizer ||= HTML::FullSanitizer.new
348 355 end
349 356
350 357 def self.assign_string_attribute_with_limit(object, attribute, value, limit=nil)
351 358 limit ||= object.class.columns_hash[attribute.to_s].limit || 255
352 359 value = value.to_s.slice(0, limit)
353 360 object.send("#{attribute}=", value)
354 361 end
355 362
356 363 # Returns a User from an email address and a full name
357 364 def self.new_user_from_attributes(email_address, fullname=nil)
358 365 user = User.new
359 366
360 367 # Truncating the email address would result in an invalid format
361 368 user.mail = email_address
362 369 assign_string_attribute_with_limit(user, 'login', email_address, User::LOGIN_LENGTH_LIMIT)
363 370
364 371 names = fullname.blank? ? email_address.gsub(/@.*$/, '').split('.') : fullname.split
365 372 assign_string_attribute_with_limit(user, 'firstname', names.shift)
366 373 assign_string_attribute_with_limit(user, 'lastname', names.join(' '))
367 374 user.lastname = '-' if user.lastname.blank?
368 375
369 376 password_length = [Setting.password_min_length.to_i, 10].max
370 377 user.password = Redmine::Utils.random_hex(password_length / 2 + 1)
371 378 user.language = Setting.default_language
372 379
373 380 unless user.valid?
374 381 user.login = "user#{Redmine::Utils.random_hex(6)}" unless user.errors[:login].blank?
375 382 user.firstname = "-" unless user.errors[:firstname].blank?
376 383 user.lastname = "-" unless user.errors[:lastname].blank?
377 384 end
378 385
379 386 user
380 387 end
381 388
382 389 # Creates a User for the +email+ sender
383 390 # Returns the user or nil if it could not be created
384 391 def create_user_from_email
385 392 addr = email.from_addrs.to_a.first
386 393 if addr && !addr.spec.blank?
387 394 user = self.class.new_user_from_attributes(addr.spec, TMail::Unquoter.unquote_and_convert_to(addr.name, 'utf-8'))
388 395 if user.save
389 396 user
390 397 else
391 398 logger.error "MailHandler: failed to create User: #{user.errors.full_messages}" if logger
392 399 nil
393 400 end
394 401 else
395 402 logger.error "MailHandler: failed to create User: no FROM address found" if logger
396 403 nil
397 404 end
398 405 end
399 406
400 407 # Removes the email body of text after the truncation configurations.
401 408 def cleanup_body(body)
402 409 delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
403 410 unless delimiters.empty?
404 411 regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
405 412 body = body.gsub(regex, '')
406 413 end
407 414 body.strip
408 415 end
409 416
410 417 def find_assignee_from_keyword(keyword, issue)
411 418 keyword = keyword.to_s.downcase
412 419 assignable = issue.assignable_users
413 420 assignee = nil
414 421 assignee ||= assignable.detect {|a|
415 422 a.mail.to_s.downcase == keyword ||
416 423 a.login.to_s.downcase == keyword
417 424 }
418 425 if assignee.nil? && keyword.match(/ /)
419 426 firstname, lastname = *(keyword.split) # "First Last Throwaway"
420 427 assignee ||= assignable.detect {|a|
421 428 a.is_a?(User) && a.firstname.to_s.downcase == firstname &&
422 429 a.lastname.to_s.downcase == lastname
423 430 }
424 431 end
425 432 if assignee.nil?
426 433 assignee ||= assignable.detect {|a| a.is_a?(Group) && a.name.downcase == keyword}
427 434 end
428 435 assignee
429 436 end
430 437 end
@@ -1,611 +1,620
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2011 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 test_add_issue
39 39 ActionMailer::Base.deliveries.clear
40 40 # This email contains: 'Project: onlinestore'
41 41 issue = submit_email('ticket_on_given_project.eml')
42 42 assert issue.is_a?(Issue)
43 43 assert !issue.new_record?
44 44 issue.reload
45 45 assert_equal Project.find(2), issue.project
46 46 assert_equal issue.project.trackers.first, issue.tracker
47 47 assert_equal 'New ticket on a given project', issue.subject
48 48 assert_equal User.find_by_login('jsmith'), issue.author
49 49 assert_equal IssueStatus.find_by_name('Resolved'), issue.status
50 50 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
51 51 assert_equal '2010-01-01', issue.start_date.to_s
52 52 assert_equal '2010-12-31', issue.due_date.to_s
53 53 assert_equal User.find_by_login('jsmith'), issue.assigned_to
54 54 assert_equal Version.find_by_name('Alpha'), issue.fixed_version
55 55 assert_equal 2.5, issue.estimated_hours
56 56 assert_equal 30, issue.done_ratio
57 57 assert_equal [issue.id, 1, 2], [issue.root_id, issue.lft, issue.rgt]
58 58 # keywords should be removed from the email body
59 59 assert !issue.description.match(/^Project:/i)
60 60 assert !issue.description.match(/^Status:/i)
61 61 assert !issue.description.match(/^Start Date:/i)
62 62 # Email notification should be sent
63 63 mail = ActionMailer::Base.deliveries.last
64 64 assert_not_nil mail
65 65 assert mail.subject.include?('New ticket on a given project')
66 66 end
67 67
68 68 def test_add_issue_with_default_tracker
69 69 # This email contains: 'Project: onlinestore'
70 70 issue = submit_email(
71 71 'ticket_on_given_project.eml',
72 72 :issue => {:tracker => 'Support request'}
73 73 )
74 74 assert issue.is_a?(Issue)
75 75 assert !issue.new_record?
76 76 issue.reload
77 77 assert_equal 'Support request', issue.tracker.name
78 78 end
79 79
80 80 def test_add_issue_with_status
81 81 # This email contains: 'Project: onlinestore' and 'Status: Resolved'
82 82 issue = submit_email('ticket_on_given_project.eml')
83 83 assert issue.is_a?(Issue)
84 84 assert !issue.new_record?
85 85 issue.reload
86 86 assert_equal Project.find(2), issue.project
87 87 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
88 88 end
89 89
90 90 def test_add_issue_with_attributes_override
91 91 issue = submit_email(
92 92 'ticket_with_attributes.eml',
93 93 :allow_override => 'tracker,category,priority'
94 94 )
95 95 assert issue.is_a?(Issue)
96 96 assert !issue.new_record?
97 97 issue.reload
98 98 assert_equal 'New ticket on a given project', issue.subject
99 99 assert_equal User.find_by_login('jsmith'), issue.author
100 100 assert_equal Project.find(2), issue.project
101 101 assert_equal 'Feature request', issue.tracker.to_s
102 102 assert_equal 'Stock management', issue.category.to_s
103 103 assert_equal 'Urgent', issue.priority.to_s
104 104 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
105 105 end
106 106
107 107 def test_add_issue_with_group_assignment
108 108 with_settings :issue_group_assignment => '1' do
109 109 issue = submit_email('ticket_on_given_project.eml') do |email|
110 110 email.gsub!('Assigned to: John Smith', 'Assigned to: B Team')
111 111 end
112 112 assert issue.is_a?(Issue)
113 113 assert !issue.new_record?
114 114 issue.reload
115 115 assert_equal Group.find(11), issue.assigned_to
116 116 end
117 117 end
118 118
119 119 def test_add_issue_with_partial_attributes_override
120 120 issue = submit_email(
121 121 'ticket_with_attributes.eml',
122 122 :issue => {:priority => 'High'},
123 123 :allow_override => ['tracker']
124 124 )
125 125 assert issue.is_a?(Issue)
126 126 assert !issue.new_record?
127 127 issue.reload
128 128 assert_equal 'New ticket on a given project', issue.subject
129 129 assert_equal User.find_by_login('jsmith'), issue.author
130 130 assert_equal Project.find(2), issue.project
131 131 assert_equal 'Feature request', issue.tracker.to_s
132 132 assert_nil issue.category
133 133 assert_equal 'High', issue.priority.to_s
134 134 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
135 135 end
136 136
137 137 def test_add_issue_with_spaces_between_attribute_and_separator
138 138 issue = submit_email(
139 139 'ticket_with_spaces_between_attribute_and_separator.eml',
140 140 :allow_override => 'tracker,category,priority'
141 141 )
142 142 assert issue.is_a?(Issue)
143 143 assert !issue.new_record?
144 144 issue.reload
145 145 assert_equal 'New ticket on a given project', issue.subject
146 146 assert_equal User.find_by_login('jsmith'), issue.author
147 147 assert_equal Project.find(2), issue.project
148 148 assert_equal 'Feature request', issue.tracker.to_s
149 149 assert_equal 'Stock management', issue.category.to_s
150 150 assert_equal 'Urgent', issue.priority.to_s
151 151 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
152 152 end
153 153
154 154 def test_add_issue_with_attachment_to_specific_project
155 155 issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
156 156 assert issue.is_a?(Issue)
157 157 assert !issue.new_record?
158 158 issue.reload
159 159 assert_equal 'Ticket created by email with attachment', issue.subject
160 160 assert_equal User.find_by_login('jsmith'), issue.author
161 161 assert_equal Project.find(2), issue.project
162 162 assert_equal 'This is a new ticket with attachments', issue.description
163 163 # Attachment properties
164 164 assert_equal 1, issue.attachments.size
165 165 assert_equal 'Paella.jpg', issue.attachments.first.filename
166 166 assert_equal 'image/jpeg', issue.attachments.first.content_type
167 167 assert_equal 10790, issue.attachments.first.filesize
168 168 end
169 169
170 170 def test_add_issue_with_custom_fields
171 171 issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'onlinestore'})
172 172 assert issue.is_a?(Issue)
173 173 assert !issue.new_record?
174 174 issue.reload
175 175 assert_equal 'New ticket with custom field values', issue.subject
176 176 assert_equal 'Value for a custom field',
177 177 issue.custom_value_for(CustomField.find_by_name('Searchable field')).value
178 178 assert !issue.description.match(/^searchable field:/i)
179 179 end
180 180
181 181 def test_add_issue_with_cc
182 182 issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'})
183 183 assert issue.is_a?(Issue)
184 184 assert !issue.new_record?
185 185 issue.reload
186 186 assert issue.watched_by?(User.find_by_mail('dlopper@somenet.foo'))
187 187 assert_equal 1, issue.watcher_user_ids.size
188 188 end
189 189
190 190 def test_add_issue_by_unknown_user
191 191 assert_no_difference 'User.count' do
192 192 assert_equal false,
193 193 submit_email(
194 194 'ticket_by_unknown_user.eml',
195 195 :issue => {:project => 'ecookbook'}
196 196 )
197 197 end
198 198 end
199 199
200 200 def test_add_issue_by_anonymous_user
201 201 Role.anonymous.add_permission!(:add_issues)
202 202 assert_no_difference 'User.count' do
203 203 issue = submit_email(
204 204 'ticket_by_unknown_user.eml',
205 205 :issue => {:project => 'ecookbook'},
206 206 :unknown_user => 'accept'
207 207 )
208 208 assert issue.is_a?(Issue)
209 209 assert issue.author.anonymous?
210 210 end
211 211 end
212 212
213 213 def test_add_issue_by_anonymous_user_with_no_from_address
214 214 Role.anonymous.add_permission!(:add_issues)
215 215 assert_no_difference 'User.count' do
216 216 issue = submit_email(
217 217 'ticket_by_empty_user.eml',
218 218 :issue => {:project => 'ecookbook'},
219 219 :unknown_user => 'accept'
220 220 )
221 221 assert issue.is_a?(Issue)
222 222 assert issue.author.anonymous?
223 223 end
224 224 end
225 225
226 226 def test_add_issue_by_anonymous_user_on_private_project
227 227 Role.anonymous.add_permission!(:add_issues)
228 228 assert_no_difference 'User.count' do
229 229 assert_no_difference 'Issue.count' do
230 230 assert_equal false,
231 231 submit_email(
232 232 'ticket_by_unknown_user.eml',
233 233 :issue => {:project => 'onlinestore'},
234 234 :unknown_user => 'accept'
235 235 )
236 236 end
237 237 end
238 238 end
239 239
240 240 def test_add_issue_by_anonymous_user_on_private_project_without_permission_check
241 241 assert_no_difference 'User.count' do
242 242 assert_difference 'Issue.count' do
243 243 issue = submit_email(
244 244 'ticket_by_unknown_user.eml',
245 245 :issue => {:project => 'onlinestore'},
246 246 :no_permission_check => '1',
247 247 :unknown_user => 'accept'
248 248 )
249 249 assert issue.is_a?(Issue)
250 250 assert issue.author.anonymous?
251 251 assert !issue.project.is_public?
252 252 assert_equal [issue.id, 1, 2], [issue.root_id, issue.lft, issue.rgt]
253 253 end
254 254 end
255 255 end
256 256
257 257 def test_add_issue_by_created_user
258 258 Setting.default_language = 'en'
259 259 assert_difference 'User.count' do
260 260 issue = submit_email(
261 261 'ticket_by_unknown_user.eml',
262 262 :issue => {:project => 'ecookbook'},
263 263 :unknown_user => 'create'
264 264 )
265 265 assert issue.is_a?(Issue)
266 266 assert issue.author.active?
267 267 assert_equal 'john.doe@somenet.foo', issue.author.mail
268 268 assert_equal 'John', issue.author.firstname
269 269 assert_equal 'Doe', issue.author.lastname
270 270
271 271 # account information
272 272 email = ActionMailer::Base.deliveries.first
273 273 assert_not_nil email
274 274 assert email.subject.include?('account activation')
275 275 login = mail_body(email).match(/\* Login: (.*)$/)[1].strip
276 276 password = mail_body(email).match(/\* Password: (.*)$/)[1].strip
277 277 assert_equal issue.author, User.try_to_login(login, password)
278 278 end
279 279 end
280 280
281 281 def test_add_issue_without_from_header
282 282 Role.anonymous.add_permission!(:add_issues)
283 283 assert_equal false, submit_email('ticket_without_from_header.eml')
284 284 end
285 285
286 286 def test_add_issue_with_invalid_attributes
287 287 issue = submit_email(
288 288 'ticket_with_invalid_attributes.eml',
289 289 :allow_override => 'tracker,category,priority'
290 290 )
291 291 assert issue.is_a?(Issue)
292 292 assert !issue.new_record?
293 293 issue.reload
294 294 assert_nil issue.assigned_to
295 295 assert_nil issue.start_date
296 296 assert_nil issue.due_date
297 297 assert_equal 0, issue.done_ratio
298 298 assert_equal 'Normal', issue.priority.to_s
299 299 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
300 300 end
301 301
302 302 def test_add_issue_with_localized_attributes
303 303 User.find_by_mail('jsmith@somenet.foo').update_attribute 'language', 'fr'
304 304 issue = submit_email(
305 305 'ticket_with_localized_attributes.eml',
306 306 :allow_override => 'tracker,category,priority'
307 307 )
308 308 assert issue.is_a?(Issue)
309 309 assert !issue.new_record?
310 310 issue.reload
311 311 assert_equal 'New ticket on a given project', issue.subject
312 312 assert_equal User.find_by_login('jsmith'), issue.author
313 313 assert_equal Project.find(2), issue.project
314 314 assert_equal 'Feature request', issue.tracker.to_s
315 315 assert_equal 'Stock management', issue.category.to_s
316 316 assert_equal 'Urgent', issue.priority.to_s
317 317 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
318 318 end
319 319
320 320 def test_add_issue_with_japanese_keywords
321 321 ja_dev = "\xe9\x96\x8b\xe7\x99\xba"
322 322 ja_dev.force_encoding('UTF-8') if ja_dev.respond_to?(:force_encoding)
323 323 tracker = Tracker.create!(:name => ja_dev)
324 324 Project.find(1).trackers << tracker
325 325 issue = submit_email(
326 326 'japanese_keywords_iso_2022_jp.eml',
327 327 :issue => {:project => 'ecookbook'},
328 328 :allow_override => 'tracker'
329 329 )
330 330 assert_kind_of Issue, issue
331 331 assert_equal tracker, issue.tracker
332 332 end
333 333
334 334 def test_add_issue_from_apple_mail
335 335 issue = submit_email(
336 336 'apple_mail_with_attachment.eml',
337 337 :issue => {:project => 'ecookbook'}
338 338 )
339 339 assert_kind_of Issue, issue
340 340 assert_equal 1, issue.attachments.size
341 341
342 342 attachment = issue.attachments.first
343 343 assert_equal 'paella.jpg', attachment.filename
344 344 assert_equal 10790, attachment.filesize
345 345 assert File.exist?(attachment.diskfile)
346 346 assert_equal 10790, File.size(attachment.diskfile)
347 347 assert_equal 'caaf384198bcbc9563ab5c058acd73cd', attachment.digest
348 348 end
349 349
350 350 def test_should_ignore_emails_from_emission_address
351 351 Role.anonymous.add_permission!(:add_issues)
352 352 assert_no_difference 'User.count' do
353 353 assert_equal false,
354 354 submit_email(
355 355 'ticket_from_emission_address.eml',
356 356 :issue => {:project => 'ecookbook'},
357 357 :unknown_user => 'create'
358 358 )
359 359 end
360 360 end
361 361
362 def test_should_ignore_oof_emails
363 raw = IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml'))
364 raw = "X-Auto-Response-Suppress: OOF\n" + raw
365
366 assert_no_difference 'Issue.count' do
367 assert_equal false, MailHandler.receive(raw)
368 end
369 end
370
362 371 def test_add_issue_should_send_email_notification
363 372 Setting.notified_events = ['issue_added']
364 373 ActionMailer::Base.deliveries.clear
365 374 # This email contains: 'Project: onlinestore'
366 375 issue = submit_email('ticket_on_given_project.eml')
367 376 assert issue.is_a?(Issue)
368 377 assert_equal 1, ActionMailer::Base.deliveries.size
369 378 end
370 379
371 380 def test_update_issue
372 381 journal = submit_email('ticket_reply.eml')
373 382 assert journal.is_a?(Journal)
374 383 assert_equal User.find_by_login('jsmith'), journal.user
375 384 assert_equal Issue.find(2), journal.journalized
376 385 assert_match /This is reply/, journal.notes
377 386 assert_equal 'Feature request', journal.issue.tracker.name
378 387 end
379 388
380 389 def test_update_issue_with_attribute_changes
381 390 # This email contains: 'Status: Resolved'
382 391 journal = submit_email('ticket_reply_with_status.eml')
383 392 assert journal.is_a?(Journal)
384 393 issue = Issue.find(journal.issue.id)
385 394 assert_equal User.find_by_login('jsmith'), journal.user
386 395 assert_equal Issue.find(2), journal.journalized
387 396 assert_match /This is reply/, journal.notes
388 397 assert_equal 'Feature request', journal.issue.tracker.name
389 398 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
390 399 assert_equal '2010-01-01', issue.start_date.to_s
391 400 assert_equal '2010-12-31', issue.due_date.to_s
392 401 assert_equal User.find_by_login('jsmith'), issue.assigned_to
393 402 assert_equal "52.6", issue.custom_value_for(CustomField.find_by_name('Float field')).value
394 403 # keywords should be removed from the email body
395 404 assert !journal.notes.match(/^Status:/i)
396 405 assert !journal.notes.match(/^Start Date:/i)
397 406 end
398 407
399 408 def test_update_issue_with_attachment
400 409 assert_difference 'Journal.count' do
401 410 assert_difference 'JournalDetail.count' do
402 411 assert_difference 'Attachment.count' do
403 412 assert_no_difference 'Issue.count' do
404 413 journal = submit_email('ticket_with_attachment.eml') do |raw|
405 414 raw.gsub! /^Subject: .*$/, 'Subject: Re: [Cookbook - Feature #2] (New) Add ingredients categories'
406 415 end
407 416 end
408 417 end
409 418 end
410 419 end
411 420 journal = Journal.first(:order => 'id DESC')
412 421 assert_equal Issue.find(2), journal.journalized
413 422 assert_equal 1, journal.details.size
414 423
415 424 detail = journal.details.first
416 425 assert_equal 'attachment', detail.property
417 426 assert_equal 'Paella.jpg', detail.value
418 427 end
419 428
420 429 def test_update_issue_should_send_email_notification
421 430 ActionMailer::Base.deliveries.clear
422 431 journal = submit_email('ticket_reply.eml')
423 432 assert journal.is_a?(Journal)
424 433 assert_equal 1, ActionMailer::Base.deliveries.size
425 434 end
426 435
427 436 def test_update_issue_should_not_set_defaults
428 437 journal = submit_email(
429 438 'ticket_reply.eml',
430 439 :issue => {:tracker => 'Support request', :priority => 'High'}
431 440 )
432 441 assert journal.is_a?(Journal)
433 442 assert_match /This is reply/, journal.notes
434 443 assert_equal 'Feature request', journal.issue.tracker.name
435 444 assert_equal 'Normal', journal.issue.priority.name
436 445 end
437 446
438 447 def test_reply_to_a_message
439 448 m = submit_email('message_reply.eml')
440 449 assert m.is_a?(Message)
441 450 assert !m.new_record?
442 451 m.reload
443 452 assert_equal 'Reply via email', m.subject
444 453 # The email replies to message #2 which is part of the thread of message #1
445 454 assert_equal Message.find(1), m.parent
446 455 end
447 456
448 457 def test_reply_to_a_message_by_subject
449 458 m = submit_email('message_reply_by_subject.eml')
450 459 assert m.is_a?(Message)
451 460 assert !m.new_record?
452 461 m.reload
453 462 assert_equal 'Reply to the first post', m.subject
454 463 assert_equal Message.find(1), m.parent
455 464 end
456 465
457 466 def test_should_strip_tags_of_html_only_emails
458 467 issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'})
459 468 assert issue.is_a?(Issue)
460 469 assert !issue.new_record?
461 470 issue.reload
462 471 assert_equal 'HTML email', issue.subject
463 472 assert_equal 'This is a html-only email.', issue.description
464 473 end
465 474
466 475 context "truncate emails based on the Setting" do
467 476 context "with no setting" do
468 477 setup do
469 478 Setting.mail_handler_body_delimiters = ''
470 479 end
471 480
472 481 should "add the entire email into the issue" do
473 482 issue = submit_email('ticket_on_given_project.eml')
474 483 assert_issue_created(issue)
475 484 assert issue.description.include?('---')
476 485 assert issue.description.include?('This paragraph is after the delimiter')
477 486 end
478 487 end
479 488
480 489 context "with a single string" do
481 490 setup do
482 491 Setting.mail_handler_body_delimiters = '---'
483 492 end
484 493 should "truncate the email at the delimiter for the issue" do
485 494 issue = submit_email('ticket_on_given_project.eml')
486 495 assert_issue_created(issue)
487 496 assert issue.description.include?('This paragraph is before delimiters')
488 497 assert issue.description.include?('--- This line starts with a delimiter')
489 498 assert !issue.description.match(/^---$/)
490 499 assert !issue.description.include?('This paragraph is after the delimiter')
491 500 end
492 501 end
493 502
494 503 context "with a single quoted reply (e.g. reply to a Redmine email notification)" do
495 504 setup do
496 505 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
497 506 end
498 507 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
499 508 journal = submit_email('issue_update_with_quoted_reply_above.eml')
500 509 assert journal.is_a?(Journal)
501 510 assert journal.notes.include?('An update to the issue by the sender.')
502 511 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
503 512 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
504 513 end
505 514 end
506 515
507 516 context "with multiple quoted replies (e.g. reply to a reply of a Redmine email notification)" do
508 517 setup do
509 518 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
510 519 end
511 520 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
512 521 journal = submit_email('issue_update_with_multiple_quoted_reply_above.eml')
513 522 assert journal.is_a?(Journal)
514 523 assert journal.notes.include?('An update to the issue by the sender.')
515 524 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
516 525 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
517 526 end
518 527 end
519 528
520 529 context "with multiple strings" do
521 530 setup do
522 531 Setting.mail_handler_body_delimiters = "---\nBREAK"
523 532 end
524 533 should "truncate the email at the first delimiter found (BREAK)" do
525 534 issue = submit_email('ticket_on_given_project.eml')
526 535 assert_issue_created(issue)
527 536 assert issue.description.include?('This paragraph is before delimiters')
528 537 assert !issue.description.include?('BREAK')
529 538 assert !issue.description.include?('This paragraph is between delimiters')
530 539 assert !issue.description.match(/^---$/)
531 540 assert !issue.description.include?('This paragraph is after the delimiter')
532 541 end
533 542 end
534 543 end
535 544
536 545 def test_email_with_long_subject_line
537 546 issue = submit_email('ticket_with_long_subject.eml')
538 547 assert issue.is_a?(Issue)
539 548 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]
540 549 end
541 550
542 551 def test_new_user_from_attributes_should_return_valid_user
543 552 to_test = {
544 553 # [address, name] => [login, firstname, lastname]
545 554 ['jsmith@example.net', nil] => ['jsmith@example.net', 'jsmith', '-'],
546 555 ['jsmith@example.net', 'John'] => ['jsmith@example.net', 'John', '-'],
547 556 ['jsmith@example.net', 'John Smith'] => ['jsmith@example.net', 'John', 'Smith'],
548 557 ['jsmith@example.net', 'John Paul Smith'] => ['jsmith@example.net', 'John', 'Paul Smith'],
549 558 ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsTheMaximumLength Smith'] => ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsT', 'Smith'],
550 559 ['jsmith@example.net', 'John AVeryLongLastnameThatExceedsTheMaximumLength'] => ['jsmith@example.net', 'John', 'AVeryLongLastnameThatExceedsTh']
551 560 }
552 561
553 562 to_test.each do |attrs, expected|
554 563 user = MailHandler.new_user_from_attributes(attrs.first, attrs.last)
555 564
556 565 assert user.valid?, user.errors.full_messages.to_s
557 566 assert_equal attrs.first, user.mail
558 567 assert_equal expected[0], user.login
559 568 assert_equal expected[1], user.firstname
560 569 assert_equal expected[2], user.lastname
561 570 end
562 571 end
563 572
564 573 def test_new_user_from_attributes_should_respect_minimum_password_length
565 574 with_settings :password_min_length => 15 do
566 575 user = MailHandler.new_user_from_attributes('jsmith@example.net')
567 576 assert user.valid?
568 577 assert user.password.length >= 15
569 578 end
570 579 end
571 580
572 581 def test_new_user_from_attributes_should_use_default_login_if_invalid
573 582 user = MailHandler.new_user_from_attributes('foo+bar@example.net')
574 583 assert user.valid?
575 584 assert user.login =~ /^user[a-f0-9]+$/
576 585 assert_equal 'foo+bar@example.net', user.mail
577 586 end
578 587
579 588 def test_new_user_with_utf8_encoded_fullname_should_be_decoded
580 589 assert_difference 'User.count' do
581 590 issue = submit_email(
582 591 'fullname_of_sender_as_utf8_encoded.eml',
583 592 :issue => {:project => 'ecookbook'},
584 593 :unknown_user => 'create'
585 594 )
586 595 end
587 596
588 597 user = User.first(:order => 'id DESC')
589 598 assert_equal "foo@example.org", user.mail
590 599 str1 = "\xc3\x84\xc3\xa4"
591 600 str2 = "\xc3\x96\xc3\xb6"
592 601 str1.force_encoding('UTF-8') if str1.respond_to?(:force_encoding)
593 602 str2.force_encoding('UTF-8') if str2.respond_to?(:force_encoding)
594 603 assert_equal str1, user.firstname
595 604 assert_equal str2, user.lastname
596 605 end
597 606
598 607 private
599 608
600 609 def submit_email(filename, options={})
601 610 raw = IO.read(File.join(FIXTURES_PATH, filename))
602 611 yield raw if block_given?
603 612 MailHandler.receive(raw, options)
604 613 end
605 614
606 615 def assert_issue_created(issue)
607 616 assert issue.is_a?(Issue)
608 617 assert !issue.new_record?
609 618 issue.reload
610 619 end
611 620 end
General Comments 0
You need to be logged in to leave comments. Login now