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