##// END OF EJS Templates
Fixed: attachments from Apple Mail not added by mail handler (#8651)....
Jean-Philippe Lang -
r6204:4f4b3594db84
parent child
Show More
@@ -0,0 +1,240
1 From JSmith@somenet.foo Mon Jun 27 06:55:56 2011
2 Return-Path: <JSmith@somenet.foo>
3 X-Original-To: redmine@somenet.foo
4 Delivered-To: redmine@somenet.foo
5 From: John Smith <JSmith@somenet.foo>
6 Mime-Version: 1.0 (Apple Message framework v1084)
7 Content-Type: multipart/alternative; boundary=Apple-Mail-3-163265085
8 Subject: Test attaching images to tickets by HTML mail
9 Date: Mon, 27 Jun 2011 16:55:46 +0300
10 To: redmine@somenet.foo
11 Message-Id: <7ABE3636-07E8-47C9-90A1-FCB1AA894DA1@somenet.foo>
12 X-Mailer: Apple Mail (2.1084)
13
14
15 --Apple-Mail-3-163265085
16 Content-Transfer-Encoding: quoted-printable
17 Content-Type: text/plain;
18 charset=us-ascii
19
20 Yet another test!
21
22
23 --Apple-Mail-3-163265085
24 Content-Type: multipart/related;
25 type="text/html";
26 boundary=Apple-Mail-4-163265085
27
28
29 --Apple-Mail-4-163265085
30 Content-Transfer-Encoding: quoted-printable
31 Content-Type: text/html;
32 charset=us-ascii
33
34 <html><head></head><body style=3D"word-wrap: break-word; =
35 -webkit-nbsp-mode: space; -webkit-line-break: after-white-space; ">
36 <br></body></html>=
37
38 --Apple-Mail-4-163265085
39 Content-Transfer-Encoding: base64
40 Content-Disposition: inline;
41 filename=paella.jpg
42 Content-Type: image/jpg;
43 x-unix-mode=0644;
44 name="paella.jpg"
45 Content-Id: <1207F0B5-9F9D-4AB4-B547-AF9033E82111>
46
47 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
236 cbis+/WpUqUcMZKdF44n/9k=
237
238 --Apple-Mail-4-163265085--
239
240 --Apple-Mail-3-163265085--
@@ -1,370 +1,370
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class MailHandler < ActionMailer::Base
19 19 include ActionView::Helpers::SanitizeHelper
20 20 include Redmine::I18n
21 21
22 22 class UnauthorizedAction < StandardError; end
23 23 class MissingInformation < StandardError; end
24 24
25 25 attr_reader :email, :user
26 26
27 27 def self.receive(email, options={})
28 28 @@handler_options = options.dup
29 29
30 30 @@handler_options[:issue] ||= {}
31 31
32 32 @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip) if @@handler_options[:allow_override].is_a?(String)
33 33 @@handler_options[:allow_override] ||= []
34 34 # Project needs to be overridable if not specified
35 35 @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
36 36 # Status overridable by default
37 37 @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
38 38
39 39 @@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1' ? true : false)
40 40 super email
41 41 end
42 42
43 43 # Processes incoming emails
44 44 # Returns the created object (eg. an issue, a message) or false
45 45 def receive(email)
46 46 @email = email
47 47 sender_email = email.from.to_a.first.to_s.strip
48 48 # Ignore emails received from the application emission address to avoid hell cycles
49 49 if sender_email.downcase == Setting.mail_from.to_s.strip.downcase
50 50 logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]" if logger && logger.info
51 51 return false
52 52 end
53 53 @user = User.find_by_mail(sender_email) if sender_email.present?
54 54 if @user && !@user.active?
55 55 logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]" if logger && logger.info
56 56 return false
57 57 end
58 58 if @user.nil?
59 59 # Email was submitted by an unknown user
60 60 case @@handler_options[:unknown_user]
61 61 when 'accept'
62 62 @user = User.anonymous
63 63 when 'create'
64 64 @user = MailHandler.create_user_from_email(email)
65 65 if @user
66 66 logger.info "MailHandler: [#{@user.login}] account created" if logger && logger.info
67 67 Mailer.deliver_account_information(@user, @user.password)
68 68 else
69 69 logger.error "MailHandler: could not create account for [#{sender_email}]" if logger && logger.error
70 70 return false
71 71 end
72 72 else
73 73 # Default behaviour, emails from unknown users are ignored
74 74 logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]" if logger && logger.info
75 75 return false
76 76 end
77 77 end
78 78 User.current = @user
79 79 dispatch
80 80 end
81 81
82 82 private
83 83
84 84 MESSAGE_ID_RE = %r{^<redmine\.([a-z0-9_]+)\-(\d+)\.\d+@}
85 85 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]*#(\d+)\]}
86 86 MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
87 87
88 88 def dispatch
89 89 headers = [email.in_reply_to, email.references].flatten.compact
90 90 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
91 91 klass, object_id = $1, $2.to_i
92 92 method_name = "receive_#{klass}_reply"
93 93 if self.class.private_instance_methods.collect(&:to_s).include?(method_name)
94 94 send method_name, object_id
95 95 else
96 96 # ignoring it
97 97 end
98 98 elsif m = email.subject.match(ISSUE_REPLY_SUBJECT_RE)
99 99 receive_issue_reply(m[1].to_i)
100 100 elsif m = email.subject.match(MESSAGE_REPLY_SUBJECT_RE)
101 101 receive_message_reply(m[1].to_i)
102 102 else
103 103 dispatch_to_default
104 104 end
105 105 rescue ActiveRecord::RecordInvalid => e
106 106 # TODO: send a email to the user
107 107 logger.error e.message if logger
108 108 false
109 109 rescue MissingInformation => e
110 110 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
111 111 false
112 112 rescue UnauthorizedAction => e
113 113 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
114 114 false
115 115 end
116 116
117 117 def dispatch_to_default
118 118 receive_issue
119 119 end
120 120
121 121 # Creates a new issue
122 122 def receive_issue
123 123 project = target_project
124 124 # check permission
125 125 unless @@handler_options[:no_permission_check]
126 126 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
127 127 end
128 128
129 129 issue = Issue.new(:author => user, :project => project)
130 130 issue.safe_attributes = issue_attributes_from_keywords(issue)
131 131 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
132 132 issue.subject = email.subject.to_s.chomp[0,255]
133 133 if issue.subject.blank?
134 134 issue.subject = '(no subject)'
135 135 end
136 136 issue.description = cleaned_up_text_body
137 137
138 138 # add To and Cc as watchers before saving so the watchers can reply to Redmine
139 139 add_watchers(issue)
140 140 issue.save!
141 141 add_attachments(issue)
142 142 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
143 143 issue
144 144 end
145 145
146 146 # Adds a note to an existing issue
147 147 def receive_issue_reply(issue_id)
148 148 issue = Issue.find_by_id(issue_id)
149 149 return unless issue
150 150 # check permission
151 151 unless @@handler_options[:no_permission_check]
152 152 raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project)
153 153 end
154 154
155 155 # ignore CLI-supplied defaults for new issues
156 156 @@handler_options[:issue].clear
157 157
158 158 journal = issue.init_journal(user)
159 159 issue.safe_attributes = issue_attributes_from_keywords(issue)
160 160 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
161 161 journal.notes = cleaned_up_text_body
162 162 add_attachments(issue)
163 163 issue.save!
164 164 logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info
165 165 journal
166 166 end
167 167
168 168 # Reply will be added to the issue
169 169 def receive_journal_reply(journal_id)
170 170 journal = Journal.find_by_id(journal_id)
171 171 if journal && journal.journalized_type == 'Issue'
172 172 receive_issue_reply(journal.journalized_id)
173 173 end
174 174 end
175 175
176 176 # Receives a reply to a forum message
177 177 def receive_message_reply(message_id)
178 178 message = Message.find_by_id(message_id)
179 179 if message
180 180 message = message.root
181 181
182 182 unless @@handler_options[:no_permission_check]
183 183 raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
184 184 end
185 185
186 186 if !message.locked?
187 187 reply = Message.new(:subject => email.subject.gsub(%r{^.*msg\d+\]}, '').strip,
188 188 :content => cleaned_up_text_body)
189 189 reply.author = user
190 190 reply.board = message.board
191 191 message.children << reply
192 192 add_attachments(reply)
193 193 reply
194 194 else
195 195 logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic" if logger && logger.info
196 196 end
197 197 end
198 198 end
199 199
200 200 def add_attachments(obj)
201 if email.has_attachments?
201 if email.attachments.any?
202 202 email.attachments.each do |attachment|
203 203 obj.attachments << Attachment.create(:container => obj,
204 204 :file => attachment,
205 205 :author => user,
206 206 :content_type => attachment.content_type)
207 207 end
208 208 end
209 209 end
210 210
211 211 # Adds To and Cc as watchers of the given object if the sender has the
212 212 # appropriate permission
213 213 def add_watchers(obj)
214 214 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
215 215 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
216 216 unless addresses.empty?
217 217 watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
218 218 watchers.each {|w| obj.add_watcher(w)}
219 219 end
220 220 end
221 221 end
222 222
223 223 def get_keyword(attr, options={})
224 224 @keywords ||= {}
225 225 if @keywords.has_key?(attr)
226 226 @keywords[attr]
227 227 else
228 228 @keywords[attr] = begin
229 229 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && (v = extract_keyword!(plain_text_body, attr, options[:format]))
230 230 v
231 231 elsif !@@handler_options[:issue][attr].blank?
232 232 @@handler_options[:issue][attr]
233 233 end
234 234 end
235 235 end
236 236 end
237 237
238 238 # Destructively extracts the value for +attr+ in +text+
239 239 # Returns nil if no matching keyword found
240 240 def extract_keyword!(text, attr, format=nil)
241 241 keys = [attr.to_s.humanize]
242 242 if attr.is_a?(Symbol)
243 243 keys << l("field_#{attr}", :default => '', :locale => user.language) if user && user.language.present?
244 244 keys << l("field_#{attr}", :default => '', :locale => Setting.default_language) if Setting.default_language.present?
245 245 end
246 246 keys.reject! {|k| k.blank?}
247 247 keys.collect! {|k| Regexp.escape(k)}
248 248 format ||= '.+'
249 249 text.gsub!(/^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i, '')
250 250 $2 && $2.strip
251 251 end
252 252
253 253 def target_project
254 254 # TODO: other ways to specify project:
255 255 # * parse the email To field
256 256 # * specific project (eg. Setting.mail_handler_target_project)
257 257 target = Project.find_by_identifier(get_keyword(:project))
258 258 raise MissingInformation.new('Unable to determine target project') if target.nil?
259 259 target
260 260 end
261 261
262 262 # Returns a Hash of issue attributes extracted from keywords in the email body
263 263 def issue_attributes_from_keywords(issue)
264 264 assigned_to = (k = get_keyword(:assigned_to, :override => true)) && find_assignee_from_keyword(k, issue)
265 265
266 266 attrs = {
267 267 'tracker_id' => (k = get_keyword(:tracker)) && issue.project.trackers.named(k).first.try(:id),
268 268 'status_id' => (k = get_keyword(:status)) && IssueStatus.named(k).first.try(:id),
269 269 'priority_id' => (k = get_keyword(:priority)) && IssuePriority.named(k).first.try(:id),
270 270 'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.named(k).first.try(:id),
271 271 'assigned_to_id' => assigned_to.try(:id),
272 272 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) && issue.project.shared_versions.named(k).first.try(:id),
273 273 'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
274 274 'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
275 275 'estimated_hours' => get_keyword(:estimated_hours, :override => true),
276 276 'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0')
277 277 }.delete_if {|k, v| v.blank? }
278 278
279 279 if issue.new_record? && attrs['tracker_id'].nil?
280 280 attrs['tracker_id'] = issue.project.trackers.find(:first).try(:id)
281 281 end
282 282
283 283 attrs
284 284 end
285 285
286 286 # Returns a Hash of issue custom field values extracted from keywords in the email body
287 287 def custom_field_values_from_keywords(customized)
288 288 customized.custom_field_values.inject({}) do |h, v|
289 289 if value = get_keyword(v.custom_field.name, :override => true)
290 290 h[v.custom_field.id.to_s] = value
291 291 end
292 292 h
293 293 end
294 294 end
295 295
296 296 # Returns the text/plain part of the email
297 297 # If not found (eg. HTML-only email), returns the body with tags removed
298 298 def plain_text_body
299 299 return @plain_text_body unless @plain_text_body.nil?
300 300 parts = @email.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten
301 301 if parts.empty?
302 302 parts << @email
303 303 end
304 304 plain_text_part = parts.detect {|p| p.content_type == 'text/plain'}
305 305 if plain_text_part.nil?
306 306 # no text/plain part found, assuming html-only email
307 307 # strip html tags and remove doctype directive
308 308 @plain_text_body = strip_tags(@email.body.to_s)
309 309 @plain_text_body.gsub! %r{^<!DOCTYPE .*$}, ''
310 310 else
311 311 @plain_text_body = plain_text_part.body.to_s
312 312 end
313 313 @plain_text_body.strip!
314 314 @plain_text_body
315 315 end
316 316
317 317 def cleaned_up_text_body
318 318 cleanup_body(plain_text_body)
319 319 end
320 320
321 321 def self.full_sanitizer
322 322 @full_sanitizer ||= HTML::FullSanitizer.new
323 323 end
324 324
325 325 # Creates a user account for the +email+ sender
326 326 def self.create_user_from_email(email)
327 327 addr = email.from_addrs.to_a.first
328 328 if addr && !addr.spec.blank?
329 329 user = User.new
330 330 user.mail = addr.spec
331 331
332 332 names = addr.name.blank? ? addr.spec.gsub(/@.*$/, '').split('.') : addr.name.split
333 333 user.firstname = names.shift
334 334 user.lastname = names.join(' ')
335 335 user.lastname = '-' if user.lastname.blank?
336 336
337 337 user.login = user.mail
338 338 user.password = ActiveSupport::SecureRandom.hex(5)
339 339 user.language = Setting.default_language
340 340 user.save ? user : nil
341 341 end
342 342 end
343 343
344 344 private
345 345
346 346 # Removes the email body of text after the truncation configurations.
347 347 def cleanup_body(body)
348 348 delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
349 349 unless delimiters.empty?
350 350 regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
351 351 body = body.gsub(regex, '')
352 352 end
353 353 body.strip
354 354 end
355 355
356 356 def find_assignee_from_keyword(keyword, issue)
357 357 keyword = keyword.to_s.downcase
358 358 assignable = issue.assignable_users
359 359 assignee = nil
360 360 assignee ||= assignable.detect {|a| a.mail.to_s.downcase == keyword || a.login.to_s.downcase == keyword}
361 361 if assignee.nil? && keyword.match(/ /)
362 362 firstname, lastname = *(keyword.split) # "First Last Throwaway"
363 363 assignee ||= assignable.detect {|a| a.is_a?(User) && a.firstname.to_s.downcase == firstname && a.lastname.to_s.downcase == lastname}
364 364 end
365 365 if assignee.nil?
366 366 assignee ||= assignable.detect {|a| a.is_a?(Group) && a.name.downcase == keyword}
367 367 end
368 368 assignee
369 369 end
370 370 end
@@ -1,491 +1,501
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2011 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 require File.expand_path('../../test_helper', __FILE__)
21 21
22 22 class MailHandlerTest < ActiveSupport::TestCase
23 23 fixtures :users, :projects,
24 24 :enabled_modules,
25 25 :roles,
26 26 :members,
27 27 :member_roles,
28 28 :users,
29 29 :issues,
30 30 :issue_statuses,
31 31 :workflows,
32 32 :trackers,
33 33 :projects_trackers,
34 34 :versions,
35 35 :enumerations,
36 36 :issue_categories,
37 37 :custom_fields,
38 38 :custom_fields_trackers,
39 39 :custom_fields_projects,
40 40 :boards,
41 41 :messages
42 42
43 43 FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler'
44 44
45 45 def setup
46 46 ActionMailer::Base.deliveries.clear
47 47 Setting.notified_events = Redmine::Notifiable.all.collect(&:name)
48 48 end
49 49
50 50 def test_add_issue
51 51 ActionMailer::Base.deliveries.clear
52 52 # This email contains: 'Project: onlinestore'
53 53 issue = submit_email('ticket_on_given_project.eml')
54 54 assert issue.is_a?(Issue)
55 55 assert !issue.new_record?
56 56 issue.reload
57 57 assert_equal Project.find(2), issue.project
58 58 assert_equal issue.project.trackers.first, issue.tracker
59 59 assert_equal 'New ticket on a given project', issue.subject
60 60 assert_equal User.find_by_login('jsmith'), issue.author
61 61 assert_equal IssueStatus.find_by_name('Resolved'), issue.status
62 62 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
63 63 assert_equal '2010-01-01', issue.start_date.to_s
64 64 assert_equal '2010-12-31', issue.due_date.to_s
65 65 assert_equal User.find_by_login('jsmith'), issue.assigned_to
66 66 assert_equal Version.find_by_name('Alpha'), issue.fixed_version
67 67 assert_equal 2.5, issue.estimated_hours
68 68 assert_equal 30, issue.done_ratio
69 69 assert_equal [issue.id, 1, 2], [issue.root_id, issue.lft, issue.rgt]
70 70 # keywords should be removed from the email body
71 71 assert !issue.description.match(/^Project:/i)
72 72 assert !issue.description.match(/^Status:/i)
73 73 assert !issue.description.match(/^Start Date:/i)
74 74 # Email notification should be sent
75 75 mail = ActionMailer::Base.deliveries.last
76 76 assert_not_nil mail
77 77 assert mail.subject.include?('New ticket on a given project')
78 78 end
79 79
80 80 def test_add_issue_with_default_tracker
81 81 # This email contains: 'Project: onlinestore'
82 82 issue = submit_email('ticket_on_given_project.eml', :issue => {:tracker => 'Support request'})
83 83 assert issue.is_a?(Issue)
84 84 assert !issue.new_record?
85 85 issue.reload
86 86 assert_equal 'Support request', issue.tracker.name
87 87 end
88 88
89 89 def test_add_issue_with_status
90 90 # This email contains: 'Project: onlinestore' and 'Status: Resolved'
91 91 issue = submit_email('ticket_on_given_project.eml')
92 92 assert issue.is_a?(Issue)
93 93 assert !issue.new_record?
94 94 issue.reload
95 95 assert_equal Project.find(2), issue.project
96 96 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
97 97 end
98 98
99 99 def test_add_issue_with_attributes_override
100 100 issue = submit_email('ticket_with_attributes.eml', :allow_override => 'tracker,category,priority')
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_equal 'Stock management', issue.category.to_s
109 109 assert_equal 'Urgent', 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_group_assignment
114 114 with_settings :issue_group_assignment => '1' do
115 115 issue = submit_email('ticket_on_given_project.eml') do |email|
116 116 email.gsub!('Assigned to: John Smith', 'Assigned to: B Team')
117 117 end
118 118 assert issue.is_a?(Issue)
119 119 assert !issue.new_record?
120 120 issue.reload
121 121 assert_equal Group.find(11), issue.assigned_to
122 122 end
123 123 end
124 124
125 125 def test_add_issue_with_partial_attributes_override
126 126 issue = submit_email('ticket_with_attributes.eml', :issue => {:priority => 'High'}, :allow_override => ['tracker'])
127 127 assert issue.is_a?(Issue)
128 128 assert !issue.new_record?
129 129 issue.reload
130 130 assert_equal 'New ticket on a given project', issue.subject
131 131 assert_equal User.find_by_login('jsmith'), issue.author
132 132 assert_equal Project.find(2), issue.project
133 133 assert_equal 'Feature request', issue.tracker.to_s
134 134 assert_nil issue.category
135 135 assert_equal 'High', issue.priority.to_s
136 136 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
137 137 end
138 138
139 139 def test_add_issue_with_spaces_between_attribute_and_separator
140 140 issue = submit_email('ticket_with_spaces_between_attribute_and_separator.eml', :allow_override => 'tracker,category,priority')
141 141 assert issue.is_a?(Issue)
142 142 assert !issue.new_record?
143 143 issue.reload
144 144 assert_equal 'New ticket on a given project', issue.subject
145 145 assert_equal User.find_by_login('jsmith'), issue.author
146 146 assert_equal Project.find(2), issue.project
147 147 assert_equal 'Feature request', issue.tracker.to_s
148 148 assert_equal 'Stock management', issue.category.to_s
149 149 assert_equal 'Urgent', issue.priority.to_s
150 150 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
151 151 end
152 152
153 153 def test_add_issue_with_attachment_to_specific_project
154 154 issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
155 155 assert issue.is_a?(Issue)
156 156 assert !issue.new_record?
157 157 issue.reload
158 158 assert_equal 'Ticket created by email with attachment', issue.subject
159 159 assert_equal User.find_by_login('jsmith'), issue.author
160 160 assert_equal Project.find(2), issue.project
161 161 assert_equal 'This is a new ticket with attachments', issue.description
162 162 # Attachment properties
163 163 assert_equal 1, issue.attachments.size
164 164 assert_equal 'Paella.jpg', issue.attachments.first.filename
165 165 assert_equal 'image/jpeg', issue.attachments.first.content_type
166 166 assert_equal 10790, issue.attachments.first.filesize
167 167 end
168 168
169 169 def test_add_issue_with_custom_fields
170 170 issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'onlinestore'})
171 171 assert issue.is_a?(Issue)
172 172 assert !issue.new_record?
173 173 issue.reload
174 174 assert_equal 'New ticket with custom field values', issue.subject
175 175 assert_equal 'Value for a custom field', issue.custom_value_for(CustomField.find_by_name('Searchable field')).value
176 176 assert !issue.description.match(/^searchable field:/i)
177 177 end
178 178
179 179 def test_add_issue_with_cc
180 180 issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'})
181 181 assert issue.is_a?(Issue)
182 182 assert !issue.new_record?
183 183 issue.reload
184 184 assert issue.watched_by?(User.find_by_mail('dlopper@somenet.foo'))
185 185 assert_equal 1, issue.watcher_user_ids.size
186 186 end
187 187
188 188 def test_add_issue_by_unknown_user
189 189 assert_no_difference 'User.count' do
190 190 assert_equal false, submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'})
191 191 end
192 192 end
193 193
194 194 def test_add_issue_by_anonymous_user
195 195 Role.anonymous.add_permission!(:add_issues)
196 196 assert_no_difference 'User.count' do
197 197 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'accept')
198 198 assert issue.is_a?(Issue)
199 199 assert issue.author.anonymous?
200 200 end
201 201 end
202 202
203 203 def test_add_issue_by_anonymous_user_with_no_from_address
204 204 Role.anonymous.add_permission!(:add_issues)
205 205 assert_no_difference 'User.count' do
206 206 issue = submit_email('ticket_by_empty_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'accept')
207 207 assert issue.is_a?(Issue)
208 208 assert issue.author.anonymous?
209 209 end
210 210 end
211 211
212 212 def test_add_issue_by_anonymous_user_on_private_project
213 213 Role.anonymous.add_permission!(:add_issues)
214 214 assert_no_difference 'User.count' do
215 215 assert_no_difference 'Issue.count' do
216 216 assert_equal false, submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'onlinestore'}, :unknown_user => 'accept')
217 217 end
218 218 end
219 219 end
220 220
221 221 def test_add_issue_by_anonymous_user_on_private_project_without_permission_check
222 222 assert_no_difference 'User.count' do
223 223 assert_difference 'Issue.count' do
224 224 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'onlinestore'}, :no_permission_check => '1', :unknown_user => 'accept')
225 225 assert issue.is_a?(Issue)
226 226 assert issue.author.anonymous?
227 227 assert !issue.project.is_public?
228 228 assert_equal [issue.id, 1, 2], [issue.root_id, issue.lft, issue.rgt]
229 229 end
230 230 end
231 231 end
232 232
233 233 def test_add_issue_by_created_user
234 234 Setting.default_language = 'en'
235 235 assert_difference 'User.count' do
236 236 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'create')
237 237 assert issue.is_a?(Issue)
238 238 assert issue.author.active?
239 239 assert_equal 'john.doe@somenet.foo', issue.author.mail
240 240 assert_equal 'John', issue.author.firstname
241 241 assert_equal 'Doe', issue.author.lastname
242 242
243 243 # account information
244 244 email = ActionMailer::Base.deliveries.first
245 245 assert_not_nil email
246 246 assert email.subject.include?('account activation')
247 247 login = email.body.match(/\* Login: (.*)$/)[1]
248 248 password = email.body.match(/\* Password: (.*)$/)[1]
249 249 assert_equal issue.author, User.try_to_login(login, password)
250 250 end
251 251 end
252 252
253 253 def test_add_issue_without_from_header
254 254 Role.anonymous.add_permission!(:add_issues)
255 255 assert_equal false, submit_email('ticket_without_from_header.eml')
256 256 end
257 257
258 258 def test_add_issue_with_invalid_attributes
259 259 issue = submit_email('ticket_with_invalid_attributes.eml', :allow_override => 'tracker,category,priority')
260 260 assert issue.is_a?(Issue)
261 261 assert !issue.new_record?
262 262 issue.reload
263 263 assert_nil issue.assigned_to
264 264 assert_nil issue.start_date
265 265 assert_nil issue.due_date
266 266 assert_equal 0, issue.done_ratio
267 267 assert_equal 'Normal', issue.priority.to_s
268 268 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
269 269 end
270 270
271 271 def test_add_issue_with_localized_attributes
272 272 User.find_by_mail('jsmith@somenet.foo').update_attribute 'language', 'fr'
273 273 issue = submit_email('ticket_with_localized_attributes.eml', :allow_override => 'tracker,category,priority')
274 274 assert issue.is_a?(Issue)
275 275 assert !issue.new_record?
276 276 issue.reload
277 277 assert_equal 'New ticket on a given project', issue.subject
278 278 assert_equal User.find_by_login('jsmith'), issue.author
279 279 assert_equal Project.find(2), issue.project
280 280 assert_equal 'Feature request', issue.tracker.to_s
281 281 assert_equal 'Stock management', issue.category.to_s
282 282 assert_equal 'Urgent', issue.priority.to_s
283 283 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
284 284 end
285 285
286 286 def test_add_issue_with_japanese_keywords
287 287 tracker = Tracker.create!(:name => 'ι–‹η™Ί')
288 288 Project.find(1).trackers << tracker
289 289 issue = submit_email('japanese_keywords_iso_2022_jp.eml', :issue => {:project => 'ecookbook'}, :allow_override => 'tracker')
290 290 assert_kind_of Issue, issue
291 291 assert_equal tracker, issue.tracker
292 292 end
293
294 def test_add_issue_from_apple_mail
295 issue = submit_email('apple_mail_with_attachment.eml', :issue => {:project => 'ecookbook'})
296 assert_kind_of Issue, issue
297 assert_equal 1, issue.attachments.size
298
299 attachment = issue.attachments.first
300 assert_equal 'paella.jpg', attachment.filename
301 assert_equal 10790, attachment.filesize
302 end
293 303
294 304 def test_should_ignore_emails_from_emission_address
295 305 Role.anonymous.add_permission!(:add_issues)
296 306 assert_no_difference 'User.count' do
297 307 assert_equal false, submit_email('ticket_from_emission_address.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'create')
298 308 end
299 309 end
300 310
301 311 def test_add_issue_should_send_email_notification
302 312 Setting.notified_events = ['issue_added']
303 313 ActionMailer::Base.deliveries.clear
304 314 # This email contains: 'Project: onlinestore'
305 315 issue = submit_email('ticket_on_given_project.eml')
306 316 assert issue.is_a?(Issue)
307 317 assert_equal 1, ActionMailer::Base.deliveries.size
308 318 end
309 319
310 320 def test_update_issue
311 321 journal = submit_email('ticket_reply.eml')
312 322 assert journal.is_a?(Journal)
313 323 assert_equal User.find_by_login('jsmith'), journal.user
314 324 assert_equal Issue.find(2), journal.journalized
315 325 assert_match /This is reply/, journal.notes
316 326 assert_equal 'Feature request', journal.issue.tracker.name
317 327 end
318 328
319 329 def test_update_issue_with_attribute_changes
320 330 # This email contains: 'Status: Resolved'
321 331 journal = submit_email('ticket_reply_with_status.eml')
322 332 assert journal.is_a?(Journal)
323 333 issue = Issue.find(journal.issue.id)
324 334 assert_equal User.find_by_login('jsmith'), journal.user
325 335 assert_equal Issue.find(2), journal.journalized
326 336 assert_match /This is reply/, journal.notes
327 337 assert_equal 'Feature request', journal.issue.tracker.name
328 338 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
329 339 assert_equal '2010-01-01', issue.start_date.to_s
330 340 assert_equal '2010-12-31', issue.due_date.to_s
331 341 assert_equal User.find_by_login('jsmith'), issue.assigned_to
332 342 assert_equal "52.6", issue.custom_value_for(CustomField.find_by_name('Float field')).value
333 343 # keywords should be removed from the email body
334 344 assert !journal.notes.match(/^Status:/i)
335 345 assert !journal.notes.match(/^Start Date:/i)
336 346 end
337 347
338 348 def test_update_issue_with_attachment
339 349 assert_difference 'Journal.count' do
340 350 assert_difference 'JournalDetail.count' do
341 351 assert_difference 'Attachment.count' do
342 352 assert_no_difference 'Issue.count' do
343 353 journal = submit_email('ticket_with_attachment.eml') do |raw|
344 354 raw.gsub! /^Subject: .*$/, 'Subject: Re: [Cookbook - Feature #2] (New) Add ingredients categories'
345 355 end
346 356 end
347 357 end
348 358 end
349 359 end
350 360 journal = Journal.first(:order => 'id DESC')
351 361 assert_equal Issue.find(2), journal.journalized
352 362 assert_equal 1, journal.details.size
353 363
354 364 detail = journal.details.first
355 365 assert_equal 'attachment', detail.property
356 366 assert_equal 'Paella.jpg', detail.value
357 367 end
358 368
359 369 def test_update_issue_should_send_email_notification
360 370 ActionMailer::Base.deliveries.clear
361 371 journal = submit_email('ticket_reply.eml')
362 372 assert journal.is_a?(Journal)
363 373 assert_equal 1, ActionMailer::Base.deliveries.size
364 374 end
365 375
366 376 def test_update_issue_should_not_set_defaults
367 377 journal = submit_email('ticket_reply.eml', :issue => {:tracker => 'Support request', :priority => 'High'})
368 378 assert journal.is_a?(Journal)
369 379 assert_match /This is reply/, journal.notes
370 380 assert_equal 'Feature request', journal.issue.tracker.name
371 381 assert_equal 'Normal', journal.issue.priority.name
372 382 end
373 383
374 384 def test_reply_to_a_message
375 385 m = submit_email('message_reply.eml')
376 386 assert m.is_a?(Message)
377 387 assert !m.new_record?
378 388 m.reload
379 389 assert_equal 'Reply via email', m.subject
380 390 # The email replies to message #2 which is part of the thread of message #1
381 391 assert_equal Message.find(1), m.parent
382 392 end
383 393
384 394 def test_reply_to_a_message_by_subject
385 395 m = submit_email('message_reply_by_subject.eml')
386 396 assert m.is_a?(Message)
387 397 assert !m.new_record?
388 398 m.reload
389 399 assert_equal 'Reply to the first post', m.subject
390 400 assert_equal Message.find(1), m.parent
391 401 end
392 402
393 403 def test_should_strip_tags_of_html_only_emails
394 404 issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'})
395 405 assert issue.is_a?(Issue)
396 406 assert !issue.new_record?
397 407 issue.reload
398 408 assert_equal 'HTML email', issue.subject
399 409 assert_equal 'This is a html-only email.', issue.description
400 410 end
401 411
402 412 context "truncate emails based on the Setting" do
403 413 context "with no setting" do
404 414 setup do
405 415 Setting.mail_handler_body_delimiters = ''
406 416 end
407 417
408 418 should "add the entire email into the issue" do
409 419 issue = submit_email('ticket_on_given_project.eml')
410 420 assert_issue_created(issue)
411 421 assert issue.description.include?('---')
412 422 assert issue.description.include?('This paragraph is after the delimiter')
413 423 end
414 424 end
415 425
416 426 context "with a single string" do
417 427 setup do
418 428 Setting.mail_handler_body_delimiters = '---'
419 429 end
420 430 should "truncate the email at the delimiter for the issue" do
421 431 issue = submit_email('ticket_on_given_project.eml')
422 432 assert_issue_created(issue)
423 433 assert issue.description.include?('This paragraph is before delimiters')
424 434 assert issue.description.include?('--- This line starts with a delimiter')
425 435 assert !issue.description.match(/^---$/)
426 436 assert !issue.description.include?('This paragraph is after the delimiter')
427 437 end
428 438 end
429 439
430 440 context "with a single quoted reply (e.g. reply to a Redmine email notification)" do
431 441 setup do
432 442 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
433 443 end
434 444 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
435 445 journal = submit_email('issue_update_with_quoted_reply_above.eml')
436 446 assert journal.is_a?(Journal)
437 447 assert journal.notes.include?('An update to the issue by the sender.')
438 448 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
439 449 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
440 450 end
441 451 end
442 452
443 453 context "with multiple quoted replies (e.g. reply to a reply of a Redmine email notification)" do
444 454 setup do
445 455 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
446 456 end
447 457 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
448 458 journal = submit_email('issue_update_with_multiple_quoted_reply_above.eml')
449 459 assert journal.is_a?(Journal)
450 460 assert journal.notes.include?('An update to the issue by the sender.')
451 461 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
452 462 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
453 463 end
454 464 end
455 465
456 466 context "with multiple strings" do
457 467 setup do
458 468 Setting.mail_handler_body_delimiters = "---\nBREAK"
459 469 end
460 470 should "truncate the email at the first delimiter found (BREAK)" do
461 471 issue = submit_email('ticket_on_given_project.eml')
462 472 assert_issue_created(issue)
463 473 assert issue.description.include?('This paragraph is before delimiters')
464 474 assert !issue.description.include?('BREAK')
465 475 assert !issue.description.include?('This paragraph is between delimiters')
466 476 assert !issue.description.match(/^---$/)
467 477 assert !issue.description.include?('This paragraph is after the delimiter')
468 478 end
469 479 end
470 480 end
471 481
472 482 def test_email_with_long_subject_line
473 483 issue = submit_email('ticket_with_long_subject.eml')
474 484 assert issue.is_a?(Issue)
475 485 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]
476 486 end
477 487
478 488 private
479 489
480 490 def submit_email(filename, options={})
481 491 raw = IO.read(File.join(FIXTURES_PATH, filename))
482 492 yield raw if block_given?
483 493 MailHandler.receive(raw, options)
484 494 end
485 495
486 496 def assert_issue_created(issue)
487 497 assert issue.is_a?(Issue)
488 498 assert !issue.new_record?
489 499 issue.reload
490 500 end
491 501 end
General Comments 0
You need to be logged in to leave comments. Login now