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