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