##// END OF EJS Templates
Fixed: CLI-supplied defaults should not be applied when replying to an issue (#7195)....
Jean-Philippe Lang -
r4462:03d4ecbbff99
parent child
Show More
@@ -1,358 +1,361
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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 @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip) if @@handler_options[:allow_override].is_a?(String)
32 @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip) if @@handler_options[:allow_override].is_a?(String)
33 @@handler_options[:allow_override] ||= []
33 @@handler_options[:allow_override] ||= []
34 # Project needs to be overridable if not specified
34 # Project needs to be overridable if not specified
35 @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
35 @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
36 # Status overridable by default
36 # Status overridable by default
37 @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
37 @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
38
38
39 @@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1' ? true : false)
39 @@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1' ? true : false)
40 super email
40 super email
41 end
41 end
42
42
43 # Processes incoming emails
43 # Processes incoming emails
44 # Returns the created object (eg. an issue, a message) or false
44 # Returns the created object (eg. an issue, a message) or false
45 def receive(email)
45 def receive(email)
46 @email = email
46 @email = email
47 sender_email = email.from.to_a.first.to_s.strip
47 sender_email = email.from.to_a.first.to_s.strip
48 # Ignore emails received from the application emission address to avoid hell cycles
48 # Ignore emails received from the application emission address to avoid hell cycles
49 if sender_email.downcase == Setting.mail_from.to_s.strip.downcase
49 if sender_email.downcase == Setting.mail_from.to_s.strip.downcase
50 logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]" if logger && logger.info
50 logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]" if logger && logger.info
51 return false
51 return false
52 end
52 end
53 @user = User.find_by_mail(sender_email) if sender_email.present?
53 @user = User.find_by_mail(sender_email) if sender_email.present?
54 if @user && !@user.active?
54 if @user && !@user.active?
55 logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]" if logger && logger.info
55 logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]" if logger && logger.info
56 return false
56 return false
57 end
57 end
58 if @user.nil?
58 if @user.nil?
59 # Email was submitted by an unknown user
59 # Email was submitted by an unknown user
60 case @@handler_options[:unknown_user]
60 case @@handler_options[:unknown_user]
61 when 'accept'
61 when 'accept'
62 @user = User.anonymous
62 @user = User.anonymous
63 when 'create'
63 when 'create'
64 @user = MailHandler.create_user_from_email(email)
64 @user = MailHandler.create_user_from_email(email)
65 if @user
65 if @user
66 logger.info "MailHandler: [#{@user.login}] account created" if logger && logger.info
66 logger.info "MailHandler: [#{@user.login}] account created" if logger && logger.info
67 Mailer.deliver_account_information(@user, @user.password)
67 Mailer.deliver_account_information(@user, @user.password)
68 else
68 else
69 logger.error "MailHandler: could not create account for [#{sender_email}]" if logger && logger.error
69 logger.error "MailHandler: could not create account for [#{sender_email}]" if logger && logger.error
70 return false
70 return false
71 end
71 end
72 else
72 else
73 # Default behaviour, emails from unknown users are ignored
73 # Default behaviour, emails from unknown users are ignored
74 logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]" if logger && logger.info
74 logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]" if logger && logger.info
75 return false
75 return false
76 end
76 end
77 end
77 end
78 User.current = @user
78 User.current = @user
79 dispatch
79 dispatch
80 end
80 end
81
81
82 private
82 private
83
83
84 MESSAGE_ID_RE = %r{^<redmine\.([a-z0-9_]+)\-(\d+)\.\d+@}
84 MESSAGE_ID_RE = %r{^<redmine\.([a-z0-9_]+)\-(\d+)\.\d+@}
85 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]*#(\d+)\]}
85 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]*#(\d+)\]}
86 MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
86 MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
87
87
88 def dispatch
88 def dispatch
89 headers = [email.in_reply_to, email.references].flatten.compact
89 headers = [email.in_reply_to, email.references].flatten.compact
90 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
90 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
91 klass, object_id = $1, $2.to_i
91 klass, object_id = $1, $2.to_i
92 method_name = "receive_#{klass}_reply"
92 method_name = "receive_#{klass}_reply"
93 if self.class.private_instance_methods.collect(&:to_s).include?(method_name)
93 if self.class.private_instance_methods.collect(&:to_s).include?(method_name)
94 send method_name, object_id
94 send method_name, object_id
95 else
95 else
96 # ignoring it
96 # ignoring it
97 end
97 end
98 elsif m = email.subject.match(ISSUE_REPLY_SUBJECT_RE)
98 elsif m = email.subject.match(ISSUE_REPLY_SUBJECT_RE)
99 receive_issue_reply(m[1].to_i)
99 receive_issue_reply(m[1].to_i)
100 elsif m = email.subject.match(MESSAGE_REPLY_SUBJECT_RE)
100 elsif m = email.subject.match(MESSAGE_REPLY_SUBJECT_RE)
101 receive_message_reply(m[1].to_i)
101 receive_message_reply(m[1].to_i)
102 else
102 else
103 receive_issue
103 receive_issue
104 end
104 end
105 rescue ActiveRecord::RecordInvalid => e
105 rescue ActiveRecord::RecordInvalid => e
106 # TODO: send a email to the user
106 # TODO: send a email to the user
107 logger.error e.message if logger
107 logger.error e.message if logger
108 false
108 false
109 rescue MissingInformation => e
109 rescue MissingInformation => e
110 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
110 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
111 false
111 false
112 rescue UnauthorizedAction => e
112 rescue UnauthorizedAction => e
113 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
113 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
114 false
114 false
115 end
115 end
116
116
117 # Creates a new issue
117 # Creates a new issue
118 def receive_issue
118 def receive_issue
119 project = target_project
119 project = target_project
120 # check permission
120 # check permission
121 unless @@handler_options[:no_permission_check]
121 unless @@handler_options[:no_permission_check]
122 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
122 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
123 end
123 end
124
124
125 issue = Issue.new(:author => user, :project => project)
125 issue = Issue.new(:author => user, :project => project)
126 issue.safe_attributes = issue_attributes_from_keywords(issue)
126 issue.safe_attributes = issue_attributes_from_keywords(issue)
127 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
127 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
128 issue.subject = email.subject.to_s.chomp[0,255]
128 issue.subject = email.subject.to_s.chomp[0,255]
129 if issue.subject.blank?
129 if issue.subject.blank?
130 issue.subject = '(no subject)'
130 issue.subject = '(no subject)'
131 end
131 end
132 issue.description = cleaned_up_text_body
132 issue.description = cleaned_up_text_body
133
133
134 # add To and Cc as watchers before saving so the watchers can reply to Redmine
134 # add To and Cc as watchers before saving so the watchers can reply to Redmine
135 add_watchers(issue)
135 add_watchers(issue)
136 issue.save!
136 issue.save!
137 add_attachments(issue)
137 add_attachments(issue)
138 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
138 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
139 issue
139 issue
140 end
140 end
141
141
142 # Adds a note to an existing issue
142 # Adds a note to an existing issue
143 def receive_issue_reply(issue_id)
143 def receive_issue_reply(issue_id)
144 issue = Issue.find_by_id(issue_id)
144 issue = Issue.find_by_id(issue_id)
145 return unless issue
145 return unless issue
146 # check permission
146 # check permission
147 unless @@handler_options[:no_permission_check]
147 unless @@handler_options[:no_permission_check]
148 raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project)
148 raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project)
149 end
149 end
150
150
151 # ignore CLI-supplied defaults for new issues
152 @@handler_options[:issue].clear
153
151 journal = issue.init_journal(user, cleaned_up_text_body)
154 journal = issue.init_journal(user, cleaned_up_text_body)
152 issue.safe_attributes = issue_attributes_from_keywords(issue)
155 issue.safe_attributes = issue_attributes_from_keywords(issue)
153 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
156 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
154 add_attachments(issue)
157 add_attachments(issue)
155 issue.save!
158 issue.save!
156 logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info
159 logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info
157 journal
160 journal
158 end
161 end
159
162
160 # Reply will be added to the issue
163 # Reply will be added to the issue
161 def receive_journal_reply(journal_id)
164 def receive_journal_reply(journal_id)
162 journal = Journal.find_by_id(journal_id)
165 journal = Journal.find_by_id(journal_id)
163 if journal && journal.journalized_type == 'Issue'
166 if journal && journal.journalized_type == 'Issue'
164 receive_issue_reply(journal.journalized_id)
167 receive_issue_reply(journal.journalized_id)
165 end
168 end
166 end
169 end
167
170
168 # Receives a reply to a forum message
171 # Receives a reply to a forum message
169 def receive_message_reply(message_id)
172 def receive_message_reply(message_id)
170 message = Message.find_by_id(message_id)
173 message = Message.find_by_id(message_id)
171 if message
174 if message
172 message = message.root
175 message = message.root
173
176
174 unless @@handler_options[:no_permission_check]
177 unless @@handler_options[:no_permission_check]
175 raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
178 raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
176 end
179 end
177
180
178 if !message.locked?
181 if !message.locked?
179 reply = Message.new(:subject => email.subject.gsub(%r{^.*msg\d+\]}, '').strip,
182 reply = Message.new(:subject => email.subject.gsub(%r{^.*msg\d+\]}, '').strip,
180 :content => cleaned_up_text_body)
183 :content => cleaned_up_text_body)
181 reply.author = user
184 reply.author = user
182 reply.board = message.board
185 reply.board = message.board
183 message.children << reply
186 message.children << reply
184 add_attachments(reply)
187 add_attachments(reply)
185 reply
188 reply
186 else
189 else
187 logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic" if logger && logger.info
190 logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic" if logger && logger.info
188 end
191 end
189 end
192 end
190 end
193 end
191
194
192 def add_attachments(obj)
195 def add_attachments(obj)
193 if email.has_attachments?
196 if email.has_attachments?
194 email.attachments.each do |attachment|
197 email.attachments.each do |attachment|
195 Attachment.create(:container => obj,
198 Attachment.create(:container => obj,
196 :file => attachment,
199 :file => attachment,
197 :author => user,
200 :author => user,
198 :content_type => attachment.content_type)
201 :content_type => attachment.content_type)
199 end
202 end
200 end
203 end
201 end
204 end
202
205
203 # Adds To and Cc as watchers of the given object if the sender has the
206 # Adds To and Cc as watchers of the given object if the sender has the
204 # appropriate permission
207 # appropriate permission
205 def add_watchers(obj)
208 def add_watchers(obj)
206 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
209 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
207 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
210 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
208 unless addresses.empty?
211 unless addresses.empty?
209 watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
212 watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
210 watchers.each {|w| obj.add_watcher(w)}
213 watchers.each {|w| obj.add_watcher(w)}
211 end
214 end
212 end
215 end
213 end
216 end
214
217
215 def get_keyword(attr, options={})
218 def get_keyword(attr, options={})
216 @keywords ||= {}
219 @keywords ||= {}
217 if @keywords.has_key?(attr)
220 if @keywords.has_key?(attr)
218 @keywords[attr]
221 @keywords[attr]
219 else
222 else
220 @keywords[attr] = begin
223 @keywords[attr] = begin
221 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && (v = extract_keyword!(plain_text_body, attr, options[:format]))
224 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && (v = extract_keyword!(plain_text_body, attr, options[:format]))
222 v
225 v
223 elsif !@@handler_options[:issue][attr].blank?
226 elsif !@@handler_options[:issue][attr].blank?
224 @@handler_options[:issue][attr]
227 @@handler_options[:issue][attr]
225 end
228 end
226 end
229 end
227 end
230 end
228 end
231 end
229
232
230 # Destructively extracts the value for +attr+ in +text+
233 # Destructively extracts the value for +attr+ in +text+
231 # Returns nil if no matching keyword found
234 # Returns nil if no matching keyword found
232 def extract_keyword!(text, attr, format=nil)
235 def extract_keyword!(text, attr, format=nil)
233 keys = [attr.to_s.humanize]
236 keys = [attr.to_s.humanize]
234 if attr.is_a?(Symbol)
237 if attr.is_a?(Symbol)
235 keys << l("field_#{attr}", :default => '', :locale => user.language) if user
238 keys << l("field_#{attr}", :default => '', :locale => user.language) if user
236 keys << l("field_#{attr}", :default => '', :locale => Setting.default_language)
239 keys << l("field_#{attr}", :default => '', :locale => Setting.default_language)
237 end
240 end
238 keys.reject! {|k| k.blank?}
241 keys.reject! {|k| k.blank?}
239 keys.collect! {|k| Regexp.escape(k)}
242 keys.collect! {|k| Regexp.escape(k)}
240 format ||= '.+'
243 format ||= '.+'
241 text.gsub!(/^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i, '')
244 text.gsub!(/^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i, '')
242 $2 && $2.strip
245 $2 && $2.strip
243 end
246 end
244
247
245 def target_project
248 def target_project
246 # TODO: other ways to specify project:
249 # TODO: other ways to specify project:
247 # * parse the email To field
250 # * parse the email To field
248 # * specific project (eg. Setting.mail_handler_target_project)
251 # * specific project (eg. Setting.mail_handler_target_project)
249 target = Project.find_by_identifier(get_keyword(:project))
252 target = Project.find_by_identifier(get_keyword(:project))
250 raise MissingInformation.new('Unable to determine target project') if target.nil?
253 raise MissingInformation.new('Unable to determine target project') if target.nil?
251 target
254 target
252 end
255 end
253
256
254 # Returns a Hash of issue attributes extracted from keywords in the email body
257 # Returns a Hash of issue attributes extracted from keywords in the email body
255 def issue_attributes_from_keywords(issue)
258 def issue_attributes_from_keywords(issue)
256 assigned_to = (k = get_keyword(:assigned_to, :override => true)) && find_user_from_keyword(k)
259 assigned_to = (k = get_keyword(:assigned_to, :override => true)) && find_user_from_keyword(k)
257 assigned_to = nil if assigned_to && !issue.assignable_users.include?(assigned_to)
260 assigned_to = nil if assigned_to && !issue.assignable_users.include?(assigned_to)
258
261
259 attrs = {
262 attrs = {
260 'tracker_id' => (k = get_keyword(:tracker)) && issue.project.trackers.find_by_name(k).try(:id),
263 'tracker_id' => (k = get_keyword(:tracker)) && issue.project.trackers.find_by_name(k).try(:id),
261 'status_id' => (k = get_keyword(:status)) && IssueStatus.find_by_name(k).try(:id),
264 'status_id' => (k = get_keyword(:status)) && IssueStatus.find_by_name(k).try(:id),
262 'priority_id' => (k = get_keyword(:priority)) && IssuePriority.find_by_name(k).try(:id),
265 'priority_id' => (k = get_keyword(:priority)) && IssuePriority.find_by_name(k).try(:id),
263 'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.find_by_name(k).try(:id),
266 'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.find_by_name(k).try(:id),
264 'assigned_to_id' => assigned_to.try(:id),
267 'assigned_to_id' => assigned_to.try(:id),
265 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) && issue.project.shared_versions.find_by_name(k).try(:id),
268 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) && issue.project.shared_versions.find_by_name(k).try(:id),
266 'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
269 'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
267 'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
270 'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
268 'estimated_hours' => get_keyword(:estimated_hours, :override => true),
271 'estimated_hours' => get_keyword(:estimated_hours, :override => true),
269 'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0')
272 'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0')
270 }.delete_if {|k, v| v.blank? }
273 }.delete_if {|k, v| v.blank? }
271
274
272 if issue.new_record? && attrs['tracker_id'].nil?
275 if issue.new_record? && attrs['tracker_id'].nil?
273 attrs['tracker_id'] = issue.project.trackers.find(:first).try(:id)
276 attrs['tracker_id'] = issue.project.trackers.find(:first).try(:id)
274 end
277 end
275
278
276 attrs
279 attrs
277 end
280 end
278
281
279 # Returns a Hash of issue custom field values extracted from keywords in the email body
282 # Returns a Hash of issue custom field values extracted from keywords in the email body
280 def custom_field_values_from_keywords(customized)
283 def custom_field_values_from_keywords(customized)
281 customized.custom_field_values.inject({}) do |h, v|
284 customized.custom_field_values.inject({}) do |h, v|
282 if value = get_keyword(v.custom_field.name, :override => true)
285 if value = get_keyword(v.custom_field.name, :override => true)
283 h[v.custom_field.id.to_s] = value
286 h[v.custom_field.id.to_s] = value
284 end
287 end
285 h
288 h
286 end
289 end
287 end
290 end
288
291
289 # Returns the text/plain part of the email
292 # Returns the text/plain part of the email
290 # If not found (eg. HTML-only email), returns the body with tags removed
293 # If not found (eg. HTML-only email), returns the body with tags removed
291 def plain_text_body
294 def plain_text_body
292 return @plain_text_body unless @plain_text_body.nil?
295 return @plain_text_body unless @plain_text_body.nil?
293 parts = @email.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten
296 parts = @email.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten
294 if parts.empty?
297 if parts.empty?
295 parts << @email
298 parts << @email
296 end
299 end
297 plain_text_part = parts.detect {|p| p.content_type == 'text/plain'}
300 plain_text_part = parts.detect {|p| p.content_type == 'text/plain'}
298 if plain_text_part.nil?
301 if plain_text_part.nil?
299 # no text/plain part found, assuming html-only email
302 # no text/plain part found, assuming html-only email
300 # strip html tags and remove doctype directive
303 # strip html tags and remove doctype directive
301 @plain_text_body = strip_tags(@email.body.to_s)
304 @plain_text_body = strip_tags(@email.body.to_s)
302 @plain_text_body.gsub! %r{^<!DOCTYPE .*$}, ''
305 @plain_text_body.gsub! %r{^<!DOCTYPE .*$}, ''
303 else
306 else
304 @plain_text_body = plain_text_part.body.to_s
307 @plain_text_body = plain_text_part.body.to_s
305 end
308 end
306 @plain_text_body.strip!
309 @plain_text_body.strip!
307 @plain_text_body
310 @plain_text_body
308 end
311 end
309
312
310 def cleaned_up_text_body
313 def cleaned_up_text_body
311 cleanup_body(plain_text_body)
314 cleanup_body(plain_text_body)
312 end
315 end
313
316
314 def self.full_sanitizer
317 def self.full_sanitizer
315 @full_sanitizer ||= HTML::FullSanitizer.new
318 @full_sanitizer ||= HTML::FullSanitizer.new
316 end
319 end
317
320
318 # Creates a user account for the +email+ sender
321 # Creates a user account for the +email+ sender
319 def self.create_user_from_email(email)
322 def self.create_user_from_email(email)
320 addr = email.from_addrs.to_a.first
323 addr = email.from_addrs.to_a.first
321 if addr && !addr.spec.blank?
324 if addr && !addr.spec.blank?
322 user = User.new
325 user = User.new
323 user.mail = addr.spec
326 user.mail = addr.spec
324
327
325 names = addr.name.blank? ? addr.spec.gsub(/@.*$/, '').split('.') : addr.name.split
328 names = addr.name.blank? ? addr.spec.gsub(/@.*$/, '').split('.') : addr.name.split
326 user.firstname = names.shift
329 user.firstname = names.shift
327 user.lastname = names.join(' ')
330 user.lastname = names.join(' ')
328 user.lastname = '-' if user.lastname.blank?
331 user.lastname = '-' if user.lastname.blank?
329
332
330 user.login = user.mail
333 user.login = user.mail
331 user.password = ActiveSupport::SecureRandom.hex(5)
334 user.password = ActiveSupport::SecureRandom.hex(5)
332 user.language = Setting.default_language
335 user.language = Setting.default_language
333 user.save ? user : nil
336 user.save ? user : nil
334 end
337 end
335 end
338 end
336
339
337 private
340 private
338
341
339 # Removes the email body of text after the truncation configurations.
342 # Removes the email body of text after the truncation configurations.
340 def cleanup_body(body)
343 def cleanup_body(body)
341 delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
344 delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
342 unless delimiters.empty?
345 unless delimiters.empty?
343 regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
346 regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
344 body = body.gsub(regex, '')
347 body = body.gsub(regex, '')
345 end
348 end
346 body.strip
349 body.strip
347 end
350 end
348
351
349 def find_user_from_keyword(keyword)
352 def find_user_from_keyword(keyword)
350 user ||= User.find_by_mail(keyword)
353 user ||= User.find_by_mail(keyword)
351 user ||= User.find_by_login(keyword)
354 user ||= User.find_by_login(keyword)
352 if user.nil? && keyword.match(/ /)
355 if user.nil? && keyword.match(/ /)
353 firstname, lastname = *(keyword.split) # "First Last Throwaway"
356 firstname, lastname = *(keyword.split) # "First Last Throwaway"
354 user ||= User.find_by_firstname_and_lastname(firstname, lastname)
357 user ||= User.find_by_firstname_and_lastname(firstname, lastname)
355 end
358 end
356 user
359 user
357 end
360 end
358 end
361 end
@@ -1,454 +1,462
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2009 Jean-Philippe Lang
4 # Copyright (C) 2006-2009 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,
23 fixtures :users, :projects,
24 :enabled_modules,
24 :enabled_modules,
25 :roles,
25 :roles,
26 :members,
26 :members,
27 :member_roles,
27 :member_roles,
28 :users,
28 :users,
29 :issues,
29 :issues,
30 :issue_statuses,
30 :issue_statuses,
31 :workflows,
31 :workflows,
32 :trackers,
32 :trackers,
33 :projects_trackers,
33 :projects_trackers,
34 :versions,
34 :versions,
35 :enumerations,
35 :enumerations,
36 :issue_categories,
36 :issue_categories,
37 :custom_fields,
37 :custom_fields,
38 :custom_fields_trackers,
38 :custom_fields_trackers,
39 :custom_fields_projects,
39 :custom_fields_projects,
40 :boards,
40 :boards,
41 :messages
41 :messages
42
42
43 FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler'
43 FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler'
44
44
45 def setup
45 def setup
46 ActionMailer::Base.deliveries.clear
46 ActionMailer::Base.deliveries.clear
47 Setting.notified_events = Redmine::Notifiable.all.collect(&:name)
47 Setting.notified_events = Redmine::Notifiable.all.collect(&:name)
48 end
48 end
49
49
50 def test_add_issue
50 def test_add_issue
51 ActionMailer::Base.deliveries.clear
51 ActionMailer::Base.deliveries.clear
52 # This email contains: 'Project: onlinestore'
52 # This email contains: 'Project: onlinestore'
53 issue = submit_email('ticket_on_given_project.eml')
53 issue = submit_email('ticket_on_given_project.eml')
54 assert issue.is_a?(Issue)
54 assert issue.is_a?(Issue)
55 assert !issue.new_record?
55 assert !issue.new_record?
56 issue.reload
56 issue.reload
57 assert_equal Project.find(2), issue.project
57 assert_equal Project.find(2), issue.project
58 assert_equal issue.project.trackers.first, issue.tracker
58 assert_equal issue.project.trackers.first, issue.tracker
59 assert_equal 'New ticket on a given project', issue.subject
59 assert_equal 'New ticket on a given project', issue.subject
60 assert_equal User.find_by_login('jsmith'), issue.author
60 assert_equal User.find_by_login('jsmith'), issue.author
61 assert_equal IssueStatus.find_by_name('Resolved'), issue.status
61 assert_equal IssueStatus.find_by_name('Resolved'), issue.status
62 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
62 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
63 assert_equal '2010-01-01', issue.start_date.to_s
63 assert_equal '2010-01-01', issue.start_date.to_s
64 assert_equal '2010-12-31', issue.due_date.to_s
64 assert_equal '2010-12-31', issue.due_date.to_s
65 assert_equal User.find_by_login('jsmith'), issue.assigned_to
65 assert_equal User.find_by_login('jsmith'), issue.assigned_to
66 assert_equal Version.find_by_name('alpha'), issue.fixed_version
66 assert_equal Version.find_by_name('alpha'), issue.fixed_version
67 assert_equal 2.5, issue.estimated_hours
67 assert_equal 2.5, issue.estimated_hours
68 assert_equal 30, issue.done_ratio
68 assert_equal 30, issue.done_ratio
69 assert_equal [issue.id, 1, 2], [issue.root_id, issue.lft, issue.rgt]
69 assert_equal [issue.id, 1, 2], [issue.root_id, issue.lft, issue.rgt]
70 # keywords should be removed from the email body
70 # keywords should be removed from the email body
71 assert !issue.description.match(/^Project:/i)
71 assert !issue.description.match(/^Project:/i)
72 assert !issue.description.match(/^Status:/i)
72 assert !issue.description.match(/^Status:/i)
73 # Email notification should be sent
73 # Email notification should be sent
74 mail = ActionMailer::Base.deliveries.last
74 mail = ActionMailer::Base.deliveries.last
75 assert_not_nil mail
75 assert_not_nil mail
76 assert mail.subject.include?('New ticket on a given project')
76 assert mail.subject.include?('New ticket on a given project')
77 end
77 end
78
78
79 def test_add_issue_with_default_tracker
79 def test_add_issue_with_default_tracker
80 # This email contains: 'Project: onlinestore'
80 # This email contains: 'Project: onlinestore'
81 issue = submit_email('ticket_on_given_project.eml', :issue => {:tracker => 'Support request'})
81 issue = submit_email('ticket_on_given_project.eml', :issue => {:tracker => 'Support request'})
82 assert issue.is_a?(Issue)
82 assert issue.is_a?(Issue)
83 assert !issue.new_record?
83 assert !issue.new_record?
84 issue.reload
84 issue.reload
85 assert_equal 'Support request', issue.tracker.name
85 assert_equal 'Support request', issue.tracker.name
86 end
86 end
87
87
88 def test_add_issue_with_status
88 def test_add_issue_with_status
89 # This email contains: 'Project: onlinestore' and 'Status: Resolved'
89 # This email contains: 'Project: onlinestore' and 'Status: Resolved'
90 issue = submit_email('ticket_on_given_project.eml')
90 issue = submit_email('ticket_on_given_project.eml')
91 assert issue.is_a?(Issue)
91 assert issue.is_a?(Issue)
92 assert !issue.new_record?
92 assert !issue.new_record?
93 issue.reload
93 issue.reload
94 assert_equal Project.find(2), issue.project
94 assert_equal Project.find(2), issue.project
95 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
95 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
96 end
96 end
97
97
98 def test_add_issue_with_attributes_override
98 def test_add_issue_with_attributes_override
99 issue = submit_email('ticket_with_attributes.eml', :allow_override => 'tracker,category,priority')
99 issue = submit_email('ticket_with_attributes.eml', :allow_override => 'tracker,category,priority')
100 assert issue.is_a?(Issue)
100 assert issue.is_a?(Issue)
101 assert !issue.new_record?
101 assert !issue.new_record?
102 issue.reload
102 issue.reload
103 assert_equal 'New ticket on a given project', issue.subject
103 assert_equal 'New ticket on a given project', issue.subject
104 assert_equal User.find_by_login('jsmith'), issue.author
104 assert_equal User.find_by_login('jsmith'), issue.author
105 assert_equal Project.find(2), issue.project
105 assert_equal Project.find(2), issue.project
106 assert_equal 'Feature request', issue.tracker.to_s
106 assert_equal 'Feature request', issue.tracker.to_s
107 assert_equal 'Stock management', issue.category.to_s
107 assert_equal 'Stock management', issue.category.to_s
108 assert_equal 'Urgent', issue.priority.to_s
108 assert_equal 'Urgent', issue.priority.to_s
109 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
109 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
110 end
110 end
111
111
112 def test_add_issue_with_partial_attributes_override
112 def test_add_issue_with_partial_attributes_override
113 issue = submit_email('ticket_with_attributes.eml', :issue => {:priority => 'High'}, :allow_override => ['tracker'])
113 issue = submit_email('ticket_with_attributes.eml', :issue => {:priority => 'High'}, :allow_override => ['tracker'])
114 assert issue.is_a?(Issue)
114 assert issue.is_a?(Issue)
115 assert !issue.new_record?
115 assert !issue.new_record?
116 issue.reload
116 issue.reload
117 assert_equal 'New ticket on a given project', issue.subject
117 assert_equal 'New ticket on a given project', issue.subject
118 assert_equal User.find_by_login('jsmith'), issue.author
118 assert_equal User.find_by_login('jsmith'), issue.author
119 assert_equal Project.find(2), issue.project
119 assert_equal Project.find(2), issue.project
120 assert_equal 'Feature request', issue.tracker.to_s
120 assert_equal 'Feature request', issue.tracker.to_s
121 assert_nil issue.category
121 assert_nil issue.category
122 assert_equal 'High', issue.priority.to_s
122 assert_equal 'High', issue.priority.to_s
123 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
123 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
124 end
124 end
125
125
126 def test_add_issue_with_spaces_between_attribute_and_separator
126 def test_add_issue_with_spaces_between_attribute_and_separator
127 issue = submit_email('ticket_with_spaces_between_attribute_and_separator.eml', :allow_override => 'tracker,category,priority')
127 issue = submit_email('ticket_with_spaces_between_attribute_and_separator.eml', :allow_override => 'tracker,category,priority')
128 assert issue.is_a?(Issue)
128 assert issue.is_a?(Issue)
129 assert !issue.new_record?
129 assert !issue.new_record?
130 issue.reload
130 issue.reload
131 assert_equal 'New ticket on a given project', issue.subject
131 assert_equal 'New ticket on a given project', issue.subject
132 assert_equal User.find_by_login('jsmith'), issue.author
132 assert_equal User.find_by_login('jsmith'), issue.author
133 assert_equal Project.find(2), issue.project
133 assert_equal Project.find(2), issue.project
134 assert_equal 'Feature request', issue.tracker.to_s
134 assert_equal 'Feature request', issue.tracker.to_s
135 assert_equal 'Stock management', issue.category.to_s
135 assert_equal 'Stock management', issue.category.to_s
136 assert_equal 'Urgent', issue.priority.to_s
136 assert_equal 'Urgent', issue.priority.to_s
137 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
137 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
138 end
138 end
139
139
140
140
141 def test_add_issue_with_attachment_to_specific_project
141 def test_add_issue_with_attachment_to_specific_project
142 issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
142 issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
143 assert issue.is_a?(Issue)
143 assert issue.is_a?(Issue)
144 assert !issue.new_record?
144 assert !issue.new_record?
145 issue.reload
145 issue.reload
146 assert_equal 'Ticket created by email with attachment', issue.subject
146 assert_equal 'Ticket created by email with attachment', issue.subject
147 assert_equal User.find_by_login('jsmith'), issue.author
147 assert_equal User.find_by_login('jsmith'), issue.author
148 assert_equal Project.find(2), issue.project
148 assert_equal Project.find(2), issue.project
149 assert_equal 'This is a new ticket with attachments', issue.description
149 assert_equal 'This is a new ticket with attachments', issue.description
150 # Attachment properties
150 # Attachment properties
151 assert_equal 1, issue.attachments.size
151 assert_equal 1, issue.attachments.size
152 assert_equal 'Paella.jpg', issue.attachments.first.filename
152 assert_equal 'Paella.jpg', issue.attachments.first.filename
153 assert_equal 'image/jpeg', issue.attachments.first.content_type
153 assert_equal 'image/jpeg', issue.attachments.first.content_type
154 assert_equal 10790, issue.attachments.first.filesize
154 assert_equal 10790, issue.attachments.first.filesize
155 end
155 end
156
156
157 def test_add_issue_with_custom_fields
157 def test_add_issue_with_custom_fields
158 issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'onlinestore'})
158 issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'onlinestore'})
159 assert issue.is_a?(Issue)
159 assert issue.is_a?(Issue)
160 assert !issue.new_record?
160 assert !issue.new_record?
161 issue.reload
161 issue.reload
162 assert_equal 'New ticket with custom field values', issue.subject
162 assert_equal 'New ticket with custom field values', issue.subject
163 assert_equal 'Value for a custom field', issue.custom_value_for(CustomField.find_by_name('Searchable field')).value
163 assert_equal 'Value for a custom field', issue.custom_value_for(CustomField.find_by_name('Searchable field')).value
164 assert !issue.description.match(/^searchable field:/i)
164 assert !issue.description.match(/^searchable field:/i)
165 end
165 end
166
166
167 def test_add_issue_with_cc
167 def test_add_issue_with_cc
168 issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'})
168 issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'})
169 assert issue.is_a?(Issue)
169 assert issue.is_a?(Issue)
170 assert !issue.new_record?
170 assert !issue.new_record?
171 issue.reload
171 issue.reload
172 assert issue.watched_by?(User.find_by_mail('dlopper@somenet.foo'))
172 assert issue.watched_by?(User.find_by_mail('dlopper@somenet.foo'))
173 assert_equal 1, issue.watcher_user_ids.size
173 assert_equal 1, issue.watcher_user_ids.size
174 end
174 end
175
175
176 def test_add_issue_by_unknown_user
176 def test_add_issue_by_unknown_user
177 assert_no_difference 'User.count' do
177 assert_no_difference 'User.count' do
178 assert_equal false, submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'})
178 assert_equal false, submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'})
179 end
179 end
180 end
180 end
181
181
182 def test_add_issue_by_anonymous_user
182 def test_add_issue_by_anonymous_user
183 Role.anonymous.add_permission!(:add_issues)
183 Role.anonymous.add_permission!(:add_issues)
184 assert_no_difference 'User.count' do
184 assert_no_difference 'User.count' do
185 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'accept')
185 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'accept')
186 assert issue.is_a?(Issue)
186 assert issue.is_a?(Issue)
187 assert issue.author.anonymous?
187 assert issue.author.anonymous?
188 end
188 end
189 end
189 end
190
190
191 def test_add_issue_by_anonymous_user_with_no_from_address
191 def test_add_issue_by_anonymous_user_with_no_from_address
192 Role.anonymous.add_permission!(:add_issues)
192 Role.anonymous.add_permission!(:add_issues)
193 assert_no_difference 'User.count' do
193 assert_no_difference 'User.count' do
194 issue = submit_email('ticket_by_empty_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'accept')
194 issue = submit_email('ticket_by_empty_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'accept')
195 assert issue.is_a?(Issue)
195 assert issue.is_a?(Issue)
196 assert issue.author.anonymous?
196 assert issue.author.anonymous?
197 end
197 end
198 end
198 end
199
199
200 def test_add_issue_by_anonymous_user_on_private_project
200 def test_add_issue_by_anonymous_user_on_private_project
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 assert_no_difference 'Issue.count' do
203 assert_no_difference 'Issue.count' do
204 assert_equal false, submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'onlinestore'}, :unknown_user => 'accept')
204 assert_equal false, submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'onlinestore'}, :unknown_user => 'accept')
205 end
205 end
206 end
206 end
207 end
207 end
208
208
209 def test_add_issue_by_anonymous_user_on_private_project_without_permission_check
209 def test_add_issue_by_anonymous_user_on_private_project_without_permission_check
210 assert_no_difference 'User.count' do
210 assert_no_difference 'User.count' do
211 assert_difference 'Issue.count' do
211 assert_difference 'Issue.count' do
212 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'onlinestore'}, :no_permission_check => '1', :unknown_user => 'accept')
212 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'onlinestore'}, :no_permission_check => '1', :unknown_user => 'accept')
213 assert issue.is_a?(Issue)
213 assert issue.is_a?(Issue)
214 assert issue.author.anonymous?
214 assert issue.author.anonymous?
215 assert !issue.project.is_public?
215 assert !issue.project.is_public?
216 assert_equal [issue.id, 1, 2], [issue.root_id, issue.lft, issue.rgt]
216 assert_equal [issue.id, 1, 2], [issue.root_id, issue.lft, issue.rgt]
217 end
217 end
218 end
218 end
219 end
219 end
220
220
221 def test_add_issue_by_created_user
221 def test_add_issue_by_created_user
222 Setting.default_language = 'en'
222 Setting.default_language = 'en'
223 assert_difference 'User.count' do
223 assert_difference 'User.count' do
224 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'create')
224 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'create')
225 assert issue.is_a?(Issue)
225 assert issue.is_a?(Issue)
226 assert issue.author.active?
226 assert issue.author.active?
227 assert_equal 'john.doe@somenet.foo', issue.author.mail
227 assert_equal 'john.doe@somenet.foo', issue.author.mail
228 assert_equal 'John', issue.author.firstname
228 assert_equal 'John', issue.author.firstname
229 assert_equal 'Doe', issue.author.lastname
229 assert_equal 'Doe', issue.author.lastname
230
230
231 # account information
231 # account information
232 email = ActionMailer::Base.deliveries.first
232 email = ActionMailer::Base.deliveries.first
233 assert_not_nil email
233 assert_not_nil email
234 assert email.subject.include?('account activation')
234 assert email.subject.include?('account activation')
235 login = email.body.match(/\* Login: (.*)$/)[1]
235 login = email.body.match(/\* Login: (.*)$/)[1]
236 password = email.body.match(/\* Password: (.*)$/)[1]
236 password = email.body.match(/\* Password: (.*)$/)[1]
237 assert_equal issue.author, User.try_to_login(login, password)
237 assert_equal issue.author, User.try_to_login(login, password)
238 end
238 end
239 end
239 end
240
240
241 def test_add_issue_without_from_header
241 def test_add_issue_without_from_header
242 Role.anonymous.add_permission!(:add_issues)
242 Role.anonymous.add_permission!(:add_issues)
243 assert_equal false, submit_email('ticket_without_from_header.eml')
243 assert_equal false, submit_email('ticket_without_from_header.eml')
244 end
244 end
245
245
246 def test_add_issue_with_invalid_attributes
246 def test_add_issue_with_invalid_attributes
247 issue = submit_email('ticket_with_invalid_attributes.eml', :allow_override => 'tracker,category,priority')
247 issue = submit_email('ticket_with_invalid_attributes.eml', :allow_override => 'tracker,category,priority')
248 assert issue.is_a?(Issue)
248 assert issue.is_a?(Issue)
249 assert !issue.new_record?
249 assert !issue.new_record?
250 issue.reload
250 issue.reload
251 assert_nil issue.assigned_to
251 assert_nil issue.assigned_to
252 assert_nil issue.start_date
252 assert_nil issue.start_date
253 assert_nil issue.due_date
253 assert_nil issue.due_date
254 assert_equal 0, issue.done_ratio
254 assert_equal 0, issue.done_ratio
255 assert_equal 'Normal', issue.priority.to_s
255 assert_equal 'Normal', issue.priority.to_s
256 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
256 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
257 end
257 end
258
258
259 def test_add_issue_with_localized_attributes
259 def test_add_issue_with_localized_attributes
260 User.find_by_mail('jsmith@somenet.foo').update_attribute 'language', 'fr'
260 User.find_by_mail('jsmith@somenet.foo').update_attribute 'language', 'fr'
261 issue = submit_email('ticket_with_localized_attributes.eml', :allow_override => 'tracker,category,priority')
261 issue = submit_email('ticket_with_localized_attributes.eml', :allow_override => 'tracker,category,priority')
262 assert issue.is_a?(Issue)
262 assert issue.is_a?(Issue)
263 assert !issue.new_record?
263 assert !issue.new_record?
264 issue.reload
264 issue.reload
265 assert_equal 'New ticket on a given project', issue.subject
265 assert_equal 'New ticket on a given project', issue.subject
266 assert_equal User.find_by_login('jsmith'), issue.author
266 assert_equal User.find_by_login('jsmith'), issue.author
267 assert_equal Project.find(2), issue.project
267 assert_equal Project.find(2), issue.project
268 assert_equal 'Feature request', issue.tracker.to_s
268 assert_equal 'Feature request', issue.tracker.to_s
269 assert_equal 'Stock management', issue.category.to_s
269 assert_equal 'Stock management', issue.category.to_s
270 assert_equal 'Urgent', issue.priority.to_s
270 assert_equal 'Urgent', issue.priority.to_s
271 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
271 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
272 end
272 end
273
273
274 def test_add_issue_with_japanese_keywords
274 def test_add_issue_with_japanese_keywords
275 tracker = Tracker.create!(:name => 'ι–‹η™Ί')
275 tracker = Tracker.create!(:name => 'ι–‹η™Ί')
276 Project.find(1).trackers << tracker
276 Project.find(1).trackers << tracker
277 issue = submit_email('japanese_keywords_iso_2022_jp.eml', :issue => {:project => 'ecookbook'}, :allow_override => 'tracker')
277 issue = submit_email('japanese_keywords_iso_2022_jp.eml', :issue => {:project => 'ecookbook'}, :allow_override => 'tracker')
278 assert_kind_of Issue, issue
278 assert_kind_of Issue, issue
279 assert_equal tracker, issue.tracker
279 assert_equal tracker, issue.tracker
280 end
280 end
281
281
282 def test_should_ignore_emails_from_emission_address
282 def test_should_ignore_emails_from_emission_address
283 Role.anonymous.add_permission!(:add_issues)
283 Role.anonymous.add_permission!(:add_issues)
284 assert_no_difference 'User.count' do
284 assert_no_difference 'User.count' do
285 assert_equal false, submit_email('ticket_from_emission_address.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'create')
285 assert_equal false, submit_email('ticket_from_emission_address.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'create')
286 end
286 end
287 end
287 end
288
288
289 def test_add_issue_should_send_email_notification
289 def test_add_issue_should_send_email_notification
290 Setting.notified_events = ['issue_added']
290 Setting.notified_events = ['issue_added']
291 ActionMailer::Base.deliveries.clear
291 ActionMailer::Base.deliveries.clear
292 # This email contains: 'Project: onlinestore'
292 # This email contains: 'Project: onlinestore'
293 issue = submit_email('ticket_on_given_project.eml')
293 issue = submit_email('ticket_on_given_project.eml')
294 assert issue.is_a?(Issue)
294 assert issue.is_a?(Issue)
295 assert_equal 1, ActionMailer::Base.deliveries.size
295 assert_equal 1, ActionMailer::Base.deliveries.size
296 end
296 end
297
297
298 def test_add_issue_note
298 def test_add_issue_note
299 journal = submit_email('ticket_reply.eml')
299 journal = submit_email('ticket_reply.eml')
300 assert journal.is_a?(Journal)
300 assert journal.is_a?(Journal)
301 assert_equal User.find_by_login('jsmith'), journal.user
301 assert_equal User.find_by_login('jsmith'), journal.user
302 assert_equal Issue.find(2), journal.journalized
302 assert_equal Issue.find(2), journal.journalized
303 assert_match /This is reply/, journal.notes
303 assert_match /This is reply/, journal.notes
304 assert_equal 'Feature request', journal.issue.tracker.name
304 assert_equal 'Feature request', journal.issue.tracker.name
305 end
305 end
306
306
307 def test_add_issue_note_with_attribute_changes
307 def test_add_issue_note_with_attribute_changes
308 # This email contains: 'Status: Resolved'
308 # This email contains: 'Status: Resolved'
309 journal = submit_email('ticket_reply_with_status.eml')
309 journal = submit_email('ticket_reply_with_status.eml')
310 assert journal.is_a?(Journal)
310 assert journal.is_a?(Journal)
311 issue = Issue.find(journal.issue.id)
311 issue = Issue.find(journal.issue.id)
312 assert_equal User.find_by_login('jsmith'), journal.user
312 assert_equal User.find_by_login('jsmith'), journal.user
313 assert_equal Issue.find(2), journal.journalized
313 assert_equal Issue.find(2), journal.journalized
314 assert_match /This is reply/, journal.notes
314 assert_match /This is reply/, journal.notes
315 assert_equal 'Feature request', journal.issue.tracker.name
315 assert_equal 'Feature request', journal.issue.tracker.name
316 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
316 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
317 assert_equal '2010-01-01', issue.start_date.to_s
317 assert_equal '2010-01-01', issue.start_date.to_s
318 assert_equal '2010-12-31', issue.due_date.to_s
318 assert_equal '2010-12-31', issue.due_date.to_s
319 assert_equal User.find_by_login('jsmith'), issue.assigned_to
319 assert_equal User.find_by_login('jsmith'), issue.assigned_to
320 assert_equal "52.6", issue.custom_value_for(CustomField.find_by_name('Float field')).value
320 assert_equal "52.6", issue.custom_value_for(CustomField.find_by_name('Float field')).value
321 end
321 end
322
322
323 def test_add_issue_note_should_send_email_notification
323 def test_add_issue_note_should_send_email_notification
324 ActionMailer::Base.deliveries.clear
324 ActionMailer::Base.deliveries.clear
325 journal = submit_email('ticket_reply.eml')
325 journal = submit_email('ticket_reply.eml')
326 assert journal.is_a?(Journal)
326 assert journal.is_a?(Journal)
327 assert_equal 1, ActionMailer::Base.deliveries.size
327 assert_equal 1, ActionMailer::Base.deliveries.size
328 end
328 end
329
329
330 def test_add_issue_note_should_not_set_defaults
331 journal = submit_email('ticket_reply.eml', :issue => {:tracker => 'Support request', :priority => 'High'})
332 assert journal.is_a?(Journal)
333 assert_match /This is reply/, journal.notes
334 assert_equal 'Feature request', journal.issue.tracker.name
335 assert_equal 'Normal', journal.issue.priority.name
336 end
337
330 def test_reply_to_a_message
338 def test_reply_to_a_message
331 m = submit_email('message_reply.eml')
339 m = submit_email('message_reply.eml')
332 assert m.is_a?(Message)
340 assert m.is_a?(Message)
333 assert !m.new_record?
341 assert !m.new_record?
334 m.reload
342 m.reload
335 assert_equal 'Reply via email', m.subject
343 assert_equal 'Reply via email', m.subject
336 # The email replies to message #2 which is part of the thread of message #1
344 # The email replies to message #2 which is part of the thread of message #1
337 assert_equal Message.find(1), m.parent
345 assert_equal Message.find(1), m.parent
338 end
346 end
339
347
340 def test_reply_to_a_message_by_subject
348 def test_reply_to_a_message_by_subject
341 m = submit_email('message_reply_by_subject.eml')
349 m = submit_email('message_reply_by_subject.eml')
342 assert m.is_a?(Message)
350 assert m.is_a?(Message)
343 assert !m.new_record?
351 assert !m.new_record?
344 m.reload
352 m.reload
345 assert_equal 'Reply to the first post', m.subject
353 assert_equal 'Reply to the first post', m.subject
346 assert_equal Message.find(1), m.parent
354 assert_equal Message.find(1), m.parent
347 end
355 end
348
356
349 def test_should_strip_tags_of_html_only_emails
357 def test_should_strip_tags_of_html_only_emails
350 issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'})
358 issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'})
351 assert issue.is_a?(Issue)
359 assert issue.is_a?(Issue)
352 assert !issue.new_record?
360 assert !issue.new_record?
353 issue.reload
361 issue.reload
354 assert_equal 'HTML email', issue.subject
362 assert_equal 'HTML email', issue.subject
355 assert_equal 'This is a html-only email.', issue.description
363 assert_equal 'This is a html-only email.', issue.description
356 end
364 end
357
365
358 context "truncate emails based on the Setting" do
366 context "truncate emails based on the Setting" do
359 context "with no setting" do
367 context "with no setting" do
360 setup do
368 setup do
361 Setting.mail_handler_body_delimiters = ''
369 Setting.mail_handler_body_delimiters = ''
362 end
370 end
363
371
364 should "add the entire email into the issue" do
372 should "add the entire email into the issue" do
365 issue = submit_email('ticket_on_given_project.eml')
373 issue = submit_email('ticket_on_given_project.eml')
366 assert_issue_created(issue)
374 assert_issue_created(issue)
367 assert issue.description.include?('---')
375 assert issue.description.include?('---')
368 assert issue.description.include?('This paragraph is after the delimiter')
376 assert issue.description.include?('This paragraph is after the delimiter')
369 end
377 end
370 end
378 end
371
379
372 context "with a single string" do
380 context "with a single string" do
373 setup do
381 setup do
374 Setting.mail_handler_body_delimiters = '---'
382 Setting.mail_handler_body_delimiters = '---'
375 end
383 end
376
384
377 should "truncate the email at the delimiter for the issue" do
385 should "truncate the email at the delimiter for the issue" do
378 issue = submit_email('ticket_on_given_project.eml')
386 issue = submit_email('ticket_on_given_project.eml')
379 assert_issue_created(issue)
387 assert_issue_created(issue)
380 assert issue.description.include?('This paragraph is before delimiters')
388 assert issue.description.include?('This paragraph is before delimiters')
381 assert issue.description.include?('--- This line starts with a delimiter')
389 assert issue.description.include?('--- This line starts with a delimiter')
382 assert !issue.description.match(/^---$/)
390 assert !issue.description.match(/^---$/)
383 assert !issue.description.include?('This paragraph is after the delimiter')
391 assert !issue.description.include?('This paragraph is after the delimiter')
384 end
392 end
385 end
393 end
386
394
387 context "with a single quoted reply (e.g. reply to a Redmine email notification)" do
395 context "with a single quoted reply (e.g. reply to a Redmine email notification)" do
388 setup do
396 setup do
389 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
397 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
390 end
398 end
391
399
392 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
400 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
393 journal = submit_email('issue_update_with_quoted_reply_above.eml')
401 journal = submit_email('issue_update_with_quoted_reply_above.eml')
394 assert journal.is_a?(Journal)
402 assert journal.is_a?(Journal)
395 assert journal.notes.include?('An update to the issue by the sender.')
403 assert journal.notes.include?('An update to the issue by the sender.')
396 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
404 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
397 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
405 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
398
406
399 end
407 end
400
408
401 end
409 end
402
410
403 context "with multiple quoted replies (e.g. reply to a reply of a Redmine email notification)" do
411 context "with multiple quoted replies (e.g. reply to a reply of a Redmine email notification)" do
404 setup do
412 setup do
405 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
413 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
406 end
414 end
407
415
408 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
416 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
409 journal = submit_email('issue_update_with_multiple_quoted_reply_above.eml')
417 journal = submit_email('issue_update_with_multiple_quoted_reply_above.eml')
410 assert journal.is_a?(Journal)
418 assert journal.is_a?(Journal)
411 assert journal.notes.include?('An update to the issue by the sender.')
419 assert journal.notes.include?('An update to the issue by the sender.')
412 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
420 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
413 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
421 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
414
422
415 end
423 end
416
424
417 end
425 end
418
426
419 context "with multiple strings" do
427 context "with multiple strings" do
420 setup do
428 setup do
421 Setting.mail_handler_body_delimiters = "---\nBREAK"
429 Setting.mail_handler_body_delimiters = "---\nBREAK"
422 end
430 end
423
431
424 should "truncate the email at the first delimiter found (BREAK)" do
432 should "truncate the email at the first delimiter found (BREAK)" do
425 issue = submit_email('ticket_on_given_project.eml')
433 issue = submit_email('ticket_on_given_project.eml')
426 assert_issue_created(issue)
434 assert_issue_created(issue)
427 assert issue.description.include?('This paragraph is before delimiters')
435 assert issue.description.include?('This paragraph is before delimiters')
428 assert !issue.description.include?('BREAK')
436 assert !issue.description.include?('BREAK')
429 assert !issue.description.include?('This paragraph is between delimiters')
437 assert !issue.description.include?('This paragraph is between delimiters')
430 assert !issue.description.match(/^---$/)
438 assert !issue.description.match(/^---$/)
431 assert !issue.description.include?('This paragraph is after the delimiter')
439 assert !issue.description.include?('This paragraph is after the delimiter')
432 end
440 end
433 end
441 end
434 end
442 end
435
443
436 def test_email_with_long_subject_line
444 def test_email_with_long_subject_line
437 issue = submit_email('ticket_with_long_subject.eml')
445 issue = submit_email('ticket_with_long_subject.eml')
438 assert issue.is_a?(Issue)
446 assert issue.is_a?(Issue)
439 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]
447 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]
440 end
448 end
441
449
442 private
450 private
443
451
444 def submit_email(filename, options={})
452 def submit_email(filename, options={})
445 raw = IO.read(File.join(FIXTURES_PATH, filename))
453 raw = IO.read(File.join(FIXTURES_PATH, filename))
446 MailHandler.receive(raw, options)
454 MailHandler.receive(raw, options)
447 end
455 end
448
456
449 def assert_issue_created(issue)
457 def assert_issue_created(issue)
450 assert issue.is_a?(Issue)
458 assert issue.is_a?(Issue)
451 assert !issue.new_record?
459 assert !issue.new_record?
452 issue.reload
460 issue.reload
453 end
461 end
454 end
462 end
General Comments 0
You need to be logged in to leave comments. Login now