##// END OF EJS Templates
Merged r10850 from trunk (#12396)....
Jean-Philippe Lang -
r10634:8c7ae204020d
parent child
Show More
@@ -0,0 +1,10
1 Content-Type: application/ms-tnef; name="winmail.dat"
2 Content-Transfer-Encoding: binary
3 From: John Smith <JSmith@somenet.foo>
4 To: "redmine@somenet.foo" <redmine@somenet.foo>
5 Date: Fri, 1 Jun 2012 14:39:38 +0200
6 Message-ID: <87C31D42249DD0489D1A1444E3232DD7019D6183@foo.bar>
7 Accept-Language: de-CH, en-US
8 Content-Language: de-CH
9
10 Fixture
@@ -1,493 +1,494
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class MailHandler < ActionMailer::Base
18 class MailHandler < ActionMailer::Base
19 include ActionView::Helpers::SanitizeHelper
19 include ActionView::Helpers::SanitizeHelper
20 include Redmine::I18n
20 include Redmine::I18n
21
21
22 class UnauthorizedAction < StandardError; end
22 class UnauthorizedAction < StandardError; end
23 class MissingInformation < StandardError; end
23 class MissingInformation < StandardError; end
24
24
25 attr_reader :email, :user
25 attr_reader :email, :user
26
26
27 def self.receive(email, options={})
27 def self.receive(email, options={})
28 @@handler_options = options.dup
28 @@handler_options = options.dup
29
29
30 @@handler_options[:issue] ||= {}
30 @@handler_options[:issue] ||= {}
31
31
32 if @@handler_options[:allow_override].is_a?(String)
32 if @@handler_options[:allow_override].is_a?(String)
33 @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip)
33 @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip)
34 end
34 end
35 @@handler_options[:allow_override] ||= []
35 @@handler_options[:allow_override] ||= []
36 # Project needs to be overridable if not specified
36 # Project needs to be overridable if not specified
37 @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
37 @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
38 # Status overridable by default
38 # Status overridable by default
39 @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
39 @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
40
40
41 @@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1' ? true : false)
41 @@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1' ? true : false)
42
42
43 email.force_encoding('ASCII-8BIT') if email.respond_to?(:force_encoding)
43 email.force_encoding('ASCII-8BIT') if email.respond_to?(:force_encoding)
44 super(email)
44 super(email)
45 end
45 end
46
46
47 def logger
47 def logger
48 Rails.logger
48 Rails.logger
49 end
49 end
50
50
51 cattr_accessor :ignored_emails_headers
51 cattr_accessor :ignored_emails_headers
52 @@ignored_emails_headers = {
52 @@ignored_emails_headers = {
53 'X-Auto-Response-Suppress' => 'oof',
53 'X-Auto-Response-Suppress' => 'oof',
54 'Auto-Submitted' => /^auto-/
54 'Auto-Submitted' => /^auto-/
55 }
55 }
56
56
57 # Processes incoming emails
57 # Processes incoming emails
58 # Returns the created object (eg. an issue, a message) or false
58 # Returns the created object (eg. an issue, a message) or false
59 def receive(email)
59 def receive(email)
60 @email = email
60 @email = email
61 sender_email = email.from.to_a.first.to_s.strip
61 sender_email = email.from.to_a.first.to_s.strip
62 # Ignore emails received from the application emission address to avoid hell cycles
62 # Ignore emails received from the application emission address to avoid hell cycles
63 if sender_email.downcase == Setting.mail_from.to_s.strip.downcase
63 if sender_email.downcase == Setting.mail_from.to_s.strip.downcase
64 if logger && logger.info
64 if logger && logger.info
65 logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]"
65 logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]"
66 end
66 end
67 return false
67 return false
68 end
68 end
69 # Ignore auto generated emails
69 # Ignore auto generated emails
70 self.class.ignored_emails_headers.each do |key, ignored_value|
70 self.class.ignored_emails_headers.each do |key, ignored_value|
71 value = email.header[key]
71 value = email.header[key]
72 if value
72 if value
73 value = value.to_s.downcase
73 value = value.to_s.downcase
74 if (ignored_value.is_a?(Regexp) && value.match(ignored_value)) || value == ignored_value
74 if (ignored_value.is_a?(Regexp) && value.match(ignored_value)) || value == ignored_value
75 if logger && logger.info
75 if logger && logger.info
76 logger.info "MailHandler: ignoring email with #{key}:#{value} header"
76 logger.info "MailHandler: ignoring email with #{key}:#{value} header"
77 end
77 end
78 return false
78 return false
79 end
79 end
80 end
80 end
81 end
81 end
82 @user = User.find_by_mail(sender_email) if sender_email.present?
82 @user = User.find_by_mail(sender_email) if sender_email.present?
83 if @user && !@user.active?
83 if @user && !@user.active?
84 if logger && logger.info
84 if logger && logger.info
85 logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]"
85 logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]"
86 end
86 end
87 return false
87 return false
88 end
88 end
89 if @user.nil?
89 if @user.nil?
90 # Email was submitted by an unknown user
90 # Email was submitted by an unknown user
91 case @@handler_options[:unknown_user]
91 case @@handler_options[:unknown_user]
92 when 'accept'
92 when 'accept'
93 @user = User.anonymous
93 @user = User.anonymous
94 when 'create'
94 when 'create'
95 @user = create_user_from_email
95 @user = create_user_from_email
96 if @user
96 if @user
97 if logger && logger.info
97 if logger && logger.info
98 logger.info "MailHandler: [#{@user.login}] account created"
98 logger.info "MailHandler: [#{@user.login}] account created"
99 end
99 end
100 Mailer.account_information(@user, @user.password).deliver
100 Mailer.account_information(@user, @user.password).deliver
101 else
101 else
102 if logger && logger.error
102 if logger && logger.error
103 logger.error "MailHandler: could not create account for [#{sender_email}]"
103 logger.error "MailHandler: could not create account for [#{sender_email}]"
104 end
104 end
105 return false
105 return false
106 end
106 end
107 else
107 else
108 # Default behaviour, emails from unknown users are ignored
108 # Default behaviour, emails from unknown users are ignored
109 if logger && logger.info
109 if logger && logger.info
110 logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]"
110 logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]"
111 end
111 end
112 return false
112 return false
113 end
113 end
114 end
114 end
115 User.current = @user
115 User.current = @user
116 dispatch
116 dispatch
117 end
117 end
118
118
119 private
119 private
120
120
121 MESSAGE_ID_RE = %r{^<?redmine\.([a-z0-9_]+)\-(\d+)\.\d+@}
121 MESSAGE_ID_RE = %r{^<?redmine\.([a-z0-9_]+)\-(\d+)\.\d+@}
122 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]*#(\d+)\]}
122 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]*#(\d+)\]}
123 MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
123 MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
124
124
125 def dispatch
125 def dispatch
126 headers = [email.in_reply_to, email.references].flatten.compact
126 headers = [email.in_reply_to, email.references].flatten.compact
127 subject = email.subject.to_s
127 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
128 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
128 klass, object_id = $1, $2.to_i
129 klass, object_id = $1, $2.to_i
129 method_name = "receive_#{klass}_reply"
130 method_name = "receive_#{klass}_reply"
130 if self.class.private_instance_methods.collect(&:to_s).include?(method_name)
131 if self.class.private_instance_methods.collect(&:to_s).include?(method_name)
131 send method_name, object_id
132 send method_name, object_id
132 else
133 else
133 # ignoring it
134 # ignoring it
134 end
135 end
135 elsif m = email.subject.match(ISSUE_REPLY_SUBJECT_RE)
136 elsif m = subject.match(ISSUE_REPLY_SUBJECT_RE)
136 receive_issue_reply(m[1].to_i)
137 receive_issue_reply(m[1].to_i)
137 elsif m = email.subject.match(MESSAGE_REPLY_SUBJECT_RE)
138 elsif m = subject.match(MESSAGE_REPLY_SUBJECT_RE)
138 receive_message_reply(m[1].to_i)
139 receive_message_reply(m[1].to_i)
139 else
140 else
140 dispatch_to_default
141 dispatch_to_default
141 end
142 end
142 rescue ActiveRecord::RecordInvalid => e
143 rescue ActiveRecord::RecordInvalid => e
143 # TODO: send a email to the user
144 # TODO: send a email to the user
144 logger.error e.message if logger
145 logger.error e.message if logger
145 false
146 false
146 rescue MissingInformation => e
147 rescue MissingInformation => e
147 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
148 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
148 false
149 false
149 rescue UnauthorizedAction => e
150 rescue UnauthorizedAction => e
150 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
151 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
151 false
152 false
152 end
153 end
153
154
154 def dispatch_to_default
155 def dispatch_to_default
155 receive_issue
156 receive_issue
156 end
157 end
157
158
158 # Creates a new issue
159 # Creates a new issue
159 def receive_issue
160 def receive_issue
160 project = target_project
161 project = target_project
161 # check permission
162 # check permission
162 unless @@handler_options[:no_permission_check]
163 unless @@handler_options[:no_permission_check]
163 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
164 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
164 end
165 end
165
166
166 issue = Issue.new(:author => user, :project => project)
167 issue = Issue.new(:author => user, :project => project)
167 issue.safe_attributes = issue_attributes_from_keywords(issue)
168 issue.safe_attributes = issue_attributes_from_keywords(issue)
168 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
169 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
169 issue.subject = cleaned_up_subject
170 issue.subject = cleaned_up_subject
170 if issue.subject.blank?
171 if issue.subject.blank?
171 issue.subject = '(no subject)'
172 issue.subject = '(no subject)'
172 end
173 end
173 issue.description = cleaned_up_text_body
174 issue.description = cleaned_up_text_body
174
175
175 # add To and Cc as watchers before saving so the watchers can reply to Redmine
176 # add To and Cc as watchers before saving so the watchers can reply to Redmine
176 add_watchers(issue)
177 add_watchers(issue)
177 issue.save!
178 issue.save!
178 add_attachments(issue)
179 add_attachments(issue)
179 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
180 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
180 issue
181 issue
181 end
182 end
182
183
183 # Adds a note to an existing issue
184 # Adds a note to an existing issue
184 def receive_issue_reply(issue_id)
185 def receive_issue_reply(issue_id)
185 issue = Issue.find_by_id(issue_id)
186 issue = Issue.find_by_id(issue_id)
186 return unless issue
187 return unless issue
187 # check permission
188 # check permission
188 unless @@handler_options[:no_permission_check]
189 unless @@handler_options[:no_permission_check]
189 unless user.allowed_to?(:add_issue_notes, issue.project) ||
190 unless user.allowed_to?(:add_issue_notes, issue.project) ||
190 user.allowed_to?(:edit_issues, issue.project)
191 user.allowed_to?(:edit_issues, issue.project)
191 raise UnauthorizedAction
192 raise UnauthorizedAction
192 end
193 end
193 end
194 end
194
195
195 # ignore CLI-supplied defaults for new issues
196 # ignore CLI-supplied defaults for new issues
196 @@handler_options[:issue].clear
197 @@handler_options[:issue].clear
197
198
198 journal = issue.init_journal(user)
199 journal = issue.init_journal(user)
199 issue.safe_attributes = issue_attributes_from_keywords(issue)
200 issue.safe_attributes = issue_attributes_from_keywords(issue)
200 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
201 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
201 journal.notes = cleaned_up_text_body
202 journal.notes = cleaned_up_text_body
202 add_attachments(issue)
203 add_attachments(issue)
203 issue.save!
204 issue.save!
204 if logger && logger.info
205 if logger && logger.info
205 logger.info "MailHandler: issue ##{issue.id} updated by #{user}"
206 logger.info "MailHandler: issue ##{issue.id} updated by #{user}"
206 end
207 end
207 journal
208 journal
208 end
209 end
209
210
210 # Reply will be added to the issue
211 # Reply will be added to the issue
211 def receive_journal_reply(journal_id)
212 def receive_journal_reply(journal_id)
212 journal = Journal.find_by_id(journal_id)
213 journal = Journal.find_by_id(journal_id)
213 if journal && journal.journalized_type == 'Issue'
214 if journal && journal.journalized_type == 'Issue'
214 receive_issue_reply(journal.journalized_id)
215 receive_issue_reply(journal.journalized_id)
215 end
216 end
216 end
217 end
217
218
218 # Receives a reply to a forum message
219 # Receives a reply to a forum message
219 def receive_message_reply(message_id)
220 def receive_message_reply(message_id)
220 message = Message.find_by_id(message_id)
221 message = Message.find_by_id(message_id)
221 if message
222 if message
222 message = message.root
223 message = message.root
223
224
224 unless @@handler_options[:no_permission_check]
225 unless @@handler_options[:no_permission_check]
225 raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
226 raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
226 end
227 end
227
228
228 if !message.locked?
229 if !message.locked?
229 reply = Message.new(:subject => cleaned_up_subject.gsub(%r{^.*msg\d+\]}, '').strip,
230 reply = Message.new(:subject => cleaned_up_subject.gsub(%r{^.*msg\d+\]}, '').strip,
230 :content => cleaned_up_text_body)
231 :content => cleaned_up_text_body)
231 reply.author = user
232 reply.author = user
232 reply.board = message.board
233 reply.board = message.board
233 message.children << reply
234 message.children << reply
234 add_attachments(reply)
235 add_attachments(reply)
235 reply
236 reply
236 else
237 else
237 if logger && logger.info
238 if logger && logger.info
238 logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic"
239 logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic"
239 end
240 end
240 end
241 end
241 end
242 end
242 end
243 end
243
244
244 def add_attachments(obj)
245 def add_attachments(obj)
245 if email.attachments && email.attachments.any?
246 if email.attachments && email.attachments.any?
246 email.attachments.each do |attachment|
247 email.attachments.each do |attachment|
247 filename = attachment.filename
248 filename = attachment.filename
248 unless filename.respond_to?(:encoding)
249 unless filename.respond_to?(:encoding)
249 # try to reencode to utf8 manually with ruby1.8
250 # try to reencode to utf8 manually with ruby1.8
250 h = attachment.header['Content-Disposition']
251 h = attachment.header['Content-Disposition']
251 unless h.nil?
252 unless h.nil?
252 begin
253 begin
253 if m = h.value.match(/filename\*[0-9\*]*=([^=']+)'/)
254 if m = h.value.match(/filename\*[0-9\*]*=([^=']+)'/)
254 filename = Redmine::CodesetUtil.to_utf8(filename, m[1])
255 filename = Redmine::CodesetUtil.to_utf8(filename, m[1])
255 elsif m = h.value.match(/filename=.*=\?([^\?]+)\?[BbQq]\?/)
256 elsif m = h.value.match(/filename=.*=\?([^\?]+)\?[BbQq]\?/)
256 # http://tools.ietf.org/html/rfc2047#section-4
257 # http://tools.ietf.org/html/rfc2047#section-4
257 filename = Redmine::CodesetUtil.to_utf8(filename, m[1])
258 filename = Redmine::CodesetUtil.to_utf8(filename, m[1])
258 end
259 end
259 rescue
260 rescue
260 # nop
261 # nop
261 end
262 end
262 end
263 end
263 end
264 end
264 obj.attachments << Attachment.create(:container => obj,
265 obj.attachments << Attachment.create(:container => obj,
265 :file => attachment.decoded,
266 :file => attachment.decoded,
266 :filename => filename,
267 :filename => filename,
267 :author => user,
268 :author => user,
268 :content_type => attachment.mime_type)
269 :content_type => attachment.mime_type)
269 end
270 end
270 end
271 end
271 end
272 end
272
273
273 # Adds To and Cc as watchers of the given object if the sender has the
274 # Adds To and Cc as watchers of the given object if the sender has the
274 # appropriate permission
275 # appropriate permission
275 def add_watchers(obj)
276 def add_watchers(obj)
276 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
277 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
277 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
278 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
278 unless addresses.empty?
279 unless addresses.empty?
279 watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
280 watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
280 watchers.each {|w| obj.add_watcher(w)}
281 watchers.each {|w| obj.add_watcher(w)}
281 end
282 end
282 end
283 end
283 end
284 end
284
285
285 def get_keyword(attr, options={})
286 def get_keyword(attr, options={})
286 @keywords ||= {}
287 @keywords ||= {}
287 if @keywords.has_key?(attr)
288 if @keywords.has_key?(attr)
288 @keywords[attr]
289 @keywords[attr]
289 else
290 else
290 @keywords[attr] = begin
291 @keywords[attr] = begin
291 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) &&
292 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) &&
292 (v = extract_keyword!(plain_text_body, attr, options[:format]))
293 (v = extract_keyword!(plain_text_body, attr, options[:format]))
293 v
294 v
294 elsif !@@handler_options[:issue][attr].blank?
295 elsif !@@handler_options[:issue][attr].blank?
295 @@handler_options[:issue][attr]
296 @@handler_options[:issue][attr]
296 end
297 end
297 end
298 end
298 end
299 end
299 end
300 end
300
301
301 # Destructively extracts the value for +attr+ in +text+
302 # Destructively extracts the value for +attr+ in +text+
302 # Returns nil if no matching keyword found
303 # Returns nil if no matching keyword found
303 def extract_keyword!(text, attr, format=nil)
304 def extract_keyword!(text, attr, format=nil)
304 keys = [attr.to_s.humanize]
305 keys = [attr.to_s.humanize]
305 if attr.is_a?(Symbol)
306 if attr.is_a?(Symbol)
306 if user && user.language.present?
307 if user && user.language.present?
307 keys << l("field_#{attr}", :default => '', :locale => user.language)
308 keys << l("field_#{attr}", :default => '', :locale => user.language)
308 end
309 end
309 if Setting.default_language.present?
310 if Setting.default_language.present?
310 keys << l("field_#{attr}", :default => '', :locale => Setting.default_language)
311 keys << l("field_#{attr}", :default => '', :locale => Setting.default_language)
311 end
312 end
312 end
313 end
313 keys.reject! {|k| k.blank?}
314 keys.reject! {|k| k.blank?}
314 keys.collect! {|k| Regexp.escape(k)}
315 keys.collect! {|k| Regexp.escape(k)}
315 format ||= '.+'
316 format ||= '.+'
316 keyword = nil
317 keyword = nil
317 regexp = /^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i
318 regexp = /^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i
318 if m = text.match(regexp)
319 if m = text.match(regexp)
319 keyword = m[2].strip
320 keyword = m[2].strip
320 text.gsub!(regexp, '')
321 text.gsub!(regexp, '')
321 end
322 end
322 keyword
323 keyword
323 end
324 end
324
325
325 def target_project
326 def target_project
326 # TODO: other ways to specify project:
327 # TODO: other ways to specify project:
327 # * parse the email To field
328 # * parse the email To field
328 # * specific project (eg. Setting.mail_handler_target_project)
329 # * specific project (eg. Setting.mail_handler_target_project)
329 target = Project.find_by_identifier(get_keyword(:project))
330 target = Project.find_by_identifier(get_keyword(:project))
330 raise MissingInformation.new('Unable to determine target project') if target.nil?
331 raise MissingInformation.new('Unable to determine target project') if target.nil?
331 target
332 target
332 end
333 end
333
334
334 # Returns a Hash of issue attributes extracted from keywords in the email body
335 # Returns a Hash of issue attributes extracted from keywords in the email body
335 def issue_attributes_from_keywords(issue)
336 def issue_attributes_from_keywords(issue)
336 assigned_to = (k = get_keyword(:assigned_to, :override => true)) && find_assignee_from_keyword(k, issue)
337 assigned_to = (k = get_keyword(:assigned_to, :override => true)) && find_assignee_from_keyword(k, issue)
337
338
338 attrs = {
339 attrs = {
339 'tracker_id' => (k = get_keyword(:tracker)) && issue.project.trackers.named(k).first.try(:id),
340 'tracker_id' => (k = get_keyword(:tracker)) && issue.project.trackers.named(k).first.try(:id),
340 'status_id' => (k = get_keyword(:status)) && IssueStatus.named(k).first.try(:id),
341 'status_id' => (k = get_keyword(:status)) && IssueStatus.named(k).first.try(:id),
341 'priority_id' => (k = get_keyword(:priority)) && IssuePriority.named(k).first.try(:id),
342 'priority_id' => (k = get_keyword(:priority)) && IssuePriority.named(k).first.try(:id),
342 'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.named(k).first.try(:id),
343 'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.named(k).first.try(:id),
343 'assigned_to_id' => assigned_to.try(:id),
344 'assigned_to_id' => assigned_to.try(:id),
344 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) &&
345 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) &&
345 issue.project.shared_versions.named(k).first.try(:id),
346 issue.project.shared_versions.named(k).first.try(:id),
346 'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
347 'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
347 'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
348 'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
348 'estimated_hours' => get_keyword(:estimated_hours, :override => true),
349 'estimated_hours' => get_keyword(:estimated_hours, :override => true),
349 'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0')
350 'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0')
350 }.delete_if {|k, v| v.blank? }
351 }.delete_if {|k, v| v.blank? }
351
352
352 if issue.new_record? && attrs['tracker_id'].nil?
353 if issue.new_record? && attrs['tracker_id'].nil?
353 attrs['tracker_id'] = issue.project.trackers.find(:first).try(:id)
354 attrs['tracker_id'] = issue.project.trackers.find(:first).try(:id)
354 end
355 end
355
356
356 attrs
357 attrs
357 end
358 end
358
359
359 # Returns a Hash of issue custom field values extracted from keywords in the email body
360 # Returns a Hash of issue custom field values extracted from keywords in the email body
360 def custom_field_values_from_keywords(customized)
361 def custom_field_values_from_keywords(customized)
361 customized.custom_field_values.inject({}) do |h, v|
362 customized.custom_field_values.inject({}) do |h, v|
362 if keyword = get_keyword(v.custom_field.name, :override => true)
363 if keyword = get_keyword(v.custom_field.name, :override => true)
363 h[v.custom_field.id.to_s] = v.custom_field.value_from_keyword(keyword, customized)
364 h[v.custom_field.id.to_s] = v.custom_field.value_from_keyword(keyword, customized)
364 end
365 end
365 h
366 h
366 end
367 end
367 end
368 end
368
369
369 # Returns the text/plain part of the email
370 # Returns the text/plain part of the email
370 # If not found (eg. HTML-only email), returns the body with tags removed
371 # If not found (eg. HTML-only email), returns the body with tags removed
371 def plain_text_body
372 def plain_text_body
372 return @plain_text_body unless @plain_text_body.nil?
373 return @plain_text_body unless @plain_text_body.nil?
373
374
374 part = email.text_part || email.html_part || email
375 part = email.text_part || email.html_part || email
375 @plain_text_body = Redmine::CodesetUtil.to_utf8(part.body.decoded, part.charset)
376 @plain_text_body = Redmine::CodesetUtil.to_utf8(part.body.decoded, part.charset)
376
377
377 # strip html tags and remove doctype directive
378 # strip html tags and remove doctype directive
378 @plain_text_body = strip_tags(@plain_text_body.strip)
379 @plain_text_body = strip_tags(@plain_text_body.strip)
379 @plain_text_body.sub! %r{^<!DOCTYPE .*$}, ''
380 @plain_text_body.sub! %r{^<!DOCTYPE .*$}, ''
380 @plain_text_body
381 @plain_text_body
381 end
382 end
382
383
383 def cleaned_up_text_body
384 def cleaned_up_text_body
384 cleanup_body(plain_text_body)
385 cleanup_body(plain_text_body)
385 end
386 end
386
387
387 def cleaned_up_subject
388 def cleaned_up_subject
388 subject = email.subject.to_s
389 subject = email.subject.to_s
389 unless subject.respond_to?(:encoding)
390 unless subject.respond_to?(:encoding)
390 # try to reencode to utf8 manually with ruby1.8
391 # try to reencode to utf8 manually with ruby1.8
391 begin
392 begin
392 if h = email.header[:subject]
393 if h = email.header[:subject]
393 # http://tools.ietf.org/html/rfc2047#section-4
394 # http://tools.ietf.org/html/rfc2047#section-4
394 if m = h.value.match(/=\?([^\?]+)\?[BbQq]\?/)
395 if m = h.value.match(/=\?([^\?]+)\?[BbQq]\?/)
395 subject = Redmine::CodesetUtil.to_utf8(subject, m[1])
396 subject = Redmine::CodesetUtil.to_utf8(subject, m[1])
396 end
397 end
397 end
398 end
398 rescue
399 rescue
399 # nop
400 # nop
400 end
401 end
401 end
402 end
402 subject.strip[0,255]
403 subject.strip[0,255]
403 end
404 end
404
405
405 def self.full_sanitizer
406 def self.full_sanitizer
406 @full_sanitizer ||= HTML::FullSanitizer.new
407 @full_sanitizer ||= HTML::FullSanitizer.new
407 end
408 end
408
409
409 def self.assign_string_attribute_with_limit(object, attribute, value, limit=nil)
410 def self.assign_string_attribute_with_limit(object, attribute, value, limit=nil)
410 limit ||= object.class.columns_hash[attribute.to_s].limit || 255
411 limit ||= object.class.columns_hash[attribute.to_s].limit || 255
411 value = value.to_s.slice(0, limit)
412 value = value.to_s.slice(0, limit)
412 object.send("#{attribute}=", value)
413 object.send("#{attribute}=", value)
413 end
414 end
414
415
415 # Returns a User from an email address and a full name
416 # Returns a User from an email address and a full name
416 def self.new_user_from_attributes(email_address, fullname=nil)
417 def self.new_user_from_attributes(email_address, fullname=nil)
417 user = User.new
418 user = User.new
418
419
419 # Truncating the email address would result in an invalid format
420 # Truncating the email address would result in an invalid format
420 user.mail = email_address
421 user.mail = email_address
421 assign_string_attribute_with_limit(user, 'login', email_address, User::LOGIN_LENGTH_LIMIT)
422 assign_string_attribute_with_limit(user, 'login', email_address, User::LOGIN_LENGTH_LIMIT)
422
423
423 names = fullname.blank? ? email_address.gsub(/@.*$/, '').split('.') : fullname.split
424 names = fullname.blank? ? email_address.gsub(/@.*$/, '').split('.') : fullname.split
424 assign_string_attribute_with_limit(user, 'firstname', names.shift)
425 assign_string_attribute_with_limit(user, 'firstname', names.shift)
425 assign_string_attribute_with_limit(user, 'lastname', names.join(' '))
426 assign_string_attribute_with_limit(user, 'lastname', names.join(' '))
426 user.lastname = '-' if user.lastname.blank?
427 user.lastname = '-' if user.lastname.blank?
427
428
428 password_length = [Setting.password_min_length.to_i, 10].max
429 password_length = [Setting.password_min_length.to_i, 10].max
429 user.password = Redmine::Utils.random_hex(password_length / 2 + 1)
430 user.password = Redmine::Utils.random_hex(password_length / 2 + 1)
430 user.language = Setting.default_language
431 user.language = Setting.default_language
431
432
432 unless user.valid?
433 unless user.valid?
433 user.login = "user#{Redmine::Utils.random_hex(6)}" unless user.errors[:login].blank?
434 user.login = "user#{Redmine::Utils.random_hex(6)}" unless user.errors[:login].blank?
434 user.firstname = "-" unless user.errors[:firstname].blank?
435 user.firstname = "-" unless user.errors[:firstname].blank?
435 user.lastname = "-" unless user.errors[:lastname].blank?
436 user.lastname = "-" unless user.errors[:lastname].blank?
436 end
437 end
437
438
438 user
439 user
439 end
440 end
440
441
441 # Creates a User for the +email+ sender
442 # Creates a User for the +email+ sender
442 # Returns the user or nil if it could not be created
443 # Returns the user or nil if it could not be created
443 def create_user_from_email
444 def create_user_from_email
444 from = email.header['from'].to_s
445 from = email.header['from'].to_s
445 addr, name = from, nil
446 addr, name = from, nil
446 if m = from.match(/^"?(.+?)"?\s+<(.+@.+)>$/)
447 if m = from.match(/^"?(.+?)"?\s+<(.+@.+)>$/)
447 addr, name = m[2], m[1]
448 addr, name = m[2], m[1]
448 end
449 end
449 if addr.present?
450 if addr.present?
450 user = self.class.new_user_from_attributes(addr, name)
451 user = self.class.new_user_from_attributes(addr, name)
451 if user.save
452 if user.save
452 user
453 user
453 else
454 else
454 logger.error "MailHandler: failed to create User: #{user.errors.full_messages}" if logger
455 logger.error "MailHandler: failed to create User: #{user.errors.full_messages}" if logger
455 nil
456 nil
456 end
457 end
457 else
458 else
458 logger.error "MailHandler: failed to create User: no FROM address found" if logger
459 logger.error "MailHandler: failed to create User: no FROM address found" if logger
459 nil
460 nil
460 end
461 end
461 end
462 end
462
463
463 # Removes the email body of text after the truncation configurations.
464 # Removes the email body of text after the truncation configurations.
464 def cleanup_body(body)
465 def cleanup_body(body)
465 delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
466 delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
466 unless delimiters.empty?
467 unless delimiters.empty?
467 regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
468 regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
468 body = body.gsub(regex, '')
469 body = body.gsub(regex, '')
469 end
470 end
470 body.strip
471 body.strip
471 end
472 end
472
473
473 def find_assignee_from_keyword(keyword, issue)
474 def find_assignee_from_keyword(keyword, issue)
474 keyword = keyword.to_s.downcase
475 keyword = keyword.to_s.downcase
475 assignable = issue.assignable_users
476 assignable = issue.assignable_users
476 assignee = nil
477 assignee = nil
477 assignee ||= assignable.detect {|a|
478 assignee ||= assignable.detect {|a|
478 a.mail.to_s.downcase == keyword ||
479 a.mail.to_s.downcase == keyword ||
479 a.login.to_s.downcase == keyword
480 a.login.to_s.downcase == keyword
480 }
481 }
481 if assignee.nil? && keyword.match(/ /)
482 if assignee.nil? && keyword.match(/ /)
482 firstname, lastname = *(keyword.split) # "First Last Throwaway"
483 firstname, lastname = *(keyword.split) # "First Last Throwaway"
483 assignee ||= assignable.detect {|a|
484 assignee ||= assignable.detect {|a|
484 a.is_a?(User) && a.firstname.to_s.downcase == firstname &&
485 a.is_a?(User) && a.firstname.to_s.downcase == firstname &&
485 a.lastname.to_s.downcase == lastname
486 a.lastname.to_s.downcase == lastname
486 }
487 }
487 end
488 end
488 if assignee.nil?
489 if assignee.nil?
489 assignee ||= assignable.detect {|a| a.name.downcase == keyword}
490 assignee ||= assignable.detect {|a| a.name.downcase == keyword}
490 end
491 end
491 assignee
492 assignee
492 end
493 end
493 end
494 end
@@ -1,768 +1,777
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 require File.expand_path('../../test_helper', __FILE__)
20 require File.expand_path('../../test_helper', __FILE__)
21
21
22 class MailHandlerTest < ActiveSupport::TestCase
22 class MailHandlerTest < ActiveSupport::TestCase
23 fixtures :users, :projects, :enabled_modules, :roles,
23 fixtures :users, :projects, :enabled_modules, :roles,
24 :members, :member_roles, :users,
24 :members, :member_roles, :users,
25 :issues, :issue_statuses,
25 :issues, :issue_statuses,
26 :workflows, :trackers, :projects_trackers,
26 :workflows, :trackers, :projects_trackers,
27 :versions, :enumerations, :issue_categories,
27 :versions, :enumerations, :issue_categories,
28 :custom_fields, :custom_fields_trackers, :custom_fields_projects,
28 :custom_fields, :custom_fields_trackers, :custom_fields_projects,
29 :boards, :messages
29 :boards, :messages
30
30
31 FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler'
31 FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler'
32
32
33 def setup
33 def setup
34 ActionMailer::Base.deliveries.clear
34 ActionMailer::Base.deliveries.clear
35 Setting.notified_events = Redmine::Notifiable.all.collect(&:name)
35 Setting.notified_events = Redmine::Notifiable.all.collect(&:name)
36 end
36 end
37
37
38 def teardown
38 def teardown
39 Setting.clear_cache
39 Setting.clear_cache
40 end
40 end
41
41
42 def test_add_issue
42 def test_add_issue
43 ActionMailer::Base.deliveries.clear
43 ActionMailer::Base.deliveries.clear
44 # This email contains: 'Project: onlinestore'
44 # This email contains: 'Project: onlinestore'
45 issue = submit_email('ticket_on_given_project.eml')
45 issue = submit_email('ticket_on_given_project.eml')
46 assert issue.is_a?(Issue)
46 assert issue.is_a?(Issue)
47 assert !issue.new_record?
47 assert !issue.new_record?
48 issue.reload
48 issue.reload
49 assert_equal Project.find(2), issue.project
49 assert_equal Project.find(2), issue.project
50 assert_equal issue.project.trackers.first, issue.tracker
50 assert_equal issue.project.trackers.first, issue.tracker
51 assert_equal 'New ticket on a given project', issue.subject
51 assert_equal 'New ticket on a given project', issue.subject
52 assert_equal User.find_by_login('jsmith'), issue.author
52 assert_equal User.find_by_login('jsmith'), issue.author
53 assert_equal IssueStatus.find_by_name('Resolved'), issue.status
53 assert_equal IssueStatus.find_by_name('Resolved'), issue.status
54 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
54 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
55 assert_equal '2010-01-01', issue.start_date.to_s
55 assert_equal '2010-01-01', issue.start_date.to_s
56 assert_equal '2010-12-31', issue.due_date.to_s
56 assert_equal '2010-12-31', issue.due_date.to_s
57 assert_equal User.find_by_login('jsmith'), issue.assigned_to
57 assert_equal User.find_by_login('jsmith'), issue.assigned_to
58 assert_equal Version.find_by_name('Alpha'), issue.fixed_version
58 assert_equal Version.find_by_name('Alpha'), issue.fixed_version
59 assert_equal 2.5, issue.estimated_hours
59 assert_equal 2.5, issue.estimated_hours
60 assert_equal 30, issue.done_ratio
60 assert_equal 30, issue.done_ratio
61 assert_equal [issue.id, 1, 2], [issue.root_id, issue.lft, issue.rgt]
61 assert_equal [issue.id, 1, 2], [issue.root_id, issue.lft, issue.rgt]
62 # keywords should be removed from the email body
62 # keywords should be removed from the email body
63 assert !issue.description.match(/^Project:/i)
63 assert !issue.description.match(/^Project:/i)
64 assert !issue.description.match(/^Status:/i)
64 assert !issue.description.match(/^Status:/i)
65 assert !issue.description.match(/^Start Date:/i)
65 assert !issue.description.match(/^Start Date:/i)
66 # Email notification should be sent
66 # Email notification should be sent
67 mail = ActionMailer::Base.deliveries.last
67 mail = ActionMailer::Base.deliveries.last
68 assert_not_nil mail
68 assert_not_nil mail
69 assert mail.subject.include?('New ticket on a given project')
69 assert mail.subject.include?('New ticket on a given project')
70 end
70 end
71
71
72 def test_add_issue_with_default_tracker
72 def test_add_issue_with_default_tracker
73 # This email contains: 'Project: onlinestore'
73 # This email contains: 'Project: onlinestore'
74 issue = submit_email(
74 issue = submit_email(
75 'ticket_on_given_project.eml',
75 'ticket_on_given_project.eml',
76 :issue => {:tracker => 'Support request'}
76 :issue => {:tracker => 'Support request'}
77 )
77 )
78 assert issue.is_a?(Issue)
78 assert issue.is_a?(Issue)
79 assert !issue.new_record?
79 assert !issue.new_record?
80 issue.reload
80 issue.reload
81 assert_equal 'Support request', issue.tracker.name
81 assert_equal 'Support request', issue.tracker.name
82 end
82 end
83
83
84 def test_add_issue_with_status
84 def test_add_issue_with_status
85 # This email contains: 'Project: onlinestore' and 'Status: Resolved'
85 # This email contains: 'Project: onlinestore' and 'Status: Resolved'
86 issue = submit_email('ticket_on_given_project.eml')
86 issue = submit_email('ticket_on_given_project.eml')
87 assert issue.is_a?(Issue)
87 assert issue.is_a?(Issue)
88 assert !issue.new_record?
88 assert !issue.new_record?
89 issue.reload
89 issue.reload
90 assert_equal Project.find(2), issue.project
90 assert_equal Project.find(2), issue.project
91 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
91 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
92 end
92 end
93
93
94 def test_add_issue_with_attributes_override
94 def test_add_issue_with_attributes_override
95 issue = submit_email(
95 issue = submit_email(
96 'ticket_with_attributes.eml',
96 'ticket_with_attributes.eml',
97 :allow_override => 'tracker,category,priority'
97 :allow_override => 'tracker,category,priority'
98 )
98 )
99 assert issue.is_a?(Issue)
99 assert issue.is_a?(Issue)
100 assert !issue.new_record?
100 assert !issue.new_record?
101 issue.reload
101 issue.reload
102 assert_equal 'New ticket on a given project', issue.subject
102 assert_equal 'New ticket on a given project', issue.subject
103 assert_equal User.find_by_login('jsmith'), issue.author
103 assert_equal User.find_by_login('jsmith'), issue.author
104 assert_equal Project.find(2), issue.project
104 assert_equal Project.find(2), issue.project
105 assert_equal 'Feature request', issue.tracker.to_s
105 assert_equal 'Feature request', issue.tracker.to_s
106 assert_equal 'Stock management', issue.category.to_s
106 assert_equal 'Stock management', issue.category.to_s
107 assert_equal 'Urgent', issue.priority.to_s
107 assert_equal 'Urgent', issue.priority.to_s
108 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
108 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
109 end
109 end
110
110
111 def test_add_issue_with_group_assignment
111 def test_add_issue_with_group_assignment
112 with_settings :issue_group_assignment => '1' do
112 with_settings :issue_group_assignment => '1' do
113 issue = submit_email('ticket_on_given_project.eml') do |email|
113 issue = submit_email('ticket_on_given_project.eml') do |email|
114 email.gsub!('Assigned to: John Smith', 'Assigned to: B Team')
114 email.gsub!('Assigned to: John Smith', 'Assigned to: B Team')
115 end
115 end
116 assert issue.is_a?(Issue)
116 assert issue.is_a?(Issue)
117 assert !issue.new_record?
117 assert !issue.new_record?
118 issue.reload
118 issue.reload
119 assert_equal Group.find(11), issue.assigned_to
119 assert_equal Group.find(11), issue.assigned_to
120 end
120 end
121 end
121 end
122
122
123 def test_add_issue_with_partial_attributes_override
123 def test_add_issue_with_partial_attributes_override
124 issue = submit_email(
124 issue = submit_email(
125 'ticket_with_attributes.eml',
125 'ticket_with_attributes.eml',
126 :issue => {:priority => 'High'},
126 :issue => {:priority => 'High'},
127 :allow_override => ['tracker']
127 :allow_override => ['tracker']
128 )
128 )
129 assert issue.is_a?(Issue)
129 assert issue.is_a?(Issue)
130 assert !issue.new_record?
130 assert !issue.new_record?
131 issue.reload
131 issue.reload
132 assert_equal 'New ticket on a given project', issue.subject
132 assert_equal 'New ticket on a given project', issue.subject
133 assert_equal User.find_by_login('jsmith'), issue.author
133 assert_equal User.find_by_login('jsmith'), issue.author
134 assert_equal Project.find(2), issue.project
134 assert_equal Project.find(2), issue.project
135 assert_equal 'Feature request', issue.tracker.to_s
135 assert_equal 'Feature request', issue.tracker.to_s
136 assert_nil issue.category
136 assert_nil issue.category
137 assert_equal 'High', issue.priority.to_s
137 assert_equal 'High', issue.priority.to_s
138 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
138 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
139 end
139 end
140
140
141 def test_add_issue_with_spaces_between_attribute_and_separator
141 def test_add_issue_with_spaces_between_attribute_and_separator
142 issue = submit_email(
142 issue = submit_email(
143 'ticket_with_spaces_between_attribute_and_separator.eml',
143 'ticket_with_spaces_between_attribute_and_separator.eml',
144 :allow_override => 'tracker,category,priority'
144 :allow_override => 'tracker,category,priority'
145 )
145 )
146 assert issue.is_a?(Issue)
146 assert issue.is_a?(Issue)
147 assert !issue.new_record?
147 assert !issue.new_record?
148 issue.reload
148 issue.reload
149 assert_equal 'New ticket on a given project', issue.subject
149 assert_equal 'New ticket on a given project', issue.subject
150 assert_equal User.find_by_login('jsmith'), issue.author
150 assert_equal User.find_by_login('jsmith'), issue.author
151 assert_equal Project.find(2), issue.project
151 assert_equal Project.find(2), issue.project
152 assert_equal 'Feature request', issue.tracker.to_s
152 assert_equal 'Feature request', issue.tracker.to_s
153 assert_equal 'Stock management', issue.category.to_s
153 assert_equal 'Stock management', issue.category.to_s
154 assert_equal 'Urgent', issue.priority.to_s
154 assert_equal 'Urgent', issue.priority.to_s
155 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
155 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
156 end
156 end
157
157
158 def test_add_issue_with_attachment_to_specific_project
158 def test_add_issue_with_attachment_to_specific_project
159 issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
159 issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
160 assert issue.is_a?(Issue)
160 assert issue.is_a?(Issue)
161 assert !issue.new_record?
161 assert !issue.new_record?
162 issue.reload
162 issue.reload
163 assert_equal 'Ticket created by email with attachment', issue.subject
163 assert_equal 'Ticket created by email with attachment', issue.subject
164 assert_equal User.find_by_login('jsmith'), issue.author
164 assert_equal User.find_by_login('jsmith'), issue.author
165 assert_equal Project.find(2), issue.project
165 assert_equal Project.find(2), issue.project
166 assert_equal 'This is a new ticket with attachments', issue.description
166 assert_equal 'This is a new ticket with attachments', issue.description
167 # Attachment properties
167 # Attachment properties
168 assert_equal 1, issue.attachments.size
168 assert_equal 1, issue.attachments.size
169 assert_equal 'Paella.jpg', issue.attachments.first.filename
169 assert_equal 'Paella.jpg', issue.attachments.first.filename
170 assert_equal 'image/jpeg', issue.attachments.first.content_type
170 assert_equal 'image/jpeg', issue.attachments.first.content_type
171 assert_equal 10790, issue.attachments.first.filesize
171 assert_equal 10790, issue.attachments.first.filesize
172 end
172 end
173
173
174 def test_add_issue_with_custom_fields
174 def test_add_issue_with_custom_fields
175 issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'onlinestore'})
175 issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'onlinestore'})
176 assert issue.is_a?(Issue)
176 assert issue.is_a?(Issue)
177 assert !issue.new_record?
177 assert !issue.new_record?
178 issue.reload
178 issue.reload
179 assert_equal 'New ticket with custom field values', issue.subject
179 assert_equal 'New ticket with custom field values', issue.subject
180 assert_equal 'Value for a custom field',
180 assert_equal 'Value for a custom field',
181 issue.custom_value_for(CustomField.find_by_name('Searchable field')).value
181 issue.custom_value_for(CustomField.find_by_name('Searchable field')).value
182 assert !issue.description.match(/^searchable field:/i)
182 assert !issue.description.match(/^searchable field:/i)
183 end
183 end
184
184
185 def test_add_issue_with_version_custom_fields
185 def test_add_issue_with_version_custom_fields
186 field = IssueCustomField.create!(:name => 'Affected version', :field_format => 'version', :is_for_all => true, :tracker_ids => [1,2,3])
186 field = IssueCustomField.create!(:name => 'Affected version', :field_format => 'version', :is_for_all => true, :tracker_ids => [1,2,3])
187
187
188 issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'ecookbook'}) do |email|
188 issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'ecookbook'}) do |email|
189 email << "Affected version: 1.0\n"
189 email << "Affected version: 1.0\n"
190 end
190 end
191 assert issue.is_a?(Issue)
191 assert issue.is_a?(Issue)
192 assert !issue.new_record?
192 assert !issue.new_record?
193 issue.reload
193 issue.reload
194 assert_equal '2', issue.custom_field_value(field)
194 assert_equal '2', issue.custom_field_value(field)
195 end
195 end
196
196
197 def test_add_issue_should_match_assignee_on_display_name
197 def test_add_issue_should_match_assignee_on_display_name
198 user = User.generate!(:firstname => 'Foo Bar', :lastname => 'Foo Baz')
198 user = User.generate!(:firstname => 'Foo Bar', :lastname => 'Foo Baz')
199 User.add_to_project(user, Project.find(2))
199 User.add_to_project(user, Project.find(2))
200 issue = submit_email('ticket_on_given_project.eml') do |email|
200 issue = submit_email('ticket_on_given_project.eml') do |email|
201 email.sub!(/^Assigned to.*$/, 'Assigned to: Foo Bar Foo baz')
201 email.sub!(/^Assigned to.*$/, 'Assigned to: Foo Bar Foo baz')
202 end
202 end
203 assert issue.is_a?(Issue)
203 assert issue.is_a?(Issue)
204 assert_equal user, issue.assigned_to
204 assert_equal user, issue.assigned_to
205 end
205 end
206
206
207 def test_add_issue_with_cc
207 def test_add_issue_with_cc
208 issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'})
208 issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'})
209 assert issue.is_a?(Issue)
209 assert issue.is_a?(Issue)
210 assert !issue.new_record?
210 assert !issue.new_record?
211 issue.reload
211 issue.reload
212 assert issue.watched_by?(User.find_by_mail('dlopper@somenet.foo'))
212 assert issue.watched_by?(User.find_by_mail('dlopper@somenet.foo'))
213 assert_equal 1, issue.watcher_user_ids.size
213 assert_equal 1, issue.watcher_user_ids.size
214 end
214 end
215
215
216 def test_add_issue_by_unknown_user
216 def test_add_issue_by_unknown_user
217 assert_no_difference 'User.count' do
217 assert_no_difference 'User.count' do
218 assert_equal false,
218 assert_equal false,
219 submit_email(
219 submit_email(
220 'ticket_by_unknown_user.eml',
220 'ticket_by_unknown_user.eml',
221 :issue => {:project => 'ecookbook'}
221 :issue => {:project => 'ecookbook'}
222 )
222 )
223 end
223 end
224 end
224 end
225
225
226 def test_add_issue_by_anonymous_user
226 def test_add_issue_by_anonymous_user
227 Role.anonymous.add_permission!(:add_issues)
227 Role.anonymous.add_permission!(:add_issues)
228 assert_no_difference 'User.count' do
228 assert_no_difference 'User.count' do
229 issue = submit_email(
229 issue = submit_email(
230 'ticket_by_unknown_user.eml',
230 'ticket_by_unknown_user.eml',
231 :issue => {:project => 'ecookbook'},
231 :issue => {:project => 'ecookbook'},
232 :unknown_user => 'accept'
232 :unknown_user => 'accept'
233 )
233 )
234 assert issue.is_a?(Issue)
234 assert issue.is_a?(Issue)
235 assert issue.author.anonymous?
235 assert issue.author.anonymous?
236 end
236 end
237 end
237 end
238
238
239 def test_add_issue_by_anonymous_user_with_no_from_address
239 def test_add_issue_by_anonymous_user_with_no_from_address
240 Role.anonymous.add_permission!(:add_issues)
240 Role.anonymous.add_permission!(:add_issues)
241 assert_no_difference 'User.count' do
241 assert_no_difference 'User.count' do
242 issue = submit_email(
242 issue = submit_email(
243 'ticket_by_empty_user.eml',
243 'ticket_by_empty_user.eml',
244 :issue => {:project => 'ecookbook'},
244 :issue => {:project => 'ecookbook'},
245 :unknown_user => 'accept'
245 :unknown_user => 'accept'
246 )
246 )
247 assert issue.is_a?(Issue)
247 assert issue.is_a?(Issue)
248 assert issue.author.anonymous?
248 assert issue.author.anonymous?
249 end
249 end
250 end
250 end
251
251
252 def test_add_issue_by_anonymous_user_on_private_project
252 def test_add_issue_by_anonymous_user_on_private_project
253 Role.anonymous.add_permission!(:add_issues)
253 Role.anonymous.add_permission!(:add_issues)
254 assert_no_difference 'User.count' do
254 assert_no_difference 'User.count' do
255 assert_no_difference 'Issue.count' do
255 assert_no_difference 'Issue.count' do
256 assert_equal false,
256 assert_equal false,
257 submit_email(
257 submit_email(
258 'ticket_by_unknown_user.eml',
258 'ticket_by_unknown_user.eml',
259 :issue => {:project => 'onlinestore'},
259 :issue => {:project => 'onlinestore'},
260 :unknown_user => 'accept'
260 :unknown_user => 'accept'
261 )
261 )
262 end
262 end
263 end
263 end
264 end
264 end
265
265
266 def test_add_issue_by_anonymous_user_on_private_project_without_permission_check
266 def test_add_issue_by_anonymous_user_on_private_project_without_permission_check
267 assert_no_difference 'User.count' do
267 assert_no_difference 'User.count' do
268 assert_difference 'Issue.count' do
268 assert_difference 'Issue.count' do
269 issue = submit_email(
269 issue = submit_email(
270 'ticket_by_unknown_user.eml',
270 'ticket_by_unknown_user.eml',
271 :issue => {:project => 'onlinestore'},
271 :issue => {:project => 'onlinestore'},
272 :no_permission_check => '1',
272 :no_permission_check => '1',
273 :unknown_user => 'accept'
273 :unknown_user => 'accept'
274 )
274 )
275 assert issue.is_a?(Issue)
275 assert issue.is_a?(Issue)
276 assert issue.author.anonymous?
276 assert issue.author.anonymous?
277 assert !issue.project.is_public?
277 assert !issue.project.is_public?
278 assert_equal [issue.id, 1, 2], [issue.root_id, issue.lft, issue.rgt]
278 assert_equal [issue.id, 1, 2], [issue.root_id, issue.lft, issue.rgt]
279 end
279 end
280 end
280 end
281 end
281 end
282
282
283 def test_add_issue_by_created_user
283 def test_add_issue_by_created_user
284 Setting.default_language = 'en'
284 Setting.default_language = 'en'
285 assert_difference 'User.count' do
285 assert_difference 'User.count' do
286 issue = submit_email(
286 issue = submit_email(
287 'ticket_by_unknown_user.eml',
287 'ticket_by_unknown_user.eml',
288 :issue => {:project => 'ecookbook'},
288 :issue => {:project => 'ecookbook'},
289 :unknown_user => 'create'
289 :unknown_user => 'create'
290 )
290 )
291 assert issue.is_a?(Issue)
291 assert issue.is_a?(Issue)
292 assert issue.author.active?
292 assert issue.author.active?
293 assert_equal 'john.doe@somenet.foo', issue.author.mail
293 assert_equal 'john.doe@somenet.foo', issue.author.mail
294 assert_equal 'John', issue.author.firstname
294 assert_equal 'John', issue.author.firstname
295 assert_equal 'Doe', issue.author.lastname
295 assert_equal 'Doe', issue.author.lastname
296
296
297 # account information
297 # account information
298 email = ActionMailer::Base.deliveries.first
298 email = ActionMailer::Base.deliveries.first
299 assert_not_nil email
299 assert_not_nil email
300 assert email.subject.include?('account activation')
300 assert email.subject.include?('account activation')
301 login = mail_body(email).match(/\* Login: (.*)$/)[1].strip
301 login = mail_body(email).match(/\* Login: (.*)$/)[1].strip
302 password = mail_body(email).match(/\* Password: (.*)$/)[1].strip
302 password = mail_body(email).match(/\* Password: (.*)$/)[1].strip
303 assert_equal issue.author, User.try_to_login(login, password)
303 assert_equal issue.author, User.try_to_login(login, password)
304 end
304 end
305 end
305 end
306
306
307 def test_add_issue_without_from_header
307 def test_add_issue_without_from_header
308 Role.anonymous.add_permission!(:add_issues)
308 Role.anonymous.add_permission!(:add_issues)
309 assert_equal false, submit_email('ticket_without_from_header.eml')
309 assert_equal false, submit_email('ticket_without_from_header.eml')
310 end
310 end
311
311
312 def test_add_issue_with_invalid_attributes
312 def test_add_issue_with_invalid_attributes
313 issue = submit_email(
313 issue = submit_email(
314 'ticket_with_invalid_attributes.eml',
314 'ticket_with_invalid_attributes.eml',
315 :allow_override => 'tracker,category,priority'
315 :allow_override => 'tracker,category,priority'
316 )
316 )
317 assert issue.is_a?(Issue)
317 assert issue.is_a?(Issue)
318 assert !issue.new_record?
318 assert !issue.new_record?
319 issue.reload
319 issue.reload
320 assert_nil issue.assigned_to
320 assert_nil issue.assigned_to
321 assert_nil issue.start_date
321 assert_nil issue.start_date
322 assert_nil issue.due_date
322 assert_nil issue.due_date
323 assert_equal 0, issue.done_ratio
323 assert_equal 0, issue.done_ratio
324 assert_equal 'Normal', issue.priority.to_s
324 assert_equal 'Normal', issue.priority.to_s
325 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
325 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
326 end
326 end
327
327
328 def test_add_issue_with_localized_attributes
328 def test_add_issue_with_localized_attributes
329 User.find_by_mail('jsmith@somenet.foo').update_attribute 'language', 'fr'
329 User.find_by_mail('jsmith@somenet.foo').update_attribute 'language', 'fr'
330 issue = submit_email(
330 issue = submit_email(
331 'ticket_with_localized_attributes.eml',
331 'ticket_with_localized_attributes.eml',
332 :allow_override => 'tracker,category,priority'
332 :allow_override => 'tracker,category,priority'
333 )
333 )
334 assert issue.is_a?(Issue)
334 assert issue.is_a?(Issue)
335 assert !issue.new_record?
335 assert !issue.new_record?
336 issue.reload
336 issue.reload
337 assert_equal 'New ticket on a given project', issue.subject
337 assert_equal 'New ticket on a given project', issue.subject
338 assert_equal User.find_by_login('jsmith'), issue.author
338 assert_equal User.find_by_login('jsmith'), issue.author
339 assert_equal Project.find(2), issue.project
339 assert_equal Project.find(2), issue.project
340 assert_equal 'Feature request', issue.tracker.to_s
340 assert_equal 'Feature request', issue.tracker.to_s
341 assert_equal 'Stock management', issue.category.to_s
341 assert_equal 'Stock management', issue.category.to_s
342 assert_equal 'Urgent', issue.priority.to_s
342 assert_equal 'Urgent', issue.priority.to_s
343 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
343 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
344 end
344 end
345
345
346 def test_add_issue_with_japanese_keywords
346 def test_add_issue_with_japanese_keywords
347 ja_dev = "\xe9\x96\x8b\xe7\x99\xba"
347 ja_dev = "\xe9\x96\x8b\xe7\x99\xba"
348 ja_dev.force_encoding('UTF-8') if ja_dev.respond_to?(:force_encoding)
348 ja_dev.force_encoding('UTF-8') if ja_dev.respond_to?(:force_encoding)
349 tracker = Tracker.create!(:name => ja_dev)
349 tracker = Tracker.create!(:name => ja_dev)
350 Project.find(1).trackers << tracker
350 Project.find(1).trackers << tracker
351 issue = submit_email(
351 issue = submit_email(
352 'japanese_keywords_iso_2022_jp.eml',
352 'japanese_keywords_iso_2022_jp.eml',
353 :issue => {:project => 'ecookbook'},
353 :issue => {:project => 'ecookbook'},
354 :allow_override => 'tracker'
354 :allow_override => 'tracker'
355 )
355 )
356 assert_kind_of Issue, issue
356 assert_kind_of Issue, issue
357 assert_equal tracker, issue.tracker
357 assert_equal tracker, issue.tracker
358 end
358 end
359
359
360 def test_add_issue_from_apple_mail
360 def test_add_issue_from_apple_mail
361 issue = submit_email(
361 issue = submit_email(
362 'apple_mail_with_attachment.eml',
362 'apple_mail_with_attachment.eml',
363 :issue => {:project => 'ecookbook'}
363 :issue => {:project => 'ecookbook'}
364 )
364 )
365 assert_kind_of Issue, issue
365 assert_kind_of Issue, issue
366 assert_equal 1, issue.attachments.size
366 assert_equal 1, issue.attachments.size
367
367
368 attachment = issue.attachments.first
368 attachment = issue.attachments.first
369 assert_equal 'paella.jpg', attachment.filename
369 assert_equal 'paella.jpg', attachment.filename
370 assert_equal 10790, attachment.filesize
370 assert_equal 10790, attachment.filesize
371 assert File.exist?(attachment.diskfile)
371 assert File.exist?(attachment.diskfile)
372 assert_equal 10790, File.size(attachment.diskfile)
372 assert_equal 10790, File.size(attachment.diskfile)
373 assert_equal 'caaf384198bcbc9563ab5c058acd73cd', attachment.digest
373 assert_equal 'caaf384198bcbc9563ab5c058acd73cd', attachment.digest
374 end
374 end
375
375
376 def test_thunderbird_with_attachment_ja
376 def test_thunderbird_with_attachment_ja
377 issue = submit_email(
377 issue = submit_email(
378 'thunderbird_with_attachment_ja.eml',
378 'thunderbird_with_attachment_ja.eml',
379 :issue => {:project => 'ecookbook'}
379 :issue => {:project => 'ecookbook'}
380 )
380 )
381 assert_kind_of Issue, issue
381 assert_kind_of Issue, issue
382 assert_equal 1, issue.attachments.size
382 assert_equal 1, issue.attachments.size
383 ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88.txt"
383 ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88.txt"
384 ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding)
384 ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding)
385 attachment = issue.attachments.first
385 attachment = issue.attachments.first
386 assert_equal ja, attachment.filename
386 assert_equal ja, attachment.filename
387 assert_equal 5, attachment.filesize
387 assert_equal 5, attachment.filesize
388 assert File.exist?(attachment.diskfile)
388 assert File.exist?(attachment.diskfile)
389 assert_equal 5, File.size(attachment.diskfile)
389 assert_equal 5, File.size(attachment.diskfile)
390 assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest
390 assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest
391 end
391 end
392
392
393 def test_gmail_with_attachment_ja
393 def test_gmail_with_attachment_ja
394 issue = submit_email(
394 issue = submit_email(
395 'gmail_with_attachment_ja.eml',
395 'gmail_with_attachment_ja.eml',
396 :issue => {:project => 'ecookbook'}
396 :issue => {:project => 'ecookbook'}
397 )
397 )
398 assert_kind_of Issue, issue
398 assert_kind_of Issue, issue
399 assert_equal 1, issue.attachments.size
399 assert_equal 1, issue.attachments.size
400 ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88.txt"
400 ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88.txt"
401 ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding)
401 ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding)
402 attachment = issue.attachments.first
402 attachment = issue.attachments.first
403 assert_equal ja, attachment.filename
403 assert_equal ja, attachment.filename
404 assert_equal 5, attachment.filesize
404 assert_equal 5, attachment.filesize
405 assert File.exist?(attachment.diskfile)
405 assert File.exist?(attachment.diskfile)
406 assert_equal 5, File.size(attachment.diskfile)
406 assert_equal 5, File.size(attachment.diskfile)
407 assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest
407 assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest
408 end
408 end
409
409
410 def test_thunderbird_with_attachment_latin1
410 def test_thunderbird_with_attachment_latin1
411 issue = submit_email(
411 issue = submit_email(
412 'thunderbird_with_attachment_iso-8859-1.eml',
412 'thunderbird_with_attachment_iso-8859-1.eml',
413 :issue => {:project => 'ecookbook'}
413 :issue => {:project => 'ecookbook'}
414 )
414 )
415 assert_kind_of Issue, issue
415 assert_kind_of Issue, issue
416 assert_equal 1, issue.attachments.size
416 assert_equal 1, issue.attachments.size
417 u = ""
417 u = ""
418 u.force_encoding('UTF-8') if u.respond_to?(:force_encoding)
418 u.force_encoding('UTF-8') if u.respond_to?(:force_encoding)
419 u1 = "\xc3\x84\xc3\xa4\xc3\x96\xc3\xb6\xc3\x9c\xc3\xbc"
419 u1 = "\xc3\x84\xc3\xa4\xc3\x96\xc3\xb6\xc3\x9c\xc3\xbc"
420 u1.force_encoding('UTF-8') if u1.respond_to?(:force_encoding)
420 u1.force_encoding('UTF-8') if u1.respond_to?(:force_encoding)
421 11.times { u << u1 }
421 11.times { u << u1 }
422 attachment = issue.attachments.first
422 attachment = issue.attachments.first
423 assert_equal "#{u}.png", attachment.filename
423 assert_equal "#{u}.png", attachment.filename
424 assert_equal 130, attachment.filesize
424 assert_equal 130, attachment.filesize
425 assert File.exist?(attachment.diskfile)
425 assert File.exist?(attachment.diskfile)
426 assert_equal 130, File.size(attachment.diskfile)
426 assert_equal 130, File.size(attachment.diskfile)
427 assert_equal '4d80e667ac37dddfe05502530f152abb', attachment.digest
427 assert_equal '4d80e667ac37dddfe05502530f152abb', attachment.digest
428 end
428 end
429
429
430 def test_gmail_with_attachment_latin1
430 def test_gmail_with_attachment_latin1
431 issue = submit_email(
431 issue = submit_email(
432 'gmail_with_attachment_iso-8859-1.eml',
432 'gmail_with_attachment_iso-8859-1.eml',
433 :issue => {:project => 'ecookbook'}
433 :issue => {:project => 'ecookbook'}
434 )
434 )
435 assert_kind_of Issue, issue
435 assert_kind_of Issue, issue
436 assert_equal 1, issue.attachments.size
436 assert_equal 1, issue.attachments.size
437 u = ""
437 u = ""
438 u.force_encoding('UTF-8') if u.respond_to?(:force_encoding)
438 u.force_encoding('UTF-8') if u.respond_to?(:force_encoding)
439 u1 = "\xc3\x84\xc3\xa4\xc3\x96\xc3\xb6\xc3\x9c\xc3\xbc"
439 u1 = "\xc3\x84\xc3\xa4\xc3\x96\xc3\xb6\xc3\x9c\xc3\xbc"
440 u1.force_encoding('UTF-8') if u1.respond_to?(:force_encoding)
440 u1.force_encoding('UTF-8') if u1.respond_to?(:force_encoding)
441 11.times { u << u1 }
441 11.times { u << u1 }
442 attachment = issue.attachments.first
442 attachment = issue.attachments.first
443 assert_equal "#{u}.txt", attachment.filename
443 assert_equal "#{u}.txt", attachment.filename
444 assert_equal 5, attachment.filesize
444 assert_equal 5, attachment.filesize
445 assert File.exist?(attachment.diskfile)
445 assert File.exist?(attachment.diskfile)
446 assert_equal 5, File.size(attachment.diskfile)
446 assert_equal 5, File.size(attachment.diskfile)
447 assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest
447 assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest
448 end
448 end
449
449
450 def test_add_issue_with_iso_8859_1_subject
450 def test_add_issue_with_iso_8859_1_subject
451 issue = submit_email(
451 issue = submit_email(
452 'subject_as_iso-8859-1.eml',
452 'subject_as_iso-8859-1.eml',
453 :issue => {:project => 'ecookbook'}
453 :issue => {:project => 'ecookbook'}
454 )
454 )
455 assert_kind_of Issue, issue
455 assert_kind_of Issue, issue
456 assert_equal 'Testmail from Webmail: Γ€ ΓΆ ΓΌ...', issue.subject
456 assert_equal 'Testmail from Webmail: Γ€ ΓΆ ΓΌ...', issue.subject
457 end
457 end
458
458
459 def test_add_issue_with_japanese_subject
459 def test_add_issue_with_japanese_subject
460 issue = submit_email(
460 issue = submit_email(
461 'subject_japanese_1.eml',
461 'subject_japanese_1.eml',
462 :issue => {:project => 'ecookbook'}
462 :issue => {:project => 'ecookbook'}
463 )
463 )
464 assert_kind_of Issue, issue
464 assert_kind_of Issue, issue
465 ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88"
465 ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88"
466 ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding)
466 ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding)
467 assert_equal ja, issue.subject
467 assert_equal ja, issue.subject
468 end
468 end
469
469
470 def test_add_issue_with_no_subject_header
471 issue = submit_email(
472 'no_subject_header.eml',
473 :issue => {:project => 'ecookbook'}
474 )
475 assert_kind_of Issue, issue
476 assert_equal '(no subject)', issue.subject
477 end
478
470 def test_add_issue_with_mixed_japanese_subject
479 def test_add_issue_with_mixed_japanese_subject
471 issue = submit_email(
480 issue = submit_email(
472 'subject_japanese_2.eml',
481 'subject_japanese_2.eml',
473 :issue => {:project => 'ecookbook'}
482 :issue => {:project => 'ecookbook'}
474 )
483 )
475 assert_kind_of Issue, issue
484 assert_kind_of Issue, issue
476 ja = "Re: \xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88"
485 ja = "Re: \xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88"
477 ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding)
486 ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding)
478 assert_equal ja, issue.subject
487 assert_equal ja, issue.subject
479 end
488 end
480
489
481 def test_should_ignore_emails_from_locked_users
490 def test_should_ignore_emails_from_locked_users
482 User.find(2).lock!
491 User.find(2).lock!
483
492
484 MailHandler.any_instance.expects(:dispatch).never
493 MailHandler.any_instance.expects(:dispatch).never
485 assert_no_difference 'Issue.count' do
494 assert_no_difference 'Issue.count' do
486 assert_equal false, submit_email('ticket_on_given_project.eml')
495 assert_equal false, submit_email('ticket_on_given_project.eml')
487 end
496 end
488 end
497 end
489
498
490 def test_should_ignore_emails_from_emission_address
499 def test_should_ignore_emails_from_emission_address
491 Role.anonymous.add_permission!(:add_issues)
500 Role.anonymous.add_permission!(:add_issues)
492 assert_no_difference 'User.count' do
501 assert_no_difference 'User.count' do
493 assert_equal false,
502 assert_equal false,
494 submit_email(
503 submit_email(
495 'ticket_from_emission_address.eml',
504 'ticket_from_emission_address.eml',
496 :issue => {:project => 'ecookbook'},
505 :issue => {:project => 'ecookbook'},
497 :unknown_user => 'create'
506 :unknown_user => 'create'
498 )
507 )
499 end
508 end
500 end
509 end
501
510
502 def test_should_ignore_auto_replied_emails
511 def test_should_ignore_auto_replied_emails
503 MailHandler.any_instance.expects(:dispatch).never
512 MailHandler.any_instance.expects(:dispatch).never
504 [
513 [
505 "X-Auto-Response-Suppress: OOF",
514 "X-Auto-Response-Suppress: OOF",
506 "Auto-Submitted: auto-replied",
515 "Auto-Submitted: auto-replied",
507 "Auto-Submitted: Auto-Replied",
516 "Auto-Submitted: Auto-Replied",
508 "Auto-Submitted: auto-generated"
517 "Auto-Submitted: auto-generated"
509 ].each do |header|
518 ].each do |header|
510 raw = IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml'))
519 raw = IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml'))
511 raw = header + "\n" + raw
520 raw = header + "\n" + raw
512
521
513 assert_no_difference 'Issue.count' do
522 assert_no_difference 'Issue.count' do
514 assert_equal false, MailHandler.receive(raw), "email with #{header} header was not ignored"
523 assert_equal false, MailHandler.receive(raw), "email with #{header} header was not ignored"
515 end
524 end
516 end
525 end
517 end
526 end
518
527
519 def test_add_issue_should_send_email_notification
528 def test_add_issue_should_send_email_notification
520 Setting.notified_events = ['issue_added']
529 Setting.notified_events = ['issue_added']
521 ActionMailer::Base.deliveries.clear
530 ActionMailer::Base.deliveries.clear
522 # This email contains: 'Project: onlinestore'
531 # This email contains: 'Project: onlinestore'
523 issue = submit_email('ticket_on_given_project.eml')
532 issue = submit_email('ticket_on_given_project.eml')
524 assert issue.is_a?(Issue)
533 assert issue.is_a?(Issue)
525 assert_equal 1, ActionMailer::Base.deliveries.size
534 assert_equal 1, ActionMailer::Base.deliveries.size
526 end
535 end
527
536
528 def test_update_issue
537 def test_update_issue
529 journal = submit_email('ticket_reply.eml')
538 journal = submit_email('ticket_reply.eml')
530 assert journal.is_a?(Journal)
539 assert journal.is_a?(Journal)
531 assert_equal User.find_by_login('jsmith'), journal.user
540 assert_equal User.find_by_login('jsmith'), journal.user
532 assert_equal Issue.find(2), journal.journalized
541 assert_equal Issue.find(2), journal.journalized
533 assert_match /This is reply/, journal.notes
542 assert_match /This is reply/, journal.notes
534 assert_equal 'Feature request', journal.issue.tracker.name
543 assert_equal 'Feature request', journal.issue.tracker.name
535 end
544 end
536
545
537 def test_update_issue_with_attribute_changes
546 def test_update_issue_with_attribute_changes
538 # This email contains: 'Status: Resolved'
547 # This email contains: 'Status: Resolved'
539 journal = submit_email('ticket_reply_with_status.eml')
548 journal = submit_email('ticket_reply_with_status.eml')
540 assert journal.is_a?(Journal)
549 assert journal.is_a?(Journal)
541 issue = Issue.find(journal.issue.id)
550 issue = Issue.find(journal.issue.id)
542 assert_equal User.find_by_login('jsmith'), journal.user
551 assert_equal User.find_by_login('jsmith'), journal.user
543 assert_equal Issue.find(2), journal.journalized
552 assert_equal Issue.find(2), journal.journalized
544 assert_match /This is reply/, journal.notes
553 assert_match /This is reply/, journal.notes
545 assert_equal 'Feature request', journal.issue.tracker.name
554 assert_equal 'Feature request', journal.issue.tracker.name
546 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
555 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
547 assert_equal '2010-01-01', issue.start_date.to_s
556 assert_equal '2010-01-01', issue.start_date.to_s
548 assert_equal '2010-12-31', issue.due_date.to_s
557 assert_equal '2010-12-31', issue.due_date.to_s
549 assert_equal User.find_by_login('jsmith'), issue.assigned_to
558 assert_equal User.find_by_login('jsmith'), issue.assigned_to
550 assert_equal "52.6", issue.custom_value_for(CustomField.find_by_name('Float field')).value
559 assert_equal "52.6", issue.custom_value_for(CustomField.find_by_name('Float field')).value
551 # keywords should be removed from the email body
560 # keywords should be removed from the email body
552 assert !journal.notes.match(/^Status:/i)
561 assert !journal.notes.match(/^Status:/i)
553 assert !journal.notes.match(/^Start Date:/i)
562 assert !journal.notes.match(/^Start Date:/i)
554 end
563 end
555
564
556 def test_update_issue_with_attachment
565 def test_update_issue_with_attachment
557 assert_difference 'Journal.count' do
566 assert_difference 'Journal.count' do
558 assert_difference 'JournalDetail.count' do
567 assert_difference 'JournalDetail.count' do
559 assert_difference 'Attachment.count' do
568 assert_difference 'Attachment.count' do
560 assert_no_difference 'Issue.count' do
569 assert_no_difference 'Issue.count' do
561 journal = submit_email('ticket_with_attachment.eml') do |raw|
570 journal = submit_email('ticket_with_attachment.eml') do |raw|
562 raw.gsub! /^Subject: .*$/, 'Subject: Re: [Cookbook - Feature #2] (New) Add ingredients categories'
571 raw.gsub! /^Subject: .*$/, 'Subject: Re: [Cookbook - Feature #2] (New) Add ingredients categories'
563 end
572 end
564 end
573 end
565 end
574 end
566 end
575 end
567 end
576 end
568 journal = Journal.first(:order => 'id DESC')
577 journal = Journal.first(:order => 'id DESC')
569 assert_equal Issue.find(2), journal.journalized
578 assert_equal Issue.find(2), journal.journalized
570 assert_equal 1, journal.details.size
579 assert_equal 1, journal.details.size
571
580
572 detail = journal.details.first
581 detail = journal.details.first
573 assert_equal 'attachment', detail.property
582 assert_equal 'attachment', detail.property
574 assert_equal 'Paella.jpg', detail.value
583 assert_equal 'Paella.jpg', detail.value
575 end
584 end
576
585
577 def test_update_issue_should_send_email_notification
586 def test_update_issue_should_send_email_notification
578 ActionMailer::Base.deliveries.clear
587 ActionMailer::Base.deliveries.clear
579 journal = submit_email('ticket_reply.eml')
588 journal = submit_email('ticket_reply.eml')
580 assert journal.is_a?(Journal)
589 assert journal.is_a?(Journal)
581 assert_equal 1, ActionMailer::Base.deliveries.size
590 assert_equal 1, ActionMailer::Base.deliveries.size
582 end
591 end
583
592
584 def test_update_issue_should_not_set_defaults
593 def test_update_issue_should_not_set_defaults
585 journal = submit_email(
594 journal = submit_email(
586 'ticket_reply.eml',
595 'ticket_reply.eml',
587 :issue => {:tracker => 'Support request', :priority => 'High'}
596 :issue => {:tracker => 'Support request', :priority => 'High'}
588 )
597 )
589 assert journal.is_a?(Journal)
598 assert journal.is_a?(Journal)
590 assert_match /This is reply/, journal.notes
599 assert_match /This is reply/, journal.notes
591 assert_equal 'Feature request', journal.issue.tracker.name
600 assert_equal 'Feature request', journal.issue.tracker.name
592 assert_equal 'Normal', journal.issue.priority.name
601 assert_equal 'Normal', journal.issue.priority.name
593 end
602 end
594
603
595 def test_reply_to_a_message
604 def test_reply_to_a_message
596 m = submit_email('message_reply.eml')
605 m = submit_email('message_reply.eml')
597 assert m.is_a?(Message)
606 assert m.is_a?(Message)
598 assert !m.new_record?
607 assert !m.new_record?
599 m.reload
608 m.reload
600 assert_equal 'Reply via email', m.subject
609 assert_equal 'Reply via email', m.subject
601 # The email replies to message #2 which is part of the thread of message #1
610 # The email replies to message #2 which is part of the thread of message #1
602 assert_equal Message.find(1), m.parent
611 assert_equal Message.find(1), m.parent
603 end
612 end
604
613
605 def test_reply_to_a_message_by_subject
614 def test_reply_to_a_message_by_subject
606 m = submit_email('message_reply_by_subject.eml')
615 m = submit_email('message_reply_by_subject.eml')
607 assert m.is_a?(Message)
616 assert m.is_a?(Message)
608 assert !m.new_record?
617 assert !m.new_record?
609 m.reload
618 m.reload
610 assert_equal 'Reply to the first post', m.subject
619 assert_equal 'Reply to the first post', m.subject
611 assert_equal Message.find(1), m.parent
620 assert_equal Message.find(1), m.parent
612 end
621 end
613
622
614 def test_should_strip_tags_of_html_only_emails
623 def test_should_strip_tags_of_html_only_emails
615 issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'})
624 issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'})
616 assert issue.is_a?(Issue)
625 assert issue.is_a?(Issue)
617 assert !issue.new_record?
626 assert !issue.new_record?
618 issue.reload
627 issue.reload
619 assert_equal 'HTML email', issue.subject
628 assert_equal 'HTML email', issue.subject
620 assert_equal 'This is a html-only email.', issue.description
629 assert_equal 'This is a html-only email.', issue.description
621 end
630 end
622
631
623 context "truncate emails based on the Setting" do
632 context "truncate emails based on the Setting" do
624 context "with no setting" do
633 context "with no setting" do
625 setup do
634 setup do
626 Setting.mail_handler_body_delimiters = ''
635 Setting.mail_handler_body_delimiters = ''
627 end
636 end
628
637
629 should "add the entire email into the issue" do
638 should "add the entire email into the issue" do
630 issue = submit_email('ticket_on_given_project.eml')
639 issue = submit_email('ticket_on_given_project.eml')
631 assert_issue_created(issue)
640 assert_issue_created(issue)
632 assert issue.description.include?('---')
641 assert issue.description.include?('---')
633 assert issue.description.include?('This paragraph is after the delimiter')
642 assert issue.description.include?('This paragraph is after the delimiter')
634 end
643 end
635 end
644 end
636
645
637 context "with a single string" do
646 context "with a single string" do
638 setup do
647 setup do
639 Setting.mail_handler_body_delimiters = '---'
648 Setting.mail_handler_body_delimiters = '---'
640 end
649 end
641 should "truncate the email at the delimiter for the issue" do
650 should "truncate the email at the delimiter for the issue" do
642 issue = submit_email('ticket_on_given_project.eml')
651 issue = submit_email('ticket_on_given_project.eml')
643 assert_issue_created(issue)
652 assert_issue_created(issue)
644 assert issue.description.include?('This paragraph is before delimiters')
653 assert issue.description.include?('This paragraph is before delimiters')
645 assert issue.description.include?('--- This line starts with a delimiter')
654 assert issue.description.include?('--- This line starts with a delimiter')
646 assert !issue.description.match(/^---$/)
655 assert !issue.description.match(/^---$/)
647 assert !issue.description.include?('This paragraph is after the delimiter')
656 assert !issue.description.include?('This paragraph is after the delimiter')
648 end
657 end
649 end
658 end
650
659
651 context "with a single quoted reply (e.g. reply to a Redmine email notification)" do
660 context "with a single quoted reply (e.g. reply to a Redmine email notification)" do
652 setup do
661 setup do
653 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
662 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
654 end
663 end
655 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
664 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
656 journal = submit_email('issue_update_with_quoted_reply_above.eml')
665 journal = submit_email('issue_update_with_quoted_reply_above.eml')
657 assert journal.is_a?(Journal)
666 assert journal.is_a?(Journal)
658 assert journal.notes.include?('An update to the issue by the sender.')
667 assert journal.notes.include?('An update to the issue by the sender.')
659 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
668 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
660 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
669 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
661 end
670 end
662 end
671 end
663
672
664 context "with multiple quoted replies (e.g. reply to a reply of a Redmine email notification)" do
673 context "with multiple quoted replies (e.g. reply to a reply of a Redmine email notification)" do
665 setup do
674 setup do
666 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
675 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
667 end
676 end
668 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
677 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
669 journal = submit_email('issue_update_with_multiple_quoted_reply_above.eml')
678 journal = submit_email('issue_update_with_multiple_quoted_reply_above.eml')
670 assert journal.is_a?(Journal)
679 assert journal.is_a?(Journal)
671 assert journal.notes.include?('An update to the issue by the sender.')
680 assert journal.notes.include?('An update to the issue by the sender.')
672 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
681 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
673 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
682 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
674 end
683 end
675 end
684 end
676
685
677 context "with multiple strings" do
686 context "with multiple strings" do
678 setup do
687 setup do
679 Setting.mail_handler_body_delimiters = "---\nBREAK"
688 Setting.mail_handler_body_delimiters = "---\nBREAK"
680 end
689 end
681 should "truncate the email at the first delimiter found (BREAK)" do
690 should "truncate the email at the first delimiter found (BREAK)" do
682 issue = submit_email('ticket_on_given_project.eml')
691 issue = submit_email('ticket_on_given_project.eml')
683 assert_issue_created(issue)
692 assert_issue_created(issue)
684 assert issue.description.include?('This paragraph is before delimiters')
693 assert issue.description.include?('This paragraph is before delimiters')
685 assert !issue.description.include?('BREAK')
694 assert !issue.description.include?('BREAK')
686 assert !issue.description.include?('This paragraph is between delimiters')
695 assert !issue.description.include?('This paragraph is between delimiters')
687 assert !issue.description.match(/^---$/)
696 assert !issue.description.match(/^---$/)
688 assert !issue.description.include?('This paragraph is after the delimiter')
697 assert !issue.description.include?('This paragraph is after the delimiter')
689 end
698 end
690 end
699 end
691 end
700 end
692
701
693 def test_email_with_long_subject_line
702 def test_email_with_long_subject_line
694 issue = submit_email('ticket_with_long_subject.eml')
703 issue = submit_email('ticket_with_long_subject.eml')
695 assert issue.is_a?(Issue)
704 assert issue.is_a?(Issue)
696 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]
705 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]
697 end
706 end
698
707
699 def test_new_user_from_attributes_should_return_valid_user
708 def test_new_user_from_attributes_should_return_valid_user
700 to_test = {
709 to_test = {
701 # [address, name] => [login, firstname, lastname]
710 # [address, name] => [login, firstname, lastname]
702 ['jsmith@example.net', nil] => ['jsmith@example.net', 'jsmith', '-'],
711 ['jsmith@example.net', nil] => ['jsmith@example.net', 'jsmith', '-'],
703 ['jsmith@example.net', 'John'] => ['jsmith@example.net', 'John', '-'],
712 ['jsmith@example.net', 'John'] => ['jsmith@example.net', 'John', '-'],
704 ['jsmith@example.net', 'John Smith'] => ['jsmith@example.net', 'John', 'Smith'],
713 ['jsmith@example.net', 'John Smith'] => ['jsmith@example.net', 'John', 'Smith'],
705 ['jsmith@example.net', 'John Paul Smith'] => ['jsmith@example.net', 'John', 'Paul Smith'],
714 ['jsmith@example.net', 'John Paul Smith'] => ['jsmith@example.net', 'John', 'Paul Smith'],
706 ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsTheMaximumLength Smith'] => ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsT', 'Smith'],
715 ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsTheMaximumLength Smith'] => ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsT', 'Smith'],
707 ['jsmith@example.net', 'John AVeryLongLastnameThatExceedsTheMaximumLength'] => ['jsmith@example.net', 'John', 'AVeryLongLastnameThatExceedsTh']
716 ['jsmith@example.net', 'John AVeryLongLastnameThatExceedsTheMaximumLength'] => ['jsmith@example.net', 'John', 'AVeryLongLastnameThatExceedsTh']
708 }
717 }
709
718
710 to_test.each do |attrs, expected|
719 to_test.each do |attrs, expected|
711 user = MailHandler.new_user_from_attributes(attrs.first, attrs.last)
720 user = MailHandler.new_user_from_attributes(attrs.first, attrs.last)
712
721
713 assert user.valid?, user.errors.full_messages.to_s
722 assert user.valid?, user.errors.full_messages.to_s
714 assert_equal attrs.first, user.mail
723 assert_equal attrs.first, user.mail
715 assert_equal expected[0], user.login
724 assert_equal expected[0], user.login
716 assert_equal expected[1], user.firstname
725 assert_equal expected[1], user.firstname
717 assert_equal expected[2], user.lastname
726 assert_equal expected[2], user.lastname
718 end
727 end
719 end
728 end
720
729
721 def test_new_user_from_attributes_should_respect_minimum_password_length
730 def test_new_user_from_attributes_should_respect_minimum_password_length
722 with_settings :password_min_length => 15 do
731 with_settings :password_min_length => 15 do
723 user = MailHandler.new_user_from_attributes('jsmith@example.net')
732 user = MailHandler.new_user_from_attributes('jsmith@example.net')
724 assert user.valid?
733 assert user.valid?
725 assert user.password.length >= 15
734 assert user.password.length >= 15
726 end
735 end
727 end
736 end
728
737
729 def test_new_user_from_attributes_should_use_default_login_if_invalid
738 def test_new_user_from_attributes_should_use_default_login_if_invalid
730 user = MailHandler.new_user_from_attributes('foo+bar@example.net')
739 user = MailHandler.new_user_from_attributes('foo+bar@example.net')
731 assert user.valid?
740 assert user.valid?
732 assert user.login =~ /^user[a-f0-9]+$/
741 assert user.login =~ /^user[a-f0-9]+$/
733 assert_equal 'foo+bar@example.net', user.mail
742 assert_equal 'foo+bar@example.net', user.mail
734 end
743 end
735
744
736 def test_new_user_with_utf8_encoded_fullname_should_be_decoded
745 def test_new_user_with_utf8_encoded_fullname_should_be_decoded
737 assert_difference 'User.count' do
746 assert_difference 'User.count' do
738 issue = submit_email(
747 issue = submit_email(
739 'fullname_of_sender_as_utf8_encoded.eml',
748 'fullname_of_sender_as_utf8_encoded.eml',
740 :issue => {:project => 'ecookbook'},
749 :issue => {:project => 'ecookbook'},
741 :unknown_user => 'create'
750 :unknown_user => 'create'
742 )
751 )
743 end
752 end
744
753
745 user = User.first(:order => 'id DESC')
754 user = User.first(:order => 'id DESC')
746 assert_equal "foo@example.org", user.mail
755 assert_equal "foo@example.org", user.mail
747 str1 = "\xc3\x84\xc3\xa4"
756 str1 = "\xc3\x84\xc3\xa4"
748 str2 = "\xc3\x96\xc3\xb6"
757 str2 = "\xc3\x96\xc3\xb6"
749 str1.force_encoding('UTF-8') if str1.respond_to?(:force_encoding)
758 str1.force_encoding('UTF-8') if str1.respond_to?(:force_encoding)
750 str2.force_encoding('UTF-8') if str2.respond_to?(:force_encoding)
759 str2.force_encoding('UTF-8') if str2.respond_to?(:force_encoding)
751 assert_equal str1, user.firstname
760 assert_equal str1, user.firstname
752 assert_equal str2, user.lastname
761 assert_equal str2, user.lastname
753 end
762 end
754
763
755 private
764 private
756
765
757 def submit_email(filename, options={})
766 def submit_email(filename, options={})
758 raw = IO.read(File.join(FIXTURES_PATH, filename))
767 raw = IO.read(File.join(FIXTURES_PATH, filename))
759 yield raw if block_given?
768 yield raw if block_given?
760 MailHandler.receive(raw, options)
769 MailHandler.receive(raw, options)
761 end
770 end
762
771
763 def assert_issue_created(issue)
772 def assert_issue_created(issue)
764 assert issue.is_a?(Issue)
773 assert issue.is_a?(Issue)
765 assert !issue.new_record?
774 assert !issue.new_record?
766 issue.reload
775 issue.reload
767 end
776 end
768 end
777 end
General Comments 0
You need to be logged in to leave comments. Login now