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