##// END OF EJS Templates
Makes MailHandler accept localized keywords for default or user language (#6112)....
Jean-Philippe Lang -
r4281:d4ab2ab4b976
parent child
Show More
@@ -0,0 +1,43
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 Projet: onlinestore
41 Tracker: Feature request
42 catοΏ½gorie: Stock management
43 prioritοΏ½: Urgent
@@ -1,333 +1,348
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 include Redmine::I18n
20 21
21 22 class UnauthorizedAction < StandardError; end
22 23 class MissingInformation < StandardError; end
23 24
24 25 attr_reader :email, :user
25 26
26 27 def self.receive(email, options={})
27 28 @@handler_options = options.dup
28 29
29 30 @@handler_options[:issue] ||= {}
30 31
31 32 @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip) if @@handler_options[:allow_override].is_a?(String)
32 33 @@handler_options[:allow_override] ||= []
33 34 # Project needs to be overridable if not specified
34 35 @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
35 36 # Status overridable by default
36 37 @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
37 38
38 39 @@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1' ? true : false)
39 40 super email
40 41 end
41 42
42 43 # Processes incoming emails
43 44 # Returns the created object (eg. an issue, a message) or false
44 45 def receive(email)
45 46 @email = email
46 47 sender_email = email.from.to_a.first.to_s.strip
47 48 # Ignore emails received from the application emission address to avoid hell cycles
48 49 if sender_email.downcase == Setting.mail_from.to_s.strip.downcase
49 50 logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]" if logger && logger.info
50 51 return false
51 52 end
52 53 @user = User.find_by_mail(sender_email) if sender_email.present?
53 54 if @user && !@user.active?
54 55 logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]" if logger && logger.info
55 56 return false
56 57 end
57 58 if @user.nil?
58 59 # Email was submitted by an unknown user
59 60 case @@handler_options[:unknown_user]
60 61 when 'accept'
61 62 @user = User.anonymous
62 63 when 'create'
63 64 @user = MailHandler.create_user_from_email(email)
64 65 if @user
65 66 logger.info "MailHandler: [#{@user.login}] account created" if logger && logger.info
66 67 Mailer.deliver_account_information(@user, @user.password)
67 68 else
68 69 logger.error "MailHandler: could not create account for [#{sender_email}]" if logger && logger.error
69 70 return false
70 71 end
71 72 else
72 73 # Default behaviour, emails from unknown users are ignored
73 74 logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]" if logger && logger.info
74 75 return false
75 76 end
76 77 end
77 78 User.current = @user
78 79 dispatch
79 80 end
80 81
81 82 private
82 83
83 84 MESSAGE_ID_RE = %r{^<redmine\.([a-z0-9_]+)\-(\d+)\.\d+@}
84 85 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]*#(\d+)\]}
85 86 MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
86 87
87 88 def dispatch
88 89 headers = [email.in_reply_to, email.references].flatten.compact
89 90 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
90 91 klass, object_id = $1, $2.to_i
91 92 method_name = "receive_#{klass}_reply"
92 93 if self.class.private_instance_methods.collect(&:to_s).include?(method_name)
93 94 send method_name, object_id
94 95 else
95 96 # ignoring it
96 97 end
97 98 elsif m = email.subject.match(ISSUE_REPLY_SUBJECT_RE)
98 99 receive_issue_reply(m[1].to_i)
99 100 elsif m = email.subject.match(MESSAGE_REPLY_SUBJECT_RE)
100 101 receive_message_reply(m[1].to_i)
101 102 else
102 103 receive_issue
103 104 end
104 105 rescue ActiveRecord::RecordInvalid => e
105 106 # TODO: send a email to the user
106 107 logger.error e.message if logger
107 108 false
108 109 rescue MissingInformation => e
109 110 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
110 111 false
111 112 rescue UnauthorizedAction => e
112 113 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
113 114 false
114 115 end
115 116
116 117 # Creates a new issue
117 118 def receive_issue
118 119 project = target_project
119 120 # check permission
120 121 unless @@handler_options[:no_permission_check]
121 122 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
122 123 end
123 124
124 125 issue = Issue.new(:author => user, :project => project)
125 126 issue.safe_attributes = issue_attributes_from_keywords(issue)
126 127 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
127 128 issue.subject = email.subject.to_s.chomp[0,255]
128 129 if issue.subject.blank?
129 130 issue.subject = '(no subject)'
130 131 end
131 132 issue.description = cleaned_up_text_body
132 133
133 134 # add To and Cc as watchers before saving so the watchers can reply to Redmine
134 135 add_watchers(issue)
135 136 issue.save!
136 137 add_attachments(issue)
137 138 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
138 139 issue
139 140 end
140 141
141 142 # Adds a note to an existing issue
142 143 def receive_issue_reply(issue_id)
143 144 issue = Issue.find_by_id(issue_id)
144 145 return unless issue
145 146 # check permission
146 147 unless @@handler_options[:no_permission_check]
147 148 raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project)
148 149 end
149 150
150 151 journal = issue.init_journal(user, cleaned_up_text_body)
151 152 issue.safe_attributes = issue_attributes_from_keywords(issue)
152 153 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
153 154 add_attachments(issue)
154 155 issue.save!
155 156 logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info
156 157 journal
157 158 end
158 159
159 160 # Reply will be added to the issue
160 161 def receive_journal_reply(journal_id)
161 162 journal = Journal.find_by_id(journal_id)
162 163 if journal && journal.journalized_type == 'Issue'
163 164 receive_issue_reply(journal.journalized_id)
164 165 end
165 166 end
166 167
167 168 # Receives a reply to a forum message
168 169 def receive_message_reply(message_id)
169 170 message = Message.find_by_id(message_id)
170 171 if message
171 172 message = message.root
172 173
173 174 unless @@handler_options[:no_permission_check]
174 175 raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
175 176 end
176 177
177 178 if !message.locked?
178 179 reply = Message.new(:subject => email.subject.gsub(%r{^.*msg\d+\]}, '').strip,
179 180 :content => cleaned_up_text_body)
180 181 reply.author = user
181 182 reply.board = message.board
182 183 message.children << reply
183 184 add_attachments(reply)
184 185 reply
185 186 else
186 187 logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic" if logger && logger.info
187 188 end
188 189 end
189 190 end
190 191
191 192 def add_attachments(obj)
192 193 if email.has_attachments?
193 194 email.attachments.each do |attachment|
194 195 Attachment.create(:container => obj,
195 196 :file => attachment,
196 197 :author => user,
197 198 :content_type => attachment.content_type)
198 199 end
199 200 end
200 201 end
201 202
202 203 # Adds To and Cc as watchers of the given object if the sender has the
203 204 # appropriate permission
204 205 def add_watchers(obj)
205 206 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
206 207 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
207 208 unless addresses.empty?
208 209 watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
209 210 watchers.each {|w| obj.add_watcher(w)}
210 211 end
211 212 end
212 213 end
213 214
214 215 def get_keyword(attr, options={})
215 216 @keywords ||= {}
216 217 if @keywords.has_key?(attr)
217 218 @keywords[attr]
218 219 else
219 220 @keywords[attr] = begin
220 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && plain_text_body.gsub!(/^#{attr.to_s.humanize}[ \t]*:[ \t]*(.+)\s*$/i, '')
221 $1.strip
221 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && (v = extract_keyword!(plain_text_body, attr))
222 v
222 223 elsif !@@handler_options[:issue][attr].blank?
223 224 @@handler_options[:issue][attr]
224 225 end
225 226 end
226 227 end
227 228 end
229
230 # Destructively extracts the value for +attr+ in +text+
231 # Returns nil if no matching keyword found
232 def extract_keyword!(text, attr)
233 keys = [attr.to_s.humanize]
234 if attr.is_a?(Symbol)
235 keys << l("field_#{attr}", :default => '', :locale => user.language) if user
236 keys << l("field_#{attr}", :default => '', :locale => Setting.default_language)
237 end
238 keys.reject! {|k| k.blank?}
239 keys.collect! {|k| Regexp.escape(k)}
240 text.gsub!(/^(#{keys.join('|')})[ \t]*:[ \t]*(.+)\s*$/i, '')
241 $2 && $2.strip
242 end
228 243
229 244 def target_project
230 245 # TODO: other ways to specify project:
231 246 # * parse the email To field
232 247 # * specific project (eg. Setting.mail_handler_target_project)
233 248 target = Project.find_by_identifier(get_keyword(:project))
234 249 raise MissingInformation.new('Unable to determine target project') if target.nil?
235 250 target
236 251 end
237 252
238 253 # Returns a Hash of issue attributes extracted from keywords in the email body
239 254 def issue_attributes_from_keywords(issue)
240 255 {
241 256 'tracker_id' => ((k = get_keyword(:tracker)) && issue.project.trackers.find_by_name(k).try(:id)) || issue.project.trackers.find(:first).try(:id),
242 257 'status_id' => (k = get_keyword(:status)) && IssueStatus.find_by_name(k).try(:id),
243 258 'priority_id' => (k = get_keyword(:priority)) && IssuePriority.find_by_name(k).try(:id),
244 259 'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.find_by_name(k).try(:id),
245 260 'assigned_to_id' => (k = get_keyword(:assigned_to, :override => true)) && find_user_from_keyword(k).try(:id),
246 261 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) && issue.project.shared_versions.find_by_name(k).try(:id),
247 262 'start_date' => get_keyword(:start_date, :override => true),
248 263 'due_date' => get_keyword(:due_date, :override => true),
249 264 'estimated_hours' => get_keyword(:estimated_hours, :override => true),
250 265 'done_ratio' => get_keyword(:done_ratio, :override => true),
251 266 }.delete_if {|k, v| v.blank? }
252 267 end
253 268
254 269 # Returns a Hash of issue custom field values extracted from keywords in the email body
255 270 def custom_field_values_from_keywords(customized)
256 271 customized.custom_field_values.inject({}) do |h, v|
257 272 if value = get_keyword(v.custom_field.name, :override => true)
258 273 h[v.custom_field.id.to_s] = value
259 274 end
260 275 h
261 276 end
262 277 end
263 278
264 279 # Returns the text/plain part of the email
265 280 # If not found (eg. HTML-only email), returns the body with tags removed
266 281 def plain_text_body
267 282 return @plain_text_body unless @plain_text_body.nil?
268 283 parts = @email.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten
269 284 if parts.empty?
270 285 parts << @email
271 286 end
272 287 plain_text_part = parts.detect {|p| p.content_type == 'text/plain'}
273 288 if plain_text_part.nil?
274 289 # no text/plain part found, assuming html-only email
275 290 # strip html tags and remove doctype directive
276 291 @plain_text_body = strip_tags(@email.body.to_s)
277 292 @plain_text_body.gsub! %r{^<!DOCTYPE .*$}, ''
278 293 else
279 294 @plain_text_body = plain_text_part.body.to_s
280 295 end
281 296 @plain_text_body.strip!
282 297 @plain_text_body
283 298 end
284 299
285 300 def cleaned_up_text_body
286 301 cleanup_body(plain_text_body)
287 302 end
288 303
289 304 def self.full_sanitizer
290 305 @full_sanitizer ||= HTML::FullSanitizer.new
291 306 end
292 307
293 308 # Creates a user account for the +email+ sender
294 309 def self.create_user_from_email(email)
295 310 addr = email.from_addrs.to_a.first
296 311 if addr && !addr.spec.blank?
297 312 user = User.new
298 313 user.mail = addr.spec
299 314
300 315 names = addr.name.blank? ? addr.spec.gsub(/@.*$/, '').split('.') : addr.name.split
301 316 user.firstname = names.shift
302 317 user.lastname = names.join(' ')
303 318 user.lastname = '-' if user.lastname.blank?
304 319
305 320 user.login = user.mail
306 321 user.password = ActiveSupport::SecureRandom.hex(5)
307 322 user.language = Setting.default_language
308 323 user.save ? user : nil
309 324 end
310 325 end
311 326
312 327 private
313 328
314 329 # Removes the email body of text after the truncation configurations.
315 330 def cleanup_body(body)
316 331 delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
317 332 unless delimiters.empty?
318 333 regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
319 334 body = body.gsub(regex, '')
320 335 end
321 336 body.strip
322 337 end
323 338
324 339 def find_user_from_keyword(keyword)
325 340 user ||= User.find_by_mail(keyword)
326 341 user ||= User.find_by_login(keyword)
327 342 if user.nil? && keyword.match(/ /)
328 343 firstname, lastname = *(keyword.split) # "First Last Throwaway"
329 344 user ||= User.find_by_firstname_and_lastname(firstname, lastname)
330 345 end
331 346 user
332 347 end
333 348 end
@@ -1,410 +1,425
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
232 def test_add_issue_with_localized_attributes
233 User.find_by_mail('jsmith@somenet.foo').update_attribute 'language', 'fr'
234 issue = submit_email('ticket_with_localized_attributes.eml', :allow_override => 'tracker,category,priority')
235 assert issue.is_a?(Issue)
236 assert !issue.new_record?
237 issue.reload
238 assert_equal 'New ticket on a given project', issue.subject
239 assert_equal User.find_by_login('jsmith'), issue.author
240 assert_equal Project.find(2), issue.project
241 assert_equal 'Feature request', issue.tracker.to_s
242 assert_equal 'Stock management', issue.category.to_s
243 assert_equal 'Urgent', issue.priority.to_s
244 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
245 end
231 246
232 247 def test_add_issue_with_japanese_keywords
233 248 tracker = Tracker.create!(:name => 'ι–‹η™Ί')
234 249 Project.find(1).trackers << tracker
235 250 issue = submit_email('japanese_keywords_iso_2022_jp.eml', :issue => {:project => 'ecookbook'}, :allow_override => 'tracker')
236 251 assert_kind_of Issue, issue
237 252 assert_equal tracker, issue.tracker
238 253 end
239 254
240 255 def test_should_ignore_emails_from_emission_address
241 256 Role.anonymous.add_permission!(:add_issues)
242 257 assert_no_difference 'User.count' do
243 258 assert_equal false, submit_email('ticket_from_emission_address.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'create')
244 259 end
245 260 end
246 261
247 262 def test_add_issue_should_send_email_notification
248 263 Setting.notified_events = ['issue_added']
249 264 ActionMailer::Base.deliveries.clear
250 265 # This email contains: 'Project: onlinestore'
251 266 issue = submit_email('ticket_on_given_project.eml')
252 267 assert issue.is_a?(Issue)
253 268 assert_equal 1, ActionMailer::Base.deliveries.size
254 269 end
255 270
256 271 def test_add_issue_note
257 272 journal = submit_email('ticket_reply.eml')
258 273 assert journal.is_a?(Journal)
259 274 assert_equal User.find_by_login('jsmith'), journal.user
260 275 assert_equal Issue.find(2), journal.journalized
261 276 assert_match /This is reply/, journal.notes
262 277 end
263 278
264 279 def test_add_issue_note_with_attribute_changes
265 280 # This email contains: 'Status: Resolved'
266 281 journal = submit_email('ticket_reply_with_status.eml')
267 282 assert journal.is_a?(Journal)
268 283 issue = Issue.find(journal.issue.id)
269 284 assert_equal User.find_by_login('jsmith'), journal.user
270 285 assert_equal Issue.find(2), journal.journalized
271 286 assert_match /This is reply/, journal.notes
272 287 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
273 288 assert_equal '2010-01-01', issue.start_date.to_s
274 289 assert_equal '2010-12-31', issue.due_date.to_s
275 290 assert_equal User.find_by_login('jsmith'), issue.assigned_to
276 291 assert_equal 'Updated custom value', issue.custom_value_for(CustomField.find_by_name('Searchable field')).value
277 292 end
278 293
279 294 def test_add_issue_note_should_send_email_notification
280 295 ActionMailer::Base.deliveries.clear
281 296 journal = submit_email('ticket_reply.eml')
282 297 assert journal.is_a?(Journal)
283 298 assert_equal 1, ActionMailer::Base.deliveries.size
284 299 end
285 300
286 301 def test_reply_to_a_message
287 302 m = submit_email('message_reply.eml')
288 303 assert m.is_a?(Message)
289 304 assert !m.new_record?
290 305 m.reload
291 306 assert_equal 'Reply via email', m.subject
292 307 # The email replies to message #2 which is part of the thread of message #1
293 308 assert_equal Message.find(1), m.parent
294 309 end
295 310
296 311 def test_reply_to_a_message_by_subject
297 312 m = submit_email('message_reply_by_subject.eml')
298 313 assert m.is_a?(Message)
299 314 assert !m.new_record?
300 315 m.reload
301 316 assert_equal 'Reply to the first post', m.subject
302 317 assert_equal Message.find(1), m.parent
303 318 end
304 319
305 320 def test_should_strip_tags_of_html_only_emails
306 321 issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'})
307 322 assert issue.is_a?(Issue)
308 323 assert !issue.new_record?
309 324 issue.reload
310 325 assert_equal 'HTML email', issue.subject
311 326 assert_equal 'This is a html-only email.', issue.description
312 327 end
313 328
314 329 context "truncate emails based on the Setting" do
315 330 context "with no setting" do
316 331 setup do
317 332 Setting.mail_handler_body_delimiters = ''
318 333 end
319 334
320 335 should "add the entire email into the issue" do
321 336 issue = submit_email('ticket_on_given_project.eml')
322 337 assert_issue_created(issue)
323 338 assert issue.description.include?('---')
324 339 assert issue.description.include?('This paragraph is after the delimiter')
325 340 end
326 341 end
327 342
328 343 context "with a single string" do
329 344 setup do
330 345 Setting.mail_handler_body_delimiters = '---'
331 346 end
332 347
333 348 should "truncate the email at the delimiter for the issue" do
334 349 issue = submit_email('ticket_on_given_project.eml')
335 350 assert_issue_created(issue)
336 351 assert issue.description.include?('This paragraph is before delimiters')
337 352 assert issue.description.include?('--- This line starts with a delimiter')
338 353 assert !issue.description.match(/^---$/)
339 354 assert !issue.description.include?('This paragraph is after the delimiter')
340 355 end
341 356 end
342 357
343 358 context "with a single quoted reply (e.g. reply to a Redmine email notification)" do
344 359 setup do
345 360 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
346 361 end
347 362
348 363 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
349 364 journal = submit_email('issue_update_with_quoted_reply_above.eml')
350 365 assert journal.is_a?(Journal)
351 366 assert journal.notes.include?('An update to the issue by the sender.')
352 367 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
353 368 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
354 369
355 370 end
356 371
357 372 end
358 373
359 374 context "with multiple quoted replies (e.g. reply to a reply of a Redmine email notification)" do
360 375 setup do
361 376 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
362 377 end
363 378
364 379 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
365 380 journal = submit_email('issue_update_with_multiple_quoted_reply_above.eml')
366 381 assert journal.is_a?(Journal)
367 382 assert journal.notes.include?('An update to the issue by the sender.')
368 383 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
369 384 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
370 385
371 386 end
372 387
373 388 end
374 389
375 390 context "with multiple strings" do
376 391 setup do
377 392 Setting.mail_handler_body_delimiters = "---\nBREAK"
378 393 end
379 394
380 395 should "truncate the email at the first delimiter found (BREAK)" do
381 396 issue = submit_email('ticket_on_given_project.eml')
382 397 assert_issue_created(issue)
383 398 assert issue.description.include?('This paragraph is before delimiters')
384 399 assert !issue.description.include?('BREAK')
385 400 assert !issue.description.include?('This paragraph is between delimiters')
386 401 assert !issue.description.match(/^---$/)
387 402 assert !issue.description.include?('This paragraph is after the delimiter')
388 403 end
389 404 end
390 405 end
391 406
392 407 def test_email_with_long_subject_line
393 408 issue = submit_email('ticket_with_long_subject.eml')
394 409 assert issue.is_a?(Issue)
395 410 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]
396 411 end
397 412
398 413 private
399 414
400 415 def submit_email(filename, options={})
401 416 raw = IO.read(File.join(FIXTURES_PATH, filename))
402 417 MailHandler.receive(raw, options)
403 418 end
404 419
405 420 def assert_issue_created(issue)
406 421 assert issue.is_a?(Issue)
407 422 assert !issue.new_record?
408 423 issue.reload
409 424 end
410 425 end
General Comments 0
You need to be logged in to leave comments. Login now