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