##// END OF EJS Templates
fix receiving mail subject broken which does not begin with encoding name (#12375)...
Toshi MARUYAMA -
r10585:617cb7ac53ee
parent child
Show More
@@ -0,0 +1,7
1 From: John Smith <JSmith@somenet.foo>
2 To: "redmine@somenet.foo" <redmine@somenet.foo>
3 Subject: Re: =?iso-2022-jp?b?GyRCJUYlOSVIGyhCCg=?=
4 Date: Fri, 1 Jun 2012 14:39:38 +0200
5 Message-ID: <87C31D42249DD0489D1A1444E3232DD7019D6183@foo.bar>
6
7 Fixture
@@ -1,479 +1,480
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 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
127 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
128 klass, object_id = $1, $2.to_i
128 klass, object_id = $1, $2.to_i
129 method_name = "receive_#{klass}_reply"
129 method_name = "receive_#{klass}_reply"
130 if self.class.private_instance_methods.collect(&:to_s).include?(method_name)
130 if self.class.private_instance_methods.collect(&:to_s).include?(method_name)
131 send method_name, object_id
131 send method_name, object_id
132 else
132 else
133 # ignoring it
133 # ignoring it
134 end
134 end
135 elsif m = email.subject.match(ISSUE_REPLY_SUBJECT_RE)
135 elsif m = email.subject.match(ISSUE_REPLY_SUBJECT_RE)
136 receive_issue_reply(m[1].to_i)
136 receive_issue_reply(m[1].to_i)
137 elsif m = email.subject.match(MESSAGE_REPLY_SUBJECT_RE)
137 elsif m = email.subject.match(MESSAGE_REPLY_SUBJECT_RE)
138 receive_message_reply(m[1].to_i)
138 receive_message_reply(m[1].to_i)
139 else
139 else
140 dispatch_to_default
140 dispatch_to_default
141 end
141 end
142 rescue ActiveRecord::RecordInvalid => e
142 rescue ActiveRecord::RecordInvalid => e
143 # TODO: send a email to the user
143 # TODO: send a email to the user
144 logger.error e.message if logger
144 logger.error e.message if logger
145 false
145 false
146 rescue MissingInformation => e
146 rescue MissingInformation => e
147 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
147 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
148 false
148 false
149 rescue UnauthorizedAction => e
149 rescue UnauthorizedAction => e
150 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
150 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
151 false
151 false
152 end
152 end
153
153
154 def dispatch_to_default
154 def dispatch_to_default
155 receive_issue
155 receive_issue
156 end
156 end
157
157
158 # Creates a new issue
158 # Creates a new issue
159 def receive_issue
159 def receive_issue
160 project = target_project
160 project = target_project
161 # check permission
161 # check permission
162 unless @@handler_options[:no_permission_check]
162 unless @@handler_options[:no_permission_check]
163 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
163 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
164 end
164 end
165
165
166 issue = Issue.new(:author => user, :project => project)
166 issue = Issue.new(:author => user, :project => project)
167 issue.safe_attributes = issue_attributes_from_keywords(issue)
167 issue.safe_attributes = issue_attributes_from_keywords(issue)
168 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
168 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
169 issue.subject = cleaned_up_subject
169 issue.subject = cleaned_up_subject
170 if issue.subject.blank?
170 if issue.subject.blank?
171 issue.subject = '(no subject)'
171 issue.subject = '(no subject)'
172 end
172 end
173 issue.description = cleaned_up_text_body
173 issue.description = cleaned_up_text_body
174
174
175 # add To and Cc as watchers before saving so the watchers can reply to Redmine
175 # add To and Cc as watchers before saving so the watchers can reply to Redmine
176 add_watchers(issue)
176 add_watchers(issue)
177 issue.save!
177 issue.save!
178 add_attachments(issue)
178 add_attachments(issue)
179 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
179 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
180 issue
180 issue
181 end
181 end
182
182
183 # Adds a note to an existing issue
183 # Adds a note to an existing issue
184 def receive_issue_reply(issue_id, from_journal=nil)
184 def receive_issue_reply(issue_id, from_journal=nil)
185 issue = Issue.find_by_id(issue_id)
185 issue = Issue.find_by_id(issue_id)
186 return unless issue
186 return unless issue
187 # check permission
187 # check permission
188 unless @@handler_options[:no_permission_check]
188 unless @@handler_options[:no_permission_check]
189 unless user.allowed_to?(:add_issue_notes, issue.project) ||
189 unless user.allowed_to?(:add_issue_notes, issue.project) ||
190 user.allowed_to?(:edit_issues, issue.project)
190 user.allowed_to?(:edit_issues, issue.project)
191 raise UnauthorizedAction
191 raise UnauthorizedAction
192 end
192 end
193 end
193 end
194
194
195 # ignore CLI-supplied defaults for new issues
195 # ignore CLI-supplied defaults for new issues
196 @@handler_options[:issue].clear
196 @@handler_options[:issue].clear
197
197
198 journal = issue.init_journal(user)
198 journal = issue.init_journal(user)
199 if from_journal && from_journal.private_notes?
199 if from_journal && from_journal.private_notes?
200 # If the received email was a reply to a private note, make the added note private
200 # If the received email was a reply to a private note, make the added note private
201 issue.private_notes = true
201 issue.private_notes = true
202 end
202 end
203 issue.safe_attributes = issue_attributes_from_keywords(issue)
203 issue.safe_attributes = issue_attributes_from_keywords(issue)
204 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
204 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
205 journal.notes = cleaned_up_text_body
205 journal.notes = cleaned_up_text_body
206 add_attachments(issue)
206 add_attachments(issue)
207 issue.save!
207 issue.save!
208 if logger && logger.info
208 if logger && logger.info
209 logger.info "MailHandler: issue ##{issue.id} updated by #{user}"
209 logger.info "MailHandler: issue ##{issue.id} updated by #{user}"
210 end
210 end
211 journal
211 journal
212 end
212 end
213
213
214 # Reply will be added to the issue
214 # Reply will be added to the issue
215 def receive_journal_reply(journal_id)
215 def receive_journal_reply(journal_id)
216 journal = Journal.find_by_id(journal_id)
216 journal = Journal.find_by_id(journal_id)
217 if journal && journal.journalized_type == 'Issue'
217 if journal && journal.journalized_type == 'Issue'
218 receive_issue_reply(journal.journalized_id, journal)
218 receive_issue_reply(journal.journalized_id, journal)
219 end
219 end
220 end
220 end
221
221
222 # Receives a reply to a forum message
222 # Receives a reply to a forum message
223 def receive_message_reply(message_id)
223 def receive_message_reply(message_id)
224 message = Message.find_by_id(message_id)
224 message = Message.find_by_id(message_id)
225 if message
225 if message
226 message = message.root
226 message = message.root
227
227
228 unless @@handler_options[:no_permission_check]
228 unless @@handler_options[:no_permission_check]
229 raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
229 raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
230 end
230 end
231
231
232 if !message.locked?
232 if !message.locked?
233 reply = Message.new(:subject => cleaned_up_subject.gsub(%r{^.*msg\d+\]}, '').strip,
233 reply = Message.new(:subject => cleaned_up_subject.gsub(%r{^.*msg\d+\]}, '').strip,
234 :content => cleaned_up_text_body)
234 :content => cleaned_up_text_body)
235 reply.author = user
235 reply.author = user
236 reply.board = message.board
236 reply.board = message.board
237 message.children << reply
237 message.children << reply
238 add_attachments(reply)
238 add_attachments(reply)
239 reply
239 reply
240 else
240 else
241 if logger && logger.info
241 if logger && logger.info
242 logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic"
242 logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic"
243 end
243 end
244 end
244 end
245 end
245 end
246 end
246 end
247
247
248 def add_attachments(obj)
248 def add_attachments(obj)
249 if email.attachments && email.attachments.any?
249 if email.attachments && email.attachments.any?
250 email.attachments.each do |attachment|
250 email.attachments.each do |attachment|
251 obj.attachments << Attachment.create(:container => obj,
251 obj.attachments << Attachment.create(:container => obj,
252 :file => attachment.decoded,
252 :file => attachment.decoded,
253 :filename => attachment.filename,
253 :filename => attachment.filename,
254 :author => user,
254 :author => user,
255 :content_type => attachment.mime_type)
255 :content_type => attachment.mime_type)
256 end
256 end
257 end
257 end
258 end
258 end
259
259
260 # Adds To and Cc as watchers of the given object if the sender has the
260 # Adds To and Cc as watchers of the given object if the sender has the
261 # appropriate permission
261 # appropriate permission
262 def add_watchers(obj)
262 def add_watchers(obj)
263 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
263 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
264 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
264 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
265 unless addresses.empty?
265 unless addresses.empty?
266 watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
266 watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
267 watchers.each {|w| obj.add_watcher(w)}
267 watchers.each {|w| obj.add_watcher(w)}
268 end
268 end
269 end
269 end
270 end
270 end
271
271
272 def get_keyword(attr, options={})
272 def get_keyword(attr, options={})
273 @keywords ||= {}
273 @keywords ||= {}
274 if @keywords.has_key?(attr)
274 if @keywords.has_key?(attr)
275 @keywords[attr]
275 @keywords[attr]
276 else
276 else
277 @keywords[attr] = begin
277 @keywords[attr] = begin
278 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) &&
278 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) &&
279 (v = extract_keyword!(plain_text_body, attr, options[:format]))
279 (v = extract_keyword!(plain_text_body, attr, options[:format]))
280 v
280 v
281 elsif !@@handler_options[:issue][attr].blank?
281 elsif !@@handler_options[:issue][attr].blank?
282 @@handler_options[:issue][attr]
282 @@handler_options[:issue][attr]
283 end
283 end
284 end
284 end
285 end
285 end
286 end
286 end
287
287
288 # Destructively extracts the value for +attr+ in +text+
288 # Destructively extracts the value for +attr+ in +text+
289 # Returns nil if no matching keyword found
289 # Returns nil if no matching keyword found
290 def extract_keyword!(text, attr, format=nil)
290 def extract_keyword!(text, attr, format=nil)
291 keys = [attr.to_s.humanize]
291 keys = [attr.to_s.humanize]
292 if attr.is_a?(Symbol)
292 if attr.is_a?(Symbol)
293 if user && user.language.present?
293 if user && user.language.present?
294 keys << l("field_#{attr}", :default => '', :locale => user.language)
294 keys << l("field_#{attr}", :default => '', :locale => user.language)
295 end
295 end
296 if Setting.default_language.present?
296 if Setting.default_language.present?
297 keys << l("field_#{attr}", :default => '', :locale => Setting.default_language)
297 keys << l("field_#{attr}", :default => '', :locale => Setting.default_language)
298 end
298 end
299 end
299 end
300 keys.reject! {|k| k.blank?}
300 keys.reject! {|k| k.blank?}
301 keys.collect! {|k| Regexp.escape(k)}
301 keys.collect! {|k| Regexp.escape(k)}
302 format ||= '.+'
302 format ||= '.+'
303 keyword = nil
303 keyword = nil
304 regexp = /^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i
304 regexp = /^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i
305 if m = text.match(regexp)
305 if m = text.match(regexp)
306 keyword = m[2].strip
306 keyword = m[2].strip
307 text.gsub!(regexp, '')
307 text.gsub!(regexp, '')
308 end
308 end
309 keyword
309 keyword
310 end
310 end
311
311
312 def target_project
312 def target_project
313 # TODO: other ways to specify project:
313 # TODO: other ways to specify project:
314 # * parse the email To field
314 # * parse the email To field
315 # * specific project (eg. Setting.mail_handler_target_project)
315 # * specific project (eg. Setting.mail_handler_target_project)
316 target = Project.find_by_identifier(get_keyword(:project))
316 target = Project.find_by_identifier(get_keyword(:project))
317 raise MissingInformation.new('Unable to determine target project') if target.nil?
317 raise MissingInformation.new('Unable to determine target project') if target.nil?
318 target
318 target
319 end
319 end
320
320
321 # Returns a Hash of issue attributes extracted from keywords in the email body
321 # Returns a Hash of issue attributes extracted from keywords in the email body
322 def issue_attributes_from_keywords(issue)
322 def issue_attributes_from_keywords(issue)
323 assigned_to = (k = get_keyword(:assigned_to, :override => true)) && find_assignee_from_keyword(k, issue)
323 assigned_to = (k = get_keyword(:assigned_to, :override => true)) && find_assignee_from_keyword(k, issue)
324
324
325 attrs = {
325 attrs = {
326 'tracker_id' => (k = get_keyword(:tracker)) && issue.project.trackers.named(k).first.try(:id),
326 'tracker_id' => (k = get_keyword(:tracker)) && issue.project.trackers.named(k).first.try(:id),
327 'status_id' => (k = get_keyword(:status)) && IssueStatus.named(k).first.try(:id),
327 'status_id' => (k = get_keyword(:status)) && IssueStatus.named(k).first.try(:id),
328 'priority_id' => (k = get_keyword(:priority)) && IssuePriority.named(k).first.try(:id),
328 'priority_id' => (k = get_keyword(:priority)) && IssuePriority.named(k).first.try(:id),
329 'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.named(k).first.try(:id),
329 'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.named(k).first.try(:id),
330 'assigned_to_id' => assigned_to.try(:id),
330 'assigned_to_id' => assigned_to.try(:id),
331 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) &&
331 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) &&
332 issue.project.shared_versions.named(k).first.try(:id),
332 issue.project.shared_versions.named(k).first.try(:id),
333 'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
333 'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
334 'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
334 'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
335 'estimated_hours' => get_keyword(:estimated_hours, :override => true),
335 'estimated_hours' => get_keyword(:estimated_hours, :override => true),
336 'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0')
336 'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0')
337 }.delete_if {|k, v| v.blank? }
337 }.delete_if {|k, v| v.blank? }
338
338
339 if issue.new_record? && attrs['tracker_id'].nil?
339 if issue.new_record? && attrs['tracker_id'].nil?
340 attrs['tracker_id'] = issue.project.trackers.find(:first).try(:id)
340 attrs['tracker_id'] = issue.project.trackers.find(:first).try(:id)
341 end
341 end
342
342
343 attrs
343 attrs
344 end
344 end
345
345
346 # Returns a Hash of issue custom field values extracted from keywords in the email body
346 # Returns a Hash of issue custom field values extracted from keywords in the email body
347 def custom_field_values_from_keywords(customized)
347 def custom_field_values_from_keywords(customized)
348 customized.custom_field_values.inject({}) do |h, v|
348 customized.custom_field_values.inject({}) do |h, v|
349 if keyword = get_keyword(v.custom_field.name, :override => true)
349 if keyword = get_keyword(v.custom_field.name, :override => true)
350 h[v.custom_field.id.to_s] = v.custom_field.value_from_keyword(keyword, customized)
350 h[v.custom_field.id.to_s] = v.custom_field.value_from_keyword(keyword, customized)
351 end
351 end
352 h
352 h
353 end
353 end
354 end
354 end
355
355
356 # Returns the text/plain part of the email
356 # Returns the text/plain part of the email
357 # If not found (eg. HTML-only email), returns the body with tags removed
357 # If not found (eg. HTML-only email), returns the body with tags removed
358 def plain_text_body
358 def plain_text_body
359 return @plain_text_body unless @plain_text_body.nil?
359 return @plain_text_body unless @plain_text_body.nil?
360
360
361 part = email.text_part || email.html_part || email
361 part = email.text_part || email.html_part || email
362 @plain_text_body = Redmine::CodesetUtil.to_utf8(part.body.decoded, part.charset)
362 @plain_text_body = Redmine::CodesetUtil.to_utf8(part.body.decoded, part.charset)
363
363
364 # strip html tags and remove doctype directive
364 # strip html tags and remove doctype directive
365 @plain_text_body = strip_tags(@plain_text_body.strip)
365 @plain_text_body = strip_tags(@plain_text_body.strip)
366 @plain_text_body.sub! %r{^<!DOCTYPE .*$}, ''
366 @plain_text_body.sub! %r{^<!DOCTYPE .*$}, ''
367 @plain_text_body
367 @plain_text_body
368 end
368 end
369
369
370 def cleaned_up_text_body
370 def cleaned_up_text_body
371 cleanup_body(plain_text_body)
371 cleanup_body(plain_text_body)
372 end
372 end
373
373
374 def cleaned_up_subject
374 def cleaned_up_subject
375 subject = email.subject.to_s
375 subject = email.subject.to_s
376 unless subject.respond_to?(:encoding)
376 unless subject.respond_to?(:encoding)
377 # try to reencode to utf8 manually with ruby1.8
377 # try to reencode to utf8 manually with ruby1.8
378 begin
378 begin
379 if h = email.header[:subject]
379 if h = email.header[:subject]
380 if m = h.value.match(/^=\?([^\?]+)\?/)
380 # http://tools.ietf.org/html/rfc2047#section-4
381 if m = h.value.match(/=\?([^\?]+)\?[BbQq]\?/)
381 subject = Redmine::CodesetUtil.to_utf8(subject, m[1])
382 subject = Redmine::CodesetUtil.to_utf8(subject, m[1])
382 end
383 end
383 end
384 end
384 rescue
385 rescue
385 # nop
386 # nop
386 end
387 end
387 end
388 end
388 subject.strip[0,255]
389 subject.strip[0,255]
389 end
390 end
390
391
391 def self.full_sanitizer
392 def self.full_sanitizer
392 @full_sanitizer ||= HTML::FullSanitizer.new
393 @full_sanitizer ||= HTML::FullSanitizer.new
393 end
394 end
394
395
395 def self.assign_string_attribute_with_limit(object, attribute, value, limit=nil)
396 def self.assign_string_attribute_with_limit(object, attribute, value, limit=nil)
396 limit ||= object.class.columns_hash[attribute.to_s].limit || 255
397 limit ||= object.class.columns_hash[attribute.to_s].limit || 255
397 value = value.to_s.slice(0, limit)
398 value = value.to_s.slice(0, limit)
398 object.send("#{attribute}=", value)
399 object.send("#{attribute}=", value)
399 end
400 end
400
401
401 # Returns a User from an email address and a full name
402 # Returns a User from an email address and a full name
402 def self.new_user_from_attributes(email_address, fullname=nil)
403 def self.new_user_from_attributes(email_address, fullname=nil)
403 user = User.new
404 user = User.new
404
405
405 # Truncating the email address would result in an invalid format
406 # Truncating the email address would result in an invalid format
406 user.mail = email_address
407 user.mail = email_address
407 assign_string_attribute_with_limit(user, 'login', email_address, User::LOGIN_LENGTH_LIMIT)
408 assign_string_attribute_with_limit(user, 'login', email_address, User::LOGIN_LENGTH_LIMIT)
408
409
409 names = fullname.blank? ? email_address.gsub(/@.*$/, '').split('.') : fullname.split
410 names = fullname.blank? ? email_address.gsub(/@.*$/, '').split('.') : fullname.split
410 assign_string_attribute_with_limit(user, 'firstname', names.shift)
411 assign_string_attribute_with_limit(user, 'firstname', names.shift)
411 assign_string_attribute_with_limit(user, 'lastname', names.join(' '))
412 assign_string_attribute_with_limit(user, 'lastname', names.join(' '))
412 user.lastname = '-' if user.lastname.blank?
413 user.lastname = '-' if user.lastname.blank?
413
414
414 password_length = [Setting.password_min_length.to_i, 10].max
415 password_length = [Setting.password_min_length.to_i, 10].max
415 user.password = Redmine::Utils.random_hex(password_length / 2 + 1)
416 user.password = Redmine::Utils.random_hex(password_length / 2 + 1)
416 user.language = Setting.default_language
417 user.language = Setting.default_language
417
418
418 unless user.valid?
419 unless user.valid?
419 user.login = "user#{Redmine::Utils.random_hex(6)}" unless user.errors[:login].blank?
420 user.login = "user#{Redmine::Utils.random_hex(6)}" unless user.errors[:login].blank?
420 user.firstname = "-" unless user.errors[:firstname].blank?
421 user.firstname = "-" unless user.errors[:firstname].blank?
421 user.lastname = "-" unless user.errors[:lastname].blank?
422 user.lastname = "-" unless user.errors[:lastname].blank?
422 end
423 end
423
424
424 user
425 user
425 end
426 end
426
427
427 # Creates a User for the +email+ sender
428 # Creates a User for the +email+ sender
428 # Returns the user or nil if it could not be created
429 # Returns the user or nil if it could not be created
429 def create_user_from_email
430 def create_user_from_email
430 from = email.header['from'].to_s
431 from = email.header['from'].to_s
431 addr, name = from, nil
432 addr, name = from, nil
432 if m = from.match(/^"?(.+?)"?\s+<(.+@.+)>$/)
433 if m = from.match(/^"?(.+?)"?\s+<(.+@.+)>$/)
433 addr, name = m[2], m[1]
434 addr, name = m[2], m[1]
434 end
435 end
435 if addr.present?
436 if addr.present?
436 user = self.class.new_user_from_attributes(addr, name)
437 user = self.class.new_user_from_attributes(addr, name)
437 if user.save
438 if user.save
438 user
439 user
439 else
440 else
440 logger.error "MailHandler: failed to create User: #{user.errors.full_messages}" if logger
441 logger.error "MailHandler: failed to create User: #{user.errors.full_messages}" if logger
441 nil
442 nil
442 end
443 end
443 else
444 else
444 logger.error "MailHandler: failed to create User: no FROM address found" if logger
445 logger.error "MailHandler: failed to create User: no FROM address found" if logger
445 nil
446 nil
446 end
447 end
447 end
448 end
448
449
449 # Removes the email body of text after the truncation configurations.
450 # Removes the email body of text after the truncation configurations.
450 def cleanup_body(body)
451 def cleanup_body(body)
451 delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
452 delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
452 unless delimiters.empty?
453 unless delimiters.empty?
453 regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
454 regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
454 body = body.gsub(regex, '')
455 body = body.gsub(regex, '')
455 end
456 end
456 body.strip
457 body.strip
457 end
458 end
458
459
459 def find_assignee_from_keyword(keyword, issue)
460 def find_assignee_from_keyword(keyword, issue)
460 keyword = keyword.to_s.downcase
461 keyword = keyword.to_s.downcase
461 assignable = issue.assignable_users
462 assignable = issue.assignable_users
462 assignee = nil
463 assignee = nil
463 assignee ||= assignable.detect {|a|
464 assignee ||= assignable.detect {|a|
464 a.mail.to_s.downcase == keyword ||
465 a.mail.to_s.downcase == keyword ||
465 a.login.to_s.downcase == keyword
466 a.login.to_s.downcase == keyword
466 }
467 }
467 if assignee.nil? && keyword.match(/ /)
468 if assignee.nil? && keyword.match(/ /)
468 firstname, lastname = *(keyword.split) # "First Last Throwaway"
469 firstname, lastname = *(keyword.split) # "First Last Throwaway"
469 assignee ||= assignable.detect {|a|
470 assignee ||= assignable.detect {|a|
470 a.is_a?(User) && a.firstname.to_s.downcase == firstname &&
471 a.is_a?(User) && a.firstname.to_s.downcase == firstname &&
471 a.lastname.to_s.downcase == lastname
472 a.lastname.to_s.downcase == lastname
472 }
473 }
473 end
474 end
474 if assignee.nil?
475 if assignee.nil?
475 assignee ||= assignable.detect {|a| a.name.downcase == keyword}
476 assignee ||= assignable.detect {|a| a.name.downcase == keyword}
476 end
477 end
477 assignee
478 assignee
478 end
479 end
479 end
480 end
@@ -1,698 +1,709
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_add_issue_with_iso_8859_1_subject
376 def test_add_issue_with_iso_8859_1_subject
377 issue = submit_email(
377 issue = submit_email(
378 'subject_as_iso-8859-1.eml',
378 'subject_as_iso-8859-1.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 'Testmail from Webmail: Γ€ ΓΆ ΓΌ...', issue.subject
382 assert_equal 'Testmail from Webmail: Γ€ ΓΆ ΓΌ...', issue.subject
383 end
383 end
384
384
385 def test_add_issue_with_japanese_subject
385 def test_add_issue_with_japanese_subject
386 issue = submit_email(
386 issue = submit_email(
387 'subject_japanese_1.eml',
387 'subject_japanese_1.eml',
388 :issue => {:project => 'ecookbook'}
388 :issue => {:project => 'ecookbook'}
389 )
389 )
390 assert_kind_of Issue, issue
390 assert_kind_of Issue, issue
391 ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88"
391 ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88"
392 ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding)
392 ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding)
393 assert_equal ja, issue.subject
393 assert_equal ja, issue.subject
394 end
394 end
395
395
396 def test_add_issue_with_mixed_japanese_subject
397 issue = submit_email(
398 'subject_japanese_2.eml',
399 :issue => {:project => 'ecookbook'}
400 )
401 assert_kind_of Issue, issue
402 ja = "Re: \xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88"
403 ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding)
404 assert_equal ja, issue.subject
405 end
406
396 def test_should_ignore_emails_from_locked_users
407 def test_should_ignore_emails_from_locked_users
397 User.find(2).lock!
408 User.find(2).lock!
398
409
399 MailHandler.any_instance.expects(:dispatch).never
410 MailHandler.any_instance.expects(:dispatch).never
400 assert_no_difference 'Issue.count' do
411 assert_no_difference 'Issue.count' do
401 assert_equal false, submit_email('ticket_on_given_project.eml')
412 assert_equal false, submit_email('ticket_on_given_project.eml')
402 end
413 end
403 end
414 end
404
415
405 def test_should_ignore_emails_from_emission_address
416 def test_should_ignore_emails_from_emission_address
406 Role.anonymous.add_permission!(:add_issues)
417 Role.anonymous.add_permission!(:add_issues)
407 assert_no_difference 'User.count' do
418 assert_no_difference 'User.count' do
408 assert_equal false,
419 assert_equal false,
409 submit_email(
420 submit_email(
410 'ticket_from_emission_address.eml',
421 'ticket_from_emission_address.eml',
411 :issue => {:project => 'ecookbook'},
422 :issue => {:project => 'ecookbook'},
412 :unknown_user => 'create'
423 :unknown_user => 'create'
413 )
424 )
414 end
425 end
415 end
426 end
416
427
417 def test_should_ignore_auto_replied_emails
428 def test_should_ignore_auto_replied_emails
418 MailHandler.any_instance.expects(:dispatch).never
429 MailHandler.any_instance.expects(:dispatch).never
419 [
430 [
420 "X-Auto-Response-Suppress: OOF",
431 "X-Auto-Response-Suppress: OOF",
421 "Auto-Submitted: auto-replied",
432 "Auto-Submitted: auto-replied",
422 "Auto-Submitted: Auto-Replied",
433 "Auto-Submitted: Auto-Replied",
423 "Auto-Submitted: auto-generated"
434 "Auto-Submitted: auto-generated"
424 ].each do |header|
435 ].each do |header|
425 raw = IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml'))
436 raw = IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml'))
426 raw = header + "\n" + raw
437 raw = header + "\n" + raw
427
438
428 assert_no_difference 'Issue.count' do
439 assert_no_difference 'Issue.count' do
429 assert_equal false, MailHandler.receive(raw), "email with #{header} header was not ignored"
440 assert_equal false, MailHandler.receive(raw), "email with #{header} header was not ignored"
430 end
441 end
431 end
442 end
432 end
443 end
433
444
434 def test_add_issue_should_send_email_notification
445 def test_add_issue_should_send_email_notification
435 Setting.notified_events = ['issue_added']
446 Setting.notified_events = ['issue_added']
436 ActionMailer::Base.deliveries.clear
447 ActionMailer::Base.deliveries.clear
437 # This email contains: 'Project: onlinestore'
448 # This email contains: 'Project: onlinestore'
438 issue = submit_email('ticket_on_given_project.eml')
449 issue = submit_email('ticket_on_given_project.eml')
439 assert issue.is_a?(Issue)
450 assert issue.is_a?(Issue)
440 assert_equal 1, ActionMailer::Base.deliveries.size
451 assert_equal 1, ActionMailer::Base.deliveries.size
441 end
452 end
442
453
443 def test_update_issue
454 def test_update_issue
444 journal = submit_email('ticket_reply.eml')
455 journal = submit_email('ticket_reply.eml')
445 assert journal.is_a?(Journal)
456 assert journal.is_a?(Journal)
446 assert_equal User.find_by_login('jsmith'), journal.user
457 assert_equal User.find_by_login('jsmith'), journal.user
447 assert_equal Issue.find(2), journal.journalized
458 assert_equal Issue.find(2), journal.journalized
448 assert_match /This is reply/, journal.notes
459 assert_match /This is reply/, journal.notes
449 assert_equal false, journal.private_notes
460 assert_equal false, journal.private_notes
450 assert_equal 'Feature request', journal.issue.tracker.name
461 assert_equal 'Feature request', journal.issue.tracker.name
451 end
462 end
452
463
453 def test_update_issue_with_attribute_changes
464 def test_update_issue_with_attribute_changes
454 # This email contains: 'Status: Resolved'
465 # This email contains: 'Status: Resolved'
455 journal = submit_email('ticket_reply_with_status.eml')
466 journal = submit_email('ticket_reply_with_status.eml')
456 assert journal.is_a?(Journal)
467 assert journal.is_a?(Journal)
457 issue = Issue.find(journal.issue.id)
468 issue = Issue.find(journal.issue.id)
458 assert_equal User.find_by_login('jsmith'), journal.user
469 assert_equal User.find_by_login('jsmith'), journal.user
459 assert_equal Issue.find(2), journal.journalized
470 assert_equal Issue.find(2), journal.journalized
460 assert_match /This is reply/, journal.notes
471 assert_match /This is reply/, journal.notes
461 assert_equal 'Feature request', journal.issue.tracker.name
472 assert_equal 'Feature request', journal.issue.tracker.name
462 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
473 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
463 assert_equal '2010-01-01', issue.start_date.to_s
474 assert_equal '2010-01-01', issue.start_date.to_s
464 assert_equal '2010-12-31', issue.due_date.to_s
475 assert_equal '2010-12-31', issue.due_date.to_s
465 assert_equal User.find_by_login('jsmith'), issue.assigned_to
476 assert_equal User.find_by_login('jsmith'), issue.assigned_to
466 assert_equal "52.6", issue.custom_value_for(CustomField.find_by_name('Float field')).value
477 assert_equal "52.6", issue.custom_value_for(CustomField.find_by_name('Float field')).value
467 # keywords should be removed from the email body
478 # keywords should be removed from the email body
468 assert !journal.notes.match(/^Status:/i)
479 assert !journal.notes.match(/^Status:/i)
469 assert !journal.notes.match(/^Start Date:/i)
480 assert !journal.notes.match(/^Start Date:/i)
470 end
481 end
471
482
472 def test_update_issue_with_attachment
483 def test_update_issue_with_attachment
473 assert_difference 'Journal.count' do
484 assert_difference 'Journal.count' do
474 assert_difference 'JournalDetail.count' do
485 assert_difference 'JournalDetail.count' do
475 assert_difference 'Attachment.count' do
486 assert_difference 'Attachment.count' do
476 assert_no_difference 'Issue.count' do
487 assert_no_difference 'Issue.count' do
477 journal = submit_email('ticket_with_attachment.eml') do |raw|
488 journal = submit_email('ticket_with_attachment.eml') do |raw|
478 raw.gsub! /^Subject: .*$/, 'Subject: Re: [Cookbook - Feature #2] (New) Add ingredients categories'
489 raw.gsub! /^Subject: .*$/, 'Subject: Re: [Cookbook - Feature #2] (New) Add ingredients categories'
479 end
490 end
480 end
491 end
481 end
492 end
482 end
493 end
483 end
494 end
484 journal = Journal.first(:order => 'id DESC')
495 journal = Journal.first(:order => 'id DESC')
485 assert_equal Issue.find(2), journal.journalized
496 assert_equal Issue.find(2), journal.journalized
486 assert_equal 1, journal.details.size
497 assert_equal 1, journal.details.size
487
498
488 detail = journal.details.first
499 detail = journal.details.first
489 assert_equal 'attachment', detail.property
500 assert_equal 'attachment', detail.property
490 assert_equal 'Paella.jpg', detail.value
501 assert_equal 'Paella.jpg', detail.value
491 end
502 end
492
503
493 def test_update_issue_should_send_email_notification
504 def test_update_issue_should_send_email_notification
494 ActionMailer::Base.deliveries.clear
505 ActionMailer::Base.deliveries.clear
495 journal = submit_email('ticket_reply.eml')
506 journal = submit_email('ticket_reply.eml')
496 assert journal.is_a?(Journal)
507 assert journal.is_a?(Journal)
497 assert_equal 1, ActionMailer::Base.deliveries.size
508 assert_equal 1, ActionMailer::Base.deliveries.size
498 end
509 end
499
510
500 def test_update_issue_should_not_set_defaults
511 def test_update_issue_should_not_set_defaults
501 journal = submit_email(
512 journal = submit_email(
502 'ticket_reply.eml',
513 'ticket_reply.eml',
503 :issue => {:tracker => 'Support request', :priority => 'High'}
514 :issue => {:tracker => 'Support request', :priority => 'High'}
504 )
515 )
505 assert journal.is_a?(Journal)
516 assert journal.is_a?(Journal)
506 assert_match /This is reply/, journal.notes
517 assert_match /This is reply/, journal.notes
507 assert_equal 'Feature request', journal.issue.tracker.name
518 assert_equal 'Feature request', journal.issue.tracker.name
508 assert_equal 'Normal', journal.issue.priority.name
519 assert_equal 'Normal', journal.issue.priority.name
509 end
520 end
510
521
511 def test_replying_to_a_private_note_should_add_reply_as_private
522 def test_replying_to_a_private_note_should_add_reply_as_private
512 private_journal = Journal.create!(:notes => 'Private notes', :journalized => Issue.find(1), :private_notes => true, :user_id => 2)
523 private_journal = Journal.create!(:notes => 'Private notes', :journalized => Issue.find(1), :private_notes => true, :user_id => 2)
513
524
514 assert_difference 'Journal.count' do
525 assert_difference 'Journal.count' do
515 journal = submit_email('ticket_reply.eml') do |email|
526 journal = submit_email('ticket_reply.eml') do |email|
516 email.sub! %r{^In-Reply-To:.*$}, "In-Reply-To: <redmine.journal-#{private_journal.id}.20060719210421@osiris>"
527 email.sub! %r{^In-Reply-To:.*$}, "In-Reply-To: <redmine.journal-#{private_journal.id}.20060719210421@osiris>"
517 end
528 end
518
529
519 assert_kind_of Journal, journal
530 assert_kind_of Journal, journal
520 assert_match /This is reply/, journal.notes
531 assert_match /This is reply/, journal.notes
521 assert_equal true, journal.private_notes
532 assert_equal true, journal.private_notes
522 end
533 end
523 end
534 end
524
535
525 def test_reply_to_a_message
536 def test_reply_to_a_message
526 m = submit_email('message_reply.eml')
537 m = submit_email('message_reply.eml')
527 assert m.is_a?(Message)
538 assert m.is_a?(Message)
528 assert !m.new_record?
539 assert !m.new_record?
529 m.reload
540 m.reload
530 assert_equal 'Reply via email', m.subject
541 assert_equal 'Reply via email', m.subject
531 # The email replies to message #2 which is part of the thread of message #1
542 # The email replies to message #2 which is part of the thread of message #1
532 assert_equal Message.find(1), m.parent
543 assert_equal Message.find(1), m.parent
533 end
544 end
534
545
535 def test_reply_to_a_message_by_subject
546 def test_reply_to_a_message_by_subject
536 m = submit_email('message_reply_by_subject.eml')
547 m = submit_email('message_reply_by_subject.eml')
537 assert m.is_a?(Message)
548 assert m.is_a?(Message)
538 assert !m.new_record?
549 assert !m.new_record?
539 m.reload
550 m.reload
540 assert_equal 'Reply to the first post', m.subject
551 assert_equal 'Reply to the first post', m.subject
541 assert_equal Message.find(1), m.parent
552 assert_equal Message.find(1), m.parent
542 end
553 end
543
554
544 def test_should_strip_tags_of_html_only_emails
555 def test_should_strip_tags_of_html_only_emails
545 issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'})
556 issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'})
546 assert issue.is_a?(Issue)
557 assert issue.is_a?(Issue)
547 assert !issue.new_record?
558 assert !issue.new_record?
548 issue.reload
559 issue.reload
549 assert_equal 'HTML email', issue.subject
560 assert_equal 'HTML email', issue.subject
550 assert_equal 'This is a html-only email.', issue.description
561 assert_equal 'This is a html-only email.', issue.description
551 end
562 end
552
563
553 context "truncate emails based on the Setting" do
564 context "truncate emails based on the Setting" do
554 context "with no setting" do
565 context "with no setting" do
555 setup do
566 setup do
556 Setting.mail_handler_body_delimiters = ''
567 Setting.mail_handler_body_delimiters = ''
557 end
568 end
558
569
559 should "add the entire email into the issue" do
570 should "add the entire email into the issue" do
560 issue = submit_email('ticket_on_given_project.eml')
571 issue = submit_email('ticket_on_given_project.eml')
561 assert_issue_created(issue)
572 assert_issue_created(issue)
562 assert issue.description.include?('---')
573 assert issue.description.include?('---')
563 assert issue.description.include?('This paragraph is after the delimiter')
574 assert issue.description.include?('This paragraph is after the delimiter')
564 end
575 end
565 end
576 end
566
577
567 context "with a single string" do
578 context "with a single string" do
568 setup do
579 setup do
569 Setting.mail_handler_body_delimiters = '---'
580 Setting.mail_handler_body_delimiters = '---'
570 end
581 end
571 should "truncate the email at the delimiter for the issue" do
582 should "truncate the email at the delimiter for the issue" do
572 issue = submit_email('ticket_on_given_project.eml')
583 issue = submit_email('ticket_on_given_project.eml')
573 assert_issue_created(issue)
584 assert_issue_created(issue)
574 assert issue.description.include?('This paragraph is before delimiters')
585 assert issue.description.include?('This paragraph is before delimiters')
575 assert issue.description.include?('--- This line starts with a delimiter')
586 assert issue.description.include?('--- This line starts with a delimiter')
576 assert !issue.description.match(/^---$/)
587 assert !issue.description.match(/^---$/)
577 assert !issue.description.include?('This paragraph is after the delimiter')
588 assert !issue.description.include?('This paragraph is after the delimiter')
578 end
589 end
579 end
590 end
580
591
581 context "with a single quoted reply (e.g. reply to a Redmine email notification)" do
592 context "with a single quoted reply (e.g. reply to a Redmine email notification)" do
582 setup do
593 setup do
583 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
594 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
584 end
595 end
585 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
596 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
586 journal = submit_email('issue_update_with_quoted_reply_above.eml')
597 journal = submit_email('issue_update_with_quoted_reply_above.eml')
587 assert journal.is_a?(Journal)
598 assert journal.is_a?(Journal)
588 assert journal.notes.include?('An update to the issue by the sender.')
599 assert journal.notes.include?('An update to the issue by the sender.')
589 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
600 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
590 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
601 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
591 end
602 end
592 end
603 end
593
604
594 context "with multiple quoted replies (e.g. reply to a reply of a Redmine email notification)" do
605 context "with multiple quoted replies (e.g. reply to a reply of a Redmine email notification)" do
595 setup do
606 setup do
596 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
607 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
597 end
608 end
598 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
609 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
599 journal = submit_email('issue_update_with_multiple_quoted_reply_above.eml')
610 journal = submit_email('issue_update_with_multiple_quoted_reply_above.eml')
600 assert journal.is_a?(Journal)
611 assert journal.is_a?(Journal)
601 assert journal.notes.include?('An update to the issue by the sender.')
612 assert journal.notes.include?('An update to the issue by the sender.')
602 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
613 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
603 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
614 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
604 end
615 end
605 end
616 end
606
617
607 context "with multiple strings" do
618 context "with multiple strings" do
608 setup do
619 setup do
609 Setting.mail_handler_body_delimiters = "---\nBREAK"
620 Setting.mail_handler_body_delimiters = "---\nBREAK"
610 end
621 end
611 should "truncate the email at the first delimiter found (BREAK)" do
622 should "truncate the email at the first delimiter found (BREAK)" do
612 issue = submit_email('ticket_on_given_project.eml')
623 issue = submit_email('ticket_on_given_project.eml')
613 assert_issue_created(issue)
624 assert_issue_created(issue)
614 assert issue.description.include?('This paragraph is before delimiters')
625 assert issue.description.include?('This paragraph is before delimiters')
615 assert !issue.description.include?('BREAK')
626 assert !issue.description.include?('BREAK')
616 assert !issue.description.include?('This paragraph is between delimiters')
627 assert !issue.description.include?('This paragraph is between delimiters')
617 assert !issue.description.match(/^---$/)
628 assert !issue.description.match(/^---$/)
618 assert !issue.description.include?('This paragraph is after the delimiter')
629 assert !issue.description.include?('This paragraph is after the delimiter')
619 end
630 end
620 end
631 end
621 end
632 end
622
633
623 def test_email_with_long_subject_line
634 def test_email_with_long_subject_line
624 issue = submit_email('ticket_with_long_subject.eml')
635 issue = submit_email('ticket_with_long_subject.eml')
625 assert issue.is_a?(Issue)
636 assert issue.is_a?(Issue)
626 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]
637 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]
627 end
638 end
628
639
629 def test_new_user_from_attributes_should_return_valid_user
640 def test_new_user_from_attributes_should_return_valid_user
630 to_test = {
641 to_test = {
631 # [address, name] => [login, firstname, lastname]
642 # [address, name] => [login, firstname, lastname]
632 ['jsmith@example.net', nil] => ['jsmith@example.net', 'jsmith', '-'],
643 ['jsmith@example.net', nil] => ['jsmith@example.net', 'jsmith', '-'],
633 ['jsmith@example.net', 'John'] => ['jsmith@example.net', 'John', '-'],
644 ['jsmith@example.net', 'John'] => ['jsmith@example.net', 'John', '-'],
634 ['jsmith@example.net', 'John Smith'] => ['jsmith@example.net', 'John', 'Smith'],
645 ['jsmith@example.net', 'John Smith'] => ['jsmith@example.net', 'John', 'Smith'],
635 ['jsmith@example.net', 'John Paul Smith'] => ['jsmith@example.net', 'John', 'Paul Smith'],
646 ['jsmith@example.net', 'John Paul Smith'] => ['jsmith@example.net', 'John', 'Paul Smith'],
636 ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsTheMaximumLength Smith'] => ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsT', 'Smith'],
647 ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsTheMaximumLength Smith'] => ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsT', 'Smith'],
637 ['jsmith@example.net', 'John AVeryLongLastnameThatExceedsTheMaximumLength'] => ['jsmith@example.net', 'John', 'AVeryLongLastnameThatExceedsTh']
648 ['jsmith@example.net', 'John AVeryLongLastnameThatExceedsTheMaximumLength'] => ['jsmith@example.net', 'John', 'AVeryLongLastnameThatExceedsTh']
638 }
649 }
639
650
640 to_test.each do |attrs, expected|
651 to_test.each do |attrs, expected|
641 user = MailHandler.new_user_from_attributes(attrs.first, attrs.last)
652 user = MailHandler.new_user_from_attributes(attrs.first, attrs.last)
642
653
643 assert user.valid?, user.errors.full_messages.to_s
654 assert user.valid?, user.errors.full_messages.to_s
644 assert_equal attrs.first, user.mail
655 assert_equal attrs.first, user.mail
645 assert_equal expected[0], user.login
656 assert_equal expected[0], user.login
646 assert_equal expected[1], user.firstname
657 assert_equal expected[1], user.firstname
647 assert_equal expected[2], user.lastname
658 assert_equal expected[2], user.lastname
648 end
659 end
649 end
660 end
650
661
651 def test_new_user_from_attributes_should_respect_minimum_password_length
662 def test_new_user_from_attributes_should_respect_minimum_password_length
652 with_settings :password_min_length => 15 do
663 with_settings :password_min_length => 15 do
653 user = MailHandler.new_user_from_attributes('jsmith@example.net')
664 user = MailHandler.new_user_from_attributes('jsmith@example.net')
654 assert user.valid?
665 assert user.valid?
655 assert user.password.length >= 15
666 assert user.password.length >= 15
656 end
667 end
657 end
668 end
658
669
659 def test_new_user_from_attributes_should_use_default_login_if_invalid
670 def test_new_user_from_attributes_should_use_default_login_if_invalid
660 user = MailHandler.new_user_from_attributes('foo+bar@example.net')
671 user = MailHandler.new_user_from_attributes('foo+bar@example.net')
661 assert user.valid?
672 assert user.valid?
662 assert user.login =~ /^user[a-f0-9]+$/
673 assert user.login =~ /^user[a-f0-9]+$/
663 assert_equal 'foo+bar@example.net', user.mail
674 assert_equal 'foo+bar@example.net', user.mail
664 end
675 end
665
676
666 def test_new_user_with_utf8_encoded_fullname_should_be_decoded
677 def test_new_user_with_utf8_encoded_fullname_should_be_decoded
667 assert_difference 'User.count' do
678 assert_difference 'User.count' do
668 issue = submit_email(
679 issue = submit_email(
669 'fullname_of_sender_as_utf8_encoded.eml',
680 'fullname_of_sender_as_utf8_encoded.eml',
670 :issue => {:project => 'ecookbook'},
681 :issue => {:project => 'ecookbook'},
671 :unknown_user => 'create'
682 :unknown_user => 'create'
672 )
683 )
673 end
684 end
674
685
675 user = User.first(:order => 'id DESC')
686 user = User.first(:order => 'id DESC')
676 assert_equal "foo@example.org", user.mail
687 assert_equal "foo@example.org", user.mail
677 str1 = "\xc3\x84\xc3\xa4"
688 str1 = "\xc3\x84\xc3\xa4"
678 str2 = "\xc3\x96\xc3\xb6"
689 str2 = "\xc3\x96\xc3\xb6"
679 str1.force_encoding('UTF-8') if str1.respond_to?(:force_encoding)
690 str1.force_encoding('UTF-8') if str1.respond_to?(:force_encoding)
680 str2.force_encoding('UTF-8') if str2.respond_to?(:force_encoding)
691 str2.force_encoding('UTF-8') if str2.respond_to?(:force_encoding)
681 assert_equal str1, user.firstname
692 assert_equal str1, user.firstname
682 assert_equal str2, user.lastname
693 assert_equal str2, user.lastname
683 end
694 end
684
695
685 private
696 private
686
697
687 def submit_email(filename, options={})
698 def submit_email(filename, options={})
688 raw = IO.read(File.join(FIXTURES_PATH, filename))
699 raw = IO.read(File.join(FIXTURES_PATH, filename))
689 yield raw if block_given?
700 yield raw if block_given?
690 MailHandler.receive(raw, options)
701 MailHandler.receive(raw, options)
691 end
702 end
692
703
693 def assert_issue_created(issue)
704 def assert_issue_created(issue)
694 assert issue.is_a?(Issue)
705 assert issue.is_a?(Issue)
695 assert !issue.new_record?
706 assert !issue.new_record?
696 issue.reload
707 issue.reload
697 end
708 end
698 end
709 end
General Comments 0
You need to be logged in to leave comments. Login now