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