##// END OF EJS Templates
Ignore emails with "Auto-Submitted: auto-*" header (#11338)....
Jean-Philippe Lang -
r9741:232edc623727
parent child
Show More
@@ -1,472 +1,475
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-replied'
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 && value.to_s.downcase == ignored_value.downcase
72 if value
73 if logger && logger.info
73 value = value.to_s.downcase
74 logger.info "MailHandler: ignoring email with #{key}:#{value} header"
74 if (ignored_value.is_a?(Regexp) && value.match(ignored_value)) || value == ignored_value
75 if logger && logger.info
76 logger.info "MailHandler: ignoring email with #{key}:#{value} header"
77 end
78 return false
75 end
79 end
76 return false
77 end
80 end
78 end
81 end
79 @user = User.find_by_mail(sender_email) if sender_email.present?
82 @user = User.find_by_mail(sender_email) if sender_email.present?
80 if @user && !@user.active?
83 if @user && !@user.active?
81 if logger && logger.info
84 if logger && logger.info
82 logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]"
85 logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]"
83 end
86 end
84 return false
87 return false
85 end
88 end
86 if @user.nil?
89 if @user.nil?
87 # Email was submitted by an unknown user
90 # Email was submitted by an unknown user
88 case @@handler_options[:unknown_user]
91 case @@handler_options[:unknown_user]
89 when 'accept'
92 when 'accept'
90 @user = User.anonymous
93 @user = User.anonymous
91 when 'create'
94 when 'create'
92 @user = create_user_from_email
95 @user = create_user_from_email
93 if @user
96 if @user
94 if logger && logger.info
97 if logger && logger.info
95 logger.info "MailHandler: [#{@user.login}] account created"
98 logger.info "MailHandler: [#{@user.login}] account created"
96 end
99 end
97 Mailer.account_information(@user, @user.password).deliver
100 Mailer.account_information(@user, @user.password).deliver
98 else
101 else
99 if logger && logger.error
102 if logger && logger.error
100 logger.error "MailHandler: could not create account for [#{sender_email}]"
103 logger.error "MailHandler: could not create account for [#{sender_email}]"
101 end
104 end
102 return false
105 return false
103 end
106 end
104 else
107 else
105 # Default behaviour, emails from unknown users are ignored
108 # Default behaviour, emails from unknown users are ignored
106 if logger && logger.info
109 if logger && logger.info
107 logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]"
110 logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]"
108 end
111 end
109 return false
112 return false
110 end
113 end
111 end
114 end
112 User.current = @user
115 User.current = @user
113 dispatch
116 dispatch
114 end
117 end
115
118
116 private
119 private
117
120
118 MESSAGE_ID_RE = %r{^<?redmine\.([a-z0-9_]+)\-(\d+)\.\d+@}
121 MESSAGE_ID_RE = %r{^<?redmine\.([a-z0-9_]+)\-(\d+)\.\d+@}
119 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]*#(\d+)\]}
122 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]*#(\d+)\]}
120 MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
123 MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
121
124
122 def dispatch
125 def dispatch
123 headers = [email.in_reply_to, email.references].flatten.compact
126 headers = [email.in_reply_to, email.references].flatten.compact
124 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
127 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
125 klass, object_id = $1, $2.to_i
128 klass, object_id = $1, $2.to_i
126 method_name = "receive_#{klass}_reply"
129 method_name = "receive_#{klass}_reply"
127 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)
128 send method_name, object_id
131 send method_name, object_id
129 else
132 else
130 # ignoring it
133 # ignoring it
131 end
134 end
132 elsif m = email.subject.match(ISSUE_REPLY_SUBJECT_RE)
135 elsif m = email.subject.match(ISSUE_REPLY_SUBJECT_RE)
133 receive_issue_reply(m[1].to_i)
136 receive_issue_reply(m[1].to_i)
134 elsif m = email.subject.match(MESSAGE_REPLY_SUBJECT_RE)
137 elsif m = email.subject.match(MESSAGE_REPLY_SUBJECT_RE)
135 receive_message_reply(m[1].to_i)
138 receive_message_reply(m[1].to_i)
136 else
139 else
137 dispatch_to_default
140 dispatch_to_default
138 end
141 end
139 rescue ActiveRecord::RecordInvalid => e
142 rescue ActiveRecord::RecordInvalid => e
140 # TODO: send a email to the user
143 # TODO: send a email to the user
141 logger.error e.message if logger
144 logger.error e.message if logger
142 false
145 false
143 rescue MissingInformation => e
146 rescue MissingInformation => e
144 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
147 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
145 false
148 false
146 rescue UnauthorizedAction => e
149 rescue UnauthorizedAction => e
147 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
150 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
148 false
151 false
149 end
152 end
150
153
151 def dispatch_to_default
154 def dispatch_to_default
152 receive_issue
155 receive_issue
153 end
156 end
154
157
155 # Creates a new issue
158 # Creates a new issue
156 def receive_issue
159 def receive_issue
157 project = target_project
160 project = target_project
158 # check permission
161 # check permission
159 unless @@handler_options[:no_permission_check]
162 unless @@handler_options[:no_permission_check]
160 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
163 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
161 end
164 end
162
165
163 issue = Issue.new(:author => user, :project => project)
166 issue = Issue.new(:author => user, :project => project)
164 issue.safe_attributes = issue_attributes_from_keywords(issue)
167 issue.safe_attributes = issue_attributes_from_keywords(issue)
165 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)}
166 issue.subject = cleaned_up_subject
169 issue.subject = cleaned_up_subject
167 if issue.subject.blank?
170 if issue.subject.blank?
168 issue.subject = '(no subject)'
171 issue.subject = '(no subject)'
169 end
172 end
170 issue.description = cleaned_up_text_body
173 issue.description = cleaned_up_text_body
171
174
172 # 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
173 add_watchers(issue)
176 add_watchers(issue)
174 issue.save!
177 issue.save!
175 add_attachments(issue)
178 add_attachments(issue)
176 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
177 issue
180 issue
178 end
181 end
179
182
180 # Adds a note to an existing issue
183 # Adds a note to an existing issue
181 def receive_issue_reply(issue_id)
184 def receive_issue_reply(issue_id)
182 issue = Issue.find_by_id(issue_id)
185 issue = Issue.find_by_id(issue_id)
183 return unless issue
186 return unless issue
184 # check permission
187 # check permission
185 unless @@handler_options[:no_permission_check]
188 unless @@handler_options[:no_permission_check]
186 unless user.allowed_to?(:add_issue_notes, issue.project) ||
189 unless user.allowed_to?(:add_issue_notes, issue.project) ||
187 user.allowed_to?(:edit_issues, issue.project)
190 user.allowed_to?(:edit_issues, issue.project)
188 raise UnauthorizedAction
191 raise UnauthorizedAction
189 end
192 end
190 end
193 end
191
194
192 # ignore CLI-supplied defaults for new issues
195 # ignore CLI-supplied defaults for new issues
193 @@handler_options[:issue].clear
196 @@handler_options[:issue].clear
194
197
195 journal = issue.init_journal(user)
198 journal = issue.init_journal(user)
196 issue.safe_attributes = issue_attributes_from_keywords(issue)
199 issue.safe_attributes = issue_attributes_from_keywords(issue)
197 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)}
198 journal.notes = cleaned_up_text_body
201 journal.notes = cleaned_up_text_body
199 add_attachments(issue)
202 add_attachments(issue)
200 issue.save!
203 issue.save!
201 if logger && logger.info
204 if logger && logger.info
202 logger.info "MailHandler: issue ##{issue.id} updated by #{user}"
205 logger.info "MailHandler: issue ##{issue.id} updated by #{user}"
203 end
206 end
204 journal
207 journal
205 end
208 end
206
209
207 # Reply will be added to the issue
210 # Reply will be added to the issue
208 def receive_journal_reply(journal_id)
211 def receive_journal_reply(journal_id)
209 journal = Journal.find_by_id(journal_id)
212 journal = Journal.find_by_id(journal_id)
210 if journal && journal.journalized_type == 'Issue'
213 if journal && journal.journalized_type == 'Issue'
211 receive_issue_reply(journal.journalized_id)
214 receive_issue_reply(journal.journalized_id)
212 end
215 end
213 end
216 end
214
217
215 # Receives a reply to a forum message
218 # Receives a reply to a forum message
216 def receive_message_reply(message_id)
219 def receive_message_reply(message_id)
217 message = Message.find_by_id(message_id)
220 message = Message.find_by_id(message_id)
218 if message
221 if message
219 message = message.root
222 message = message.root
220
223
221 unless @@handler_options[:no_permission_check]
224 unless @@handler_options[:no_permission_check]
222 raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
225 raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
223 end
226 end
224
227
225 if !message.locked?
228 if !message.locked?
226 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,
227 :content => cleaned_up_text_body)
230 :content => cleaned_up_text_body)
228 reply.author = user
231 reply.author = user
229 reply.board = message.board
232 reply.board = message.board
230 message.children << reply
233 message.children << reply
231 add_attachments(reply)
234 add_attachments(reply)
232 reply
235 reply
233 else
236 else
234 if logger && logger.info
237 if logger && logger.info
235 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"
236 end
239 end
237 end
240 end
238 end
241 end
239 end
242 end
240
243
241 def add_attachments(obj)
244 def add_attachments(obj)
242 if email.attachments && email.attachments.any?
245 if email.attachments && email.attachments.any?
243 email.attachments.each do |attachment|
246 email.attachments.each do |attachment|
244 obj.attachments << Attachment.create(:container => obj,
247 obj.attachments << Attachment.create(:container => obj,
245 :file => attachment.decoded,
248 :file => attachment.decoded,
246 :filename => attachment.filename,
249 :filename => attachment.filename,
247 :author => user,
250 :author => user,
248 :content_type => attachment.mime_type)
251 :content_type => attachment.mime_type)
249 end
252 end
250 end
253 end
251 end
254 end
252
255
253 # 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
254 # appropriate permission
257 # appropriate permission
255 def add_watchers(obj)
258 def add_watchers(obj)
256 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)
257 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}
258 unless addresses.empty?
261 unless addresses.empty?
259 watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
262 watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
260 watchers.each {|w| obj.add_watcher(w)}
263 watchers.each {|w| obj.add_watcher(w)}
261 end
264 end
262 end
265 end
263 end
266 end
264
267
265 def get_keyword(attr, options={})
268 def get_keyword(attr, options={})
266 @keywords ||= {}
269 @keywords ||= {}
267 if @keywords.has_key?(attr)
270 if @keywords.has_key?(attr)
268 @keywords[attr]
271 @keywords[attr]
269 else
272 else
270 @keywords[attr] = begin
273 @keywords[attr] = begin
271 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) &&
274 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) &&
272 (v = extract_keyword!(plain_text_body, attr, options[:format]))
275 (v = extract_keyword!(plain_text_body, attr, options[:format]))
273 v
276 v
274 elsif !@@handler_options[:issue][attr].blank?
277 elsif !@@handler_options[:issue][attr].blank?
275 @@handler_options[:issue][attr]
278 @@handler_options[:issue][attr]
276 end
279 end
277 end
280 end
278 end
281 end
279 end
282 end
280
283
281 # Destructively extracts the value for +attr+ in +text+
284 # Destructively extracts the value for +attr+ in +text+
282 # Returns nil if no matching keyword found
285 # Returns nil if no matching keyword found
283 def extract_keyword!(text, attr, format=nil)
286 def extract_keyword!(text, attr, format=nil)
284 keys = [attr.to_s.humanize]
287 keys = [attr.to_s.humanize]
285 if attr.is_a?(Symbol)
288 if attr.is_a?(Symbol)
286 if user && user.language.present?
289 if user && user.language.present?
287 keys << l("field_#{attr}", :default => '', :locale => user.language)
290 keys << l("field_#{attr}", :default => '', :locale => user.language)
288 end
291 end
289 if Setting.default_language.present?
292 if Setting.default_language.present?
290 keys << l("field_#{attr}", :default => '', :locale => Setting.default_language)
293 keys << l("field_#{attr}", :default => '', :locale => Setting.default_language)
291 end
294 end
292 end
295 end
293 keys.reject! {|k| k.blank?}
296 keys.reject! {|k| k.blank?}
294 keys.collect! {|k| Regexp.escape(k)}
297 keys.collect! {|k| Regexp.escape(k)}
295 format ||= '.+'
298 format ||= '.+'
296 keyword = nil
299 keyword = nil
297 regexp = /^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i
300 regexp = /^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i
298 if m = text.match(regexp)
301 if m = text.match(regexp)
299 keyword = m[2].strip
302 keyword = m[2].strip
300 text.gsub!(regexp, '')
303 text.gsub!(regexp, '')
301 end
304 end
302 keyword
305 keyword
303 end
306 end
304
307
305 def target_project
308 def target_project
306 # TODO: other ways to specify project:
309 # TODO: other ways to specify project:
307 # * parse the email To field
310 # * parse the email To field
308 # * specific project (eg. Setting.mail_handler_target_project)
311 # * specific project (eg. Setting.mail_handler_target_project)
309 target = Project.find_by_identifier(get_keyword(:project))
312 target = Project.find_by_identifier(get_keyword(:project))
310 raise MissingInformation.new('Unable to determine target project') if target.nil?
313 raise MissingInformation.new('Unable to determine target project') if target.nil?
311 target
314 target
312 end
315 end
313
316
314 # 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
315 def issue_attributes_from_keywords(issue)
318 def issue_attributes_from_keywords(issue)
316 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)
317
320
318 attrs = {
321 attrs = {
319 '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),
320 '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),
321 '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),
322 '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),
323 'assigned_to_id' => assigned_to.try(:id),
326 'assigned_to_id' => assigned_to.try(:id),
324 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) &&
327 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) &&
325 issue.project.shared_versions.named(k).first.try(:id),
328 issue.project.shared_versions.named(k).first.try(:id),
326 '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}'),
327 '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}'),
328 'estimated_hours' => get_keyword(:estimated_hours, :override => true),
331 'estimated_hours' => get_keyword(:estimated_hours, :override => true),
329 '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')
330 }.delete_if {|k, v| v.blank? }
333 }.delete_if {|k, v| v.blank? }
331
334
332 if issue.new_record? && attrs['tracker_id'].nil?
335 if issue.new_record? && attrs['tracker_id'].nil?
333 attrs['tracker_id'] = issue.project.trackers.find(:first).try(:id)
336 attrs['tracker_id'] = issue.project.trackers.find(:first).try(:id)
334 end
337 end
335
338
336 attrs
339 attrs
337 end
340 end
338
341
339 # 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
340 def custom_field_values_from_keywords(customized)
343 def custom_field_values_from_keywords(customized)
341 customized.custom_field_values.inject({}) do |h, v|
344 customized.custom_field_values.inject({}) do |h, v|
342 if value = get_keyword(v.custom_field.name, :override => true)
345 if value = get_keyword(v.custom_field.name, :override => true)
343 h[v.custom_field.id.to_s] = value
346 h[v.custom_field.id.to_s] = value
344 end
347 end
345 h
348 h
346 end
349 end
347 end
350 end
348
351
349 # Returns the text/plain part of the email
352 # Returns the text/plain part of the email
350 # 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
351 def plain_text_body
354 def plain_text_body
352 return @plain_text_body unless @plain_text_body.nil?
355 return @plain_text_body unless @plain_text_body.nil?
353
356
354 part = email.text_part || email.html_part || email
357 part = email.text_part || email.html_part || email
355 @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)
356
359
357 # strip html tags and remove doctype directive
360 # strip html tags and remove doctype directive
358 @plain_text_body = strip_tags(@plain_text_body.strip)
361 @plain_text_body = strip_tags(@plain_text_body.strip)
359 @plain_text_body.sub! %r{^<!DOCTYPE .*$}, ''
362 @plain_text_body.sub! %r{^<!DOCTYPE .*$}, ''
360 @plain_text_body
363 @plain_text_body
361 end
364 end
362
365
363 def cleaned_up_text_body
366 def cleaned_up_text_body
364 cleanup_body(plain_text_body)
367 cleanup_body(plain_text_body)
365 end
368 end
366
369
367 def cleaned_up_subject
370 def cleaned_up_subject
368 subject = email.subject.to_s
371 subject = email.subject.to_s
369 unless subject.respond_to?(:encoding)
372 unless subject.respond_to?(:encoding)
370 # try to reencode to utf8 manually with ruby1.8
373 # try to reencode to utf8 manually with ruby1.8
371 begin
374 begin
372 if h = email.header[:subject]
375 if h = email.header[:subject]
373 if m = h.value.match(/^=\?([^\?]+)\?/)
376 if m = h.value.match(/^=\?([^\?]+)\?/)
374 subject = Redmine::CodesetUtil.to_utf8(subject, m[1])
377 subject = Redmine::CodesetUtil.to_utf8(subject, m[1])
375 end
378 end
376 end
379 end
377 rescue
380 rescue
378 # nop
381 # nop
379 end
382 end
380 end
383 end
381 subject.strip[0,255]
384 subject.strip[0,255]
382 end
385 end
383
386
384 def self.full_sanitizer
387 def self.full_sanitizer
385 @full_sanitizer ||= HTML::FullSanitizer.new
388 @full_sanitizer ||= HTML::FullSanitizer.new
386 end
389 end
387
390
388 def self.assign_string_attribute_with_limit(object, attribute, value, limit=nil)
391 def self.assign_string_attribute_with_limit(object, attribute, value, limit=nil)
389 limit ||= object.class.columns_hash[attribute.to_s].limit || 255
392 limit ||= object.class.columns_hash[attribute.to_s].limit || 255
390 value = value.to_s.slice(0, limit)
393 value = value.to_s.slice(0, limit)
391 object.send("#{attribute}=", value)
394 object.send("#{attribute}=", value)
392 end
395 end
393
396
394 # Returns a User from an email address and a full name
397 # Returns a User from an email address and a full name
395 def self.new_user_from_attributes(email_address, fullname=nil)
398 def self.new_user_from_attributes(email_address, fullname=nil)
396 user = User.new
399 user = User.new
397
400
398 # Truncating the email address would result in an invalid format
401 # Truncating the email address would result in an invalid format
399 user.mail = email_address
402 user.mail = email_address
400 assign_string_attribute_with_limit(user, 'login', email_address, User::LOGIN_LENGTH_LIMIT)
403 assign_string_attribute_with_limit(user, 'login', email_address, User::LOGIN_LENGTH_LIMIT)
401
404
402 names = fullname.blank? ? email_address.gsub(/@.*$/, '').split('.') : fullname.split
405 names = fullname.blank? ? email_address.gsub(/@.*$/, '').split('.') : fullname.split
403 assign_string_attribute_with_limit(user, 'firstname', names.shift)
406 assign_string_attribute_with_limit(user, 'firstname', names.shift)
404 assign_string_attribute_with_limit(user, 'lastname', names.join(' '))
407 assign_string_attribute_with_limit(user, 'lastname', names.join(' '))
405 user.lastname = '-' if user.lastname.blank?
408 user.lastname = '-' if user.lastname.blank?
406
409
407 password_length = [Setting.password_min_length.to_i, 10].max
410 password_length = [Setting.password_min_length.to_i, 10].max
408 user.password = Redmine::Utils.random_hex(password_length / 2 + 1)
411 user.password = Redmine::Utils.random_hex(password_length / 2 + 1)
409 user.language = Setting.default_language
412 user.language = Setting.default_language
410
413
411 unless user.valid?
414 unless user.valid?
412 user.login = "user#{Redmine::Utils.random_hex(6)}" unless user.errors[:login].blank?
415 user.login = "user#{Redmine::Utils.random_hex(6)}" unless user.errors[:login].blank?
413 user.firstname = "-" unless user.errors[:firstname].blank?
416 user.firstname = "-" unless user.errors[:firstname].blank?
414 user.lastname = "-" unless user.errors[:lastname].blank?
417 user.lastname = "-" unless user.errors[:lastname].blank?
415 end
418 end
416
419
417 user
420 user
418 end
421 end
419
422
420 # Creates a User for the +email+ sender
423 # Creates a User for the +email+ sender
421 # Returns the user or nil if it could not be created
424 # Returns the user or nil if it could not be created
422 def create_user_from_email
425 def create_user_from_email
423 from = email.header['from'].to_s
426 from = email.header['from'].to_s
424 addr, name = from, nil
427 addr, name = from, nil
425 if m = from.match(/^"?(.+?)"?\s+<(.+@.+)>$/)
428 if m = from.match(/^"?(.+?)"?\s+<(.+@.+)>$/)
426 addr, name = m[2], m[1]
429 addr, name = m[2], m[1]
427 end
430 end
428 if addr.present?
431 if addr.present?
429 user = self.class.new_user_from_attributes(addr, name)
432 user = self.class.new_user_from_attributes(addr, name)
430 if user.save
433 if user.save
431 user
434 user
432 else
435 else
433 logger.error "MailHandler: failed to create User: #{user.errors.full_messages}" if logger
436 logger.error "MailHandler: failed to create User: #{user.errors.full_messages}" if logger
434 nil
437 nil
435 end
438 end
436 else
439 else
437 logger.error "MailHandler: failed to create User: no FROM address found" if logger
440 logger.error "MailHandler: failed to create User: no FROM address found" if logger
438 nil
441 nil
439 end
442 end
440 end
443 end
441
444
442 # Removes the email body of text after the truncation configurations.
445 # Removes the email body of text after the truncation configurations.
443 def cleanup_body(body)
446 def cleanup_body(body)
444 delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
447 delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
445 unless delimiters.empty?
448 unless delimiters.empty?
446 regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
449 regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
447 body = body.gsub(regex, '')
450 body = body.gsub(regex, '')
448 end
451 end
449 body.strip
452 body.strip
450 end
453 end
451
454
452 def find_assignee_from_keyword(keyword, issue)
455 def find_assignee_from_keyword(keyword, issue)
453 keyword = keyword.to_s.downcase
456 keyword = keyword.to_s.downcase
454 assignable = issue.assignable_users
457 assignable = issue.assignable_users
455 assignee = nil
458 assignee = nil
456 assignee ||= assignable.detect {|a|
459 assignee ||= assignable.detect {|a|
457 a.mail.to_s.downcase == keyword ||
460 a.mail.to_s.downcase == keyword ||
458 a.login.to_s.downcase == keyword
461 a.login.to_s.downcase == keyword
459 }
462 }
460 if assignee.nil? && keyword.match(/ /)
463 if assignee.nil? && keyword.match(/ /)
461 firstname, lastname = *(keyword.split) # "First Last Throwaway"
464 firstname, lastname = *(keyword.split) # "First Last Throwaway"
462 assignee ||= assignable.detect {|a|
465 assignee ||= assignable.detect {|a|
463 a.is_a?(User) && a.firstname.to_s.downcase == firstname &&
466 a.is_a?(User) && a.firstname.to_s.downcase == firstname &&
464 a.lastname.to_s.downcase == lastname
467 a.lastname.to_s.downcase == lastname
465 }
468 }
466 end
469 end
467 if assignee.nil?
470 if assignee.nil?
468 assignee ||= assignable.detect {|a| a.is_a?(Group) && a.name.downcase == keyword}
471 assignee ||= assignable.detect {|a| a.is_a?(Group) && a.name.downcase == keyword}
469 end
472 end
470 assignee
473 assignee
471 end
474 end
472 end
475 end
@@ -1,645 +1,646
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 test_add_issue
38 def test_add_issue
39 ActionMailer::Base.deliveries.clear
39 ActionMailer::Base.deliveries.clear
40 # This email contains: 'Project: onlinestore'
40 # This email contains: 'Project: onlinestore'
41 issue = submit_email('ticket_on_given_project.eml')
41 issue = submit_email('ticket_on_given_project.eml')
42 assert issue.is_a?(Issue)
42 assert issue.is_a?(Issue)
43 assert !issue.new_record?
43 assert !issue.new_record?
44 issue.reload
44 issue.reload
45 assert_equal Project.find(2), issue.project
45 assert_equal Project.find(2), issue.project
46 assert_equal issue.project.trackers.first, issue.tracker
46 assert_equal issue.project.trackers.first, issue.tracker
47 assert_equal 'New ticket on a given project', issue.subject
47 assert_equal 'New ticket on a given project', issue.subject
48 assert_equal User.find_by_login('jsmith'), issue.author
48 assert_equal User.find_by_login('jsmith'), issue.author
49 assert_equal IssueStatus.find_by_name('Resolved'), issue.status
49 assert_equal IssueStatus.find_by_name('Resolved'), issue.status
50 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
50 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
51 assert_equal '2010-01-01', issue.start_date.to_s
51 assert_equal '2010-01-01', issue.start_date.to_s
52 assert_equal '2010-12-31', issue.due_date.to_s
52 assert_equal '2010-12-31', issue.due_date.to_s
53 assert_equal User.find_by_login('jsmith'), issue.assigned_to
53 assert_equal User.find_by_login('jsmith'), issue.assigned_to
54 assert_equal Version.find_by_name('Alpha'), issue.fixed_version
54 assert_equal Version.find_by_name('Alpha'), issue.fixed_version
55 assert_equal 2.5, issue.estimated_hours
55 assert_equal 2.5, issue.estimated_hours
56 assert_equal 30, issue.done_ratio
56 assert_equal 30, issue.done_ratio
57 assert_equal [issue.id, 1, 2], [issue.root_id, issue.lft, issue.rgt]
57 assert_equal [issue.id, 1, 2], [issue.root_id, issue.lft, issue.rgt]
58 # keywords should be removed from the email body
58 # keywords should be removed from the email body
59 assert !issue.description.match(/^Project:/i)
59 assert !issue.description.match(/^Project:/i)
60 assert !issue.description.match(/^Status:/i)
60 assert !issue.description.match(/^Status:/i)
61 assert !issue.description.match(/^Start Date:/i)
61 assert !issue.description.match(/^Start Date:/i)
62 # Email notification should be sent
62 # Email notification should be sent
63 mail = ActionMailer::Base.deliveries.last
63 mail = ActionMailer::Base.deliveries.last
64 assert_not_nil mail
64 assert_not_nil mail
65 assert mail.subject.include?('New ticket on a given project')
65 assert mail.subject.include?('New ticket on a given project')
66 end
66 end
67
67
68 def test_add_issue_with_default_tracker
68 def test_add_issue_with_default_tracker
69 # This email contains: 'Project: onlinestore'
69 # This email contains: 'Project: onlinestore'
70 issue = submit_email(
70 issue = submit_email(
71 'ticket_on_given_project.eml',
71 'ticket_on_given_project.eml',
72 :issue => {:tracker => 'Support request'}
72 :issue => {:tracker => 'Support request'}
73 )
73 )
74 assert issue.is_a?(Issue)
74 assert issue.is_a?(Issue)
75 assert !issue.new_record?
75 assert !issue.new_record?
76 issue.reload
76 issue.reload
77 assert_equal 'Support request', issue.tracker.name
77 assert_equal 'Support request', issue.tracker.name
78 end
78 end
79
79
80 def test_add_issue_with_status
80 def test_add_issue_with_status
81 # This email contains: 'Project: onlinestore' and 'Status: Resolved'
81 # This email contains: 'Project: onlinestore' and 'Status: Resolved'
82 issue = submit_email('ticket_on_given_project.eml')
82 issue = submit_email('ticket_on_given_project.eml')
83 assert issue.is_a?(Issue)
83 assert issue.is_a?(Issue)
84 assert !issue.new_record?
84 assert !issue.new_record?
85 issue.reload
85 issue.reload
86 assert_equal Project.find(2), issue.project
86 assert_equal Project.find(2), issue.project
87 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
87 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
88 end
88 end
89
89
90 def test_add_issue_with_attributes_override
90 def test_add_issue_with_attributes_override
91 issue = submit_email(
91 issue = submit_email(
92 'ticket_with_attributes.eml',
92 'ticket_with_attributes.eml',
93 :allow_override => 'tracker,category,priority'
93 :allow_override => 'tracker,category,priority'
94 )
94 )
95 assert issue.is_a?(Issue)
95 assert issue.is_a?(Issue)
96 assert !issue.new_record?
96 assert !issue.new_record?
97 issue.reload
97 issue.reload
98 assert_equal 'New ticket on a given project', issue.subject
98 assert_equal 'New ticket on a given project', issue.subject
99 assert_equal User.find_by_login('jsmith'), issue.author
99 assert_equal User.find_by_login('jsmith'), issue.author
100 assert_equal Project.find(2), issue.project
100 assert_equal Project.find(2), issue.project
101 assert_equal 'Feature request', issue.tracker.to_s
101 assert_equal 'Feature request', issue.tracker.to_s
102 assert_equal 'Stock management', issue.category.to_s
102 assert_equal 'Stock management', issue.category.to_s
103 assert_equal 'Urgent', issue.priority.to_s
103 assert_equal 'Urgent', issue.priority.to_s
104 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
104 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
105 end
105 end
106
106
107 def test_add_issue_with_group_assignment
107 def test_add_issue_with_group_assignment
108 with_settings :issue_group_assignment => '1' do
108 with_settings :issue_group_assignment => '1' do
109 issue = submit_email('ticket_on_given_project.eml') do |email|
109 issue = submit_email('ticket_on_given_project.eml') do |email|
110 email.gsub!('Assigned to: John Smith', 'Assigned to: B Team')
110 email.gsub!('Assigned to: John Smith', 'Assigned to: B Team')
111 end
111 end
112 assert issue.is_a?(Issue)
112 assert issue.is_a?(Issue)
113 assert !issue.new_record?
113 assert !issue.new_record?
114 issue.reload
114 issue.reload
115 assert_equal Group.find(11), issue.assigned_to
115 assert_equal Group.find(11), issue.assigned_to
116 end
116 end
117 end
117 end
118
118
119 def test_add_issue_with_partial_attributes_override
119 def test_add_issue_with_partial_attributes_override
120 issue = submit_email(
120 issue = submit_email(
121 'ticket_with_attributes.eml',
121 'ticket_with_attributes.eml',
122 :issue => {:priority => 'High'},
122 :issue => {:priority => 'High'},
123 :allow_override => ['tracker']
123 :allow_override => ['tracker']
124 )
124 )
125 assert issue.is_a?(Issue)
125 assert issue.is_a?(Issue)
126 assert !issue.new_record?
126 assert !issue.new_record?
127 issue.reload
127 issue.reload
128 assert_equal 'New ticket on a given project', issue.subject
128 assert_equal 'New ticket on a given project', issue.subject
129 assert_equal User.find_by_login('jsmith'), issue.author
129 assert_equal User.find_by_login('jsmith'), issue.author
130 assert_equal Project.find(2), issue.project
130 assert_equal Project.find(2), issue.project
131 assert_equal 'Feature request', issue.tracker.to_s
131 assert_equal 'Feature request', issue.tracker.to_s
132 assert_nil issue.category
132 assert_nil issue.category
133 assert_equal 'High', issue.priority.to_s
133 assert_equal 'High', issue.priority.to_s
134 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
134 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
135 end
135 end
136
136
137 def test_add_issue_with_spaces_between_attribute_and_separator
137 def test_add_issue_with_spaces_between_attribute_and_separator
138 issue = submit_email(
138 issue = submit_email(
139 'ticket_with_spaces_between_attribute_and_separator.eml',
139 'ticket_with_spaces_between_attribute_and_separator.eml',
140 :allow_override => 'tracker,category,priority'
140 :allow_override => 'tracker,category,priority'
141 )
141 )
142 assert issue.is_a?(Issue)
142 assert issue.is_a?(Issue)
143 assert !issue.new_record?
143 assert !issue.new_record?
144 issue.reload
144 issue.reload
145 assert_equal 'New ticket on a given project', issue.subject
145 assert_equal 'New ticket on a given project', issue.subject
146 assert_equal User.find_by_login('jsmith'), issue.author
146 assert_equal User.find_by_login('jsmith'), issue.author
147 assert_equal Project.find(2), issue.project
147 assert_equal Project.find(2), issue.project
148 assert_equal 'Feature request', issue.tracker.to_s
148 assert_equal 'Feature request', issue.tracker.to_s
149 assert_equal 'Stock management', issue.category.to_s
149 assert_equal 'Stock management', issue.category.to_s
150 assert_equal 'Urgent', issue.priority.to_s
150 assert_equal 'Urgent', issue.priority.to_s
151 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
151 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
152 end
152 end
153
153
154 def test_add_issue_with_attachment_to_specific_project
154 def test_add_issue_with_attachment_to_specific_project
155 issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
155 issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
156 assert issue.is_a?(Issue)
156 assert issue.is_a?(Issue)
157 assert !issue.new_record?
157 assert !issue.new_record?
158 issue.reload
158 issue.reload
159 assert_equal 'Ticket created by email with attachment', issue.subject
159 assert_equal 'Ticket created by email with attachment', issue.subject
160 assert_equal User.find_by_login('jsmith'), issue.author
160 assert_equal User.find_by_login('jsmith'), issue.author
161 assert_equal Project.find(2), issue.project
161 assert_equal Project.find(2), issue.project
162 assert_equal 'This is a new ticket with attachments', issue.description
162 assert_equal 'This is a new ticket with attachments', issue.description
163 # Attachment properties
163 # Attachment properties
164 assert_equal 1, issue.attachments.size
164 assert_equal 1, issue.attachments.size
165 assert_equal 'Paella.jpg', issue.attachments.first.filename
165 assert_equal 'Paella.jpg', issue.attachments.first.filename
166 assert_equal 'image/jpeg', issue.attachments.first.content_type
166 assert_equal 'image/jpeg', issue.attachments.first.content_type
167 assert_equal 10790, issue.attachments.first.filesize
167 assert_equal 10790, issue.attachments.first.filesize
168 end
168 end
169
169
170 def test_add_issue_with_custom_fields
170 def test_add_issue_with_custom_fields
171 issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'onlinestore'})
171 issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'onlinestore'})
172 assert issue.is_a?(Issue)
172 assert issue.is_a?(Issue)
173 assert !issue.new_record?
173 assert !issue.new_record?
174 issue.reload
174 issue.reload
175 assert_equal 'New ticket with custom field values', issue.subject
175 assert_equal 'New ticket with custom field values', issue.subject
176 assert_equal 'Value for a custom field',
176 assert_equal 'Value for a custom field',
177 issue.custom_value_for(CustomField.find_by_name('Searchable field')).value
177 issue.custom_value_for(CustomField.find_by_name('Searchable field')).value
178 assert !issue.description.match(/^searchable field:/i)
178 assert !issue.description.match(/^searchable field:/i)
179 end
179 end
180
180
181 def test_add_issue_with_cc
181 def test_add_issue_with_cc
182 issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'})
182 issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'})
183 assert issue.is_a?(Issue)
183 assert issue.is_a?(Issue)
184 assert !issue.new_record?
184 assert !issue.new_record?
185 issue.reload
185 issue.reload
186 assert issue.watched_by?(User.find_by_mail('dlopper@somenet.foo'))
186 assert issue.watched_by?(User.find_by_mail('dlopper@somenet.foo'))
187 assert_equal 1, issue.watcher_user_ids.size
187 assert_equal 1, issue.watcher_user_ids.size
188 end
188 end
189
189
190 def test_add_issue_by_unknown_user
190 def test_add_issue_by_unknown_user
191 assert_no_difference 'User.count' do
191 assert_no_difference 'User.count' do
192 assert_equal false,
192 assert_equal false,
193 submit_email(
193 submit_email(
194 'ticket_by_unknown_user.eml',
194 'ticket_by_unknown_user.eml',
195 :issue => {:project => 'ecookbook'}
195 :issue => {:project => 'ecookbook'}
196 )
196 )
197 end
197 end
198 end
198 end
199
199
200 def test_add_issue_by_anonymous_user
200 def test_add_issue_by_anonymous_user
201 Role.anonymous.add_permission!(:add_issues)
201 Role.anonymous.add_permission!(:add_issues)
202 assert_no_difference 'User.count' do
202 assert_no_difference 'User.count' do
203 issue = submit_email(
203 issue = submit_email(
204 'ticket_by_unknown_user.eml',
204 'ticket_by_unknown_user.eml',
205 :issue => {:project => 'ecookbook'},
205 :issue => {:project => 'ecookbook'},
206 :unknown_user => 'accept'
206 :unknown_user => 'accept'
207 )
207 )
208 assert issue.is_a?(Issue)
208 assert issue.is_a?(Issue)
209 assert issue.author.anonymous?
209 assert issue.author.anonymous?
210 end
210 end
211 end
211 end
212
212
213 def test_add_issue_by_anonymous_user_with_no_from_address
213 def test_add_issue_by_anonymous_user_with_no_from_address
214 Role.anonymous.add_permission!(:add_issues)
214 Role.anonymous.add_permission!(:add_issues)
215 assert_no_difference 'User.count' do
215 assert_no_difference 'User.count' do
216 issue = submit_email(
216 issue = submit_email(
217 'ticket_by_empty_user.eml',
217 'ticket_by_empty_user.eml',
218 :issue => {:project => 'ecookbook'},
218 :issue => {:project => 'ecookbook'},
219 :unknown_user => 'accept'
219 :unknown_user => 'accept'
220 )
220 )
221 assert issue.is_a?(Issue)
221 assert issue.is_a?(Issue)
222 assert issue.author.anonymous?
222 assert issue.author.anonymous?
223 end
223 end
224 end
224 end
225
225
226 def test_add_issue_by_anonymous_user_on_private_project
226 def test_add_issue_by_anonymous_user_on_private_project
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 assert_no_difference 'Issue.count' do
229 assert_no_difference 'Issue.count' do
230 assert_equal false,
230 assert_equal false,
231 submit_email(
231 submit_email(
232 'ticket_by_unknown_user.eml',
232 'ticket_by_unknown_user.eml',
233 :issue => {:project => 'onlinestore'},
233 :issue => {:project => 'onlinestore'},
234 :unknown_user => 'accept'
234 :unknown_user => 'accept'
235 )
235 )
236 end
236 end
237 end
237 end
238 end
238 end
239
239
240 def test_add_issue_by_anonymous_user_on_private_project_without_permission_check
240 def test_add_issue_by_anonymous_user_on_private_project_without_permission_check
241 assert_no_difference 'User.count' do
241 assert_no_difference 'User.count' do
242 assert_difference 'Issue.count' do
242 assert_difference 'Issue.count' do
243 issue = submit_email(
243 issue = submit_email(
244 'ticket_by_unknown_user.eml',
244 'ticket_by_unknown_user.eml',
245 :issue => {:project => 'onlinestore'},
245 :issue => {:project => 'onlinestore'},
246 :no_permission_check => '1',
246 :no_permission_check => '1',
247 :unknown_user => 'accept'
247 :unknown_user => 'accept'
248 )
248 )
249 assert issue.is_a?(Issue)
249 assert issue.is_a?(Issue)
250 assert issue.author.anonymous?
250 assert issue.author.anonymous?
251 assert !issue.project.is_public?
251 assert !issue.project.is_public?
252 assert_equal [issue.id, 1, 2], [issue.root_id, issue.lft, issue.rgt]
252 assert_equal [issue.id, 1, 2], [issue.root_id, issue.lft, issue.rgt]
253 end
253 end
254 end
254 end
255 end
255 end
256
256
257 def test_add_issue_by_created_user
257 def test_add_issue_by_created_user
258 Setting.default_language = 'en'
258 Setting.default_language = 'en'
259 assert_difference 'User.count' do
259 assert_difference 'User.count' do
260 issue = submit_email(
260 issue = submit_email(
261 'ticket_by_unknown_user.eml',
261 'ticket_by_unknown_user.eml',
262 :issue => {:project => 'ecookbook'},
262 :issue => {:project => 'ecookbook'},
263 :unknown_user => 'create'
263 :unknown_user => 'create'
264 )
264 )
265 assert issue.is_a?(Issue)
265 assert issue.is_a?(Issue)
266 assert issue.author.active?
266 assert issue.author.active?
267 assert_equal 'john.doe@somenet.foo', issue.author.mail
267 assert_equal 'john.doe@somenet.foo', issue.author.mail
268 assert_equal 'John', issue.author.firstname
268 assert_equal 'John', issue.author.firstname
269 assert_equal 'Doe', issue.author.lastname
269 assert_equal 'Doe', issue.author.lastname
270
270
271 # account information
271 # account information
272 email = ActionMailer::Base.deliveries.first
272 email = ActionMailer::Base.deliveries.first
273 assert_not_nil email
273 assert_not_nil email
274 assert email.subject.include?('account activation')
274 assert email.subject.include?('account activation')
275 login = mail_body(email).match(/\* Login: (.*)$/)[1].strip
275 login = mail_body(email).match(/\* Login: (.*)$/)[1].strip
276 password = mail_body(email).match(/\* Password: (.*)$/)[1].strip
276 password = mail_body(email).match(/\* Password: (.*)$/)[1].strip
277 assert_equal issue.author, User.try_to_login(login, password)
277 assert_equal issue.author, User.try_to_login(login, password)
278 end
278 end
279 end
279 end
280
280
281 def test_add_issue_without_from_header
281 def test_add_issue_without_from_header
282 Role.anonymous.add_permission!(:add_issues)
282 Role.anonymous.add_permission!(:add_issues)
283 assert_equal false, submit_email('ticket_without_from_header.eml')
283 assert_equal false, submit_email('ticket_without_from_header.eml')
284 end
284 end
285
285
286 def test_add_issue_with_invalid_attributes
286 def test_add_issue_with_invalid_attributes
287 issue = submit_email(
287 issue = submit_email(
288 'ticket_with_invalid_attributes.eml',
288 'ticket_with_invalid_attributes.eml',
289 :allow_override => 'tracker,category,priority'
289 :allow_override => 'tracker,category,priority'
290 )
290 )
291 assert issue.is_a?(Issue)
291 assert issue.is_a?(Issue)
292 assert !issue.new_record?
292 assert !issue.new_record?
293 issue.reload
293 issue.reload
294 assert_nil issue.assigned_to
294 assert_nil issue.assigned_to
295 assert_nil issue.start_date
295 assert_nil issue.start_date
296 assert_nil issue.due_date
296 assert_nil issue.due_date
297 assert_equal 0, issue.done_ratio
297 assert_equal 0, issue.done_ratio
298 assert_equal 'Normal', issue.priority.to_s
298 assert_equal 'Normal', issue.priority.to_s
299 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
299 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
300 end
300 end
301
301
302 def test_add_issue_with_localized_attributes
302 def test_add_issue_with_localized_attributes
303 User.find_by_mail('jsmith@somenet.foo').update_attribute 'language', 'fr'
303 User.find_by_mail('jsmith@somenet.foo').update_attribute 'language', 'fr'
304 issue = submit_email(
304 issue = submit_email(
305 'ticket_with_localized_attributes.eml',
305 'ticket_with_localized_attributes.eml',
306 :allow_override => 'tracker,category,priority'
306 :allow_override => 'tracker,category,priority'
307 )
307 )
308 assert issue.is_a?(Issue)
308 assert issue.is_a?(Issue)
309 assert !issue.new_record?
309 assert !issue.new_record?
310 issue.reload
310 issue.reload
311 assert_equal 'New ticket on a given project', issue.subject
311 assert_equal 'New ticket on a given project', issue.subject
312 assert_equal User.find_by_login('jsmith'), issue.author
312 assert_equal User.find_by_login('jsmith'), issue.author
313 assert_equal Project.find(2), issue.project
313 assert_equal Project.find(2), issue.project
314 assert_equal 'Feature request', issue.tracker.to_s
314 assert_equal 'Feature request', issue.tracker.to_s
315 assert_equal 'Stock management', issue.category.to_s
315 assert_equal 'Stock management', issue.category.to_s
316 assert_equal 'Urgent', issue.priority.to_s
316 assert_equal 'Urgent', issue.priority.to_s
317 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
317 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
318 end
318 end
319
319
320 def test_add_issue_with_japanese_keywords
320 def test_add_issue_with_japanese_keywords
321 ja_dev = "\xe9\x96\x8b\xe7\x99\xba"
321 ja_dev = "\xe9\x96\x8b\xe7\x99\xba"
322 ja_dev.force_encoding('UTF-8') if ja_dev.respond_to?(:force_encoding)
322 ja_dev.force_encoding('UTF-8') if ja_dev.respond_to?(:force_encoding)
323 tracker = Tracker.create!(:name => ja_dev)
323 tracker = Tracker.create!(:name => ja_dev)
324 Project.find(1).trackers << tracker
324 Project.find(1).trackers << tracker
325 issue = submit_email(
325 issue = submit_email(
326 'japanese_keywords_iso_2022_jp.eml',
326 'japanese_keywords_iso_2022_jp.eml',
327 :issue => {:project => 'ecookbook'},
327 :issue => {:project => 'ecookbook'},
328 :allow_override => 'tracker'
328 :allow_override => 'tracker'
329 )
329 )
330 assert_kind_of Issue, issue
330 assert_kind_of Issue, issue
331 assert_equal tracker, issue.tracker
331 assert_equal tracker, issue.tracker
332 end
332 end
333
333
334 def test_add_issue_from_apple_mail
334 def test_add_issue_from_apple_mail
335 issue = submit_email(
335 issue = submit_email(
336 'apple_mail_with_attachment.eml',
336 'apple_mail_with_attachment.eml',
337 :issue => {:project => 'ecookbook'}
337 :issue => {:project => 'ecookbook'}
338 )
338 )
339 assert_kind_of Issue, issue
339 assert_kind_of Issue, issue
340 assert_equal 1, issue.attachments.size
340 assert_equal 1, issue.attachments.size
341
341
342 attachment = issue.attachments.first
342 attachment = issue.attachments.first
343 assert_equal 'paella.jpg', attachment.filename
343 assert_equal 'paella.jpg', attachment.filename
344 assert_equal 10790, attachment.filesize
344 assert_equal 10790, attachment.filesize
345 assert File.exist?(attachment.diskfile)
345 assert File.exist?(attachment.diskfile)
346 assert_equal 10790, File.size(attachment.diskfile)
346 assert_equal 10790, File.size(attachment.diskfile)
347 assert_equal 'caaf384198bcbc9563ab5c058acd73cd', attachment.digest
347 assert_equal 'caaf384198bcbc9563ab5c058acd73cd', attachment.digest
348 end
348 end
349
349
350 def test_add_issue_with_iso_8859_1_subject
350 def test_add_issue_with_iso_8859_1_subject
351 issue = submit_email(
351 issue = submit_email(
352 'subject_as_iso-8859-1.eml',
352 'subject_as_iso-8859-1.eml',
353 :issue => {:project => 'ecookbook'}
353 :issue => {:project => 'ecookbook'}
354 )
354 )
355 assert_kind_of Issue, issue
355 assert_kind_of Issue, issue
356 assert_equal 'Testmail from Webmail: Γ€ ΓΆ ΓΌ...', issue.subject
356 assert_equal 'Testmail from Webmail: Γ€ ΓΆ ΓΌ...', issue.subject
357 end
357 end
358
358
359 def test_should_ignore_emails_from_locked_users
359 def test_should_ignore_emails_from_locked_users
360 User.find(2).lock!
360 User.find(2).lock!
361
361
362 MailHandler.any_instance.expects(:dispatch).never
362 MailHandler.any_instance.expects(:dispatch).never
363 assert_no_difference 'Issue.count' do
363 assert_no_difference 'Issue.count' do
364 assert_equal false, submit_email('ticket_on_given_project.eml')
364 assert_equal false, submit_email('ticket_on_given_project.eml')
365 end
365 end
366 end
366 end
367
367
368 def test_should_ignore_emails_from_emission_address
368 def test_should_ignore_emails_from_emission_address
369 Role.anonymous.add_permission!(:add_issues)
369 Role.anonymous.add_permission!(:add_issues)
370 assert_no_difference 'User.count' do
370 assert_no_difference 'User.count' do
371 assert_equal false,
371 assert_equal false,
372 submit_email(
372 submit_email(
373 'ticket_from_emission_address.eml',
373 'ticket_from_emission_address.eml',
374 :issue => {:project => 'ecookbook'},
374 :issue => {:project => 'ecookbook'},
375 :unknown_user => 'create'
375 :unknown_user => 'create'
376 )
376 )
377 end
377 end
378 end
378 end
379
379
380 def test_should_ignore_auto_replied_emails
380 def test_should_ignore_auto_replied_emails
381 MailHandler.any_instance.expects(:dispatch).never
381 MailHandler.any_instance.expects(:dispatch).never
382 [
382 [
383 "X-Auto-Response-Suppress: OOF",
383 "X-Auto-Response-Suppress: OOF",
384 "Auto-Submitted: auto-replied",
384 "Auto-Submitted: auto-replied",
385 "Auto-Submitted: Auto-Replied"
385 "Auto-Submitted: Auto-Replied",
386 "Auto-Submitted: auto-generated"
386 ].each do |header|
387 ].each do |header|
387 raw = IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml'))
388 raw = IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml'))
388 raw = header + "\n" + raw
389 raw = header + "\n" + raw
389
390
390 assert_no_difference 'Issue.count' do
391 assert_no_difference 'Issue.count' do
391 assert_equal false, MailHandler.receive(raw), "email with #{header} header was not ignored"
392 assert_equal false, MailHandler.receive(raw), "email with #{header} header was not ignored"
392 end
393 end
393 end
394 end
394 end
395 end
395
396
396 def test_add_issue_should_send_email_notification
397 def test_add_issue_should_send_email_notification
397 Setting.notified_events = ['issue_added']
398 Setting.notified_events = ['issue_added']
398 ActionMailer::Base.deliveries.clear
399 ActionMailer::Base.deliveries.clear
399 # This email contains: 'Project: onlinestore'
400 # This email contains: 'Project: onlinestore'
400 issue = submit_email('ticket_on_given_project.eml')
401 issue = submit_email('ticket_on_given_project.eml')
401 assert issue.is_a?(Issue)
402 assert issue.is_a?(Issue)
402 assert_equal 1, ActionMailer::Base.deliveries.size
403 assert_equal 1, ActionMailer::Base.deliveries.size
403 end
404 end
404
405
405 def test_update_issue
406 def test_update_issue
406 journal = submit_email('ticket_reply.eml')
407 journal = submit_email('ticket_reply.eml')
407 assert journal.is_a?(Journal)
408 assert journal.is_a?(Journal)
408 assert_equal User.find_by_login('jsmith'), journal.user
409 assert_equal User.find_by_login('jsmith'), journal.user
409 assert_equal Issue.find(2), journal.journalized
410 assert_equal Issue.find(2), journal.journalized
410 assert_match /This is reply/, journal.notes
411 assert_match /This is reply/, journal.notes
411 assert_equal 'Feature request', journal.issue.tracker.name
412 assert_equal 'Feature request', journal.issue.tracker.name
412 end
413 end
413
414
414 def test_update_issue_with_attribute_changes
415 def test_update_issue_with_attribute_changes
415 # This email contains: 'Status: Resolved'
416 # This email contains: 'Status: Resolved'
416 journal = submit_email('ticket_reply_with_status.eml')
417 journal = submit_email('ticket_reply_with_status.eml')
417 assert journal.is_a?(Journal)
418 assert journal.is_a?(Journal)
418 issue = Issue.find(journal.issue.id)
419 issue = Issue.find(journal.issue.id)
419 assert_equal User.find_by_login('jsmith'), journal.user
420 assert_equal User.find_by_login('jsmith'), journal.user
420 assert_equal Issue.find(2), journal.journalized
421 assert_equal Issue.find(2), journal.journalized
421 assert_match /This is reply/, journal.notes
422 assert_match /This is reply/, journal.notes
422 assert_equal 'Feature request', journal.issue.tracker.name
423 assert_equal 'Feature request', journal.issue.tracker.name
423 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
424 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
424 assert_equal '2010-01-01', issue.start_date.to_s
425 assert_equal '2010-01-01', issue.start_date.to_s
425 assert_equal '2010-12-31', issue.due_date.to_s
426 assert_equal '2010-12-31', issue.due_date.to_s
426 assert_equal User.find_by_login('jsmith'), issue.assigned_to
427 assert_equal User.find_by_login('jsmith'), issue.assigned_to
427 assert_equal "52.6", issue.custom_value_for(CustomField.find_by_name('Float field')).value
428 assert_equal "52.6", issue.custom_value_for(CustomField.find_by_name('Float field')).value
428 # keywords should be removed from the email body
429 # keywords should be removed from the email body
429 assert !journal.notes.match(/^Status:/i)
430 assert !journal.notes.match(/^Status:/i)
430 assert !journal.notes.match(/^Start Date:/i)
431 assert !journal.notes.match(/^Start Date:/i)
431 end
432 end
432
433
433 def test_update_issue_with_attachment
434 def test_update_issue_with_attachment
434 assert_difference 'Journal.count' do
435 assert_difference 'Journal.count' do
435 assert_difference 'JournalDetail.count' do
436 assert_difference 'JournalDetail.count' do
436 assert_difference 'Attachment.count' do
437 assert_difference 'Attachment.count' do
437 assert_no_difference 'Issue.count' do
438 assert_no_difference 'Issue.count' do
438 journal = submit_email('ticket_with_attachment.eml') do |raw|
439 journal = submit_email('ticket_with_attachment.eml') do |raw|
439 raw.gsub! /^Subject: .*$/, 'Subject: Re: [Cookbook - Feature #2] (New) Add ingredients categories'
440 raw.gsub! /^Subject: .*$/, 'Subject: Re: [Cookbook - Feature #2] (New) Add ingredients categories'
440 end
441 end
441 end
442 end
442 end
443 end
443 end
444 end
444 end
445 end
445 journal = Journal.first(:order => 'id DESC')
446 journal = Journal.first(:order => 'id DESC')
446 assert_equal Issue.find(2), journal.journalized
447 assert_equal Issue.find(2), journal.journalized
447 assert_equal 1, journal.details.size
448 assert_equal 1, journal.details.size
448
449
449 detail = journal.details.first
450 detail = journal.details.first
450 assert_equal 'attachment', detail.property
451 assert_equal 'attachment', detail.property
451 assert_equal 'Paella.jpg', detail.value
452 assert_equal 'Paella.jpg', detail.value
452 end
453 end
453
454
454 def test_update_issue_should_send_email_notification
455 def test_update_issue_should_send_email_notification
455 ActionMailer::Base.deliveries.clear
456 ActionMailer::Base.deliveries.clear
456 journal = submit_email('ticket_reply.eml')
457 journal = submit_email('ticket_reply.eml')
457 assert journal.is_a?(Journal)
458 assert journal.is_a?(Journal)
458 assert_equal 1, ActionMailer::Base.deliveries.size
459 assert_equal 1, ActionMailer::Base.deliveries.size
459 end
460 end
460
461
461 def test_update_issue_should_not_set_defaults
462 def test_update_issue_should_not_set_defaults
462 journal = submit_email(
463 journal = submit_email(
463 'ticket_reply.eml',
464 'ticket_reply.eml',
464 :issue => {:tracker => 'Support request', :priority => 'High'}
465 :issue => {:tracker => 'Support request', :priority => 'High'}
465 )
466 )
466 assert journal.is_a?(Journal)
467 assert journal.is_a?(Journal)
467 assert_match /This is reply/, journal.notes
468 assert_match /This is reply/, journal.notes
468 assert_equal 'Feature request', journal.issue.tracker.name
469 assert_equal 'Feature request', journal.issue.tracker.name
469 assert_equal 'Normal', journal.issue.priority.name
470 assert_equal 'Normal', journal.issue.priority.name
470 end
471 end
471
472
472 def test_reply_to_a_message
473 def test_reply_to_a_message
473 m = submit_email('message_reply.eml')
474 m = submit_email('message_reply.eml')
474 assert m.is_a?(Message)
475 assert m.is_a?(Message)
475 assert !m.new_record?
476 assert !m.new_record?
476 m.reload
477 m.reload
477 assert_equal 'Reply via email', m.subject
478 assert_equal 'Reply via email', m.subject
478 # The email replies to message #2 which is part of the thread of message #1
479 # The email replies to message #2 which is part of the thread of message #1
479 assert_equal Message.find(1), m.parent
480 assert_equal Message.find(1), m.parent
480 end
481 end
481
482
482 def test_reply_to_a_message_by_subject
483 def test_reply_to_a_message_by_subject
483 m = submit_email('message_reply_by_subject.eml')
484 m = submit_email('message_reply_by_subject.eml')
484 assert m.is_a?(Message)
485 assert m.is_a?(Message)
485 assert !m.new_record?
486 assert !m.new_record?
486 m.reload
487 m.reload
487 assert_equal 'Reply to the first post', m.subject
488 assert_equal 'Reply to the first post', m.subject
488 assert_equal Message.find(1), m.parent
489 assert_equal Message.find(1), m.parent
489 end
490 end
490
491
491 def test_should_strip_tags_of_html_only_emails
492 def test_should_strip_tags_of_html_only_emails
492 issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'})
493 issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'})
493 assert issue.is_a?(Issue)
494 assert issue.is_a?(Issue)
494 assert !issue.new_record?
495 assert !issue.new_record?
495 issue.reload
496 issue.reload
496 assert_equal 'HTML email', issue.subject
497 assert_equal 'HTML email', issue.subject
497 assert_equal 'This is a html-only email.', issue.description
498 assert_equal 'This is a html-only email.', issue.description
498 end
499 end
499
500
500 context "truncate emails based on the Setting" do
501 context "truncate emails based on the Setting" do
501 context "with no setting" do
502 context "with no setting" do
502 setup do
503 setup do
503 Setting.mail_handler_body_delimiters = ''
504 Setting.mail_handler_body_delimiters = ''
504 end
505 end
505
506
506 should "add the entire email into the issue" do
507 should "add the entire email into the issue" do
507 issue = submit_email('ticket_on_given_project.eml')
508 issue = submit_email('ticket_on_given_project.eml')
508 assert_issue_created(issue)
509 assert_issue_created(issue)
509 assert issue.description.include?('---')
510 assert issue.description.include?('---')
510 assert issue.description.include?('This paragraph is after the delimiter')
511 assert issue.description.include?('This paragraph is after the delimiter')
511 end
512 end
512 end
513 end
513
514
514 context "with a single string" do
515 context "with a single string" do
515 setup do
516 setup do
516 Setting.mail_handler_body_delimiters = '---'
517 Setting.mail_handler_body_delimiters = '---'
517 end
518 end
518 should "truncate the email at the delimiter for the issue" do
519 should "truncate the email at the delimiter for the issue" do
519 issue = submit_email('ticket_on_given_project.eml')
520 issue = submit_email('ticket_on_given_project.eml')
520 assert_issue_created(issue)
521 assert_issue_created(issue)
521 assert issue.description.include?('This paragraph is before delimiters')
522 assert issue.description.include?('This paragraph is before delimiters')
522 assert issue.description.include?('--- This line starts with a delimiter')
523 assert issue.description.include?('--- This line starts with a delimiter')
523 assert !issue.description.match(/^---$/)
524 assert !issue.description.match(/^---$/)
524 assert !issue.description.include?('This paragraph is after the delimiter')
525 assert !issue.description.include?('This paragraph is after the delimiter')
525 end
526 end
526 end
527 end
527
528
528 context "with a single quoted reply (e.g. reply to a Redmine email notification)" do
529 context "with a single quoted reply (e.g. reply to a Redmine email notification)" do
529 setup do
530 setup do
530 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
531 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
531 end
532 end
532 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
533 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
533 journal = submit_email('issue_update_with_quoted_reply_above.eml')
534 journal = submit_email('issue_update_with_quoted_reply_above.eml')
534 assert journal.is_a?(Journal)
535 assert journal.is_a?(Journal)
535 assert journal.notes.include?('An update to the issue by the sender.')
536 assert journal.notes.include?('An update to the issue by the sender.')
536 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
537 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
537 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
538 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
538 end
539 end
539 end
540 end
540
541
541 context "with multiple quoted replies (e.g. reply to a reply of a Redmine email notification)" do
542 context "with multiple quoted replies (e.g. reply to a reply of a Redmine email notification)" do
542 setup do
543 setup do
543 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
544 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
544 end
545 end
545 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
546 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
546 journal = submit_email('issue_update_with_multiple_quoted_reply_above.eml')
547 journal = submit_email('issue_update_with_multiple_quoted_reply_above.eml')
547 assert journal.is_a?(Journal)
548 assert journal.is_a?(Journal)
548 assert journal.notes.include?('An update to the issue by the sender.')
549 assert journal.notes.include?('An update to the issue by the sender.')
549 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
550 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
550 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
551 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
551 end
552 end
552 end
553 end
553
554
554 context "with multiple strings" do
555 context "with multiple strings" do
555 setup do
556 setup do
556 Setting.mail_handler_body_delimiters = "---\nBREAK"
557 Setting.mail_handler_body_delimiters = "---\nBREAK"
557 end
558 end
558 should "truncate the email at the first delimiter found (BREAK)" do
559 should "truncate the email at the first delimiter found (BREAK)" do
559 issue = submit_email('ticket_on_given_project.eml')
560 issue = submit_email('ticket_on_given_project.eml')
560 assert_issue_created(issue)
561 assert_issue_created(issue)
561 assert issue.description.include?('This paragraph is before delimiters')
562 assert issue.description.include?('This paragraph is before delimiters')
562 assert !issue.description.include?('BREAK')
563 assert !issue.description.include?('BREAK')
563 assert !issue.description.include?('This paragraph is between delimiters')
564 assert !issue.description.include?('This paragraph is between delimiters')
564 assert !issue.description.match(/^---$/)
565 assert !issue.description.match(/^---$/)
565 assert !issue.description.include?('This paragraph is after the delimiter')
566 assert !issue.description.include?('This paragraph is after the delimiter')
566 end
567 end
567 end
568 end
568 end
569 end
569
570
570 def test_email_with_long_subject_line
571 def test_email_with_long_subject_line
571 issue = submit_email('ticket_with_long_subject.eml')
572 issue = submit_email('ticket_with_long_subject.eml')
572 assert issue.is_a?(Issue)
573 assert issue.is_a?(Issue)
573 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]
574 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]
574 end
575 end
575
576
576 def test_new_user_from_attributes_should_return_valid_user
577 def test_new_user_from_attributes_should_return_valid_user
577 to_test = {
578 to_test = {
578 # [address, name] => [login, firstname, lastname]
579 # [address, name] => [login, firstname, lastname]
579 ['jsmith@example.net', nil] => ['jsmith@example.net', 'jsmith', '-'],
580 ['jsmith@example.net', nil] => ['jsmith@example.net', 'jsmith', '-'],
580 ['jsmith@example.net', 'John'] => ['jsmith@example.net', 'John', '-'],
581 ['jsmith@example.net', 'John'] => ['jsmith@example.net', 'John', '-'],
581 ['jsmith@example.net', 'John Smith'] => ['jsmith@example.net', 'John', 'Smith'],
582 ['jsmith@example.net', 'John Smith'] => ['jsmith@example.net', 'John', 'Smith'],
582 ['jsmith@example.net', 'John Paul Smith'] => ['jsmith@example.net', 'John', 'Paul Smith'],
583 ['jsmith@example.net', 'John Paul Smith'] => ['jsmith@example.net', 'John', 'Paul Smith'],
583 ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsTheMaximumLength Smith'] => ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsT', 'Smith'],
584 ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsTheMaximumLength Smith'] => ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsT', 'Smith'],
584 ['jsmith@example.net', 'John AVeryLongLastnameThatExceedsTheMaximumLength'] => ['jsmith@example.net', 'John', 'AVeryLongLastnameThatExceedsTh']
585 ['jsmith@example.net', 'John AVeryLongLastnameThatExceedsTheMaximumLength'] => ['jsmith@example.net', 'John', 'AVeryLongLastnameThatExceedsTh']
585 }
586 }
586
587
587 to_test.each do |attrs, expected|
588 to_test.each do |attrs, expected|
588 user = MailHandler.new_user_from_attributes(attrs.first, attrs.last)
589 user = MailHandler.new_user_from_attributes(attrs.first, attrs.last)
589
590
590 assert user.valid?, user.errors.full_messages.to_s
591 assert user.valid?, user.errors.full_messages.to_s
591 assert_equal attrs.first, user.mail
592 assert_equal attrs.first, user.mail
592 assert_equal expected[0], user.login
593 assert_equal expected[0], user.login
593 assert_equal expected[1], user.firstname
594 assert_equal expected[1], user.firstname
594 assert_equal expected[2], user.lastname
595 assert_equal expected[2], user.lastname
595 end
596 end
596 end
597 end
597
598
598 def test_new_user_from_attributes_should_respect_minimum_password_length
599 def test_new_user_from_attributes_should_respect_minimum_password_length
599 with_settings :password_min_length => 15 do
600 with_settings :password_min_length => 15 do
600 user = MailHandler.new_user_from_attributes('jsmith@example.net')
601 user = MailHandler.new_user_from_attributes('jsmith@example.net')
601 assert user.valid?
602 assert user.valid?
602 assert user.password.length >= 15
603 assert user.password.length >= 15
603 end
604 end
604 end
605 end
605
606
606 def test_new_user_from_attributes_should_use_default_login_if_invalid
607 def test_new_user_from_attributes_should_use_default_login_if_invalid
607 user = MailHandler.new_user_from_attributes('foo+bar@example.net')
608 user = MailHandler.new_user_from_attributes('foo+bar@example.net')
608 assert user.valid?
609 assert user.valid?
609 assert user.login =~ /^user[a-f0-9]+$/
610 assert user.login =~ /^user[a-f0-9]+$/
610 assert_equal 'foo+bar@example.net', user.mail
611 assert_equal 'foo+bar@example.net', user.mail
611 end
612 end
612
613
613 def test_new_user_with_utf8_encoded_fullname_should_be_decoded
614 def test_new_user_with_utf8_encoded_fullname_should_be_decoded
614 assert_difference 'User.count' do
615 assert_difference 'User.count' do
615 issue = submit_email(
616 issue = submit_email(
616 'fullname_of_sender_as_utf8_encoded.eml',
617 'fullname_of_sender_as_utf8_encoded.eml',
617 :issue => {:project => 'ecookbook'},
618 :issue => {:project => 'ecookbook'},
618 :unknown_user => 'create'
619 :unknown_user => 'create'
619 )
620 )
620 end
621 end
621
622
622 user = User.first(:order => 'id DESC')
623 user = User.first(:order => 'id DESC')
623 assert_equal "foo@example.org", user.mail
624 assert_equal "foo@example.org", user.mail
624 str1 = "\xc3\x84\xc3\xa4"
625 str1 = "\xc3\x84\xc3\xa4"
625 str2 = "\xc3\x96\xc3\xb6"
626 str2 = "\xc3\x96\xc3\xb6"
626 str1.force_encoding('UTF-8') if str1.respond_to?(:force_encoding)
627 str1.force_encoding('UTF-8') if str1.respond_to?(:force_encoding)
627 str2.force_encoding('UTF-8') if str2.respond_to?(:force_encoding)
628 str2.force_encoding('UTF-8') if str2.respond_to?(:force_encoding)
628 assert_equal str1, user.firstname
629 assert_equal str1, user.firstname
629 assert_equal str2, user.lastname
630 assert_equal str2, user.lastname
630 end
631 end
631
632
632 private
633 private
633
634
634 def submit_email(filename, options={})
635 def submit_email(filename, options={})
635 raw = IO.read(File.join(FIXTURES_PATH, filename))
636 raw = IO.read(File.join(FIXTURES_PATH, filename))
636 yield raw if block_given?
637 yield raw if block_given?
637 MailHandler.receive(raw, options)
638 MailHandler.receive(raw, options)
638 end
639 end
639
640
640 def assert_issue_created(issue)
641 def assert_issue_created(issue)
641 assert issue.is_a?(Issue)
642 assert issue.is_a?(Issue)
642 assert !issue.new_record?
643 assert !issue.new_record?
643 issue.reload
644 issue.reload
644 end
645 end
645 end
646 end
General Comments 0
You need to be logged in to leave comments. Login now