##// END OF EJS Templates
MailHandler: ignore assignee if invalid....
Jean-Philippe Lang -
r4290:d7cdd58db6b8
parent child
Show More
@@ -1,349 +1,352
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 journal = issue.init_journal(user, cleaned_up_text_body)
151 journal = issue.init_journal(user, cleaned_up_text_body)
152 issue.safe_attributes = issue_attributes_from_keywords(issue)
152 issue.safe_attributes = issue_attributes_from_keywords(issue)
153 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
153 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
154 add_attachments(issue)
154 add_attachments(issue)
155 issue.save!
155 issue.save!
156 logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info
156 logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info
157 journal
157 journal
158 end
158 end
159
159
160 # Reply will be added to the issue
160 # Reply will be added to the issue
161 def receive_journal_reply(journal_id)
161 def receive_journal_reply(journal_id)
162 journal = Journal.find_by_id(journal_id)
162 journal = Journal.find_by_id(journal_id)
163 if journal && journal.journalized_type == 'Issue'
163 if journal && journal.journalized_type == 'Issue'
164 receive_issue_reply(journal.journalized_id)
164 receive_issue_reply(journal.journalized_id)
165 end
165 end
166 end
166 end
167
167
168 # Receives a reply to a forum message
168 # Receives a reply to a forum message
169 def receive_message_reply(message_id)
169 def receive_message_reply(message_id)
170 message = Message.find_by_id(message_id)
170 message = Message.find_by_id(message_id)
171 if message
171 if message
172 message = message.root
172 message = message.root
173
173
174 unless @@handler_options[:no_permission_check]
174 unless @@handler_options[:no_permission_check]
175 raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
175 raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
176 end
176 end
177
177
178 if !message.locked?
178 if !message.locked?
179 reply = Message.new(:subject => email.subject.gsub(%r{^.*msg\d+\]}, '').strip,
179 reply = Message.new(:subject => email.subject.gsub(%r{^.*msg\d+\]}, '').strip,
180 :content => cleaned_up_text_body)
180 :content => cleaned_up_text_body)
181 reply.author = user
181 reply.author = user
182 reply.board = message.board
182 reply.board = message.board
183 message.children << reply
183 message.children << reply
184 add_attachments(reply)
184 add_attachments(reply)
185 reply
185 reply
186 else
186 else
187 logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic" if logger && logger.info
187 logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic" if logger && logger.info
188 end
188 end
189 end
189 end
190 end
190 end
191
191
192 def add_attachments(obj)
192 def add_attachments(obj)
193 if email.has_attachments?
193 if email.has_attachments?
194 email.attachments.each do |attachment|
194 email.attachments.each do |attachment|
195 Attachment.create(:container => obj,
195 Attachment.create(:container => obj,
196 :file => attachment,
196 :file => attachment,
197 :author => user,
197 :author => user,
198 :content_type => attachment.content_type)
198 :content_type => attachment.content_type)
199 end
199 end
200 end
200 end
201 end
201 end
202
202
203 # Adds To and Cc as watchers of the given object if the sender has the
203 # Adds To and Cc as watchers of the given object if the sender has the
204 # appropriate permission
204 # appropriate permission
205 def add_watchers(obj)
205 def add_watchers(obj)
206 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
206 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}
207 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
208 unless addresses.empty?
208 unless addresses.empty?
209 watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
209 watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
210 watchers.each {|w| obj.add_watcher(w)}
210 watchers.each {|w| obj.add_watcher(w)}
211 end
211 end
212 end
212 end
213 end
213 end
214
214
215 def get_keyword(attr, options={})
215 def get_keyword(attr, options={})
216 @keywords ||= {}
216 @keywords ||= {}
217 if @keywords.has_key?(attr)
217 if @keywords.has_key?(attr)
218 @keywords[attr]
218 @keywords[attr]
219 else
219 else
220 @keywords[attr] = begin
220 @keywords[attr] = begin
221 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && (v = extract_keyword!(plain_text_body, attr, options[:format]))
221 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && (v = extract_keyword!(plain_text_body, attr, options[:format]))
222 v
222 v
223 elsif !@@handler_options[:issue][attr].blank?
223 elsif !@@handler_options[:issue][attr].blank?
224 @@handler_options[:issue][attr]
224 @@handler_options[:issue][attr]
225 end
225 end
226 end
226 end
227 end
227 end
228 end
228 end
229
229
230 # Destructively extracts the value for +attr+ in +text+
230 # Destructively extracts the value for +attr+ in +text+
231 # Returns nil if no matching keyword found
231 # Returns nil if no matching keyword found
232 def extract_keyword!(text, attr, format=nil)
232 def extract_keyword!(text, attr, format=nil)
233 keys = [attr.to_s.humanize]
233 keys = [attr.to_s.humanize]
234 if attr.is_a?(Symbol)
234 if attr.is_a?(Symbol)
235 keys << l("field_#{attr}", :default => '', :locale => user.language) if user
235 keys << l("field_#{attr}", :default => '', :locale => user.language) if user
236 keys << l("field_#{attr}", :default => '', :locale => Setting.default_language)
236 keys << l("field_#{attr}", :default => '', :locale => Setting.default_language)
237 end
237 end
238 keys.reject! {|k| k.blank?}
238 keys.reject! {|k| k.blank?}
239 keys.collect! {|k| Regexp.escape(k)}
239 keys.collect! {|k| Regexp.escape(k)}
240 format ||= '.+'
240 format ||= '.+'
241 text.gsub!(/^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i, '')
241 text.gsub!(/^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i, '')
242 $2 && $2.strip
242 $2 && $2.strip
243 end
243 end
244
244
245 def target_project
245 def target_project
246 # TODO: other ways to specify project:
246 # TODO: other ways to specify project:
247 # * parse the email To field
247 # * parse the email To field
248 # * specific project (eg. Setting.mail_handler_target_project)
248 # * specific project (eg. Setting.mail_handler_target_project)
249 target = Project.find_by_identifier(get_keyword(:project))
249 target = Project.find_by_identifier(get_keyword(:project))
250 raise MissingInformation.new('Unable to determine target project') if target.nil?
250 raise MissingInformation.new('Unable to determine target project') if target.nil?
251 target
251 target
252 end
252 end
253
253
254 # Returns a Hash of issue attributes extracted from keywords in the email body
254 # Returns a Hash of issue attributes extracted from keywords in the email body
255 def issue_attributes_from_keywords(issue)
255 def issue_attributes_from_keywords(issue)
256 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)
258
256 {
259 {
257 'tracker_id' => ((k = get_keyword(:tracker)) && issue.project.trackers.find_by_name(k).try(:id)) || issue.project.trackers.find(:first).try(:id),
260 'tracker_id' => ((k = get_keyword(:tracker)) && issue.project.trackers.find_by_name(k).try(:id)) || issue.project.trackers.find(:first).try(:id),
258 'status_id' => (k = get_keyword(:status)) && IssueStatus.find_by_name(k).try(:id),
261 'status_id' => (k = get_keyword(:status)) && IssueStatus.find_by_name(k).try(:id),
259 'priority_id' => (k = get_keyword(:priority)) && IssuePriority.find_by_name(k).try(:id),
262 'priority_id' => (k = get_keyword(:priority)) && IssuePriority.find_by_name(k).try(:id),
260 'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.find_by_name(k).try(:id),
263 'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.find_by_name(k).try(:id),
261 'assigned_to_id' => (k = get_keyword(:assigned_to, :override => true)) && find_user_from_keyword(k).try(:id),
264 'assigned_to_id' => assigned_to.try(:id),
262 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) && issue.project.shared_versions.find_by_name(k).try(:id),
265 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) && issue.project.shared_versions.find_by_name(k).try(:id),
263 'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
266 'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
264 'due_date' => get_keyword(:due_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}'),
265 'estimated_hours' => get_keyword(:estimated_hours, :override => true),
268 'estimated_hours' => get_keyword(:estimated_hours, :override => true),
266 'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0')
269 'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0')
267 }.delete_if {|k, v| v.blank? }
270 }.delete_if {|k, v| v.blank? }
268 end
271 end
269
272
270 # Returns a Hash of issue custom field values extracted from keywords in the email body
273 # Returns a Hash of issue custom field values extracted from keywords in the email body
271 def custom_field_values_from_keywords(customized)
274 def custom_field_values_from_keywords(customized)
272 customized.custom_field_values.inject({}) do |h, v|
275 customized.custom_field_values.inject({}) do |h, v|
273 if value = get_keyword(v.custom_field.name, :override => true)
276 if value = get_keyword(v.custom_field.name, :override => true)
274 h[v.custom_field.id.to_s] = value
277 h[v.custom_field.id.to_s] = value
275 end
278 end
276 h
279 h
277 end
280 end
278 end
281 end
279
282
280 # Returns the text/plain part of the email
283 # Returns the text/plain part of the email
281 # If not found (eg. HTML-only email), returns the body with tags removed
284 # If not found (eg. HTML-only email), returns the body with tags removed
282 def plain_text_body
285 def plain_text_body
283 return @plain_text_body unless @plain_text_body.nil?
286 return @plain_text_body unless @plain_text_body.nil?
284 parts = @email.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten
287 parts = @email.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten
285 if parts.empty?
288 if parts.empty?
286 parts << @email
289 parts << @email
287 end
290 end
288 plain_text_part = parts.detect {|p| p.content_type == 'text/plain'}
291 plain_text_part = parts.detect {|p| p.content_type == 'text/plain'}
289 if plain_text_part.nil?
292 if plain_text_part.nil?
290 # no text/plain part found, assuming html-only email
293 # no text/plain part found, assuming html-only email
291 # strip html tags and remove doctype directive
294 # strip html tags and remove doctype directive
292 @plain_text_body = strip_tags(@email.body.to_s)
295 @plain_text_body = strip_tags(@email.body.to_s)
293 @plain_text_body.gsub! %r{^<!DOCTYPE .*$}, ''
296 @plain_text_body.gsub! %r{^<!DOCTYPE .*$}, ''
294 else
297 else
295 @plain_text_body = plain_text_part.body.to_s
298 @plain_text_body = plain_text_part.body.to_s
296 end
299 end
297 @plain_text_body.strip!
300 @plain_text_body.strip!
298 @plain_text_body
301 @plain_text_body
299 end
302 end
300
303
301 def cleaned_up_text_body
304 def cleaned_up_text_body
302 cleanup_body(plain_text_body)
305 cleanup_body(plain_text_body)
303 end
306 end
304
307
305 def self.full_sanitizer
308 def self.full_sanitizer
306 @full_sanitizer ||= HTML::FullSanitizer.new
309 @full_sanitizer ||= HTML::FullSanitizer.new
307 end
310 end
308
311
309 # Creates a user account for the +email+ sender
312 # Creates a user account for the +email+ sender
310 def self.create_user_from_email(email)
313 def self.create_user_from_email(email)
311 addr = email.from_addrs.to_a.first
314 addr = email.from_addrs.to_a.first
312 if addr && !addr.spec.blank?
315 if addr && !addr.spec.blank?
313 user = User.new
316 user = User.new
314 user.mail = addr.spec
317 user.mail = addr.spec
315
318
316 names = addr.name.blank? ? addr.spec.gsub(/@.*$/, '').split('.') : addr.name.split
319 names = addr.name.blank? ? addr.spec.gsub(/@.*$/, '').split('.') : addr.name.split
317 user.firstname = names.shift
320 user.firstname = names.shift
318 user.lastname = names.join(' ')
321 user.lastname = names.join(' ')
319 user.lastname = '-' if user.lastname.blank?
322 user.lastname = '-' if user.lastname.blank?
320
323
321 user.login = user.mail
324 user.login = user.mail
322 user.password = ActiveSupport::SecureRandom.hex(5)
325 user.password = ActiveSupport::SecureRandom.hex(5)
323 user.language = Setting.default_language
326 user.language = Setting.default_language
324 user.save ? user : nil
327 user.save ? user : nil
325 end
328 end
326 end
329 end
327
330
328 private
331 private
329
332
330 # Removes the email body of text after the truncation configurations.
333 # Removes the email body of text after the truncation configurations.
331 def cleanup_body(body)
334 def cleanup_body(body)
332 delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
335 delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
333 unless delimiters.empty?
336 unless delimiters.empty?
334 regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
337 regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
335 body = body.gsub(regex, '')
338 body = body.gsub(regex, '')
336 end
339 end
337 body.strip
340 body.strip
338 end
341 end
339
342
340 def find_user_from_keyword(keyword)
343 def find_user_from_keyword(keyword)
341 user ||= User.find_by_mail(keyword)
344 user ||= User.find_by_mail(keyword)
342 user ||= User.find_by_login(keyword)
345 user ||= User.find_by_login(keyword)
343 if user.nil? && keyword.match(/ /)
346 if user.nil? && keyword.match(/ /)
344 firstname, lastname = *(keyword.split) # "First Last Throwaway"
347 firstname, lastname = *(keyword.split) # "First Last Throwaway"
345 user ||= User.find_by_firstname_and_lastname(firstname, lastname)
348 user ||= User.find_by_firstname_and_lastname(firstname, lastname)
346 end
349 end
347 user
350 user
348 end
351 end
349 end
352 end
@@ -1,46 +1,47
1 Return-Path: <jsmith@somenet.foo>
1 Return-Path: <jsmith@somenet.foo>
2 Received: from osiris ([127.0.0.1])
2 Received: from osiris ([127.0.0.1])
3 by OSIRIS
3 by OSIRIS
4 with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200
4 with hMailServer ; Sun, 22 Jun 2008 12:28:07 +0200
5 Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris>
5 Message-ID: <000501c8d452$a95cd7e0$0a00a8c0@osiris>
6 From: "John Smith" <jsmith@somenet.foo>
6 From: "John Smith" <jsmith@somenet.foo>
7 To: <redmine@somenet.foo>
7 To: <redmine@somenet.foo>
8 Subject: New ticket on a given project
8 Subject: New ticket on a given project
9 Date: Sun, 22 Jun 2008 12:28:07 +0200
9 Date: Sun, 22 Jun 2008 12:28:07 +0200
10 MIME-Version: 1.0
10 MIME-Version: 1.0
11 Content-Type: text/plain;
11 Content-Type: text/plain;
12 format=flowed;
12 format=flowed;
13 charset="iso-8859-1";
13 charset="iso-8859-1";
14 reply-type=original
14 reply-type=original
15 Content-Transfer-Encoding: 7bit
15 Content-Transfer-Encoding: 7bit
16 X-Priority: 3
16 X-Priority: 3
17 X-MSMail-Priority: Normal
17 X-MSMail-Priority: Normal
18 X-Mailer: Microsoft Outlook Express 6.00.2900.2869
18 X-Mailer: Microsoft Outlook Express 6.00.2900.2869
19 X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869
19 X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869
20
20
21 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet
21 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas imperdiet
22 turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus
22 turpis et odio. Integer eget pede vel dolor euismod varius. Phasellus
23 blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti
23 blandit eleifend augue. Nulla facilisi. Duis id diam. Class aptent taciti
24 sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In
24 sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In
25 in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras
25 in urna sed tellus aliquet lobortis. Morbi scelerisque tortor in dolor. Cras
26 sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum
26 sagittis odio eu lacus. Aliquam sem tortor, consequat sit amet, vestibulum
27 id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus
27 id, iaculis at, lectus. Fusce tortor libero, congue ut, euismod nec, luctus
28 eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique
28 eget, eros. Pellentesque tortor enim, feugiat in, dignissim eget, tristique
29 sed, mauris. Pellentesque habitant morbi tristique senectus et netus et
29 sed, mauris. Pellentesque habitant morbi tristique senectus et netus et
30 malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse
30 malesuada fames ac turpis egestas. Quisque sit amet libero. In hac habitasse
31 platea dictumst.
31 platea dictumst.
32
32
33 Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque
33 Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque
34 sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem.
34 sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem.
35 Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et,
35 Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et,
36 dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed,
36 dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed,
37 massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo
37 massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo
38 pulvinar dui, a gravida orci mi eget odio. Nunc a lacus.
38 pulvinar dui, a gravida orci mi eget odio. Nunc a lacus.
39
39
40 Project: onlinestore
40 Project: onlinestore
41 Tracker: Feature request
41 Tracker: Feature request
42 category: Stock management
42 category: Stock management
43 assigned to: miscuser9@foo.bar
43 priority: foo
44 priority: foo
44 done ratio: x
45 done ratio: x
45 start date: some day
46 start date: some day
46 due date: never
47 due date: never
@@ -1,437 +1,439
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.dirname(__FILE__) + '/../test_helper'
20 require File.dirname(__FILE__) + '/../test_helper'
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 :issues,
29 :issues,
29 :issue_statuses,
30 :issue_statuses,
30 :workflows,
31 :workflows,
31 :trackers,
32 :trackers,
32 :projects_trackers,
33 :projects_trackers,
33 :versions,
34 :versions,
34 :enumerations,
35 :enumerations,
35 :issue_categories,
36 :issue_categories,
36 :custom_fields,
37 :custom_fields,
37 :custom_fields_trackers,
38 :custom_fields_trackers,
38 :boards,
39 :boards,
39 :messages
40 :messages
40
41
41 FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler'
42 FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler'
42
43
43 def setup
44 def setup
44 ActionMailer::Base.deliveries.clear
45 ActionMailer::Base.deliveries.clear
45 Setting.notified_events = Redmine::Notifiable.all.collect(&:name)
46 Setting.notified_events = Redmine::Notifiable.all.collect(&:name)
46 end
47 end
47
48
48 def test_add_issue
49 def test_add_issue
49 ActionMailer::Base.deliveries.clear
50 ActionMailer::Base.deliveries.clear
50 # This email contains: 'Project: onlinestore'
51 # This email contains: 'Project: onlinestore'
51 issue = submit_email('ticket_on_given_project.eml')
52 issue = submit_email('ticket_on_given_project.eml')
52 assert issue.is_a?(Issue)
53 assert issue.is_a?(Issue)
53 assert !issue.new_record?
54 assert !issue.new_record?
54 issue.reload
55 issue.reload
55 assert_equal 'New ticket on a given project', issue.subject
56 assert_equal 'New ticket on a given project', issue.subject
56 assert_equal User.find_by_login('jsmith'), issue.author
57 assert_equal User.find_by_login('jsmith'), issue.author
57 assert_equal Project.find(2), issue.project
58 assert_equal Project.find(2), issue.project
58 assert_equal IssueStatus.find_by_name('Resolved'), issue.status
59 assert_equal IssueStatus.find_by_name('Resolved'), issue.status
59 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
60 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
60 assert_equal '2010-01-01', issue.start_date.to_s
61 assert_equal '2010-01-01', issue.start_date.to_s
61 assert_equal '2010-12-31', issue.due_date.to_s
62 assert_equal '2010-12-31', issue.due_date.to_s
62 assert_equal User.find_by_login('jsmith'), issue.assigned_to
63 assert_equal User.find_by_login('jsmith'), issue.assigned_to
63 assert_equal Version.find_by_name('alpha'), issue.fixed_version
64 assert_equal Version.find_by_name('alpha'), issue.fixed_version
64 assert_equal 2.5, issue.estimated_hours
65 assert_equal 2.5, issue.estimated_hours
65 assert_equal 30, issue.done_ratio
66 assert_equal 30, issue.done_ratio
66 # keywords should be removed from the email body
67 # keywords should be removed from the email body
67 assert !issue.description.match(/^Project:/i)
68 assert !issue.description.match(/^Project:/i)
68 assert !issue.description.match(/^Status:/i)
69 assert !issue.description.match(/^Status:/i)
69 # Email notification should be sent
70 # Email notification should be sent
70 mail = ActionMailer::Base.deliveries.last
71 mail = ActionMailer::Base.deliveries.last
71 assert_not_nil mail
72 assert_not_nil mail
72 assert mail.subject.include?('New ticket on a given project')
73 assert mail.subject.include?('New ticket on a given project')
73 end
74 end
74
75
75 def test_add_issue_with_status
76 def test_add_issue_with_status
76 # This email contains: 'Project: onlinestore' and 'Status: Resolved'
77 # This email contains: 'Project: onlinestore' and 'Status: Resolved'
77 issue = submit_email('ticket_on_given_project.eml')
78 issue = submit_email('ticket_on_given_project.eml')
78 assert issue.is_a?(Issue)
79 assert issue.is_a?(Issue)
79 assert !issue.new_record?
80 assert !issue.new_record?
80 issue.reload
81 issue.reload
81 assert_equal Project.find(2), issue.project
82 assert_equal Project.find(2), issue.project
82 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
83 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
83 end
84 end
84
85
85 def test_add_issue_with_attributes_override
86 def test_add_issue_with_attributes_override
86 issue = submit_email('ticket_with_attributes.eml', :allow_override => 'tracker,category,priority')
87 issue = submit_email('ticket_with_attributes.eml', :allow_override => 'tracker,category,priority')
87 assert issue.is_a?(Issue)
88 assert issue.is_a?(Issue)
88 assert !issue.new_record?
89 assert !issue.new_record?
89 issue.reload
90 issue.reload
90 assert_equal 'New ticket on a given project', issue.subject
91 assert_equal 'New ticket on a given project', issue.subject
91 assert_equal User.find_by_login('jsmith'), issue.author
92 assert_equal User.find_by_login('jsmith'), issue.author
92 assert_equal Project.find(2), issue.project
93 assert_equal Project.find(2), issue.project
93 assert_equal 'Feature request', issue.tracker.to_s
94 assert_equal 'Feature request', issue.tracker.to_s
94 assert_equal 'Stock management', issue.category.to_s
95 assert_equal 'Stock management', issue.category.to_s
95 assert_equal 'Urgent', issue.priority.to_s
96 assert_equal 'Urgent', issue.priority.to_s
96 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
97 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
97 end
98 end
98
99
99 def test_add_issue_with_partial_attributes_override
100 def test_add_issue_with_partial_attributes_override
100 issue = submit_email('ticket_with_attributes.eml', :issue => {:priority => 'High'}, :allow_override => ['tracker'])
101 issue = submit_email('ticket_with_attributes.eml', :issue => {:priority => 'High'}, :allow_override => ['tracker'])
101 assert issue.is_a?(Issue)
102 assert issue.is_a?(Issue)
102 assert !issue.new_record?
103 assert !issue.new_record?
103 issue.reload
104 issue.reload
104 assert_equal 'New ticket on a given project', issue.subject
105 assert_equal 'New ticket on a given project', issue.subject
105 assert_equal User.find_by_login('jsmith'), issue.author
106 assert_equal User.find_by_login('jsmith'), issue.author
106 assert_equal Project.find(2), issue.project
107 assert_equal Project.find(2), issue.project
107 assert_equal 'Feature request', issue.tracker.to_s
108 assert_equal 'Feature request', issue.tracker.to_s
108 assert_nil issue.category
109 assert_nil issue.category
109 assert_equal 'High', issue.priority.to_s
110 assert_equal 'High', issue.priority.to_s
110 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
111 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
111 end
112 end
112
113
113 def test_add_issue_with_spaces_between_attribute_and_separator
114 def test_add_issue_with_spaces_between_attribute_and_separator
114 issue = submit_email('ticket_with_spaces_between_attribute_and_separator.eml', :allow_override => 'tracker,category,priority')
115 issue = submit_email('ticket_with_spaces_between_attribute_and_separator.eml', :allow_override => 'tracker,category,priority')
115 assert issue.is_a?(Issue)
116 assert issue.is_a?(Issue)
116 assert !issue.new_record?
117 assert !issue.new_record?
117 issue.reload
118 issue.reload
118 assert_equal 'New ticket on a given project', issue.subject
119 assert_equal 'New ticket on a given project', issue.subject
119 assert_equal User.find_by_login('jsmith'), issue.author
120 assert_equal User.find_by_login('jsmith'), issue.author
120 assert_equal Project.find(2), issue.project
121 assert_equal Project.find(2), issue.project
121 assert_equal 'Feature request', issue.tracker.to_s
122 assert_equal 'Feature request', issue.tracker.to_s
122 assert_equal 'Stock management', issue.category.to_s
123 assert_equal 'Stock management', issue.category.to_s
123 assert_equal 'Urgent', issue.priority.to_s
124 assert_equal 'Urgent', issue.priority.to_s
124 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
125 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
125 end
126 end
126
127
127
128
128 def test_add_issue_with_attachment_to_specific_project
129 def test_add_issue_with_attachment_to_specific_project
129 issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
130 issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
130 assert issue.is_a?(Issue)
131 assert issue.is_a?(Issue)
131 assert !issue.new_record?
132 assert !issue.new_record?
132 issue.reload
133 issue.reload
133 assert_equal 'Ticket created by email with attachment', issue.subject
134 assert_equal 'Ticket created by email with attachment', issue.subject
134 assert_equal User.find_by_login('jsmith'), issue.author
135 assert_equal User.find_by_login('jsmith'), issue.author
135 assert_equal Project.find(2), issue.project
136 assert_equal Project.find(2), issue.project
136 assert_equal 'This is a new ticket with attachments', issue.description
137 assert_equal 'This is a new ticket with attachments', issue.description
137 # Attachment properties
138 # Attachment properties
138 assert_equal 1, issue.attachments.size
139 assert_equal 1, issue.attachments.size
139 assert_equal 'Paella.jpg', issue.attachments.first.filename
140 assert_equal 'Paella.jpg', issue.attachments.first.filename
140 assert_equal 'image/jpeg', issue.attachments.first.content_type
141 assert_equal 'image/jpeg', issue.attachments.first.content_type
141 assert_equal 10790, issue.attachments.first.filesize
142 assert_equal 10790, issue.attachments.first.filesize
142 end
143 end
143
144
144 def test_add_issue_with_custom_fields
145 def test_add_issue_with_custom_fields
145 issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'onlinestore'})
146 issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'onlinestore'})
146 assert issue.is_a?(Issue)
147 assert issue.is_a?(Issue)
147 assert !issue.new_record?
148 assert !issue.new_record?
148 issue.reload
149 issue.reload
149 assert_equal 'New ticket with custom field values', issue.subject
150 assert_equal 'New ticket with custom field values', issue.subject
150 assert_equal 'Value for a custom field', issue.custom_value_for(CustomField.find_by_name('Searchable field')).value
151 assert_equal 'Value for a custom field', issue.custom_value_for(CustomField.find_by_name('Searchable field')).value
151 assert !issue.description.match(/^searchable field:/i)
152 assert !issue.description.match(/^searchable field:/i)
152 end
153 end
153
154
154 def test_add_issue_with_cc
155 def test_add_issue_with_cc
155 issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'})
156 issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'})
156 assert issue.is_a?(Issue)
157 assert issue.is_a?(Issue)
157 assert !issue.new_record?
158 assert !issue.new_record?
158 issue.reload
159 issue.reload
159 assert issue.watched_by?(User.find_by_mail('dlopper@somenet.foo'))
160 assert issue.watched_by?(User.find_by_mail('dlopper@somenet.foo'))
160 assert_equal 1, issue.watcher_user_ids.size
161 assert_equal 1, issue.watcher_user_ids.size
161 end
162 end
162
163
163 def test_add_issue_by_unknown_user
164 def test_add_issue_by_unknown_user
164 assert_no_difference 'User.count' do
165 assert_no_difference 'User.count' do
165 assert_equal false, submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'})
166 assert_equal false, submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'})
166 end
167 end
167 end
168 end
168
169
169 def test_add_issue_by_anonymous_user
170 def test_add_issue_by_anonymous_user
170 Role.anonymous.add_permission!(:add_issues)
171 Role.anonymous.add_permission!(:add_issues)
171 assert_no_difference 'User.count' do
172 assert_no_difference 'User.count' do
172 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'accept')
173 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'accept')
173 assert issue.is_a?(Issue)
174 assert issue.is_a?(Issue)
174 assert issue.author.anonymous?
175 assert issue.author.anonymous?
175 end
176 end
176 end
177 end
177
178
178 def test_add_issue_by_anonymous_user_with_no_from_address
179 def test_add_issue_by_anonymous_user_with_no_from_address
179 Role.anonymous.add_permission!(:add_issues)
180 Role.anonymous.add_permission!(:add_issues)
180 assert_no_difference 'User.count' do
181 assert_no_difference 'User.count' do
181 issue = submit_email('ticket_by_empty_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'accept')
182 issue = submit_email('ticket_by_empty_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'accept')
182 assert issue.is_a?(Issue)
183 assert issue.is_a?(Issue)
183 assert issue.author.anonymous?
184 assert issue.author.anonymous?
184 end
185 end
185 end
186 end
186
187
187 def test_add_issue_by_anonymous_user_on_private_project
188 def test_add_issue_by_anonymous_user_on_private_project
188 Role.anonymous.add_permission!(:add_issues)
189 Role.anonymous.add_permission!(:add_issues)
189 assert_no_difference 'User.count' do
190 assert_no_difference 'User.count' do
190 assert_no_difference 'Issue.count' do
191 assert_no_difference 'Issue.count' do
191 assert_equal false, submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'onlinestore'}, :unknown_user => 'accept')
192 assert_equal false, submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'onlinestore'}, :unknown_user => 'accept')
192 end
193 end
193 end
194 end
194 end
195 end
195
196
196 def test_add_issue_by_anonymous_user_on_private_project_without_permission_check
197 def test_add_issue_by_anonymous_user_on_private_project_without_permission_check
197 assert_no_difference 'User.count' do
198 assert_no_difference 'User.count' do
198 assert_difference 'Issue.count' do
199 assert_difference 'Issue.count' do
199 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'onlinestore'}, :no_permission_check => '1', :unknown_user => 'accept')
200 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'onlinestore'}, :no_permission_check => '1', :unknown_user => 'accept')
200 assert issue.is_a?(Issue)
201 assert issue.is_a?(Issue)
201 assert issue.author.anonymous?
202 assert issue.author.anonymous?
202 assert !issue.project.is_public?
203 assert !issue.project.is_public?
203 end
204 end
204 end
205 end
205 end
206 end
206
207
207 def test_add_issue_by_created_user
208 def test_add_issue_by_created_user
208 Setting.default_language = 'en'
209 Setting.default_language = 'en'
209 assert_difference 'User.count' do
210 assert_difference 'User.count' do
210 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'create')
211 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'create')
211 assert issue.is_a?(Issue)
212 assert issue.is_a?(Issue)
212 assert issue.author.active?
213 assert issue.author.active?
213 assert_equal 'john.doe@somenet.foo', issue.author.mail
214 assert_equal 'john.doe@somenet.foo', issue.author.mail
214 assert_equal 'John', issue.author.firstname
215 assert_equal 'John', issue.author.firstname
215 assert_equal 'Doe', issue.author.lastname
216 assert_equal 'Doe', issue.author.lastname
216
217
217 # account information
218 # account information
218 email = ActionMailer::Base.deliveries.first
219 email = ActionMailer::Base.deliveries.first
219 assert_not_nil email
220 assert_not_nil email
220 assert email.subject.include?('account activation')
221 assert email.subject.include?('account activation')
221 login = email.body.match(/\* Login: (.*)$/)[1]
222 login = email.body.match(/\* Login: (.*)$/)[1]
222 password = email.body.match(/\* Password: (.*)$/)[1]
223 password = email.body.match(/\* Password: (.*)$/)[1]
223 assert_equal issue.author, User.try_to_login(login, password)
224 assert_equal issue.author, User.try_to_login(login, password)
224 end
225 end
225 end
226 end
226
227
227 def test_add_issue_without_from_header
228 def test_add_issue_without_from_header
228 Role.anonymous.add_permission!(:add_issues)
229 Role.anonymous.add_permission!(:add_issues)
229 assert_equal false, submit_email('ticket_without_from_header.eml')
230 assert_equal false, submit_email('ticket_without_from_header.eml')
230 end
231 end
231
232
232 def test_add_issue_with_invalid_attributes
233 def test_add_issue_with_invalid_attributes
233 issue = submit_email('ticket_with_invalid_attributes.eml', :allow_override => 'tracker,category,priority')
234 issue = submit_email('ticket_with_invalid_attributes.eml', :allow_override => 'tracker,category,priority')
234 assert issue.is_a?(Issue)
235 assert issue.is_a?(Issue)
235 assert !issue.new_record?
236 assert !issue.new_record?
236 issue.reload
237 issue.reload
238 assert_nil issue.assigned_to
237 assert_nil issue.start_date
239 assert_nil issue.start_date
238 assert_nil issue.due_date
240 assert_nil issue.due_date
239 assert_equal 0, issue.done_ratio
241 assert_equal 0, issue.done_ratio
240 assert_equal 'Normal', issue.priority.to_s
242 assert_equal 'Normal', issue.priority.to_s
241 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
243 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
242 end
244 end
243
245
244 def test_add_issue_with_localized_attributes
246 def test_add_issue_with_localized_attributes
245 User.find_by_mail('jsmith@somenet.foo').update_attribute 'language', 'fr'
247 User.find_by_mail('jsmith@somenet.foo').update_attribute 'language', 'fr'
246 issue = submit_email('ticket_with_localized_attributes.eml', :allow_override => 'tracker,category,priority')
248 issue = submit_email('ticket_with_localized_attributes.eml', :allow_override => 'tracker,category,priority')
247 assert issue.is_a?(Issue)
249 assert issue.is_a?(Issue)
248 assert !issue.new_record?
250 assert !issue.new_record?
249 issue.reload
251 issue.reload
250 assert_equal 'New ticket on a given project', issue.subject
252 assert_equal 'New ticket on a given project', issue.subject
251 assert_equal User.find_by_login('jsmith'), issue.author
253 assert_equal User.find_by_login('jsmith'), issue.author
252 assert_equal Project.find(2), issue.project
254 assert_equal Project.find(2), issue.project
253 assert_equal 'Feature request', issue.tracker.to_s
255 assert_equal 'Feature request', issue.tracker.to_s
254 assert_equal 'Stock management', issue.category.to_s
256 assert_equal 'Stock management', issue.category.to_s
255 assert_equal 'Urgent', issue.priority.to_s
257 assert_equal 'Urgent', issue.priority.to_s
256 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
258 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
257 end
259 end
258
260
259 def test_add_issue_with_japanese_keywords
261 def test_add_issue_with_japanese_keywords
260 tracker = Tracker.create!(:name => 'ι–‹η™Ί')
262 tracker = Tracker.create!(:name => 'ι–‹η™Ί')
261 Project.find(1).trackers << tracker
263 Project.find(1).trackers << tracker
262 issue = submit_email('japanese_keywords_iso_2022_jp.eml', :issue => {:project => 'ecookbook'}, :allow_override => 'tracker')
264 issue = submit_email('japanese_keywords_iso_2022_jp.eml', :issue => {:project => 'ecookbook'}, :allow_override => 'tracker')
263 assert_kind_of Issue, issue
265 assert_kind_of Issue, issue
264 assert_equal tracker, issue.tracker
266 assert_equal tracker, issue.tracker
265 end
267 end
266
268
267 def test_should_ignore_emails_from_emission_address
269 def test_should_ignore_emails_from_emission_address
268 Role.anonymous.add_permission!(:add_issues)
270 Role.anonymous.add_permission!(:add_issues)
269 assert_no_difference 'User.count' do
271 assert_no_difference 'User.count' do
270 assert_equal false, submit_email('ticket_from_emission_address.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'create')
272 assert_equal false, submit_email('ticket_from_emission_address.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'create')
271 end
273 end
272 end
274 end
273
275
274 def test_add_issue_should_send_email_notification
276 def test_add_issue_should_send_email_notification
275 Setting.notified_events = ['issue_added']
277 Setting.notified_events = ['issue_added']
276 ActionMailer::Base.deliveries.clear
278 ActionMailer::Base.deliveries.clear
277 # This email contains: 'Project: onlinestore'
279 # This email contains: 'Project: onlinestore'
278 issue = submit_email('ticket_on_given_project.eml')
280 issue = submit_email('ticket_on_given_project.eml')
279 assert issue.is_a?(Issue)
281 assert issue.is_a?(Issue)
280 assert_equal 1, ActionMailer::Base.deliveries.size
282 assert_equal 1, ActionMailer::Base.deliveries.size
281 end
283 end
282
284
283 def test_add_issue_note
285 def test_add_issue_note
284 journal = submit_email('ticket_reply.eml')
286 journal = submit_email('ticket_reply.eml')
285 assert journal.is_a?(Journal)
287 assert journal.is_a?(Journal)
286 assert_equal User.find_by_login('jsmith'), journal.user
288 assert_equal User.find_by_login('jsmith'), journal.user
287 assert_equal Issue.find(2), journal.journalized
289 assert_equal Issue.find(2), journal.journalized
288 assert_match /This is reply/, journal.notes
290 assert_match /This is reply/, journal.notes
289 end
291 end
290
292
291 def test_add_issue_note_with_attribute_changes
293 def test_add_issue_note_with_attribute_changes
292 # This email contains: 'Status: Resolved'
294 # This email contains: 'Status: Resolved'
293 journal = submit_email('ticket_reply_with_status.eml')
295 journal = submit_email('ticket_reply_with_status.eml')
294 assert journal.is_a?(Journal)
296 assert journal.is_a?(Journal)
295 issue = Issue.find(journal.issue.id)
297 issue = Issue.find(journal.issue.id)
296 assert_equal User.find_by_login('jsmith'), journal.user
298 assert_equal User.find_by_login('jsmith'), journal.user
297 assert_equal Issue.find(2), journal.journalized
299 assert_equal Issue.find(2), journal.journalized
298 assert_match /This is reply/, journal.notes
300 assert_match /This is reply/, journal.notes
299 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
301 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
300 assert_equal '2010-01-01', issue.start_date.to_s
302 assert_equal '2010-01-01', issue.start_date.to_s
301 assert_equal '2010-12-31', issue.due_date.to_s
303 assert_equal '2010-12-31', issue.due_date.to_s
302 assert_equal User.find_by_login('jsmith'), issue.assigned_to
304 assert_equal User.find_by_login('jsmith'), issue.assigned_to
303 assert_equal 'Updated custom value', issue.custom_value_for(CustomField.find_by_name('Searchable field')).value
305 assert_equal 'Updated custom value', issue.custom_value_for(CustomField.find_by_name('Searchable field')).value
304 end
306 end
305
307
306 def test_add_issue_note_should_send_email_notification
308 def test_add_issue_note_should_send_email_notification
307 ActionMailer::Base.deliveries.clear
309 ActionMailer::Base.deliveries.clear
308 journal = submit_email('ticket_reply.eml')
310 journal = submit_email('ticket_reply.eml')
309 assert journal.is_a?(Journal)
311 assert journal.is_a?(Journal)
310 assert_equal 1, ActionMailer::Base.deliveries.size
312 assert_equal 1, ActionMailer::Base.deliveries.size
311 end
313 end
312
314
313 def test_reply_to_a_message
315 def test_reply_to_a_message
314 m = submit_email('message_reply.eml')
316 m = submit_email('message_reply.eml')
315 assert m.is_a?(Message)
317 assert m.is_a?(Message)
316 assert !m.new_record?
318 assert !m.new_record?
317 m.reload
319 m.reload
318 assert_equal 'Reply via email', m.subject
320 assert_equal 'Reply via email', m.subject
319 # The email replies to message #2 which is part of the thread of message #1
321 # The email replies to message #2 which is part of the thread of message #1
320 assert_equal Message.find(1), m.parent
322 assert_equal Message.find(1), m.parent
321 end
323 end
322
324
323 def test_reply_to_a_message_by_subject
325 def test_reply_to_a_message_by_subject
324 m = submit_email('message_reply_by_subject.eml')
326 m = submit_email('message_reply_by_subject.eml')
325 assert m.is_a?(Message)
327 assert m.is_a?(Message)
326 assert !m.new_record?
328 assert !m.new_record?
327 m.reload
329 m.reload
328 assert_equal 'Reply to the first post', m.subject
330 assert_equal 'Reply to the first post', m.subject
329 assert_equal Message.find(1), m.parent
331 assert_equal Message.find(1), m.parent
330 end
332 end
331
333
332 def test_should_strip_tags_of_html_only_emails
334 def test_should_strip_tags_of_html_only_emails
333 issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'})
335 issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'})
334 assert issue.is_a?(Issue)
336 assert issue.is_a?(Issue)
335 assert !issue.new_record?
337 assert !issue.new_record?
336 issue.reload
338 issue.reload
337 assert_equal 'HTML email', issue.subject
339 assert_equal 'HTML email', issue.subject
338 assert_equal 'This is a html-only email.', issue.description
340 assert_equal 'This is a html-only email.', issue.description
339 end
341 end
340
342
341 context "truncate emails based on the Setting" do
343 context "truncate emails based on the Setting" do
342 context "with no setting" do
344 context "with no setting" do
343 setup do
345 setup do
344 Setting.mail_handler_body_delimiters = ''
346 Setting.mail_handler_body_delimiters = ''
345 end
347 end
346
348
347 should "add the entire email into the issue" do
349 should "add the entire email into the issue" do
348 issue = submit_email('ticket_on_given_project.eml')
350 issue = submit_email('ticket_on_given_project.eml')
349 assert_issue_created(issue)
351 assert_issue_created(issue)
350 assert issue.description.include?('---')
352 assert issue.description.include?('---')
351 assert issue.description.include?('This paragraph is after the delimiter')
353 assert issue.description.include?('This paragraph is after the delimiter')
352 end
354 end
353 end
355 end
354
356
355 context "with a single string" do
357 context "with a single string" do
356 setup do
358 setup do
357 Setting.mail_handler_body_delimiters = '---'
359 Setting.mail_handler_body_delimiters = '---'
358 end
360 end
359
361
360 should "truncate the email at the delimiter for the issue" do
362 should "truncate the email at the delimiter for the issue" do
361 issue = submit_email('ticket_on_given_project.eml')
363 issue = submit_email('ticket_on_given_project.eml')
362 assert_issue_created(issue)
364 assert_issue_created(issue)
363 assert issue.description.include?('This paragraph is before delimiters')
365 assert issue.description.include?('This paragraph is before delimiters')
364 assert issue.description.include?('--- This line starts with a delimiter')
366 assert issue.description.include?('--- This line starts with a delimiter')
365 assert !issue.description.match(/^---$/)
367 assert !issue.description.match(/^---$/)
366 assert !issue.description.include?('This paragraph is after the delimiter')
368 assert !issue.description.include?('This paragraph is after the delimiter')
367 end
369 end
368 end
370 end
369
371
370 context "with a single quoted reply (e.g. reply to a Redmine email notification)" do
372 context "with a single quoted reply (e.g. reply to a Redmine email notification)" do
371 setup do
373 setup do
372 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
374 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
373 end
375 end
374
376
375 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
377 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
376 journal = submit_email('issue_update_with_quoted_reply_above.eml')
378 journal = submit_email('issue_update_with_quoted_reply_above.eml')
377 assert journal.is_a?(Journal)
379 assert journal.is_a?(Journal)
378 assert journal.notes.include?('An update to the issue by the sender.')
380 assert journal.notes.include?('An update to the issue by the sender.')
379 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
381 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
380 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
382 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
381
383
382 end
384 end
383
385
384 end
386 end
385
387
386 context "with multiple quoted replies (e.g. reply to a reply of a Redmine email notification)" do
388 context "with multiple quoted replies (e.g. reply to a reply of a Redmine email notification)" do
387 setup do
389 setup do
388 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
390 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
389 end
391 end
390
392
391 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
393 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
392 journal = submit_email('issue_update_with_multiple_quoted_reply_above.eml')
394 journal = submit_email('issue_update_with_multiple_quoted_reply_above.eml')
393 assert journal.is_a?(Journal)
395 assert journal.is_a?(Journal)
394 assert journal.notes.include?('An update to the issue by the sender.')
396 assert journal.notes.include?('An update to the issue by the sender.')
395 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
397 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
396 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
398 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
397
399
398 end
400 end
399
401
400 end
402 end
401
403
402 context "with multiple strings" do
404 context "with multiple strings" do
403 setup do
405 setup do
404 Setting.mail_handler_body_delimiters = "---\nBREAK"
406 Setting.mail_handler_body_delimiters = "---\nBREAK"
405 end
407 end
406
408
407 should "truncate the email at the first delimiter found (BREAK)" do
409 should "truncate the email at the first delimiter found (BREAK)" do
408 issue = submit_email('ticket_on_given_project.eml')
410 issue = submit_email('ticket_on_given_project.eml')
409 assert_issue_created(issue)
411 assert_issue_created(issue)
410 assert issue.description.include?('This paragraph is before delimiters')
412 assert issue.description.include?('This paragraph is before delimiters')
411 assert !issue.description.include?('BREAK')
413 assert !issue.description.include?('BREAK')
412 assert !issue.description.include?('This paragraph is between delimiters')
414 assert !issue.description.include?('This paragraph is between delimiters')
413 assert !issue.description.match(/^---$/)
415 assert !issue.description.match(/^---$/)
414 assert !issue.description.include?('This paragraph is after the delimiter')
416 assert !issue.description.include?('This paragraph is after the delimiter')
415 end
417 end
416 end
418 end
417 end
419 end
418
420
419 def test_email_with_long_subject_line
421 def test_email_with_long_subject_line
420 issue = submit_email('ticket_with_long_subject.eml')
422 issue = submit_email('ticket_with_long_subject.eml')
421 assert issue.is_a?(Issue)
423 assert issue.is_a?(Issue)
422 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]
424 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]
423 end
425 end
424
426
425 private
427 private
426
428
427 def submit_email(filename, options={})
429 def submit_email(filename, options={})
428 raw = IO.read(File.join(FIXTURES_PATH, filename))
430 raw = IO.read(File.join(FIXTURES_PATH, filename))
429 MailHandler.receive(raw, options)
431 MailHandler.receive(raw, options)
430 end
432 end
431
433
432 def assert_issue_created(issue)
434 def assert_issue_created(issue)
433 assert issue.is_a?(Issue)
435 assert issue.is_a?(Issue)
434 assert !issue.new_record?
436 assert !issue.new_record?
435 issue.reload
437 issue.reload
436 end
438 end
437 end
439 end
General Comments 0
You need to be logged in to leave comments. Login now