##// END OF EJS Templates
Fixed: CLI-supplied defaults should not be applied when replying to an issue (#7195)....
Jean-Philippe Lang -
r4462:03d4ecbbff99
parent child
Show More
@@ -1,358 +1,361
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 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 64 @user = MailHandler.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 receive_issue
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 # Creates a new issue
118 118 def receive_issue
119 119 project = target_project
120 120 # check permission
121 121 unless @@handler_options[:no_permission_check]
122 122 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
123 123 end
124 124
125 125 issue = Issue.new(:author => user, :project => project)
126 126 issue.safe_attributes = issue_attributes_from_keywords(issue)
127 127 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
128 128 issue.subject = email.subject.to_s.chomp[0,255]
129 129 if issue.subject.blank?
130 130 issue.subject = '(no subject)'
131 131 end
132 132 issue.description = cleaned_up_text_body
133 133
134 134 # add To and Cc as watchers before saving so the watchers can reply to Redmine
135 135 add_watchers(issue)
136 136 issue.save!
137 137 add_attachments(issue)
138 138 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
139 139 issue
140 140 end
141 141
142 142 # Adds a note to an existing issue
143 143 def receive_issue_reply(issue_id)
144 144 issue = Issue.find_by_id(issue_id)
145 145 return unless issue
146 146 # check permission
147 147 unless @@handler_options[:no_permission_check]
148 148 raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project)
149 149 end
150 150
151 # ignore CLI-supplied defaults for new issues
152 @@handler_options[:issue].clear
153
151 154 journal = issue.init_journal(user, cleaned_up_text_body)
152 155 issue.safe_attributes = issue_attributes_from_keywords(issue)
153 156 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
154 157 add_attachments(issue)
155 158 issue.save!
156 159 logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info
157 160 journal
158 161 end
159 162
160 163 # Reply will be added to the issue
161 164 def receive_journal_reply(journal_id)
162 165 journal = Journal.find_by_id(journal_id)
163 166 if journal && journal.journalized_type == 'Issue'
164 167 receive_issue_reply(journal.journalized_id)
165 168 end
166 169 end
167 170
168 171 # Receives a reply to a forum message
169 172 def receive_message_reply(message_id)
170 173 message = Message.find_by_id(message_id)
171 174 if message
172 175 message = message.root
173 176
174 177 unless @@handler_options[:no_permission_check]
175 178 raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
176 179 end
177 180
178 181 if !message.locked?
179 182 reply = Message.new(:subject => email.subject.gsub(%r{^.*msg\d+\]}, '').strip,
180 183 :content => cleaned_up_text_body)
181 184 reply.author = user
182 185 reply.board = message.board
183 186 message.children << reply
184 187 add_attachments(reply)
185 188 reply
186 189 else
187 190 logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic" if logger && logger.info
188 191 end
189 192 end
190 193 end
191 194
192 195 def add_attachments(obj)
193 196 if email.has_attachments?
194 197 email.attachments.each do |attachment|
195 198 Attachment.create(:container => obj,
196 199 :file => attachment,
197 200 :author => user,
198 201 :content_type => attachment.content_type)
199 202 end
200 203 end
201 204 end
202 205
203 206 # Adds To and Cc as watchers of the given object if the sender has the
204 207 # appropriate permission
205 208 def add_watchers(obj)
206 209 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
207 210 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
208 211 unless addresses.empty?
209 212 watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
210 213 watchers.each {|w| obj.add_watcher(w)}
211 214 end
212 215 end
213 216 end
214 217
215 218 def get_keyword(attr, options={})
216 219 @keywords ||= {}
217 220 if @keywords.has_key?(attr)
218 221 @keywords[attr]
219 222 else
220 223 @keywords[attr] = begin
221 224 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && (v = extract_keyword!(plain_text_body, attr, options[:format]))
222 225 v
223 226 elsif !@@handler_options[:issue][attr].blank?
224 227 @@handler_options[:issue][attr]
225 228 end
226 229 end
227 230 end
228 231 end
229 232
230 233 # Destructively extracts the value for +attr+ in +text+
231 234 # Returns nil if no matching keyword found
232 235 def extract_keyword!(text, attr, format=nil)
233 236 keys = [attr.to_s.humanize]
234 237 if attr.is_a?(Symbol)
235 238 keys << l("field_#{attr}", :default => '', :locale => user.language) if user
236 239 keys << l("field_#{attr}", :default => '', :locale => Setting.default_language)
237 240 end
238 241 keys.reject! {|k| k.blank?}
239 242 keys.collect! {|k| Regexp.escape(k)}
240 243 format ||= '.+'
241 244 text.gsub!(/^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i, '')
242 245 $2 && $2.strip
243 246 end
244 247
245 248 def target_project
246 249 # TODO: other ways to specify project:
247 250 # * parse the email To field
248 251 # * specific project (eg. Setting.mail_handler_target_project)
249 252 target = Project.find_by_identifier(get_keyword(:project))
250 253 raise MissingInformation.new('Unable to determine target project') if target.nil?
251 254 target
252 255 end
253 256
254 257 # Returns a Hash of issue attributes extracted from keywords in the email body
255 258 def issue_attributes_from_keywords(issue)
256 259 assigned_to = (k = get_keyword(:assigned_to, :override => true)) && find_user_from_keyword(k)
257 260 assigned_to = nil if assigned_to && !issue.assignable_users.include?(assigned_to)
258 261
259 262 attrs = {
260 263 'tracker_id' => (k = get_keyword(:tracker)) && issue.project.trackers.find_by_name(k).try(:id),
261 264 'status_id' => (k = get_keyword(:status)) && IssueStatus.find_by_name(k).try(:id),
262 265 'priority_id' => (k = get_keyword(:priority)) && IssuePriority.find_by_name(k).try(:id),
263 266 'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.find_by_name(k).try(:id),
264 267 'assigned_to_id' => assigned_to.try(:id),
265 268 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) && issue.project.shared_versions.find_by_name(k).try(:id),
266 269 'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
267 270 'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
268 271 'estimated_hours' => get_keyword(:estimated_hours, :override => true),
269 272 'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0')
270 273 }.delete_if {|k, v| v.blank? }
271 274
272 275 if issue.new_record? && attrs['tracker_id'].nil?
273 276 attrs['tracker_id'] = issue.project.trackers.find(:first).try(:id)
274 277 end
275 278
276 279 attrs
277 280 end
278 281
279 282 # Returns a Hash of issue custom field values extracted from keywords in the email body
280 283 def custom_field_values_from_keywords(customized)
281 284 customized.custom_field_values.inject({}) do |h, v|
282 285 if value = get_keyword(v.custom_field.name, :override => true)
283 286 h[v.custom_field.id.to_s] = value
284 287 end
285 288 h
286 289 end
287 290 end
288 291
289 292 # Returns the text/plain part of the email
290 293 # If not found (eg. HTML-only email), returns the body with tags removed
291 294 def plain_text_body
292 295 return @plain_text_body unless @plain_text_body.nil?
293 296 parts = @email.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten
294 297 if parts.empty?
295 298 parts << @email
296 299 end
297 300 plain_text_part = parts.detect {|p| p.content_type == 'text/plain'}
298 301 if plain_text_part.nil?
299 302 # no text/plain part found, assuming html-only email
300 303 # strip html tags and remove doctype directive
301 304 @plain_text_body = strip_tags(@email.body.to_s)
302 305 @plain_text_body.gsub! %r{^<!DOCTYPE .*$}, ''
303 306 else
304 307 @plain_text_body = plain_text_part.body.to_s
305 308 end
306 309 @plain_text_body.strip!
307 310 @plain_text_body
308 311 end
309 312
310 313 def cleaned_up_text_body
311 314 cleanup_body(plain_text_body)
312 315 end
313 316
314 317 def self.full_sanitizer
315 318 @full_sanitizer ||= HTML::FullSanitizer.new
316 319 end
317 320
318 321 # Creates a user account for the +email+ sender
319 322 def self.create_user_from_email(email)
320 323 addr = email.from_addrs.to_a.first
321 324 if addr && !addr.spec.blank?
322 325 user = User.new
323 326 user.mail = addr.spec
324 327
325 328 names = addr.name.blank? ? addr.spec.gsub(/@.*$/, '').split('.') : addr.name.split
326 329 user.firstname = names.shift
327 330 user.lastname = names.join(' ')
328 331 user.lastname = '-' if user.lastname.blank?
329 332
330 333 user.login = user.mail
331 334 user.password = ActiveSupport::SecureRandom.hex(5)
332 335 user.language = Setting.default_language
333 336 user.save ? user : nil
334 337 end
335 338 end
336 339
337 340 private
338 341
339 342 # Removes the email body of text after the truncation configurations.
340 343 def cleanup_body(body)
341 344 delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
342 345 unless delimiters.empty?
343 346 regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
344 347 body = body.gsub(regex, '')
345 348 end
346 349 body.strip
347 350 end
348 351
349 352 def find_user_from_keyword(keyword)
350 353 user ||= User.find_by_mail(keyword)
351 354 user ||= User.find_by_login(keyword)
352 355 if user.nil? && keyword.match(/ /)
353 356 firstname, lastname = *(keyword.split) # "First Last Throwaway"
354 357 user ||= User.find_by_firstname_and_lastname(firstname, lastname)
355 358 end
356 359 user
357 360 end
358 361 end
@@ -1,454 +1,462
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2009 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 # Email notification should be sent
74 74 mail = ActionMailer::Base.deliveries.last
75 75 assert_not_nil mail
76 76 assert mail.subject.include?('New ticket on a given project')
77 77 end
78 78
79 79 def test_add_issue_with_default_tracker
80 80 # This email contains: 'Project: onlinestore'
81 81 issue = submit_email('ticket_on_given_project.eml', :issue => {:tracker => 'Support request'})
82 82 assert issue.is_a?(Issue)
83 83 assert !issue.new_record?
84 84 issue.reload
85 85 assert_equal 'Support request', issue.tracker.name
86 86 end
87 87
88 88 def test_add_issue_with_status
89 89 # This email contains: 'Project: onlinestore' and 'Status: Resolved'
90 90 issue = submit_email('ticket_on_given_project.eml')
91 91 assert issue.is_a?(Issue)
92 92 assert !issue.new_record?
93 93 issue.reload
94 94 assert_equal Project.find(2), issue.project
95 95 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
96 96 end
97 97
98 98 def test_add_issue_with_attributes_override
99 99 issue = submit_email('ticket_with_attributes.eml', :allow_override => 'tracker,category,priority')
100 100 assert issue.is_a?(Issue)
101 101 assert !issue.new_record?
102 102 issue.reload
103 103 assert_equal 'New ticket on a given project', issue.subject
104 104 assert_equal User.find_by_login('jsmith'), issue.author
105 105 assert_equal Project.find(2), issue.project
106 106 assert_equal 'Feature request', issue.tracker.to_s
107 107 assert_equal 'Stock management', issue.category.to_s
108 108 assert_equal 'Urgent', issue.priority.to_s
109 109 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
110 110 end
111 111
112 112 def test_add_issue_with_partial_attributes_override
113 113 issue = submit_email('ticket_with_attributes.eml', :issue => {:priority => 'High'}, :allow_override => ['tracker'])
114 114 assert issue.is_a?(Issue)
115 115 assert !issue.new_record?
116 116 issue.reload
117 117 assert_equal 'New ticket on a given project', issue.subject
118 118 assert_equal User.find_by_login('jsmith'), issue.author
119 119 assert_equal Project.find(2), issue.project
120 120 assert_equal 'Feature request', issue.tracker.to_s
121 121 assert_nil issue.category
122 122 assert_equal 'High', issue.priority.to_s
123 123 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
124 124 end
125 125
126 126 def test_add_issue_with_spaces_between_attribute_and_separator
127 127 issue = submit_email('ticket_with_spaces_between_attribute_and_separator.eml', :allow_override => 'tracker,category,priority')
128 128 assert issue.is_a?(Issue)
129 129 assert !issue.new_record?
130 130 issue.reload
131 131 assert_equal 'New ticket on a given project', issue.subject
132 132 assert_equal User.find_by_login('jsmith'), issue.author
133 133 assert_equal Project.find(2), issue.project
134 134 assert_equal 'Feature request', issue.tracker.to_s
135 135 assert_equal 'Stock management', issue.category.to_s
136 136 assert_equal 'Urgent', issue.priority.to_s
137 137 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
138 138 end
139 139
140 140
141 141 def test_add_issue_with_attachment_to_specific_project
142 142 issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
143 143 assert issue.is_a?(Issue)
144 144 assert !issue.new_record?
145 145 issue.reload
146 146 assert_equal 'Ticket created by email with attachment', issue.subject
147 147 assert_equal User.find_by_login('jsmith'), issue.author
148 148 assert_equal Project.find(2), issue.project
149 149 assert_equal 'This is a new ticket with attachments', issue.description
150 150 # Attachment properties
151 151 assert_equal 1, issue.attachments.size
152 152 assert_equal 'Paella.jpg', issue.attachments.first.filename
153 153 assert_equal 'image/jpeg', issue.attachments.first.content_type
154 154 assert_equal 10790, issue.attachments.first.filesize
155 155 end
156 156
157 157 def test_add_issue_with_custom_fields
158 158 issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'onlinestore'})
159 159 assert issue.is_a?(Issue)
160 160 assert !issue.new_record?
161 161 issue.reload
162 162 assert_equal 'New ticket with custom field values', issue.subject
163 163 assert_equal 'Value for a custom field', issue.custom_value_for(CustomField.find_by_name('Searchable field')).value
164 164 assert !issue.description.match(/^searchable field:/i)
165 165 end
166 166
167 167 def test_add_issue_with_cc
168 168 issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'})
169 169 assert issue.is_a?(Issue)
170 170 assert !issue.new_record?
171 171 issue.reload
172 172 assert issue.watched_by?(User.find_by_mail('dlopper@somenet.foo'))
173 173 assert_equal 1, issue.watcher_user_ids.size
174 174 end
175 175
176 176 def test_add_issue_by_unknown_user
177 177 assert_no_difference 'User.count' do
178 178 assert_equal false, submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'})
179 179 end
180 180 end
181 181
182 182 def test_add_issue_by_anonymous_user
183 183 Role.anonymous.add_permission!(:add_issues)
184 184 assert_no_difference 'User.count' do
185 185 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'accept')
186 186 assert issue.is_a?(Issue)
187 187 assert issue.author.anonymous?
188 188 end
189 189 end
190 190
191 191 def test_add_issue_by_anonymous_user_with_no_from_address
192 192 Role.anonymous.add_permission!(:add_issues)
193 193 assert_no_difference 'User.count' do
194 194 issue = submit_email('ticket_by_empty_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'accept')
195 195 assert issue.is_a?(Issue)
196 196 assert issue.author.anonymous?
197 197 end
198 198 end
199 199
200 200 def test_add_issue_by_anonymous_user_on_private_project
201 201 Role.anonymous.add_permission!(:add_issues)
202 202 assert_no_difference 'User.count' do
203 203 assert_no_difference 'Issue.count' do
204 204 assert_equal false, submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'onlinestore'}, :unknown_user => 'accept')
205 205 end
206 206 end
207 207 end
208 208
209 209 def test_add_issue_by_anonymous_user_on_private_project_without_permission_check
210 210 assert_no_difference 'User.count' do
211 211 assert_difference 'Issue.count' do
212 212 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'onlinestore'}, :no_permission_check => '1', :unknown_user => 'accept')
213 213 assert issue.is_a?(Issue)
214 214 assert issue.author.anonymous?
215 215 assert !issue.project.is_public?
216 216 assert_equal [issue.id, 1, 2], [issue.root_id, issue.lft, issue.rgt]
217 217 end
218 218 end
219 219 end
220 220
221 221 def test_add_issue_by_created_user
222 222 Setting.default_language = 'en'
223 223 assert_difference 'User.count' do
224 224 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'create')
225 225 assert issue.is_a?(Issue)
226 226 assert issue.author.active?
227 227 assert_equal 'john.doe@somenet.foo', issue.author.mail
228 228 assert_equal 'John', issue.author.firstname
229 229 assert_equal 'Doe', issue.author.lastname
230 230
231 231 # account information
232 232 email = ActionMailer::Base.deliveries.first
233 233 assert_not_nil email
234 234 assert email.subject.include?('account activation')
235 235 login = email.body.match(/\* Login: (.*)$/)[1]
236 236 password = email.body.match(/\* Password: (.*)$/)[1]
237 237 assert_equal issue.author, User.try_to_login(login, password)
238 238 end
239 239 end
240 240
241 241 def test_add_issue_without_from_header
242 242 Role.anonymous.add_permission!(:add_issues)
243 243 assert_equal false, submit_email('ticket_without_from_header.eml')
244 244 end
245 245
246 246 def test_add_issue_with_invalid_attributes
247 247 issue = submit_email('ticket_with_invalid_attributes.eml', :allow_override => 'tracker,category,priority')
248 248 assert issue.is_a?(Issue)
249 249 assert !issue.new_record?
250 250 issue.reload
251 251 assert_nil issue.assigned_to
252 252 assert_nil issue.start_date
253 253 assert_nil issue.due_date
254 254 assert_equal 0, issue.done_ratio
255 255 assert_equal 'Normal', issue.priority.to_s
256 256 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
257 257 end
258 258
259 259 def test_add_issue_with_localized_attributes
260 260 User.find_by_mail('jsmith@somenet.foo').update_attribute 'language', 'fr'
261 261 issue = submit_email('ticket_with_localized_attributes.eml', :allow_override => 'tracker,category,priority')
262 262 assert issue.is_a?(Issue)
263 263 assert !issue.new_record?
264 264 issue.reload
265 265 assert_equal 'New ticket on a given project', issue.subject
266 266 assert_equal User.find_by_login('jsmith'), issue.author
267 267 assert_equal Project.find(2), issue.project
268 268 assert_equal 'Feature request', issue.tracker.to_s
269 269 assert_equal 'Stock management', issue.category.to_s
270 270 assert_equal 'Urgent', issue.priority.to_s
271 271 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
272 272 end
273 273
274 274 def test_add_issue_with_japanese_keywords
275 275 tracker = Tracker.create!(:name => 'ι–‹η™Ί')
276 276 Project.find(1).trackers << tracker
277 277 issue = submit_email('japanese_keywords_iso_2022_jp.eml', :issue => {:project => 'ecookbook'}, :allow_override => 'tracker')
278 278 assert_kind_of Issue, issue
279 279 assert_equal tracker, issue.tracker
280 280 end
281 281
282 282 def test_should_ignore_emails_from_emission_address
283 283 Role.anonymous.add_permission!(:add_issues)
284 284 assert_no_difference 'User.count' do
285 285 assert_equal false, submit_email('ticket_from_emission_address.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'create')
286 286 end
287 287 end
288 288
289 289 def test_add_issue_should_send_email_notification
290 290 Setting.notified_events = ['issue_added']
291 291 ActionMailer::Base.deliveries.clear
292 292 # This email contains: 'Project: onlinestore'
293 293 issue = submit_email('ticket_on_given_project.eml')
294 294 assert issue.is_a?(Issue)
295 295 assert_equal 1, ActionMailer::Base.deliveries.size
296 296 end
297 297
298 298 def test_add_issue_note
299 299 journal = submit_email('ticket_reply.eml')
300 300 assert journal.is_a?(Journal)
301 301 assert_equal User.find_by_login('jsmith'), journal.user
302 302 assert_equal Issue.find(2), journal.journalized
303 303 assert_match /This is reply/, journal.notes
304 304 assert_equal 'Feature request', journal.issue.tracker.name
305 305 end
306 306
307 307 def test_add_issue_note_with_attribute_changes
308 308 # This email contains: 'Status: Resolved'
309 309 journal = submit_email('ticket_reply_with_status.eml')
310 310 assert journal.is_a?(Journal)
311 311 issue = Issue.find(journal.issue.id)
312 312 assert_equal User.find_by_login('jsmith'), journal.user
313 313 assert_equal Issue.find(2), journal.journalized
314 314 assert_match /This is reply/, journal.notes
315 315 assert_equal 'Feature request', journal.issue.tracker.name
316 316 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
317 317 assert_equal '2010-01-01', issue.start_date.to_s
318 318 assert_equal '2010-12-31', issue.due_date.to_s
319 319 assert_equal User.find_by_login('jsmith'), issue.assigned_to
320 320 assert_equal "52.6", issue.custom_value_for(CustomField.find_by_name('Float field')).value
321 321 end
322 322
323 323 def test_add_issue_note_should_send_email_notification
324 324 ActionMailer::Base.deliveries.clear
325 325 journal = submit_email('ticket_reply.eml')
326 326 assert journal.is_a?(Journal)
327 327 assert_equal 1, ActionMailer::Base.deliveries.size
328 328 end
329 329
330 def test_add_issue_note_should_not_set_defaults
331 journal = submit_email('ticket_reply.eml', :issue => {:tracker => 'Support request', :priority => 'High'})
332 assert journal.is_a?(Journal)
333 assert_match /This is reply/, journal.notes
334 assert_equal 'Feature request', journal.issue.tracker.name
335 assert_equal 'Normal', journal.issue.priority.name
336 end
337
330 338 def test_reply_to_a_message
331 339 m = submit_email('message_reply.eml')
332 340 assert m.is_a?(Message)
333 341 assert !m.new_record?
334 342 m.reload
335 343 assert_equal 'Reply via email', m.subject
336 344 # The email replies to message #2 which is part of the thread of message #1
337 345 assert_equal Message.find(1), m.parent
338 346 end
339 347
340 348 def test_reply_to_a_message_by_subject
341 349 m = submit_email('message_reply_by_subject.eml')
342 350 assert m.is_a?(Message)
343 351 assert !m.new_record?
344 352 m.reload
345 353 assert_equal 'Reply to the first post', m.subject
346 354 assert_equal Message.find(1), m.parent
347 355 end
348 356
349 357 def test_should_strip_tags_of_html_only_emails
350 358 issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'})
351 359 assert issue.is_a?(Issue)
352 360 assert !issue.new_record?
353 361 issue.reload
354 362 assert_equal 'HTML email', issue.subject
355 363 assert_equal 'This is a html-only email.', issue.description
356 364 end
357 365
358 366 context "truncate emails based on the Setting" do
359 367 context "with no setting" do
360 368 setup do
361 369 Setting.mail_handler_body_delimiters = ''
362 370 end
363 371
364 372 should "add the entire email into the issue" do
365 373 issue = submit_email('ticket_on_given_project.eml')
366 374 assert_issue_created(issue)
367 375 assert issue.description.include?('---')
368 376 assert issue.description.include?('This paragraph is after the delimiter')
369 377 end
370 378 end
371 379
372 380 context "with a single string" do
373 381 setup do
374 382 Setting.mail_handler_body_delimiters = '---'
375 383 end
376 384
377 385 should "truncate the email at the delimiter for the issue" do
378 386 issue = submit_email('ticket_on_given_project.eml')
379 387 assert_issue_created(issue)
380 388 assert issue.description.include?('This paragraph is before delimiters')
381 389 assert issue.description.include?('--- This line starts with a delimiter')
382 390 assert !issue.description.match(/^---$/)
383 391 assert !issue.description.include?('This paragraph is after the delimiter')
384 392 end
385 393 end
386 394
387 395 context "with a single quoted reply (e.g. reply to a Redmine email notification)" do
388 396 setup do
389 397 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
390 398 end
391 399
392 400 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
393 401 journal = submit_email('issue_update_with_quoted_reply_above.eml')
394 402 assert journal.is_a?(Journal)
395 403 assert journal.notes.include?('An update to the issue by the sender.')
396 404 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
397 405 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
398 406
399 407 end
400 408
401 409 end
402 410
403 411 context "with multiple quoted replies (e.g. reply to a reply of a Redmine email notification)" do
404 412 setup do
405 413 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
406 414 end
407 415
408 416 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
409 417 journal = submit_email('issue_update_with_multiple_quoted_reply_above.eml')
410 418 assert journal.is_a?(Journal)
411 419 assert journal.notes.include?('An update to the issue by the sender.')
412 420 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
413 421 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
414 422
415 423 end
416 424
417 425 end
418 426
419 427 context "with multiple strings" do
420 428 setup do
421 429 Setting.mail_handler_body_delimiters = "---\nBREAK"
422 430 end
423 431
424 432 should "truncate the email at the first delimiter found (BREAK)" do
425 433 issue = submit_email('ticket_on_given_project.eml')
426 434 assert_issue_created(issue)
427 435 assert issue.description.include?('This paragraph is before delimiters')
428 436 assert !issue.description.include?('BREAK')
429 437 assert !issue.description.include?('This paragraph is between delimiters')
430 438 assert !issue.description.match(/^---$/)
431 439 assert !issue.description.include?('This paragraph is after the delimiter')
432 440 end
433 441 end
434 442 end
435 443
436 444 def test_email_with_long_subject_line
437 445 issue = submit_email('ticket_with_long_subject.eml')
438 446 assert issue.is_a?(Issue)
439 447 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]
440 448 end
441 449
442 450 private
443 451
444 452 def submit_email(filename, options={})
445 453 raw = IO.read(File.join(FIXTURES_PATH, filename))
446 454 MailHandler.receive(raw, options)
447 455 end
448 456
449 457 def assert_issue_created(issue)
450 458 assert issue.is_a?(Issue)
451 459 assert !issue.new_record?
452 460 issue.reload
453 461 end
454 462 end
General Comments 0
You need to be logged in to leave comments. Login now