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