##// END OF EJS Templates
Makes MailHandler ignore invalid keyword values to avoid validation failures....
Jean-Philippe Lang -
r4282:abf988ad69cc
parent child
Show More
@@ -0,0 +1,46
1 Return-Path: <jsmith@somenet.foo>
2 Received: from osiris ([127.0.0.1])
3 by OSIRIS
4 with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200
5 Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris>
6 From: "John Smith" <jsmith@somenet.foo>
7 To: <redmine@somenet.foo>
8 Subject: New ticket on a given project
9 Date: Sun, 22 Jun 2008 12:28:07 +0200
10 MIME-Version: 1.0
11 Content-Type: text/plain;
12 format=flowed;
13 charset="iso-8859-1";
14 reply-type=original
15 Content-Transfer-Encoding: 7bit
16 X-Priority: 3
17 X-MSMail-Priority: Normal
18 X-Mailer: Microsoft Outlook Express 6.00.2900.2869
19 X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869
20
21 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet
22 turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus
23 blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti
24 sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In
25 in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras
26 sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum
27 id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus
28 eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique
29 sed, mauris. Pellentesque habitant morbi tristique senectus et netus et
30 malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse
31 platea dictumst.
32
33 Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque
34 sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem.
35 Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et,
36 dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed,
37 massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo
38 pulvinar dui, a gravida orci mi eget odio. Nunc a lacus.
39
40 Project: onlinestore
41 Tracker: Feature request
42 category: Stock management
43 priority: foo
44 done ratio: x
45 start date: some day
46 due date: never
@@ -1,348 +1,349
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 151 journal = issue.init_journal(user, cleaned_up_text_body)
152 152 issue.safe_attributes = issue_attributes_from_keywords(issue)
153 153 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
154 154 add_attachments(issue)
155 155 issue.save!
156 156 logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info
157 157 journal
158 158 end
159 159
160 160 # Reply will be added to the issue
161 161 def receive_journal_reply(journal_id)
162 162 journal = Journal.find_by_id(journal_id)
163 163 if journal && journal.journalized_type == 'Issue'
164 164 receive_issue_reply(journal.journalized_id)
165 165 end
166 166 end
167 167
168 168 # Receives a reply to a forum message
169 169 def receive_message_reply(message_id)
170 170 message = Message.find_by_id(message_id)
171 171 if message
172 172 message = message.root
173 173
174 174 unless @@handler_options[:no_permission_check]
175 175 raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
176 176 end
177 177
178 178 if !message.locked?
179 179 reply = Message.new(:subject => email.subject.gsub(%r{^.*msg\d+\]}, '').strip,
180 180 :content => cleaned_up_text_body)
181 181 reply.author = user
182 182 reply.board = message.board
183 183 message.children << reply
184 184 add_attachments(reply)
185 185 reply
186 186 else
187 187 logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic" if logger && logger.info
188 188 end
189 189 end
190 190 end
191 191
192 192 def add_attachments(obj)
193 193 if email.has_attachments?
194 194 email.attachments.each do |attachment|
195 195 Attachment.create(:container => obj,
196 196 :file => attachment,
197 197 :author => user,
198 198 :content_type => attachment.content_type)
199 199 end
200 200 end
201 201 end
202 202
203 203 # Adds To and Cc as watchers of the given object if the sender has the
204 204 # appropriate permission
205 205 def add_watchers(obj)
206 206 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
207 207 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
208 208 unless addresses.empty?
209 209 watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
210 210 watchers.each {|w| obj.add_watcher(w)}
211 211 end
212 212 end
213 213 end
214 214
215 215 def get_keyword(attr, options={})
216 216 @keywords ||= {}
217 217 if @keywords.has_key?(attr)
218 218 @keywords[attr]
219 219 else
220 220 @keywords[attr] = begin
221 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && (v = extract_keyword!(plain_text_body, attr))
221 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && (v = extract_keyword!(plain_text_body, attr, options[:format]))
222 222 v
223 223 elsif !@@handler_options[:issue][attr].blank?
224 224 @@handler_options[:issue][attr]
225 225 end
226 226 end
227 227 end
228 228 end
229 229
230 230 # Destructively extracts the value for +attr+ in +text+
231 231 # Returns nil if no matching keyword found
232 def extract_keyword!(text, attr)
232 def extract_keyword!(text, attr, format=nil)
233 233 keys = [attr.to_s.humanize]
234 234 if attr.is_a?(Symbol)
235 235 keys << l("field_#{attr}", :default => '', :locale => user.language) if user
236 236 keys << l("field_#{attr}", :default => '', :locale => Setting.default_language)
237 237 end
238 238 keys.reject! {|k| k.blank?}
239 239 keys.collect! {|k| Regexp.escape(k)}
240 text.gsub!(/^(#{keys.join('|')})[ \t]*:[ \t]*(.+)\s*$/i, '')
240 format ||= '.+'
241 text.gsub!(/^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i, '')
241 242 $2 && $2.strip
242 243 end
243 244
244 245 def target_project
245 246 # TODO: other ways to specify project:
246 247 # * parse the email To field
247 248 # * specific project (eg. Setting.mail_handler_target_project)
248 249 target = Project.find_by_identifier(get_keyword(:project))
249 250 raise MissingInformation.new('Unable to determine target project') if target.nil?
250 251 target
251 252 end
252 253
253 254 # Returns a Hash of issue attributes extracted from keywords in the email body
254 255 def issue_attributes_from_keywords(issue)
255 256 {
256 257 'tracker_id' => ((k = get_keyword(:tracker)) && issue.project.trackers.find_by_name(k).try(:id)) || issue.project.trackers.find(:first).try(:id),
257 258 'status_id' => (k = get_keyword(:status)) && IssueStatus.find_by_name(k).try(:id),
258 259 'priority_id' => (k = get_keyword(:priority)) && IssuePriority.find_by_name(k).try(:id),
259 260 'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.find_by_name(k).try(:id),
260 261 'assigned_to_id' => (k = get_keyword(:assigned_to, :override => true)) && find_user_from_keyword(k).try(:id),
261 262 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) && issue.project.shared_versions.find_by_name(k).try(:id),
262 'start_date' => get_keyword(:start_date, :override => true),
263 'due_date' => get_keyword(:due_date, :override => true),
263 'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
264 'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
264 265 'estimated_hours' => get_keyword(:estimated_hours, :override => true),
265 'done_ratio' => get_keyword(:done_ratio, :override => true),
266 'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0')
266 267 }.delete_if {|k, v| v.blank? }
267 268 end
268 269
269 270 # Returns a Hash of issue custom field values extracted from keywords in the email body
270 271 def custom_field_values_from_keywords(customized)
271 272 customized.custom_field_values.inject({}) do |h, v|
272 273 if value = get_keyword(v.custom_field.name, :override => true)
273 274 h[v.custom_field.id.to_s] = value
274 275 end
275 276 h
276 277 end
277 278 end
278 279
279 280 # Returns the text/plain part of the email
280 281 # If not found (eg. HTML-only email), returns the body with tags removed
281 282 def plain_text_body
282 283 return @plain_text_body unless @plain_text_body.nil?
283 284 parts = @email.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten
284 285 if parts.empty?
285 286 parts << @email
286 287 end
287 288 plain_text_part = parts.detect {|p| p.content_type == 'text/plain'}
288 289 if plain_text_part.nil?
289 290 # no text/plain part found, assuming html-only email
290 291 # strip html tags and remove doctype directive
291 292 @plain_text_body = strip_tags(@email.body.to_s)
292 293 @plain_text_body.gsub! %r{^<!DOCTYPE .*$}, ''
293 294 else
294 295 @plain_text_body = plain_text_part.body.to_s
295 296 end
296 297 @plain_text_body.strip!
297 298 @plain_text_body
298 299 end
299 300
300 301 def cleaned_up_text_body
301 302 cleanup_body(plain_text_body)
302 303 end
303 304
304 305 def self.full_sanitizer
305 306 @full_sanitizer ||= HTML::FullSanitizer.new
306 307 end
307 308
308 309 # Creates a user account for the +email+ sender
309 310 def self.create_user_from_email(email)
310 311 addr = email.from_addrs.to_a.first
311 312 if addr && !addr.spec.blank?
312 313 user = User.new
313 314 user.mail = addr.spec
314 315
315 316 names = addr.name.blank? ? addr.spec.gsub(/@.*$/, '').split('.') : addr.name.split
316 317 user.firstname = names.shift
317 318 user.lastname = names.join(' ')
318 319 user.lastname = '-' if user.lastname.blank?
319 320
320 321 user.login = user.mail
321 322 user.password = ActiveSupport::SecureRandom.hex(5)
322 323 user.language = Setting.default_language
323 324 user.save ? user : nil
324 325 end
325 326 end
326 327
327 328 private
328 329
329 330 # Removes the email body of text after the truncation configurations.
330 331 def cleanup_body(body)
331 332 delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
332 333 unless delimiters.empty?
333 334 regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
334 335 body = body.gsub(regex, '')
335 336 end
336 337 body.strip
337 338 end
338 339
339 340 def find_user_from_keyword(keyword)
340 341 user ||= User.find_by_mail(keyword)
341 342 user ||= User.find_by_login(keyword)
342 343 if user.nil? && keyword.match(/ /)
343 344 firstname, lastname = *(keyword.split) # "First Last Throwaway"
344 345 user ||= User.find_by_firstname_and_lastname(firstname, lastname)
345 346 end
346 347 user
347 348 end
348 349 end
@@ -1,425 +1,437
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.dirname(__FILE__) + '/../test_helper'
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 :issues,
29 29 :issue_statuses,
30 30 :workflows,
31 31 :trackers,
32 32 :projects_trackers,
33 33 :versions,
34 34 :enumerations,
35 35 :issue_categories,
36 36 :custom_fields,
37 37 :custom_fields_trackers,
38 38 :boards,
39 39 :messages
40 40
41 41 FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler'
42 42
43 43 def setup
44 44 ActionMailer::Base.deliveries.clear
45 45 Setting.notified_events = Redmine::Notifiable.all.collect(&:name)
46 46 end
47 47
48 48 def test_add_issue
49 49 ActionMailer::Base.deliveries.clear
50 50 # This email contains: 'Project: onlinestore'
51 51 issue = submit_email('ticket_on_given_project.eml')
52 52 assert issue.is_a?(Issue)
53 53 assert !issue.new_record?
54 54 issue.reload
55 55 assert_equal 'New ticket on a given project', issue.subject
56 56 assert_equal User.find_by_login('jsmith'), issue.author
57 57 assert_equal Project.find(2), issue.project
58 58 assert_equal IssueStatus.find_by_name('Resolved'), issue.status
59 59 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
60 60 assert_equal '2010-01-01', issue.start_date.to_s
61 61 assert_equal '2010-12-31', issue.due_date.to_s
62 62 assert_equal User.find_by_login('jsmith'), issue.assigned_to
63 63 assert_equal Version.find_by_name('alpha'), issue.fixed_version
64 64 assert_equal 2.5, issue.estimated_hours
65 65 assert_equal 30, issue.done_ratio
66 66 # keywords should be removed from the email body
67 67 assert !issue.description.match(/^Project:/i)
68 68 assert !issue.description.match(/^Status:/i)
69 69 # Email notification should be sent
70 70 mail = ActionMailer::Base.deliveries.last
71 71 assert_not_nil mail
72 72 assert mail.subject.include?('New ticket on a given project')
73 73 end
74 74
75 75 def test_add_issue_with_status
76 76 # This email contains: 'Project: onlinestore' and 'Status: Resolved'
77 77 issue = submit_email('ticket_on_given_project.eml')
78 78 assert issue.is_a?(Issue)
79 79 assert !issue.new_record?
80 80 issue.reload
81 81 assert_equal Project.find(2), issue.project
82 82 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
83 83 end
84 84
85 85 def test_add_issue_with_attributes_override
86 86 issue = submit_email('ticket_with_attributes.eml', :allow_override => 'tracker,category,priority')
87 87 assert issue.is_a?(Issue)
88 88 assert !issue.new_record?
89 89 issue.reload
90 90 assert_equal 'New ticket on a given project', issue.subject
91 91 assert_equal User.find_by_login('jsmith'), issue.author
92 92 assert_equal Project.find(2), issue.project
93 93 assert_equal 'Feature request', issue.tracker.to_s
94 94 assert_equal 'Stock management', issue.category.to_s
95 95 assert_equal 'Urgent', issue.priority.to_s
96 96 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
97 97 end
98 98
99 99 def test_add_issue_with_partial_attributes_override
100 100 issue = submit_email('ticket_with_attributes.eml', :issue => {:priority => 'High'}, :allow_override => ['tracker'])
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_nil issue.category
109 109 assert_equal 'High', 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_spaces_between_attribute_and_separator
114 114 issue = submit_email('ticket_with_spaces_between_attribute_and_separator.eml', :allow_override => 'tracker,category,priority')
115 115 assert issue.is_a?(Issue)
116 116 assert !issue.new_record?
117 117 issue.reload
118 118 assert_equal 'New ticket on a given project', issue.subject
119 119 assert_equal User.find_by_login('jsmith'), issue.author
120 120 assert_equal Project.find(2), issue.project
121 121 assert_equal 'Feature request', issue.tracker.to_s
122 122 assert_equal 'Stock management', issue.category.to_s
123 123 assert_equal 'Urgent', issue.priority.to_s
124 124 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
125 125 end
126 126
127 127
128 128 def test_add_issue_with_attachment_to_specific_project
129 129 issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
130 130 assert issue.is_a?(Issue)
131 131 assert !issue.new_record?
132 132 issue.reload
133 133 assert_equal 'Ticket created by email with attachment', issue.subject
134 134 assert_equal User.find_by_login('jsmith'), issue.author
135 135 assert_equal Project.find(2), issue.project
136 136 assert_equal 'This is a new ticket with attachments', issue.description
137 137 # Attachment properties
138 138 assert_equal 1, issue.attachments.size
139 139 assert_equal 'Paella.jpg', issue.attachments.first.filename
140 140 assert_equal 'image/jpeg', issue.attachments.first.content_type
141 141 assert_equal 10790, issue.attachments.first.filesize
142 142 end
143 143
144 144 def test_add_issue_with_custom_fields
145 145 issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'onlinestore'})
146 146 assert issue.is_a?(Issue)
147 147 assert !issue.new_record?
148 148 issue.reload
149 149 assert_equal 'New ticket with custom field values', issue.subject
150 150 assert_equal 'Value for a custom field', issue.custom_value_for(CustomField.find_by_name('Searchable field')).value
151 151 assert !issue.description.match(/^searchable field:/i)
152 152 end
153 153
154 154 def test_add_issue_with_cc
155 155 issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'})
156 156 assert issue.is_a?(Issue)
157 157 assert !issue.new_record?
158 158 issue.reload
159 159 assert issue.watched_by?(User.find_by_mail('dlopper@somenet.foo'))
160 160 assert_equal 1, issue.watcher_user_ids.size
161 161 end
162 162
163 163 def test_add_issue_by_unknown_user
164 164 assert_no_difference 'User.count' do
165 165 assert_equal false, submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'})
166 166 end
167 167 end
168 168
169 169 def test_add_issue_by_anonymous_user
170 170 Role.anonymous.add_permission!(:add_issues)
171 171 assert_no_difference 'User.count' do
172 172 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'accept')
173 173 assert issue.is_a?(Issue)
174 174 assert issue.author.anonymous?
175 175 end
176 176 end
177 177
178 178 def test_add_issue_by_anonymous_user_with_no_from_address
179 179 Role.anonymous.add_permission!(:add_issues)
180 180 assert_no_difference 'User.count' do
181 181 issue = submit_email('ticket_by_empty_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'accept')
182 182 assert issue.is_a?(Issue)
183 183 assert issue.author.anonymous?
184 184 end
185 185 end
186 186
187 187 def test_add_issue_by_anonymous_user_on_private_project
188 188 Role.anonymous.add_permission!(:add_issues)
189 189 assert_no_difference 'User.count' do
190 190 assert_no_difference 'Issue.count' do
191 191 assert_equal false, submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'onlinestore'}, :unknown_user => 'accept')
192 192 end
193 193 end
194 194 end
195 195
196 196 def test_add_issue_by_anonymous_user_on_private_project_without_permission_check
197 197 assert_no_difference 'User.count' do
198 198 assert_difference 'Issue.count' do
199 199 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'onlinestore'}, :no_permission_check => '1', :unknown_user => 'accept')
200 200 assert issue.is_a?(Issue)
201 201 assert issue.author.anonymous?
202 202 assert !issue.project.is_public?
203 203 end
204 204 end
205 205 end
206 206
207 207 def test_add_issue_by_created_user
208 208 Setting.default_language = 'en'
209 209 assert_difference 'User.count' do
210 210 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'create')
211 211 assert issue.is_a?(Issue)
212 212 assert issue.author.active?
213 213 assert_equal 'john.doe@somenet.foo', issue.author.mail
214 214 assert_equal 'John', issue.author.firstname
215 215 assert_equal 'Doe', issue.author.lastname
216 216
217 217 # account information
218 218 email = ActionMailer::Base.deliveries.first
219 219 assert_not_nil email
220 220 assert email.subject.include?('account activation')
221 221 login = email.body.match(/\* Login: (.*)$/)[1]
222 222 password = email.body.match(/\* Password: (.*)$/)[1]
223 223 assert_equal issue.author, User.try_to_login(login, password)
224 224 end
225 225 end
226 226
227 227 def test_add_issue_without_from_header
228 228 Role.anonymous.add_permission!(:add_issues)
229 229 assert_equal false, submit_email('ticket_without_from_header.eml')
230 230 end
231 231
232 def test_add_issue_with_invalid_attributes
233 issue = submit_email('ticket_with_invalid_attributes.eml', :allow_override => 'tracker,category,priority')
234 assert issue.is_a?(Issue)
235 assert !issue.new_record?
236 issue.reload
237 assert_nil issue.start_date
238 assert_nil issue.due_date
239 assert_equal 0, issue.done_ratio
240 assert_equal 'Normal', issue.priority.to_s
241 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
242 end
243
232 244 def test_add_issue_with_localized_attributes
233 245 User.find_by_mail('jsmith@somenet.foo').update_attribute 'language', 'fr'
234 246 issue = submit_email('ticket_with_localized_attributes.eml', :allow_override => 'tracker,category,priority')
235 247 assert issue.is_a?(Issue)
236 248 assert !issue.new_record?
237 249 issue.reload
238 250 assert_equal 'New ticket on a given project', issue.subject
239 251 assert_equal User.find_by_login('jsmith'), issue.author
240 252 assert_equal Project.find(2), issue.project
241 253 assert_equal 'Feature request', issue.tracker.to_s
242 254 assert_equal 'Stock management', issue.category.to_s
243 255 assert_equal 'Urgent', issue.priority.to_s
244 256 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
245 257 end
246 258
247 259 def test_add_issue_with_japanese_keywords
248 260 tracker = Tracker.create!(:name => 'ι–‹η™Ί')
249 261 Project.find(1).trackers << tracker
250 262 issue = submit_email('japanese_keywords_iso_2022_jp.eml', :issue => {:project => 'ecookbook'}, :allow_override => 'tracker')
251 263 assert_kind_of Issue, issue
252 264 assert_equal tracker, issue.tracker
253 265 end
254 266
255 267 def test_should_ignore_emails_from_emission_address
256 268 Role.anonymous.add_permission!(:add_issues)
257 269 assert_no_difference 'User.count' do
258 270 assert_equal false, submit_email('ticket_from_emission_address.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'create')
259 271 end
260 272 end
261 273
262 274 def test_add_issue_should_send_email_notification
263 275 Setting.notified_events = ['issue_added']
264 276 ActionMailer::Base.deliveries.clear
265 277 # This email contains: 'Project: onlinestore'
266 278 issue = submit_email('ticket_on_given_project.eml')
267 279 assert issue.is_a?(Issue)
268 280 assert_equal 1, ActionMailer::Base.deliveries.size
269 281 end
270 282
271 283 def test_add_issue_note
272 284 journal = submit_email('ticket_reply.eml')
273 285 assert journal.is_a?(Journal)
274 286 assert_equal User.find_by_login('jsmith'), journal.user
275 287 assert_equal Issue.find(2), journal.journalized
276 288 assert_match /This is reply/, journal.notes
277 289 end
278 290
279 291 def test_add_issue_note_with_attribute_changes
280 292 # This email contains: 'Status: Resolved'
281 293 journal = submit_email('ticket_reply_with_status.eml')
282 294 assert journal.is_a?(Journal)
283 295 issue = Issue.find(journal.issue.id)
284 296 assert_equal User.find_by_login('jsmith'), journal.user
285 297 assert_equal Issue.find(2), journal.journalized
286 298 assert_match /This is reply/, journal.notes
287 299 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
288 300 assert_equal '2010-01-01', issue.start_date.to_s
289 301 assert_equal '2010-12-31', issue.due_date.to_s
290 302 assert_equal User.find_by_login('jsmith'), issue.assigned_to
291 303 assert_equal 'Updated custom value', issue.custom_value_for(CustomField.find_by_name('Searchable field')).value
292 304 end
293 305
294 306 def test_add_issue_note_should_send_email_notification
295 307 ActionMailer::Base.deliveries.clear
296 308 journal = submit_email('ticket_reply.eml')
297 309 assert journal.is_a?(Journal)
298 310 assert_equal 1, ActionMailer::Base.deliveries.size
299 311 end
300 312
301 313 def test_reply_to_a_message
302 314 m = submit_email('message_reply.eml')
303 315 assert m.is_a?(Message)
304 316 assert !m.new_record?
305 317 m.reload
306 318 assert_equal 'Reply via email', m.subject
307 319 # The email replies to message #2 which is part of the thread of message #1
308 320 assert_equal Message.find(1), m.parent
309 321 end
310 322
311 323 def test_reply_to_a_message_by_subject
312 324 m = submit_email('message_reply_by_subject.eml')
313 325 assert m.is_a?(Message)
314 326 assert !m.new_record?
315 327 m.reload
316 328 assert_equal 'Reply to the first post', m.subject
317 329 assert_equal Message.find(1), m.parent
318 330 end
319 331
320 332 def test_should_strip_tags_of_html_only_emails
321 333 issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'})
322 334 assert issue.is_a?(Issue)
323 335 assert !issue.new_record?
324 336 issue.reload
325 337 assert_equal 'HTML email', issue.subject
326 338 assert_equal 'This is a html-only email.', issue.description
327 339 end
328 340
329 341 context "truncate emails based on the Setting" do
330 342 context "with no setting" do
331 343 setup do
332 344 Setting.mail_handler_body_delimiters = ''
333 345 end
334 346
335 347 should "add the entire email into the issue" do
336 348 issue = submit_email('ticket_on_given_project.eml')
337 349 assert_issue_created(issue)
338 350 assert issue.description.include?('---')
339 351 assert issue.description.include?('This paragraph is after the delimiter')
340 352 end
341 353 end
342 354
343 355 context "with a single string" do
344 356 setup do
345 357 Setting.mail_handler_body_delimiters = '---'
346 358 end
347 359
348 360 should "truncate the email at the delimiter for the issue" do
349 361 issue = submit_email('ticket_on_given_project.eml')
350 362 assert_issue_created(issue)
351 363 assert issue.description.include?('This paragraph is before delimiters')
352 364 assert issue.description.include?('--- This line starts with a delimiter')
353 365 assert !issue.description.match(/^---$/)
354 366 assert !issue.description.include?('This paragraph is after the delimiter')
355 367 end
356 368 end
357 369
358 370 context "with a single quoted reply (e.g. reply to a Redmine email notification)" do
359 371 setup do
360 372 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
361 373 end
362 374
363 375 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
364 376 journal = submit_email('issue_update_with_quoted_reply_above.eml')
365 377 assert journal.is_a?(Journal)
366 378 assert journal.notes.include?('An update to the issue by the sender.')
367 379 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
368 380 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
369 381
370 382 end
371 383
372 384 end
373 385
374 386 context "with multiple quoted replies (e.g. reply to a reply of a Redmine email notification)" do
375 387 setup do
376 388 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
377 389 end
378 390
379 391 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
380 392 journal = submit_email('issue_update_with_multiple_quoted_reply_above.eml')
381 393 assert journal.is_a?(Journal)
382 394 assert journal.notes.include?('An update to the issue by the sender.')
383 395 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
384 396 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
385 397
386 398 end
387 399
388 400 end
389 401
390 402 context "with multiple strings" do
391 403 setup do
392 404 Setting.mail_handler_body_delimiters = "---\nBREAK"
393 405 end
394 406
395 407 should "truncate the email at the first delimiter found (BREAK)" do
396 408 issue = submit_email('ticket_on_given_project.eml')
397 409 assert_issue_created(issue)
398 410 assert issue.description.include?('This paragraph is before delimiters')
399 411 assert !issue.description.include?('BREAK')
400 412 assert !issue.description.include?('This paragraph is between delimiters')
401 413 assert !issue.description.match(/^---$/)
402 414 assert !issue.description.include?('This paragraph is after the delimiter')
403 415 end
404 416 end
405 417 end
406 418
407 419 def test_email_with_long_subject_line
408 420 issue = submit_email('ticket_with_long_subject.eml')
409 421 assert issue.is_a?(Issue)
410 422 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]
411 423 end
412 424
413 425 private
414 426
415 427 def submit_email(filename, options={})
416 428 raw = IO.read(File.join(FIXTURES_PATH, filename))
417 429 MailHandler.receive(raw, options)
418 430 end
419 431
420 432 def assert_issue_created(issue)
421 433 assert issue.is_a?(Issue)
422 434 assert !issue.new_record?
423 435 issue.reload
424 436 end
425 437 end
General Comments 0
You need to be logged in to leave comments. Login now