##// END OF EJS Templates
Allow a default version to be set on the command line for incoming emails (#7346)....
Jean-Philippe Lang -
r14407:0df7fa714b42
parent child
Show More
@@ -1,567 +1,567
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 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, :handler_options
25 attr_reader :email, :user, :handler_options
26
26
27 def self.receive(raw_mail, options={})
27 def self.receive(raw_mail, options={})
28 options = options.deep_dup
28 options = options.deep_dup
29
29
30 options[:issue] ||= {}
30 options[:issue] ||= {}
31
31
32 options[:allow_override] ||= []
32 options[:allow_override] ||= []
33 if options[:allow_override].is_a?(String)
33 if options[:allow_override].is_a?(String)
34 options[:allow_override] = options[:allow_override].split(',')
34 options[:allow_override] = options[:allow_override].split(',')
35 end
35 end
36 options[:allow_override].map! {|s| s.strip.downcase.gsub(/\s+/, '_')}
36 options[:allow_override].map! {|s| s.strip.downcase.gsub(/\s+/, '_')}
37 # Project needs to be overridable if not specified
37 # Project needs to be overridable if not specified
38 options[:allow_override] << 'project' unless options[:issue].has_key?(:project)
38 options[:allow_override] << 'project' unless options[:issue].has_key?(:project)
39
39
40 options[:no_account_notice] = (options[:no_account_notice].to_s == '1')
40 options[:no_account_notice] = (options[:no_account_notice].to_s == '1')
41 options[:no_notification] = (options[:no_notification].to_s == '1')
41 options[:no_notification] = (options[:no_notification].to_s == '1')
42 options[:no_permission_check] = (options[:no_permission_check].to_s == '1')
42 options[:no_permission_check] = (options[:no_permission_check].to_s == '1')
43
43
44 raw_mail.force_encoding('ASCII-8BIT')
44 raw_mail.force_encoding('ASCII-8BIT')
45
45
46 ActiveSupport::Notifications.instrument("receive.action_mailer") do |payload|
46 ActiveSupport::Notifications.instrument("receive.action_mailer") do |payload|
47 mail = Mail.new(raw_mail)
47 mail = Mail.new(raw_mail)
48 set_payload_for_mail(payload, mail)
48 set_payload_for_mail(payload, mail)
49 new.receive(mail, options)
49 new.receive(mail, options)
50 end
50 end
51 end
51 end
52
52
53 # Receives an email and rescues any exception
53 # Receives an email and rescues any exception
54 def self.safe_receive(*args)
54 def self.safe_receive(*args)
55 receive(*args)
55 receive(*args)
56 rescue Exception => e
56 rescue Exception => e
57 logger.error "An unexpected error occurred when receiving email: #{e.message}" if logger
57 logger.error "An unexpected error occurred when receiving email: #{e.message}" if logger
58 return false
58 return false
59 end
59 end
60
60
61 # Extracts MailHandler options from environment variables
61 # Extracts MailHandler options from environment variables
62 # Use when receiving emails with rake tasks
62 # Use when receiving emails with rake tasks
63 def self.extract_options_from_env(env)
63 def self.extract_options_from_env(env)
64 options = {:issue => {}}
64 options = {:issue => {}}
65 %w(project status tracker category priority).each do |option|
65 %w(project status tracker category priority fixed_version).each do |option|
66 options[:issue][option.to_sym] = env[option] if env[option]
66 options[:issue][option.to_sym] = env[option] if env[option]
67 end
67 end
68 %w(allow_override unknown_user no_permission_check no_account_notice default_group project_from_subaddress).each do |option|
68 %w(allow_override unknown_user no_permission_check no_account_notice default_group project_from_subaddress).each do |option|
69 options[option.to_sym] = env[option] if env[option]
69 options[option.to_sym] = env[option] if env[option]
70 end
70 end
71 if env['private']
71 if env['private']
72 options[:issue][:is_private] = '1'
72 options[:issue][:is_private] = '1'
73 end
73 end
74 options
74 options
75 end
75 end
76
76
77 def logger
77 def logger
78 Rails.logger
78 Rails.logger
79 end
79 end
80
80
81 cattr_accessor :ignored_emails_headers
81 cattr_accessor :ignored_emails_headers
82 self.ignored_emails_headers = {
82 self.ignored_emails_headers = {
83 'Auto-Submitted' => /\Aauto-(replied|generated)/,
83 'Auto-Submitted' => /\Aauto-(replied|generated)/,
84 'X-Autoreply' => 'yes'
84 'X-Autoreply' => 'yes'
85 }
85 }
86
86
87 # Processes incoming emails
87 # Processes incoming emails
88 # Returns the created object (eg. an issue, a message) or false
88 # Returns the created object (eg. an issue, a message) or false
89 def receive(email, options={})
89 def receive(email, options={})
90 @email = email
90 @email = email
91 @handler_options = options
91 @handler_options = options
92 sender_email = email.from.to_a.first.to_s.strip
92 sender_email = email.from.to_a.first.to_s.strip
93 # Ignore emails received from the application emission address to avoid hell cycles
93 # Ignore emails received from the application emission address to avoid hell cycles
94 if sender_email.casecmp(Setting.mail_from.to_s.strip) == 0
94 if sender_email.casecmp(Setting.mail_from.to_s.strip) == 0
95 if logger
95 if logger
96 logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]"
96 logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]"
97 end
97 end
98 return false
98 return false
99 end
99 end
100 # Ignore auto generated emails
100 # Ignore auto generated emails
101 self.class.ignored_emails_headers.each do |key, ignored_value|
101 self.class.ignored_emails_headers.each do |key, ignored_value|
102 value = email.header[key]
102 value = email.header[key]
103 if value
103 if value
104 value = value.to_s.downcase
104 value = value.to_s.downcase
105 if (ignored_value.is_a?(Regexp) && value.match(ignored_value)) || value == ignored_value
105 if (ignored_value.is_a?(Regexp) && value.match(ignored_value)) || value == ignored_value
106 if logger
106 if logger
107 logger.info "MailHandler: ignoring email with #{key}:#{value} header"
107 logger.info "MailHandler: ignoring email with #{key}:#{value} header"
108 end
108 end
109 return false
109 return false
110 end
110 end
111 end
111 end
112 end
112 end
113 @user = User.find_by_mail(sender_email) if sender_email.present?
113 @user = User.find_by_mail(sender_email) if sender_email.present?
114 if @user && !@user.active?
114 if @user && !@user.active?
115 if logger
115 if logger
116 logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]"
116 logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]"
117 end
117 end
118 return false
118 return false
119 end
119 end
120 if @user.nil?
120 if @user.nil?
121 # Email was submitted by an unknown user
121 # Email was submitted by an unknown user
122 case handler_options[:unknown_user]
122 case handler_options[:unknown_user]
123 when 'accept'
123 when 'accept'
124 @user = User.anonymous
124 @user = User.anonymous
125 when 'create'
125 when 'create'
126 @user = create_user_from_email
126 @user = create_user_from_email
127 if @user
127 if @user
128 if logger
128 if logger
129 logger.info "MailHandler: [#{@user.login}] account created"
129 logger.info "MailHandler: [#{@user.login}] account created"
130 end
130 end
131 add_user_to_group(handler_options[:default_group])
131 add_user_to_group(handler_options[:default_group])
132 unless handler_options[:no_account_notice]
132 unless handler_options[:no_account_notice]
133 Mailer.account_information(@user, @user.password).deliver
133 Mailer.account_information(@user, @user.password).deliver
134 end
134 end
135 else
135 else
136 if logger
136 if logger
137 logger.error "MailHandler: could not create account for [#{sender_email}]"
137 logger.error "MailHandler: could not create account for [#{sender_email}]"
138 end
138 end
139 return false
139 return false
140 end
140 end
141 else
141 else
142 # Default behaviour, emails from unknown users are ignored
142 # Default behaviour, emails from unknown users are ignored
143 if logger
143 if logger
144 logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]"
144 logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]"
145 end
145 end
146 return false
146 return false
147 end
147 end
148 end
148 end
149 User.current = @user
149 User.current = @user
150 dispatch
150 dispatch
151 end
151 end
152
152
153 private
153 private
154
154
155 MESSAGE_ID_RE = %r{^<?redmine\.([a-z0-9_]+)\-(\d+)\.\d+(\.[a-f0-9]+)?@}
155 MESSAGE_ID_RE = %r{^<?redmine\.([a-z0-9_]+)\-(\d+)\.\d+(\.[a-f0-9]+)?@}
156 ISSUE_REPLY_SUBJECT_RE = %r{\[(?:[^\]]*\s+)?#(\d+)\]}
156 ISSUE_REPLY_SUBJECT_RE = %r{\[(?:[^\]]*\s+)?#(\d+)\]}
157 MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
157 MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
158
158
159 def dispatch
159 def dispatch
160 headers = [email.in_reply_to, email.references].flatten.compact
160 headers = [email.in_reply_to, email.references].flatten.compact
161 subject = email.subject.to_s
161 subject = email.subject.to_s
162 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
162 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
163 klass, object_id = $1, $2.to_i
163 klass, object_id = $1, $2.to_i
164 method_name = "receive_#{klass}_reply"
164 method_name = "receive_#{klass}_reply"
165 if self.class.private_instance_methods.collect(&:to_s).include?(method_name)
165 if self.class.private_instance_methods.collect(&:to_s).include?(method_name)
166 send method_name, object_id
166 send method_name, object_id
167 else
167 else
168 # ignoring it
168 # ignoring it
169 end
169 end
170 elsif m = subject.match(ISSUE_REPLY_SUBJECT_RE)
170 elsif m = subject.match(ISSUE_REPLY_SUBJECT_RE)
171 receive_issue_reply(m[1].to_i)
171 receive_issue_reply(m[1].to_i)
172 elsif m = subject.match(MESSAGE_REPLY_SUBJECT_RE)
172 elsif m = subject.match(MESSAGE_REPLY_SUBJECT_RE)
173 receive_message_reply(m[1].to_i)
173 receive_message_reply(m[1].to_i)
174 else
174 else
175 dispatch_to_default
175 dispatch_to_default
176 end
176 end
177 rescue ActiveRecord::RecordInvalid => e
177 rescue ActiveRecord::RecordInvalid => e
178 # TODO: send a email to the user
178 # TODO: send a email to the user
179 logger.error e.message if logger
179 logger.error e.message if logger
180 false
180 false
181 rescue MissingInformation => e
181 rescue MissingInformation => e
182 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
182 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
183 false
183 false
184 rescue UnauthorizedAction => e
184 rescue UnauthorizedAction => e
185 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
185 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
186 false
186 false
187 end
187 end
188
188
189 def dispatch_to_default
189 def dispatch_to_default
190 receive_issue
190 receive_issue
191 end
191 end
192
192
193 # Creates a new issue
193 # Creates a new issue
194 def receive_issue
194 def receive_issue
195 project = target_project
195 project = target_project
196 # check permission
196 # check permission
197 unless handler_options[:no_permission_check]
197 unless handler_options[:no_permission_check]
198 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
198 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
199 end
199 end
200
200
201 issue = Issue.new(:author => user, :project => project)
201 issue = Issue.new(:author => user, :project => project)
202 issue.safe_attributes = issue_attributes_from_keywords(issue)
202 issue.safe_attributes = issue_attributes_from_keywords(issue)
203 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
203 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
204 issue.subject = cleaned_up_subject
204 issue.subject = cleaned_up_subject
205 if issue.subject.blank?
205 if issue.subject.blank?
206 issue.subject = '(no subject)'
206 issue.subject = '(no subject)'
207 end
207 end
208 issue.description = cleaned_up_text_body
208 issue.description = cleaned_up_text_body
209 issue.start_date ||= Date.today if Setting.default_issue_start_date_to_creation_date?
209 issue.start_date ||= Date.today if Setting.default_issue_start_date_to_creation_date?
210 issue.is_private = (handler_options[:issue][:is_private] == '1')
210 issue.is_private = (handler_options[:issue][:is_private] == '1')
211
211
212 # add To and Cc as watchers before saving so the watchers can reply to Redmine
212 # add To and Cc as watchers before saving so the watchers can reply to Redmine
213 add_watchers(issue)
213 add_watchers(issue)
214 issue.save!
214 issue.save!
215 add_attachments(issue)
215 add_attachments(issue)
216 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger
216 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger
217 issue
217 issue
218 end
218 end
219
219
220 # Adds a note to an existing issue
220 # Adds a note to an existing issue
221 def receive_issue_reply(issue_id, from_journal=nil)
221 def receive_issue_reply(issue_id, from_journal=nil)
222 issue = Issue.find_by_id(issue_id)
222 issue = Issue.find_by_id(issue_id)
223 return unless issue
223 return unless issue
224 # check permission
224 # check permission
225 unless handler_options[:no_permission_check]
225 unless handler_options[:no_permission_check]
226 unless user.allowed_to?(:add_issue_notes, issue.project) ||
226 unless user.allowed_to?(:add_issue_notes, issue.project) ||
227 user.allowed_to?(:edit_issues, issue.project)
227 user.allowed_to?(:edit_issues, issue.project)
228 raise UnauthorizedAction
228 raise UnauthorizedAction
229 end
229 end
230 end
230 end
231
231
232 # ignore CLI-supplied defaults for new issues
232 # ignore CLI-supplied defaults for new issues
233 handler_options[:issue].clear
233 handler_options[:issue].clear
234
234
235 journal = issue.init_journal(user)
235 journal = issue.init_journal(user)
236 if from_journal && from_journal.private_notes?
236 if from_journal && from_journal.private_notes?
237 # If the received email was a reply to a private note, make the added note private
237 # If the received email was a reply to a private note, make the added note private
238 issue.private_notes = true
238 issue.private_notes = true
239 end
239 end
240 issue.safe_attributes = issue_attributes_from_keywords(issue)
240 issue.safe_attributes = issue_attributes_from_keywords(issue)
241 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
241 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
242 journal.notes = cleaned_up_text_body
242 journal.notes = cleaned_up_text_body
243 add_attachments(issue)
243 add_attachments(issue)
244 issue.save!
244 issue.save!
245 if logger
245 if logger
246 logger.info "MailHandler: issue ##{issue.id} updated by #{user}"
246 logger.info "MailHandler: issue ##{issue.id} updated by #{user}"
247 end
247 end
248 journal
248 journal
249 end
249 end
250
250
251 # Reply will be added to the issue
251 # Reply will be added to the issue
252 def receive_journal_reply(journal_id)
252 def receive_journal_reply(journal_id)
253 journal = Journal.find_by_id(journal_id)
253 journal = Journal.find_by_id(journal_id)
254 if journal && journal.journalized_type == 'Issue'
254 if journal && journal.journalized_type == 'Issue'
255 receive_issue_reply(journal.journalized_id, journal)
255 receive_issue_reply(journal.journalized_id, journal)
256 end
256 end
257 end
257 end
258
258
259 # Receives a reply to a forum message
259 # Receives a reply to a forum message
260 def receive_message_reply(message_id)
260 def receive_message_reply(message_id)
261 message = Message.find_by_id(message_id)
261 message = Message.find_by_id(message_id)
262 if message
262 if message
263 message = message.root
263 message = message.root
264
264
265 unless handler_options[:no_permission_check]
265 unless handler_options[:no_permission_check]
266 raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
266 raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
267 end
267 end
268
268
269 if !message.locked?
269 if !message.locked?
270 reply = Message.new(:subject => cleaned_up_subject.gsub(%r{^.*msg\d+\]}, '').strip,
270 reply = Message.new(:subject => cleaned_up_subject.gsub(%r{^.*msg\d+\]}, '').strip,
271 :content => cleaned_up_text_body)
271 :content => cleaned_up_text_body)
272 reply.author = user
272 reply.author = user
273 reply.board = message.board
273 reply.board = message.board
274 message.children << reply
274 message.children << reply
275 add_attachments(reply)
275 add_attachments(reply)
276 reply
276 reply
277 else
277 else
278 if logger
278 if logger
279 logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic"
279 logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic"
280 end
280 end
281 end
281 end
282 end
282 end
283 end
283 end
284
284
285 def add_attachments(obj)
285 def add_attachments(obj)
286 if email.attachments && email.attachments.any?
286 if email.attachments && email.attachments.any?
287 email.attachments.each do |attachment|
287 email.attachments.each do |attachment|
288 next unless accept_attachment?(attachment)
288 next unless accept_attachment?(attachment)
289 obj.attachments << Attachment.create(:container => obj,
289 obj.attachments << Attachment.create(:container => obj,
290 :file => attachment.decoded,
290 :file => attachment.decoded,
291 :filename => attachment.filename,
291 :filename => attachment.filename,
292 :author => user,
292 :author => user,
293 :content_type => attachment.mime_type)
293 :content_type => attachment.mime_type)
294 end
294 end
295 end
295 end
296 end
296 end
297
297
298 # Returns false if the +attachment+ of the incoming email should be ignored
298 # Returns false if the +attachment+ of the incoming email should be ignored
299 def accept_attachment?(attachment)
299 def accept_attachment?(attachment)
300 @excluded ||= Setting.mail_handler_excluded_filenames.to_s.split(',').map(&:strip).reject(&:blank?)
300 @excluded ||= Setting.mail_handler_excluded_filenames.to_s.split(',').map(&:strip).reject(&:blank?)
301 @excluded.each do |pattern|
301 @excluded.each do |pattern|
302 regexp = %r{\A#{Regexp.escape(pattern).gsub("\\*", ".*")}\z}i
302 regexp = %r{\A#{Regexp.escape(pattern).gsub("\\*", ".*")}\z}i
303 if attachment.filename.to_s =~ regexp
303 if attachment.filename.to_s =~ regexp
304 logger.info "MailHandler: ignoring attachment #{attachment.filename} matching #{pattern}"
304 logger.info "MailHandler: ignoring attachment #{attachment.filename} matching #{pattern}"
305 return false
305 return false
306 end
306 end
307 end
307 end
308 true
308 true
309 end
309 end
310
310
311 # Adds To and Cc as watchers of the given object if the sender has the
311 # Adds To and Cc as watchers of the given object if the sender has the
312 # appropriate permission
312 # appropriate permission
313 def add_watchers(obj)
313 def add_watchers(obj)
314 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
314 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
315 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
315 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
316 unless addresses.empty?
316 unless addresses.empty?
317 User.active.having_mail(addresses).each do |w|
317 User.active.having_mail(addresses).each do |w|
318 obj.add_watcher(w)
318 obj.add_watcher(w)
319 end
319 end
320 end
320 end
321 end
321 end
322 end
322 end
323
323
324 def get_keyword(attr, options={})
324 def get_keyword(attr, options={})
325 @keywords ||= {}
325 @keywords ||= {}
326 if @keywords.has_key?(attr)
326 if @keywords.has_key?(attr)
327 @keywords[attr]
327 @keywords[attr]
328 else
328 else
329 @keywords[attr] = begin
329 @keywords[attr] = begin
330 override = options.key?(:override) ?
330 override = options.key?(:override) ?
331 options[:override] :
331 options[:override] :
332 (handler_options[:allow_override] & [attr.to_s.downcase.gsub(/\s+/, '_'), 'all']).present?
332 (handler_options[:allow_override] & [attr.to_s.downcase.gsub(/\s+/, '_'), 'all']).present?
333
333
334 if override && (v = extract_keyword!(cleaned_up_text_body, attr, options[:format]))
334 if override && (v = extract_keyword!(cleaned_up_text_body, attr, options[:format]))
335 v
335 v
336 elsif !handler_options[:issue][attr].blank?
336 elsif !handler_options[:issue][attr].blank?
337 handler_options[:issue][attr]
337 handler_options[:issue][attr]
338 end
338 end
339 end
339 end
340 end
340 end
341 end
341 end
342
342
343 # Destructively extracts the value for +attr+ in +text+
343 # Destructively extracts the value for +attr+ in +text+
344 # Returns nil if no matching keyword found
344 # Returns nil if no matching keyword found
345 def extract_keyword!(text, attr, format=nil)
345 def extract_keyword!(text, attr, format=nil)
346 keys = [attr.to_s.humanize]
346 keys = [attr.to_s.humanize]
347 if attr.is_a?(Symbol)
347 if attr.is_a?(Symbol)
348 if user && user.language.present?
348 if user && user.language.present?
349 keys << l("field_#{attr}", :default => '', :locale => user.language)
349 keys << l("field_#{attr}", :default => '', :locale => user.language)
350 end
350 end
351 if Setting.default_language.present?
351 if Setting.default_language.present?
352 keys << l("field_#{attr}", :default => '', :locale => Setting.default_language)
352 keys << l("field_#{attr}", :default => '', :locale => Setting.default_language)
353 end
353 end
354 end
354 end
355 keys.reject! {|k| k.blank?}
355 keys.reject! {|k| k.blank?}
356 keys.collect! {|k| Regexp.escape(k)}
356 keys.collect! {|k| Regexp.escape(k)}
357 format ||= '.+'
357 format ||= '.+'
358 keyword = nil
358 keyword = nil
359 regexp = /^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i
359 regexp = /^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i
360 if m = text.match(regexp)
360 if m = text.match(regexp)
361 keyword = m[2].strip
361 keyword = m[2].strip
362 text.sub!(regexp, '')
362 text.sub!(regexp, '')
363 end
363 end
364 keyword
364 keyword
365 end
365 end
366
366
367 def get_project_from_receiver_addresses
367 def get_project_from_receiver_addresses
368 local, domain = handler_options[:project_from_subaddress].to_s.split("@")
368 local, domain = handler_options[:project_from_subaddress].to_s.split("@")
369 return nil unless local && domain
369 return nil unless local && domain
370 local = Regexp.escape(local)
370 local = Regexp.escape(local)
371
371
372 [:to, :cc, :bcc].each do |field|
372 [:to, :cc, :bcc].each do |field|
373 header = @email[field]
373 header = @email[field]
374 next if header.blank? || header.field.blank? || !header.field.respond_to?(:addrs)
374 next if header.blank? || header.field.blank? || !header.field.respond_to?(:addrs)
375 header.field.addrs.each do |addr|
375 header.field.addrs.each do |addr|
376 if addr.domain.to_s.casecmp(domain)==0 && addr.local.to_s =~ /\A#{local}\+([^+]+)\z/
376 if addr.domain.to_s.casecmp(domain)==0 && addr.local.to_s =~ /\A#{local}\+([^+]+)\z/
377 if project = Project.find_by_identifier($1)
377 if project = Project.find_by_identifier($1)
378 return project
378 return project
379 end
379 end
380 end
380 end
381 end
381 end
382 end
382 end
383 nil
383 nil
384 end
384 end
385
385
386 def target_project
386 def target_project
387 # TODO: other ways to specify project:
387 # TODO: other ways to specify project:
388 # * parse the email To field
388 # * parse the email To field
389 # * specific project (eg. Setting.mail_handler_target_project)
389 # * specific project (eg. Setting.mail_handler_target_project)
390 target = get_project_from_receiver_addresses
390 target = get_project_from_receiver_addresses
391 target ||= Project.find_by_identifier(get_keyword(:project))
391 target ||= Project.find_by_identifier(get_keyword(:project))
392 if target.nil?
392 if target.nil?
393 # Invalid project keyword, use the project specified as the default one
393 # Invalid project keyword, use the project specified as the default one
394 default_project = handler_options[:issue][:project]
394 default_project = handler_options[:issue][:project]
395 if default_project.present?
395 if default_project.present?
396 target = Project.find_by_identifier(default_project)
396 target = Project.find_by_identifier(default_project)
397 end
397 end
398 end
398 end
399 raise MissingInformation.new('Unable to determine target project') if target.nil?
399 raise MissingInformation.new('Unable to determine target project') if target.nil?
400 target
400 target
401 end
401 end
402
402
403 # Returns a Hash of issue attributes extracted from keywords in the email body
403 # Returns a Hash of issue attributes extracted from keywords in the email body
404 def issue_attributes_from_keywords(issue)
404 def issue_attributes_from_keywords(issue)
405 attrs = {
405 attrs = {
406 'tracker_id' => (k = get_keyword(:tracker)) && issue.project.trackers.named(k).first.try(:id),
406 'tracker_id' => (k = get_keyword(:tracker)) && issue.project.trackers.named(k).first.try(:id),
407 'status_id' => (k = get_keyword(:status)) && IssueStatus.named(k).first.try(:id),
407 'status_id' => (k = get_keyword(:status)) && IssueStatus.named(k).first.try(:id),
408 'priority_id' => (k = get_keyword(:priority)) && IssuePriority.named(k).first.try(:id),
408 'priority_id' => (k = get_keyword(:priority)) && IssuePriority.named(k).first.try(:id),
409 'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.named(k).first.try(:id),
409 'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.named(k).first.try(:id),
410 'assigned_to_id' => (k = get_keyword(:assigned_to)) && find_assignee_from_keyword(k, issue).try(:id),
410 'assigned_to_id' => (k = get_keyword(:assigned_to)) && find_assignee_from_keyword(k, issue).try(:id),
411 'fixed_version_id' => (k = get_keyword(:fixed_version)) && issue.project.shared_versions.named(k).first.try(:id),
411 'fixed_version_id' => (k = get_keyword(:fixed_version)) && issue.project.shared_versions.named(k).first.try(:id),
412 'start_date' => get_keyword(:start_date, :format => '\d{4}-\d{2}-\d{2}'),
412 'start_date' => get_keyword(:start_date, :format => '\d{4}-\d{2}-\d{2}'),
413 'due_date' => get_keyword(:due_date, :format => '\d{4}-\d{2}-\d{2}'),
413 'due_date' => get_keyword(:due_date, :format => '\d{4}-\d{2}-\d{2}'),
414 'estimated_hours' => get_keyword(:estimated_hours),
414 'estimated_hours' => get_keyword(:estimated_hours),
415 'done_ratio' => get_keyword(:done_ratio, :format => '(\d|10)?0')
415 'done_ratio' => get_keyword(:done_ratio, :format => '(\d|10)?0')
416 }.delete_if {|k, v| v.blank? }
416 }.delete_if {|k, v| v.blank? }
417
417
418 if issue.new_record? && attrs['tracker_id'].nil?
418 if issue.new_record? && attrs['tracker_id'].nil?
419 attrs['tracker_id'] = issue.project.trackers.first.try(:id)
419 attrs['tracker_id'] = issue.project.trackers.first.try(:id)
420 end
420 end
421
421
422 attrs
422 attrs
423 end
423 end
424
424
425 # Returns a Hash of issue custom field values extracted from keywords in the email body
425 # Returns a Hash of issue custom field values extracted from keywords in the email body
426 def custom_field_values_from_keywords(customized)
426 def custom_field_values_from_keywords(customized)
427 customized.custom_field_values.inject({}) do |h, v|
427 customized.custom_field_values.inject({}) do |h, v|
428 if keyword = get_keyword(v.custom_field.name)
428 if keyword = get_keyword(v.custom_field.name)
429 h[v.custom_field.id.to_s] = v.custom_field.value_from_keyword(keyword, customized)
429 h[v.custom_field.id.to_s] = v.custom_field.value_from_keyword(keyword, customized)
430 end
430 end
431 h
431 h
432 end
432 end
433 end
433 end
434
434
435 # Returns the text/plain part of the email
435 # Returns the text/plain part of the email
436 # If not found (eg. HTML-only email), returns the body with tags removed
436 # If not found (eg. HTML-only email), returns the body with tags removed
437 def plain_text_body
437 def plain_text_body
438 return @plain_text_body unless @plain_text_body.nil?
438 return @plain_text_body unless @plain_text_body.nil?
439
439
440 parts = if (text_parts = email.all_parts.select {|p| p.mime_type == 'text/plain'}).present?
440 parts = if (text_parts = email.all_parts.select {|p| p.mime_type == 'text/plain'}).present?
441 text_parts
441 text_parts
442 elsif (html_parts = email.all_parts.select {|p| p.mime_type == 'text/html'}).present?
442 elsif (html_parts = email.all_parts.select {|p| p.mime_type == 'text/html'}).present?
443 html_parts
443 html_parts
444 else
444 else
445 [email]
445 [email]
446 end
446 end
447
447
448 parts.reject! do |part|
448 parts.reject! do |part|
449 part.attachment?
449 part.attachment?
450 end
450 end
451
451
452 @plain_text_body = parts.map do |p|
452 @plain_text_body = parts.map do |p|
453 body_charset = Mail::RubyVer.respond_to?(:pick_encoding) ?
453 body_charset = Mail::RubyVer.respond_to?(:pick_encoding) ?
454 Mail::RubyVer.pick_encoding(p.charset).to_s : p.charset
454 Mail::RubyVer.pick_encoding(p.charset).to_s : p.charset
455
455
456 body = Redmine::CodesetUtil.to_utf8(p.body.decoded, body_charset)
456 body = Redmine::CodesetUtil.to_utf8(p.body.decoded, body_charset)
457 # convert html parts to text
457 # convert html parts to text
458 p.mime_type == 'text/html' ? self.class.html_body_to_text(body) : self.class.plain_text_body_to_text(body)
458 p.mime_type == 'text/html' ? self.class.html_body_to_text(body) : self.class.plain_text_body_to_text(body)
459 end.join("\r\n")
459 end.join("\r\n")
460
460
461 @plain_text_body
461 @plain_text_body
462 end
462 end
463
463
464 def cleaned_up_text_body
464 def cleaned_up_text_body
465 @cleaned_up_text_body ||= cleanup_body(plain_text_body)
465 @cleaned_up_text_body ||= cleanup_body(plain_text_body)
466 end
466 end
467
467
468 def cleaned_up_subject
468 def cleaned_up_subject
469 subject = email.subject.to_s
469 subject = email.subject.to_s
470 subject.strip[0,255]
470 subject.strip[0,255]
471 end
471 end
472
472
473 # Converts a HTML email body to text
473 # Converts a HTML email body to text
474 def self.html_body_to_text(html)
474 def self.html_body_to_text(html)
475 Redmine::WikiFormatting.html_parser.to_text(html)
475 Redmine::WikiFormatting.html_parser.to_text(html)
476 end
476 end
477
477
478 # Converts a plain/text email body to text
478 # Converts a plain/text email body to text
479 def self.plain_text_body_to_text(text)
479 def self.plain_text_body_to_text(text)
480 # Removes leading spaces that would cause the line to be rendered as
480 # Removes leading spaces that would cause the line to be rendered as
481 # preformatted text with textile
481 # preformatted text with textile
482 text.gsub(/^ +(?![*#])/, '')
482 text.gsub(/^ +(?![*#])/, '')
483 end
483 end
484
484
485 def self.assign_string_attribute_with_limit(object, attribute, value, limit=nil)
485 def self.assign_string_attribute_with_limit(object, attribute, value, limit=nil)
486 limit ||= object.class.columns_hash[attribute.to_s].limit || 255
486 limit ||= object.class.columns_hash[attribute.to_s].limit || 255
487 value = value.to_s.slice(0, limit)
487 value = value.to_s.slice(0, limit)
488 object.send("#{attribute}=", value)
488 object.send("#{attribute}=", value)
489 end
489 end
490
490
491 # Returns a User from an email address and a full name
491 # Returns a User from an email address and a full name
492 def self.new_user_from_attributes(email_address, fullname=nil)
492 def self.new_user_from_attributes(email_address, fullname=nil)
493 user = User.new
493 user = User.new
494
494
495 # Truncating the email address would result in an invalid format
495 # Truncating the email address would result in an invalid format
496 user.mail = email_address
496 user.mail = email_address
497 assign_string_attribute_with_limit(user, 'login', email_address, User::LOGIN_LENGTH_LIMIT)
497 assign_string_attribute_with_limit(user, 'login', email_address, User::LOGIN_LENGTH_LIMIT)
498
498
499 names = fullname.blank? ? email_address.gsub(/@.*$/, '').split('.') : fullname.split
499 names = fullname.blank? ? email_address.gsub(/@.*$/, '').split('.') : fullname.split
500 assign_string_attribute_with_limit(user, 'firstname', names.shift, 30)
500 assign_string_attribute_with_limit(user, 'firstname', names.shift, 30)
501 assign_string_attribute_with_limit(user, 'lastname', names.join(' '), 30)
501 assign_string_attribute_with_limit(user, 'lastname', names.join(' '), 30)
502 user.lastname = '-' if user.lastname.blank?
502 user.lastname = '-' if user.lastname.blank?
503 user.language = Setting.default_language
503 user.language = Setting.default_language
504 user.generate_password = true
504 user.generate_password = true
505 user.mail_notification = 'only_my_events'
505 user.mail_notification = 'only_my_events'
506
506
507 unless user.valid?
507 unless user.valid?
508 user.login = "user#{Redmine::Utils.random_hex(6)}" unless user.errors[:login].blank?
508 user.login = "user#{Redmine::Utils.random_hex(6)}" unless user.errors[:login].blank?
509 user.firstname = "-" unless user.errors[:firstname].blank?
509 user.firstname = "-" unless user.errors[:firstname].blank?
510 (puts user.errors[:lastname];user.lastname = "-") unless user.errors[:lastname].blank?
510 (puts user.errors[:lastname];user.lastname = "-") unless user.errors[:lastname].blank?
511 end
511 end
512
512
513 user
513 user
514 end
514 end
515
515
516 # Creates a User for the +email+ sender
516 # Creates a User for the +email+ sender
517 # Returns the user or nil if it could not be created
517 # Returns the user or nil if it could not be created
518 def create_user_from_email
518 def create_user_from_email
519 from = email.header['from'].to_s
519 from = email.header['from'].to_s
520 addr, name = from, nil
520 addr, name = from, nil
521 if m = from.match(/^"?(.+?)"?\s+<(.+@.+)>$/)
521 if m = from.match(/^"?(.+?)"?\s+<(.+@.+)>$/)
522 addr, name = m[2], m[1]
522 addr, name = m[2], m[1]
523 end
523 end
524 if addr.present?
524 if addr.present?
525 user = self.class.new_user_from_attributes(addr, name)
525 user = self.class.new_user_from_attributes(addr, name)
526 if handler_options[:no_notification]
526 if handler_options[:no_notification]
527 user.mail_notification = 'none'
527 user.mail_notification = 'none'
528 end
528 end
529 if user.save
529 if user.save
530 user
530 user
531 else
531 else
532 logger.error "MailHandler: failed to create User: #{user.errors.full_messages}" if logger
532 logger.error "MailHandler: failed to create User: #{user.errors.full_messages}" if logger
533 nil
533 nil
534 end
534 end
535 else
535 else
536 logger.error "MailHandler: failed to create User: no FROM address found" if logger
536 logger.error "MailHandler: failed to create User: no FROM address found" if logger
537 nil
537 nil
538 end
538 end
539 end
539 end
540
540
541 # Adds the newly created user to default group
541 # Adds the newly created user to default group
542 def add_user_to_group(default_group)
542 def add_user_to_group(default_group)
543 if default_group.present?
543 if default_group.present?
544 default_group.split(',').each do |group_name|
544 default_group.split(',').each do |group_name|
545 if group = Group.named(group_name).first
545 if group = Group.named(group_name).first
546 group.users << @user
546 group.users << @user
547 elsif logger
547 elsif logger
548 logger.warn "MailHandler: could not add user to [#{group_name}], group not found"
548 logger.warn "MailHandler: could not add user to [#{group_name}], group not found"
549 end
549 end
550 end
550 end
551 end
551 end
552 end
552 end
553
553
554 # Removes the email body of text after the truncation configurations.
554 # Removes the email body of text after the truncation configurations.
555 def cleanup_body(body)
555 def cleanup_body(body)
556 delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
556 delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
557 unless delimiters.empty?
557 unless delimiters.empty?
558 regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
558 regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
559 body = body.gsub(regex, '')
559 body = body.gsub(regex, '')
560 end
560 end
561 body.strip
561 body.strip
562 end
562 end
563
563
564 def find_assignee_from_keyword(keyword, issue)
564 def find_assignee_from_keyword(keyword, issue)
565 Principal.detect_by_keyword(issue.assignable_users, keyword)
565 Principal.detect_by_keyword(issue.assignable_users, keyword)
566 end
566 end
567 end
567 end
@@ -1,211 +1,212
1 #!/usr/bin/env ruby
1 #!/usr/bin/env ruby
2 # Redmine - project management software
2 # Redmine - project management software
3 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 #
4 #
5 # This program is free software; you can redistribute it and/or
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; either version 2
7 # as published by the Free Software Foundation; either version 2
8 # of the License, or (at your option) any later version.
8 # of the License, or (at your option) any later version.
9 #
9 #
10 # This program is distributed in the hope that it will be useful,
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
13 # GNU General Public License for more details.
14 #
14 #
15 # You should have received a copy of the GNU General Public License
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
18
19 require 'net/http'
19 require 'net/http'
20 require 'net/https'
20 require 'net/https'
21 require 'uri'
21 require 'uri'
22 require 'optparse'
22 require 'optparse'
23
23
24 module Net
24 module Net
25 class HTTPS < HTTP
25 class HTTPS < HTTP
26 def self.post_form(url, params, headers, options={})
26 def self.post_form(url, params, headers, options={})
27 request = Post.new(url.path)
27 request = Post.new(url.path)
28 request.form_data = params
28 request.form_data = params
29 request.initialize_http_header(headers)
29 request.initialize_http_header(headers)
30 request.basic_auth url.user, url.password if url.user
30 request.basic_auth url.user, url.password if url.user
31 http = new(url.host, url.port)
31 http = new(url.host, url.port)
32 http.use_ssl = (url.scheme == 'https')
32 http.use_ssl = (url.scheme == 'https')
33 if options[:certificate_bundle]
33 if options[:certificate_bundle]
34 http.ca_file = options[:certificate_bundle]
34 http.ca_file = options[:certificate_bundle]
35 end
35 end
36 if options[:no_check_certificate]
36 if options[:no_check_certificate]
37 http.verify_mode = OpenSSL::SSL::VERIFY_NONE
37 http.verify_mode = OpenSSL::SSL::VERIFY_NONE
38 end
38 end
39 http.start {|h| h.request(request) }
39 http.start {|h| h.request(request) }
40 end
40 end
41 end
41 end
42 end
42 end
43
43
44 class RedmineMailHandler
44 class RedmineMailHandler
45 VERSION = '0.2.3'
45 VERSION = '0.2.3'
46
46
47 attr_accessor :verbose, :issue_attributes, :allow_override, :unknown_user, :default_group, :no_permission_check,
47 attr_accessor :verbose, :issue_attributes, :allow_override, :unknown_user, :default_group, :no_permission_check,
48 :url, :key, :no_check_certificate, :certificate_bundle, :no_account_notice, :no_notification, :project_from_subaddress
48 :url, :key, :no_check_certificate, :certificate_bundle, :no_account_notice, :no_notification, :project_from_subaddress
49
49
50 def initialize
50 def initialize
51 self.issue_attributes = {}
51 self.issue_attributes = {}
52
52
53 optparse = OptionParser.new do |opts|
53 optparse = OptionParser.new do |opts|
54 opts.banner = "Usage: rdm-mailhandler.rb [options] --url=<Redmine URL> --key=<API key>"
54 opts.banner = "Usage: rdm-mailhandler.rb [options] --url=<Redmine URL> --key=<API key>"
55 opts.separator("")
55 opts.separator("")
56 opts.separator("Reads an email from standard input and forwards it to a Redmine server through a HTTP request.")
56 opts.separator("Reads an email from standard input and forwards it to a Redmine server through a HTTP request.")
57 opts.separator("")
57 opts.separator("")
58 opts.separator("Required arguments:")
58 opts.separator("Required arguments:")
59 opts.on("-u", "--url URL", "URL of the Redmine server") {|v| self.url = v}
59 opts.on("-u", "--url URL", "URL of the Redmine server") {|v| self.url = v}
60 opts.on("-k", "--key KEY", "Redmine API key") {|v| self.key = v}
60 opts.on("-k", "--key KEY", "Redmine API key") {|v| self.key = v}
61 opts.separator("")
61 opts.separator("")
62 opts.separator("General options:")
62 opts.separator("General options:")
63 opts.on("--key-file FILE", "full path to a file that contains your Redmine",
63 opts.on("--key-file FILE", "full path to a file that contains your Redmine",
64 "API key (use this option instead of --key if",
64 "API key (use this option instead of --key if",
65 "you don't want the key to appear in the command",
65 "you don't want the key to appear in the command",
66 "line)") {|v| read_key_from_file(v)}
66 "line)") {|v| read_key_from_file(v)}
67 opts.on("--no-check-certificate", "do not check server certificate") {self.no_check_certificate = true}
67 opts.on("--no-check-certificate", "do not check server certificate") {self.no_check_certificate = true}
68 opts.on("--certificate-bundle FILE", "certificate bundle to use") {|v| self.certificate_bundle = v}
68 opts.on("--certificate-bundle FILE", "certificate bundle to use") {|v| self.certificate_bundle = v}
69 opts.on("-h", "--help", "show this help") {puts opts; exit 1}
69 opts.on("-h", "--help", "show this help") {puts opts; exit 1}
70 opts.on("-v", "--verbose", "show extra information") {self.verbose = true}
70 opts.on("-v", "--verbose", "show extra information") {self.verbose = true}
71 opts.on("-V", "--version", "show version information and exit") {puts VERSION; exit}
71 opts.on("-V", "--version", "show version information and exit") {puts VERSION; exit}
72 opts.separator("")
72 opts.separator("")
73 opts.separator("User and permissions options:")
73 opts.separator("User and permissions options:")
74 opts.on("--unknown-user ACTION", "how to handle emails from an unknown user",
74 opts.on("--unknown-user ACTION", "how to handle emails from an unknown user",
75 "ACTION can be one of the following values:",
75 "ACTION can be one of the following values:",
76 "* ignore: email is ignored (default)",
76 "* ignore: email is ignored (default)",
77 "* accept: accept as anonymous user",
77 "* accept: accept as anonymous user",
78 "* create: create a user account") {|v| self.unknown_user = v}
78 "* create: create a user account") {|v| self.unknown_user = v}
79 opts.on("--no-permission-check", "disable permission checking when receiving",
79 opts.on("--no-permission-check", "disable permission checking when receiving",
80 "the email") {self.no_permission_check = '1'}
80 "the email") {self.no_permission_check = '1'}
81 opts.on("--default-group GROUP", "add created user to GROUP (none by default)",
81 opts.on("--default-group GROUP", "add created user to GROUP (none by default)",
82 "GROUP can be a comma separated list of groups") { |v| self.default_group = v}
82 "GROUP can be a comma separated list of groups") { |v| self.default_group = v}
83 opts.on("--no-account-notice", "don't send account information to the newly",
83 opts.on("--no-account-notice", "don't send account information to the newly",
84 "created user") { |v| self.no_account_notice = '1'}
84 "created user") { |v| self.no_account_notice = '1'}
85 opts.on("--no-notification", "disable email notifications for the created",
85 opts.on("--no-notification", "disable email notifications for the created",
86 "user") { |v| self.no_notification = '1'}
86 "user") { |v| self.no_notification = '1'}
87 opts.separator("")
87 opts.separator("")
88 opts.separator("Issue attributes control options:")
88 opts.separator("Issue attributes control options:")
89 opts.on( "--project-from-subaddress ADDR", "select project from subadress of ADDR found",
89 opts.on( "--project-from-subaddress ADDR", "select project from subadress of ADDR found",
90 "in To, Cc, Bcc headers") {|v| self.project_from_subaddress['project'] = v}
90 "in To, Cc, Bcc headers") {|v| self.project_from_subaddress['project'] = v}
91 opts.on("-p", "--project PROJECT", "identifier of the target project") {|v| self.issue_attributes['project'] = v}
91 opts.on("-p", "--project PROJECT", "identifier of the target project") {|v| self.issue_attributes['project'] = v}
92 opts.on("-s", "--status STATUS", "name of the target status") {|v| self.issue_attributes['status'] = v}
92 opts.on("-s", "--status STATUS", "name of the target status") {|v| self.issue_attributes['status'] = v}
93 opts.on("-t", "--tracker TRACKER", "name of the target tracker") {|v| self.issue_attributes['tracker'] = v}
93 opts.on("-t", "--tracker TRACKER", "name of the target tracker") {|v| self.issue_attributes['tracker'] = v}
94 opts.on( "--category CATEGORY", "name of the target category") {|v| self.issue_attributes['category'] = v}
94 opts.on( "--category CATEGORY", "name of the target category") {|v| self.issue_attributes['category'] = v}
95 opts.on( "--priority PRIORITY", "name of the target priority") {|v| self.issue_attributes['priority'] = v}
95 opts.on( "--priority PRIORITY", "name of the target priority") {|v| self.issue_attributes['priority'] = v}
96 opts.on( "--fixed-version VERSION","name of the target version") {|v| self.issue_attributes['fixed_version'] = v}
96 opts.on( "--private", "create new issues as private") {|v| self.issue_attributes['is_private'] = '1'}
97 opts.on( "--private", "create new issues as private") {|v| self.issue_attributes['is_private'] = '1'}
97 opts.on("-o", "--allow-override ATTRS", "allow email content to set attributes values",
98 opts.on("-o", "--allow-override ATTRS", "allow email content to set attributes values",
98 "ATTRS is a comma separated list of attributes",
99 "ATTRS is a comma separated list of attributes",
99 "or 'all' to allow all attributes to be",
100 "or 'all' to allow all attributes to be",
100 "overridable (see below for details)") {|v| self.allow_override = v}
101 "overridable (see below for details)") {|v| self.allow_override = v}
101
102
102 opts.separator <<-END_DESC
103 opts.separator <<-END_DESC
103
104
104 Overrides:
105 Overrides:
105 ATTRS is a comma separated list of attributes among:
106 ATTRS is a comma separated list of attributes among:
106 * project, tracker, status, priority, category, assigned_to, fixed_version,
107 * project, tracker, status, priority, category, assigned_to, fixed_version,
107 start_date, due_date, estimated_hours, done_ratio
108 start_date, due_date, estimated_hours, done_ratio
108 * custom fields names with underscores instead of spaces (case insensitive)
109 * custom fields names with underscores instead of spaces (case insensitive)
109 Example: --allow_override=project,priority,my_custom_field
110 Example: --allow_override=project,priority,my_custom_field
110
111
111 If the --project option is not set, project is overridable by default for
112 If the --project option is not set, project is overridable by default for
112 emails that create new issues.
113 emails that create new issues.
113
114
114 You can use --allow_override=all to allow all attributes to be overridable.
115 You can use --allow_override=all to allow all attributes to be overridable.
115
116
116 Examples:
117 Examples:
117 No project specified, emails MUST contain the 'Project' keyword, otherwise
118 No project specified, emails MUST contain the 'Project' keyword, otherwise
118 they will be dropped (not recommanded):
119 they will be dropped (not recommanded):
119
120
120 rdm-mailhandler.rb --url http://redmine.domain.foo --key secret
121 rdm-mailhandler.rb --url http://redmine.domain.foo --key secret
121
122
122 Fixed project and default tracker specified, but emails can override
123 Fixed project and default tracker specified, but emails can override
123 both tracker and priority attributes using keywords:
124 both tracker and priority attributes using keywords:
124
125
125 rdm-mailhandler.rb --url https://domain.foo/redmine --key secret \\
126 rdm-mailhandler.rb --url https://domain.foo/redmine --key secret \\
126 --project myproject \\
127 --project myproject \\
127 --tracker bug \\
128 --tracker bug \\
128 --allow-override tracker,priority
129 --allow-override tracker,priority
129
130
130 Project selected by subaddress of redmine@example.net. Sending the email
131 Project selected by subaddress of redmine@example.net. Sending the email
131 to redmine+myproject@example.net will add the issue to myproject:
132 to redmine+myproject@example.net will add the issue to myproject:
132
133
133 rdm-mailhandler.rb --url http://redmine.domain.foo --key secret \\
134 rdm-mailhandler.rb --url http://redmine.domain.foo --key secret \\
134 --project-from-subaddress redmine@example.net
135 --project-from-subaddress redmine@example.net
135 END_DESC
136 END_DESC
136
137
137 opts.summary_width = 27
138 opts.summary_width = 27
138 end
139 end
139 optparse.parse!
140 optparse.parse!
140
141
141 unless url && key
142 unless url && key
142 puts "Some arguments are missing. Use `rdm-mailhandler.rb --help` for getting help."
143 puts "Some arguments are missing. Use `rdm-mailhandler.rb --help` for getting help."
143 exit 1
144 exit 1
144 end
145 end
145 end
146 end
146
147
147 def submit(email)
148 def submit(email)
148 uri = url.gsub(%r{/*$}, '') + '/mail_handler'
149 uri = url.gsub(%r{/*$}, '') + '/mail_handler'
149
150
150 headers = { 'User-Agent' => "Redmine mail handler/#{VERSION}" }
151 headers = { 'User-Agent' => "Redmine mail handler/#{VERSION}" }
151
152
152 data = { 'key' => key, 'email' => email,
153 data = { 'key' => key, 'email' => email,
153 'allow_override' => allow_override,
154 'allow_override' => allow_override,
154 'unknown_user' => unknown_user,
155 'unknown_user' => unknown_user,
155 'default_group' => default_group,
156 'default_group' => default_group,
156 'no_account_notice' => no_account_notice,
157 'no_account_notice' => no_account_notice,
157 'no_notification' => no_notification,
158 'no_notification' => no_notification,
158 'no_permission_check' => no_permission_check,
159 'no_permission_check' => no_permission_check,
159 'project_from_subaddress' => project_from_subaddress}
160 'project_from_subaddress' => project_from_subaddress}
160 issue_attributes.each { |attr, value| data["issue[#{attr}]"] = value }
161 issue_attributes.each { |attr, value| data["issue[#{attr}]"] = value }
161
162
162 debug "Posting to #{uri}..."
163 debug "Posting to #{uri}..."
163 begin
164 begin
164 response = Net::HTTPS.post_form(URI.parse(uri), data, headers, :no_check_certificate => no_check_certificate, :certificate_bundle => certificate_bundle)
165 response = Net::HTTPS.post_form(URI.parse(uri), data, headers, :no_check_certificate => no_check_certificate, :certificate_bundle => certificate_bundle)
165 rescue SystemCallError, IOError => e # connection refused, etc.
166 rescue SystemCallError, IOError => e # connection refused, etc.
166 warn "An error occured while contacting your Redmine server: #{e.message}"
167 warn "An error occured while contacting your Redmine server: #{e.message}"
167 return 75 # temporary failure
168 return 75 # temporary failure
168 end
169 end
169 debug "Response received: #{response.code}"
170 debug "Response received: #{response.code}"
170
171
171 case response.code.to_i
172 case response.code.to_i
172 when 403
173 when 403
173 warn "Request was denied by your Redmine server. " +
174 warn "Request was denied by your Redmine server. " +
174 "Make sure that 'WS for incoming emails' is enabled in application settings and that you provided the correct API key."
175 "Make sure that 'WS for incoming emails' is enabled in application settings and that you provided the correct API key."
175 return 77
176 return 77
176 when 422
177 when 422
177 warn "Request was denied by your Redmine server. " +
178 warn "Request was denied by your Redmine server. " +
178 "Possible reasons: email is sent from an invalid email address or is missing some information."
179 "Possible reasons: email is sent from an invalid email address or is missing some information."
179 return 77
180 return 77
180 when 400..499
181 when 400..499
181 warn "Request was denied by your Redmine server (#{response.code})."
182 warn "Request was denied by your Redmine server (#{response.code})."
182 return 77
183 return 77
183 when 500..599
184 when 500..599
184 warn "Failed to contact your Redmine server (#{response.code})."
185 warn "Failed to contact your Redmine server (#{response.code})."
185 return 75
186 return 75
186 when 201
187 when 201
187 debug "Proccessed successfully"
188 debug "Proccessed successfully"
188 return 0
189 return 0
189 else
190 else
190 return 1
191 return 1
191 end
192 end
192 end
193 end
193
194
194 private
195 private
195
196
196 def debug(msg)
197 def debug(msg)
197 puts msg if verbose
198 puts msg if verbose
198 end
199 end
199
200
200 def read_key_from_file(filename)
201 def read_key_from_file(filename)
201 begin
202 begin
202 self.key = File.read(filename).strip
203 self.key = File.read(filename).strip
203 rescue Exception => e
204 rescue Exception => e
204 $stderr.puts "Unable to read the key from #{filename}:\n#{e.message}"
205 $stderr.puts "Unable to read the key from #{filename}:\n#{e.message}"
205 exit 1
206 exit 1
206 end
207 end
207 end
208 end
208 end
209 end
209
210
210 handler = RedmineMailHandler.new
211 handler = RedmineMailHandler.new
211 exit(handler.submit(STDIN.read))
212 exit(handler.submit(STDIN.read))
@@ -1,169 +1,170
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 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 namespace :redmine do
18 namespace :redmine do
19 namespace :email do
19 namespace :email do
20
20
21 desc <<-END_DESC
21 desc <<-END_DESC
22 Read an email from standard input.
22 Read an email from standard input.
23
23
24 See redmine:email:receive_imap for more options and examples.
24 See redmine:email:receive_imap for more options and examples.
25 END_DESC
25 END_DESC
26
26
27 task :read => :environment do
27 task :read => :environment do
28 Mailer.with_synched_deliveries do
28 Mailer.with_synched_deliveries do
29 MailHandler.receive(STDIN.read, MailHandler.extract_options_from_env(ENV))
29 MailHandler.receive(STDIN.read, MailHandler.extract_options_from_env(ENV))
30 end
30 end
31 end
31 end
32
32
33 desc <<-END_DESC
33 desc <<-END_DESC
34 Read emails from an IMAP server.
34 Read emails from an IMAP server.
35
35
36 Available IMAP options:
36 Available IMAP options:
37 host=HOST IMAP server host (default: 127.0.0.1)
37 host=HOST IMAP server host (default: 127.0.0.1)
38 port=PORT IMAP server port (default: 143)
38 port=PORT IMAP server port (default: 143)
39 ssl=SSL Use SSL/TLS? (default: false)
39 ssl=SSL Use SSL/TLS? (default: false)
40 starttls=STARTTLS Use STARTTLS? (default: false)
40 starttls=STARTTLS Use STARTTLS? (default: false)
41 username=USERNAME IMAP account
41 username=USERNAME IMAP account
42 password=PASSWORD IMAP password
42 password=PASSWORD IMAP password
43 folder=FOLDER IMAP folder to read (default: INBOX)
43 folder=FOLDER IMAP folder to read (default: INBOX)
44
44
45 Processed emails control options:
45 Processed emails control options:
46 move_on_success=MAILBOX move emails that were successfully received
46 move_on_success=MAILBOX move emails that were successfully received
47 to MAILBOX instead of deleting them
47 to MAILBOX instead of deleting them
48 move_on_failure=MAILBOX move emails that were ignored to MAILBOX
48 move_on_failure=MAILBOX move emails that were ignored to MAILBOX
49
49
50 User and permissions options:
50 User and permissions options:
51 unknown_user=ACTION how to handle emails from an unknown user
51 unknown_user=ACTION how to handle emails from an unknown user
52 ACTION can be one of the following values:
52 ACTION can be one of the following values:
53 ignore: email is ignored (default)
53 ignore: email is ignored (default)
54 accept: accept as anonymous user
54 accept: accept as anonymous user
55 create: create a user account
55 create: create a user account
56 no_permission_check=1 disable permission checking when receiving
56 no_permission_check=1 disable permission checking when receiving
57 the email
57 the email
58 no_account_notice=1 disable new user account notification
58 no_account_notice=1 disable new user account notification
59 default_group=foo,bar adds created user to foo and bar groups
59 default_group=foo,bar adds created user to foo and bar groups
60
60
61 Issue attributes control options:
61 Issue attributes control options:
62 project=PROJECT identifier of the target project
62 project=PROJECT identifier of the target project
63 status=STATUS name of the target status
63 status=STATUS name of the target status
64 tracker=TRACKER name of the target tracker
64 tracker=TRACKER name of the target tracker
65 category=CATEGORY name of the target category
65 category=CATEGORY name of the target category
66 priority=PRIORITY name of the target priority
66 priority=PRIORITY name of the target priority
67 fixed_version=VERSION name of the target version
67 private create new issues as private
68 private create new issues as private
68 allow_override=ATTRS allow email content to set attributes values
69 allow_override=ATTRS allow email content to set attributes values
69 ATTRS is a comma separated list of attributes
70 ATTRS is a comma separated list of attributes
70 or 'all' to allow all attributes to be overridable
71 or 'all' to allow all attributes to be overridable
71 (see below for details)
72 (see below for details)
72
73
73 Overrides:
74 Overrides:
74 ATTRS is a comma separated list of attributes among:
75 ATTRS is a comma separated list of attributes among:
75 * project, tracker, status, priority, category, assigned_to, fixed_version,
76 * project, tracker, status, priority, category, assigned_to, fixed_version,
76 start_date, due_date, estimated_hours, done_ratio
77 start_date, due_date, estimated_hours, done_ratio
77 * custom fields names with underscores instead of spaces (case insensitive)
78 * custom fields names with underscores instead of spaces (case insensitive)
78
79
79 Example: allow_override=project,priority,my_custom_field
80 Example: allow_override=project,priority,my_custom_field
80
81
81 If the project option is not set, project is overridable by default for
82 If the project option is not set, project is overridable by default for
82 emails that create new issues.
83 emails that create new issues.
83
84
84 You can use allow_override=all to allow all attributes to be overridable.
85 You can use allow_override=all to allow all attributes to be overridable.
85
86
86 Examples:
87 Examples:
87 # No project specified. Emails MUST contain the 'Project' keyword:
88 # No project specified. Emails MUST contain the 'Project' keyword:
88
89
89 rake redmine:email:receive_imap RAILS_ENV="production" \\
90 rake redmine:email:receive_imap RAILS_ENV="production" \\
90 host=imap.foo.bar username=redmine@example.net password=xxx
91 host=imap.foo.bar username=redmine@example.net password=xxx
91
92
92
93
93 # Fixed project and default tracker specified, but emails can override
94 # Fixed project and default tracker specified, but emails can override
94 # both tracker and priority attributes:
95 # both tracker and priority attributes:
95
96
96 rake redmine:email:receive_imap RAILS_ENV="production" \\
97 rake redmine:email:receive_imap RAILS_ENV="production" \\
97 host=imap.foo.bar username=redmine@example.net password=xxx ssl=1 \\
98 host=imap.foo.bar username=redmine@example.net password=xxx ssl=1 \\
98 project=foo \\
99 project=foo \\
99 tracker=bug \\
100 tracker=bug \\
100 allow_override=tracker,priority
101 allow_override=tracker,priority
101 END_DESC
102 END_DESC
102
103
103 task :receive_imap => :environment do
104 task :receive_imap => :environment do
104 imap_options = {:host => ENV['host'],
105 imap_options = {:host => ENV['host'],
105 :port => ENV['port'],
106 :port => ENV['port'],
106 :ssl => ENV['ssl'],
107 :ssl => ENV['ssl'],
107 :starttls => ENV['starttls'],
108 :starttls => ENV['starttls'],
108 :username => ENV['username'],
109 :username => ENV['username'],
109 :password => ENV['password'],
110 :password => ENV['password'],
110 :folder => ENV['folder'],
111 :folder => ENV['folder'],
111 :move_on_success => ENV['move_on_success'],
112 :move_on_success => ENV['move_on_success'],
112 :move_on_failure => ENV['move_on_failure']}
113 :move_on_failure => ENV['move_on_failure']}
113
114
114 Mailer.with_synched_deliveries do
115 Mailer.with_synched_deliveries do
115 Redmine::IMAP.check(imap_options, MailHandler.extract_options_from_env(ENV))
116 Redmine::IMAP.check(imap_options, MailHandler.extract_options_from_env(ENV))
116 end
117 end
117 end
118 end
118
119
119 desc <<-END_DESC
120 desc <<-END_DESC
120 Read emails from an POP3 server.
121 Read emails from an POP3 server.
121
122
122 Available POP3 options:
123 Available POP3 options:
123 host=HOST POP3 server host (default: 127.0.0.1)
124 host=HOST POP3 server host (default: 127.0.0.1)
124 port=PORT POP3 server port (default: 110)
125 port=PORT POP3 server port (default: 110)
125 username=USERNAME POP3 account
126 username=USERNAME POP3 account
126 password=PASSWORD POP3 password
127 password=PASSWORD POP3 password
127 apop=1 use APOP authentication (default: false)
128 apop=1 use APOP authentication (default: false)
128 ssl=SSL Use SSL? (default: false)
129 ssl=SSL Use SSL? (default: false)
129 delete_unprocessed=1 delete messages that could not be processed
130 delete_unprocessed=1 delete messages that could not be processed
130 successfully from the server (default
131 successfully from the server (default
131 behaviour is to leave them on the server)
132 behaviour is to leave them on the server)
132
133
133 See redmine:email:receive_imap for more options and examples.
134 See redmine:email:receive_imap for more options and examples.
134 END_DESC
135 END_DESC
135
136
136 task :receive_pop3 => :environment do
137 task :receive_pop3 => :environment do
137 pop_options = {:host => ENV['host'],
138 pop_options = {:host => ENV['host'],
138 :port => ENV['port'],
139 :port => ENV['port'],
139 :apop => ENV['apop'],
140 :apop => ENV['apop'],
140 :ssl => ENV['ssl'],
141 :ssl => ENV['ssl'],
141 :username => ENV['username'],
142 :username => ENV['username'],
142 :password => ENV['password'],
143 :password => ENV['password'],
143 :delete_unprocessed => ENV['delete_unprocessed']}
144 :delete_unprocessed => ENV['delete_unprocessed']}
144
145
145 Mailer.with_synched_deliveries do
146 Mailer.with_synched_deliveries do
146 Redmine::POP3.check(pop_options, MailHandler.extract_options_from_env(ENV))
147 Redmine::POP3.check(pop_options, MailHandler.extract_options_from_env(ENV))
147 end
148 end
148 end
149 end
149
150
150 desc "Send a test email to the user with the provided login name"
151 desc "Send a test email to the user with the provided login name"
151 task :test, [:login] => :environment do |task, args|
152 task :test, [:login] => :environment do |task, args|
152 include Redmine::I18n
153 include Redmine::I18n
153 abort l(:notice_email_error, "Please include the user login to test with. Example: rake redmine:email:test[login]") if args[:login].blank?
154 abort l(:notice_email_error, "Please include the user login to test with. Example: rake redmine:email:test[login]") if args[:login].blank?
154
155
155 user = User.find_by_login(args[:login])
156 user = User.find_by_login(args[:login])
156 abort l(:notice_email_error, "User #{args[:login]} not found") unless user && user.logged?
157 abort l(:notice_email_error, "User #{args[:login]} not found") unless user && user.logged?
157
158
158 ActionMailer::Base.raise_delivery_errors = true
159 ActionMailer::Base.raise_delivery_errors = true
159 begin
160 begin
160 Mailer.with_synched_deliveries do
161 Mailer.with_synched_deliveries do
161 Mailer.test_email(user).deliver
162 Mailer.test_email(user).deliver
162 end
163 end
163 puts l(:notice_email_sent, user.mail)
164 puts l(:notice_email_sent, user.mail)
164 rescue Exception => e
165 rescue Exception => e
165 abort l(:notice_email_error, e.message)
166 abort l(:notice_email_error, e.message)
166 end
167 end
167 end
168 end
168 end
169 end
169 end
170 end
@@ -1,1035 +1,1046
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2015 Jean-Philippe Lang
4 # Copyright (C) 2006-2015 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, :enabled_modules, :roles,
23 fixtures :users, :projects, :enabled_modules, :roles,
24 :members, :member_roles, :users,
24 :members, :member_roles, :users,
25 :email_addresses,
25 :email_addresses,
26 :issues, :issue_statuses,
26 :issues, :issue_statuses,
27 :workflows, :trackers, :projects_trackers,
27 :workflows, :trackers, :projects_trackers,
28 :versions, :enumerations, :issue_categories,
28 :versions, :enumerations, :issue_categories,
29 :custom_fields, :custom_fields_trackers, :custom_fields_projects,
29 :custom_fields, :custom_fields_trackers, :custom_fields_projects,
30 :boards, :messages
30 :boards, :messages
31
31
32 FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler'
32 FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler'
33
33
34 def setup
34 def setup
35 ActionMailer::Base.deliveries.clear
35 ActionMailer::Base.deliveries.clear
36 Setting.notified_events = Redmine::Notifiable.all.collect(&:name)
36 Setting.notified_events = Redmine::Notifiable.all.collect(&:name)
37 end
37 end
38
38
39 def teardown
39 def teardown
40 Setting.clear_cache
40 Setting.clear_cache
41 end
41 end
42
42
43 def test_add_issue_with_specific_overrides
43 def test_add_issue_with_specific_overrides
44 issue = submit_email('ticket_on_given_project.eml',
44 issue = submit_email('ticket_on_given_project.eml',
45 :allow_override => ['status', 'start_date', 'due_date', 'assigned_to', 'fixed_version', 'estimated_hours', 'done_ratio']
45 :allow_override => ['status', 'start_date', 'due_date', 'assigned_to', 'fixed_version', 'estimated_hours', 'done_ratio']
46 )
46 )
47 assert issue.is_a?(Issue)
47 assert issue.is_a?(Issue)
48 assert !issue.new_record?
48 assert !issue.new_record?
49 issue.reload
49 issue.reload
50 assert_equal Project.find(2), issue.project
50 assert_equal Project.find(2), issue.project
51 assert_equal issue.project.trackers.first, issue.tracker
51 assert_equal issue.project.trackers.first, issue.tracker
52 assert_equal 'New ticket on a given project', issue.subject
52 assert_equal 'New ticket on a given project', issue.subject
53 assert_equal User.find_by_login('jsmith'), issue.author
53 assert_equal User.find_by_login('jsmith'), issue.author
54 assert_equal IssueStatus.find_by_name('Resolved'), issue.status
54 assert_equal IssueStatus.find_by_name('Resolved'), issue.status
55 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
55 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
56 assert_equal '2010-01-01', issue.start_date.to_s
56 assert_equal '2010-01-01', issue.start_date.to_s
57 assert_equal '2010-12-31', issue.due_date.to_s
57 assert_equal '2010-12-31', issue.due_date.to_s
58 assert_equal User.find_by_login('jsmith'), issue.assigned_to
58 assert_equal User.find_by_login('jsmith'), issue.assigned_to
59 assert_equal Version.find_by_name('Alpha'), issue.fixed_version
59 assert_equal Version.find_by_name('Alpha'), issue.fixed_version
60 assert_equal 2.5, issue.estimated_hours
60 assert_equal 2.5, issue.estimated_hours
61 assert_equal 30, issue.done_ratio
61 assert_equal 30, issue.done_ratio
62 # keywords should be removed from the email body
62 # keywords should be removed from the email body
63 assert !issue.description.match(/^Project:/i)
63 assert !issue.description.match(/^Project:/i)
64 assert !issue.description.match(/^Status:/i)
64 assert !issue.description.match(/^Status:/i)
65 assert !issue.description.match(/^Start Date:/i)
65 assert !issue.description.match(/^Start Date:/i)
66 end
66 end
67
67
68 def test_add_issue_with_all_overrides
68 def test_add_issue_with_all_overrides
69 issue = submit_email('ticket_on_given_project.eml', :allow_override => 'all')
69 issue = submit_email('ticket_on_given_project.eml', :allow_override => 'all')
70 assert issue.is_a?(Issue)
70 assert issue.is_a?(Issue)
71 assert !issue.new_record?
71 assert !issue.new_record?
72 issue.reload
72 issue.reload
73 assert_equal Project.find(2), issue.project
73 assert_equal Project.find(2), issue.project
74 assert_equal issue.project.trackers.first, issue.tracker
74 assert_equal issue.project.trackers.first, issue.tracker
75 assert_equal IssueStatus.find_by_name('Resolved'), issue.status
75 assert_equal IssueStatus.find_by_name('Resolved'), issue.status
76 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
76 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
77 assert_equal '2010-01-01', issue.start_date.to_s
77 assert_equal '2010-01-01', issue.start_date.to_s
78 assert_equal '2010-12-31', issue.due_date.to_s
78 assert_equal '2010-12-31', issue.due_date.to_s
79 assert_equal User.find_by_login('jsmith'), issue.assigned_to
79 assert_equal User.find_by_login('jsmith'), issue.assigned_to
80 assert_equal Version.find_by_name('Alpha'), issue.fixed_version
80 assert_equal Version.find_by_name('Alpha'), issue.fixed_version
81 assert_equal 2.5, issue.estimated_hours
81 assert_equal 2.5, issue.estimated_hours
82 assert_equal 30, issue.done_ratio
82 assert_equal 30, issue.done_ratio
83 end
83 end
84
84
85 def test_add_issue_without_overrides_should_ignore_attributes
85 def test_add_issue_without_overrides_should_ignore_attributes
86 WorkflowRule.delete_all
86 WorkflowRule.delete_all
87 issue = submit_email('ticket_on_given_project.eml')
87 issue = submit_email('ticket_on_given_project.eml')
88 assert issue.is_a?(Issue)
88 assert issue.is_a?(Issue)
89 assert !issue.new_record?
89 assert !issue.new_record?
90 issue.reload
90 issue.reload
91 assert_equal Project.find(2), issue.project
91 assert_equal Project.find(2), issue.project
92 assert_equal 'New ticket on a given project', issue.subject
92 assert_equal 'New ticket on a given project', issue.subject
93 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
93 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
94 assert_equal User.find_by_login('jsmith'), issue.author
94 assert_equal User.find_by_login('jsmith'), issue.author
95
95
96 assert_equal issue.project.trackers.first, issue.tracker
96 assert_equal issue.project.trackers.first, issue.tracker
97 assert_equal 'New', issue.status.name
97 assert_equal 'New', issue.status.name
98 assert_not_equal '2010-01-01', issue.start_date.to_s
98 assert_not_equal '2010-01-01', issue.start_date.to_s
99 assert_nil issue.due_date
99 assert_nil issue.due_date
100 assert_nil issue.assigned_to
100 assert_nil issue.assigned_to
101 assert_nil issue.fixed_version
101 assert_nil issue.fixed_version
102 assert_nil issue.estimated_hours
102 assert_nil issue.estimated_hours
103 assert_equal 0, issue.done_ratio
103 assert_equal 0, issue.done_ratio
104 end
104 end
105
105
106 def test_add_issue_to_project_specified_by_subaddress
106 def test_add_issue_to_project_specified_by_subaddress
107 # This email has redmine+onlinestore@somenet.foo as 'To' header
107 # This email has redmine+onlinestore@somenet.foo as 'To' header
108 issue = submit_email(
108 issue = submit_email(
109 'ticket_on_project_given_by_to_header.eml',
109 'ticket_on_project_given_by_to_header.eml',
110 :issue => {:tracker => 'Support request'},
110 :issue => {:tracker => 'Support request'},
111 :project_from_subaddress => 'redmine@somenet.foo'
111 :project_from_subaddress => 'redmine@somenet.foo'
112 )
112 )
113 assert issue.is_a?(Issue)
113 assert issue.is_a?(Issue)
114 assert !issue.new_record?
114 assert !issue.new_record?
115 issue.reload
115 issue.reload
116 assert_equal 'onlinestore', issue.project.identifier
116 assert_equal 'onlinestore', issue.project.identifier
117 assert_equal 'Support request', issue.tracker.name
117 assert_equal 'Support request', issue.tracker.name
118 end
118 end
119
119
120 def test_add_issue_with_default_tracker
120 def test_add_issue_with_default_tracker
121 # This email contains: 'Project: onlinestore'
121 # This email contains: 'Project: onlinestore'
122 issue = submit_email(
122 issue = submit_email(
123 'ticket_on_given_project.eml',
123 'ticket_on_given_project.eml',
124 :issue => {:tracker => 'Support request'}
124 :issue => {:tracker => 'Support request'}
125 )
125 )
126 assert issue.is_a?(Issue)
126 assert issue.is_a?(Issue)
127 assert !issue.new_record?
127 assert !issue.new_record?
128 issue.reload
128 issue.reload
129 assert_equal 'Support request', issue.tracker.name
129 assert_equal 'Support request', issue.tracker.name
130 end
130 end
131
131
132 def test_add_issue_with_default_version
133 # This email contains: 'Project: onlinestore'
134 issue = submit_email(
135 'ticket_on_given_project.eml',
136 :issue => {:fixed_version => 'Alpha'}
137 )
138 assert issue.is_a?(Issue)
139 assert !issue.new_record?
140 assert_equal 'Alpha', issue.reload.fixed_version.name
141 end
142
132 def test_add_issue_with_status_override
143 def test_add_issue_with_status_override
133 # This email contains: 'Project: onlinestore' and 'Status: Resolved'
144 # This email contains: 'Project: onlinestore' and 'Status: Resolved'
134 issue = submit_email('ticket_on_given_project.eml', :allow_override => ['status'])
145 issue = submit_email('ticket_on_given_project.eml', :allow_override => ['status'])
135 assert issue.is_a?(Issue)
146 assert issue.is_a?(Issue)
136 assert !issue.new_record?
147 assert !issue.new_record?
137 issue.reload
148 issue.reload
138 assert_equal Project.find(2), issue.project
149 assert_equal Project.find(2), issue.project
139 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
150 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
140 end
151 end
141
152
142 def test_add_issue_should_accept_is_private_attribute
153 def test_add_issue_should_accept_is_private_attribute
143 issue = submit_email('ticket_on_given_project.eml', :issue => {:is_private => '1'})
154 issue = submit_email('ticket_on_given_project.eml', :issue => {:is_private => '1'})
144 assert issue.is_a?(Issue)
155 assert issue.is_a?(Issue)
145 assert !issue.new_record?
156 assert !issue.new_record?
146 assert_equal true, issue.reload.is_private
157 assert_equal true, issue.reload.is_private
147 end
158 end
148
159
149 def test_add_issue_with_group_assignment
160 def test_add_issue_with_group_assignment
150 with_settings :issue_group_assignment => '1' do
161 with_settings :issue_group_assignment => '1' do
151 issue = submit_email('ticket_on_given_project.eml', :allow_override => ['assigned_to']) do |email|
162 issue = submit_email('ticket_on_given_project.eml', :allow_override => ['assigned_to']) do |email|
152 email.gsub!('Assigned to: John Smith', 'Assigned to: B Team')
163 email.gsub!('Assigned to: John Smith', 'Assigned to: B Team')
153 end
164 end
154 assert issue.is_a?(Issue)
165 assert issue.is_a?(Issue)
155 assert !issue.new_record?
166 assert !issue.new_record?
156 issue.reload
167 issue.reload
157 assert_equal Group.find(11), issue.assigned_to
168 assert_equal Group.find(11), issue.assigned_to
158 end
169 end
159 end
170 end
160
171
161 def test_add_issue_with_partial_attributes_override
172 def test_add_issue_with_partial_attributes_override
162 issue = submit_email(
173 issue = submit_email(
163 'ticket_with_attributes.eml',
174 'ticket_with_attributes.eml',
164 :issue => {:priority => 'High'},
175 :issue => {:priority => 'High'},
165 :allow_override => ['tracker']
176 :allow_override => ['tracker']
166 )
177 )
167 assert issue.is_a?(Issue)
178 assert issue.is_a?(Issue)
168 assert !issue.new_record?
179 assert !issue.new_record?
169 issue.reload
180 issue.reload
170 assert_equal 'New ticket on a given project', issue.subject
181 assert_equal 'New ticket on a given project', issue.subject
171 assert_equal User.find_by_login('jsmith'), issue.author
182 assert_equal User.find_by_login('jsmith'), issue.author
172 assert_equal Project.find(2), issue.project
183 assert_equal Project.find(2), issue.project
173 assert_equal 'Feature request', issue.tracker.to_s
184 assert_equal 'Feature request', issue.tracker.to_s
174 assert_nil issue.category
185 assert_nil issue.category
175 assert_equal 'High', issue.priority.to_s
186 assert_equal 'High', issue.priority.to_s
176 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
187 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
177 end
188 end
178
189
179 def test_add_issue_with_spaces_between_attribute_and_separator
190 def test_add_issue_with_spaces_between_attribute_and_separator
180 issue = submit_email(
191 issue = submit_email(
181 'ticket_with_spaces_between_attribute_and_separator.eml',
192 'ticket_with_spaces_between_attribute_and_separator.eml',
182 :allow_override => 'tracker,category,priority'
193 :allow_override => 'tracker,category,priority'
183 )
194 )
184 assert issue.is_a?(Issue)
195 assert issue.is_a?(Issue)
185 assert !issue.new_record?
196 assert !issue.new_record?
186 issue.reload
197 issue.reload
187 assert_equal 'New ticket on a given project', issue.subject
198 assert_equal 'New ticket on a given project', issue.subject
188 assert_equal User.find_by_login('jsmith'), issue.author
199 assert_equal User.find_by_login('jsmith'), issue.author
189 assert_equal Project.find(2), issue.project
200 assert_equal Project.find(2), issue.project
190 assert_equal 'Feature request', issue.tracker.to_s
201 assert_equal 'Feature request', issue.tracker.to_s
191 assert_equal 'Stock management', issue.category.to_s
202 assert_equal 'Stock management', issue.category.to_s
192 assert_equal 'Urgent', issue.priority.to_s
203 assert_equal 'Urgent', issue.priority.to_s
193 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
204 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
194 end
205 end
195
206
196 def test_add_issue_with_attachment_to_specific_project
207 def test_add_issue_with_attachment_to_specific_project
197 issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
208 issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
198 assert issue.is_a?(Issue)
209 assert issue.is_a?(Issue)
199 assert !issue.new_record?
210 assert !issue.new_record?
200 issue.reload
211 issue.reload
201 assert_equal 'Ticket created by email with attachment', issue.subject
212 assert_equal 'Ticket created by email with attachment', issue.subject
202 assert_equal User.find_by_login('jsmith'), issue.author
213 assert_equal User.find_by_login('jsmith'), issue.author
203 assert_equal Project.find(2), issue.project
214 assert_equal Project.find(2), issue.project
204 assert_equal 'This is a new ticket with attachments', issue.description
215 assert_equal 'This is a new ticket with attachments', issue.description
205 # Attachment properties
216 # Attachment properties
206 assert_equal 1, issue.attachments.size
217 assert_equal 1, issue.attachments.size
207 assert_equal 'Paella.jpg', issue.attachments.first.filename
218 assert_equal 'Paella.jpg', issue.attachments.first.filename
208 assert_equal 'image/jpeg', issue.attachments.first.content_type
219 assert_equal 'image/jpeg', issue.attachments.first.content_type
209 assert_equal 10790, issue.attachments.first.filesize
220 assert_equal 10790, issue.attachments.first.filesize
210 end
221 end
211
222
212 def test_add_issue_with_custom_fields
223 def test_add_issue_with_custom_fields
213 issue = submit_email('ticket_with_custom_fields.eml',
224 issue = submit_email('ticket_with_custom_fields.eml',
214 :issue => {:project => 'onlinestore'}, :allow_override => ['database', 'Searchable_field']
225 :issue => {:project => 'onlinestore'}, :allow_override => ['database', 'Searchable_field']
215 )
226 )
216 assert issue.is_a?(Issue)
227 assert issue.is_a?(Issue)
217 assert !issue.new_record?
228 assert !issue.new_record?
218 issue.reload
229 issue.reload
219 assert_equal 'New ticket with custom field values', issue.subject
230 assert_equal 'New ticket with custom field values', issue.subject
220 assert_equal 'PostgreSQL', issue.custom_field_value(1)
231 assert_equal 'PostgreSQL', issue.custom_field_value(1)
221 assert_equal 'Value for a custom field', issue.custom_field_value(2)
232 assert_equal 'Value for a custom field', issue.custom_field_value(2)
222 assert !issue.description.match(/^searchable field:/i)
233 assert !issue.description.match(/^searchable field:/i)
223 end
234 end
224
235
225 def test_add_issue_with_version_custom_fields
236 def test_add_issue_with_version_custom_fields
226 field = IssueCustomField.create!(:name => 'Affected version', :field_format => 'version', :is_for_all => true, :tracker_ids => [1,2,3])
237 field = IssueCustomField.create!(:name => 'Affected version', :field_format => 'version', :is_for_all => true, :tracker_ids => [1,2,3])
227
238
228 issue = submit_email('ticket_with_custom_fields.eml',
239 issue = submit_email('ticket_with_custom_fields.eml',
229 :issue => {:project => 'ecookbook'}, :allow_override => ['affected version']
240 :issue => {:project => 'ecookbook'}, :allow_override => ['affected version']
230 ) do |email|
241 ) do |email|
231 email << "Affected version: 1.0\n"
242 email << "Affected version: 1.0\n"
232 end
243 end
233 assert issue.is_a?(Issue)
244 assert issue.is_a?(Issue)
234 assert !issue.new_record?
245 assert !issue.new_record?
235 issue.reload
246 issue.reload
236 assert_equal '2', issue.custom_field_value(field)
247 assert_equal '2', issue.custom_field_value(field)
237 end
248 end
238
249
239 def test_add_issue_should_match_assignee_on_display_name
250 def test_add_issue_should_match_assignee_on_display_name
240 user = User.generate!(:firstname => 'Foo Bar', :lastname => 'Foo Baz')
251 user = User.generate!(:firstname => 'Foo Bar', :lastname => 'Foo Baz')
241 User.add_to_project(user, Project.find(2))
252 User.add_to_project(user, Project.find(2))
242 issue = submit_email('ticket_on_given_project.eml', :allow_override => ['assigned_to']) do |email|
253 issue = submit_email('ticket_on_given_project.eml', :allow_override => ['assigned_to']) do |email|
243 email.sub!(/^Assigned to.*$/, 'Assigned to: Foo Bar Foo baz')
254 email.sub!(/^Assigned to.*$/, 'Assigned to: Foo Bar Foo baz')
244 end
255 end
245 assert issue.is_a?(Issue)
256 assert issue.is_a?(Issue)
246 assert_equal user, issue.assigned_to
257 assert_equal user, issue.assigned_to
247 end
258 end
248
259
249 def test_add_issue_should_set_default_start_date
260 def test_add_issue_should_set_default_start_date
250 with_settings :default_issue_start_date_to_creation_date => '1' do
261 with_settings :default_issue_start_date_to_creation_date => '1' do
251 issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'})
262 issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'})
252 assert issue.is_a?(Issue)
263 assert issue.is_a?(Issue)
253 assert_equal Date.today, issue.start_date
264 assert_equal Date.today, issue.start_date
254 end
265 end
255 end
266 end
256
267
257 def test_add_issue_with_cc
268 def test_add_issue_with_cc
258 issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'})
269 issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'})
259 assert issue.is_a?(Issue)
270 assert issue.is_a?(Issue)
260 assert !issue.new_record?
271 assert !issue.new_record?
261 issue.reload
272 issue.reload
262 assert issue.watched_by?(User.find_by_mail('dlopper@somenet.foo'))
273 assert issue.watched_by?(User.find_by_mail('dlopper@somenet.foo'))
263 assert_equal 1, issue.watcher_user_ids.size
274 assert_equal 1, issue.watcher_user_ids.size
264 end
275 end
265
276
266 def test_add_issue_from_additional_email_address
277 def test_add_issue_from_additional_email_address
267 user = User.find(2)
278 user = User.find(2)
268 user.mail = 'mainaddress@somenet.foo'
279 user.mail = 'mainaddress@somenet.foo'
269 user.save!
280 user.save!
270 EmailAddress.create!(:user => user, :address => 'jsmith@somenet.foo')
281 EmailAddress.create!(:user => user, :address => 'jsmith@somenet.foo')
271
282
272 issue = submit_email('ticket_on_given_project.eml')
283 issue = submit_email('ticket_on_given_project.eml')
273 assert issue
284 assert issue
274 assert_equal user, issue.author
285 assert_equal user, issue.author
275 end
286 end
276
287
277 def test_add_issue_by_unknown_user
288 def test_add_issue_by_unknown_user
278 assert_no_difference 'User.count' do
289 assert_no_difference 'User.count' do
279 assert_equal false,
290 assert_equal false,
280 submit_email(
291 submit_email(
281 'ticket_by_unknown_user.eml',
292 'ticket_by_unknown_user.eml',
282 :issue => {:project => 'ecookbook'}
293 :issue => {:project => 'ecookbook'}
283 )
294 )
284 end
295 end
285 end
296 end
286
297
287 def test_add_issue_by_anonymous_user
298 def test_add_issue_by_anonymous_user
288 Role.anonymous.add_permission!(:add_issues)
299 Role.anonymous.add_permission!(:add_issues)
289 assert_no_difference 'User.count' do
300 assert_no_difference 'User.count' do
290 issue = submit_email(
301 issue = submit_email(
291 'ticket_by_unknown_user.eml',
302 'ticket_by_unknown_user.eml',
292 :issue => {:project => 'ecookbook'},
303 :issue => {:project => 'ecookbook'},
293 :unknown_user => 'accept'
304 :unknown_user => 'accept'
294 )
305 )
295 assert issue.is_a?(Issue)
306 assert issue.is_a?(Issue)
296 assert issue.author.anonymous?
307 assert issue.author.anonymous?
297 end
308 end
298 end
309 end
299
310
300 def test_add_issue_by_anonymous_user_with_no_from_address
311 def test_add_issue_by_anonymous_user_with_no_from_address
301 Role.anonymous.add_permission!(:add_issues)
312 Role.anonymous.add_permission!(:add_issues)
302 assert_no_difference 'User.count' do
313 assert_no_difference 'User.count' do
303 issue = submit_email(
314 issue = submit_email(
304 'ticket_by_empty_user.eml',
315 'ticket_by_empty_user.eml',
305 :issue => {:project => 'ecookbook'},
316 :issue => {:project => 'ecookbook'},
306 :unknown_user => 'accept'
317 :unknown_user => 'accept'
307 )
318 )
308 assert issue.is_a?(Issue)
319 assert issue.is_a?(Issue)
309 assert issue.author.anonymous?
320 assert issue.author.anonymous?
310 end
321 end
311 end
322 end
312
323
313 def test_add_issue_by_anonymous_user_on_private_project
324 def test_add_issue_by_anonymous_user_on_private_project
314 Role.anonymous.add_permission!(:add_issues)
325 Role.anonymous.add_permission!(:add_issues)
315 assert_no_difference 'User.count' do
326 assert_no_difference 'User.count' do
316 assert_no_difference 'Issue.count' do
327 assert_no_difference 'Issue.count' do
317 assert_equal false,
328 assert_equal false,
318 submit_email(
329 submit_email(
319 'ticket_by_unknown_user.eml',
330 'ticket_by_unknown_user.eml',
320 :issue => {:project => 'onlinestore'},
331 :issue => {:project => 'onlinestore'},
321 :unknown_user => 'accept'
332 :unknown_user => 'accept'
322 )
333 )
323 end
334 end
324 end
335 end
325 end
336 end
326
337
327 def test_add_issue_by_anonymous_user_on_private_project_without_permission_check
338 def test_add_issue_by_anonymous_user_on_private_project_without_permission_check
328 assert_no_difference 'User.count' do
339 assert_no_difference 'User.count' do
329 assert_difference 'Issue.count' do
340 assert_difference 'Issue.count' do
330 issue = submit_email(
341 issue = submit_email(
331 'ticket_by_unknown_user.eml',
342 'ticket_by_unknown_user.eml',
332 :issue => {:project => 'onlinestore'},
343 :issue => {:project => 'onlinestore'},
333 :no_permission_check => '1',
344 :no_permission_check => '1',
334 :unknown_user => 'accept'
345 :unknown_user => 'accept'
335 )
346 )
336 assert issue.is_a?(Issue)
347 assert issue.is_a?(Issue)
337 assert issue.author.anonymous?
348 assert issue.author.anonymous?
338 assert !issue.project.is_public?
349 assert !issue.project.is_public?
339 end
350 end
340 end
351 end
341 end
352 end
342
353
343 def test_add_issue_by_created_user
354 def test_add_issue_by_created_user
344 Setting.default_language = 'en'
355 Setting.default_language = 'en'
345 assert_difference 'User.count' do
356 assert_difference 'User.count' do
346 issue = submit_email(
357 issue = submit_email(
347 'ticket_by_unknown_user.eml',
358 'ticket_by_unknown_user.eml',
348 :issue => {:project => 'ecookbook'},
359 :issue => {:project => 'ecookbook'},
349 :unknown_user => 'create'
360 :unknown_user => 'create'
350 )
361 )
351 assert issue.is_a?(Issue)
362 assert issue.is_a?(Issue)
352 assert issue.author.active?
363 assert issue.author.active?
353 assert_equal 'john.doe@somenet.foo', issue.author.mail
364 assert_equal 'john.doe@somenet.foo', issue.author.mail
354 assert_equal 'John', issue.author.firstname
365 assert_equal 'John', issue.author.firstname
355 assert_equal 'Doe', issue.author.lastname
366 assert_equal 'Doe', issue.author.lastname
356
367
357 # account information
368 # account information
358 email = ActionMailer::Base.deliveries.first
369 email = ActionMailer::Base.deliveries.first
359 assert_not_nil email
370 assert_not_nil email
360 assert email.subject.include?('account activation')
371 assert email.subject.include?('account activation')
361 login = mail_body(email).match(/\* Login: (.*)$/)[1].strip
372 login = mail_body(email).match(/\* Login: (.*)$/)[1].strip
362 password = mail_body(email).match(/\* Password: (.*)$/)[1].strip
373 password = mail_body(email).match(/\* Password: (.*)$/)[1].strip
363 assert_equal issue.author, User.try_to_login(login, password)
374 assert_equal issue.author, User.try_to_login(login, password)
364 end
375 end
365 end
376 end
366
377
367 def test_add_issue_should_send_notification
378 def test_add_issue_should_send_notification
368 issue = submit_email('ticket_on_given_project.eml', :allow_override => 'all')
379 issue = submit_email('ticket_on_given_project.eml', :allow_override => 'all')
369 assert issue.is_a?(Issue)
380 assert issue.is_a?(Issue)
370 assert !issue.new_record?
381 assert !issue.new_record?
371
382
372 mail = ActionMailer::Base.deliveries.last
383 mail = ActionMailer::Base.deliveries.last
373 assert_not_nil mail
384 assert_not_nil mail
374 assert mail.subject.include?("##{issue.id}")
385 assert mail.subject.include?("##{issue.id}")
375 assert mail.subject.include?('New ticket on a given project')
386 assert mail.subject.include?('New ticket on a given project')
376 end
387 end
377
388
378 def test_created_user_should_be_added_to_groups
389 def test_created_user_should_be_added_to_groups
379 group1 = Group.generate!
390 group1 = Group.generate!
380 group2 = Group.generate!
391 group2 = Group.generate!
381
392
382 assert_difference 'User.count' do
393 assert_difference 'User.count' do
383 submit_email(
394 submit_email(
384 'ticket_by_unknown_user.eml',
395 'ticket_by_unknown_user.eml',
385 :issue => {:project => 'ecookbook'},
396 :issue => {:project => 'ecookbook'},
386 :unknown_user => 'create',
397 :unknown_user => 'create',
387 :default_group => "#{group1.name},#{group2.name}"
398 :default_group => "#{group1.name},#{group2.name}"
388 )
399 )
389 end
400 end
390 user = User.order('id DESC').first
401 user = User.order('id DESC').first
391 assert_equal [group1, group2].sort, user.groups.sort
402 assert_equal [group1, group2].sort, user.groups.sort
392 end
403 end
393
404
394 def test_created_user_should_not_receive_account_information_with_no_account_info_option
405 def test_created_user_should_not_receive_account_information_with_no_account_info_option
395 assert_difference 'User.count' do
406 assert_difference 'User.count' do
396 submit_email(
407 submit_email(
397 'ticket_by_unknown_user.eml',
408 'ticket_by_unknown_user.eml',
398 :issue => {:project => 'ecookbook'},
409 :issue => {:project => 'ecookbook'},
399 :unknown_user => 'create',
410 :unknown_user => 'create',
400 :no_account_notice => '1'
411 :no_account_notice => '1'
401 )
412 )
402 end
413 end
403
414
404 # only 1 email for the new issue notification
415 # only 1 email for the new issue notification
405 assert_equal 1, ActionMailer::Base.deliveries.size
416 assert_equal 1, ActionMailer::Base.deliveries.size
406 email = ActionMailer::Base.deliveries.first
417 email = ActionMailer::Base.deliveries.first
407 assert_include 'Ticket by unknown user', email.subject
418 assert_include 'Ticket by unknown user', email.subject
408 end
419 end
409
420
410 def test_created_user_should_have_mail_notification_to_none_with_no_notification_option
421 def test_created_user_should_have_mail_notification_to_none_with_no_notification_option
411 assert_difference 'User.count' do
422 assert_difference 'User.count' do
412 submit_email(
423 submit_email(
413 'ticket_by_unknown_user.eml',
424 'ticket_by_unknown_user.eml',
414 :issue => {:project => 'ecookbook'},
425 :issue => {:project => 'ecookbook'},
415 :unknown_user => 'create',
426 :unknown_user => 'create',
416 :no_notification => '1'
427 :no_notification => '1'
417 )
428 )
418 end
429 end
419 user = User.order('id DESC').first
430 user = User.order('id DESC').first
420 assert_equal 'none', user.mail_notification
431 assert_equal 'none', user.mail_notification
421 end
432 end
422
433
423 def test_add_issue_without_from_header
434 def test_add_issue_without_from_header
424 Role.anonymous.add_permission!(:add_issues)
435 Role.anonymous.add_permission!(:add_issues)
425 assert_equal false, submit_email('ticket_without_from_header.eml')
436 assert_equal false, submit_email('ticket_without_from_header.eml')
426 end
437 end
427
438
428 def test_add_issue_with_invalid_attributes
439 def test_add_issue_with_invalid_attributes
429 with_settings :default_issue_start_date_to_creation_date => '0' do
440 with_settings :default_issue_start_date_to_creation_date => '0' do
430 issue = submit_email(
441 issue = submit_email(
431 'ticket_with_invalid_attributes.eml',
442 'ticket_with_invalid_attributes.eml',
432 :allow_override => 'tracker,category,priority'
443 :allow_override => 'tracker,category,priority'
433 )
444 )
434 assert issue.is_a?(Issue)
445 assert issue.is_a?(Issue)
435 assert !issue.new_record?
446 assert !issue.new_record?
436 issue.reload
447 issue.reload
437 assert_nil issue.assigned_to
448 assert_nil issue.assigned_to
438 assert_nil issue.start_date
449 assert_nil issue.start_date
439 assert_nil issue.due_date
450 assert_nil issue.due_date
440 assert_equal 0, issue.done_ratio
451 assert_equal 0, issue.done_ratio
441 assert_equal 'Normal', issue.priority.to_s
452 assert_equal 'Normal', issue.priority.to_s
442 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
453 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
443 end
454 end
444 end
455 end
445
456
446 def test_add_issue_with_invalid_project_should_be_assigned_to_default_project
457 def test_add_issue_with_invalid_project_should_be_assigned_to_default_project
447 issue = submit_email('ticket_on_given_project.eml', :issue => {:project => 'ecookbook'}, :allow_override => 'project') do |email|
458 issue = submit_email('ticket_on_given_project.eml', :issue => {:project => 'ecookbook'}, :allow_override => 'project') do |email|
448 email.gsub!(/^Project:.+$/, 'Project: invalid')
459 email.gsub!(/^Project:.+$/, 'Project: invalid')
449 end
460 end
450 assert issue.is_a?(Issue)
461 assert issue.is_a?(Issue)
451 assert !issue.new_record?
462 assert !issue.new_record?
452 assert_equal 'ecookbook', issue.project.identifier
463 assert_equal 'ecookbook', issue.project.identifier
453 end
464 end
454
465
455 def test_add_issue_with_localized_attributes
466 def test_add_issue_with_localized_attributes
456 User.find_by_mail('jsmith@somenet.foo').update_attribute 'language', 'fr'
467 User.find_by_mail('jsmith@somenet.foo').update_attribute 'language', 'fr'
457 issue = submit_email(
468 issue = submit_email(
458 'ticket_with_localized_attributes.eml',
469 'ticket_with_localized_attributes.eml',
459 :allow_override => 'tracker,category,priority'
470 :allow_override => 'tracker,category,priority'
460 )
471 )
461 assert issue.is_a?(Issue)
472 assert issue.is_a?(Issue)
462 assert !issue.new_record?
473 assert !issue.new_record?
463 issue.reload
474 issue.reload
464 assert_equal 'New ticket on a given project', issue.subject
475 assert_equal 'New ticket on a given project', issue.subject
465 assert_equal User.find_by_login('jsmith'), issue.author
476 assert_equal User.find_by_login('jsmith'), issue.author
466 assert_equal Project.find(2), issue.project
477 assert_equal Project.find(2), issue.project
467 assert_equal 'Feature request', issue.tracker.to_s
478 assert_equal 'Feature request', issue.tracker.to_s
468 assert_equal 'Stock management', issue.category.to_s
479 assert_equal 'Stock management', issue.category.to_s
469 assert_equal 'Urgent', issue.priority.to_s
480 assert_equal 'Urgent', issue.priority.to_s
470 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
481 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
471 end
482 end
472
483
473 def test_add_issue_with_japanese_keywords
484 def test_add_issue_with_japanese_keywords
474 ja_dev = "\xe9\x96\x8b\xe7\x99\xba".force_encoding('UTF-8')
485 ja_dev = "\xe9\x96\x8b\xe7\x99\xba".force_encoding('UTF-8')
475 tracker = Tracker.generate!(:name => ja_dev)
486 tracker = Tracker.generate!(:name => ja_dev)
476 Project.find(1).trackers << tracker
487 Project.find(1).trackers << tracker
477 issue = submit_email(
488 issue = submit_email(
478 'japanese_keywords_iso_2022_jp.eml',
489 'japanese_keywords_iso_2022_jp.eml',
479 :issue => {:project => 'ecookbook'},
490 :issue => {:project => 'ecookbook'},
480 :allow_override => 'tracker'
491 :allow_override => 'tracker'
481 )
492 )
482 assert_kind_of Issue, issue
493 assert_kind_of Issue, issue
483 assert_equal tracker, issue.tracker
494 assert_equal tracker, issue.tracker
484 end
495 end
485
496
486 def test_add_issue_from_apple_mail
497 def test_add_issue_from_apple_mail
487 issue = submit_email(
498 issue = submit_email(
488 'apple_mail_with_attachment.eml',
499 'apple_mail_with_attachment.eml',
489 :issue => {:project => 'ecookbook'}
500 :issue => {:project => 'ecookbook'}
490 )
501 )
491 assert_kind_of Issue, issue
502 assert_kind_of Issue, issue
492 assert_equal 1, issue.attachments.size
503 assert_equal 1, issue.attachments.size
493
504
494 attachment = issue.attachments.first
505 attachment = issue.attachments.first
495 assert_equal 'paella.jpg', attachment.filename
506 assert_equal 'paella.jpg', attachment.filename
496 assert_equal 10790, attachment.filesize
507 assert_equal 10790, attachment.filesize
497 assert File.exist?(attachment.diskfile)
508 assert File.exist?(attachment.diskfile)
498 assert_equal 10790, File.size(attachment.diskfile)
509 assert_equal 10790, File.size(attachment.diskfile)
499 assert_equal 'caaf384198bcbc9563ab5c058acd73cd', attachment.digest
510 assert_equal 'caaf384198bcbc9563ab5c058acd73cd', attachment.digest
500 end
511 end
501
512
502 def test_thunderbird_with_attachment_ja
513 def test_thunderbird_with_attachment_ja
503 issue = submit_email(
514 issue = submit_email(
504 'thunderbird_with_attachment_ja.eml',
515 'thunderbird_with_attachment_ja.eml',
505 :issue => {:project => 'ecookbook'}
516 :issue => {:project => 'ecookbook'}
506 )
517 )
507 assert_kind_of Issue, issue
518 assert_kind_of Issue, issue
508 assert_equal 1, issue.attachments.size
519 assert_equal 1, issue.attachments.size
509 ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88.txt".force_encoding('UTF-8')
520 ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88.txt".force_encoding('UTF-8')
510 attachment = issue.attachments.first
521 attachment = issue.attachments.first
511 assert_equal ja, attachment.filename
522 assert_equal ja, attachment.filename
512 assert_equal 5, attachment.filesize
523 assert_equal 5, attachment.filesize
513 assert File.exist?(attachment.diskfile)
524 assert File.exist?(attachment.diskfile)
514 assert_equal 5, File.size(attachment.diskfile)
525 assert_equal 5, File.size(attachment.diskfile)
515 assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest
526 assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest
516 end
527 end
517
528
518 def test_gmail_with_attachment_ja
529 def test_gmail_with_attachment_ja
519 issue = submit_email(
530 issue = submit_email(
520 'gmail_with_attachment_ja.eml',
531 'gmail_with_attachment_ja.eml',
521 :issue => {:project => 'ecookbook'}
532 :issue => {:project => 'ecookbook'}
522 )
533 )
523 assert_kind_of Issue, issue
534 assert_kind_of Issue, issue
524 assert_equal 1, issue.attachments.size
535 assert_equal 1, issue.attachments.size
525 ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88.txt".force_encoding('UTF-8')
536 ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88.txt".force_encoding('UTF-8')
526 attachment = issue.attachments.first
537 attachment = issue.attachments.first
527 assert_equal ja, attachment.filename
538 assert_equal ja, attachment.filename
528 assert_equal 5, attachment.filesize
539 assert_equal 5, attachment.filesize
529 assert File.exist?(attachment.diskfile)
540 assert File.exist?(attachment.diskfile)
530 assert_equal 5, File.size(attachment.diskfile)
541 assert_equal 5, File.size(attachment.diskfile)
531 assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest
542 assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest
532 end
543 end
533
544
534 def test_thunderbird_with_attachment_latin1
545 def test_thunderbird_with_attachment_latin1
535 issue = submit_email(
546 issue = submit_email(
536 'thunderbird_with_attachment_iso-8859-1.eml',
547 'thunderbird_with_attachment_iso-8859-1.eml',
537 :issue => {:project => 'ecookbook'}
548 :issue => {:project => 'ecookbook'}
538 )
549 )
539 assert_kind_of Issue, issue
550 assert_kind_of Issue, issue
540 assert_equal 1, issue.attachments.size
551 assert_equal 1, issue.attachments.size
541 u = "".force_encoding('UTF-8')
552 u = "".force_encoding('UTF-8')
542 u1 = "\xc3\x84\xc3\xa4\xc3\x96\xc3\xb6\xc3\x9c\xc3\xbc".force_encoding('UTF-8')
553 u1 = "\xc3\x84\xc3\xa4\xc3\x96\xc3\xb6\xc3\x9c\xc3\xbc".force_encoding('UTF-8')
543 11.times { u << u1 }
554 11.times { u << u1 }
544 attachment = issue.attachments.first
555 attachment = issue.attachments.first
545 assert_equal "#{u}.png", attachment.filename
556 assert_equal "#{u}.png", attachment.filename
546 assert_equal 130, attachment.filesize
557 assert_equal 130, attachment.filesize
547 assert File.exist?(attachment.diskfile)
558 assert File.exist?(attachment.diskfile)
548 assert_equal 130, File.size(attachment.diskfile)
559 assert_equal 130, File.size(attachment.diskfile)
549 assert_equal '4d80e667ac37dddfe05502530f152abb', attachment.digest
560 assert_equal '4d80e667ac37dddfe05502530f152abb', attachment.digest
550 end
561 end
551
562
552 def test_gmail_with_attachment_latin1
563 def test_gmail_with_attachment_latin1
553 issue = submit_email(
564 issue = submit_email(
554 'gmail_with_attachment_iso-8859-1.eml',
565 'gmail_with_attachment_iso-8859-1.eml',
555 :issue => {:project => 'ecookbook'}
566 :issue => {:project => 'ecookbook'}
556 )
567 )
557 assert_kind_of Issue, issue
568 assert_kind_of Issue, issue
558 assert_equal 1, issue.attachments.size
569 assert_equal 1, issue.attachments.size
559 u = "".force_encoding('UTF-8')
570 u = "".force_encoding('UTF-8')
560 u1 = "\xc3\x84\xc3\xa4\xc3\x96\xc3\xb6\xc3\x9c\xc3\xbc".force_encoding('UTF-8')
571 u1 = "\xc3\x84\xc3\xa4\xc3\x96\xc3\xb6\xc3\x9c\xc3\xbc".force_encoding('UTF-8')
561 11.times { u << u1 }
572 11.times { u << u1 }
562 attachment = issue.attachments.first
573 attachment = issue.attachments.first
563 assert_equal "#{u}.txt", attachment.filename
574 assert_equal "#{u}.txt", attachment.filename
564 assert_equal 5, attachment.filesize
575 assert_equal 5, attachment.filesize
565 assert File.exist?(attachment.diskfile)
576 assert File.exist?(attachment.diskfile)
566 assert_equal 5, File.size(attachment.diskfile)
577 assert_equal 5, File.size(attachment.diskfile)
567 assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest
578 assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest
568 end
579 end
569
580
570 def test_multiple_inline_text_parts_should_be_appended_to_issue_description
581 def test_multiple_inline_text_parts_should_be_appended_to_issue_description
571 issue = submit_email('multiple_text_parts.eml', :issue => {:project => 'ecookbook'})
582 issue = submit_email('multiple_text_parts.eml', :issue => {:project => 'ecookbook'})
572 assert_include 'first', issue.description
583 assert_include 'first', issue.description
573 assert_include 'second', issue.description
584 assert_include 'second', issue.description
574 assert_include 'third', issue.description
585 assert_include 'third', issue.description
575 end
586 end
576
587
577 def test_attachment_text_part_should_be_added_as_issue_attachment
588 def test_attachment_text_part_should_be_added_as_issue_attachment
578 issue = submit_email('multiple_text_parts.eml', :issue => {:project => 'ecookbook'})
589 issue = submit_email('multiple_text_parts.eml', :issue => {:project => 'ecookbook'})
579 assert_not_include 'Plain text attachment', issue.description
590 assert_not_include 'Plain text attachment', issue.description
580 attachment = issue.attachments.detect {|a| a.filename == 'textfile.txt'}
591 attachment = issue.attachments.detect {|a| a.filename == 'textfile.txt'}
581 assert_not_nil attachment
592 assert_not_nil attachment
582 assert_include 'Plain text attachment', File.read(attachment.diskfile)
593 assert_include 'Plain text attachment', File.read(attachment.diskfile)
583 end
594 end
584
595
585 def test_add_issue_with_iso_8859_1_subject
596 def test_add_issue_with_iso_8859_1_subject
586 issue = submit_email(
597 issue = submit_email(
587 'subject_as_iso-8859-1.eml',
598 'subject_as_iso-8859-1.eml',
588 :issue => {:project => 'ecookbook'}
599 :issue => {:project => 'ecookbook'}
589 )
600 )
590 str = "Testmail from Webmail: \xc3\xa4 \xc3\xb6 \xc3\xbc...".force_encoding('UTF-8')
601 str = "Testmail from Webmail: \xc3\xa4 \xc3\xb6 \xc3\xbc...".force_encoding('UTF-8')
591 assert_kind_of Issue, issue
602 assert_kind_of Issue, issue
592 assert_equal str, issue.subject
603 assert_equal str, issue.subject
593 end
604 end
594
605
595 def test_quoted_printable_utf8
606 def test_quoted_printable_utf8
596 issue = submit_email(
607 issue = submit_email(
597 'quoted_printable_utf8.eml',
608 'quoted_printable_utf8.eml',
598 :issue => {:project => 'ecookbook'}
609 :issue => {:project => 'ecookbook'}
599 )
610 )
600 assert_kind_of Issue, issue
611 assert_kind_of Issue, issue
601 str = "Freundliche Gr\xc3\xbcsse".force_encoding('UTF-8')
612 str = "Freundliche Gr\xc3\xbcsse".force_encoding('UTF-8')
602 assert_equal str, issue.description
613 assert_equal str, issue.description
603 end
614 end
604
615
605 def test_gmail_iso8859_2
616 def test_gmail_iso8859_2
606 issue = submit_email(
617 issue = submit_email(
607 'gmail-iso8859-2.eml',
618 'gmail-iso8859-2.eml',
608 :issue => {:project => 'ecookbook'}
619 :issue => {:project => 'ecookbook'}
609 )
620 )
610 assert_kind_of Issue, issue
621 assert_kind_of Issue, issue
611 str = "Na \xc5\xa1triku se su\xc5\xa1i \xc5\xa1osi\xc4\x87.".force_encoding('UTF-8')
622 str = "Na \xc5\xa1triku se su\xc5\xa1i \xc5\xa1osi\xc4\x87.".force_encoding('UTF-8')
612 assert issue.description.include?(str)
623 assert issue.description.include?(str)
613 end
624 end
614
625
615 def test_add_issue_with_japanese_subject
626 def test_add_issue_with_japanese_subject
616 issue = submit_email(
627 issue = submit_email(
617 'subject_japanese_1.eml',
628 'subject_japanese_1.eml',
618 :issue => {:project => 'ecookbook'}
629 :issue => {:project => 'ecookbook'}
619 )
630 )
620 assert_kind_of Issue, issue
631 assert_kind_of Issue, issue
621 ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88".force_encoding('UTF-8')
632 ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88".force_encoding('UTF-8')
622 assert_equal ja, issue.subject
633 assert_equal ja, issue.subject
623 end
634 end
624
635
625 def test_add_issue_with_korean_body
636 def test_add_issue_with_korean_body
626 # Make sure mail bodies with a charset unknown to Ruby
637 # Make sure mail bodies with a charset unknown to Ruby
627 # but known to the Mail gem 2.5.4 are handled correctly
638 # but known to the Mail gem 2.5.4 are handled correctly
628 kr = "\xEA\xB3\xA0\xEB\xA7\x99\xEC\x8A\xB5\xEB\x8B\x88\xEB\x8B\xA4.".force_encoding('UTF-8')
639 kr = "\xEA\xB3\xA0\xEB\xA7\x99\xEC\x8A\xB5\xEB\x8B\x88\xEB\x8B\xA4.".force_encoding('UTF-8')
629 issue = submit_email(
640 issue = submit_email(
630 'body_ks_c_5601-1987.eml',
641 'body_ks_c_5601-1987.eml',
631 :issue => {:project => 'ecookbook'}
642 :issue => {:project => 'ecookbook'}
632 )
643 )
633 assert_kind_of Issue, issue
644 assert_kind_of Issue, issue
634 assert_equal kr, issue.description
645 assert_equal kr, issue.description
635 end
646 end
636
647
637 def test_add_issue_with_no_subject_header
648 def test_add_issue_with_no_subject_header
638 issue = submit_email(
649 issue = submit_email(
639 'no_subject_header.eml',
650 'no_subject_header.eml',
640 :issue => {:project => 'ecookbook'}
651 :issue => {:project => 'ecookbook'}
641 )
652 )
642 assert_kind_of Issue, issue
653 assert_kind_of Issue, issue
643 assert_equal '(no subject)', issue.subject
654 assert_equal '(no subject)', issue.subject
644 end
655 end
645
656
646 def test_add_issue_with_mixed_japanese_subject
657 def test_add_issue_with_mixed_japanese_subject
647 issue = submit_email(
658 issue = submit_email(
648 'subject_japanese_2.eml',
659 'subject_japanese_2.eml',
649 :issue => {:project => 'ecookbook'}
660 :issue => {:project => 'ecookbook'}
650 )
661 )
651 assert_kind_of Issue, issue
662 assert_kind_of Issue, issue
652 ja = "Re: \xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88".force_encoding('UTF-8')
663 ja = "Re: \xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88".force_encoding('UTF-8')
653 assert_equal ja, issue.subject
664 assert_equal ja, issue.subject
654 end
665 end
655
666
656 def test_should_ignore_emails_from_locked_users
667 def test_should_ignore_emails_from_locked_users
657 User.find(2).lock!
668 User.find(2).lock!
658
669
659 MailHandler.any_instance.expects(:dispatch).never
670 MailHandler.any_instance.expects(:dispatch).never
660 assert_no_difference 'Issue.count' do
671 assert_no_difference 'Issue.count' do
661 assert_equal false, submit_email('ticket_on_given_project.eml')
672 assert_equal false, submit_email('ticket_on_given_project.eml')
662 end
673 end
663 end
674 end
664
675
665 def test_should_ignore_emails_from_emission_address
676 def test_should_ignore_emails_from_emission_address
666 Role.anonymous.add_permission!(:add_issues)
677 Role.anonymous.add_permission!(:add_issues)
667 assert_no_difference 'User.count' do
678 assert_no_difference 'User.count' do
668 assert_equal false,
679 assert_equal false,
669 submit_email(
680 submit_email(
670 'ticket_from_emission_address.eml',
681 'ticket_from_emission_address.eml',
671 :issue => {:project => 'ecookbook'},
682 :issue => {:project => 'ecookbook'},
672 :unknown_user => 'create'
683 :unknown_user => 'create'
673 )
684 )
674 end
685 end
675 end
686 end
676
687
677 def test_should_ignore_auto_replied_emails
688 def test_should_ignore_auto_replied_emails
678 MailHandler.any_instance.expects(:dispatch).never
689 MailHandler.any_instance.expects(:dispatch).never
679 [
690 [
680 "Auto-Submitted: auto-replied",
691 "Auto-Submitted: auto-replied",
681 "Auto-Submitted: Auto-Replied",
692 "Auto-Submitted: Auto-Replied",
682 "Auto-Submitted: auto-generated",
693 "Auto-Submitted: auto-generated",
683 'X-Autoreply: yes'
694 'X-Autoreply: yes'
684 ].each do |header|
695 ].each do |header|
685 raw = IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml'))
696 raw = IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml'))
686 raw = header + "\n" + raw
697 raw = header + "\n" + raw
687
698
688 assert_no_difference 'Issue.count' do
699 assert_no_difference 'Issue.count' do
689 assert_equal false, MailHandler.receive(raw), "email with #{header} header was not ignored"
700 assert_equal false, MailHandler.receive(raw), "email with #{header} header was not ignored"
690 end
701 end
691 end
702 end
692 end
703 end
693
704
694 test "should not ignore Auto-Submitted headers not defined in RFC3834" do
705 test "should not ignore Auto-Submitted headers not defined in RFC3834" do
695 [
706 [
696 "Auto-Submitted: auto-forwarded"
707 "Auto-Submitted: auto-forwarded"
697 ].each do |header|
708 ].each do |header|
698 raw = IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml'))
709 raw = IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml'))
699 raw = header + "\n" + raw
710 raw = header + "\n" + raw
700
711
701 assert_difference 'Issue.count', 1 do
712 assert_difference 'Issue.count', 1 do
702 assert_not_nil MailHandler.receive(raw), "email with #{header} header was ignored"
713 assert_not_nil MailHandler.receive(raw), "email with #{header} header was ignored"
703 end
714 end
704 end
715 end
705 end
716 end
706
717
707 def test_add_issue_should_send_email_notification
718 def test_add_issue_should_send_email_notification
708 Setting.notified_events = ['issue_added']
719 Setting.notified_events = ['issue_added']
709 # This email contains: 'Project: onlinestore'
720 # This email contains: 'Project: onlinestore'
710 issue = submit_email('ticket_on_given_project.eml')
721 issue = submit_email('ticket_on_given_project.eml')
711 assert issue.is_a?(Issue)
722 assert issue.is_a?(Issue)
712 assert_equal 1, ActionMailer::Base.deliveries.size
723 assert_equal 1, ActionMailer::Base.deliveries.size
713 end
724 end
714
725
715 def test_update_issue
726 def test_update_issue
716 journal = submit_email('ticket_reply.eml')
727 journal = submit_email('ticket_reply.eml')
717 assert journal.is_a?(Journal)
728 assert journal.is_a?(Journal)
718 assert_equal User.find_by_login('jsmith'), journal.user
729 assert_equal User.find_by_login('jsmith'), journal.user
719 assert_equal Issue.find(2), journal.journalized
730 assert_equal Issue.find(2), journal.journalized
720 assert_match /This is reply/, journal.notes
731 assert_match /This is reply/, journal.notes
721 assert_equal false, journal.private_notes
732 assert_equal false, journal.private_notes
722 assert_equal 'Feature request', journal.issue.tracker.name
733 assert_equal 'Feature request', journal.issue.tracker.name
723 end
734 end
724
735
725 def test_update_issue_should_accept_issue_id_after_space_inside_brackets
736 def test_update_issue_should_accept_issue_id_after_space_inside_brackets
726 journal = submit_email('ticket_reply_with_status.eml') do |email|
737 journal = submit_email('ticket_reply_with_status.eml') do |email|
727 assert email.sub!(/^Subject:.*$/, "Subject: Re: [Feature request #2] Add ingredients categories")
738 assert email.sub!(/^Subject:.*$/, "Subject: Re: [Feature request #2] Add ingredients categories")
728 end
739 end
729 assert journal.is_a?(Journal)
740 assert journal.is_a?(Journal)
730 assert_equal Issue.find(2), journal.journalized
741 assert_equal Issue.find(2), journal.journalized
731 end
742 end
732
743
733 def test_update_issue_should_accept_issue_id_inside_brackets
744 def test_update_issue_should_accept_issue_id_inside_brackets
734 journal = submit_email('ticket_reply_with_status.eml') do |email|
745 journal = submit_email('ticket_reply_with_status.eml') do |email|
735 assert email.sub!(/^Subject:.*$/, "Subject: Re: [#2] Add ingredients categories")
746 assert email.sub!(/^Subject:.*$/, "Subject: Re: [#2] Add ingredients categories")
736 end
747 end
737 assert journal.is_a?(Journal)
748 assert journal.is_a?(Journal)
738 assert_equal Issue.find(2), journal.journalized
749 assert_equal Issue.find(2), journal.journalized
739 end
750 end
740
751
741 def test_update_issue_should_ignore_bogus_issue_ids_in_subject
752 def test_update_issue_should_ignore_bogus_issue_ids_in_subject
742 journal = submit_email('ticket_reply_with_status.eml') do |email|
753 journal = submit_email('ticket_reply_with_status.eml') do |email|
743 assert email.sub!(/^Subject:.*$/, "Subject: Re: [12345#1][bogus#1][Feature request #2] Add ingredients categories")
754 assert email.sub!(/^Subject:.*$/, "Subject: Re: [12345#1][bogus#1][Feature request #2] Add ingredients categories")
744 end
755 end
745 assert journal.is_a?(Journal)
756 assert journal.is_a?(Journal)
746 assert_equal Issue.find(2), journal.journalized
757 assert_equal Issue.find(2), journal.journalized
747 end
758 end
748
759
749 def test_update_issue_with_attribute_changes
760 def test_update_issue_with_attribute_changes
750 journal = submit_email('ticket_reply_with_status.eml', :allow_override => ['status','assigned_to','start_date','due_date', 'float field'])
761 journal = submit_email('ticket_reply_with_status.eml', :allow_override => ['status','assigned_to','start_date','due_date', 'float field'])
751 assert journal.is_a?(Journal)
762 assert journal.is_a?(Journal)
752 issue = Issue.find(journal.issue.id)
763 issue = Issue.find(journal.issue.id)
753 assert_equal User.find_by_login('jsmith'), journal.user
764 assert_equal User.find_by_login('jsmith'), journal.user
754 assert_equal Issue.find(2), journal.journalized
765 assert_equal Issue.find(2), journal.journalized
755 assert_match /This is reply/, journal.notes
766 assert_match /This is reply/, journal.notes
756 assert_equal 'Feature request', journal.issue.tracker.name
767 assert_equal 'Feature request', journal.issue.tracker.name
757 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
768 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
758 assert_equal '2010-01-01', issue.start_date.to_s
769 assert_equal '2010-01-01', issue.start_date.to_s
759 assert_equal '2010-12-31', issue.due_date.to_s
770 assert_equal '2010-12-31', issue.due_date.to_s
760 assert_equal User.find_by_login('jsmith'), issue.assigned_to
771 assert_equal User.find_by_login('jsmith'), issue.assigned_to
761 assert_equal "52.6", issue.custom_value_for(CustomField.find_by_name('Float field')).value
772 assert_equal "52.6", issue.custom_value_for(CustomField.find_by_name('Float field')).value
762 # keywords should be removed from the email body
773 # keywords should be removed from the email body
763 assert !journal.notes.match(/^Status:/i)
774 assert !journal.notes.match(/^Status:/i)
764 assert !journal.notes.match(/^Start Date:/i)
775 assert !journal.notes.match(/^Start Date:/i)
765 end
776 end
766
777
767 def test_update_issue_with_attachment
778 def test_update_issue_with_attachment
768 assert_difference 'Journal.count' do
779 assert_difference 'Journal.count' do
769 assert_difference 'JournalDetail.count' do
780 assert_difference 'JournalDetail.count' do
770 assert_difference 'Attachment.count' do
781 assert_difference 'Attachment.count' do
771 assert_no_difference 'Issue.count' do
782 assert_no_difference 'Issue.count' do
772 journal = submit_email('ticket_with_attachment.eml') do |raw|
783 journal = submit_email('ticket_with_attachment.eml') do |raw|
773 raw.gsub! /^Subject: .*$/, 'Subject: Re: [Cookbook - Feature #2] (New) Add ingredients categories'
784 raw.gsub! /^Subject: .*$/, 'Subject: Re: [Cookbook - Feature #2] (New) Add ingredients categories'
774 end
785 end
775 end
786 end
776 end
787 end
777 end
788 end
778 end
789 end
779 journal = Journal.order('id DESC').first
790 journal = Journal.order('id DESC').first
780 assert_equal Issue.find(2), journal.journalized
791 assert_equal Issue.find(2), journal.journalized
781 assert_equal 1, journal.details.size
792 assert_equal 1, journal.details.size
782
793
783 detail = journal.details.first
794 detail = journal.details.first
784 assert_equal 'attachment', detail.property
795 assert_equal 'attachment', detail.property
785 assert_equal 'Paella.jpg', detail.value
796 assert_equal 'Paella.jpg', detail.value
786 end
797 end
787
798
788 def test_update_issue_should_send_email_notification
799 def test_update_issue_should_send_email_notification
789 journal = submit_email('ticket_reply.eml')
800 journal = submit_email('ticket_reply.eml')
790 assert journal.is_a?(Journal)
801 assert journal.is_a?(Journal)
791 assert_equal 1, ActionMailer::Base.deliveries.size
802 assert_equal 1, ActionMailer::Base.deliveries.size
792 end
803 end
793
804
794 def test_update_issue_should_not_set_defaults
805 def test_update_issue_should_not_set_defaults
795 journal = submit_email(
806 journal = submit_email(
796 'ticket_reply.eml',
807 'ticket_reply.eml',
797 :issue => {:tracker => 'Support request', :priority => 'High'}
808 :issue => {:tracker => 'Support request', :priority => 'High'}
798 )
809 )
799 assert journal.is_a?(Journal)
810 assert journal.is_a?(Journal)
800 assert_match /This is reply/, journal.notes
811 assert_match /This is reply/, journal.notes
801 assert_equal 'Feature request', journal.issue.tracker.name
812 assert_equal 'Feature request', journal.issue.tracker.name
802 assert_equal 'Normal', journal.issue.priority.name
813 assert_equal 'Normal', journal.issue.priority.name
803 end
814 end
804
815
805 def test_replying_to_a_private_note_should_add_reply_as_private
816 def test_replying_to_a_private_note_should_add_reply_as_private
806 private_journal = Journal.create!(:notes => 'Private notes', :journalized => Issue.find(1), :private_notes => true, :user_id => 2)
817 private_journal = Journal.create!(:notes => 'Private notes', :journalized => Issue.find(1), :private_notes => true, :user_id => 2)
807
818
808 assert_difference 'Journal.count' do
819 assert_difference 'Journal.count' do
809 journal = submit_email('ticket_reply.eml') do |email|
820 journal = submit_email('ticket_reply.eml') do |email|
810 email.sub! %r{^In-Reply-To:.*$}, "In-Reply-To: <redmine.journal-#{private_journal.id}.20060719210421@osiris>"
821 email.sub! %r{^In-Reply-To:.*$}, "In-Reply-To: <redmine.journal-#{private_journal.id}.20060719210421@osiris>"
811 end
822 end
812
823
813 assert_kind_of Journal, journal
824 assert_kind_of Journal, journal
814 assert_match /This is reply/, journal.notes
825 assert_match /This is reply/, journal.notes
815 assert_equal true, journal.private_notes
826 assert_equal true, journal.private_notes
816 end
827 end
817 end
828 end
818
829
819 def test_reply_to_a_message
830 def test_reply_to_a_message
820 m = submit_email('message_reply.eml')
831 m = submit_email('message_reply.eml')
821 assert m.is_a?(Message)
832 assert m.is_a?(Message)
822 assert !m.new_record?
833 assert !m.new_record?
823 m.reload
834 m.reload
824 assert_equal 'Reply via email', m.subject
835 assert_equal 'Reply via email', m.subject
825 # The email replies to message #2 which is part of the thread of message #1
836 # The email replies to message #2 which is part of the thread of message #1
826 assert_equal Message.find(1), m.parent
837 assert_equal Message.find(1), m.parent
827 end
838 end
828
839
829 def test_reply_to_a_message_by_subject
840 def test_reply_to_a_message_by_subject
830 m = submit_email('message_reply_by_subject.eml')
841 m = submit_email('message_reply_by_subject.eml')
831 assert m.is_a?(Message)
842 assert m.is_a?(Message)
832 assert !m.new_record?
843 assert !m.new_record?
833 m.reload
844 m.reload
834 assert_equal 'Reply to the first post', m.subject
845 assert_equal 'Reply to the first post', m.subject
835 assert_equal Message.find(1), m.parent
846 assert_equal Message.find(1), m.parent
836 end
847 end
837
848
838 def test_should_convert_tags_of_html_only_emails
849 def test_should_convert_tags_of_html_only_emails
839 with_settings :text_formatting => 'textile' do
850 with_settings :text_formatting => 'textile' do
840 issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'})
851 issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'})
841 assert issue.is_a?(Issue)
852 assert issue.is_a?(Issue)
842 assert !issue.new_record?
853 assert !issue.new_record?
843 issue.reload
854 issue.reload
844 assert_equal 'HTML email', issue.subject
855 assert_equal 'HTML email', issue.subject
845 assert_equal "This is a *html-only* email.\r\n\r\nh1. With a title\r\n\r\nand a paragraph.", issue.description
856 assert_equal "This is a *html-only* email.\r\n\r\nh1. With a title\r\n\r\nand a paragraph.", issue.description
846 end
857 end
847 end
858 end
848
859
849 def test_should_handle_outlook_web_access_2010_html_only
860 def test_should_handle_outlook_web_access_2010_html_only
850 issue = submit_email('outlook_web_access_2010_html_only.eml', :issue => {:project => 'ecookbook'})
861 issue = submit_email('outlook_web_access_2010_html_only.eml', :issue => {:project => 'ecookbook'})
851 assert issue.is_a?(Issue)
862 assert issue.is_a?(Issue)
852 issue.reload
863 issue.reload
853 assert_equal 'Upgrade Redmine to 3.0.x', issue.subject
864 assert_equal 'Upgrade Redmine to 3.0.x', issue.subject
854 assert_equal "A mess.\r\n\r\n--Geoff Maciolek\r\nMYCOMPANYNAME, LLC", issue.description
865 assert_equal "A mess.\r\n\r\n--Geoff Maciolek\r\nMYCOMPANYNAME, LLC", issue.description
855 end
866 end
856
867
857 def test_should_handle_outlook_2010_html_only
868 def test_should_handle_outlook_2010_html_only
858 issue = submit_email('outlook_2010_html_only.eml', :issue => {:project => 'ecookbook'})
869 issue = submit_email('outlook_2010_html_only.eml', :issue => {:project => 'ecookbook'})
859 assert issue.is_a?(Issue)
870 assert issue.is_a?(Issue)
860 issue.reload
871 issue.reload
861 assert_equal 'Test email', issue.subject
872 assert_equal 'Test email', issue.subject
862 assert_equal "Simple, unadorned test email generated by Outlook 2010. It is in HTML format, but" +
873 assert_equal "Simple, unadorned test email generated by Outlook 2010. It is in HTML format, but" +
863 " no special formatting has been chosen. I’m going to save this as a draft and then manually" +
874 " no special formatting has been chosen. I’m going to save this as a draft and then manually" +
864 " drop it into the Inbox for scraping by Redmine 3.0.2.", issue.description
875 " drop it into the Inbox for scraping by Redmine 3.0.2.", issue.description
865 end
876 end
866
877
867 test "truncate emails with no setting should add the entire email into the issue" do
878 test "truncate emails with no setting should add the entire email into the issue" do
868 with_settings :mail_handler_body_delimiters => '' do
879 with_settings :mail_handler_body_delimiters => '' do
869 issue = submit_email('ticket_on_given_project.eml')
880 issue = submit_email('ticket_on_given_project.eml')
870 assert_issue_created(issue)
881 assert_issue_created(issue)
871 assert issue.description.include?('---')
882 assert issue.description.include?('---')
872 assert issue.description.include?('This paragraph is after the delimiter')
883 assert issue.description.include?('This paragraph is after the delimiter')
873 end
884 end
874 end
885 end
875
886
876 test "truncate emails with a single string should truncate the email at the delimiter for the issue" do
887 test "truncate emails with a single string should truncate the email at the delimiter for the issue" do
877 with_settings :mail_handler_body_delimiters => '---' do
888 with_settings :mail_handler_body_delimiters => '---' do
878 issue = submit_email('ticket_on_given_project.eml')
889 issue = submit_email('ticket_on_given_project.eml')
879 assert_issue_created(issue)
890 assert_issue_created(issue)
880 assert issue.description.include?('This paragraph is before delimiters')
891 assert issue.description.include?('This paragraph is before delimiters')
881 assert issue.description.include?('--- This line starts with a delimiter')
892 assert issue.description.include?('--- This line starts with a delimiter')
882 assert !issue.description.match(/^---$/)
893 assert !issue.description.match(/^---$/)
883 assert !issue.description.include?('This paragraph is after the delimiter')
894 assert !issue.description.include?('This paragraph is after the delimiter')
884 end
895 end
885 end
896 end
886
897
887 test "truncate emails with a single quoted reply should truncate the email at the delimiter with the quoted reply symbols (>)" do
898 test "truncate emails with a single quoted reply should truncate the email at the delimiter with the quoted reply symbols (>)" do
888 with_settings :mail_handler_body_delimiters => '--- Reply above. Do not remove this line. ---' do
899 with_settings :mail_handler_body_delimiters => '--- Reply above. Do not remove this line. ---' do
889 journal = submit_email('issue_update_with_quoted_reply_above.eml')
900 journal = submit_email('issue_update_with_quoted_reply_above.eml')
890 assert journal.is_a?(Journal)
901 assert journal.is_a?(Journal)
891 assert journal.notes.include?('An update to the issue by the sender.')
902 assert journal.notes.include?('An update to the issue by the sender.')
892 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
903 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
893 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
904 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
894 end
905 end
895 end
906 end
896
907
897 test "truncate emails with multiple quoted replies should truncate the email at the delimiter with the quoted reply symbols (>)" do
908 test "truncate emails with multiple quoted replies should truncate the email at the delimiter with the quoted reply symbols (>)" do
898 with_settings :mail_handler_body_delimiters => '--- Reply above. Do not remove this line. ---' do
909 with_settings :mail_handler_body_delimiters => '--- Reply above. Do not remove this line. ---' do
899 journal = submit_email('issue_update_with_multiple_quoted_reply_above.eml')
910 journal = submit_email('issue_update_with_multiple_quoted_reply_above.eml')
900 assert journal.is_a?(Journal)
911 assert journal.is_a?(Journal)
901 assert journal.notes.include?('An update to the issue by the sender.')
912 assert journal.notes.include?('An update to the issue by the sender.')
902 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
913 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
903 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
914 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
904 end
915 end
905 end
916 end
906
917
907 test "truncate emails with multiple strings should truncate the email at the first delimiter found (BREAK)" do
918 test "truncate emails with multiple strings should truncate the email at the first delimiter found (BREAK)" do
908 with_settings :mail_handler_body_delimiters => "---\nBREAK" do
919 with_settings :mail_handler_body_delimiters => "---\nBREAK" do
909 issue = submit_email('ticket_on_given_project.eml')
920 issue = submit_email('ticket_on_given_project.eml')
910 assert_issue_created(issue)
921 assert_issue_created(issue)
911 assert issue.description.include?('This paragraph is before delimiters')
922 assert issue.description.include?('This paragraph is before delimiters')
912 assert !issue.description.include?('BREAK')
923 assert !issue.description.include?('BREAK')
913 assert !issue.description.include?('This paragraph is between delimiters')
924 assert !issue.description.include?('This paragraph is between delimiters')
914 assert !issue.description.match(/^---$/)
925 assert !issue.description.match(/^---$/)
915 assert !issue.description.include?('This paragraph is after the delimiter')
926 assert !issue.description.include?('This paragraph is after the delimiter')
916 end
927 end
917 end
928 end
918
929
919 def test_attachments_that_match_mail_handler_excluded_filenames_should_be_ignored
930 def test_attachments_that_match_mail_handler_excluded_filenames_should_be_ignored
920 with_settings :mail_handler_excluded_filenames => '*.vcf, *.jpg' do
931 with_settings :mail_handler_excluded_filenames => '*.vcf, *.jpg' do
921 issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
932 issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
922 assert issue.is_a?(Issue)
933 assert issue.is_a?(Issue)
923 assert !issue.new_record?
934 assert !issue.new_record?
924 assert_equal 0, issue.reload.attachments.size
935 assert_equal 0, issue.reload.attachments.size
925 end
936 end
926 end
937 end
927
938
928 def test_attachments_that_do_not_match_mail_handler_excluded_filenames_should_be_attached
939 def test_attachments_that_do_not_match_mail_handler_excluded_filenames_should_be_attached
929 with_settings :mail_handler_excluded_filenames => '*.vcf, *.gif' do
940 with_settings :mail_handler_excluded_filenames => '*.vcf, *.gif' do
930 issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
941 issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
931 assert issue.is_a?(Issue)
942 assert issue.is_a?(Issue)
932 assert !issue.new_record?
943 assert !issue.new_record?
933 assert_equal 1, issue.reload.attachments.size
944 assert_equal 1, issue.reload.attachments.size
934 end
945 end
935 end
946 end
936
947
937 def test_email_with_long_subject_line
948 def test_email_with_long_subject_line
938 issue = submit_email('ticket_with_long_subject.eml')
949 issue = submit_email('ticket_with_long_subject.eml')
939 assert issue.is_a?(Issue)
950 assert issue.is_a?(Issue)
940 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]
951 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]
941 end
952 end
942
953
943 def test_first_keyword_should_be_matched
954 def test_first_keyword_should_be_matched
944 issue = submit_email('ticket_with_duplicate_keyword.eml', :allow_override => 'priority')
955 issue = submit_email('ticket_with_duplicate_keyword.eml', :allow_override => 'priority')
945 assert issue.is_a?(Issue)
956 assert issue.is_a?(Issue)
946 assert_equal 'High', issue.priority.name
957 assert_equal 'High', issue.priority.name
947 end
958 end
948
959
949 def test_keyword_after_delimiter_should_be_ignored
960 def test_keyword_after_delimiter_should_be_ignored
950 with_settings :mail_handler_body_delimiters => "== DELIMITER ==" do
961 with_settings :mail_handler_body_delimiters => "== DELIMITER ==" do
951 issue = submit_email('ticket_with_keyword_after_delimiter.eml', :allow_override => 'priority')
962 issue = submit_email('ticket_with_keyword_after_delimiter.eml', :allow_override => 'priority')
952 assert issue.is_a?(Issue)
963 assert issue.is_a?(Issue)
953 assert_equal 'Normal', issue.priority.name
964 assert_equal 'Normal', issue.priority.name
954 end
965 end
955 end
966 end
956
967
957 def test_new_user_from_attributes_should_return_valid_user
968 def test_new_user_from_attributes_should_return_valid_user
958 to_test = {
969 to_test = {
959 # [address, name] => [login, firstname, lastname]
970 # [address, name] => [login, firstname, lastname]
960 ['jsmith@example.net', nil] => ['jsmith@example.net', 'jsmith', '-'],
971 ['jsmith@example.net', nil] => ['jsmith@example.net', 'jsmith', '-'],
961 ['jsmith@example.net', 'John'] => ['jsmith@example.net', 'John', '-'],
972 ['jsmith@example.net', 'John'] => ['jsmith@example.net', 'John', '-'],
962 ['jsmith@example.net', 'John Smith'] => ['jsmith@example.net', 'John', 'Smith'],
973 ['jsmith@example.net', 'John Smith'] => ['jsmith@example.net', 'John', 'Smith'],
963 ['jsmith@example.net', 'John Paul Smith'] => ['jsmith@example.net', 'John', 'Paul Smith'],
974 ['jsmith@example.net', 'John Paul Smith'] => ['jsmith@example.net', 'John', 'Paul Smith'],
964 ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsTheMaximumLength Smith'] => ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsT', 'Smith'],
975 ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsTheMaximumLength Smith'] => ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsT', 'Smith'],
965 ['jsmith@example.net', 'John AVeryLongLastnameThatExceedsTheMaximumLength'] => ['jsmith@example.net', 'John', 'AVeryLongLastnameThatExceedsTh']
976 ['jsmith@example.net', 'John AVeryLongLastnameThatExceedsTheMaximumLength'] => ['jsmith@example.net', 'John', 'AVeryLongLastnameThatExceedsTh']
966 }
977 }
967
978
968 to_test.each do |attrs, expected|
979 to_test.each do |attrs, expected|
969 user = MailHandler.new_user_from_attributes(attrs.first, attrs.last)
980 user = MailHandler.new_user_from_attributes(attrs.first, attrs.last)
970
981
971 assert user.valid?, user.errors.full_messages.to_s
982 assert user.valid?, user.errors.full_messages.to_s
972 assert_equal attrs.first, user.mail
983 assert_equal attrs.first, user.mail
973 assert_equal expected[0], user.login
984 assert_equal expected[0], user.login
974 assert_equal expected[1], user.firstname
985 assert_equal expected[1], user.firstname
975 assert_equal expected[2], user.lastname
986 assert_equal expected[2], user.lastname
976 assert_equal 'only_my_events', user.mail_notification
987 assert_equal 'only_my_events', user.mail_notification
977 end
988 end
978 end
989 end
979
990
980 def test_new_user_from_attributes_should_use_default_login_if_invalid
991 def test_new_user_from_attributes_should_use_default_login_if_invalid
981 user = MailHandler.new_user_from_attributes('foo+bar@example.net')
992 user = MailHandler.new_user_from_attributes('foo+bar@example.net')
982 assert user.valid?
993 assert user.valid?
983 assert user.login =~ /^user[a-f0-9]+$/
994 assert user.login =~ /^user[a-f0-9]+$/
984 assert_equal 'foo+bar@example.net', user.mail
995 assert_equal 'foo+bar@example.net', user.mail
985 end
996 end
986
997
987 def test_new_user_with_utf8_encoded_fullname_should_be_decoded
998 def test_new_user_with_utf8_encoded_fullname_should_be_decoded
988 assert_difference 'User.count' do
999 assert_difference 'User.count' do
989 issue = submit_email(
1000 issue = submit_email(
990 'fullname_of_sender_as_utf8_encoded.eml',
1001 'fullname_of_sender_as_utf8_encoded.eml',
991 :issue => {:project => 'ecookbook'},
1002 :issue => {:project => 'ecookbook'},
992 :unknown_user => 'create'
1003 :unknown_user => 'create'
993 )
1004 )
994 end
1005 end
995 user = User.order('id DESC').first
1006 user = User.order('id DESC').first
996 assert_equal "foo@example.org", user.mail
1007 assert_equal "foo@example.org", user.mail
997 str1 = "\xc3\x84\xc3\xa4".force_encoding('UTF-8')
1008 str1 = "\xc3\x84\xc3\xa4".force_encoding('UTF-8')
998 str2 = "\xc3\x96\xc3\xb6".force_encoding('UTF-8')
1009 str2 = "\xc3\x96\xc3\xb6".force_encoding('UTF-8')
999 assert_equal str1, user.firstname
1010 assert_equal str1, user.firstname
1000 assert_equal str2, user.lastname
1011 assert_equal str2, user.lastname
1001 end
1012 end
1002
1013
1003 def test_extract_options_from_env_should_return_options
1014 def test_extract_options_from_env_should_return_options
1004 options = MailHandler.extract_options_from_env({
1015 options = MailHandler.extract_options_from_env({
1005 'tracker' => 'defect',
1016 'tracker' => 'defect',
1006 'project' => 'foo',
1017 'project' => 'foo',
1007 'unknown_user' => 'create'
1018 'unknown_user' => 'create'
1008 })
1019 })
1009
1020
1010 assert_equal({
1021 assert_equal({
1011 :issue => {:tracker => 'defect', :project => 'foo'},
1022 :issue => {:tracker => 'defect', :project => 'foo'},
1012 :unknown_user => 'create'
1023 :unknown_user => 'create'
1013 }, options)
1024 }, options)
1014 end
1025 end
1015
1026
1016 def test_safe_receive_should_rescue_exceptions_and_return_false
1027 def test_safe_receive_should_rescue_exceptions_and_return_false
1017 MailHandler.stubs(:receive).raises(Exception.new "Something went wrong")
1028 MailHandler.stubs(:receive).raises(Exception.new "Something went wrong")
1018
1029
1019 assert_equal false, MailHandler.safe_receive
1030 assert_equal false, MailHandler.safe_receive
1020 end
1031 end
1021
1032
1022 private
1033 private
1023
1034
1024 def submit_email(filename, options={})
1035 def submit_email(filename, options={})
1025 raw = IO.read(File.join(FIXTURES_PATH, filename))
1036 raw = IO.read(File.join(FIXTURES_PATH, filename))
1026 yield raw if block_given?
1037 yield raw if block_given?
1027 MailHandler.receive(raw, options)
1038 MailHandler.receive(raw, options)
1028 end
1039 end
1029
1040
1030 def assert_issue_created(issue)
1041 def assert_issue_created(issue)
1031 assert issue.is_a?(Issue)
1042 assert issue.is_a?(Issue)
1032 assert !issue.new_record?
1043 assert !issue.new_record?
1033 issue.reload
1044 issue.reload
1034 end
1045 end
1035 end
1046 end
General Comments 0
You need to be logged in to leave comments. Login now