##// END OF EJS Templates
support more character encoding in incoming emails (#15785)...
Toshi MARUYAMA -
r12199:803a0a030eb5
parent child
Show More
@@ -0,0 +1,14
1 From: John Smith <JSmith@somenet.foo>
2 To: "redmine@somenet.foo" <redmine@somenet.foo>
3 Subject: This is a test
4 Content-Type: multipart/alternative;
5 boundary="_c20d9cfa-d16a-43a3-a7e5-71da7877ab23_"
6
7 --_c20d9cfa-d16a-43a3-a7e5-71da7877ab23_
8 Content-Type: text/plain; charset="ks_c_5601-1987"
9 Content-Transfer-Encoding: base64
10
11 sO24v73AtM+02S4=
12
13 --_c20d9cfa-d16a-43a3-a7e5-71da7877ab23_--
14
@@ -1,536 +1,540
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 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_account_notice] = (@@handler_options[:no_account_notice].to_s == '1')
41 @@handler_options[:no_account_notice] = (@@handler_options[:no_account_notice].to_s == '1')
42 @@handler_options[:no_notification] = (@@handler_options[:no_notification].to_s == '1')
42 @@handler_options[:no_notification] = (@@handler_options[:no_notification].to_s == '1')
43 @@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1')
43 @@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1')
44
44
45 email.force_encoding('ASCII-8BIT') if email.respond_to?(:force_encoding)
45 email.force_encoding('ASCII-8BIT') if email.respond_to?(:force_encoding)
46 super(email)
46 super(email)
47 end
47 end
48
48
49 # Extracts MailHandler options from environment variables
49 # Extracts MailHandler options from environment variables
50 # Use when receiving emails with rake tasks
50 # Use when receiving emails with rake tasks
51 def self.extract_options_from_env(env)
51 def self.extract_options_from_env(env)
52 options = {:issue => {}}
52 options = {:issue => {}}
53 %w(project status tracker category priority).each do |option|
53 %w(project status tracker category priority).each do |option|
54 options[:issue][option.to_sym] = env[option] if env[option]
54 options[:issue][option.to_sym] = env[option] if env[option]
55 end
55 end
56 %w(allow_override unknown_user no_permission_check no_account_notice default_group).each do |option|
56 %w(allow_override unknown_user no_permission_check no_account_notice default_group).each do |option|
57 options[option.to_sym] = env[option] if env[option]
57 options[option.to_sym] = env[option] if env[option]
58 end
58 end
59 options
59 options
60 end
60 end
61
61
62 def logger
62 def logger
63 Rails.logger
63 Rails.logger
64 end
64 end
65
65
66 cattr_accessor :ignored_emails_headers
66 cattr_accessor :ignored_emails_headers
67 @@ignored_emails_headers = {
67 @@ignored_emails_headers = {
68 'X-Auto-Response-Suppress' => 'oof',
68 'X-Auto-Response-Suppress' => 'oof',
69 'Auto-Submitted' => /^auto-/
69 'Auto-Submitted' => /^auto-/
70 }
70 }
71
71
72 # Processes incoming emails
72 # Processes incoming emails
73 # Returns the created object (eg. an issue, a message) or false
73 # Returns the created object (eg. an issue, a message) or false
74 def receive(email)
74 def receive(email)
75 @email = email
75 @email = email
76 sender_email = email.from.to_a.first.to_s.strip
76 sender_email = email.from.to_a.first.to_s.strip
77 # Ignore emails received from the application emission address to avoid hell cycles
77 # Ignore emails received from the application emission address to avoid hell cycles
78 if sender_email.downcase == Setting.mail_from.to_s.strip.downcase
78 if sender_email.downcase == Setting.mail_from.to_s.strip.downcase
79 if logger
79 if logger
80 logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]"
80 logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]"
81 end
81 end
82 return false
82 return false
83 end
83 end
84 # Ignore auto generated emails
84 # Ignore auto generated emails
85 self.class.ignored_emails_headers.each do |key, ignored_value|
85 self.class.ignored_emails_headers.each do |key, ignored_value|
86 value = email.header[key]
86 value = email.header[key]
87 if value
87 if value
88 value = value.to_s.downcase
88 value = value.to_s.downcase
89 if (ignored_value.is_a?(Regexp) && value.match(ignored_value)) || value == ignored_value
89 if (ignored_value.is_a?(Regexp) && value.match(ignored_value)) || value == ignored_value
90 if logger
90 if logger
91 logger.info "MailHandler: ignoring email with #{key}:#{value} header"
91 logger.info "MailHandler: ignoring email with #{key}:#{value} header"
92 end
92 end
93 return false
93 return false
94 end
94 end
95 end
95 end
96 end
96 end
97 @user = User.find_by_mail(sender_email) if sender_email.present?
97 @user = User.find_by_mail(sender_email) if sender_email.present?
98 if @user && !@user.active?
98 if @user && !@user.active?
99 if logger
99 if logger
100 logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]"
100 logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]"
101 end
101 end
102 return false
102 return false
103 end
103 end
104 if @user.nil?
104 if @user.nil?
105 # Email was submitted by an unknown user
105 # Email was submitted by an unknown user
106 case @@handler_options[:unknown_user]
106 case @@handler_options[:unknown_user]
107 when 'accept'
107 when 'accept'
108 @user = User.anonymous
108 @user = User.anonymous
109 when 'create'
109 when 'create'
110 @user = create_user_from_email
110 @user = create_user_from_email
111 if @user
111 if @user
112 if logger
112 if logger
113 logger.info "MailHandler: [#{@user.login}] account created"
113 logger.info "MailHandler: [#{@user.login}] account created"
114 end
114 end
115 add_user_to_group(@@handler_options[:default_group])
115 add_user_to_group(@@handler_options[:default_group])
116 unless @@handler_options[:no_account_notice]
116 unless @@handler_options[:no_account_notice]
117 Mailer.account_information(@user, @user.password).deliver
117 Mailer.account_information(@user, @user.password).deliver
118 end
118 end
119 else
119 else
120 if logger
120 if logger
121 logger.error "MailHandler: could not create account for [#{sender_email}]"
121 logger.error "MailHandler: could not create account for [#{sender_email}]"
122 end
122 end
123 return false
123 return false
124 end
124 end
125 else
125 else
126 # Default behaviour, emails from unknown users are ignored
126 # Default behaviour, emails from unknown users are ignored
127 if logger
127 if logger
128 logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]"
128 logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]"
129 end
129 end
130 return false
130 return false
131 end
131 end
132 end
132 end
133 User.current = @user
133 User.current = @user
134 dispatch
134 dispatch
135 end
135 end
136
136
137 private
137 private
138
138
139 MESSAGE_ID_RE = %r{^<?redmine\.([a-z0-9_]+)\-(\d+)\.\d+(\.[a-f0-9]+)?@}
139 MESSAGE_ID_RE = %r{^<?redmine\.([a-z0-9_]+)\-(\d+)\.\d+(\.[a-f0-9]+)?@}
140 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]*#(\d+)\]}
140 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]*#(\d+)\]}
141 MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
141 MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
142
142
143 def dispatch
143 def dispatch
144 headers = [email.in_reply_to, email.references].flatten.compact
144 headers = [email.in_reply_to, email.references].flatten.compact
145 subject = email.subject.to_s
145 subject = email.subject.to_s
146 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
146 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
147 klass, object_id = $1, $2.to_i
147 klass, object_id = $1, $2.to_i
148 method_name = "receive_#{klass}_reply"
148 method_name = "receive_#{klass}_reply"
149 if self.class.private_instance_methods.collect(&:to_s).include?(method_name)
149 if self.class.private_instance_methods.collect(&:to_s).include?(method_name)
150 send method_name, object_id
150 send method_name, object_id
151 else
151 else
152 # ignoring it
152 # ignoring it
153 end
153 end
154 elsif m = subject.match(ISSUE_REPLY_SUBJECT_RE)
154 elsif m = subject.match(ISSUE_REPLY_SUBJECT_RE)
155 receive_issue_reply(m[1].to_i)
155 receive_issue_reply(m[1].to_i)
156 elsif m = subject.match(MESSAGE_REPLY_SUBJECT_RE)
156 elsif m = subject.match(MESSAGE_REPLY_SUBJECT_RE)
157 receive_message_reply(m[1].to_i)
157 receive_message_reply(m[1].to_i)
158 else
158 else
159 dispatch_to_default
159 dispatch_to_default
160 end
160 end
161 rescue ActiveRecord::RecordInvalid => e
161 rescue ActiveRecord::RecordInvalid => e
162 # TODO: send a email to the user
162 # TODO: send a email to the user
163 logger.error e.message if logger
163 logger.error e.message if logger
164 false
164 false
165 rescue MissingInformation => e
165 rescue MissingInformation => e
166 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
166 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
167 false
167 false
168 rescue UnauthorizedAction => e
168 rescue UnauthorizedAction => e
169 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
169 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
170 false
170 false
171 end
171 end
172
172
173 def dispatch_to_default
173 def dispatch_to_default
174 receive_issue
174 receive_issue
175 end
175 end
176
176
177 # Creates a new issue
177 # Creates a new issue
178 def receive_issue
178 def receive_issue
179 project = target_project
179 project = target_project
180 # check permission
180 # check permission
181 unless @@handler_options[:no_permission_check]
181 unless @@handler_options[:no_permission_check]
182 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
182 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
183 end
183 end
184
184
185 issue = Issue.new(:author => user, :project => project)
185 issue = Issue.new(:author => user, :project => project)
186 issue.safe_attributes = issue_attributes_from_keywords(issue)
186 issue.safe_attributes = issue_attributes_from_keywords(issue)
187 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
187 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
188 issue.subject = cleaned_up_subject
188 issue.subject = cleaned_up_subject
189 if issue.subject.blank?
189 if issue.subject.blank?
190 issue.subject = '(no subject)'
190 issue.subject = '(no subject)'
191 end
191 end
192 issue.description = cleaned_up_text_body
192 issue.description = cleaned_up_text_body
193
193
194 # add To and Cc as watchers before saving so the watchers can reply to Redmine
194 # add To and Cc as watchers before saving so the watchers can reply to Redmine
195 add_watchers(issue)
195 add_watchers(issue)
196 issue.save!
196 issue.save!
197 add_attachments(issue)
197 add_attachments(issue)
198 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger
198 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger
199 issue
199 issue
200 end
200 end
201
201
202 # Adds a note to an existing issue
202 # Adds a note to an existing issue
203 def receive_issue_reply(issue_id, from_journal=nil)
203 def receive_issue_reply(issue_id, from_journal=nil)
204 issue = Issue.find_by_id(issue_id)
204 issue = Issue.find_by_id(issue_id)
205 return unless issue
205 return unless issue
206 # check permission
206 # check permission
207 unless @@handler_options[:no_permission_check]
207 unless @@handler_options[:no_permission_check]
208 unless user.allowed_to?(:add_issue_notes, issue.project) ||
208 unless user.allowed_to?(:add_issue_notes, issue.project) ||
209 user.allowed_to?(:edit_issues, issue.project)
209 user.allowed_to?(:edit_issues, issue.project)
210 raise UnauthorizedAction
210 raise UnauthorizedAction
211 end
211 end
212 end
212 end
213
213
214 # ignore CLI-supplied defaults for new issues
214 # ignore CLI-supplied defaults for new issues
215 @@handler_options[:issue].clear
215 @@handler_options[:issue].clear
216
216
217 journal = issue.init_journal(user)
217 journal = issue.init_journal(user)
218 if from_journal && from_journal.private_notes?
218 if from_journal && from_journal.private_notes?
219 # If the received email was a reply to a private note, make the added note private
219 # If the received email was a reply to a private note, make the added note private
220 issue.private_notes = true
220 issue.private_notes = true
221 end
221 end
222 issue.safe_attributes = issue_attributes_from_keywords(issue)
222 issue.safe_attributes = issue_attributes_from_keywords(issue)
223 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
223 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
224 journal.notes = cleaned_up_text_body
224 journal.notes = cleaned_up_text_body
225 add_attachments(issue)
225 add_attachments(issue)
226 issue.save!
226 issue.save!
227 if logger
227 if logger
228 logger.info "MailHandler: issue ##{issue.id} updated by #{user}"
228 logger.info "MailHandler: issue ##{issue.id} updated by #{user}"
229 end
229 end
230 journal
230 journal
231 end
231 end
232
232
233 # Reply will be added to the issue
233 # Reply will be added to the issue
234 def receive_journal_reply(journal_id)
234 def receive_journal_reply(journal_id)
235 journal = Journal.find_by_id(journal_id)
235 journal = Journal.find_by_id(journal_id)
236 if journal && journal.journalized_type == 'Issue'
236 if journal && journal.journalized_type == 'Issue'
237 receive_issue_reply(journal.journalized_id, journal)
237 receive_issue_reply(journal.journalized_id, journal)
238 end
238 end
239 end
239 end
240
240
241 # Receives a reply to a forum message
241 # Receives a reply to a forum message
242 def receive_message_reply(message_id)
242 def receive_message_reply(message_id)
243 message = Message.find_by_id(message_id)
243 message = Message.find_by_id(message_id)
244 if message
244 if message
245 message = message.root
245 message = message.root
246
246
247 unless @@handler_options[:no_permission_check]
247 unless @@handler_options[:no_permission_check]
248 raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
248 raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
249 end
249 end
250
250
251 if !message.locked?
251 if !message.locked?
252 reply = Message.new(:subject => cleaned_up_subject.gsub(%r{^.*msg\d+\]}, '').strip,
252 reply = Message.new(:subject => cleaned_up_subject.gsub(%r{^.*msg\d+\]}, '').strip,
253 :content => cleaned_up_text_body)
253 :content => cleaned_up_text_body)
254 reply.author = user
254 reply.author = user
255 reply.board = message.board
255 reply.board = message.board
256 message.children << reply
256 message.children << reply
257 add_attachments(reply)
257 add_attachments(reply)
258 reply
258 reply
259 else
259 else
260 if logger
260 if logger
261 logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic"
261 logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic"
262 end
262 end
263 end
263 end
264 end
264 end
265 end
265 end
266
266
267 def add_attachments(obj)
267 def add_attachments(obj)
268 if email.attachments && email.attachments.any?
268 if email.attachments && email.attachments.any?
269 email.attachments.each do |attachment|
269 email.attachments.each do |attachment|
270 next unless accept_attachment?(attachment)
270 next unless accept_attachment?(attachment)
271 obj.attachments << Attachment.create(:container => obj,
271 obj.attachments << Attachment.create(:container => obj,
272 :file => attachment.decoded,
272 :file => attachment.decoded,
273 :filename => attachment.filename,
273 :filename => attachment.filename,
274 :author => user,
274 :author => user,
275 :content_type => attachment.mime_type)
275 :content_type => attachment.mime_type)
276 end
276 end
277 end
277 end
278 end
278 end
279
279
280 # Returns false if the +attachment+ of the incoming email should be ignored
280 # Returns false if the +attachment+ of the incoming email should be ignored
281 def accept_attachment?(attachment)
281 def accept_attachment?(attachment)
282 @excluded ||= Setting.mail_handler_excluded_filenames.to_s.split(',').map(&:strip).reject(&:blank?)
282 @excluded ||= Setting.mail_handler_excluded_filenames.to_s.split(',').map(&:strip).reject(&:blank?)
283 @excluded.each do |pattern|
283 @excluded.each do |pattern|
284 regexp = %r{\A#{Regexp.escape(pattern).gsub("\\*", ".*")}\z}i
284 regexp = %r{\A#{Regexp.escape(pattern).gsub("\\*", ".*")}\z}i
285 if attachment.filename.to_s =~ regexp
285 if attachment.filename.to_s =~ regexp
286 logger.info "MailHandler: ignoring attachment #{attachment.filename} matching #{pattern}"
286 logger.info "MailHandler: ignoring attachment #{attachment.filename} matching #{pattern}"
287 return false
287 return false
288 end
288 end
289 end
289 end
290 true
290 true
291 end
291 end
292
292
293 # Adds To and Cc as watchers of the given object if the sender has the
293 # Adds To and Cc as watchers of the given object if the sender has the
294 # appropriate permission
294 # appropriate permission
295 def add_watchers(obj)
295 def add_watchers(obj)
296 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
296 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
297 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
297 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
298 unless addresses.empty?
298 unless addresses.empty?
299 watchers = User.active.where('LOWER(mail) IN (?)', addresses).all
299 watchers = User.active.where('LOWER(mail) IN (?)', addresses).all
300 watchers.each {|w| obj.add_watcher(w)}
300 watchers.each {|w| obj.add_watcher(w)}
301 end
301 end
302 end
302 end
303 end
303 end
304
304
305 def get_keyword(attr, options={})
305 def get_keyword(attr, options={})
306 @keywords ||= {}
306 @keywords ||= {}
307 if @keywords.has_key?(attr)
307 if @keywords.has_key?(attr)
308 @keywords[attr]
308 @keywords[attr]
309 else
309 else
310 @keywords[attr] = begin
310 @keywords[attr] = begin
311 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) &&
311 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) &&
312 (v = extract_keyword!(plain_text_body, attr, options[:format]))
312 (v = extract_keyword!(plain_text_body, attr, options[:format]))
313 v
313 v
314 elsif !@@handler_options[:issue][attr].blank?
314 elsif !@@handler_options[:issue][attr].blank?
315 @@handler_options[:issue][attr]
315 @@handler_options[:issue][attr]
316 end
316 end
317 end
317 end
318 end
318 end
319 end
319 end
320
320
321 # Destructively extracts the value for +attr+ in +text+
321 # Destructively extracts the value for +attr+ in +text+
322 # Returns nil if no matching keyword found
322 # Returns nil if no matching keyword found
323 def extract_keyword!(text, attr, format=nil)
323 def extract_keyword!(text, attr, format=nil)
324 keys = [attr.to_s.humanize]
324 keys = [attr.to_s.humanize]
325 if attr.is_a?(Symbol)
325 if attr.is_a?(Symbol)
326 if user && user.language.present?
326 if user && user.language.present?
327 keys << l("field_#{attr}", :default => '', :locale => user.language)
327 keys << l("field_#{attr}", :default => '', :locale => user.language)
328 end
328 end
329 if Setting.default_language.present?
329 if Setting.default_language.present?
330 keys << l("field_#{attr}", :default => '', :locale => Setting.default_language)
330 keys << l("field_#{attr}", :default => '', :locale => Setting.default_language)
331 end
331 end
332 end
332 end
333 keys.reject! {|k| k.blank?}
333 keys.reject! {|k| k.blank?}
334 keys.collect! {|k| Regexp.escape(k)}
334 keys.collect! {|k| Regexp.escape(k)}
335 format ||= '.+'
335 format ||= '.+'
336 keyword = nil
336 keyword = nil
337 regexp = /^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i
337 regexp = /^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i
338 if m = text.match(regexp)
338 if m = text.match(regexp)
339 keyword = m[2].strip
339 keyword = m[2].strip
340 text.gsub!(regexp, '')
340 text.gsub!(regexp, '')
341 end
341 end
342 keyword
342 keyword
343 end
343 end
344
344
345 def target_project
345 def target_project
346 # TODO: other ways to specify project:
346 # TODO: other ways to specify project:
347 # * parse the email To field
347 # * parse the email To field
348 # * specific project (eg. Setting.mail_handler_target_project)
348 # * specific project (eg. Setting.mail_handler_target_project)
349 target = Project.find_by_identifier(get_keyword(:project))
349 target = Project.find_by_identifier(get_keyword(:project))
350 if target.nil?
350 if target.nil?
351 # Invalid project keyword, use the project specified as the default one
351 # Invalid project keyword, use the project specified as the default one
352 default_project = @@handler_options[:issue][:project]
352 default_project = @@handler_options[:issue][:project]
353 if default_project.present?
353 if default_project.present?
354 target = Project.find_by_identifier(default_project)
354 target = Project.find_by_identifier(default_project)
355 end
355 end
356 end
356 end
357 raise MissingInformation.new('Unable to determine target project') if target.nil?
357 raise MissingInformation.new('Unable to determine target project') if target.nil?
358 target
358 target
359 end
359 end
360
360
361 # Returns a Hash of issue attributes extracted from keywords in the email body
361 # Returns a Hash of issue attributes extracted from keywords in the email body
362 def issue_attributes_from_keywords(issue)
362 def issue_attributes_from_keywords(issue)
363 assigned_to = (k = get_keyword(:assigned_to, :override => true)) && find_assignee_from_keyword(k, issue)
363 assigned_to = (k = get_keyword(:assigned_to, :override => true)) && find_assignee_from_keyword(k, issue)
364
364
365 attrs = {
365 attrs = {
366 'tracker_id' => (k = get_keyword(:tracker)) && issue.project.trackers.named(k).first.try(:id),
366 'tracker_id' => (k = get_keyword(:tracker)) && issue.project.trackers.named(k).first.try(:id),
367 'status_id' => (k = get_keyword(:status)) && IssueStatus.named(k).first.try(:id),
367 'status_id' => (k = get_keyword(:status)) && IssueStatus.named(k).first.try(:id),
368 'priority_id' => (k = get_keyword(:priority)) && IssuePriority.named(k).first.try(:id),
368 'priority_id' => (k = get_keyword(:priority)) && IssuePriority.named(k).first.try(:id),
369 'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.named(k).first.try(:id),
369 'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.named(k).first.try(:id),
370 'assigned_to_id' => assigned_to.try(:id),
370 'assigned_to_id' => assigned_to.try(:id),
371 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) &&
371 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) &&
372 issue.project.shared_versions.named(k).first.try(:id),
372 issue.project.shared_versions.named(k).first.try(:id),
373 'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
373 'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
374 'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
374 'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
375 'estimated_hours' => get_keyword(:estimated_hours, :override => true),
375 'estimated_hours' => get_keyword(:estimated_hours, :override => true),
376 'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0')
376 'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0')
377 }.delete_if {|k, v| v.blank? }
377 }.delete_if {|k, v| v.blank? }
378
378
379 if issue.new_record? && attrs['tracker_id'].nil?
379 if issue.new_record? && attrs['tracker_id'].nil?
380 attrs['tracker_id'] = issue.project.trackers.first.try(:id)
380 attrs['tracker_id'] = issue.project.trackers.first.try(:id)
381 end
381 end
382
382
383 attrs
383 attrs
384 end
384 end
385
385
386 # Returns a Hash of issue custom field values extracted from keywords in the email body
386 # Returns a Hash of issue custom field values extracted from keywords in the email body
387 def custom_field_values_from_keywords(customized)
387 def custom_field_values_from_keywords(customized)
388 customized.custom_field_values.inject({}) do |h, v|
388 customized.custom_field_values.inject({}) do |h, v|
389 if keyword = get_keyword(v.custom_field.name, :override => true)
389 if keyword = get_keyword(v.custom_field.name, :override => true)
390 h[v.custom_field.id.to_s] = v.custom_field.value_from_keyword(keyword, customized)
390 h[v.custom_field.id.to_s] = v.custom_field.value_from_keyword(keyword, customized)
391 end
391 end
392 h
392 h
393 end
393 end
394 end
394 end
395
395
396 # Returns the text/plain part of the email
396 # Returns the text/plain part of the email
397 # If not found (eg. HTML-only email), returns the body with tags removed
397 # If not found (eg. HTML-only email), returns the body with tags removed
398 def plain_text_body
398 def plain_text_body
399 return @plain_text_body unless @plain_text_body.nil?
399 return @plain_text_body unless @plain_text_body.nil?
400
400
401 parts = if (text_parts = email.all_parts.select {|p| p.mime_type == 'text/plain'}).present?
401 parts = if (text_parts = email.all_parts.select {|p| p.mime_type == 'text/plain'}).present?
402 text_parts
402 text_parts
403 elsif (html_parts = email.all_parts.select {|p| p.mime_type == 'text/html'}).present?
403 elsif (html_parts = email.all_parts.select {|p| p.mime_type == 'text/html'}).present?
404 html_parts
404 html_parts
405 else
405 else
406 [email]
406 [email]
407 end
407 end
408
408
409 parts.reject! do |part|
409 parts.reject! do |part|
410 part.header[:content_disposition].try(:disposition_type) == 'attachment'
410 part.header[:content_disposition].try(:disposition_type) == 'attachment'
411 end
411 end
412
412
413 @plain_text_body = parts.map {|p| Redmine::CodesetUtil.to_utf8(p.body.decoded, p.charset)}.join("\r\n")
413 @plain_text_body = parts.map do |p|
414 body_charset = p.charset.respond_to?(:force_encoding) ?
415 Mail::RubyVer.pick_encoding(p.charset).to_s : p.charset
416 Redmine::CodesetUtil.to_utf8(p.body.decoded, body_charset)
417 end.join("\r\n")
414
418
415 # strip html tags and remove doctype directive
419 # strip html tags and remove doctype directive
416 if parts.any? {|p| p.mime_type == 'text/html'}
420 if parts.any? {|p| p.mime_type == 'text/html'}
417 @plain_text_body = strip_tags(@plain_text_body.strip)
421 @plain_text_body = strip_tags(@plain_text_body.strip)
418 @plain_text_body.sub! %r{^<!DOCTYPE .*$}, ''
422 @plain_text_body.sub! %r{^<!DOCTYPE .*$}, ''
419 end
423 end
420
424
421 @plain_text_body
425 @plain_text_body
422 end
426 end
423
427
424 def cleaned_up_text_body
428 def cleaned_up_text_body
425 cleanup_body(plain_text_body)
429 cleanup_body(plain_text_body)
426 end
430 end
427
431
428 def cleaned_up_subject
432 def cleaned_up_subject
429 subject = email.subject.to_s
433 subject = email.subject.to_s
430 subject.strip[0,255]
434 subject.strip[0,255]
431 end
435 end
432
436
433 def self.full_sanitizer
437 def self.full_sanitizer
434 @full_sanitizer ||= HTML::FullSanitizer.new
438 @full_sanitizer ||= HTML::FullSanitizer.new
435 end
439 end
436
440
437 def self.assign_string_attribute_with_limit(object, attribute, value, limit=nil)
441 def self.assign_string_attribute_with_limit(object, attribute, value, limit=nil)
438 limit ||= object.class.columns_hash[attribute.to_s].limit || 255
442 limit ||= object.class.columns_hash[attribute.to_s].limit || 255
439 value = value.to_s.slice(0, limit)
443 value = value.to_s.slice(0, limit)
440 object.send("#{attribute}=", value)
444 object.send("#{attribute}=", value)
441 end
445 end
442
446
443 # Returns a User from an email address and a full name
447 # Returns a User from an email address and a full name
444 def self.new_user_from_attributes(email_address, fullname=nil)
448 def self.new_user_from_attributes(email_address, fullname=nil)
445 user = User.new
449 user = User.new
446
450
447 # Truncating the email address would result in an invalid format
451 # Truncating the email address would result in an invalid format
448 user.mail = email_address
452 user.mail = email_address
449 assign_string_attribute_with_limit(user, 'login', email_address, User::LOGIN_LENGTH_LIMIT)
453 assign_string_attribute_with_limit(user, 'login', email_address, User::LOGIN_LENGTH_LIMIT)
450
454
451 names = fullname.blank? ? email_address.gsub(/@.*$/, '').split('.') : fullname.split
455 names = fullname.blank? ? email_address.gsub(/@.*$/, '').split('.') : fullname.split
452 assign_string_attribute_with_limit(user, 'firstname', names.shift, 30)
456 assign_string_attribute_with_limit(user, 'firstname', names.shift, 30)
453 assign_string_attribute_with_limit(user, 'lastname', names.join(' '), 30)
457 assign_string_attribute_with_limit(user, 'lastname', names.join(' '), 30)
454 user.lastname = '-' if user.lastname.blank?
458 user.lastname = '-' if user.lastname.blank?
455 user.language = Setting.default_language
459 user.language = Setting.default_language
456 user.generate_password = true
460 user.generate_password = true
457 user.mail_notification = 'only_my_events'
461 user.mail_notification = 'only_my_events'
458
462
459 unless user.valid?
463 unless user.valid?
460 user.login = "user#{Redmine::Utils.random_hex(6)}" unless user.errors[:login].blank?
464 user.login = "user#{Redmine::Utils.random_hex(6)}" unless user.errors[:login].blank?
461 user.firstname = "-" unless user.errors[:firstname].blank?
465 user.firstname = "-" unless user.errors[:firstname].blank?
462 (puts user.errors[:lastname];user.lastname = "-") unless user.errors[:lastname].blank?
466 (puts user.errors[:lastname];user.lastname = "-") unless user.errors[:lastname].blank?
463 end
467 end
464
468
465 user
469 user
466 end
470 end
467
471
468 # Creates a User for the +email+ sender
472 # Creates a User for the +email+ sender
469 # Returns the user or nil if it could not be created
473 # Returns the user or nil if it could not be created
470 def create_user_from_email
474 def create_user_from_email
471 from = email.header['from'].to_s
475 from = email.header['from'].to_s
472 addr, name = from, nil
476 addr, name = from, nil
473 if m = from.match(/^"?(.+?)"?\s+<(.+@.+)>$/)
477 if m = from.match(/^"?(.+?)"?\s+<(.+@.+)>$/)
474 addr, name = m[2], m[1]
478 addr, name = m[2], m[1]
475 end
479 end
476 if addr.present?
480 if addr.present?
477 user = self.class.new_user_from_attributes(addr, name)
481 user = self.class.new_user_from_attributes(addr, name)
478 if @@handler_options[:no_notification]
482 if @@handler_options[:no_notification]
479 user.mail_notification = 'none'
483 user.mail_notification = 'none'
480 end
484 end
481 if user.save
485 if user.save
482 user
486 user
483 else
487 else
484 logger.error "MailHandler: failed to create User: #{user.errors.full_messages}" if logger
488 logger.error "MailHandler: failed to create User: #{user.errors.full_messages}" if logger
485 nil
489 nil
486 end
490 end
487 else
491 else
488 logger.error "MailHandler: failed to create User: no FROM address found" if logger
492 logger.error "MailHandler: failed to create User: no FROM address found" if logger
489 nil
493 nil
490 end
494 end
491 end
495 end
492
496
493 # Adds the newly created user to default group
497 # Adds the newly created user to default group
494 def add_user_to_group(default_group)
498 def add_user_to_group(default_group)
495 if default_group.present?
499 if default_group.present?
496 default_group.split(',').each do |group_name|
500 default_group.split(',').each do |group_name|
497 if group = Group.named(group_name).first
501 if group = Group.named(group_name).first
498 group.users << @user
502 group.users << @user
499 elsif logger
503 elsif logger
500 logger.warn "MailHandler: could not add user to [#{group_name}], group not found"
504 logger.warn "MailHandler: could not add user to [#{group_name}], group not found"
501 end
505 end
502 end
506 end
503 end
507 end
504 end
508 end
505
509
506 # Removes the email body of text after the truncation configurations.
510 # Removes the email body of text after the truncation configurations.
507 def cleanup_body(body)
511 def cleanup_body(body)
508 delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
512 delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
509 unless delimiters.empty?
513 unless delimiters.empty?
510 regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
514 regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
511 body = body.gsub(regex, '')
515 body = body.gsub(regex, '')
512 end
516 end
513 body.strip
517 body.strip
514 end
518 end
515
519
516 def find_assignee_from_keyword(keyword, issue)
520 def find_assignee_from_keyword(keyword, issue)
517 keyword = keyword.to_s.downcase
521 keyword = keyword.to_s.downcase
518 assignable = issue.assignable_users
522 assignable = issue.assignable_users
519 assignee = nil
523 assignee = nil
520 assignee ||= assignable.detect {|a|
524 assignee ||= assignable.detect {|a|
521 a.mail.to_s.downcase == keyword ||
525 a.mail.to_s.downcase == keyword ||
522 a.login.to_s.downcase == keyword
526 a.login.to_s.downcase == keyword
523 }
527 }
524 if assignee.nil? && keyword.match(/ /)
528 if assignee.nil? && keyword.match(/ /)
525 firstname, lastname = *(keyword.split) # "First Last Throwaway"
529 firstname, lastname = *(keyword.split) # "First Last Throwaway"
526 assignee ||= assignable.detect {|a|
530 assignee ||= assignable.detect {|a|
527 a.is_a?(User) && a.firstname.to_s.downcase == firstname &&
531 a.is_a?(User) && a.firstname.to_s.downcase == firstname &&
528 a.lastname.to_s.downcase == lastname
532 a.lastname.to_s.downcase == lastname
529 }
533 }
530 end
534 end
531 if assignee.nil?
535 if assignee.nil?
532 assignee ||= assignable.detect {|a| a.name.downcase == keyword}
536 assignee ||= assignable.detect {|a| a.name.downcase == keyword}
533 end
537 end
534 assignee
538 assignee
535 end
539 end
536 end
540 end
@@ -1,869 +1,886
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 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 'PostgreSQL', issue.custom_field_value(1)
180 assert_equal 'PostgreSQL', issue.custom_field_value(1)
181 assert_equal 'Value for a custom field', issue.custom_field_value(2)
181 assert_equal 'Value for a custom field', issue.custom_field_value(2)
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_created_user_should_be_added_to_groups
307 def test_created_user_should_be_added_to_groups
308 group1 = Group.generate!
308 group1 = Group.generate!
309 group2 = Group.generate!
309 group2 = Group.generate!
310
310
311 assert_difference 'User.count' do
311 assert_difference 'User.count' do
312 submit_email(
312 submit_email(
313 'ticket_by_unknown_user.eml',
313 'ticket_by_unknown_user.eml',
314 :issue => {:project => 'ecookbook'},
314 :issue => {:project => 'ecookbook'},
315 :unknown_user => 'create',
315 :unknown_user => 'create',
316 :default_group => "#{group1.name},#{group2.name}"
316 :default_group => "#{group1.name},#{group2.name}"
317 )
317 )
318 end
318 end
319 user = User.order('id DESC').first
319 user = User.order('id DESC').first
320 assert_same_elements [group1, group2], user.groups
320 assert_same_elements [group1, group2], user.groups
321 end
321 end
322
322
323 def test_created_user_should_not_receive_account_information_with_no_account_info_option
323 def test_created_user_should_not_receive_account_information_with_no_account_info_option
324 assert_difference 'User.count' do
324 assert_difference 'User.count' do
325 submit_email(
325 submit_email(
326 'ticket_by_unknown_user.eml',
326 'ticket_by_unknown_user.eml',
327 :issue => {:project => 'ecookbook'},
327 :issue => {:project => 'ecookbook'},
328 :unknown_user => 'create',
328 :unknown_user => 'create',
329 :no_account_notice => '1'
329 :no_account_notice => '1'
330 )
330 )
331 end
331 end
332
332
333 # only 1 email for the new issue notification
333 # only 1 email for the new issue notification
334 assert_equal 1, ActionMailer::Base.deliveries.size
334 assert_equal 1, ActionMailer::Base.deliveries.size
335 email = ActionMailer::Base.deliveries.first
335 email = ActionMailer::Base.deliveries.first
336 assert_include 'Ticket by unknown user', email.subject
336 assert_include 'Ticket by unknown user', email.subject
337 end
337 end
338
338
339 def test_created_user_should_have_mail_notification_to_none_with_no_notification_option
339 def test_created_user_should_have_mail_notification_to_none_with_no_notification_option
340 assert_difference 'User.count' do
340 assert_difference 'User.count' do
341 submit_email(
341 submit_email(
342 'ticket_by_unknown_user.eml',
342 'ticket_by_unknown_user.eml',
343 :issue => {:project => 'ecookbook'},
343 :issue => {:project => 'ecookbook'},
344 :unknown_user => 'create',
344 :unknown_user => 'create',
345 :no_notification => '1'
345 :no_notification => '1'
346 )
346 )
347 end
347 end
348 user = User.order('id DESC').first
348 user = User.order('id DESC').first
349 assert_equal 'none', user.mail_notification
349 assert_equal 'none', user.mail_notification
350 end
350 end
351
351
352 def test_add_issue_without_from_header
352 def test_add_issue_without_from_header
353 Role.anonymous.add_permission!(:add_issues)
353 Role.anonymous.add_permission!(:add_issues)
354 assert_equal false, submit_email('ticket_without_from_header.eml')
354 assert_equal false, submit_email('ticket_without_from_header.eml')
355 end
355 end
356
356
357 def test_add_issue_with_invalid_attributes
357 def test_add_issue_with_invalid_attributes
358 issue = submit_email(
358 issue = submit_email(
359 'ticket_with_invalid_attributes.eml',
359 'ticket_with_invalid_attributes.eml',
360 :allow_override => 'tracker,category,priority'
360 :allow_override => 'tracker,category,priority'
361 )
361 )
362 assert issue.is_a?(Issue)
362 assert issue.is_a?(Issue)
363 assert !issue.new_record?
363 assert !issue.new_record?
364 issue.reload
364 issue.reload
365 assert_nil issue.assigned_to
365 assert_nil issue.assigned_to
366 assert_nil issue.start_date
366 assert_nil issue.start_date
367 assert_nil issue.due_date
367 assert_nil issue.due_date
368 assert_equal 0, issue.done_ratio
368 assert_equal 0, issue.done_ratio
369 assert_equal 'Normal', issue.priority.to_s
369 assert_equal 'Normal', issue.priority.to_s
370 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
370 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
371 end
371 end
372
372
373 def test_add_issue_with_invalid_project_should_be_assigned_to_default_project
373 def test_add_issue_with_invalid_project_should_be_assigned_to_default_project
374 issue = submit_email('ticket_on_given_project.eml', :issue => {:project => 'ecookbook'}, :allow_override => 'project') do |email|
374 issue = submit_email('ticket_on_given_project.eml', :issue => {:project => 'ecookbook'}, :allow_override => 'project') do |email|
375 email.gsub!(/^Project:.+$/, 'Project: invalid')
375 email.gsub!(/^Project:.+$/, 'Project: invalid')
376 end
376 end
377 assert issue.is_a?(Issue)
377 assert issue.is_a?(Issue)
378 assert !issue.new_record?
378 assert !issue.new_record?
379 assert_equal 'ecookbook', issue.project.identifier
379 assert_equal 'ecookbook', issue.project.identifier
380 end
380 end
381
381
382 def test_add_issue_with_localized_attributes
382 def test_add_issue_with_localized_attributes
383 User.find_by_mail('jsmith@somenet.foo').update_attribute 'language', 'fr'
383 User.find_by_mail('jsmith@somenet.foo').update_attribute 'language', 'fr'
384 issue = submit_email(
384 issue = submit_email(
385 'ticket_with_localized_attributes.eml',
385 'ticket_with_localized_attributes.eml',
386 :allow_override => 'tracker,category,priority'
386 :allow_override => 'tracker,category,priority'
387 )
387 )
388 assert issue.is_a?(Issue)
388 assert issue.is_a?(Issue)
389 assert !issue.new_record?
389 assert !issue.new_record?
390 issue.reload
390 issue.reload
391 assert_equal 'New ticket on a given project', issue.subject
391 assert_equal 'New ticket on a given project', issue.subject
392 assert_equal User.find_by_login('jsmith'), issue.author
392 assert_equal User.find_by_login('jsmith'), issue.author
393 assert_equal Project.find(2), issue.project
393 assert_equal Project.find(2), issue.project
394 assert_equal 'Feature request', issue.tracker.to_s
394 assert_equal 'Feature request', issue.tracker.to_s
395 assert_equal 'Stock management', issue.category.to_s
395 assert_equal 'Stock management', issue.category.to_s
396 assert_equal 'Urgent', issue.priority.to_s
396 assert_equal 'Urgent', issue.priority.to_s
397 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
397 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
398 end
398 end
399
399
400 def test_add_issue_with_japanese_keywords
400 def test_add_issue_with_japanese_keywords
401 ja_dev = "\xe9\x96\x8b\xe7\x99\xba"
401 ja_dev = "\xe9\x96\x8b\xe7\x99\xba"
402 ja_dev.force_encoding('UTF-8') if ja_dev.respond_to?(:force_encoding)
402 ja_dev.force_encoding('UTF-8') if ja_dev.respond_to?(:force_encoding)
403 tracker = Tracker.create!(:name => ja_dev)
403 tracker = Tracker.create!(:name => ja_dev)
404 Project.find(1).trackers << tracker
404 Project.find(1).trackers << tracker
405 issue = submit_email(
405 issue = submit_email(
406 'japanese_keywords_iso_2022_jp.eml',
406 'japanese_keywords_iso_2022_jp.eml',
407 :issue => {:project => 'ecookbook'},
407 :issue => {:project => 'ecookbook'},
408 :allow_override => 'tracker'
408 :allow_override => 'tracker'
409 )
409 )
410 assert_kind_of Issue, issue
410 assert_kind_of Issue, issue
411 assert_equal tracker, issue.tracker
411 assert_equal tracker, issue.tracker
412 end
412 end
413
413
414 def test_add_issue_from_apple_mail
414 def test_add_issue_from_apple_mail
415 issue = submit_email(
415 issue = submit_email(
416 'apple_mail_with_attachment.eml',
416 'apple_mail_with_attachment.eml',
417 :issue => {:project => 'ecookbook'}
417 :issue => {:project => 'ecookbook'}
418 )
418 )
419 assert_kind_of Issue, issue
419 assert_kind_of Issue, issue
420 assert_equal 1, issue.attachments.size
420 assert_equal 1, issue.attachments.size
421
421
422 attachment = issue.attachments.first
422 attachment = issue.attachments.first
423 assert_equal 'paella.jpg', attachment.filename
423 assert_equal 'paella.jpg', attachment.filename
424 assert_equal 10790, attachment.filesize
424 assert_equal 10790, attachment.filesize
425 assert File.exist?(attachment.diskfile)
425 assert File.exist?(attachment.diskfile)
426 assert_equal 10790, File.size(attachment.diskfile)
426 assert_equal 10790, File.size(attachment.diskfile)
427 assert_equal 'caaf384198bcbc9563ab5c058acd73cd', attachment.digest
427 assert_equal 'caaf384198bcbc9563ab5c058acd73cd', attachment.digest
428 end
428 end
429
429
430 def test_thunderbird_with_attachment_ja
430 def test_thunderbird_with_attachment_ja
431 issue = submit_email(
431 issue = submit_email(
432 'thunderbird_with_attachment_ja.eml',
432 'thunderbird_with_attachment_ja.eml',
433 :issue => {:project => 'ecookbook'}
433 :issue => {:project => 'ecookbook'}
434 )
434 )
435 assert_kind_of Issue, issue
435 assert_kind_of Issue, issue
436 assert_equal 1, issue.attachments.size
436 assert_equal 1, issue.attachments.size
437 ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88.txt"
437 ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88.txt"
438 ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding)
438 ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding)
439 attachment = issue.attachments.first
439 attachment = issue.attachments.first
440 assert_equal ja, attachment.filename
440 assert_equal ja, attachment.filename
441 assert_equal 5, attachment.filesize
441 assert_equal 5, attachment.filesize
442 assert File.exist?(attachment.diskfile)
442 assert File.exist?(attachment.diskfile)
443 assert_equal 5, File.size(attachment.diskfile)
443 assert_equal 5, File.size(attachment.diskfile)
444 assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest
444 assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest
445 end
445 end
446
446
447 def test_gmail_with_attachment_ja
447 def test_gmail_with_attachment_ja
448 issue = submit_email(
448 issue = submit_email(
449 'gmail_with_attachment_ja.eml',
449 'gmail_with_attachment_ja.eml',
450 :issue => {:project => 'ecookbook'}
450 :issue => {:project => 'ecookbook'}
451 )
451 )
452 assert_kind_of Issue, issue
452 assert_kind_of Issue, issue
453 assert_equal 1, issue.attachments.size
453 assert_equal 1, issue.attachments.size
454 ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88.txt"
454 ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88.txt"
455 ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding)
455 ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding)
456 attachment = issue.attachments.first
456 attachment = issue.attachments.first
457 assert_equal ja, attachment.filename
457 assert_equal ja, attachment.filename
458 assert_equal 5, attachment.filesize
458 assert_equal 5, attachment.filesize
459 assert File.exist?(attachment.diskfile)
459 assert File.exist?(attachment.diskfile)
460 assert_equal 5, File.size(attachment.diskfile)
460 assert_equal 5, File.size(attachment.diskfile)
461 assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest
461 assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest
462 end
462 end
463
463
464 def test_thunderbird_with_attachment_latin1
464 def test_thunderbird_with_attachment_latin1
465 issue = submit_email(
465 issue = submit_email(
466 'thunderbird_with_attachment_iso-8859-1.eml',
466 'thunderbird_with_attachment_iso-8859-1.eml',
467 :issue => {:project => 'ecookbook'}
467 :issue => {:project => 'ecookbook'}
468 )
468 )
469 assert_kind_of Issue, issue
469 assert_kind_of Issue, issue
470 assert_equal 1, issue.attachments.size
470 assert_equal 1, issue.attachments.size
471 u = ""
471 u = ""
472 u.force_encoding('UTF-8') if u.respond_to?(:force_encoding)
472 u.force_encoding('UTF-8') if u.respond_to?(:force_encoding)
473 u1 = "\xc3\x84\xc3\xa4\xc3\x96\xc3\xb6\xc3\x9c\xc3\xbc"
473 u1 = "\xc3\x84\xc3\xa4\xc3\x96\xc3\xb6\xc3\x9c\xc3\xbc"
474 u1.force_encoding('UTF-8') if u1.respond_to?(:force_encoding)
474 u1.force_encoding('UTF-8') if u1.respond_to?(:force_encoding)
475 11.times { u << u1 }
475 11.times { u << u1 }
476 attachment = issue.attachments.first
476 attachment = issue.attachments.first
477 assert_equal "#{u}.png", attachment.filename
477 assert_equal "#{u}.png", attachment.filename
478 assert_equal 130, attachment.filesize
478 assert_equal 130, attachment.filesize
479 assert File.exist?(attachment.diskfile)
479 assert File.exist?(attachment.diskfile)
480 assert_equal 130, File.size(attachment.diskfile)
480 assert_equal 130, File.size(attachment.diskfile)
481 assert_equal '4d80e667ac37dddfe05502530f152abb', attachment.digest
481 assert_equal '4d80e667ac37dddfe05502530f152abb', attachment.digest
482 end
482 end
483
483
484 def test_gmail_with_attachment_latin1
484 def test_gmail_with_attachment_latin1
485 issue = submit_email(
485 issue = submit_email(
486 'gmail_with_attachment_iso-8859-1.eml',
486 'gmail_with_attachment_iso-8859-1.eml',
487 :issue => {:project => 'ecookbook'}
487 :issue => {:project => 'ecookbook'}
488 )
488 )
489 assert_kind_of Issue, issue
489 assert_kind_of Issue, issue
490 assert_equal 1, issue.attachments.size
490 assert_equal 1, issue.attachments.size
491 u = ""
491 u = ""
492 u.force_encoding('UTF-8') if u.respond_to?(:force_encoding)
492 u.force_encoding('UTF-8') if u.respond_to?(:force_encoding)
493 u1 = "\xc3\x84\xc3\xa4\xc3\x96\xc3\xb6\xc3\x9c\xc3\xbc"
493 u1 = "\xc3\x84\xc3\xa4\xc3\x96\xc3\xb6\xc3\x9c\xc3\xbc"
494 u1.force_encoding('UTF-8') if u1.respond_to?(:force_encoding)
494 u1.force_encoding('UTF-8') if u1.respond_to?(:force_encoding)
495 11.times { u << u1 }
495 11.times { u << u1 }
496 attachment = issue.attachments.first
496 attachment = issue.attachments.first
497 assert_equal "#{u}.txt", attachment.filename
497 assert_equal "#{u}.txt", attachment.filename
498 assert_equal 5, attachment.filesize
498 assert_equal 5, attachment.filesize
499 assert File.exist?(attachment.diskfile)
499 assert File.exist?(attachment.diskfile)
500 assert_equal 5, File.size(attachment.diskfile)
500 assert_equal 5, File.size(attachment.diskfile)
501 assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest
501 assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest
502 end
502 end
503
503
504 def test_multiple_inline_text_parts_should_be_appended_to_issue_description
504 def test_multiple_inline_text_parts_should_be_appended_to_issue_description
505 issue = submit_email('multiple_text_parts.eml', :issue => {:project => 'ecookbook'})
505 issue = submit_email('multiple_text_parts.eml', :issue => {:project => 'ecookbook'})
506 assert_include 'first', issue.description
506 assert_include 'first', issue.description
507 assert_include 'second', issue.description
507 assert_include 'second', issue.description
508 assert_include 'third', issue.description
508 assert_include 'third', issue.description
509 end
509 end
510
510
511 def test_attachment_text_part_should_be_added_as_issue_attachment
511 def test_attachment_text_part_should_be_added_as_issue_attachment
512 issue = submit_email('multiple_text_parts.eml', :issue => {:project => 'ecookbook'})
512 issue = submit_email('multiple_text_parts.eml', :issue => {:project => 'ecookbook'})
513 assert_not_include 'Plain text attachment', issue.description
513 assert_not_include 'Plain text attachment', issue.description
514 attachment = issue.attachments.detect {|a| a.filename == 'textfile.txt'}
514 attachment = issue.attachments.detect {|a| a.filename == 'textfile.txt'}
515 assert_not_nil attachment
515 assert_not_nil attachment
516 assert_include 'Plain text attachment', File.read(attachment.diskfile)
516 assert_include 'Plain text attachment', File.read(attachment.diskfile)
517 end
517 end
518
518
519 def test_add_issue_with_iso_8859_1_subject
519 def test_add_issue_with_iso_8859_1_subject
520 issue = submit_email(
520 issue = submit_email(
521 'subject_as_iso-8859-1.eml',
521 'subject_as_iso-8859-1.eml',
522 :issue => {:project => 'ecookbook'}
522 :issue => {:project => 'ecookbook'}
523 )
523 )
524 str = "Testmail from Webmail: \xc3\xa4 \xc3\xb6 \xc3\xbc..."
524 str = "Testmail from Webmail: \xc3\xa4 \xc3\xb6 \xc3\xbc..."
525 str.force_encoding('UTF-8') if str.respond_to?(:force_encoding)
525 str.force_encoding('UTF-8') if str.respond_to?(:force_encoding)
526 assert_kind_of Issue, issue
526 assert_kind_of Issue, issue
527 assert_equal str, issue.subject
527 assert_equal str, issue.subject
528 end
528 end
529
529
530 def test_add_issue_with_japanese_subject
530 def test_add_issue_with_japanese_subject
531 issue = submit_email(
531 issue = submit_email(
532 'subject_japanese_1.eml',
532 'subject_japanese_1.eml',
533 :issue => {:project => 'ecookbook'}
533 :issue => {:project => 'ecookbook'}
534 )
534 )
535 assert_kind_of Issue, issue
535 assert_kind_of Issue, issue
536 ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88"
536 ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88"
537 ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding)
537 ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding)
538 assert_equal ja, issue.subject
538 assert_equal ja, issue.subject
539 end
539 end
540
540
541 def test_add_issue_with_korean_body
542 # Make sure mail bodies with a charset unknown to Ruby
543 # but known to the Mail gem 2.5.4 are handled correctly
544 kr = "\xEA\xB3\xA0\xEB\xA7\x99\xEC\x8A\xB5\xEB\x8B\x88\xEB\x8B\xA4."
545 if !kr.respond_to?(:force_encoding)
546 puts "\nOn Ruby 1.8, skip Korean encoding mail body test"
547 else
548 kr.force_encoding('UTF-8')
549 issue = submit_email(
550 'body_ks_c_5601-1987.eml',
551 :issue => {:project => 'ecookbook'}
552 )
553 assert_kind_of Issue, issue
554 assert_equal kr, issue.description
555 end
556 end
557
541 def test_add_issue_with_no_subject_header
558 def test_add_issue_with_no_subject_header
542 issue = submit_email(
559 issue = submit_email(
543 'no_subject_header.eml',
560 'no_subject_header.eml',
544 :issue => {:project => 'ecookbook'}
561 :issue => {:project => 'ecookbook'}
545 )
562 )
546 assert_kind_of Issue, issue
563 assert_kind_of Issue, issue
547 assert_equal '(no subject)', issue.subject
564 assert_equal '(no subject)', issue.subject
548 end
565 end
549
566
550 def test_add_issue_with_mixed_japanese_subject
567 def test_add_issue_with_mixed_japanese_subject
551 issue = submit_email(
568 issue = submit_email(
552 'subject_japanese_2.eml',
569 'subject_japanese_2.eml',
553 :issue => {:project => 'ecookbook'}
570 :issue => {:project => 'ecookbook'}
554 )
571 )
555 assert_kind_of Issue, issue
572 assert_kind_of Issue, issue
556 ja = "Re: \xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88"
573 ja = "Re: \xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88"
557 ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding)
574 ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding)
558 assert_equal ja, issue.subject
575 assert_equal ja, issue.subject
559 end
576 end
560
577
561 def test_should_ignore_emails_from_locked_users
578 def test_should_ignore_emails_from_locked_users
562 User.find(2).lock!
579 User.find(2).lock!
563
580
564 MailHandler.any_instance.expects(:dispatch).never
581 MailHandler.any_instance.expects(:dispatch).never
565 assert_no_difference 'Issue.count' do
582 assert_no_difference 'Issue.count' do
566 assert_equal false, submit_email('ticket_on_given_project.eml')
583 assert_equal false, submit_email('ticket_on_given_project.eml')
567 end
584 end
568 end
585 end
569
586
570 def test_should_ignore_emails_from_emission_address
587 def test_should_ignore_emails_from_emission_address
571 Role.anonymous.add_permission!(:add_issues)
588 Role.anonymous.add_permission!(:add_issues)
572 assert_no_difference 'User.count' do
589 assert_no_difference 'User.count' do
573 assert_equal false,
590 assert_equal false,
574 submit_email(
591 submit_email(
575 'ticket_from_emission_address.eml',
592 'ticket_from_emission_address.eml',
576 :issue => {:project => 'ecookbook'},
593 :issue => {:project => 'ecookbook'},
577 :unknown_user => 'create'
594 :unknown_user => 'create'
578 )
595 )
579 end
596 end
580 end
597 end
581
598
582 def test_should_ignore_auto_replied_emails
599 def test_should_ignore_auto_replied_emails
583 MailHandler.any_instance.expects(:dispatch).never
600 MailHandler.any_instance.expects(:dispatch).never
584 [
601 [
585 "X-Auto-Response-Suppress: OOF",
602 "X-Auto-Response-Suppress: OOF",
586 "Auto-Submitted: auto-replied",
603 "Auto-Submitted: auto-replied",
587 "Auto-Submitted: Auto-Replied",
604 "Auto-Submitted: Auto-Replied",
588 "Auto-Submitted: auto-generated"
605 "Auto-Submitted: auto-generated"
589 ].each do |header|
606 ].each do |header|
590 raw = IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml'))
607 raw = IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml'))
591 raw = header + "\n" + raw
608 raw = header + "\n" + raw
592
609
593 assert_no_difference 'Issue.count' do
610 assert_no_difference 'Issue.count' do
594 assert_equal false, MailHandler.receive(raw), "email with #{header} header was not ignored"
611 assert_equal false, MailHandler.receive(raw), "email with #{header} header was not ignored"
595 end
612 end
596 end
613 end
597 end
614 end
598
615
599 def test_add_issue_should_send_email_notification
616 def test_add_issue_should_send_email_notification
600 Setting.notified_events = ['issue_added']
617 Setting.notified_events = ['issue_added']
601 ActionMailer::Base.deliveries.clear
618 ActionMailer::Base.deliveries.clear
602 # This email contains: 'Project: onlinestore'
619 # This email contains: 'Project: onlinestore'
603 issue = submit_email('ticket_on_given_project.eml')
620 issue = submit_email('ticket_on_given_project.eml')
604 assert issue.is_a?(Issue)
621 assert issue.is_a?(Issue)
605 assert_equal 1, ActionMailer::Base.deliveries.size
622 assert_equal 1, ActionMailer::Base.deliveries.size
606 end
623 end
607
624
608 def test_update_issue
625 def test_update_issue
609 journal = submit_email('ticket_reply.eml')
626 journal = submit_email('ticket_reply.eml')
610 assert journal.is_a?(Journal)
627 assert journal.is_a?(Journal)
611 assert_equal User.find_by_login('jsmith'), journal.user
628 assert_equal User.find_by_login('jsmith'), journal.user
612 assert_equal Issue.find(2), journal.journalized
629 assert_equal Issue.find(2), journal.journalized
613 assert_match /This is reply/, journal.notes
630 assert_match /This is reply/, journal.notes
614 assert_equal false, journal.private_notes
631 assert_equal false, journal.private_notes
615 assert_equal 'Feature request', journal.issue.tracker.name
632 assert_equal 'Feature request', journal.issue.tracker.name
616 end
633 end
617
634
618 def test_update_issue_with_attribute_changes
635 def test_update_issue_with_attribute_changes
619 # This email contains: 'Status: Resolved'
636 # This email contains: 'Status: Resolved'
620 journal = submit_email('ticket_reply_with_status.eml')
637 journal = submit_email('ticket_reply_with_status.eml')
621 assert journal.is_a?(Journal)
638 assert journal.is_a?(Journal)
622 issue = Issue.find(journal.issue.id)
639 issue = Issue.find(journal.issue.id)
623 assert_equal User.find_by_login('jsmith'), journal.user
640 assert_equal User.find_by_login('jsmith'), journal.user
624 assert_equal Issue.find(2), journal.journalized
641 assert_equal Issue.find(2), journal.journalized
625 assert_match /This is reply/, journal.notes
642 assert_match /This is reply/, journal.notes
626 assert_equal 'Feature request', journal.issue.tracker.name
643 assert_equal 'Feature request', journal.issue.tracker.name
627 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
644 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
628 assert_equal '2010-01-01', issue.start_date.to_s
645 assert_equal '2010-01-01', issue.start_date.to_s
629 assert_equal '2010-12-31', issue.due_date.to_s
646 assert_equal '2010-12-31', issue.due_date.to_s
630 assert_equal User.find_by_login('jsmith'), issue.assigned_to
647 assert_equal User.find_by_login('jsmith'), issue.assigned_to
631 assert_equal "52.6", issue.custom_value_for(CustomField.find_by_name('Float field')).value
648 assert_equal "52.6", issue.custom_value_for(CustomField.find_by_name('Float field')).value
632 # keywords should be removed from the email body
649 # keywords should be removed from the email body
633 assert !journal.notes.match(/^Status:/i)
650 assert !journal.notes.match(/^Status:/i)
634 assert !journal.notes.match(/^Start Date:/i)
651 assert !journal.notes.match(/^Start Date:/i)
635 end
652 end
636
653
637 def test_update_issue_with_attachment
654 def test_update_issue_with_attachment
638 assert_difference 'Journal.count' do
655 assert_difference 'Journal.count' do
639 assert_difference 'JournalDetail.count' do
656 assert_difference 'JournalDetail.count' do
640 assert_difference 'Attachment.count' do
657 assert_difference 'Attachment.count' do
641 assert_no_difference 'Issue.count' do
658 assert_no_difference 'Issue.count' do
642 journal = submit_email('ticket_with_attachment.eml') do |raw|
659 journal = submit_email('ticket_with_attachment.eml') do |raw|
643 raw.gsub! /^Subject: .*$/, 'Subject: Re: [Cookbook - Feature #2] (New) Add ingredients categories'
660 raw.gsub! /^Subject: .*$/, 'Subject: Re: [Cookbook - Feature #2] (New) Add ingredients categories'
644 end
661 end
645 end
662 end
646 end
663 end
647 end
664 end
648 end
665 end
649 journal = Journal.first(:order => 'id DESC')
666 journal = Journal.first(:order => 'id DESC')
650 assert_equal Issue.find(2), journal.journalized
667 assert_equal Issue.find(2), journal.journalized
651 assert_equal 1, journal.details.size
668 assert_equal 1, journal.details.size
652
669
653 detail = journal.details.first
670 detail = journal.details.first
654 assert_equal 'attachment', detail.property
671 assert_equal 'attachment', detail.property
655 assert_equal 'Paella.jpg', detail.value
672 assert_equal 'Paella.jpg', detail.value
656 end
673 end
657
674
658 def test_update_issue_should_send_email_notification
675 def test_update_issue_should_send_email_notification
659 ActionMailer::Base.deliveries.clear
676 ActionMailer::Base.deliveries.clear
660 journal = submit_email('ticket_reply.eml')
677 journal = submit_email('ticket_reply.eml')
661 assert journal.is_a?(Journal)
678 assert journal.is_a?(Journal)
662 assert_equal 1, ActionMailer::Base.deliveries.size
679 assert_equal 1, ActionMailer::Base.deliveries.size
663 end
680 end
664
681
665 def test_update_issue_should_not_set_defaults
682 def test_update_issue_should_not_set_defaults
666 journal = submit_email(
683 journal = submit_email(
667 'ticket_reply.eml',
684 'ticket_reply.eml',
668 :issue => {:tracker => 'Support request', :priority => 'High'}
685 :issue => {:tracker => 'Support request', :priority => 'High'}
669 )
686 )
670 assert journal.is_a?(Journal)
687 assert journal.is_a?(Journal)
671 assert_match /This is reply/, journal.notes
688 assert_match /This is reply/, journal.notes
672 assert_equal 'Feature request', journal.issue.tracker.name
689 assert_equal 'Feature request', journal.issue.tracker.name
673 assert_equal 'Normal', journal.issue.priority.name
690 assert_equal 'Normal', journal.issue.priority.name
674 end
691 end
675
692
676 def test_replying_to_a_private_note_should_add_reply_as_private
693 def test_replying_to_a_private_note_should_add_reply_as_private
677 private_journal = Journal.create!(:notes => 'Private notes', :journalized => Issue.find(1), :private_notes => true, :user_id => 2)
694 private_journal = Journal.create!(:notes => 'Private notes', :journalized => Issue.find(1), :private_notes => true, :user_id => 2)
678
695
679 assert_difference 'Journal.count' do
696 assert_difference 'Journal.count' do
680 journal = submit_email('ticket_reply.eml') do |email|
697 journal = submit_email('ticket_reply.eml') do |email|
681 email.sub! %r{^In-Reply-To:.*$}, "In-Reply-To: <redmine.journal-#{private_journal.id}.20060719210421@osiris>"
698 email.sub! %r{^In-Reply-To:.*$}, "In-Reply-To: <redmine.journal-#{private_journal.id}.20060719210421@osiris>"
682 end
699 end
683
700
684 assert_kind_of Journal, journal
701 assert_kind_of Journal, journal
685 assert_match /This is reply/, journal.notes
702 assert_match /This is reply/, journal.notes
686 assert_equal true, journal.private_notes
703 assert_equal true, journal.private_notes
687 end
704 end
688 end
705 end
689
706
690 def test_reply_to_a_message
707 def test_reply_to_a_message
691 m = submit_email('message_reply.eml')
708 m = submit_email('message_reply.eml')
692 assert m.is_a?(Message)
709 assert m.is_a?(Message)
693 assert !m.new_record?
710 assert !m.new_record?
694 m.reload
711 m.reload
695 assert_equal 'Reply via email', m.subject
712 assert_equal 'Reply via email', m.subject
696 # The email replies to message #2 which is part of the thread of message #1
713 # The email replies to message #2 which is part of the thread of message #1
697 assert_equal Message.find(1), m.parent
714 assert_equal Message.find(1), m.parent
698 end
715 end
699
716
700 def test_reply_to_a_message_by_subject
717 def test_reply_to_a_message_by_subject
701 m = submit_email('message_reply_by_subject.eml')
718 m = submit_email('message_reply_by_subject.eml')
702 assert m.is_a?(Message)
719 assert m.is_a?(Message)
703 assert !m.new_record?
720 assert !m.new_record?
704 m.reload
721 m.reload
705 assert_equal 'Reply to the first post', m.subject
722 assert_equal 'Reply to the first post', m.subject
706 assert_equal Message.find(1), m.parent
723 assert_equal Message.find(1), m.parent
707 end
724 end
708
725
709 def test_should_strip_tags_of_html_only_emails
726 def test_should_strip_tags_of_html_only_emails
710 issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'})
727 issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'})
711 assert issue.is_a?(Issue)
728 assert issue.is_a?(Issue)
712 assert !issue.new_record?
729 assert !issue.new_record?
713 issue.reload
730 issue.reload
714 assert_equal 'HTML email', issue.subject
731 assert_equal 'HTML email', issue.subject
715 assert_equal 'This is a html-only email.', issue.description
732 assert_equal 'This is a html-only email.', issue.description
716 end
733 end
717
734
718 test "truncate emails with no setting should add the entire email into the issue" do
735 test "truncate emails with no setting should add the entire email into the issue" do
719 with_settings :mail_handler_body_delimiters => '' do
736 with_settings :mail_handler_body_delimiters => '' do
720 issue = submit_email('ticket_on_given_project.eml')
737 issue = submit_email('ticket_on_given_project.eml')
721 assert_issue_created(issue)
738 assert_issue_created(issue)
722 assert issue.description.include?('---')
739 assert issue.description.include?('---')
723 assert issue.description.include?('This paragraph is after the delimiter')
740 assert issue.description.include?('This paragraph is after the delimiter')
724 end
741 end
725 end
742 end
726
743
727 test "truncate emails with a single string should truncate the email at the delimiter for the issue" do
744 test "truncate emails with a single string should truncate the email at the delimiter for the issue" do
728 with_settings :mail_handler_body_delimiters => '---' do
745 with_settings :mail_handler_body_delimiters => '---' do
729 issue = submit_email('ticket_on_given_project.eml')
746 issue = submit_email('ticket_on_given_project.eml')
730 assert_issue_created(issue)
747 assert_issue_created(issue)
731 assert issue.description.include?('This paragraph is before delimiters')
748 assert issue.description.include?('This paragraph is before delimiters')
732 assert issue.description.include?('--- This line starts with a delimiter')
749 assert issue.description.include?('--- This line starts with a delimiter')
733 assert !issue.description.match(/^---$/)
750 assert !issue.description.match(/^---$/)
734 assert !issue.description.include?('This paragraph is after the delimiter')
751 assert !issue.description.include?('This paragraph is after the delimiter')
735 end
752 end
736 end
753 end
737
754
738 test "truncate emails with a single quoted reply should truncate the email at the delimiter with the quoted reply symbols (>)" do
755 test "truncate emails with a single quoted reply should truncate the email at the delimiter with the quoted reply symbols (>)" do
739 with_settings :mail_handler_body_delimiters => '--- Reply above. Do not remove this line. ---' do
756 with_settings :mail_handler_body_delimiters => '--- Reply above. Do not remove this line. ---' do
740 journal = submit_email('issue_update_with_quoted_reply_above.eml')
757 journal = submit_email('issue_update_with_quoted_reply_above.eml')
741 assert journal.is_a?(Journal)
758 assert journal.is_a?(Journal)
742 assert journal.notes.include?('An update to the issue by the sender.')
759 assert journal.notes.include?('An update to the issue by the sender.')
743 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
760 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
744 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
761 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
745 end
762 end
746 end
763 end
747
764
748 test "truncate emails with multiple quoted replies should truncate the email at the delimiter with the quoted reply symbols (>)" do
765 test "truncate emails with multiple quoted replies should truncate the email at the delimiter with the quoted reply symbols (>)" do
749 with_settings :mail_handler_body_delimiters => '--- Reply above. Do not remove this line. ---' do
766 with_settings :mail_handler_body_delimiters => '--- Reply above. Do not remove this line. ---' do
750 journal = submit_email('issue_update_with_multiple_quoted_reply_above.eml')
767 journal = submit_email('issue_update_with_multiple_quoted_reply_above.eml')
751 assert journal.is_a?(Journal)
768 assert journal.is_a?(Journal)
752 assert journal.notes.include?('An update to the issue by the sender.')
769 assert journal.notes.include?('An update to the issue by the sender.')
753 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
770 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
754 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
771 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
755 end
772 end
756 end
773 end
757
774
758 test "truncate emails with multiple strings should truncate the email at the first delimiter found (BREAK)" do
775 test "truncate emails with multiple strings should truncate the email at the first delimiter found (BREAK)" do
759 with_settings :mail_handler_body_delimiters => "---\nBREAK" do
776 with_settings :mail_handler_body_delimiters => "---\nBREAK" do
760 issue = submit_email('ticket_on_given_project.eml')
777 issue = submit_email('ticket_on_given_project.eml')
761 assert_issue_created(issue)
778 assert_issue_created(issue)
762 assert issue.description.include?('This paragraph is before delimiters')
779 assert issue.description.include?('This paragraph is before delimiters')
763 assert !issue.description.include?('BREAK')
780 assert !issue.description.include?('BREAK')
764 assert !issue.description.include?('This paragraph is between delimiters')
781 assert !issue.description.include?('This paragraph is between delimiters')
765 assert !issue.description.match(/^---$/)
782 assert !issue.description.match(/^---$/)
766 assert !issue.description.include?('This paragraph is after the delimiter')
783 assert !issue.description.include?('This paragraph is after the delimiter')
767 end
784 end
768 end
785 end
769
786
770 def test_attachments_that_match_mail_handler_excluded_filenames_should_be_ignored
787 def test_attachments_that_match_mail_handler_excluded_filenames_should_be_ignored
771 with_settings :mail_handler_excluded_filenames => '*.vcf, *.jpg' do
788 with_settings :mail_handler_excluded_filenames => '*.vcf, *.jpg' do
772 issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
789 issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
773 assert issue.is_a?(Issue)
790 assert issue.is_a?(Issue)
774 assert !issue.new_record?
791 assert !issue.new_record?
775 assert_equal 0, issue.reload.attachments.size
792 assert_equal 0, issue.reload.attachments.size
776 end
793 end
777 end
794 end
778
795
779 def test_attachments_that_do_not_match_mail_handler_excluded_filenames_should_be_attached
796 def test_attachments_that_do_not_match_mail_handler_excluded_filenames_should_be_attached
780 with_settings :mail_handler_excluded_filenames => '*.vcf, *.gif' do
797 with_settings :mail_handler_excluded_filenames => '*.vcf, *.gif' do
781 issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
798 issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
782 assert issue.is_a?(Issue)
799 assert issue.is_a?(Issue)
783 assert !issue.new_record?
800 assert !issue.new_record?
784 assert_equal 1, issue.reload.attachments.size
801 assert_equal 1, issue.reload.attachments.size
785 end
802 end
786 end
803 end
787
804
788 def test_email_with_long_subject_line
805 def test_email_with_long_subject_line
789 issue = submit_email('ticket_with_long_subject.eml')
806 issue = submit_email('ticket_with_long_subject.eml')
790 assert issue.is_a?(Issue)
807 assert issue.is_a?(Issue)
791 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]
808 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]
792 end
809 end
793
810
794 def test_new_user_from_attributes_should_return_valid_user
811 def test_new_user_from_attributes_should_return_valid_user
795 to_test = {
812 to_test = {
796 # [address, name] => [login, firstname, lastname]
813 # [address, name] => [login, firstname, lastname]
797 ['jsmith@example.net', nil] => ['jsmith@example.net', 'jsmith', '-'],
814 ['jsmith@example.net', nil] => ['jsmith@example.net', 'jsmith', '-'],
798 ['jsmith@example.net', 'John'] => ['jsmith@example.net', 'John', '-'],
815 ['jsmith@example.net', 'John'] => ['jsmith@example.net', 'John', '-'],
799 ['jsmith@example.net', 'John Smith'] => ['jsmith@example.net', 'John', 'Smith'],
816 ['jsmith@example.net', 'John Smith'] => ['jsmith@example.net', 'John', 'Smith'],
800 ['jsmith@example.net', 'John Paul Smith'] => ['jsmith@example.net', 'John', 'Paul Smith'],
817 ['jsmith@example.net', 'John Paul Smith'] => ['jsmith@example.net', 'John', 'Paul Smith'],
801 ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsTheMaximumLength Smith'] => ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsT', 'Smith'],
818 ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsTheMaximumLength Smith'] => ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsT', 'Smith'],
802 ['jsmith@example.net', 'John AVeryLongLastnameThatExceedsTheMaximumLength'] => ['jsmith@example.net', 'John', 'AVeryLongLastnameThatExceedsTh']
819 ['jsmith@example.net', 'John AVeryLongLastnameThatExceedsTheMaximumLength'] => ['jsmith@example.net', 'John', 'AVeryLongLastnameThatExceedsTh']
803 }
820 }
804
821
805 to_test.each do |attrs, expected|
822 to_test.each do |attrs, expected|
806 user = MailHandler.new_user_from_attributes(attrs.first, attrs.last)
823 user = MailHandler.new_user_from_attributes(attrs.first, attrs.last)
807
824
808 assert user.valid?, user.errors.full_messages.to_s
825 assert user.valid?, user.errors.full_messages.to_s
809 assert_equal attrs.first, user.mail
826 assert_equal attrs.first, user.mail
810 assert_equal expected[0], user.login
827 assert_equal expected[0], user.login
811 assert_equal expected[1], user.firstname
828 assert_equal expected[1], user.firstname
812 assert_equal expected[2], user.lastname
829 assert_equal expected[2], user.lastname
813 assert_equal 'only_my_events', user.mail_notification
830 assert_equal 'only_my_events', user.mail_notification
814 end
831 end
815 end
832 end
816
833
817 def test_new_user_from_attributes_should_use_default_login_if_invalid
834 def test_new_user_from_attributes_should_use_default_login_if_invalid
818 user = MailHandler.new_user_from_attributes('foo+bar@example.net')
835 user = MailHandler.new_user_from_attributes('foo+bar@example.net')
819 assert user.valid?
836 assert user.valid?
820 assert user.login =~ /^user[a-f0-9]+$/
837 assert user.login =~ /^user[a-f0-9]+$/
821 assert_equal 'foo+bar@example.net', user.mail
838 assert_equal 'foo+bar@example.net', user.mail
822 end
839 end
823
840
824 def test_new_user_with_utf8_encoded_fullname_should_be_decoded
841 def test_new_user_with_utf8_encoded_fullname_should_be_decoded
825 assert_difference 'User.count' do
842 assert_difference 'User.count' do
826 issue = submit_email(
843 issue = submit_email(
827 'fullname_of_sender_as_utf8_encoded.eml',
844 'fullname_of_sender_as_utf8_encoded.eml',
828 :issue => {:project => 'ecookbook'},
845 :issue => {:project => 'ecookbook'},
829 :unknown_user => 'create'
846 :unknown_user => 'create'
830 )
847 )
831 end
848 end
832
849
833 user = User.first(:order => 'id DESC')
850 user = User.first(:order => 'id DESC')
834 assert_equal "foo@example.org", user.mail
851 assert_equal "foo@example.org", user.mail
835 str1 = "\xc3\x84\xc3\xa4"
852 str1 = "\xc3\x84\xc3\xa4"
836 str2 = "\xc3\x96\xc3\xb6"
853 str2 = "\xc3\x96\xc3\xb6"
837 str1.force_encoding('UTF-8') if str1.respond_to?(:force_encoding)
854 str1.force_encoding('UTF-8') if str1.respond_to?(:force_encoding)
838 str2.force_encoding('UTF-8') if str2.respond_to?(:force_encoding)
855 str2.force_encoding('UTF-8') if str2.respond_to?(:force_encoding)
839 assert_equal str1, user.firstname
856 assert_equal str1, user.firstname
840 assert_equal str2, user.lastname
857 assert_equal str2, user.lastname
841 end
858 end
842
859
843 def test_extract_options_from_env_should_return_options
860 def test_extract_options_from_env_should_return_options
844 options = MailHandler.extract_options_from_env({
861 options = MailHandler.extract_options_from_env({
845 'tracker' => 'defect',
862 'tracker' => 'defect',
846 'project' => 'foo',
863 'project' => 'foo',
847 'unknown_user' => 'create'
864 'unknown_user' => 'create'
848 })
865 })
849
866
850 assert_equal({
867 assert_equal({
851 :issue => {:tracker => 'defect', :project => 'foo'},
868 :issue => {:tracker => 'defect', :project => 'foo'},
852 :unknown_user => 'create'
869 :unknown_user => 'create'
853 }, options)
870 }, options)
854 end
871 end
855
872
856 private
873 private
857
874
858 def submit_email(filename, options={})
875 def submit_email(filename, options={})
859 raw = IO.read(File.join(FIXTURES_PATH, filename))
876 raw = IO.read(File.join(FIXTURES_PATH, filename))
860 yield raw if block_given?
877 yield raw if block_given?
861 MailHandler.receive(raw, options)
878 MailHandler.receive(raw, options)
862 end
879 end
863
880
864 def assert_issue_created(issue)
881 def assert_issue_created(issue)
865 assert issue.is_a?(Issue)
882 assert issue.is_a?(Issue)
866 assert !issue.new_record?
883 assert !issue.new_record?
867 issue.reload
884 issue.reload
868 end
885 end
869 end
886 end
General Comments 0
You need to be logged in to leave comments. Login now