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