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