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