##// END OF EJS Templates
Improved user creation from incoming email....
Jean-Philippe Lang -
r7832:6076db74f179
parent child
Show More
@@ -1,370 +1,401
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class MailHandler < ActionMailer::Base
18 class MailHandler < ActionMailer::Base
19 include ActionView::Helpers::SanitizeHelper
19 include ActionView::Helpers::SanitizeHelper
20 include Redmine::I18n
20 include Redmine::I18n
21
21
22 class UnauthorizedAction < StandardError; end
22 class UnauthorizedAction < StandardError; end
23 class MissingInformation < StandardError; end
23 class MissingInformation < StandardError; end
24
24
25 attr_reader :email, :user
25 attr_reader :email, :user
26
26
27 def self.receive(email, options={})
27 def self.receive(email, options={})
28 @@handler_options = options.dup
28 @@handler_options = options.dup
29
29
30 @@handler_options[:issue] ||= {}
30 @@handler_options[:issue] ||= {}
31
31
32 @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip) if @@handler_options[:allow_override].is_a?(String)
32 @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip) if @@handler_options[:allow_override].is_a?(String)
33 @@handler_options[:allow_override] ||= []
33 @@handler_options[:allow_override] ||= []
34 # Project needs to be overridable if not specified
34 # Project needs to be overridable if not specified
35 @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
35 @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
36 # Status overridable by default
36 # Status overridable by default
37 @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
37 @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
38
38
39 @@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1' ? true : false)
39 @@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1' ? true : false)
40 super email
40 super email
41 end
41 end
42
42
43 # Processes incoming emails
43 # Processes incoming emails
44 # Returns the created object (eg. an issue, a message) or false
44 # Returns the created object (eg. an issue, a message) or false
45 def receive(email)
45 def receive(email)
46 @email = email
46 @email = email
47 sender_email = email.from.to_a.first.to_s.strip
47 sender_email = email.from.to_a.first.to_s.strip
48 # Ignore emails received from the application emission address to avoid hell cycles
48 # Ignore emails received from the application emission address to avoid hell cycles
49 if sender_email.downcase == Setting.mail_from.to_s.strip.downcase
49 if sender_email.downcase == Setting.mail_from.to_s.strip.downcase
50 logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]" if logger && logger.info
50 logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]" if logger && logger.info
51 return false
51 return false
52 end
52 end
53 @user = User.find_by_mail(sender_email) if sender_email.present?
53 @user = User.find_by_mail(sender_email) if sender_email.present?
54 if @user && !@user.active?
54 if @user && !@user.active?
55 logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]" if logger && logger.info
55 logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]" if logger && logger.info
56 return false
56 return false
57 end
57 end
58 if @user.nil?
58 if @user.nil?
59 # Email was submitted by an unknown user
59 # Email was submitted by an unknown user
60 case @@handler_options[:unknown_user]
60 case @@handler_options[:unknown_user]
61 when 'accept'
61 when 'accept'
62 @user = User.anonymous
62 @user = User.anonymous
63 when 'create'
63 when 'create'
64 @user = MailHandler.create_user_from_email(email)
64 @user = create_user_from_email(email)
65 if @user
65 if @user
66 logger.info "MailHandler: [#{@user.login}] account created" if logger && logger.info
66 logger.info "MailHandler: [#{@user.login}] account created" if logger && logger.info
67 Mailer.deliver_account_information(@user, @user.password)
67 Mailer.deliver_account_information(@user, @user.password)
68 else
68 else
69 logger.error "MailHandler: could not create account for [#{sender_email}]" if logger && logger.error
69 logger.error "MailHandler: could not create account for [#{sender_email}]" if logger && logger.error
70 return false
70 return false
71 end
71 end
72 else
72 else
73 # Default behaviour, emails from unknown users are ignored
73 # Default behaviour, emails from unknown users are ignored
74 logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]" if logger && logger.info
74 logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]" if logger && logger.info
75 return false
75 return false
76 end
76 end
77 end
77 end
78 User.current = @user
78 User.current = @user
79 dispatch
79 dispatch
80 end
80 end
81
81
82 private
82 private
83
83
84 MESSAGE_ID_RE = %r{^<redmine\.([a-z0-9_]+)\-(\d+)\.\d+@}
84 MESSAGE_ID_RE = %r{^<redmine\.([a-z0-9_]+)\-(\d+)\.\d+@}
85 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]*#(\d+)\]}
85 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]*#(\d+)\]}
86 MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
86 MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
87
87
88 def dispatch
88 def dispatch
89 headers = [email.in_reply_to, email.references].flatten.compact
89 headers = [email.in_reply_to, email.references].flatten.compact
90 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
90 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
91 klass, object_id = $1, $2.to_i
91 klass, object_id = $1, $2.to_i
92 method_name = "receive_#{klass}_reply"
92 method_name = "receive_#{klass}_reply"
93 if self.class.private_instance_methods.collect(&:to_s).include?(method_name)
93 if self.class.private_instance_methods.collect(&:to_s).include?(method_name)
94 send method_name, object_id
94 send method_name, object_id
95 else
95 else
96 # ignoring it
96 # ignoring it
97 end
97 end
98 elsif m = email.subject.match(ISSUE_REPLY_SUBJECT_RE)
98 elsif m = email.subject.match(ISSUE_REPLY_SUBJECT_RE)
99 receive_issue_reply(m[1].to_i)
99 receive_issue_reply(m[1].to_i)
100 elsif m = email.subject.match(MESSAGE_REPLY_SUBJECT_RE)
100 elsif m = email.subject.match(MESSAGE_REPLY_SUBJECT_RE)
101 receive_message_reply(m[1].to_i)
101 receive_message_reply(m[1].to_i)
102 else
102 else
103 dispatch_to_default
103 dispatch_to_default
104 end
104 end
105 rescue ActiveRecord::RecordInvalid => e
105 rescue ActiveRecord::RecordInvalid => e
106 # TODO: send a email to the user
106 # TODO: send a email to the user
107 logger.error e.message if logger
107 logger.error e.message if logger
108 false
108 false
109 rescue MissingInformation => e
109 rescue MissingInformation => e
110 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
110 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
111 false
111 false
112 rescue UnauthorizedAction => e
112 rescue UnauthorizedAction => e
113 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
113 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
114 false
114 false
115 end
115 end
116
116
117 def dispatch_to_default
117 def dispatch_to_default
118 receive_issue
118 receive_issue
119 end
119 end
120
120
121 # Creates a new issue
121 # Creates a new issue
122 def receive_issue
122 def receive_issue
123 project = target_project
123 project = target_project
124 # check permission
124 # check permission
125 unless @@handler_options[:no_permission_check]
125 unless @@handler_options[:no_permission_check]
126 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
126 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
127 end
127 end
128
128
129 issue = Issue.new(:author => user, :project => project)
129 issue = Issue.new(:author => user, :project => project)
130 issue.safe_attributes = issue_attributes_from_keywords(issue)
130 issue.safe_attributes = issue_attributes_from_keywords(issue)
131 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
131 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
132 issue.subject = email.subject.to_s.chomp[0,255]
132 issue.subject = email.subject.to_s.chomp[0,255]
133 if issue.subject.blank?
133 if issue.subject.blank?
134 issue.subject = '(no subject)'
134 issue.subject = '(no subject)'
135 end
135 end
136 issue.description = cleaned_up_text_body
136 issue.description = cleaned_up_text_body
137
137
138 # add To and Cc as watchers before saving so the watchers can reply to Redmine
138 # add To and Cc as watchers before saving so the watchers can reply to Redmine
139 add_watchers(issue)
139 add_watchers(issue)
140 issue.save!
140 issue.save!
141 add_attachments(issue)
141 add_attachments(issue)
142 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
142 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
143 issue
143 issue
144 end
144 end
145
145
146 # Adds a note to an existing issue
146 # Adds a note to an existing issue
147 def receive_issue_reply(issue_id)
147 def receive_issue_reply(issue_id)
148 issue = Issue.find_by_id(issue_id)
148 issue = Issue.find_by_id(issue_id)
149 return unless issue
149 return unless issue
150 # check permission
150 # check permission
151 unless @@handler_options[:no_permission_check]
151 unless @@handler_options[:no_permission_check]
152 raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project)
152 raise UnauthorizedAction unless user.allowed_to?(:add_issue_notes, issue.project) || user.allowed_to?(:edit_issues, issue.project)
153 end
153 end
154
154
155 # ignore CLI-supplied defaults for new issues
155 # ignore CLI-supplied defaults for new issues
156 @@handler_options[:issue].clear
156 @@handler_options[:issue].clear
157
157
158 journal = issue.init_journal(user)
158 journal = issue.init_journal(user)
159 issue.safe_attributes = issue_attributes_from_keywords(issue)
159 issue.safe_attributes = issue_attributes_from_keywords(issue)
160 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
160 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
161 journal.notes = cleaned_up_text_body
161 journal.notes = cleaned_up_text_body
162 add_attachments(issue)
162 add_attachments(issue)
163 issue.save!
163 issue.save!
164 logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info
164 logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info
165 journal
165 journal
166 end
166 end
167
167
168 # Reply will be added to the issue
168 # Reply will be added to the issue
169 def receive_journal_reply(journal_id)
169 def receive_journal_reply(journal_id)
170 journal = Journal.find_by_id(journal_id)
170 journal = Journal.find_by_id(journal_id)
171 if journal && journal.journalized_type == 'Issue'
171 if journal && journal.journalized_type == 'Issue'
172 receive_issue_reply(journal.journalized_id)
172 receive_issue_reply(journal.journalized_id)
173 end
173 end
174 end
174 end
175
175
176 # Receives a reply to a forum message
176 # Receives a reply to a forum message
177 def receive_message_reply(message_id)
177 def receive_message_reply(message_id)
178 message = Message.find_by_id(message_id)
178 message = Message.find_by_id(message_id)
179 if message
179 if message
180 message = message.root
180 message = message.root
181
181
182 unless @@handler_options[:no_permission_check]
182 unless @@handler_options[:no_permission_check]
183 raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
183 raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
184 end
184 end
185
185
186 if !message.locked?
186 if !message.locked?
187 reply = Message.new(:subject => email.subject.gsub(%r{^.*msg\d+\]}, '').strip,
187 reply = Message.new(:subject => email.subject.gsub(%r{^.*msg\d+\]}, '').strip,
188 :content => cleaned_up_text_body)
188 :content => cleaned_up_text_body)
189 reply.author = user
189 reply.author = user
190 reply.board = message.board
190 reply.board = message.board
191 message.children << reply
191 message.children << reply
192 add_attachments(reply)
192 add_attachments(reply)
193 reply
193 reply
194 else
194 else
195 logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic" if logger && logger.info
195 logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic" if logger && logger.info
196 end
196 end
197 end
197 end
198 end
198 end
199
199
200 def add_attachments(obj)
200 def add_attachments(obj)
201 if email.attachments && email.attachments.any?
201 if email.attachments && email.attachments.any?
202 email.attachments.each do |attachment|
202 email.attachments.each do |attachment|
203 obj.attachments << Attachment.create(:container => obj,
203 obj.attachments << Attachment.create(:container => obj,
204 :file => attachment,
204 :file => attachment,
205 :author => user,
205 :author => user,
206 :content_type => attachment.content_type)
206 :content_type => attachment.content_type)
207 end
207 end
208 end
208 end
209 end
209 end
210
210
211 # Adds To and Cc as watchers of the given object if the sender has the
211 # Adds To and Cc as watchers of the given object if the sender has the
212 # appropriate permission
212 # appropriate permission
213 def add_watchers(obj)
213 def add_watchers(obj)
214 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
214 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
215 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
215 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
216 unless addresses.empty?
216 unless addresses.empty?
217 watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
217 watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
218 watchers.each {|w| obj.add_watcher(w)}
218 watchers.each {|w| obj.add_watcher(w)}
219 end
219 end
220 end
220 end
221 end
221 end
222
222
223 def get_keyword(attr, options={})
223 def get_keyword(attr, options={})
224 @keywords ||= {}
224 @keywords ||= {}
225 if @keywords.has_key?(attr)
225 if @keywords.has_key?(attr)
226 @keywords[attr]
226 @keywords[attr]
227 else
227 else
228 @keywords[attr] = begin
228 @keywords[attr] = begin
229 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && (v = extract_keyword!(plain_text_body, attr, options[:format]))
229 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) && (v = extract_keyword!(plain_text_body, attr, options[:format]))
230 v
230 v
231 elsif !@@handler_options[:issue][attr].blank?
231 elsif !@@handler_options[:issue][attr].blank?
232 @@handler_options[:issue][attr]
232 @@handler_options[:issue][attr]
233 end
233 end
234 end
234 end
235 end
235 end
236 end
236 end
237
237
238 # Destructively extracts the value for +attr+ in +text+
238 # Destructively extracts the value for +attr+ in +text+
239 # Returns nil if no matching keyword found
239 # Returns nil if no matching keyword found
240 def extract_keyword!(text, attr, format=nil)
240 def extract_keyword!(text, attr, format=nil)
241 keys = [attr.to_s.humanize]
241 keys = [attr.to_s.humanize]
242 if attr.is_a?(Symbol)
242 if attr.is_a?(Symbol)
243 keys << l("field_#{attr}", :default => '', :locale => user.language) if user && user.language.present?
243 keys << l("field_#{attr}", :default => '', :locale => user.language) if user && user.language.present?
244 keys << l("field_#{attr}", :default => '', :locale => Setting.default_language) if Setting.default_language.present?
244 keys << l("field_#{attr}", :default => '', :locale => Setting.default_language) if Setting.default_language.present?
245 end
245 end
246 keys.reject! {|k| k.blank?}
246 keys.reject! {|k| k.blank?}
247 keys.collect! {|k| Regexp.escape(k)}
247 keys.collect! {|k| Regexp.escape(k)}
248 format ||= '.+'
248 format ||= '.+'
249 text.gsub!(/^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i, '')
249 text.gsub!(/^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i, '')
250 $2 && $2.strip
250 $2 && $2.strip
251 end
251 end
252
252
253 def target_project
253 def target_project
254 # TODO: other ways to specify project:
254 # TODO: other ways to specify project:
255 # * parse the email To field
255 # * parse the email To field
256 # * specific project (eg. Setting.mail_handler_target_project)
256 # * specific project (eg. Setting.mail_handler_target_project)
257 target = Project.find_by_identifier(get_keyword(:project))
257 target = Project.find_by_identifier(get_keyword(:project))
258 raise MissingInformation.new('Unable to determine target project') if target.nil?
258 raise MissingInformation.new('Unable to determine target project') if target.nil?
259 target
259 target
260 end
260 end
261
261
262 # Returns a Hash of issue attributes extracted from keywords in the email body
262 # Returns a Hash of issue attributes extracted from keywords in the email body
263 def issue_attributes_from_keywords(issue)
263 def issue_attributes_from_keywords(issue)
264 assigned_to = (k = get_keyword(:assigned_to, :override => true)) && find_assignee_from_keyword(k, issue)
264 assigned_to = (k = get_keyword(:assigned_to, :override => true)) && find_assignee_from_keyword(k, issue)
265
265
266 attrs = {
266 attrs = {
267 'tracker_id' => (k = get_keyword(:tracker)) && issue.project.trackers.named(k).first.try(:id),
267 'tracker_id' => (k = get_keyword(:tracker)) && issue.project.trackers.named(k).first.try(:id),
268 'status_id' => (k = get_keyword(:status)) && IssueStatus.named(k).first.try(:id),
268 'status_id' => (k = get_keyword(:status)) && IssueStatus.named(k).first.try(:id),
269 'priority_id' => (k = get_keyword(:priority)) && IssuePriority.named(k).first.try(:id),
269 'priority_id' => (k = get_keyword(:priority)) && IssuePriority.named(k).first.try(:id),
270 'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.named(k).first.try(:id),
270 'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.named(k).first.try(:id),
271 'assigned_to_id' => assigned_to.try(:id),
271 'assigned_to_id' => assigned_to.try(:id),
272 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) && issue.project.shared_versions.named(k).first.try(:id),
272 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) && issue.project.shared_versions.named(k).first.try(:id),
273 'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
273 'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
274 'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
274 'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
275 'estimated_hours' => get_keyword(:estimated_hours, :override => true),
275 'estimated_hours' => get_keyword(:estimated_hours, :override => true),
276 'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0')
276 'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0')
277 }.delete_if {|k, v| v.blank? }
277 }.delete_if {|k, v| v.blank? }
278
278
279 if issue.new_record? && attrs['tracker_id'].nil?
279 if issue.new_record? && attrs['tracker_id'].nil?
280 attrs['tracker_id'] = issue.project.trackers.find(:first).try(:id)
280 attrs['tracker_id'] = issue.project.trackers.find(:first).try(:id)
281 end
281 end
282
282
283 attrs
283 attrs
284 end
284 end
285
285
286 # Returns a Hash of issue custom field values extracted from keywords in the email body
286 # Returns a Hash of issue custom field values extracted from keywords in the email body
287 def custom_field_values_from_keywords(customized)
287 def custom_field_values_from_keywords(customized)
288 customized.custom_field_values.inject({}) do |h, v|
288 customized.custom_field_values.inject({}) do |h, v|
289 if value = get_keyword(v.custom_field.name, :override => true)
289 if value = get_keyword(v.custom_field.name, :override => true)
290 h[v.custom_field.id.to_s] = value
290 h[v.custom_field.id.to_s] = value
291 end
291 end
292 h
292 h
293 end
293 end
294 end
294 end
295
295
296 # Returns the text/plain part of the email
296 # Returns the text/plain part of the email
297 # If not found (eg. HTML-only email), returns the body with tags removed
297 # If not found (eg. HTML-only email), returns the body with tags removed
298 def plain_text_body
298 def plain_text_body
299 return @plain_text_body unless @plain_text_body.nil?
299 return @plain_text_body unless @plain_text_body.nil?
300 parts = @email.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten
300 parts = @email.parts.collect {|c| (c.respond_to?(:parts) && !c.parts.empty?) ? c.parts : c}.flatten
301 if parts.empty?
301 if parts.empty?
302 parts << @email
302 parts << @email
303 end
303 end
304 plain_text_part = parts.detect {|p| p.content_type == 'text/plain'}
304 plain_text_part = parts.detect {|p| p.content_type == 'text/plain'}
305 if plain_text_part.nil?
305 if plain_text_part.nil?
306 # no text/plain part found, assuming html-only email
306 # no text/plain part found, assuming html-only email
307 # strip html tags and remove doctype directive
307 # strip html tags and remove doctype directive
308 @plain_text_body = strip_tags(@email.body.to_s)
308 @plain_text_body = strip_tags(@email.body.to_s)
309 @plain_text_body.gsub! %r{^<!DOCTYPE .*$}, ''
309 @plain_text_body.gsub! %r{^<!DOCTYPE .*$}, ''
310 else
310 else
311 @plain_text_body = plain_text_part.body.to_s
311 @plain_text_body = plain_text_part.body.to_s
312 end
312 end
313 @plain_text_body.strip!
313 @plain_text_body.strip!
314 @plain_text_body
314 @plain_text_body
315 end
315 end
316
316
317 def cleaned_up_text_body
317 def cleaned_up_text_body
318 cleanup_body(plain_text_body)
318 cleanup_body(plain_text_body)
319 end
319 end
320
320
321 def self.full_sanitizer
321 def self.full_sanitizer
322 @full_sanitizer ||= HTML::FullSanitizer.new
322 @full_sanitizer ||= HTML::FullSanitizer.new
323 end
323 end
324
324
325 # Creates a user account for the +email+ sender
325 def self.assign_string_attribute_with_limit(object, attribute, value)
326 def self.create_user_from_email(email)
326 limit = object.class.columns_hash[attribute.to_s].limit || 255
327 addr = email.from_addrs.to_a.first
327 value = value.to_s.slice(0, limit)
328 if addr && !addr.spec.blank?
328 object.send("#{attribute}=", value)
329 end
330
331 # Returns a User from an email address and a full name
332 def self.new_user_from_attributes(email_address, fullname=nil)
329 user = User.new
333 user = User.new
330 user.mail = addr.spec
331
334
332 names = addr.name.blank? ? addr.spec.gsub(/@.*$/, '').split('.') : addr.name.split
335 # Truncating the email address would result in an invalid format
333 user.firstname = names.shift
336 user.mail = email_address
334 user.lastname = names.join(' ')
337 assign_string_attribute_with_limit(user, 'login', email_address)
338
339 names = fullname.blank? ? email_address.gsub(/@.*$/, '').split('.') : fullname.split
340 assign_string_attribute_with_limit(user, 'firstname', names.shift)
341 assign_string_attribute_with_limit(user, 'lastname', names.join(' '))
335 user.lastname = '-' if user.lastname.blank?
342 user.lastname = '-' if user.lastname.blank?
336
343
337 user.login = user.mail
344 password_length = [Setting.password_min_length.to_i, 10].max
338 user.password = ActiveSupport::SecureRandom.hex(5)
345 user.password = ActiveSupport::SecureRandom.hex(password_length / 2 + 1)
339 user.language = Setting.default_language
346 user.language = Setting.default_language
340 user.save ? user : nil
347
348 unless user.valid?
349 user.login = "user#{ActiveSupport::SecureRandom.hex(6)}" if user.errors.on(:login)
350 user.firstname = "-" if user.errors.on(:firstname)
351 user.lastname = "-" if user.errors.on(:lastname)
352 end
353
354 user
355 end
356
357 # Creates a User for the +email+ sender
358 # Returns the user or nil if it could not be created
359 def create_user_from_email(email)
360 addr = email.from_addrs.to_a.first
361 if addr && !addr.spec.blank?
362 user = self.class.new_user_from_attributes(addr.spec, addr.name)
363 if user.save
364 user
365 else
366 logger.error "MailHandler: failed to create User: #{user.errors.full_messages}" if logger
367 nil
368 end
369 else
370 logger.error "MailHandler: failed to create User: no FROM address found" if logger
371 nil
341 end
372 end
342 end
373 end
343
374
344 private
375 private
345
376
346 # Removes the email body of text after the truncation configurations.
377 # Removes the email body of text after the truncation configurations.
347 def cleanup_body(body)
378 def cleanup_body(body)
348 delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
379 delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
349 unless delimiters.empty?
380 unless delimiters.empty?
350 regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
381 regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
351 body = body.gsub(regex, '')
382 body = body.gsub(regex, '')
352 end
383 end
353 body.strip
384 body.strip
354 end
385 end
355
386
356 def find_assignee_from_keyword(keyword, issue)
387 def find_assignee_from_keyword(keyword, issue)
357 keyword = keyword.to_s.downcase
388 keyword = keyword.to_s.downcase
358 assignable = issue.assignable_users
389 assignable = issue.assignable_users
359 assignee = nil
390 assignee = nil
360 assignee ||= assignable.detect {|a| a.mail.to_s.downcase == keyword || a.login.to_s.downcase == keyword}
391 assignee ||= assignable.detect {|a| a.mail.to_s.downcase == keyword || a.login.to_s.downcase == keyword}
361 if assignee.nil? && keyword.match(/ /)
392 if assignee.nil? && keyword.match(/ /)
362 firstname, lastname = *(keyword.split) # "First Last Throwaway"
393 firstname, lastname = *(keyword.split) # "First Last Throwaway"
363 assignee ||= assignable.detect {|a| a.is_a?(User) && a.firstname.to_s.downcase == firstname && a.lastname.to_s.downcase == lastname}
394 assignee ||= assignable.detect {|a| a.is_a?(User) && a.firstname.to_s.downcase == firstname && a.lastname.to_s.downcase == lastname}
364 end
395 end
365 if assignee.nil?
396 if assignee.nil?
366 assignee ||= assignable.detect {|a| a.is_a?(Group) && a.name.downcase == keyword}
397 assignee ||= assignable.detect {|a| a.is_a?(Group) && a.name.downcase == keyword}
367 end
398 end
368 assignee
399 assignee
369 end
400 end
370 end
401 end
@@ -1,501 +1,541
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2011 Jean-Philippe Lang
4 # Copyright (C) 2006-2011 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 require File.expand_path('../../test_helper', __FILE__)
20 require File.expand_path('../../test_helper', __FILE__)
21
21
22 class MailHandlerTest < ActiveSupport::TestCase
22 class MailHandlerTest < ActiveSupport::TestCase
23 fixtures :users, :projects,
23 fixtures :users, :projects,
24 :enabled_modules,
24 :enabled_modules,
25 :roles,
25 :roles,
26 :members,
26 :members,
27 :member_roles,
27 :member_roles,
28 :users,
28 :users,
29 :issues,
29 :issues,
30 :issue_statuses,
30 :issue_statuses,
31 :workflows,
31 :workflows,
32 :trackers,
32 :trackers,
33 :projects_trackers,
33 :projects_trackers,
34 :versions,
34 :versions,
35 :enumerations,
35 :enumerations,
36 :issue_categories,
36 :issue_categories,
37 :custom_fields,
37 :custom_fields,
38 :custom_fields_trackers,
38 :custom_fields_trackers,
39 :custom_fields_projects,
39 :custom_fields_projects,
40 :boards,
40 :boards,
41 :messages
41 :messages
42
42
43 FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler'
43 FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler'
44
44
45 def setup
45 def setup
46 ActionMailer::Base.deliveries.clear
46 ActionMailer::Base.deliveries.clear
47 Setting.notified_events = Redmine::Notifiable.all.collect(&:name)
47 Setting.notified_events = Redmine::Notifiable.all.collect(&:name)
48 end
48 end
49
49
50 def test_add_issue
50 def test_add_issue
51 ActionMailer::Base.deliveries.clear
51 ActionMailer::Base.deliveries.clear
52 # This email contains: 'Project: onlinestore'
52 # This email contains: 'Project: onlinestore'
53 issue = submit_email('ticket_on_given_project.eml')
53 issue = submit_email('ticket_on_given_project.eml')
54 assert issue.is_a?(Issue)
54 assert issue.is_a?(Issue)
55 assert !issue.new_record?
55 assert !issue.new_record?
56 issue.reload
56 issue.reload
57 assert_equal Project.find(2), issue.project
57 assert_equal Project.find(2), issue.project
58 assert_equal issue.project.trackers.first, issue.tracker
58 assert_equal issue.project.trackers.first, issue.tracker
59 assert_equal 'New ticket on a given project', issue.subject
59 assert_equal 'New ticket on a given project', issue.subject
60 assert_equal User.find_by_login('jsmith'), issue.author
60 assert_equal User.find_by_login('jsmith'), issue.author
61 assert_equal IssueStatus.find_by_name('Resolved'), issue.status
61 assert_equal IssueStatus.find_by_name('Resolved'), issue.status
62 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
62 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
63 assert_equal '2010-01-01', issue.start_date.to_s
63 assert_equal '2010-01-01', issue.start_date.to_s
64 assert_equal '2010-12-31', issue.due_date.to_s
64 assert_equal '2010-12-31', issue.due_date.to_s
65 assert_equal User.find_by_login('jsmith'), issue.assigned_to
65 assert_equal User.find_by_login('jsmith'), issue.assigned_to
66 assert_equal Version.find_by_name('Alpha'), issue.fixed_version
66 assert_equal Version.find_by_name('Alpha'), issue.fixed_version
67 assert_equal 2.5, issue.estimated_hours
67 assert_equal 2.5, issue.estimated_hours
68 assert_equal 30, issue.done_ratio
68 assert_equal 30, issue.done_ratio
69 assert_equal [issue.id, 1, 2], [issue.root_id, issue.lft, issue.rgt]
69 assert_equal [issue.id, 1, 2], [issue.root_id, issue.lft, issue.rgt]
70 # keywords should be removed from the email body
70 # keywords should be removed from the email body
71 assert !issue.description.match(/^Project:/i)
71 assert !issue.description.match(/^Project:/i)
72 assert !issue.description.match(/^Status:/i)
72 assert !issue.description.match(/^Status:/i)
73 assert !issue.description.match(/^Start Date:/i)
73 assert !issue.description.match(/^Start Date:/i)
74 # Email notification should be sent
74 # Email notification should be sent
75 mail = ActionMailer::Base.deliveries.last
75 mail = ActionMailer::Base.deliveries.last
76 assert_not_nil mail
76 assert_not_nil mail
77 assert mail.subject.include?('New ticket on a given project')
77 assert mail.subject.include?('New ticket on a given project')
78 end
78 end
79
79
80 def test_add_issue_with_default_tracker
80 def test_add_issue_with_default_tracker
81 # This email contains: 'Project: onlinestore'
81 # This email contains: 'Project: onlinestore'
82 issue = submit_email('ticket_on_given_project.eml', :issue => {:tracker => 'Support request'})
82 issue = submit_email('ticket_on_given_project.eml', :issue => {:tracker => 'Support request'})
83 assert issue.is_a?(Issue)
83 assert issue.is_a?(Issue)
84 assert !issue.new_record?
84 assert !issue.new_record?
85 issue.reload
85 issue.reload
86 assert_equal 'Support request', issue.tracker.name
86 assert_equal 'Support request', issue.tracker.name
87 end
87 end
88
88
89 def test_add_issue_with_status
89 def test_add_issue_with_status
90 # This email contains: 'Project: onlinestore' and 'Status: Resolved'
90 # This email contains: 'Project: onlinestore' and 'Status: Resolved'
91 issue = submit_email('ticket_on_given_project.eml')
91 issue = submit_email('ticket_on_given_project.eml')
92 assert issue.is_a?(Issue)
92 assert issue.is_a?(Issue)
93 assert !issue.new_record?
93 assert !issue.new_record?
94 issue.reload
94 issue.reload
95 assert_equal Project.find(2), issue.project
95 assert_equal Project.find(2), issue.project
96 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
96 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
97 end
97 end
98
98
99 def test_add_issue_with_attributes_override
99 def test_add_issue_with_attributes_override
100 issue = submit_email('ticket_with_attributes.eml', :allow_override => 'tracker,category,priority')
100 issue = submit_email('ticket_with_attributes.eml', :allow_override => 'tracker,category,priority')
101 assert issue.is_a?(Issue)
101 assert issue.is_a?(Issue)
102 assert !issue.new_record?
102 assert !issue.new_record?
103 issue.reload
103 issue.reload
104 assert_equal 'New ticket on a given project', issue.subject
104 assert_equal 'New ticket on a given project', issue.subject
105 assert_equal User.find_by_login('jsmith'), issue.author
105 assert_equal User.find_by_login('jsmith'), issue.author
106 assert_equal Project.find(2), issue.project
106 assert_equal Project.find(2), issue.project
107 assert_equal 'Feature request', issue.tracker.to_s
107 assert_equal 'Feature request', issue.tracker.to_s
108 assert_equal 'Stock management', issue.category.to_s
108 assert_equal 'Stock management', issue.category.to_s
109 assert_equal 'Urgent', issue.priority.to_s
109 assert_equal 'Urgent', issue.priority.to_s
110 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
110 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
111 end
111 end
112
112
113 def test_add_issue_with_group_assignment
113 def test_add_issue_with_group_assignment
114 with_settings :issue_group_assignment => '1' do
114 with_settings :issue_group_assignment => '1' do
115 issue = submit_email('ticket_on_given_project.eml') do |email|
115 issue = submit_email('ticket_on_given_project.eml') do |email|
116 email.gsub!('Assigned to: John Smith', 'Assigned to: B Team')
116 email.gsub!('Assigned to: John Smith', 'Assigned to: B Team')
117 end
117 end
118 assert issue.is_a?(Issue)
118 assert issue.is_a?(Issue)
119 assert !issue.new_record?
119 assert !issue.new_record?
120 issue.reload
120 issue.reload
121 assert_equal Group.find(11), issue.assigned_to
121 assert_equal Group.find(11), issue.assigned_to
122 end
122 end
123 end
123 end
124
124
125 def test_add_issue_with_partial_attributes_override
125 def test_add_issue_with_partial_attributes_override
126 issue = submit_email('ticket_with_attributes.eml', :issue => {:priority => 'High'}, :allow_override => ['tracker'])
126 issue = submit_email('ticket_with_attributes.eml', :issue => {:priority => 'High'}, :allow_override => ['tracker'])
127 assert issue.is_a?(Issue)
127 assert issue.is_a?(Issue)
128 assert !issue.new_record?
128 assert !issue.new_record?
129 issue.reload
129 issue.reload
130 assert_equal 'New ticket on a given project', issue.subject
130 assert_equal 'New ticket on a given project', issue.subject
131 assert_equal User.find_by_login('jsmith'), issue.author
131 assert_equal User.find_by_login('jsmith'), issue.author
132 assert_equal Project.find(2), issue.project
132 assert_equal Project.find(2), issue.project
133 assert_equal 'Feature request', issue.tracker.to_s
133 assert_equal 'Feature request', issue.tracker.to_s
134 assert_nil issue.category
134 assert_nil issue.category
135 assert_equal 'High', issue.priority.to_s
135 assert_equal 'High', issue.priority.to_s
136 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
136 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
137 end
137 end
138
138
139 def test_add_issue_with_spaces_between_attribute_and_separator
139 def test_add_issue_with_spaces_between_attribute_and_separator
140 issue = submit_email('ticket_with_spaces_between_attribute_and_separator.eml', :allow_override => 'tracker,category,priority')
140 issue = submit_email('ticket_with_spaces_between_attribute_and_separator.eml', :allow_override => 'tracker,category,priority')
141 assert issue.is_a?(Issue)
141 assert issue.is_a?(Issue)
142 assert !issue.new_record?
142 assert !issue.new_record?
143 issue.reload
143 issue.reload
144 assert_equal 'New ticket on a given project', issue.subject
144 assert_equal 'New ticket on a given project', issue.subject
145 assert_equal User.find_by_login('jsmith'), issue.author
145 assert_equal User.find_by_login('jsmith'), issue.author
146 assert_equal Project.find(2), issue.project
146 assert_equal Project.find(2), issue.project
147 assert_equal 'Feature request', issue.tracker.to_s
147 assert_equal 'Feature request', issue.tracker.to_s
148 assert_equal 'Stock management', issue.category.to_s
148 assert_equal 'Stock management', issue.category.to_s
149 assert_equal 'Urgent', issue.priority.to_s
149 assert_equal 'Urgent', issue.priority.to_s
150 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
150 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
151 end
151 end
152
152
153 def test_add_issue_with_attachment_to_specific_project
153 def test_add_issue_with_attachment_to_specific_project
154 issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
154 issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
155 assert issue.is_a?(Issue)
155 assert issue.is_a?(Issue)
156 assert !issue.new_record?
156 assert !issue.new_record?
157 issue.reload
157 issue.reload
158 assert_equal 'Ticket created by email with attachment', issue.subject
158 assert_equal 'Ticket created by email with attachment', issue.subject
159 assert_equal User.find_by_login('jsmith'), issue.author
159 assert_equal User.find_by_login('jsmith'), issue.author
160 assert_equal Project.find(2), issue.project
160 assert_equal Project.find(2), issue.project
161 assert_equal 'This is a new ticket with attachments', issue.description
161 assert_equal 'This is a new ticket with attachments', issue.description
162 # Attachment properties
162 # Attachment properties
163 assert_equal 1, issue.attachments.size
163 assert_equal 1, issue.attachments.size
164 assert_equal 'Paella.jpg', issue.attachments.first.filename
164 assert_equal 'Paella.jpg', issue.attachments.first.filename
165 assert_equal 'image/jpeg', issue.attachments.first.content_type
165 assert_equal 'image/jpeg', issue.attachments.first.content_type
166 assert_equal 10790, issue.attachments.first.filesize
166 assert_equal 10790, issue.attachments.first.filesize
167 end
167 end
168
168
169 def test_add_issue_with_custom_fields
169 def test_add_issue_with_custom_fields
170 issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'onlinestore'})
170 issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'onlinestore'})
171 assert issue.is_a?(Issue)
171 assert issue.is_a?(Issue)
172 assert !issue.new_record?
172 assert !issue.new_record?
173 issue.reload
173 issue.reload
174 assert_equal 'New ticket with custom field values', issue.subject
174 assert_equal 'New ticket with custom field values', issue.subject
175 assert_equal 'Value for a custom field', issue.custom_value_for(CustomField.find_by_name('Searchable field')).value
175 assert_equal 'Value for a custom field', issue.custom_value_for(CustomField.find_by_name('Searchable field')).value
176 assert !issue.description.match(/^searchable field:/i)
176 assert !issue.description.match(/^searchable field:/i)
177 end
177 end
178
178
179 def test_add_issue_with_cc
179 def test_add_issue_with_cc
180 issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'})
180 issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'})
181 assert issue.is_a?(Issue)
181 assert issue.is_a?(Issue)
182 assert !issue.new_record?
182 assert !issue.new_record?
183 issue.reload
183 issue.reload
184 assert issue.watched_by?(User.find_by_mail('dlopper@somenet.foo'))
184 assert issue.watched_by?(User.find_by_mail('dlopper@somenet.foo'))
185 assert_equal 1, issue.watcher_user_ids.size
185 assert_equal 1, issue.watcher_user_ids.size
186 end
186 end
187
187
188 def test_add_issue_by_unknown_user
188 def test_add_issue_by_unknown_user
189 assert_no_difference 'User.count' do
189 assert_no_difference 'User.count' do
190 assert_equal false, submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'})
190 assert_equal false, submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'})
191 end
191 end
192 end
192 end
193
193
194 def test_add_issue_by_anonymous_user
194 def test_add_issue_by_anonymous_user
195 Role.anonymous.add_permission!(:add_issues)
195 Role.anonymous.add_permission!(:add_issues)
196 assert_no_difference 'User.count' do
196 assert_no_difference 'User.count' do
197 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'accept')
197 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'accept')
198 assert issue.is_a?(Issue)
198 assert issue.is_a?(Issue)
199 assert issue.author.anonymous?
199 assert issue.author.anonymous?
200 end
200 end
201 end
201 end
202
202
203 def test_add_issue_by_anonymous_user_with_no_from_address
203 def test_add_issue_by_anonymous_user_with_no_from_address
204 Role.anonymous.add_permission!(:add_issues)
204 Role.anonymous.add_permission!(:add_issues)
205 assert_no_difference 'User.count' do
205 assert_no_difference 'User.count' do
206 issue = submit_email('ticket_by_empty_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'accept')
206 issue = submit_email('ticket_by_empty_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'accept')
207 assert issue.is_a?(Issue)
207 assert issue.is_a?(Issue)
208 assert issue.author.anonymous?
208 assert issue.author.anonymous?
209 end
209 end
210 end
210 end
211
211
212 def test_add_issue_by_anonymous_user_on_private_project
212 def test_add_issue_by_anonymous_user_on_private_project
213 Role.anonymous.add_permission!(:add_issues)
213 Role.anonymous.add_permission!(:add_issues)
214 assert_no_difference 'User.count' do
214 assert_no_difference 'User.count' do
215 assert_no_difference 'Issue.count' do
215 assert_no_difference 'Issue.count' do
216 assert_equal false, submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'onlinestore'}, :unknown_user => 'accept')
216 assert_equal false, submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'onlinestore'}, :unknown_user => 'accept')
217 end
217 end
218 end
218 end
219 end
219 end
220
220
221 def test_add_issue_by_anonymous_user_on_private_project_without_permission_check
221 def test_add_issue_by_anonymous_user_on_private_project_without_permission_check
222 assert_no_difference 'User.count' do
222 assert_no_difference 'User.count' do
223 assert_difference 'Issue.count' do
223 assert_difference 'Issue.count' do
224 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'onlinestore'}, :no_permission_check => '1', :unknown_user => 'accept')
224 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'onlinestore'}, :no_permission_check => '1', :unknown_user => 'accept')
225 assert issue.is_a?(Issue)
225 assert issue.is_a?(Issue)
226 assert issue.author.anonymous?
226 assert issue.author.anonymous?
227 assert !issue.project.is_public?
227 assert !issue.project.is_public?
228 assert_equal [issue.id, 1, 2], [issue.root_id, issue.lft, issue.rgt]
228 assert_equal [issue.id, 1, 2], [issue.root_id, issue.lft, issue.rgt]
229 end
229 end
230 end
230 end
231 end
231 end
232
232
233 def test_add_issue_by_created_user
233 def test_add_issue_by_created_user
234 Setting.default_language = 'en'
234 Setting.default_language = 'en'
235 assert_difference 'User.count' do
235 assert_difference 'User.count' do
236 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'create')
236 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'create')
237 assert issue.is_a?(Issue)
237 assert issue.is_a?(Issue)
238 assert issue.author.active?
238 assert issue.author.active?
239 assert_equal 'john.doe@somenet.foo', issue.author.mail
239 assert_equal 'john.doe@somenet.foo', issue.author.mail
240 assert_equal 'John', issue.author.firstname
240 assert_equal 'John', issue.author.firstname
241 assert_equal 'Doe', issue.author.lastname
241 assert_equal 'Doe', issue.author.lastname
242
242
243 # account information
243 # account information
244 email = ActionMailer::Base.deliveries.first
244 email = ActionMailer::Base.deliveries.first
245 assert_not_nil email
245 assert_not_nil email
246 assert email.subject.include?('account activation')
246 assert email.subject.include?('account activation')
247 login = email.body.match(/\* Login: (.*)$/)[1]
247 login = email.body.match(/\* Login: (.*)$/)[1]
248 password = email.body.match(/\* Password: (.*)$/)[1]
248 password = email.body.match(/\* Password: (.*)$/)[1]
249 assert_equal issue.author, User.try_to_login(login, password)
249 assert_equal issue.author, User.try_to_login(login, password)
250 end
250 end
251 end
251 end
252
252
253 def test_add_issue_without_from_header
253 def test_add_issue_without_from_header
254 Role.anonymous.add_permission!(:add_issues)
254 Role.anonymous.add_permission!(:add_issues)
255 assert_equal false, submit_email('ticket_without_from_header.eml')
255 assert_equal false, submit_email('ticket_without_from_header.eml')
256 end
256 end
257
257
258 def test_add_issue_with_invalid_attributes
258 def test_add_issue_with_invalid_attributes
259 issue = submit_email('ticket_with_invalid_attributes.eml', :allow_override => 'tracker,category,priority')
259 issue = submit_email('ticket_with_invalid_attributes.eml', :allow_override => 'tracker,category,priority')
260 assert issue.is_a?(Issue)
260 assert issue.is_a?(Issue)
261 assert !issue.new_record?
261 assert !issue.new_record?
262 issue.reload
262 issue.reload
263 assert_nil issue.assigned_to
263 assert_nil issue.assigned_to
264 assert_nil issue.start_date
264 assert_nil issue.start_date
265 assert_nil issue.due_date
265 assert_nil issue.due_date
266 assert_equal 0, issue.done_ratio
266 assert_equal 0, issue.done_ratio
267 assert_equal 'Normal', issue.priority.to_s
267 assert_equal 'Normal', issue.priority.to_s
268 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
268 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
269 end
269 end
270
270
271 def test_add_issue_with_localized_attributes
271 def test_add_issue_with_localized_attributes
272 User.find_by_mail('jsmith@somenet.foo').update_attribute 'language', 'fr'
272 User.find_by_mail('jsmith@somenet.foo').update_attribute 'language', 'fr'
273 issue = submit_email('ticket_with_localized_attributes.eml', :allow_override => 'tracker,category,priority')
273 issue = submit_email('ticket_with_localized_attributes.eml', :allow_override => 'tracker,category,priority')
274 assert issue.is_a?(Issue)
274 assert issue.is_a?(Issue)
275 assert !issue.new_record?
275 assert !issue.new_record?
276 issue.reload
276 issue.reload
277 assert_equal 'New ticket on a given project', issue.subject
277 assert_equal 'New ticket on a given project', issue.subject
278 assert_equal User.find_by_login('jsmith'), issue.author
278 assert_equal User.find_by_login('jsmith'), issue.author
279 assert_equal Project.find(2), issue.project
279 assert_equal Project.find(2), issue.project
280 assert_equal 'Feature request', issue.tracker.to_s
280 assert_equal 'Feature request', issue.tracker.to_s
281 assert_equal 'Stock management', issue.category.to_s
281 assert_equal 'Stock management', issue.category.to_s
282 assert_equal 'Urgent', issue.priority.to_s
282 assert_equal 'Urgent', issue.priority.to_s
283 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
283 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
284 end
284 end
285
285
286 def test_add_issue_with_japanese_keywords
286 def test_add_issue_with_japanese_keywords
287 tracker = Tracker.create!(:name => 'ι–‹η™Ί')
287 tracker = Tracker.create!(:name => 'ι–‹η™Ί')
288 Project.find(1).trackers << tracker
288 Project.find(1).trackers << tracker
289 issue = submit_email('japanese_keywords_iso_2022_jp.eml', :issue => {:project => 'ecookbook'}, :allow_override => 'tracker')
289 issue = submit_email('japanese_keywords_iso_2022_jp.eml', :issue => {:project => 'ecookbook'}, :allow_override => 'tracker')
290 assert_kind_of Issue, issue
290 assert_kind_of Issue, issue
291 assert_equal tracker, issue.tracker
291 assert_equal tracker, issue.tracker
292 end
292 end
293
293
294 def test_add_issue_from_apple_mail
294 def test_add_issue_from_apple_mail
295 issue = submit_email('apple_mail_with_attachment.eml', :issue => {:project => 'ecookbook'})
295 issue = submit_email('apple_mail_with_attachment.eml', :issue => {:project => 'ecookbook'})
296 assert_kind_of Issue, issue
296 assert_kind_of Issue, issue
297 assert_equal 1, issue.attachments.size
297 assert_equal 1, issue.attachments.size
298
298
299 attachment = issue.attachments.first
299 attachment = issue.attachments.first
300 assert_equal 'paella.jpg', attachment.filename
300 assert_equal 'paella.jpg', attachment.filename
301 assert_equal 10790, attachment.filesize
301 assert_equal 10790, attachment.filesize
302 end
302 end
303
303
304 def test_should_ignore_emails_from_emission_address
304 def test_should_ignore_emails_from_emission_address
305 Role.anonymous.add_permission!(:add_issues)
305 Role.anonymous.add_permission!(:add_issues)
306 assert_no_difference 'User.count' do
306 assert_no_difference 'User.count' do
307 assert_equal false, submit_email('ticket_from_emission_address.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'create')
307 assert_equal false, submit_email('ticket_from_emission_address.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'create')
308 end
308 end
309 end
309 end
310
310
311 def test_add_issue_should_send_email_notification
311 def test_add_issue_should_send_email_notification
312 Setting.notified_events = ['issue_added']
312 Setting.notified_events = ['issue_added']
313 ActionMailer::Base.deliveries.clear
313 ActionMailer::Base.deliveries.clear
314 # This email contains: 'Project: onlinestore'
314 # This email contains: 'Project: onlinestore'
315 issue = submit_email('ticket_on_given_project.eml')
315 issue = submit_email('ticket_on_given_project.eml')
316 assert issue.is_a?(Issue)
316 assert issue.is_a?(Issue)
317 assert_equal 1, ActionMailer::Base.deliveries.size
317 assert_equal 1, ActionMailer::Base.deliveries.size
318 end
318 end
319
319
320 def test_update_issue
320 def test_update_issue
321 journal = submit_email('ticket_reply.eml')
321 journal = submit_email('ticket_reply.eml')
322 assert journal.is_a?(Journal)
322 assert journal.is_a?(Journal)
323 assert_equal User.find_by_login('jsmith'), journal.user
323 assert_equal User.find_by_login('jsmith'), journal.user
324 assert_equal Issue.find(2), journal.journalized
324 assert_equal Issue.find(2), journal.journalized
325 assert_match /This is reply/, journal.notes
325 assert_match /This is reply/, journal.notes
326 assert_equal 'Feature request', journal.issue.tracker.name
326 assert_equal 'Feature request', journal.issue.tracker.name
327 end
327 end
328
328
329 def test_update_issue_with_attribute_changes
329 def test_update_issue_with_attribute_changes
330 # This email contains: 'Status: Resolved'
330 # This email contains: 'Status: Resolved'
331 journal = submit_email('ticket_reply_with_status.eml')
331 journal = submit_email('ticket_reply_with_status.eml')
332 assert journal.is_a?(Journal)
332 assert journal.is_a?(Journal)
333 issue = Issue.find(journal.issue.id)
333 issue = Issue.find(journal.issue.id)
334 assert_equal User.find_by_login('jsmith'), journal.user
334 assert_equal User.find_by_login('jsmith'), journal.user
335 assert_equal Issue.find(2), journal.journalized
335 assert_equal Issue.find(2), journal.journalized
336 assert_match /This is reply/, journal.notes
336 assert_match /This is reply/, journal.notes
337 assert_equal 'Feature request', journal.issue.tracker.name
337 assert_equal 'Feature request', journal.issue.tracker.name
338 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
338 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
339 assert_equal '2010-01-01', issue.start_date.to_s
339 assert_equal '2010-01-01', issue.start_date.to_s
340 assert_equal '2010-12-31', issue.due_date.to_s
340 assert_equal '2010-12-31', issue.due_date.to_s
341 assert_equal User.find_by_login('jsmith'), issue.assigned_to
341 assert_equal User.find_by_login('jsmith'), issue.assigned_to
342 assert_equal "52.6", issue.custom_value_for(CustomField.find_by_name('Float field')).value
342 assert_equal "52.6", issue.custom_value_for(CustomField.find_by_name('Float field')).value
343 # keywords should be removed from the email body
343 # keywords should be removed from the email body
344 assert !journal.notes.match(/^Status:/i)
344 assert !journal.notes.match(/^Status:/i)
345 assert !journal.notes.match(/^Start Date:/i)
345 assert !journal.notes.match(/^Start Date:/i)
346 end
346 end
347
347
348 def test_update_issue_with_attachment
348 def test_update_issue_with_attachment
349 assert_difference 'Journal.count' do
349 assert_difference 'Journal.count' do
350 assert_difference 'JournalDetail.count' do
350 assert_difference 'JournalDetail.count' do
351 assert_difference 'Attachment.count' do
351 assert_difference 'Attachment.count' do
352 assert_no_difference 'Issue.count' do
352 assert_no_difference 'Issue.count' do
353 journal = submit_email('ticket_with_attachment.eml') do |raw|
353 journal = submit_email('ticket_with_attachment.eml') do |raw|
354 raw.gsub! /^Subject: .*$/, 'Subject: Re: [Cookbook - Feature #2] (New) Add ingredients categories'
354 raw.gsub! /^Subject: .*$/, 'Subject: Re: [Cookbook - Feature #2] (New) Add ingredients categories'
355 end
355 end
356 end
356 end
357 end
357 end
358 end
358 end
359 end
359 end
360 journal = Journal.first(:order => 'id DESC')
360 journal = Journal.first(:order => 'id DESC')
361 assert_equal Issue.find(2), journal.journalized
361 assert_equal Issue.find(2), journal.journalized
362 assert_equal 1, journal.details.size
362 assert_equal 1, journal.details.size
363
363
364 detail = journal.details.first
364 detail = journal.details.first
365 assert_equal 'attachment', detail.property
365 assert_equal 'attachment', detail.property
366 assert_equal 'Paella.jpg', detail.value
366 assert_equal 'Paella.jpg', detail.value
367 end
367 end
368
368
369 def test_update_issue_should_send_email_notification
369 def test_update_issue_should_send_email_notification
370 ActionMailer::Base.deliveries.clear
370 ActionMailer::Base.deliveries.clear
371 journal = submit_email('ticket_reply.eml')
371 journal = submit_email('ticket_reply.eml')
372 assert journal.is_a?(Journal)
372 assert journal.is_a?(Journal)
373 assert_equal 1, ActionMailer::Base.deliveries.size
373 assert_equal 1, ActionMailer::Base.deliveries.size
374 end
374 end
375
375
376 def test_update_issue_should_not_set_defaults
376 def test_update_issue_should_not_set_defaults
377 journal = submit_email('ticket_reply.eml', :issue => {:tracker => 'Support request', :priority => 'High'})
377 journal = submit_email('ticket_reply.eml', :issue => {:tracker => 'Support request', :priority => 'High'})
378 assert journal.is_a?(Journal)
378 assert journal.is_a?(Journal)
379 assert_match /This is reply/, journal.notes
379 assert_match /This is reply/, journal.notes
380 assert_equal 'Feature request', journal.issue.tracker.name
380 assert_equal 'Feature request', journal.issue.tracker.name
381 assert_equal 'Normal', journal.issue.priority.name
381 assert_equal 'Normal', journal.issue.priority.name
382 end
382 end
383
383
384 def test_reply_to_a_message
384 def test_reply_to_a_message
385 m = submit_email('message_reply.eml')
385 m = submit_email('message_reply.eml')
386 assert m.is_a?(Message)
386 assert m.is_a?(Message)
387 assert !m.new_record?
387 assert !m.new_record?
388 m.reload
388 m.reload
389 assert_equal 'Reply via email', m.subject
389 assert_equal 'Reply via email', m.subject
390 # The email replies to message #2 which is part of the thread of message #1
390 # The email replies to message #2 which is part of the thread of message #1
391 assert_equal Message.find(1), m.parent
391 assert_equal Message.find(1), m.parent
392 end
392 end
393
393
394 def test_reply_to_a_message_by_subject
394 def test_reply_to_a_message_by_subject
395 m = submit_email('message_reply_by_subject.eml')
395 m = submit_email('message_reply_by_subject.eml')
396 assert m.is_a?(Message)
396 assert m.is_a?(Message)
397 assert !m.new_record?
397 assert !m.new_record?
398 m.reload
398 m.reload
399 assert_equal 'Reply to the first post', m.subject
399 assert_equal 'Reply to the first post', m.subject
400 assert_equal Message.find(1), m.parent
400 assert_equal Message.find(1), m.parent
401 end
401 end
402
402
403 def test_should_strip_tags_of_html_only_emails
403 def test_should_strip_tags_of_html_only_emails
404 issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'})
404 issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'})
405 assert issue.is_a?(Issue)
405 assert issue.is_a?(Issue)
406 assert !issue.new_record?
406 assert !issue.new_record?
407 issue.reload
407 issue.reload
408 assert_equal 'HTML email', issue.subject
408 assert_equal 'HTML email', issue.subject
409 assert_equal 'This is a html-only email.', issue.description
409 assert_equal 'This is a html-only email.', issue.description
410 end
410 end
411
411
412 context "truncate emails based on the Setting" do
412 context "truncate emails based on the Setting" do
413 context "with no setting" do
413 context "with no setting" do
414 setup do
414 setup do
415 Setting.mail_handler_body_delimiters = ''
415 Setting.mail_handler_body_delimiters = ''
416 end
416 end
417
417
418 should "add the entire email into the issue" do
418 should "add the entire email into the issue" do
419 issue = submit_email('ticket_on_given_project.eml')
419 issue = submit_email('ticket_on_given_project.eml')
420 assert_issue_created(issue)
420 assert_issue_created(issue)
421 assert issue.description.include?('---')
421 assert issue.description.include?('---')
422 assert issue.description.include?('This paragraph is after the delimiter')
422 assert issue.description.include?('This paragraph is after the delimiter')
423 end
423 end
424 end
424 end
425
425
426 context "with a single string" do
426 context "with a single string" do
427 setup do
427 setup do
428 Setting.mail_handler_body_delimiters = '---'
428 Setting.mail_handler_body_delimiters = '---'
429 end
429 end
430 should "truncate the email at the delimiter for the issue" do
430 should "truncate the email at the delimiter for the issue" do
431 issue = submit_email('ticket_on_given_project.eml')
431 issue = submit_email('ticket_on_given_project.eml')
432 assert_issue_created(issue)
432 assert_issue_created(issue)
433 assert issue.description.include?('This paragraph is before delimiters')
433 assert issue.description.include?('This paragraph is before delimiters')
434 assert issue.description.include?('--- This line starts with a delimiter')
434 assert issue.description.include?('--- This line starts with a delimiter')
435 assert !issue.description.match(/^---$/)
435 assert !issue.description.match(/^---$/)
436 assert !issue.description.include?('This paragraph is after the delimiter')
436 assert !issue.description.include?('This paragraph is after the delimiter')
437 end
437 end
438 end
438 end
439
439
440 context "with a single quoted reply (e.g. reply to a Redmine email notification)" do
440 context "with a single quoted reply (e.g. reply to a Redmine email notification)" do
441 setup do
441 setup do
442 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
442 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
443 end
443 end
444 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
444 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
445 journal = submit_email('issue_update_with_quoted_reply_above.eml')
445 journal = submit_email('issue_update_with_quoted_reply_above.eml')
446 assert journal.is_a?(Journal)
446 assert journal.is_a?(Journal)
447 assert journal.notes.include?('An update to the issue by the sender.')
447 assert journal.notes.include?('An update to the issue by the sender.')
448 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
448 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
449 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
449 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
450 end
450 end
451 end
451 end
452
452
453 context "with multiple quoted replies (e.g. reply to a reply of a Redmine email notification)" do
453 context "with multiple quoted replies (e.g. reply to a reply of a Redmine email notification)" do
454 setup do
454 setup do
455 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
455 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
456 end
456 end
457 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
457 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
458 journal = submit_email('issue_update_with_multiple_quoted_reply_above.eml')
458 journal = submit_email('issue_update_with_multiple_quoted_reply_above.eml')
459 assert journal.is_a?(Journal)
459 assert journal.is_a?(Journal)
460 assert journal.notes.include?('An update to the issue by the sender.')
460 assert journal.notes.include?('An update to the issue by the sender.')
461 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
461 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
462 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
462 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
463 end
463 end
464 end
464 end
465
465
466 context "with multiple strings" do
466 context "with multiple strings" do
467 setup do
467 setup do
468 Setting.mail_handler_body_delimiters = "---\nBREAK"
468 Setting.mail_handler_body_delimiters = "---\nBREAK"
469 end
469 end
470 should "truncate the email at the first delimiter found (BREAK)" do
470 should "truncate the email at the first delimiter found (BREAK)" do
471 issue = submit_email('ticket_on_given_project.eml')
471 issue = submit_email('ticket_on_given_project.eml')
472 assert_issue_created(issue)
472 assert_issue_created(issue)
473 assert issue.description.include?('This paragraph is before delimiters')
473 assert issue.description.include?('This paragraph is before delimiters')
474 assert !issue.description.include?('BREAK')
474 assert !issue.description.include?('BREAK')
475 assert !issue.description.include?('This paragraph is between delimiters')
475 assert !issue.description.include?('This paragraph is between delimiters')
476 assert !issue.description.match(/^---$/)
476 assert !issue.description.match(/^---$/)
477 assert !issue.description.include?('This paragraph is after the delimiter')
477 assert !issue.description.include?('This paragraph is after the delimiter')
478 end
478 end
479 end
479 end
480 end
480 end
481
481
482 def test_email_with_long_subject_line
482 def test_email_with_long_subject_line
483 issue = submit_email('ticket_with_long_subject.eml')
483 issue = submit_email('ticket_with_long_subject.eml')
484 assert issue.is_a?(Issue)
484 assert issue.is_a?(Issue)
485 assert_equal issue.subject, 'New ticket on a given project with a very long subject line which exceeds 255 chars and should not be ignored but chopped off. And if the subject line is still not long enough, we just add more text. And more text. Wow, this is really annoying. Especially, if you have nothing to say...'[0,255]
485 assert_equal issue.subject, 'New ticket on a given project with a very long subject line which exceeds 255 chars and should not be ignored but chopped off. And if the subject line is still not long enough, we just add more text. And more text. Wow, this is really annoying. Especially, if you have nothing to say...'[0,255]
486 end
486 end
487
487
488 def test_new_user_from_attributes_should_return_valid_user
489 to_test = {
490 # [address, name] => [login, firstname, lastname]
491 ['jsmith@example.net', nil] => ['jsmith@example.net', 'jsmith', '-'],
492 ['jsmith@example.net', 'John'] => ['jsmith@example.net', 'John', '-'],
493 ['jsmith@example.net', 'John Smith'] => ['jsmith@example.net', 'John', 'Smith'],
494 ['jsmith@example.net', 'John Paul Smith'] => ['jsmith@example.net', 'John', 'Paul Smith'],
495 ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsTheMaximumLength Smith'] => ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsT', 'Smith'],
496 ['jsmith@example.net', 'John AVeryLongLastnameThatExceedsTheMaximumLength'] => ['jsmith@example.net', 'John', 'AVeryLongLastnameThatExceedsTh'],
497 ['alongemailaddressthatexceedsloginlength@example.net', 'John Smith'] => ['alongemailaddressthatexceedslo', 'John', 'Smith']
498 }
499
500 to_test.each do |attrs, expected|
501 user = MailHandler.new_user_from_attributes(attrs.first, attrs.last)
502
503 assert user.valid?
504 assert_equal attrs.first, user.mail
505 assert_equal expected[0], user.login
506 assert_equal expected[1], user.firstname
507 assert_equal expected[2], user.lastname
508 end
509 end
510
511 def test_new_user_from_attributes_should_respect_minimum_password_length
512 with_settings :password_min_length => 15 do
513 user = MailHandler.new_user_from_attributes('jsmith@example.net')
514 assert user.valid?
515 assert user.password.length >= 15
516 end
517 end
518
519 def test_new_user_from_attributes_should_use_default_login_if_invalid
520 MailHandler.new_user_from_attributes('alongemailaddressthatexceedsloginlength-1@example.net').save!
521
522 # another long address that would result in duplicate login
523 user = MailHandler.new_user_from_attributes('alongemailaddressthatexceedsloginlength-2@example.net')
524 assert user.valid?
525 assert user.login =~ /^user[a-f0-9]+$/
526 end
527
488 private
528 private
489
529
490 def submit_email(filename, options={})
530 def submit_email(filename, options={})
491 raw = IO.read(File.join(FIXTURES_PATH, filename))
531 raw = IO.read(File.join(FIXTURES_PATH, filename))
492 yield raw if block_given?
532 yield raw if block_given?
493 MailHandler.receive(raw, options)
533 MailHandler.receive(raw, options)
494 end
534 end
495
535
496 def assert_issue_created(issue)
536 def assert_issue_created(issue)
497 assert issue.is_a?(Issue)
537 assert issue.is_a?(Issue)
498 assert !issue.new_record?
538 assert !issue.new_record?
499 issue.reload
539 issue.reload
500 end
540 end
501 end
541 end
General Comments 0
You need to be logged in to leave comments. Login now