##// END OF EJS Templates
Add an "Assigned To" keyword to receiving email. #5594...
Eric Davis -
r3650:e94c45d54841
parent child
Show More
@@ -1,323 +1,336
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
20
21 class UnauthorizedAction < StandardError; end
21 class UnauthorizedAction < StandardError; end
22 class MissingInformation < StandardError; end
22 class MissingInformation < StandardError; end
23
23
24 attr_reader :email, :user
24 attr_reader :email, :user
25
25
26 def self.receive(email, options={})
26 def self.receive(email, options={})
27 @@handler_options = options.dup
27 @@handler_options = options.dup
28
28
29 @@handler_options[:issue] ||= {}
29 @@handler_options[:issue] ||= {}
30
30
31 @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip) if @@handler_options[:allow_override].is_a?(String)
31 @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip) if @@handler_options[:allow_override].is_a?(String)
32 @@handler_options[:allow_override] ||= []
32 @@handler_options[:allow_override] ||= []
33 # Project needs to be overridable if not specified
33 # Project needs to be overridable if not specified
34 @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
34 @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
35 # Status overridable by default
35 # Status overridable by default
36 @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
36 @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
37
37
38 @@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1' ? true : false)
38 @@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1' ? true : false)
39 super email
39 super email
40 end
40 end
41
41
42 # Processes incoming emails
42 # Processes incoming emails
43 # Returns the created object (eg. an issue, a message) or false
43 # Returns the created object (eg. an issue, a message) or false
44 def receive(email)
44 def receive(email)
45 @email = email
45 @email = email
46 sender_email = email.from.to_a.first.to_s.strip
46 sender_email = email.from.to_a.first.to_s.strip
47 # Ignore emails received from the application emission address to avoid hell cycles
47 # Ignore emails received from the application emission address to avoid hell cycles
48 if sender_email.downcase == Setting.mail_from.to_s.strip.downcase
48 if sender_email.downcase == Setting.mail_from.to_s.strip.downcase
49 logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]" if logger && logger.info
49 logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]" if logger && logger.info
50 return false
50 return false
51 end
51 end
52 @user = User.find_by_mail(sender_email)
52 @user = User.find_by_mail(sender_email)
53 if @user && !@user.active?
53 if @user && !@user.active?
54 logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]" if logger && logger.info
54 logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]" if logger && logger.info
55 return false
55 return false
56 end
56 end
57 if @user.nil?
57 if @user.nil?
58 # Email was submitted by an unknown user
58 # Email was submitted by an unknown user
59 case @@handler_options[:unknown_user]
59 case @@handler_options[:unknown_user]
60 when 'accept'
60 when 'accept'
61 @user = User.anonymous
61 @user = User.anonymous
62 when 'create'
62 when 'create'
63 @user = MailHandler.create_user_from_email(email)
63 @user = MailHandler.create_user_from_email(email)
64 if @user
64 if @user
65 logger.info "MailHandler: [#{@user.login}] account created" if logger && logger.info
65 logger.info "MailHandler: [#{@user.login}] account created" if logger && logger.info
66 Mailer.deliver_account_information(@user, @user.password)
66 Mailer.deliver_account_information(@user, @user.password)
67 else
67 else
68 logger.error "MailHandler: could not create account for [#{sender_email}]" if logger && logger.error
68 logger.error "MailHandler: could not create account for [#{sender_email}]" if logger && logger.error
69 return false
69 return false
70 end
70 end
71 else
71 else
72 # Default behaviour, emails from unknown users are ignored
72 # Default behaviour, emails from unknown users are ignored
73 logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]" if logger && logger.info
73 logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]" if logger && logger.info
74 return false
74 return false
75 end
75 end
76 end
76 end
77 User.current = @user
77 User.current = @user
78 dispatch
78 dispatch
79 end
79 end
80
80
81 private
81 private
82
82
83 MESSAGE_ID_RE = %r{^<redmine\.([a-z0-9_]+)\-(\d+)\.\d+@}
83 MESSAGE_ID_RE = %r{^<redmine\.([a-z0-9_]+)\-(\d+)\.\d+@}
84 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]*#(\d+)\]}
84 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]*#(\d+)\]}
85 MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
85 MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
86
86
87 def dispatch
87 def dispatch
88 headers = [email.in_reply_to, email.references].flatten.compact
88 headers = [email.in_reply_to, email.references].flatten.compact
89 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
89 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
90 klass, object_id = $1, $2.to_i
90 klass, object_id = $1, $2.to_i
91 method_name = "receive_#{klass}_reply"
91 method_name = "receive_#{klass}_reply"
92 if self.class.private_instance_methods.collect(&:to_s).include?(method_name)
92 if self.class.private_instance_methods.collect(&:to_s).include?(method_name)
93 send method_name, object_id
93 send method_name, object_id
94 else
94 else
95 # ignoring it
95 # ignoring it
96 end
96 end
97 elsif m = email.subject.match(ISSUE_REPLY_SUBJECT_RE)
97 elsif m = email.subject.match(ISSUE_REPLY_SUBJECT_RE)
98 receive_issue_reply(m[1].to_i)
98 receive_issue_reply(m[1].to_i)
99 elsif m = email.subject.match(MESSAGE_REPLY_SUBJECT_RE)
99 elsif m = email.subject.match(MESSAGE_REPLY_SUBJECT_RE)
100 receive_message_reply(m[1].to_i)
100 receive_message_reply(m[1].to_i)
101 else
101 else
102 receive_issue
102 receive_issue
103 end
103 end
104 rescue ActiveRecord::RecordInvalid => e
104 rescue ActiveRecord::RecordInvalid => e
105 # TODO: send a email to the user
105 # TODO: send a email to the user
106 logger.error e.message if logger
106 logger.error e.message if logger
107 false
107 false
108 rescue MissingInformation => e
108 rescue MissingInformation => e
109 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
109 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
110 false
110 false
111 rescue UnauthorizedAction => e
111 rescue UnauthorizedAction => e
112 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
112 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
113 false
113 false
114 end
114 end
115
115
116 # Creates a new issue
116 # Creates a new issue
117 def receive_issue
117 def receive_issue
118 project = target_project
118 project = target_project
119 tracker = (get_keyword(:tracker) && project.trackers.find_by_name(get_keyword(:tracker))) || project.trackers.find(:first)
119 tracker = (get_keyword(:tracker) && project.trackers.find_by_name(get_keyword(:tracker))) || project.trackers.find(:first)
120 category = (get_keyword(:category) && project.issue_categories.find_by_name(get_keyword(:category)))
120 category = (get_keyword(:category) && project.issue_categories.find_by_name(get_keyword(:category)))
121 priority = (get_keyword(:priority) && IssuePriority.find_by_name(get_keyword(:priority)))
121 priority = (get_keyword(:priority) && IssuePriority.find_by_name(get_keyword(:priority)))
122 status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
122 status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
123 assigned_to = (get_keyword(:assigned_to, :override => true) && find_user_from_keyword(get_keyword(:assigned_to, :override => true)))
123 due_date = get_keyword(:due_date, :override => true)
124 due_date = get_keyword(:due_date, :override => true)
124 start_date = get_keyword(:start_date, :override => true)
125 start_date = get_keyword(:start_date, :override => true)
125
126
126 # check permission
127 # check permission
127 unless @@handler_options[:no_permission_check]
128 unless @@handler_options[:no_permission_check]
128 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
129 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
129 end
130 end
130
131
131 issue = Issue.new(:author => user, :project => project, :tracker => tracker, :category => category, :priority => priority, :due_date => due_date, :start_date => start_date)
132 issue = Issue.new(:author => user, :project => project, :tracker => tracker, :category => category, :priority => priority, :due_date => due_date, :start_date => start_date, :assigned_to => assigned_to)
132 # check workflow
133 # check workflow
133 if status && issue.new_statuses_allowed_to(user).include?(status)
134 if status && issue.new_statuses_allowed_to(user).include?(status)
134 issue.status = status
135 issue.status = status
135 end
136 end
136 issue.subject = email.subject.chomp
137 issue.subject = email.subject.chomp
137 if issue.subject.blank?
138 if issue.subject.blank?
138 issue.subject = '(no subject)'
139 issue.subject = '(no subject)'
139 end
140 end
140 # custom fields
141 # custom fields
141 issue.custom_field_values = issue.available_custom_fields.inject({}) do |h, c|
142 issue.custom_field_values = issue.available_custom_fields.inject({}) do |h, c|
142 if value = get_keyword(c.name, :override => true)
143 if value = get_keyword(c.name, :override => true)
143 h[c.id] = value
144 h[c.id] = value
144 end
145 end
145 h
146 h
146 end
147 end
147 issue.description = cleaned_up_text_body
148 issue.description = cleaned_up_text_body
148 # add To and Cc as watchers before saving so the watchers can reply to Redmine
149 # add To and Cc as watchers before saving so the watchers can reply to Redmine
149 add_watchers(issue)
150 add_watchers(issue)
150 issue.save!
151 issue.save!
151 add_attachments(issue)
152 add_attachments(issue)
152 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
153 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
153 issue
154 issue
154 end
155 end
155
156
156 def target_project
157 def target_project
157 # TODO: other ways to specify project:
158 # TODO: other ways to specify project:
158 # * parse the email To field
159 # * parse the email To field
159 # * specific project (eg. Setting.mail_handler_target_project)
160 # * specific project (eg. Setting.mail_handler_target_project)
160 target = Project.find_by_identifier(get_keyword(:project))
161 target = Project.find_by_identifier(get_keyword(:project))
161 raise MissingInformation.new('Unable to determine target project') if target.nil?
162 raise MissingInformation.new('Unable to determine target project') if target.nil?
162 target
163 target
163 end
164 end
164
165
165 # Adds a note to an existing issue
166 # Adds a note to an existing issue
166 def receive_issue_reply(issue_id)
167 def receive_issue_reply(issue_id)
167 status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
168 status = (get_keyword(:status) && IssueStatus.find_by_name(get_keyword(:status)))
168 due_date = get_keyword(:due_date, :override => true)
169 due_date = get_keyword(:due_date, :override => true)
169 start_date = get_keyword(:start_date, :override => true)
170 start_date = get_keyword(:start_date, :override => true)
171 assigned_to = (get_keyword(:assigned_to, :override => true) && find_user_from_keyword(get_keyword(:assigned_to, :override => true)))
170
172
171 issue = Issue.find_by_id(issue_id)
173 issue = Issue.find_by_id(issue_id)
172 return unless issue
174 return unless issue
173 # check permission
175 # check permission
174 unless @@handler_options[:no_permission_check]
176 unless @@handler_options[:no_permission_check]
175 raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project)
177 raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project)
176 raise UnauthorizedAction unless status.nil? || user.allowed_to?(:edit_issues, issue.project)
178 raise UnauthorizedAction unless status.nil? || user.allowed_to?(:edit_issues, issue.project)
177 end
179 end
178
180
179 # add the note
181 # add the note
180 journal = issue.init_journal(user, cleaned_up_text_body)
182 journal = issue.init_journal(user, cleaned_up_text_body)
181 add_attachments(issue)
183 add_attachments(issue)
182 # check workflow
184 # check workflow
183 if status && issue.new_statuses_allowed_to(user).include?(status)
185 if status && issue.new_statuses_allowed_to(user).include?(status)
184 issue.status = status
186 issue.status = status
185 end
187 end
186 issue.start_date = start_date if start_date
188 issue.start_date = start_date if start_date
187 issue.due_date = due_date if due_date
189 issue.due_date = due_date if due_date
190 issue.assigned_to = assigned_to if assigned_to
188
191
189 issue.save!
192 issue.save!
190 logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info
193 logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info
191 journal
194 journal
192 end
195 end
193
196
194 # Reply will be added to the issue
197 # Reply will be added to the issue
195 def receive_journal_reply(journal_id)
198 def receive_journal_reply(journal_id)
196 journal = Journal.find_by_id(journal_id)
199 journal = Journal.find_by_id(journal_id)
197 if journal && journal.journalized_type == 'Issue'
200 if journal && journal.journalized_type == 'Issue'
198 receive_issue_reply(journal.journalized_id)
201 receive_issue_reply(journal.journalized_id)
199 end
202 end
200 end
203 end
201
204
202 # Receives a reply to a forum message
205 # Receives a reply to a forum message
203 def receive_message_reply(message_id)
206 def receive_message_reply(message_id)
204 message = Message.find_by_id(message_id)
207 message = Message.find_by_id(message_id)
205 if message
208 if message
206 message = message.root
209 message = message.root
207
210
208 unless @@handler_options[:no_permission_check]
211 unless @@handler_options[:no_permission_check]
209 raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
212 raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
210 end
213 end
211
214
212 if !message.locked?
215 if !message.locked?
213 reply = Message.new(:subject => email.subject.gsub(%r{^.*msg\d+\]}, '').strip,
216 reply = Message.new(:subject => email.subject.gsub(%r{^.*msg\d+\]}, '').strip,
214 :content => cleaned_up_text_body)
217 :content => cleaned_up_text_body)
215 reply.author = user
218 reply.author = user
216 reply.board = message.board
219 reply.board = message.board
217 message.children << reply
220 message.children << reply
218 add_attachments(reply)
221 add_attachments(reply)
219 reply
222 reply
220 else
223 else
221 logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic" if logger && logger.info
224 logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic" if logger && logger.info
222 end
225 end
223 end
226 end
224 end
227 end
225
228
226 def add_attachments(obj)
229 def add_attachments(obj)
227 if email.has_attachments?
230 if email.has_attachments?
228 email.attachments.each do |attachment|
231 email.attachments.each do |attachment|
229 Attachment.create(:container => obj,
232 Attachment.create(:container => obj,
230 :file => attachment,
233 :file => attachment,
231 :author => user,
234 :author => user,
232 :content_type => attachment.content_type)
235 :content_type => attachment.content_type)
233 end
236 end
234 end
237 end
235 end
238 end
236
239
237 # Adds To and Cc as watchers of the given object if the sender has the
240 # Adds To and Cc as watchers of the given object if the sender has the
238 # appropriate permission
241 # appropriate permission
239 def add_watchers(obj)
242 def add_watchers(obj)
240 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
243 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
241 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
244 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
242 unless addresses.empty?
245 unless addresses.empty?
243 watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
246 watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
244 watchers.each {|w| obj.add_watcher(w)}
247 watchers.each {|w| obj.add_watcher(w)}
245 end
248 end
246 end
249 end
247 end
250 end
248
251
249 def get_keyword(attr, options={})
252 def get_keyword(attr, options={})
250 @keywords ||= {}
253 @keywords ||= {}
251 if @keywords.has_key?(attr)
254 if @keywords.has_key?(attr)
252 @keywords[attr]
255 @keywords[attr]
253 else
256 else
254 @keywords[attr] = begin
257 @keywords[attr] = begin
255 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && plain_text_body.gsub!(/^#{attr.to_s.humanize}[ \t]*:[ \t]*(.+)\s*$/i, '')
258 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && plain_text_body.gsub!(/^#{attr.to_s.humanize}[ \t]*:[ \t]*(.+)\s*$/i, '')
256 $1.strip
259 $1.strip
257 elsif !@@handler_options[:issue][attr].blank?
260 elsif !@@handler_options[:issue][attr].blank?
258 @@handler_options[:issue][attr]
261 @@handler_options[:issue][attr]
259 end
262 end
260 end
263 end
261 end
264 end
262 end
265 end
263
266
264 # Returns the text/plain part of the email
267 # Returns the text/plain part of the email
265 # If not found (eg. HTML-only email), returns the body with tags removed
268 # If not found (eg. HTML-only email), returns the body with tags removed
266 def plain_text_body
269 def plain_text_body
267 return @plain_text_body unless @plain_text_body.nil?
270 return @plain_text_body unless @plain_text_body.nil?
268 parts = @email.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten
271 parts = @email.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten
269 if parts.empty?
272 if parts.empty?
270 parts << @email
273 parts << @email
271 end
274 end
272 plain_text_part = parts.detect {|p| p.content_type == 'text/plain'}
275 plain_text_part = parts.detect {|p| p.content_type == 'text/plain'}
273 if plain_text_part.nil?
276 if plain_text_part.nil?
274 # no text/plain part found, assuming html-only email
277 # no text/plain part found, assuming html-only email
275 # strip html tags and remove doctype directive
278 # strip html tags and remove doctype directive
276 @plain_text_body = strip_tags(@email.body.to_s)
279 @plain_text_body = strip_tags(@email.body.to_s)
277 @plain_text_body.gsub! %r{^<!DOCTYPE .*$}, ''
280 @plain_text_body.gsub! %r{^<!DOCTYPE .*$}, ''
278 else
281 else
279 @plain_text_body = plain_text_part.body.to_s
282 @plain_text_body = plain_text_part.body.to_s
280 end
283 end
281 @plain_text_body.strip!
284 @plain_text_body.strip!
282 @plain_text_body
285 @plain_text_body
283 end
286 end
284
287
285 def cleaned_up_text_body
288 def cleaned_up_text_body
286 cleanup_body(plain_text_body)
289 cleanup_body(plain_text_body)
287 end
290 end
288
291
289 def self.full_sanitizer
292 def self.full_sanitizer
290 @full_sanitizer ||= HTML::FullSanitizer.new
293 @full_sanitizer ||= HTML::FullSanitizer.new
291 end
294 end
292
295
293 # Creates a user account for the +email+ sender
296 # Creates a user account for the +email+ sender
294 def self.create_user_from_email(email)
297 def self.create_user_from_email(email)
295 addr = email.from_addrs.to_a.first
298 addr = email.from_addrs.to_a.first
296 if addr && !addr.spec.blank?
299 if addr && !addr.spec.blank?
297 user = User.new
300 user = User.new
298 user.mail = addr.spec
301 user.mail = addr.spec
299
302
300 names = addr.name.blank? ? addr.spec.gsub(/@.*$/, '').split('.') : addr.name.split
303 names = addr.name.blank? ? addr.spec.gsub(/@.*$/, '').split('.') : addr.name.split
301 user.firstname = names.shift
304 user.firstname = names.shift
302 user.lastname = names.join(' ')
305 user.lastname = names.join(' ')
303 user.lastname = '-' if user.lastname.blank?
306 user.lastname = '-' if user.lastname.blank?
304
307
305 user.login = user.mail
308 user.login = user.mail
306 user.password = ActiveSupport::SecureRandom.hex(5)
309 user.password = ActiveSupport::SecureRandom.hex(5)
307 user.language = Setting.default_language
310 user.language = Setting.default_language
308 user.save ? user : nil
311 user.save ? user : nil
309 end
312 end
310 end
313 end
311
314
312 private
315 private
313
316
314 # Removes the email body of text after the truncation configurations.
317 # Removes the email body of text after the truncation configurations.
315 def cleanup_body(body)
318 def cleanup_body(body)
316 delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
319 delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
317 unless delimiters.empty?
320 unless delimiters.empty?
318 regex = Regexp.new("^(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
321 regex = Regexp.new("^(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
319 body = body.gsub(regex, '')
322 body = body.gsub(regex, '')
320 end
323 end
321 body.strip
324 body.strip
322 end
325 end
326
327 def find_user_from_keyword(keyword)
328 user ||= User.find_by_mail(keyword)
329 user ||= User.find_by_login(keyword)
330 if user.nil? && keyword.match(/ /)
331 firstname, lastname = *(keyword.split) # "First Last Throwaway"
332 user ||= User.find_by_firstname_and_lastname(firstname, lastname)
333 end
334 user
335 end
323 end
336 end
@@ -1,55 +1,57
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 --- This line starts with a delimiter and should not be stripped
33 --- This line starts with a delimiter and should not be stripped
34
34
35 This paragraph is before delimiters.
35 This paragraph is before delimiters.
36
36
37 BREAK
37 BREAK
38
38
39 This paragraph is between delimiters.
39 This paragraph is between delimiters.
40
40
41 ---
41 ---
42
42
43 This paragraph is after the delimiter so it shouldn't appear.
43 This paragraph is after the delimiter so it shouldn't appear.
44
44
45 Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque
45 Nulla et nunc. Duis pede. Donec et ipsum. Nam ut dui tincidunt neque
46 sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem.
46 sollicitudin iaculis. Duis vitae dolor. Vestibulum eget massa. Sed lorem.
47 Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et,
47 Nullam volutpat cursus erat. Cras felis dolor, lacinia quis, rutrum et,
48 dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed,
48 dictum et, ligula. Sed erat nibh, gravida in, accumsan non, placerat sed,
49 massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo
49 massa. Sed sodales, ante fermentum ultricies sollicitudin, massa leo
50 pulvinar dui, a gravida orci mi eget odio. Nunc a lacus.
50 pulvinar dui, a gravida orci mi eget odio. Nunc a lacus.
51
51
52 Project: onlinestore
52 Project: onlinestore
53 Status: Resolved
53 Status: Resolved
54 due date: 2010-12-31
54 due date: 2010-12-31
55 Start Date:2010-01-01
55 Start Date:2010-01-01
56 Assigned to: John Smith
57
@@ -1,78 +1,79
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 ; Sat, 21 Jun 2008 18:41:39 +0200
4 with hMailServer ; Sat, 21 Jun 2008 18:41:39 +0200
5 Message-ID: <006a01c8d3bd$ad9baec0$0a00a8c0@osiris>
5 Message-ID: <006a01c8d3bd$ad9baec0$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 References: <485d0ad366c88_d7014663a025f@osiris.tmail>
8 References: <485d0ad366c88_d7014663a025f@osiris.tmail>
9 Subject: Re: [Cookbook - Feature #2] (New) Add ingredients categories
9 Subject: Re: [Cookbook - Feature #2] (New) Add ingredients categories
10 Date: Sat, 21 Jun 2008 18:41:39 +0200
10 Date: Sat, 21 Jun 2008 18:41:39 +0200
11 MIME-Version: 1.0
11 MIME-Version: 1.0
12 Content-Type: multipart/alternative;
12 Content-Type: multipart/alternative;
13 boundary="----=_NextPart_000_0067_01C8D3CE.711F9CC0"
13 boundary="----=_NextPart_000_0067_01C8D3CE.711F9CC0"
14 X-Priority: 3
14 X-Priority: 3
15 X-MSMail-Priority: Normal
15 X-MSMail-Priority: Normal
16 X-Mailer: Microsoft Outlook Express 6.00.2900.2869
16 X-Mailer: Microsoft Outlook Express 6.00.2900.2869
17 X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869
17 X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.2869
18
18
19 This is a multi-part message in MIME format.
19 This is a multi-part message in MIME format.
20
20
21 ------=_NextPart_000_0067_01C8D3CE.711F9CC0
21 ------=_NextPart_000_0067_01C8D3CE.711F9CC0
22 Content-Type: text/plain;
22 Content-Type: text/plain;
23 charset="utf-8"
23 charset="utf-8"
24 Content-Transfer-Encoding: quoted-printable
24 Content-Transfer-Encoding: quoted-printable
25
25
26 This is reply
26 This is reply
27
27
28 Status: Resolved
28 Status: Resolved
29 due date: 2010-12-31
29 due date: 2010-12-31
30 Start Date:2010-01-01
30 Start Date:2010-01-01
31 Assigned to: jsmith@somenet.foo
31
32
32 ------=_NextPart_000_0067_01C8D3CE.711F9CC0
33 ------=_NextPart_000_0067_01C8D3CE.711F9CC0
33 Content-Type: text/html;
34 Content-Type: text/html;
34 charset="utf-8"
35 charset="utf-8"
35 Content-Transfer-Encoding: quoted-printable
36 Content-Transfer-Encoding: quoted-printable
36
37
37 =EF=BB=BF<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
38 =EF=BB=BF<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
38 <HTML><HEAD>
39 <HTML><HEAD>
39 <META http-equiv=3DContent-Type content=3D"text/html; charset=3Dutf-8">
40 <META http-equiv=3DContent-Type content=3D"text/html; charset=3Dutf-8">
40 <STYLE>BODY {
41 <STYLE>BODY {
41 FONT-SIZE: 0.8em; COLOR: #484848; FONT-FAMILY: Verdana, sans-serif
42 FONT-SIZE: 0.8em; COLOR: #484848; FONT-FAMILY: Verdana, sans-serif
42 }
43 }
43 BODY H1 {
44 BODY H1 {
44 FONT-SIZE: 1.2em; MARGIN: 0px; FONT-FAMILY: "Trebuchet MS", Verdana, =
45 FONT-SIZE: 1.2em; MARGIN: 0px; FONT-FAMILY: "Trebuchet MS", Verdana, =
45 sans-serif
46 sans-serif
46 }
47 }
47 A {
48 A {
48 COLOR: #2a5685
49 COLOR: #2a5685
49 }
50 }
50 A:link {
51 A:link {
51 COLOR: #2a5685
52 COLOR: #2a5685
52 }
53 }
53 A:visited {
54 A:visited {
54 COLOR: #2a5685
55 COLOR: #2a5685
55 }
56 }
56 A:hover {
57 A:hover {
57 COLOR: #c61a1a
58 COLOR: #c61a1a
58 }
59 }
59 A:active {
60 A:active {
60 COLOR: #c61a1a
61 COLOR: #c61a1a
61 }
62 }
62 HR {
63 HR {
63 BORDER-RIGHT: 0px; BORDER-TOP: 0px; BACKGROUND: #ccc; BORDER-LEFT: 0px; =
64 BORDER-RIGHT: 0px; BORDER-TOP: 0px; BACKGROUND: #ccc; BORDER-LEFT: 0px; =
64 WIDTH: 100%; BORDER-BOTTOM: 0px; HEIGHT: 1px
65 WIDTH: 100%; BORDER-BOTTOM: 0px; HEIGHT: 1px
65 }
66 }
66 .footer {
67 .footer {
67 FONT-SIZE: 0.8em; FONT-STYLE: italic
68 FONT-SIZE: 0.8em; FONT-STYLE: italic
68 }
69 }
69 </STYLE>
70 </STYLE>
70
71
71 <META content=3D"MSHTML 6.00.2900.2883" name=3DGENERATOR></HEAD>
72 <META content=3D"MSHTML 6.00.2900.2883" name=3DGENERATOR></HEAD>
72 <BODY bgColor=3D#ffffff>
73 <BODY bgColor=3D#ffffff>
73 <DIV><SPAN class=3Dfooter><FONT face=3DArial color=3D#000000 =
74 <DIV><SPAN class=3Dfooter><FONT face=3DArial color=3D#000000 =
74 size=3D2>This is=20
75 size=3D2>This is=20
75 reply Status: Resolved</FONT></DIV></SPAN></BODY></HTML>
76 reply Status: Resolved</FONT></DIV></SPAN></BODY></HTML>
76
77
77 ------=_NextPart_000_0067_01C8D3CE.711F9CC0--
78 ------=_NextPart_000_0067_01C8D3CE.711F9CC0--
78
79
@@ -1,354 +1,356
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 :issues,
28 :issues,
29 :issue_statuses,
29 :issue_statuses,
30 :workflows,
30 :workflows,
31 :trackers,
31 :trackers,
32 :projects_trackers,
32 :projects_trackers,
33 :enumerations,
33 :enumerations,
34 :issue_categories,
34 :issue_categories,
35 :custom_fields,
35 :custom_fields,
36 :custom_fields_trackers,
36 :custom_fields_trackers,
37 :boards,
37 :boards,
38 :messages
38 :messages
39
39
40 FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler'
40 FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler'
41
41
42 def setup
42 def setup
43 ActionMailer::Base.deliveries.clear
43 ActionMailer::Base.deliveries.clear
44 end
44 end
45
45
46 def test_add_issue
46 def test_add_issue
47 ActionMailer::Base.deliveries.clear
47 ActionMailer::Base.deliveries.clear
48 # This email contains: 'Project: onlinestore'
48 # This email contains: 'Project: onlinestore'
49 issue = submit_email('ticket_on_given_project.eml')
49 issue = submit_email('ticket_on_given_project.eml')
50 assert issue.is_a?(Issue)
50 assert issue.is_a?(Issue)
51 assert !issue.new_record?
51 assert !issue.new_record?
52 issue.reload
52 issue.reload
53 assert_equal 'New ticket on a given project', issue.subject
53 assert_equal 'New ticket on a given project', issue.subject
54 assert_equal User.find_by_login('jsmith'), issue.author
54 assert_equal User.find_by_login('jsmith'), issue.author
55 assert_equal Project.find(2), issue.project
55 assert_equal Project.find(2), issue.project
56 assert_equal IssueStatus.find_by_name('Resolved'), issue.status
56 assert_equal IssueStatus.find_by_name('Resolved'), issue.status
57 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
57 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
58 assert_equal '2010-01-01', issue.start_date.to_s
58 assert_equal '2010-01-01', issue.start_date.to_s
59 assert_equal '2010-12-31', issue.due_date.to_s
59 assert_equal '2010-12-31', issue.due_date.to_s
60 assert_equal User.find_by_login('jsmith'), issue.assigned_to
60 # keywords should be removed from the email body
61 # keywords should be removed from the email body
61 assert !issue.description.match(/^Project:/i)
62 assert !issue.description.match(/^Project:/i)
62 assert !issue.description.match(/^Status:/i)
63 assert !issue.description.match(/^Status:/i)
63 # Email notification should be sent
64 # Email notification should be sent
64 mail = ActionMailer::Base.deliveries.last
65 mail = ActionMailer::Base.deliveries.last
65 assert_not_nil mail
66 assert_not_nil mail
66 assert mail.subject.include?('New ticket on a given project')
67 assert mail.subject.include?('New ticket on a given project')
67 end
68 end
68
69
69 def test_add_issue_with_status
70 def test_add_issue_with_status
70 # This email contains: 'Project: onlinestore' and 'Status: Resolved'
71 # This email contains: 'Project: onlinestore' and 'Status: Resolved'
71 issue = submit_email('ticket_on_given_project.eml')
72 issue = submit_email('ticket_on_given_project.eml')
72 assert issue.is_a?(Issue)
73 assert issue.is_a?(Issue)
73 assert !issue.new_record?
74 assert !issue.new_record?
74 issue.reload
75 issue.reload
75 assert_equal Project.find(2), issue.project
76 assert_equal Project.find(2), issue.project
76 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
77 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
77 end
78 end
78
79
79 def test_add_issue_with_attributes_override
80 def test_add_issue_with_attributes_override
80 issue = submit_email('ticket_with_attributes.eml', :allow_override => 'tracker,category,priority')
81 issue = submit_email('ticket_with_attributes.eml', :allow_override => 'tracker,category,priority')
81 assert issue.is_a?(Issue)
82 assert issue.is_a?(Issue)
82 assert !issue.new_record?
83 assert !issue.new_record?
83 issue.reload
84 issue.reload
84 assert_equal 'New ticket on a given project', issue.subject
85 assert_equal 'New ticket on a given project', issue.subject
85 assert_equal User.find_by_login('jsmith'), issue.author
86 assert_equal User.find_by_login('jsmith'), issue.author
86 assert_equal Project.find(2), issue.project
87 assert_equal Project.find(2), issue.project
87 assert_equal 'Feature request', issue.tracker.to_s
88 assert_equal 'Feature request', issue.tracker.to_s
88 assert_equal 'Stock management', issue.category.to_s
89 assert_equal 'Stock management', issue.category.to_s
89 assert_equal 'Urgent', issue.priority.to_s
90 assert_equal 'Urgent', issue.priority.to_s
90 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
91 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
91 end
92 end
92
93
93 def test_add_issue_with_partial_attributes_override
94 def test_add_issue_with_partial_attributes_override
94 issue = submit_email('ticket_with_attributes.eml', :issue => {:priority => 'High'}, :allow_override => ['tracker'])
95 issue = submit_email('ticket_with_attributes.eml', :issue => {:priority => 'High'}, :allow_override => ['tracker'])
95 assert issue.is_a?(Issue)
96 assert issue.is_a?(Issue)
96 assert !issue.new_record?
97 assert !issue.new_record?
97 issue.reload
98 issue.reload
98 assert_equal 'New ticket on a given project', issue.subject
99 assert_equal 'New ticket on a given project', issue.subject
99 assert_equal User.find_by_login('jsmith'), issue.author
100 assert_equal User.find_by_login('jsmith'), issue.author
100 assert_equal Project.find(2), issue.project
101 assert_equal Project.find(2), issue.project
101 assert_equal 'Feature request', issue.tracker.to_s
102 assert_equal 'Feature request', issue.tracker.to_s
102 assert_nil issue.category
103 assert_nil issue.category
103 assert_equal 'High', issue.priority.to_s
104 assert_equal 'High', issue.priority.to_s
104 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
105 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
105 end
106 end
106
107
107 def test_add_issue_with_spaces_between_attribute_and_separator
108 def test_add_issue_with_spaces_between_attribute_and_separator
108 issue = submit_email('ticket_with_spaces_between_attribute_and_separator.eml', :allow_override => 'tracker,category,priority')
109 issue = submit_email('ticket_with_spaces_between_attribute_and_separator.eml', :allow_override => 'tracker,category,priority')
109 assert issue.is_a?(Issue)
110 assert issue.is_a?(Issue)
110 assert !issue.new_record?
111 assert !issue.new_record?
111 issue.reload
112 issue.reload
112 assert_equal 'New ticket on a given project', issue.subject
113 assert_equal 'New ticket on a given project', issue.subject
113 assert_equal User.find_by_login('jsmith'), issue.author
114 assert_equal User.find_by_login('jsmith'), issue.author
114 assert_equal Project.find(2), issue.project
115 assert_equal Project.find(2), issue.project
115 assert_equal 'Feature request', issue.tracker.to_s
116 assert_equal 'Feature request', issue.tracker.to_s
116 assert_equal 'Stock management', issue.category.to_s
117 assert_equal 'Stock management', issue.category.to_s
117 assert_equal 'Urgent', issue.priority.to_s
118 assert_equal 'Urgent', issue.priority.to_s
118 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
119 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
119 end
120 end
120
121
121
122
122 def test_add_issue_with_attachment_to_specific_project
123 def test_add_issue_with_attachment_to_specific_project
123 issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
124 issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
124 assert issue.is_a?(Issue)
125 assert issue.is_a?(Issue)
125 assert !issue.new_record?
126 assert !issue.new_record?
126 issue.reload
127 issue.reload
127 assert_equal 'Ticket created by email with attachment', issue.subject
128 assert_equal 'Ticket created by email with attachment', issue.subject
128 assert_equal User.find_by_login('jsmith'), issue.author
129 assert_equal User.find_by_login('jsmith'), issue.author
129 assert_equal Project.find(2), issue.project
130 assert_equal Project.find(2), issue.project
130 assert_equal 'This is a new ticket with attachments', issue.description
131 assert_equal 'This is a new ticket with attachments', issue.description
131 # Attachment properties
132 # Attachment properties
132 assert_equal 1, issue.attachments.size
133 assert_equal 1, issue.attachments.size
133 assert_equal 'Paella.jpg', issue.attachments.first.filename
134 assert_equal 'Paella.jpg', issue.attachments.first.filename
134 assert_equal 'image/jpeg', issue.attachments.first.content_type
135 assert_equal 'image/jpeg', issue.attachments.first.content_type
135 assert_equal 10790, issue.attachments.first.filesize
136 assert_equal 10790, issue.attachments.first.filesize
136 end
137 end
137
138
138 def test_add_issue_with_custom_fields
139 def test_add_issue_with_custom_fields
139 issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'onlinestore'})
140 issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'onlinestore'})
140 assert issue.is_a?(Issue)
141 assert issue.is_a?(Issue)
141 assert !issue.new_record?
142 assert !issue.new_record?
142 issue.reload
143 issue.reload
143 assert_equal 'New ticket with custom field values', issue.subject
144 assert_equal 'New ticket with custom field values', issue.subject
144 assert_equal 'Value for a custom field', issue.custom_value_for(CustomField.find_by_name('Searchable field')).value
145 assert_equal 'Value for a custom field', issue.custom_value_for(CustomField.find_by_name('Searchable field')).value
145 assert !issue.description.match(/^searchable field:/i)
146 assert !issue.description.match(/^searchable field:/i)
146 end
147 end
147
148
148 def test_add_issue_with_cc
149 def test_add_issue_with_cc
149 issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'})
150 issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'})
150 assert issue.is_a?(Issue)
151 assert issue.is_a?(Issue)
151 assert !issue.new_record?
152 assert !issue.new_record?
152 issue.reload
153 issue.reload
153 assert issue.watched_by?(User.find_by_mail('dlopper@somenet.foo'))
154 assert issue.watched_by?(User.find_by_mail('dlopper@somenet.foo'))
154 assert_equal 1, issue.watchers.size
155 assert_equal 1, issue.watchers.size
155 end
156 end
156
157
157 def test_add_issue_by_unknown_user
158 def test_add_issue_by_unknown_user
158 assert_no_difference 'User.count' do
159 assert_no_difference 'User.count' do
159 assert_equal false, submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'})
160 assert_equal false, submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'})
160 end
161 end
161 end
162 end
162
163
163 def test_add_issue_by_anonymous_user
164 def test_add_issue_by_anonymous_user
164 Role.anonymous.add_permission!(:add_issues)
165 Role.anonymous.add_permission!(:add_issues)
165 assert_no_difference 'User.count' do
166 assert_no_difference 'User.count' do
166 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'accept')
167 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'accept')
167 assert issue.is_a?(Issue)
168 assert issue.is_a?(Issue)
168 assert issue.author.anonymous?
169 assert issue.author.anonymous?
169 end
170 end
170 end
171 end
171
172
172 def test_add_issue_by_anonymous_user_on_private_project
173 def test_add_issue_by_anonymous_user_on_private_project
173 Role.anonymous.add_permission!(:add_issues)
174 Role.anonymous.add_permission!(:add_issues)
174 assert_no_difference 'User.count' do
175 assert_no_difference 'User.count' do
175 assert_no_difference 'Issue.count' do
176 assert_no_difference 'Issue.count' do
176 assert_equal false, submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'onlinestore'}, :unknown_user => 'accept')
177 assert_equal false, submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'onlinestore'}, :unknown_user => 'accept')
177 end
178 end
178 end
179 end
179 end
180 end
180
181
181 def test_add_issue_by_anonymous_user_on_private_project_without_permission_check
182 def test_add_issue_by_anonymous_user_on_private_project_without_permission_check
182 assert_no_difference 'User.count' do
183 assert_no_difference 'User.count' do
183 assert_difference 'Issue.count' do
184 assert_difference 'Issue.count' do
184 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'onlinestore'}, :no_permission_check => '1', :unknown_user => 'accept')
185 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'onlinestore'}, :no_permission_check => '1', :unknown_user => 'accept')
185 assert issue.is_a?(Issue)
186 assert issue.is_a?(Issue)
186 assert issue.author.anonymous?
187 assert issue.author.anonymous?
187 assert !issue.project.is_public?
188 assert !issue.project.is_public?
188 end
189 end
189 end
190 end
190 end
191 end
191
192
192 def test_add_issue_by_created_user
193 def test_add_issue_by_created_user
193 Setting.default_language = 'en'
194 Setting.default_language = 'en'
194 assert_difference 'User.count' do
195 assert_difference 'User.count' do
195 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'create')
196 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'create')
196 assert issue.is_a?(Issue)
197 assert issue.is_a?(Issue)
197 assert issue.author.active?
198 assert issue.author.active?
198 assert_equal 'john.doe@somenet.foo', issue.author.mail
199 assert_equal 'john.doe@somenet.foo', issue.author.mail
199 assert_equal 'John', issue.author.firstname
200 assert_equal 'John', issue.author.firstname
200 assert_equal 'Doe', issue.author.lastname
201 assert_equal 'Doe', issue.author.lastname
201
202
202 # account information
203 # account information
203 email = ActionMailer::Base.deliveries.first
204 email = ActionMailer::Base.deliveries.first
204 assert_not_nil email
205 assert_not_nil email
205 assert email.subject.include?('account activation')
206 assert email.subject.include?('account activation')
206 login = email.body.match(/\* Login: (.*)$/)[1]
207 login = email.body.match(/\* Login: (.*)$/)[1]
207 password = email.body.match(/\* Password: (.*)$/)[1]
208 password = email.body.match(/\* Password: (.*)$/)[1]
208 assert_equal issue.author, User.try_to_login(login, password)
209 assert_equal issue.author, User.try_to_login(login, password)
209 end
210 end
210 end
211 end
211
212
212 def test_add_issue_without_from_header
213 def test_add_issue_without_from_header
213 Role.anonymous.add_permission!(:add_issues)
214 Role.anonymous.add_permission!(:add_issues)
214 assert_equal false, submit_email('ticket_without_from_header.eml')
215 assert_equal false, submit_email('ticket_without_from_header.eml')
215 end
216 end
216
217
217 def test_add_issue_with_japanese_keywords
218 def test_add_issue_with_japanese_keywords
218 tracker = Tracker.create!(:name => 'ι–‹η™Ί')
219 tracker = Tracker.create!(:name => 'ι–‹η™Ί')
219 Project.find(1).trackers << tracker
220 Project.find(1).trackers << tracker
220 issue = submit_email('japanese_keywords_iso_2022_jp.eml', :issue => {:project => 'ecookbook'}, :allow_override => 'tracker')
221 issue = submit_email('japanese_keywords_iso_2022_jp.eml', :issue => {:project => 'ecookbook'}, :allow_override => 'tracker')
221 assert_kind_of Issue, issue
222 assert_kind_of Issue, issue
222 assert_equal tracker, issue.tracker
223 assert_equal tracker, issue.tracker
223 end
224 end
224
225
225 def test_should_ignore_emails_from_emission_address
226 def test_should_ignore_emails_from_emission_address
226 Role.anonymous.add_permission!(:add_issues)
227 Role.anonymous.add_permission!(:add_issues)
227 assert_no_difference 'User.count' do
228 assert_no_difference 'User.count' do
228 assert_equal false, submit_email('ticket_from_emission_address.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'create')
229 assert_equal false, submit_email('ticket_from_emission_address.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'create')
229 end
230 end
230 end
231 end
231
232
232 def test_add_issue_should_send_email_notification
233 def test_add_issue_should_send_email_notification
233 ActionMailer::Base.deliveries.clear
234 ActionMailer::Base.deliveries.clear
234 # This email contains: 'Project: onlinestore'
235 # This email contains: 'Project: onlinestore'
235 issue = submit_email('ticket_on_given_project.eml')
236 issue = submit_email('ticket_on_given_project.eml')
236 assert issue.is_a?(Issue)
237 assert issue.is_a?(Issue)
237 assert_equal 1, ActionMailer::Base.deliveries.size
238 assert_equal 1, ActionMailer::Base.deliveries.size
238 end
239 end
239
240
240 def test_add_issue_note
241 def test_add_issue_note
241 journal = submit_email('ticket_reply.eml')
242 journal = submit_email('ticket_reply.eml')
242 assert journal.is_a?(Journal)
243 assert journal.is_a?(Journal)
243 assert_equal User.find_by_login('jsmith'), journal.user
244 assert_equal User.find_by_login('jsmith'), journal.user
244 assert_equal Issue.find(2), journal.journalized
245 assert_equal Issue.find(2), journal.journalized
245 assert_match /This is reply/, journal.notes
246 assert_match /This is reply/, journal.notes
246 end
247 end
247
248
248 def test_add_issue_note_with_attribute_changes
249 def test_add_issue_note_with_attribute_changes
249 # This email contains: 'Status: Resolved'
250 # This email contains: 'Status: Resolved'
250 journal = submit_email('ticket_reply_with_status.eml')
251 journal = submit_email('ticket_reply_with_status.eml')
251 assert journal.is_a?(Journal)
252 assert journal.is_a?(Journal)
252 issue = Issue.find(journal.issue.id)
253 issue = Issue.find(journal.issue.id)
253 assert_equal User.find_by_login('jsmith'), journal.user
254 assert_equal User.find_by_login('jsmith'), journal.user
254 assert_equal Issue.find(2), journal.journalized
255 assert_equal Issue.find(2), journal.journalized
255 assert_match /This is reply/, journal.notes
256 assert_match /This is reply/, journal.notes
256 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
257 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
257 assert_equal '2010-01-01', issue.start_date.to_s
258 assert_equal '2010-01-01', issue.start_date.to_s
258 assert_equal '2010-12-31', issue.due_date.to_s
259 assert_equal '2010-12-31', issue.due_date.to_s
260 assert_equal User.find_by_login('jsmith'), issue.assigned_to
259 end
261 end
260
262
261 def test_add_issue_note_should_send_email_notification
263 def test_add_issue_note_should_send_email_notification
262 ActionMailer::Base.deliveries.clear
264 ActionMailer::Base.deliveries.clear
263 journal = submit_email('ticket_reply.eml')
265 journal = submit_email('ticket_reply.eml')
264 assert journal.is_a?(Journal)
266 assert journal.is_a?(Journal)
265 assert_equal 1, ActionMailer::Base.deliveries.size
267 assert_equal 1, ActionMailer::Base.deliveries.size
266 end
268 end
267
269
268 def test_reply_to_a_message
270 def test_reply_to_a_message
269 m = submit_email('message_reply.eml')
271 m = submit_email('message_reply.eml')
270 assert m.is_a?(Message)
272 assert m.is_a?(Message)
271 assert !m.new_record?
273 assert !m.new_record?
272 m.reload
274 m.reload
273 assert_equal 'Reply via email', m.subject
275 assert_equal 'Reply via email', m.subject
274 # The email replies to message #2 which is part of the thread of message #1
276 # The email replies to message #2 which is part of the thread of message #1
275 assert_equal Message.find(1), m.parent
277 assert_equal Message.find(1), m.parent
276 end
278 end
277
279
278 def test_reply_to_a_message_by_subject
280 def test_reply_to_a_message_by_subject
279 m = submit_email('message_reply_by_subject.eml')
281 m = submit_email('message_reply_by_subject.eml')
280 assert m.is_a?(Message)
282 assert m.is_a?(Message)
281 assert !m.new_record?
283 assert !m.new_record?
282 m.reload
284 m.reload
283 assert_equal 'Reply to the first post', m.subject
285 assert_equal 'Reply to the first post', m.subject
284 assert_equal Message.find(1), m.parent
286 assert_equal Message.find(1), m.parent
285 end
287 end
286
288
287 def test_should_strip_tags_of_html_only_emails
289 def test_should_strip_tags_of_html_only_emails
288 issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'})
290 issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'})
289 assert issue.is_a?(Issue)
291 assert issue.is_a?(Issue)
290 assert !issue.new_record?
292 assert !issue.new_record?
291 issue.reload
293 issue.reload
292 assert_equal 'HTML email', issue.subject
294 assert_equal 'HTML email', issue.subject
293 assert_equal 'This is a html-only email.', issue.description
295 assert_equal 'This is a html-only email.', issue.description
294 end
296 end
295
297
296 context "truncate emails based on the Setting" do
298 context "truncate emails based on the Setting" do
297 context "with no setting" do
299 context "with no setting" do
298 setup do
300 setup do
299 Setting.mail_handler_body_delimiters = ''
301 Setting.mail_handler_body_delimiters = ''
300 end
302 end
301
303
302 should "add the entire email into the issue" do
304 should "add the entire email into the issue" do
303 issue = submit_email('ticket_on_given_project.eml')
305 issue = submit_email('ticket_on_given_project.eml')
304 assert_issue_created(issue)
306 assert_issue_created(issue)
305 assert issue.description.include?('---')
307 assert issue.description.include?('---')
306 assert issue.description.include?('This paragraph is after the delimiter')
308 assert issue.description.include?('This paragraph is after the delimiter')
307 end
309 end
308 end
310 end
309
311
310 context "with a single string" do
312 context "with a single string" do
311 setup do
313 setup do
312 Setting.mail_handler_body_delimiters = '---'
314 Setting.mail_handler_body_delimiters = '---'
313 end
315 end
314
316
315 should "truncate the email at the delimiter for the issue" do
317 should "truncate the email at the delimiter for the issue" do
316 issue = submit_email('ticket_on_given_project.eml')
318 issue = submit_email('ticket_on_given_project.eml')
317 assert_issue_created(issue)
319 assert_issue_created(issue)
318 assert issue.description.include?('This paragraph is before delimiters')
320 assert issue.description.include?('This paragraph is before delimiters')
319 assert issue.description.include?('--- This line starts with a delimiter')
321 assert issue.description.include?('--- This line starts with a delimiter')
320 assert !issue.description.match(/^---$/)
322 assert !issue.description.match(/^---$/)
321 assert !issue.description.include?('This paragraph is after the delimiter')
323 assert !issue.description.include?('This paragraph is after the delimiter')
322 end
324 end
323 end
325 end
324
326
325 context "with multiple strings" do
327 context "with multiple strings" do
326 setup do
328 setup do
327 Setting.mail_handler_body_delimiters = "---\nBREAK"
329 Setting.mail_handler_body_delimiters = "---\nBREAK"
328 end
330 end
329
331
330 should "truncate the email at the first delimiter found (BREAK)" do
332 should "truncate the email at the first delimiter found (BREAK)" do
331 issue = submit_email('ticket_on_given_project.eml')
333 issue = submit_email('ticket_on_given_project.eml')
332 assert_issue_created(issue)
334 assert_issue_created(issue)
333 assert issue.description.include?('This paragraph is before delimiters')
335 assert issue.description.include?('This paragraph is before delimiters')
334 assert !issue.description.include?('BREAK')
336 assert !issue.description.include?('BREAK')
335 assert !issue.description.include?('This paragraph is between delimiters')
337 assert !issue.description.include?('This paragraph is between delimiters')
336 assert !issue.description.match(/^---$/)
338 assert !issue.description.match(/^---$/)
337 assert !issue.description.include?('This paragraph is after the delimiter')
339 assert !issue.description.include?('This paragraph is after the delimiter')
338 end
340 end
339 end
341 end
340 end
342 end
341
343
342 private
344 private
343
345
344 def submit_email(filename, options={})
346 def submit_email(filename, options={})
345 raw = IO.read(File.join(FIXTURES_PATH, filename))
347 raw = IO.read(File.join(FIXTURES_PATH, filename))
346 MailHandler.receive(raw, options)
348 MailHandler.receive(raw, options)
347 end
349 end
348
350
349 def assert_issue_created(issue)
351 def assert_issue_created(issue)
350 assert issue.is_a?(Issue)
352 assert issue.is_a?(Issue)
351 assert !issue.new_record?
353 assert !issue.new_record?
352 issue.reload
354 issue.reload
353 end
355 end
354 end
356 end
General Comments 0
You need to be logged in to leave comments. Login now