##// END OF EJS Templates
Merged r11525 from trunk (#11498)....
Jean-Philippe Lang -
r11351:974863e8f419
parent child
Show More
@@ -1,512 +1,515
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class MailHandler < ActionMailer::Base
18 class MailHandler < ActionMailer::Base
19 include ActionView::Helpers::SanitizeHelper
19 include ActionView::Helpers::SanitizeHelper
20 include Redmine::I18n
20 include Redmine::I18n
21
21
22 class UnauthorizedAction < StandardError; end
22 class UnauthorizedAction < StandardError; end
23 class MissingInformation < StandardError; end
23 class MissingInformation < StandardError; end
24
24
25 attr_reader :email, :user
25 attr_reader :email, :user
26
26
27 def self.receive(email, options={})
27 def self.receive(email, options={})
28 @@handler_options = options.dup
28 @@handler_options = options.dup
29
29
30 @@handler_options[:issue] ||= {}
30 @@handler_options[:issue] ||= {}
31
31
32 if @@handler_options[:allow_override].is_a?(String)
32 if @@handler_options[:allow_override].is_a?(String)
33 @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip)
33 @@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip)
34 end
34 end
35 @@handler_options[:allow_override] ||= []
35 @@handler_options[:allow_override] ||= []
36 # Project needs to be overridable if not specified
36 # Project needs to be overridable if not specified
37 @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
37 @@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
38 # Status overridable by default
38 # Status overridable by default
39 @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
39 @@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
40
40
41 @@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1' ? true : false)
41 @@handler_options[:no_account_notice] = (@@handler_options[:no_account_notice].to_s == '1')
42 @@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1')
42
43
43 email.force_encoding('ASCII-8BIT') if email.respond_to?(:force_encoding)
44 email.force_encoding('ASCII-8BIT') if email.respond_to?(:force_encoding)
44 super(email)
45 super(email)
45 end
46 end
46
47
47 def logger
48 def logger
48 Rails.logger
49 Rails.logger
49 end
50 end
50
51
51 cattr_accessor :ignored_emails_headers
52 cattr_accessor :ignored_emails_headers
52 @@ignored_emails_headers = {
53 @@ignored_emails_headers = {
53 'X-Auto-Response-Suppress' => 'oof',
54 'X-Auto-Response-Suppress' => 'oof',
54 'Auto-Submitted' => /^auto-/
55 'Auto-Submitted' => /^auto-/
55 }
56 }
56
57
57 # Processes incoming emails
58 # Processes incoming emails
58 # Returns the created object (eg. an issue, a message) or false
59 # Returns the created object (eg. an issue, a message) or false
59 def receive(email)
60 def receive(email)
60 @email = email
61 @email = email
61 sender_email = email.from.to_a.first.to_s.strip
62 sender_email = email.from.to_a.first.to_s.strip
62 # Ignore emails received from the application emission address to avoid hell cycles
63 # Ignore emails received from the application emission address to avoid hell cycles
63 if sender_email.downcase == Setting.mail_from.to_s.strip.downcase
64 if sender_email.downcase == Setting.mail_from.to_s.strip.downcase
64 if logger && logger.info
65 if logger && logger.info
65 logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]"
66 logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]"
66 end
67 end
67 return false
68 return false
68 end
69 end
69 # Ignore auto generated emails
70 # Ignore auto generated emails
70 self.class.ignored_emails_headers.each do |key, ignored_value|
71 self.class.ignored_emails_headers.each do |key, ignored_value|
71 value = email.header[key]
72 value = email.header[key]
72 if value
73 if value
73 value = value.to_s.downcase
74 value = value.to_s.downcase
74 if (ignored_value.is_a?(Regexp) && value.match(ignored_value)) || value == ignored_value
75 if (ignored_value.is_a?(Regexp) && value.match(ignored_value)) || value == ignored_value
75 if logger && logger.info
76 if logger && logger.info
76 logger.info "MailHandler: ignoring email with #{key}:#{value} header"
77 logger.info "MailHandler: ignoring email with #{key}:#{value} header"
77 end
78 end
78 return false
79 return false
79 end
80 end
80 end
81 end
81 end
82 end
82 @user = User.find_by_mail(sender_email) if sender_email.present?
83 @user = User.find_by_mail(sender_email) if sender_email.present?
83 if @user && !@user.active?
84 if @user && !@user.active?
84 if logger && logger.info
85 if logger && logger.info
85 logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]"
86 logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]"
86 end
87 end
87 return false
88 return false
88 end
89 end
89 if @user.nil?
90 if @user.nil?
90 # Email was submitted by an unknown user
91 # Email was submitted by an unknown user
91 case @@handler_options[:unknown_user]
92 case @@handler_options[:unknown_user]
92 when 'accept'
93 when 'accept'
93 @user = User.anonymous
94 @user = User.anonymous
94 when 'create'
95 when 'create'
95 @user = create_user_from_email
96 @user = create_user_from_email
96 if @user
97 if @user
97 if logger && logger.info
98 if logger && logger.info
98 logger.info "MailHandler: [#{@user.login}] account created"
99 logger.info "MailHandler: [#{@user.login}] account created"
99 end
100 end
100 add_user_to_group(@@handler_options[:default_group])
101 add_user_to_group(@@handler_options[:default_group])
101 Mailer.account_information(@user, @user.password).deliver
102 unless @@handler_options[:no_account_notice]
103 Mailer.account_information(@user, @user.password).deliver
104 end
102 else
105 else
103 if logger && logger.error
106 if logger && logger.error
104 logger.error "MailHandler: could not create account for [#{sender_email}]"
107 logger.error "MailHandler: could not create account for [#{sender_email}]"
105 end
108 end
106 return false
109 return false
107 end
110 end
108 else
111 else
109 # Default behaviour, emails from unknown users are ignored
112 # Default behaviour, emails from unknown users are ignored
110 if logger && logger.info
113 if logger && logger.info
111 logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]"
114 logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]"
112 end
115 end
113 return false
116 return false
114 end
117 end
115 end
118 end
116 User.current = @user
119 User.current = @user
117 dispatch
120 dispatch
118 end
121 end
119
122
120 private
123 private
121
124
122 MESSAGE_ID_RE = %r{^<?redmine\.([a-z0-9_]+)\-(\d+)\.\d+@}
125 MESSAGE_ID_RE = %r{^<?redmine\.([a-z0-9_]+)\-(\d+)\.\d+@}
123 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]*#(\d+)\]}
126 ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]*#(\d+)\]}
124 MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
127 MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
125
128
126 def dispatch
129 def dispatch
127 headers = [email.in_reply_to, email.references].flatten.compact
130 headers = [email.in_reply_to, email.references].flatten.compact
128 subject = email.subject.to_s
131 subject = email.subject.to_s
129 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
132 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
130 klass, object_id = $1, $2.to_i
133 klass, object_id = $1, $2.to_i
131 method_name = "receive_#{klass}_reply"
134 method_name = "receive_#{klass}_reply"
132 if self.class.private_instance_methods.collect(&:to_s).include?(method_name)
135 if self.class.private_instance_methods.collect(&:to_s).include?(method_name)
133 send method_name, object_id
136 send method_name, object_id
134 else
137 else
135 # ignoring it
138 # ignoring it
136 end
139 end
137 elsif m = subject.match(ISSUE_REPLY_SUBJECT_RE)
140 elsif m = subject.match(ISSUE_REPLY_SUBJECT_RE)
138 receive_issue_reply(m[1].to_i)
141 receive_issue_reply(m[1].to_i)
139 elsif m = subject.match(MESSAGE_REPLY_SUBJECT_RE)
142 elsif m = subject.match(MESSAGE_REPLY_SUBJECT_RE)
140 receive_message_reply(m[1].to_i)
143 receive_message_reply(m[1].to_i)
141 else
144 else
142 dispatch_to_default
145 dispatch_to_default
143 end
146 end
144 rescue ActiveRecord::RecordInvalid => e
147 rescue ActiveRecord::RecordInvalid => e
145 # TODO: send a email to the user
148 # TODO: send a email to the user
146 logger.error e.message if logger
149 logger.error e.message if logger
147 false
150 false
148 rescue MissingInformation => e
151 rescue MissingInformation => e
149 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
152 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
150 false
153 false
151 rescue UnauthorizedAction => e
154 rescue UnauthorizedAction => e
152 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
155 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
153 false
156 false
154 end
157 end
155
158
156 def dispatch_to_default
159 def dispatch_to_default
157 receive_issue
160 receive_issue
158 end
161 end
159
162
160 # Creates a new issue
163 # Creates a new issue
161 def receive_issue
164 def receive_issue
162 project = target_project
165 project = target_project
163 # check permission
166 # check permission
164 unless @@handler_options[:no_permission_check]
167 unless @@handler_options[:no_permission_check]
165 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
168 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
166 end
169 end
167
170
168 issue = Issue.new(:author => user, :project => project)
171 issue = Issue.new(:author => user, :project => project)
169 issue.safe_attributes = issue_attributes_from_keywords(issue)
172 issue.safe_attributes = issue_attributes_from_keywords(issue)
170 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
173 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
171 issue.subject = cleaned_up_subject
174 issue.subject = cleaned_up_subject
172 if issue.subject.blank?
175 if issue.subject.blank?
173 issue.subject = '(no subject)'
176 issue.subject = '(no subject)'
174 end
177 end
175 issue.description = cleaned_up_text_body
178 issue.description = cleaned_up_text_body
176
179
177 # add To and Cc as watchers before saving so the watchers can reply to Redmine
180 # add To and Cc as watchers before saving so the watchers can reply to Redmine
178 add_watchers(issue)
181 add_watchers(issue)
179 issue.save!
182 issue.save!
180 add_attachments(issue)
183 add_attachments(issue)
181 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
184 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
182 issue
185 issue
183 end
186 end
184
187
185 # Adds a note to an existing issue
188 # Adds a note to an existing issue
186 def receive_issue_reply(issue_id, from_journal=nil)
189 def receive_issue_reply(issue_id, from_journal=nil)
187 issue = Issue.find_by_id(issue_id)
190 issue = Issue.find_by_id(issue_id)
188 return unless issue
191 return unless issue
189 # check permission
192 # check permission
190 unless @@handler_options[:no_permission_check]
193 unless @@handler_options[:no_permission_check]
191 unless user.allowed_to?(:add_issue_notes, issue.project) ||
194 unless user.allowed_to?(:add_issue_notes, issue.project) ||
192 user.allowed_to?(:edit_issues, issue.project)
195 user.allowed_to?(:edit_issues, issue.project)
193 raise UnauthorizedAction
196 raise UnauthorizedAction
194 end
197 end
195 end
198 end
196
199
197 # ignore CLI-supplied defaults for new issues
200 # ignore CLI-supplied defaults for new issues
198 @@handler_options[:issue].clear
201 @@handler_options[:issue].clear
199
202
200 journal = issue.init_journal(user)
203 journal = issue.init_journal(user)
201 if from_journal && from_journal.private_notes?
204 if from_journal && from_journal.private_notes?
202 # If the received email was a reply to a private note, make the added note private
205 # If the received email was a reply to a private note, make the added note private
203 issue.private_notes = true
206 issue.private_notes = true
204 end
207 end
205 issue.safe_attributes = issue_attributes_from_keywords(issue)
208 issue.safe_attributes = issue_attributes_from_keywords(issue)
206 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
209 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
207 journal.notes = cleaned_up_text_body
210 journal.notes = cleaned_up_text_body
208 add_attachments(issue)
211 add_attachments(issue)
209 issue.save!
212 issue.save!
210 if logger && logger.info
213 if logger && logger.info
211 logger.info "MailHandler: issue ##{issue.id} updated by #{user}"
214 logger.info "MailHandler: issue ##{issue.id} updated by #{user}"
212 end
215 end
213 journal
216 journal
214 end
217 end
215
218
216 # Reply will be added to the issue
219 # Reply will be added to the issue
217 def receive_journal_reply(journal_id)
220 def receive_journal_reply(journal_id)
218 journal = Journal.find_by_id(journal_id)
221 journal = Journal.find_by_id(journal_id)
219 if journal && journal.journalized_type == 'Issue'
222 if journal && journal.journalized_type == 'Issue'
220 receive_issue_reply(journal.journalized_id, journal)
223 receive_issue_reply(journal.journalized_id, journal)
221 end
224 end
222 end
225 end
223
226
224 # Receives a reply to a forum message
227 # Receives a reply to a forum message
225 def receive_message_reply(message_id)
228 def receive_message_reply(message_id)
226 message = Message.find_by_id(message_id)
229 message = Message.find_by_id(message_id)
227 if message
230 if message
228 message = message.root
231 message = message.root
229
232
230 unless @@handler_options[:no_permission_check]
233 unless @@handler_options[:no_permission_check]
231 raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
234 raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
232 end
235 end
233
236
234 if !message.locked?
237 if !message.locked?
235 reply = Message.new(:subject => cleaned_up_subject.gsub(%r{^.*msg\d+\]}, '').strip,
238 reply = Message.new(:subject => cleaned_up_subject.gsub(%r{^.*msg\d+\]}, '').strip,
236 :content => cleaned_up_text_body)
239 :content => cleaned_up_text_body)
237 reply.author = user
240 reply.author = user
238 reply.board = message.board
241 reply.board = message.board
239 message.children << reply
242 message.children << reply
240 add_attachments(reply)
243 add_attachments(reply)
241 reply
244 reply
242 else
245 else
243 if logger && logger.info
246 if logger && logger.info
244 logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic"
247 logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic"
245 end
248 end
246 end
249 end
247 end
250 end
248 end
251 end
249
252
250 def add_attachments(obj)
253 def add_attachments(obj)
251 if email.attachments && email.attachments.any?
254 if email.attachments && email.attachments.any?
252 email.attachments.each do |attachment|
255 email.attachments.each do |attachment|
253 filename = attachment.filename
256 filename = attachment.filename
254 unless filename.respond_to?(:encoding)
257 unless filename.respond_to?(:encoding)
255 # try to reencode to utf8 manually with ruby1.8
258 # try to reencode to utf8 manually with ruby1.8
256 h = attachment.header['Content-Disposition']
259 h = attachment.header['Content-Disposition']
257 unless h.nil?
260 unless h.nil?
258 begin
261 begin
259 if m = h.value.match(/filename\*[0-9\*]*=([^=']+)'/)
262 if m = h.value.match(/filename\*[0-9\*]*=([^=']+)'/)
260 filename = Redmine::CodesetUtil.to_utf8(filename, m[1])
263 filename = Redmine::CodesetUtil.to_utf8(filename, m[1])
261 elsif m = h.value.match(/filename=.*=\?([^\?]+)\?[BbQq]\?/)
264 elsif m = h.value.match(/filename=.*=\?([^\?]+)\?[BbQq]\?/)
262 # http://tools.ietf.org/html/rfc2047#section-4
265 # http://tools.ietf.org/html/rfc2047#section-4
263 filename = Redmine::CodesetUtil.to_utf8(filename, m[1])
266 filename = Redmine::CodesetUtil.to_utf8(filename, m[1])
264 end
267 end
265 rescue
268 rescue
266 # nop
269 # nop
267 end
270 end
268 end
271 end
269 end
272 end
270 obj.attachments << Attachment.create(:container => obj,
273 obj.attachments << Attachment.create(:container => obj,
271 :file => attachment.decoded,
274 :file => attachment.decoded,
272 :filename => filename,
275 :filename => filename,
273 :author => user,
276 :author => user,
274 :content_type => attachment.mime_type)
277 :content_type => attachment.mime_type)
275 end
278 end
276 end
279 end
277 end
280 end
278
281
279 # Adds To and Cc as watchers of the given object if the sender has the
282 # Adds To and Cc as watchers of the given object if the sender has the
280 # appropriate permission
283 # appropriate permission
281 def add_watchers(obj)
284 def add_watchers(obj)
282 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
285 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
283 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
286 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
284 unless addresses.empty?
287 unless addresses.empty?
285 watchers = User.active.where('LOWER(mail) IN (?)', addresses).all
288 watchers = User.active.where('LOWER(mail) IN (?)', addresses).all
286 watchers.each {|w| obj.add_watcher(w)}
289 watchers.each {|w| obj.add_watcher(w)}
287 end
290 end
288 end
291 end
289 end
292 end
290
293
291 def get_keyword(attr, options={})
294 def get_keyword(attr, options={})
292 @keywords ||= {}
295 @keywords ||= {}
293 if @keywords.has_key?(attr)
296 if @keywords.has_key?(attr)
294 @keywords[attr]
297 @keywords[attr]
295 else
298 else
296 @keywords[attr] = begin
299 @keywords[attr] = begin
297 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) &&
300 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) &&
298 (v = extract_keyword!(plain_text_body, attr, options[:format]))
301 (v = extract_keyword!(plain_text_body, attr, options[:format]))
299 v
302 v
300 elsif !@@handler_options[:issue][attr].blank?
303 elsif !@@handler_options[:issue][attr].blank?
301 @@handler_options[:issue][attr]
304 @@handler_options[:issue][attr]
302 end
305 end
303 end
306 end
304 end
307 end
305 end
308 end
306
309
307 # Destructively extracts the value for +attr+ in +text+
310 # Destructively extracts the value for +attr+ in +text+
308 # Returns nil if no matching keyword found
311 # Returns nil if no matching keyword found
309 def extract_keyword!(text, attr, format=nil)
312 def extract_keyword!(text, attr, format=nil)
310 keys = [attr.to_s.humanize]
313 keys = [attr.to_s.humanize]
311 if attr.is_a?(Symbol)
314 if attr.is_a?(Symbol)
312 if user && user.language.present?
315 if user && user.language.present?
313 keys << l("field_#{attr}", :default => '', :locale => user.language)
316 keys << l("field_#{attr}", :default => '', :locale => user.language)
314 end
317 end
315 if Setting.default_language.present?
318 if Setting.default_language.present?
316 keys << l("field_#{attr}", :default => '', :locale => Setting.default_language)
319 keys << l("field_#{attr}", :default => '', :locale => Setting.default_language)
317 end
320 end
318 end
321 end
319 keys.reject! {|k| k.blank?}
322 keys.reject! {|k| k.blank?}
320 keys.collect! {|k| Regexp.escape(k)}
323 keys.collect! {|k| Regexp.escape(k)}
321 format ||= '.+'
324 format ||= '.+'
322 keyword = nil
325 keyword = nil
323 regexp = /^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i
326 regexp = /^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i
324 if m = text.match(regexp)
327 if m = text.match(regexp)
325 keyword = m[2].strip
328 keyword = m[2].strip
326 text.gsub!(regexp, '')
329 text.gsub!(regexp, '')
327 end
330 end
328 keyword
331 keyword
329 end
332 end
330
333
331 def target_project
334 def target_project
332 # TODO: other ways to specify project:
335 # TODO: other ways to specify project:
333 # * parse the email To field
336 # * parse the email To field
334 # * specific project (eg. Setting.mail_handler_target_project)
337 # * specific project (eg. Setting.mail_handler_target_project)
335 target = Project.find_by_identifier(get_keyword(:project))
338 target = Project.find_by_identifier(get_keyword(:project))
336 raise MissingInformation.new('Unable to determine target project') if target.nil?
339 raise MissingInformation.new('Unable to determine target project') if target.nil?
337 target
340 target
338 end
341 end
339
342
340 # Returns a Hash of issue attributes extracted from keywords in the email body
343 # Returns a Hash of issue attributes extracted from keywords in the email body
341 def issue_attributes_from_keywords(issue)
344 def issue_attributes_from_keywords(issue)
342 assigned_to = (k = get_keyword(:assigned_to, :override => true)) && find_assignee_from_keyword(k, issue)
345 assigned_to = (k = get_keyword(:assigned_to, :override => true)) && find_assignee_from_keyword(k, issue)
343
346
344 attrs = {
347 attrs = {
345 'tracker_id' => (k = get_keyword(:tracker)) && issue.project.trackers.named(k).first.try(:id),
348 'tracker_id' => (k = get_keyword(:tracker)) && issue.project.trackers.named(k).first.try(:id),
346 'status_id' => (k = get_keyword(:status)) && IssueStatus.named(k).first.try(:id),
349 'status_id' => (k = get_keyword(:status)) && IssueStatus.named(k).first.try(:id),
347 'priority_id' => (k = get_keyword(:priority)) && IssuePriority.named(k).first.try(:id),
350 'priority_id' => (k = get_keyword(:priority)) && IssuePriority.named(k).first.try(:id),
348 'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.named(k).first.try(:id),
351 'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.named(k).first.try(:id),
349 'assigned_to_id' => assigned_to.try(:id),
352 'assigned_to_id' => assigned_to.try(:id),
350 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) &&
353 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) &&
351 issue.project.shared_versions.named(k).first.try(:id),
354 issue.project.shared_versions.named(k).first.try(:id),
352 'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
355 'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
353 'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
356 'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
354 'estimated_hours' => get_keyword(:estimated_hours, :override => true),
357 'estimated_hours' => get_keyword(:estimated_hours, :override => true),
355 'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0')
358 'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0')
356 }.delete_if {|k, v| v.blank? }
359 }.delete_if {|k, v| v.blank? }
357
360
358 if issue.new_record? && attrs['tracker_id'].nil?
361 if issue.new_record? && attrs['tracker_id'].nil?
359 attrs['tracker_id'] = issue.project.trackers.first.try(:id)
362 attrs['tracker_id'] = issue.project.trackers.first.try(:id)
360 end
363 end
361
364
362 attrs
365 attrs
363 end
366 end
364
367
365 # Returns a Hash of issue custom field values extracted from keywords in the email body
368 # Returns a Hash of issue custom field values extracted from keywords in the email body
366 def custom_field_values_from_keywords(customized)
369 def custom_field_values_from_keywords(customized)
367 customized.custom_field_values.inject({}) do |h, v|
370 customized.custom_field_values.inject({}) do |h, v|
368 if keyword = get_keyword(v.custom_field.name, :override => true)
371 if keyword = get_keyword(v.custom_field.name, :override => true)
369 h[v.custom_field.id.to_s] = v.custom_field.value_from_keyword(keyword, customized)
372 h[v.custom_field.id.to_s] = v.custom_field.value_from_keyword(keyword, customized)
370 end
373 end
371 h
374 h
372 end
375 end
373 end
376 end
374
377
375 # Returns the text/plain part of the email
378 # Returns the text/plain part of the email
376 # If not found (eg. HTML-only email), returns the body with tags removed
379 # If not found (eg. HTML-only email), returns the body with tags removed
377 def plain_text_body
380 def plain_text_body
378 return @plain_text_body unless @plain_text_body.nil?
381 return @plain_text_body unless @plain_text_body.nil?
379
382
380 part = email.text_part || email.html_part || email
383 part = email.text_part || email.html_part || email
381 @plain_text_body = Redmine::CodesetUtil.to_utf8(part.body.decoded, part.charset)
384 @plain_text_body = Redmine::CodesetUtil.to_utf8(part.body.decoded, part.charset)
382
385
383 # strip html tags and remove doctype directive
386 # strip html tags and remove doctype directive
384 @plain_text_body = strip_tags(@plain_text_body.strip)
387 @plain_text_body = strip_tags(@plain_text_body.strip)
385 @plain_text_body.sub! %r{^<!DOCTYPE .*$}, ''
388 @plain_text_body.sub! %r{^<!DOCTYPE .*$}, ''
386 @plain_text_body
389 @plain_text_body
387 end
390 end
388
391
389 def cleaned_up_text_body
392 def cleaned_up_text_body
390 cleanup_body(plain_text_body)
393 cleanup_body(plain_text_body)
391 end
394 end
392
395
393 def cleaned_up_subject
396 def cleaned_up_subject
394 subject = email.subject.to_s
397 subject = email.subject.to_s
395 unless subject.respond_to?(:encoding)
398 unless subject.respond_to?(:encoding)
396 # try to reencode to utf8 manually with ruby1.8
399 # try to reencode to utf8 manually with ruby1.8
397 begin
400 begin
398 if h = email.header[:subject]
401 if h = email.header[:subject]
399 # http://tools.ietf.org/html/rfc2047#section-4
402 # http://tools.ietf.org/html/rfc2047#section-4
400 if m = h.value.match(/=\?([^\?]+)\?[BbQq]\?/)
403 if m = h.value.match(/=\?([^\?]+)\?[BbQq]\?/)
401 subject = Redmine::CodesetUtil.to_utf8(subject, m[1])
404 subject = Redmine::CodesetUtil.to_utf8(subject, m[1])
402 end
405 end
403 end
406 end
404 rescue
407 rescue
405 # nop
408 # nop
406 end
409 end
407 end
410 end
408 subject.strip[0,255]
411 subject.strip[0,255]
409 end
412 end
410
413
411 def self.full_sanitizer
414 def self.full_sanitizer
412 @full_sanitizer ||= HTML::FullSanitizer.new
415 @full_sanitizer ||= HTML::FullSanitizer.new
413 end
416 end
414
417
415 def self.assign_string_attribute_with_limit(object, attribute, value, limit=nil)
418 def self.assign_string_attribute_with_limit(object, attribute, value, limit=nil)
416 limit ||= object.class.columns_hash[attribute.to_s].limit || 255
419 limit ||= object.class.columns_hash[attribute.to_s].limit || 255
417 value = value.to_s.slice(0, limit)
420 value = value.to_s.slice(0, limit)
418 object.send("#{attribute}=", value)
421 object.send("#{attribute}=", value)
419 end
422 end
420
423
421 # Returns a User from an email address and a full name
424 # Returns a User from an email address and a full name
422 def self.new_user_from_attributes(email_address, fullname=nil)
425 def self.new_user_from_attributes(email_address, fullname=nil)
423 user = User.new
426 user = User.new
424
427
425 # Truncating the email address would result in an invalid format
428 # Truncating the email address would result in an invalid format
426 user.mail = email_address
429 user.mail = email_address
427 assign_string_attribute_with_limit(user, 'login', email_address, User::LOGIN_LENGTH_LIMIT)
430 assign_string_attribute_with_limit(user, 'login', email_address, User::LOGIN_LENGTH_LIMIT)
428
431
429 names = fullname.blank? ? email_address.gsub(/@.*$/, '').split('.') : fullname.split
432 names = fullname.blank? ? email_address.gsub(/@.*$/, '').split('.') : fullname.split
430 assign_string_attribute_with_limit(user, 'firstname', names.shift, 30)
433 assign_string_attribute_with_limit(user, 'firstname', names.shift, 30)
431 assign_string_attribute_with_limit(user, 'lastname', names.join(' '), 30)
434 assign_string_attribute_with_limit(user, 'lastname', names.join(' '), 30)
432 user.lastname = '-' if user.lastname.blank?
435 user.lastname = '-' if user.lastname.blank?
433
436
434 password_length = [Setting.password_min_length.to_i, 10].max
437 password_length = [Setting.password_min_length.to_i, 10].max
435 user.password = Redmine::Utils.random_hex(password_length / 2 + 1)
438 user.password = Redmine::Utils.random_hex(password_length / 2 + 1)
436 user.language = Setting.default_language
439 user.language = Setting.default_language
437
440
438 unless user.valid?
441 unless user.valid?
439 user.login = "user#{Redmine::Utils.random_hex(6)}" unless user.errors[:login].blank?
442 user.login = "user#{Redmine::Utils.random_hex(6)}" unless user.errors[:login].blank?
440 user.firstname = "-" unless user.errors[:firstname].blank?
443 user.firstname = "-" unless user.errors[:firstname].blank?
441 (puts user.errors[:lastname];user.lastname = "-") unless user.errors[:lastname].blank?
444 (puts user.errors[:lastname];user.lastname = "-") unless user.errors[:lastname].blank?
442 end
445 end
443
446
444 user
447 user
445 end
448 end
446
449
447 # Creates a User for the +email+ sender
450 # Creates a User for the +email+ sender
448 # Returns the user or nil if it could not be created
451 # Returns the user or nil if it could not be created
449 def create_user_from_email
452 def create_user_from_email
450 from = email.header['from'].to_s
453 from = email.header['from'].to_s
451 addr, name = from, nil
454 addr, name = from, nil
452 if m = from.match(/^"?(.+?)"?\s+<(.+@.+)>$/)
455 if m = from.match(/^"?(.+?)"?\s+<(.+@.+)>$/)
453 addr, name = m[2], m[1]
456 addr, name = m[2], m[1]
454 end
457 end
455 if addr.present?
458 if addr.present?
456 user = self.class.new_user_from_attributes(addr, name)
459 user = self.class.new_user_from_attributes(addr, name)
457 if user.save
460 if user.save
458 user
461 user
459 else
462 else
460 logger.error "MailHandler: failed to create User: #{user.errors.full_messages}" if logger
463 logger.error "MailHandler: failed to create User: #{user.errors.full_messages}" if logger
461 nil
464 nil
462 end
465 end
463 else
466 else
464 logger.error "MailHandler: failed to create User: no FROM address found" if logger
467 logger.error "MailHandler: failed to create User: no FROM address found" if logger
465 nil
468 nil
466 end
469 end
467 end
470 end
468
471
469 # Adds the newly created user to default group
472 # Adds the newly created user to default group
470 def add_user_to_group(default_group)
473 def add_user_to_group(default_group)
471 if default_group.present?
474 if default_group.present?
472 default_group.split(',').each do |group_name|
475 default_group.split(',').each do |group_name|
473 if group = Group.named(group_name).first
476 if group = Group.named(group_name).first
474 group.users << @user
477 group.users << @user
475 elsif logger
478 elsif logger
476 logger.warn "MailHandler: could not add user to [#{group_name}], group not found"
479 logger.warn "MailHandler: could not add user to [#{group_name}], group not found"
477 end
480 end
478 end
481 end
479 end
482 end
480 end
483 end
481
484
482 # Removes the email body of text after the truncation configurations.
485 # Removes the email body of text after the truncation configurations.
483 def cleanup_body(body)
486 def cleanup_body(body)
484 delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
487 delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
485 unless delimiters.empty?
488 unless delimiters.empty?
486 regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
489 regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
487 body = body.gsub(regex, '')
490 body = body.gsub(regex, '')
488 end
491 end
489 body.strip
492 body.strip
490 end
493 end
491
494
492 def find_assignee_from_keyword(keyword, issue)
495 def find_assignee_from_keyword(keyword, issue)
493 keyword = keyword.to_s.downcase
496 keyword = keyword.to_s.downcase
494 assignable = issue.assignable_users
497 assignable = issue.assignable_users
495 assignee = nil
498 assignee = nil
496 assignee ||= assignable.detect {|a|
499 assignee ||= assignable.detect {|a|
497 a.mail.to_s.downcase == keyword ||
500 a.mail.to_s.downcase == keyword ||
498 a.login.to_s.downcase == keyword
501 a.login.to_s.downcase == keyword
499 }
502 }
500 if assignee.nil? && keyword.match(/ /)
503 if assignee.nil? && keyword.match(/ /)
501 firstname, lastname = *(keyword.split) # "First Last Throwaway"
504 firstname, lastname = *(keyword.split) # "First Last Throwaway"
502 assignee ||= assignable.detect {|a|
505 assignee ||= assignable.detect {|a|
503 a.is_a?(User) && a.firstname.to_s.downcase == firstname &&
506 a.is_a?(User) && a.firstname.to_s.downcase == firstname &&
504 a.lastname.to_s.downcase == lastname
507 a.lastname.to_s.downcase == lastname
505 }
508 }
506 end
509 end
507 if assignee.nil?
510 if assignee.nil?
508 assignee ||= assignable.detect {|a| a.name.downcase == keyword}
511 assignee ||= assignable.detect {|a| a.name.downcase == keyword}
509 end
512 end
510 assignee
513 assignee
511 end
514 end
512 end
515 end
@@ -1,155 +1,159
1 #!/usr/bin/env ruby
1 #!/usr/bin/env ruby
2
2
3 require 'net/http'
3 require 'net/http'
4 require 'net/https'
4 require 'net/https'
5 require 'uri'
5 require 'uri'
6 require 'optparse'
6 require 'optparse'
7
7
8 module Net
8 module Net
9 class HTTPS < HTTP
9 class HTTPS < HTTP
10 def self.post_form(url, params, headers, options={})
10 def self.post_form(url, params, headers, options={})
11 request = Post.new(url.path)
11 request = Post.new(url.path)
12 request.form_data = params
12 request.form_data = params
13 request.initialize_http_header(headers)
13 request.initialize_http_header(headers)
14 request.basic_auth url.user, url.password if url.user
14 request.basic_auth url.user, url.password if url.user
15 http = new(url.host, url.port)
15 http = new(url.host, url.port)
16 http.use_ssl = (url.scheme == 'https')
16 http.use_ssl = (url.scheme == 'https')
17 if options[:no_check_certificate]
17 if options[:no_check_certificate]
18 http.verify_mode = OpenSSL::SSL::VERIFY_NONE
18 http.verify_mode = OpenSSL::SSL::VERIFY_NONE
19 end
19 end
20 http.start {|h| h.request(request) }
20 http.start {|h| h.request(request) }
21 end
21 end
22 end
22 end
23 end
23 end
24
24
25 class RedmineMailHandler
25 class RedmineMailHandler
26 VERSION = '0.2.2'
26 VERSION = '0.2.3'
27
27
28 attr_accessor :verbose, :issue_attributes, :allow_override, :unknown_user, :default_group, :no_permission_check, :url, :key, :no_check_certificate
28 attr_accessor :verbose, :issue_attributes, :allow_override, :unknown_user, :default_group, :no_permission_check,
29 :url, :key, :no_check_certificate, :no_account_notice
29
30
30 def initialize
31 def initialize
31 self.issue_attributes = {}
32 self.issue_attributes = {}
32
33
33 optparse = OptionParser.new do |opts|
34 optparse = OptionParser.new do |opts|
34 opts.banner = "Usage: rdm-mailhandler.rb [options] --url=<Redmine URL> --key=<API key>"
35 opts.banner = "Usage: rdm-mailhandler.rb [options] --url=<Redmine URL> --key=<API key>"
35 opts.separator("")
36 opts.separator("")
36 opts.separator("Reads an email from standard input and forward it to a Redmine server through a HTTP request.")
37 opts.separator("Reads an email from standard input and forward it to a Redmine server through a HTTP request.")
37 opts.separator("")
38 opts.separator("")
38 opts.separator("Required arguments:")
39 opts.separator("Required arguments:")
39 opts.on("-u", "--url URL", "URL of the Redmine server") {|v| self.url = v}
40 opts.on("-u", "--url URL", "URL of the Redmine server") {|v| self.url = v}
40 opts.on("-k", "--key KEY", "Redmine API key") {|v| self.key = v}
41 opts.on("-k", "--key KEY", "Redmine API key") {|v| self.key = v}
41 opts.separator("")
42 opts.separator("")
42 opts.separator("General options:")
43 opts.separator("General options:")
43 opts.on("--no-permission-check", "disable permission checking when receiving",
44 opts.on("--no-permission-check", "disable permission checking when receiving",
44 "the email") {self.no_permission_check = '1'}
45 "the email") {self.no_permission_check = '1'}
45 opts.on("--key-file FILE", "path to a file that contains the Redmine",
46 opts.on("--key-file FILE", "path to a file that contains the Redmine",
46 "API key (use this option instead of --key",
47 "API key (use this option instead of --key",
47 "if you don't the key to appear in the",
48 "if you don't the key to appear in the",
48 "command line)") {|v| read_key_from_file(v)}
49 "command line)") {|v| read_key_from_file(v)}
49 opts.on("--no-check-certificate", "do not check server certificate") {self.no_check_certificate = true}
50 opts.on("--no-check-certificate", "do not check server certificate") {self.no_check_certificate = true}
50 opts.on("-h", "--help", "show this help") {puts opts; exit 1}
51 opts.on("-h", "--help", "show this help") {puts opts; exit 1}
51 opts.on("-v", "--verbose", "show extra information") {self.verbose = true}
52 opts.on("-v", "--verbose", "show extra information") {self.verbose = true}
52 opts.on("-V", "--version", "show version information and exit") {puts VERSION; exit}
53 opts.on("-V", "--version", "show version information and exit") {puts VERSION; exit}
53 opts.separator("")
54 opts.separator("")
54 opts.separator("User creation options:")
55 opts.separator("User creation options:")
55 opts.on("--unknown-user ACTION", "how to handle emails from an unknown user",
56 opts.on("--unknown-user ACTION", "how to handle emails from an unknown user",
56 "ACTION can be one of the following values:",
57 "ACTION can be one of the following values:",
57 "* ignore: email is ignored (default)",
58 "* ignore: email is ignored (default)",
58 "* accept: accept as anonymous user",
59 "* accept: accept as anonymous user",
59 "* create: create a user account") {|v| self.unknown_user = v}
60 "* create: create a user account") {|v| self.unknown_user = v}
60 opts.on("--default-group GROUP", "add created user to GROUP (none by default)",
61 opts.on("--default-group GROUP", "add created user to GROUP (none by default)",
61 "GROUP can be a comma separated list of groups") { |v| self.default_group = v}
62 "GROUP can be a comma separated list of groups") { |v| self.default_group = v}
63 opts.on("--no-account-notice", "don't send account information to the newly",
64 "created user") { |v| self.no_account_notice = '1'}
62 opts.separator("")
65 opts.separator("")
63 opts.separator("Issue attributes control options:")
66 opts.separator("Issue attributes control options:")
64 opts.on("-p", "--project PROJECT", "identifier of the target project") {|v| self.issue_attributes['project'] = v}
67 opts.on("-p", "--project PROJECT", "identifier of the target project") {|v| self.issue_attributes['project'] = v}
65 opts.on("-s", "--status STATUS", "name of the target status") {|v| self.issue_attributes['status'] = v}
68 opts.on("-s", "--status STATUS", "name of the target status") {|v| self.issue_attributes['status'] = v}
66 opts.on("-t", "--tracker TRACKER", "name of the target tracker") {|v| self.issue_attributes['tracker'] = v}
69 opts.on("-t", "--tracker TRACKER", "name of the target tracker") {|v| self.issue_attributes['tracker'] = v}
67 opts.on( "--category CATEGORY", "name of the target category") {|v| self.issue_attributes['category'] = v}
70 opts.on( "--category CATEGORY", "name of the target category") {|v| self.issue_attributes['category'] = v}
68 opts.on( "--priority PRIORITY", "name of the target priority") {|v| self.issue_attributes['priority'] = v}
71 opts.on( "--priority PRIORITY", "name of the target priority") {|v| self.issue_attributes['priority'] = v}
69 opts.on("-o", "--allow-override ATTRS", "allow email content to override attributes",
72 opts.on("-o", "--allow-override ATTRS", "allow email content to override attributes",
70 "specified by previous options",
73 "specified by previous options",
71 "ATTRS is a comma separated list of attributes") {|v| self.allow_override = v}
74 "ATTRS is a comma separated list of attributes") {|v| self.allow_override = v}
72 opts.separator("")
75 opts.separator("")
73 opts.separator("Examples:")
76 opts.separator("Examples:")
74 opts.separator("No project specified. Emails MUST contain the 'Project' keyword:")
77 opts.separator("No project specified. Emails MUST contain the 'Project' keyword:")
75 opts.separator(" rdm-mailhandler.rb --url http://redmine.domain.foo --key secret")
78 opts.separator(" rdm-mailhandler.rb --url http://redmine.domain.foo --key secret")
76 opts.separator("")
79 opts.separator("")
77 opts.separator("Fixed project and default tracker specified, but emails can override")
80 opts.separator("Fixed project and default tracker specified, but emails can override")
78 opts.separator("both tracker and priority attributes using keywords:")
81 opts.separator("both tracker and priority attributes using keywords:")
79 opts.separator(" rdm-mailhandler.rb --url https://domain.foo/redmine --key secret \\")
82 opts.separator(" rdm-mailhandler.rb --url https://domain.foo/redmine --key secret \\")
80 opts.separator(" --project foo \\")
83 opts.separator(" --project foo \\")
81 opts.separator(" --tracker bug \\")
84 opts.separator(" --tracker bug \\")
82 opts.separator(" --allow-override tracker,priority")
85 opts.separator(" --allow-override tracker,priority")
83
86
84 opts.summary_width = 27
87 opts.summary_width = 27
85 end
88 end
86 optparse.parse!
89 optparse.parse!
87
90
88 unless url && key
91 unless url && key
89 puts "Some arguments are missing. Use `rdm-mailhandler.rb --help` for getting help."
92 puts "Some arguments are missing. Use `rdm-mailhandler.rb --help` for getting help."
90 exit 1
93 exit 1
91 end
94 end
92 end
95 end
93
96
94 def submit(email)
97 def submit(email)
95 uri = url.gsub(%r{/*$}, '') + '/mail_handler'
98 uri = url.gsub(%r{/*$}, '') + '/mail_handler'
96
99
97 headers = { 'User-Agent' => "Redmine mail handler/#{VERSION}" }
100 headers = { 'User-Agent' => "Redmine mail handler/#{VERSION}" }
98
101
99 data = { 'key' => key, 'email' => email,
102 data = { 'key' => key, 'email' => email,
100 'allow_override' => allow_override,
103 'allow_override' => allow_override,
101 'unknown_user' => unknown_user,
104 'unknown_user' => unknown_user,
102 'default_group' => default_group,
105 'default_group' => default_group,
106 'no_account_notice' => no_account_notice,
103 'no_permission_check' => no_permission_check}
107 'no_permission_check' => no_permission_check}
104 issue_attributes.each { |attr, value| data["issue[#{attr}]"] = value }
108 issue_attributes.each { |attr, value| data["issue[#{attr}]"] = value }
105
109
106 debug "Posting to #{uri}..."
110 debug "Posting to #{uri}..."
107 begin
111 begin
108 response = Net::HTTPS.post_form(URI.parse(uri), data, headers, :no_check_certificate => no_check_certificate)
112 response = Net::HTTPS.post_form(URI.parse(uri), data, headers, :no_check_certificate => no_check_certificate)
109 rescue SystemCallError => e # connection refused, etc.
113 rescue SystemCallError => e # connection refused, etc.
110 warn "An error occured while contacting your Redmine server: #{e.message}"
114 warn "An error occured while contacting your Redmine server: #{e.message}"
111 return 75 # temporary failure
115 return 75 # temporary failure
112 end
116 end
113 debug "Response received: #{response.code}"
117 debug "Response received: #{response.code}"
114
118
115 case response.code.to_i
119 case response.code.to_i
116 when 403
120 when 403
117 warn "Request was denied by your Redmine server. " +
121 warn "Request was denied by your Redmine server. " +
118 "Make sure that 'WS for incoming emails' is enabled in application settings and that you provided the correct API key."
122 "Make sure that 'WS for incoming emails' is enabled in application settings and that you provided the correct API key."
119 return 77
123 return 77
120 when 422
124 when 422
121 warn "Request was denied by your Redmine server. " +
125 warn "Request was denied by your Redmine server. " +
122 "Possible reasons: email is sent from an invalid email address or is missing some information."
126 "Possible reasons: email is sent from an invalid email address or is missing some information."
123 return 77
127 return 77
124 when 400..499
128 when 400..499
125 warn "Request was denied by your Redmine server (#{response.code})."
129 warn "Request was denied by your Redmine server (#{response.code})."
126 return 77
130 return 77
127 when 500..599
131 when 500..599
128 warn "Failed to contact your Redmine server (#{response.code})."
132 warn "Failed to contact your Redmine server (#{response.code})."
129 return 75
133 return 75
130 when 201
134 when 201
131 debug "Proccessed successfully"
135 debug "Proccessed successfully"
132 return 0
136 return 0
133 else
137 else
134 return 1
138 return 1
135 end
139 end
136 end
140 end
137
141
138 private
142 private
139
143
140 def debug(msg)
144 def debug(msg)
141 puts msg if verbose
145 puts msg if verbose
142 end
146 end
143
147
144 def read_key_from_file(filename)
148 def read_key_from_file(filename)
145 begin
149 begin
146 self.key = File.read(filename).strip
150 self.key = File.read(filename).strip
147 rescue Exception => e
151 rescue Exception => e
148 $stderr.puts "Unable to read the key from #{filename}:\n#{e.message}"
152 $stderr.puts "Unable to read the key from #{filename}:\n#{e.message}"
149 exit 1
153 exit 1
150 end
154 end
151 end
155 end
152 end
156 end
153
157
154 handler = RedmineMailHandler.new
158 handler = RedmineMailHandler.new
155 exit(handler.submit(STDIN.read))
159 exit(handler.submit(STDIN.read))
@@ -1,792 +1,808
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2013 Jean-Philippe Lang
4 # Copyright (C) 2006-2013 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 :issues, :issue_statuses,
25 :issues, :issue_statuses,
26 :workflows, :trackers, :projects_trackers,
26 :workflows, :trackers, :projects_trackers,
27 :versions, :enumerations, :issue_categories,
27 :versions, :enumerations, :issue_categories,
28 :custom_fields, :custom_fields_trackers, :custom_fields_projects,
28 :custom_fields, :custom_fields_trackers, :custom_fields_projects,
29 :boards, :messages
29 :boards, :messages
30
30
31 FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler'
31 FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler'
32
32
33 def setup
33 def setup
34 ActionMailer::Base.deliveries.clear
34 ActionMailer::Base.deliveries.clear
35 Setting.notified_events = Redmine::Notifiable.all.collect(&:name)
35 Setting.notified_events = Redmine::Notifiable.all.collect(&:name)
36 end
36 end
37
37
38 def teardown
38 def teardown
39 Setting.clear_cache
39 Setting.clear_cache
40 end
40 end
41
41
42 def test_add_issue
42 def test_add_issue
43 ActionMailer::Base.deliveries.clear
43 ActionMailer::Base.deliveries.clear
44 # This email contains: 'Project: onlinestore'
44 # This email contains: 'Project: onlinestore'
45 issue = submit_email('ticket_on_given_project.eml')
45 issue = submit_email('ticket_on_given_project.eml')
46 assert issue.is_a?(Issue)
46 assert issue.is_a?(Issue)
47 assert !issue.new_record?
47 assert !issue.new_record?
48 issue.reload
48 issue.reload
49 assert_equal Project.find(2), issue.project
49 assert_equal Project.find(2), issue.project
50 assert_equal issue.project.trackers.first, issue.tracker
50 assert_equal issue.project.trackers.first, issue.tracker
51 assert_equal 'New ticket on a given project', issue.subject
51 assert_equal 'New ticket on a given project', issue.subject
52 assert_equal User.find_by_login('jsmith'), issue.author
52 assert_equal User.find_by_login('jsmith'), issue.author
53 assert_equal IssueStatus.find_by_name('Resolved'), issue.status
53 assert_equal IssueStatus.find_by_name('Resolved'), issue.status
54 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
54 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
55 assert_equal '2010-01-01', issue.start_date.to_s
55 assert_equal '2010-01-01', issue.start_date.to_s
56 assert_equal '2010-12-31', issue.due_date.to_s
56 assert_equal '2010-12-31', issue.due_date.to_s
57 assert_equal User.find_by_login('jsmith'), issue.assigned_to
57 assert_equal User.find_by_login('jsmith'), issue.assigned_to
58 assert_equal Version.find_by_name('Alpha'), issue.fixed_version
58 assert_equal Version.find_by_name('Alpha'), issue.fixed_version
59 assert_equal 2.5, issue.estimated_hours
59 assert_equal 2.5, issue.estimated_hours
60 assert_equal 30, issue.done_ratio
60 assert_equal 30, issue.done_ratio
61 assert_equal [issue.id, 1, 2], [issue.root_id, issue.lft, issue.rgt]
61 assert_equal [issue.id, 1, 2], [issue.root_id, issue.lft, issue.rgt]
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 # Email notification should be sent
66 # Email notification should be sent
67 mail = ActionMailer::Base.deliveries.last
67 mail = ActionMailer::Base.deliveries.last
68 assert_not_nil mail
68 assert_not_nil mail
69 assert mail.subject.include?('New ticket on a given project')
69 assert mail.subject.include?('New ticket on a given project')
70 end
70 end
71
71
72 def test_add_issue_with_default_tracker
72 def test_add_issue_with_default_tracker
73 # This email contains: 'Project: onlinestore'
73 # This email contains: 'Project: onlinestore'
74 issue = submit_email(
74 issue = submit_email(
75 'ticket_on_given_project.eml',
75 'ticket_on_given_project.eml',
76 :issue => {:tracker => 'Support request'}
76 :issue => {:tracker => 'Support request'}
77 )
77 )
78 assert issue.is_a?(Issue)
78 assert issue.is_a?(Issue)
79 assert !issue.new_record?
79 assert !issue.new_record?
80 issue.reload
80 issue.reload
81 assert_equal 'Support request', issue.tracker.name
81 assert_equal 'Support request', issue.tracker.name
82 end
82 end
83
83
84 def test_add_issue_with_status
84 def test_add_issue_with_status
85 # This email contains: 'Project: onlinestore' and 'Status: Resolved'
85 # This email contains: 'Project: onlinestore' and 'Status: Resolved'
86 issue = submit_email('ticket_on_given_project.eml')
86 issue = submit_email('ticket_on_given_project.eml')
87 assert issue.is_a?(Issue)
87 assert issue.is_a?(Issue)
88 assert !issue.new_record?
88 assert !issue.new_record?
89 issue.reload
89 issue.reload
90 assert_equal Project.find(2), issue.project
90 assert_equal Project.find(2), issue.project
91 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
91 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
92 end
92 end
93
93
94 def test_add_issue_with_attributes_override
94 def test_add_issue_with_attributes_override
95 issue = submit_email(
95 issue = submit_email(
96 'ticket_with_attributes.eml',
96 'ticket_with_attributes.eml',
97 :allow_override => 'tracker,category,priority'
97 :allow_override => 'tracker,category,priority'
98 )
98 )
99 assert issue.is_a?(Issue)
99 assert issue.is_a?(Issue)
100 assert !issue.new_record?
100 assert !issue.new_record?
101 issue.reload
101 issue.reload
102 assert_equal 'New ticket on a given project', issue.subject
102 assert_equal 'New ticket on a given project', issue.subject
103 assert_equal User.find_by_login('jsmith'), issue.author
103 assert_equal User.find_by_login('jsmith'), issue.author
104 assert_equal Project.find(2), issue.project
104 assert_equal Project.find(2), issue.project
105 assert_equal 'Feature request', issue.tracker.to_s
105 assert_equal 'Feature request', issue.tracker.to_s
106 assert_equal 'Stock management', issue.category.to_s
106 assert_equal 'Stock management', issue.category.to_s
107 assert_equal 'Urgent', issue.priority.to_s
107 assert_equal 'Urgent', issue.priority.to_s
108 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
108 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
109 end
109 end
110
110
111 def test_add_issue_with_group_assignment
111 def test_add_issue_with_group_assignment
112 with_settings :issue_group_assignment => '1' do
112 with_settings :issue_group_assignment => '1' do
113 issue = submit_email('ticket_on_given_project.eml') do |email|
113 issue = submit_email('ticket_on_given_project.eml') do |email|
114 email.gsub!('Assigned to: John Smith', 'Assigned to: B Team')
114 email.gsub!('Assigned to: John Smith', 'Assigned to: B Team')
115 end
115 end
116 assert issue.is_a?(Issue)
116 assert issue.is_a?(Issue)
117 assert !issue.new_record?
117 assert !issue.new_record?
118 issue.reload
118 issue.reload
119 assert_equal Group.find(11), issue.assigned_to
119 assert_equal Group.find(11), issue.assigned_to
120 end
120 end
121 end
121 end
122
122
123 def test_add_issue_with_partial_attributes_override
123 def test_add_issue_with_partial_attributes_override
124 issue = submit_email(
124 issue = submit_email(
125 'ticket_with_attributes.eml',
125 'ticket_with_attributes.eml',
126 :issue => {:priority => 'High'},
126 :issue => {:priority => 'High'},
127 :allow_override => ['tracker']
127 :allow_override => ['tracker']
128 )
128 )
129 assert issue.is_a?(Issue)
129 assert issue.is_a?(Issue)
130 assert !issue.new_record?
130 assert !issue.new_record?
131 issue.reload
131 issue.reload
132 assert_equal 'New ticket on a given project', issue.subject
132 assert_equal 'New ticket on a given project', issue.subject
133 assert_equal User.find_by_login('jsmith'), issue.author
133 assert_equal User.find_by_login('jsmith'), issue.author
134 assert_equal Project.find(2), issue.project
134 assert_equal Project.find(2), issue.project
135 assert_equal 'Feature request', issue.tracker.to_s
135 assert_equal 'Feature request', issue.tracker.to_s
136 assert_nil issue.category
136 assert_nil issue.category
137 assert_equal 'High', issue.priority.to_s
137 assert_equal 'High', issue.priority.to_s
138 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
138 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
139 end
139 end
140
140
141 def test_add_issue_with_spaces_between_attribute_and_separator
141 def test_add_issue_with_spaces_between_attribute_and_separator
142 issue = submit_email(
142 issue = submit_email(
143 'ticket_with_spaces_between_attribute_and_separator.eml',
143 'ticket_with_spaces_between_attribute_and_separator.eml',
144 :allow_override => 'tracker,category,priority'
144 :allow_override => 'tracker,category,priority'
145 )
145 )
146 assert issue.is_a?(Issue)
146 assert issue.is_a?(Issue)
147 assert !issue.new_record?
147 assert !issue.new_record?
148 issue.reload
148 issue.reload
149 assert_equal 'New ticket on a given project', issue.subject
149 assert_equal 'New ticket on a given project', issue.subject
150 assert_equal User.find_by_login('jsmith'), issue.author
150 assert_equal User.find_by_login('jsmith'), issue.author
151 assert_equal Project.find(2), issue.project
151 assert_equal Project.find(2), issue.project
152 assert_equal 'Feature request', issue.tracker.to_s
152 assert_equal 'Feature request', issue.tracker.to_s
153 assert_equal 'Stock management', issue.category.to_s
153 assert_equal 'Stock management', issue.category.to_s
154 assert_equal 'Urgent', issue.priority.to_s
154 assert_equal 'Urgent', issue.priority.to_s
155 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
155 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
156 end
156 end
157
157
158 def test_add_issue_with_attachment_to_specific_project
158 def test_add_issue_with_attachment_to_specific_project
159 issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
159 issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
160 assert issue.is_a?(Issue)
160 assert issue.is_a?(Issue)
161 assert !issue.new_record?
161 assert !issue.new_record?
162 issue.reload
162 issue.reload
163 assert_equal 'Ticket created by email with attachment', issue.subject
163 assert_equal 'Ticket created by email with attachment', issue.subject
164 assert_equal User.find_by_login('jsmith'), issue.author
164 assert_equal User.find_by_login('jsmith'), issue.author
165 assert_equal Project.find(2), issue.project
165 assert_equal Project.find(2), issue.project
166 assert_equal 'This is a new ticket with attachments', issue.description
166 assert_equal 'This is a new ticket with attachments', issue.description
167 # Attachment properties
167 # Attachment properties
168 assert_equal 1, issue.attachments.size
168 assert_equal 1, issue.attachments.size
169 assert_equal 'Paella.jpg', issue.attachments.first.filename
169 assert_equal 'Paella.jpg', issue.attachments.first.filename
170 assert_equal 'image/jpeg', issue.attachments.first.content_type
170 assert_equal 'image/jpeg', issue.attachments.first.content_type
171 assert_equal 10790, issue.attachments.first.filesize
171 assert_equal 10790, issue.attachments.first.filesize
172 end
172 end
173
173
174 def test_add_issue_with_custom_fields
174 def test_add_issue_with_custom_fields
175 issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'onlinestore'})
175 issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'onlinestore'})
176 assert issue.is_a?(Issue)
176 assert issue.is_a?(Issue)
177 assert !issue.new_record?
177 assert !issue.new_record?
178 issue.reload
178 issue.reload
179 assert_equal 'New ticket with custom field values', issue.subject
179 assert_equal 'New ticket with custom field values', issue.subject
180 assert_equal 'PostgreSQL', issue.custom_field_value(1)
180 assert_equal 'PostgreSQL', issue.custom_field_value(1)
181 assert_equal 'Value for a custom field', issue.custom_field_value(2)
181 assert_equal 'Value for a custom field', issue.custom_field_value(2)
182 assert !issue.description.match(/^searchable field:/i)
182 assert !issue.description.match(/^searchable field:/i)
183 end
183 end
184
184
185 def test_add_issue_with_version_custom_fields
185 def test_add_issue_with_version_custom_fields
186 field = IssueCustomField.create!(:name => 'Affected version', :field_format => 'version', :is_for_all => true, :tracker_ids => [1,2,3])
186 field = IssueCustomField.create!(:name => 'Affected version', :field_format => 'version', :is_for_all => true, :tracker_ids => [1,2,3])
187
187
188 issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'ecookbook'}) do |email|
188 issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'ecookbook'}) do |email|
189 email << "Affected version: 1.0\n"
189 email << "Affected version: 1.0\n"
190 end
190 end
191 assert issue.is_a?(Issue)
191 assert issue.is_a?(Issue)
192 assert !issue.new_record?
192 assert !issue.new_record?
193 issue.reload
193 issue.reload
194 assert_equal '2', issue.custom_field_value(field)
194 assert_equal '2', issue.custom_field_value(field)
195 end
195 end
196
196
197 def test_add_issue_should_match_assignee_on_display_name
197 def test_add_issue_should_match_assignee_on_display_name
198 user = User.generate!(:firstname => 'Foo Bar', :lastname => 'Foo Baz')
198 user = User.generate!(:firstname => 'Foo Bar', :lastname => 'Foo Baz')
199 User.add_to_project(user, Project.find(2))
199 User.add_to_project(user, Project.find(2))
200 issue = submit_email('ticket_on_given_project.eml') do |email|
200 issue = submit_email('ticket_on_given_project.eml') do |email|
201 email.sub!(/^Assigned to.*$/, 'Assigned to: Foo Bar Foo baz')
201 email.sub!(/^Assigned to.*$/, 'Assigned to: Foo Bar Foo baz')
202 end
202 end
203 assert issue.is_a?(Issue)
203 assert issue.is_a?(Issue)
204 assert_equal user, issue.assigned_to
204 assert_equal user, issue.assigned_to
205 end
205 end
206
206
207 def test_add_issue_with_cc
207 def test_add_issue_with_cc
208 issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'})
208 issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'})
209 assert issue.is_a?(Issue)
209 assert issue.is_a?(Issue)
210 assert !issue.new_record?
210 assert !issue.new_record?
211 issue.reload
211 issue.reload
212 assert issue.watched_by?(User.find_by_mail('dlopper@somenet.foo'))
212 assert issue.watched_by?(User.find_by_mail('dlopper@somenet.foo'))
213 assert_equal 1, issue.watcher_user_ids.size
213 assert_equal 1, issue.watcher_user_ids.size
214 end
214 end
215
215
216 def test_add_issue_by_unknown_user
216 def test_add_issue_by_unknown_user
217 assert_no_difference 'User.count' do
217 assert_no_difference 'User.count' do
218 assert_equal false,
218 assert_equal false,
219 submit_email(
219 submit_email(
220 'ticket_by_unknown_user.eml',
220 'ticket_by_unknown_user.eml',
221 :issue => {:project => 'ecookbook'}
221 :issue => {:project => 'ecookbook'}
222 )
222 )
223 end
223 end
224 end
224 end
225
225
226 def test_add_issue_by_anonymous_user
226 def test_add_issue_by_anonymous_user
227 Role.anonymous.add_permission!(:add_issues)
227 Role.anonymous.add_permission!(:add_issues)
228 assert_no_difference 'User.count' do
228 assert_no_difference 'User.count' do
229 issue = submit_email(
229 issue = submit_email(
230 'ticket_by_unknown_user.eml',
230 'ticket_by_unknown_user.eml',
231 :issue => {:project => 'ecookbook'},
231 :issue => {:project => 'ecookbook'},
232 :unknown_user => 'accept'
232 :unknown_user => 'accept'
233 )
233 )
234 assert issue.is_a?(Issue)
234 assert issue.is_a?(Issue)
235 assert issue.author.anonymous?
235 assert issue.author.anonymous?
236 end
236 end
237 end
237 end
238
238
239 def test_add_issue_by_anonymous_user_with_no_from_address
239 def test_add_issue_by_anonymous_user_with_no_from_address
240 Role.anonymous.add_permission!(:add_issues)
240 Role.anonymous.add_permission!(:add_issues)
241 assert_no_difference 'User.count' do
241 assert_no_difference 'User.count' do
242 issue = submit_email(
242 issue = submit_email(
243 'ticket_by_empty_user.eml',
243 'ticket_by_empty_user.eml',
244 :issue => {:project => 'ecookbook'},
244 :issue => {:project => 'ecookbook'},
245 :unknown_user => 'accept'
245 :unknown_user => 'accept'
246 )
246 )
247 assert issue.is_a?(Issue)
247 assert issue.is_a?(Issue)
248 assert issue.author.anonymous?
248 assert issue.author.anonymous?
249 end
249 end
250 end
250 end
251
251
252 def test_add_issue_by_anonymous_user_on_private_project
252 def test_add_issue_by_anonymous_user_on_private_project
253 Role.anonymous.add_permission!(:add_issues)
253 Role.anonymous.add_permission!(:add_issues)
254 assert_no_difference 'User.count' do
254 assert_no_difference 'User.count' do
255 assert_no_difference 'Issue.count' do
255 assert_no_difference 'Issue.count' do
256 assert_equal false,
256 assert_equal false,
257 submit_email(
257 submit_email(
258 'ticket_by_unknown_user.eml',
258 'ticket_by_unknown_user.eml',
259 :issue => {:project => 'onlinestore'},
259 :issue => {:project => 'onlinestore'},
260 :unknown_user => 'accept'
260 :unknown_user => 'accept'
261 )
261 )
262 end
262 end
263 end
263 end
264 end
264 end
265
265
266 def test_add_issue_by_anonymous_user_on_private_project_without_permission_check
266 def test_add_issue_by_anonymous_user_on_private_project_without_permission_check
267 assert_no_difference 'User.count' do
267 assert_no_difference 'User.count' do
268 assert_difference 'Issue.count' do
268 assert_difference 'Issue.count' do
269 issue = submit_email(
269 issue = submit_email(
270 'ticket_by_unknown_user.eml',
270 'ticket_by_unknown_user.eml',
271 :issue => {:project => 'onlinestore'},
271 :issue => {:project => 'onlinestore'},
272 :no_permission_check => '1',
272 :no_permission_check => '1',
273 :unknown_user => 'accept'
273 :unknown_user => 'accept'
274 )
274 )
275 assert issue.is_a?(Issue)
275 assert issue.is_a?(Issue)
276 assert issue.author.anonymous?
276 assert issue.author.anonymous?
277 assert !issue.project.is_public?
277 assert !issue.project.is_public?
278 assert_equal [issue.id, 1, 2], [issue.root_id, issue.lft, issue.rgt]
278 assert_equal [issue.id, 1, 2], [issue.root_id, issue.lft, issue.rgt]
279 end
279 end
280 end
280 end
281 end
281 end
282
282
283 def test_add_issue_by_created_user
283 def test_add_issue_by_created_user
284 Setting.default_language = 'en'
284 Setting.default_language = 'en'
285 assert_difference 'User.count' do
285 assert_difference 'User.count' do
286 issue = submit_email(
286 issue = submit_email(
287 'ticket_by_unknown_user.eml',
287 'ticket_by_unknown_user.eml',
288 :issue => {:project => 'ecookbook'},
288 :issue => {:project => 'ecookbook'},
289 :unknown_user => 'create'
289 :unknown_user => 'create'
290 )
290 )
291 assert issue.is_a?(Issue)
291 assert issue.is_a?(Issue)
292 assert issue.author.active?
292 assert issue.author.active?
293 assert_equal 'john.doe@somenet.foo', issue.author.mail
293 assert_equal 'john.doe@somenet.foo', issue.author.mail
294 assert_equal 'John', issue.author.firstname
294 assert_equal 'John', issue.author.firstname
295 assert_equal 'Doe', issue.author.lastname
295 assert_equal 'Doe', issue.author.lastname
296
296
297 # account information
297 # account information
298 email = ActionMailer::Base.deliveries.first
298 email = ActionMailer::Base.deliveries.first
299 assert_not_nil email
299 assert_not_nil email
300 assert email.subject.include?('account activation')
300 assert email.subject.include?('account activation')
301 login = mail_body(email).match(/\* Login: (.*)$/)[1].strip
301 login = mail_body(email).match(/\* Login: (.*)$/)[1].strip
302 password = mail_body(email).match(/\* Password: (.*)$/)[1].strip
302 password = mail_body(email).match(/\* Password: (.*)$/)[1].strip
303 assert_equal issue.author, User.try_to_login(login, password)
303 assert_equal issue.author, User.try_to_login(login, password)
304 end
304 end
305 end
305 end
306
306
307 def test_created_user_should_be_added_to_groups
307 def test_created_user_should_be_added_to_groups
308 group1 = Group.generate!
308 group1 = Group.generate!
309 group2 = Group.generate!
309 group2 = Group.generate!
310
310
311 assert_difference 'User.count' do
311 assert_difference 'User.count' do
312 submit_email(
312 submit_email(
313 'ticket_by_unknown_user.eml',
313 'ticket_by_unknown_user.eml',
314 :issue => {:project => 'ecookbook'},
314 :issue => {:project => 'ecookbook'},
315 :unknown_user => 'create',
315 :unknown_user => 'create',
316 :default_group => "#{group1.name},#{group2.name}"
316 :default_group => "#{group1.name},#{group2.name}"
317 )
317 )
318 end
318 end
319 user = User.order('id DESC').first
319 user = User.order('id DESC').first
320 assert_same_elements [group1, group2], user.groups
320 assert_same_elements [group1, group2], user.groups
321 end
321 end
322
322
323 def test_created_user_should_not_receive_account_information_with_no_account_info_option
324 assert_difference 'User.count' do
325 submit_email(
326 'ticket_by_unknown_user.eml',
327 :issue => {:project => 'ecookbook'},
328 :unknown_user => 'create',
329 :no_account_notice => '1'
330 )
331 end
332
333 # only 1 email for the new issue notification
334 assert_equal 1, ActionMailer::Base.deliveries.size
335 email = ActionMailer::Base.deliveries.first
336 assert_include 'Ticket by unknown user', email.subject
337 end
338
323 def test_add_issue_without_from_header
339 def test_add_issue_without_from_header
324 Role.anonymous.add_permission!(:add_issues)
340 Role.anonymous.add_permission!(:add_issues)
325 assert_equal false, submit_email('ticket_without_from_header.eml')
341 assert_equal false, submit_email('ticket_without_from_header.eml')
326 end
342 end
327
343
328 def test_add_issue_with_invalid_attributes
344 def test_add_issue_with_invalid_attributes
329 issue = submit_email(
345 issue = submit_email(
330 'ticket_with_invalid_attributes.eml',
346 'ticket_with_invalid_attributes.eml',
331 :allow_override => 'tracker,category,priority'
347 :allow_override => 'tracker,category,priority'
332 )
348 )
333 assert issue.is_a?(Issue)
349 assert issue.is_a?(Issue)
334 assert !issue.new_record?
350 assert !issue.new_record?
335 issue.reload
351 issue.reload
336 assert_nil issue.assigned_to
352 assert_nil issue.assigned_to
337 assert_nil issue.start_date
353 assert_nil issue.start_date
338 assert_nil issue.due_date
354 assert_nil issue.due_date
339 assert_equal 0, issue.done_ratio
355 assert_equal 0, issue.done_ratio
340 assert_equal 'Normal', issue.priority.to_s
356 assert_equal 'Normal', issue.priority.to_s
341 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
357 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
342 end
358 end
343
359
344 def test_add_issue_with_localized_attributes
360 def test_add_issue_with_localized_attributes
345 User.find_by_mail('jsmith@somenet.foo').update_attribute 'language', 'fr'
361 User.find_by_mail('jsmith@somenet.foo').update_attribute 'language', 'fr'
346 issue = submit_email(
362 issue = submit_email(
347 'ticket_with_localized_attributes.eml',
363 'ticket_with_localized_attributes.eml',
348 :allow_override => 'tracker,category,priority'
364 :allow_override => 'tracker,category,priority'
349 )
365 )
350 assert issue.is_a?(Issue)
366 assert issue.is_a?(Issue)
351 assert !issue.new_record?
367 assert !issue.new_record?
352 issue.reload
368 issue.reload
353 assert_equal 'New ticket on a given project', issue.subject
369 assert_equal 'New ticket on a given project', issue.subject
354 assert_equal User.find_by_login('jsmith'), issue.author
370 assert_equal User.find_by_login('jsmith'), issue.author
355 assert_equal Project.find(2), issue.project
371 assert_equal Project.find(2), issue.project
356 assert_equal 'Feature request', issue.tracker.to_s
372 assert_equal 'Feature request', issue.tracker.to_s
357 assert_equal 'Stock management', issue.category.to_s
373 assert_equal 'Stock management', issue.category.to_s
358 assert_equal 'Urgent', issue.priority.to_s
374 assert_equal 'Urgent', issue.priority.to_s
359 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
375 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
360 end
376 end
361
377
362 def test_add_issue_with_japanese_keywords
378 def test_add_issue_with_japanese_keywords
363 ja_dev = "\xe9\x96\x8b\xe7\x99\xba"
379 ja_dev = "\xe9\x96\x8b\xe7\x99\xba"
364 ja_dev.force_encoding('UTF-8') if ja_dev.respond_to?(:force_encoding)
380 ja_dev.force_encoding('UTF-8') if ja_dev.respond_to?(:force_encoding)
365 tracker = Tracker.create!(:name => ja_dev)
381 tracker = Tracker.create!(:name => ja_dev)
366 Project.find(1).trackers << tracker
382 Project.find(1).trackers << tracker
367 issue = submit_email(
383 issue = submit_email(
368 'japanese_keywords_iso_2022_jp.eml',
384 'japanese_keywords_iso_2022_jp.eml',
369 :issue => {:project => 'ecookbook'},
385 :issue => {:project => 'ecookbook'},
370 :allow_override => 'tracker'
386 :allow_override => 'tracker'
371 )
387 )
372 assert_kind_of Issue, issue
388 assert_kind_of Issue, issue
373 assert_equal tracker, issue.tracker
389 assert_equal tracker, issue.tracker
374 end
390 end
375
391
376 def test_add_issue_from_apple_mail
392 def test_add_issue_from_apple_mail
377 issue = submit_email(
393 issue = submit_email(
378 'apple_mail_with_attachment.eml',
394 'apple_mail_with_attachment.eml',
379 :issue => {:project => 'ecookbook'}
395 :issue => {:project => 'ecookbook'}
380 )
396 )
381 assert_kind_of Issue, issue
397 assert_kind_of Issue, issue
382 assert_equal 1, issue.attachments.size
398 assert_equal 1, issue.attachments.size
383
399
384 attachment = issue.attachments.first
400 attachment = issue.attachments.first
385 assert_equal 'paella.jpg', attachment.filename
401 assert_equal 'paella.jpg', attachment.filename
386 assert_equal 10790, attachment.filesize
402 assert_equal 10790, attachment.filesize
387 assert File.exist?(attachment.diskfile)
403 assert File.exist?(attachment.diskfile)
388 assert_equal 10790, File.size(attachment.diskfile)
404 assert_equal 10790, File.size(attachment.diskfile)
389 assert_equal 'caaf384198bcbc9563ab5c058acd73cd', attachment.digest
405 assert_equal 'caaf384198bcbc9563ab5c058acd73cd', attachment.digest
390 end
406 end
391
407
392 def test_thunderbird_with_attachment_ja
408 def test_thunderbird_with_attachment_ja
393 issue = submit_email(
409 issue = submit_email(
394 'thunderbird_with_attachment_ja.eml',
410 'thunderbird_with_attachment_ja.eml',
395 :issue => {:project => 'ecookbook'}
411 :issue => {:project => 'ecookbook'}
396 )
412 )
397 assert_kind_of Issue, issue
413 assert_kind_of Issue, issue
398 assert_equal 1, issue.attachments.size
414 assert_equal 1, issue.attachments.size
399 ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88.txt"
415 ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88.txt"
400 ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding)
416 ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding)
401 attachment = issue.attachments.first
417 attachment = issue.attachments.first
402 assert_equal ja, attachment.filename
418 assert_equal ja, attachment.filename
403 assert_equal 5, attachment.filesize
419 assert_equal 5, attachment.filesize
404 assert File.exist?(attachment.diskfile)
420 assert File.exist?(attachment.diskfile)
405 assert_equal 5, File.size(attachment.diskfile)
421 assert_equal 5, File.size(attachment.diskfile)
406 assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest
422 assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest
407 end
423 end
408
424
409 def test_gmail_with_attachment_ja
425 def test_gmail_with_attachment_ja
410 issue = submit_email(
426 issue = submit_email(
411 'gmail_with_attachment_ja.eml',
427 'gmail_with_attachment_ja.eml',
412 :issue => {:project => 'ecookbook'}
428 :issue => {:project => 'ecookbook'}
413 )
429 )
414 assert_kind_of Issue, issue
430 assert_kind_of Issue, issue
415 assert_equal 1, issue.attachments.size
431 assert_equal 1, issue.attachments.size
416 ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88.txt"
432 ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88.txt"
417 ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding)
433 ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding)
418 attachment = issue.attachments.first
434 attachment = issue.attachments.first
419 assert_equal ja, attachment.filename
435 assert_equal ja, attachment.filename
420 assert_equal 5, attachment.filesize
436 assert_equal 5, attachment.filesize
421 assert File.exist?(attachment.diskfile)
437 assert File.exist?(attachment.diskfile)
422 assert_equal 5, File.size(attachment.diskfile)
438 assert_equal 5, File.size(attachment.diskfile)
423 assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest
439 assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest
424 end
440 end
425
441
426 def test_thunderbird_with_attachment_latin1
442 def test_thunderbird_with_attachment_latin1
427 issue = submit_email(
443 issue = submit_email(
428 'thunderbird_with_attachment_iso-8859-1.eml',
444 'thunderbird_with_attachment_iso-8859-1.eml',
429 :issue => {:project => 'ecookbook'}
445 :issue => {:project => 'ecookbook'}
430 )
446 )
431 assert_kind_of Issue, issue
447 assert_kind_of Issue, issue
432 assert_equal 1, issue.attachments.size
448 assert_equal 1, issue.attachments.size
433 u = ""
449 u = ""
434 u.force_encoding('UTF-8') if u.respond_to?(:force_encoding)
450 u.force_encoding('UTF-8') if u.respond_to?(:force_encoding)
435 u1 = "\xc3\x84\xc3\xa4\xc3\x96\xc3\xb6\xc3\x9c\xc3\xbc"
451 u1 = "\xc3\x84\xc3\xa4\xc3\x96\xc3\xb6\xc3\x9c\xc3\xbc"
436 u1.force_encoding('UTF-8') if u1.respond_to?(:force_encoding)
452 u1.force_encoding('UTF-8') if u1.respond_to?(:force_encoding)
437 11.times { u << u1 }
453 11.times { u << u1 }
438 attachment = issue.attachments.first
454 attachment = issue.attachments.first
439 assert_equal "#{u}.png", attachment.filename
455 assert_equal "#{u}.png", attachment.filename
440 assert_equal 130, attachment.filesize
456 assert_equal 130, attachment.filesize
441 assert File.exist?(attachment.diskfile)
457 assert File.exist?(attachment.diskfile)
442 assert_equal 130, File.size(attachment.diskfile)
458 assert_equal 130, File.size(attachment.diskfile)
443 assert_equal '4d80e667ac37dddfe05502530f152abb', attachment.digest
459 assert_equal '4d80e667ac37dddfe05502530f152abb', attachment.digest
444 end
460 end
445
461
446 def test_gmail_with_attachment_latin1
462 def test_gmail_with_attachment_latin1
447 issue = submit_email(
463 issue = submit_email(
448 'gmail_with_attachment_iso-8859-1.eml',
464 'gmail_with_attachment_iso-8859-1.eml',
449 :issue => {:project => 'ecookbook'}
465 :issue => {:project => 'ecookbook'}
450 )
466 )
451 assert_kind_of Issue, issue
467 assert_kind_of Issue, issue
452 assert_equal 1, issue.attachments.size
468 assert_equal 1, issue.attachments.size
453 u = ""
469 u = ""
454 u.force_encoding('UTF-8') if u.respond_to?(:force_encoding)
470 u.force_encoding('UTF-8') if u.respond_to?(:force_encoding)
455 u1 = "\xc3\x84\xc3\xa4\xc3\x96\xc3\xb6\xc3\x9c\xc3\xbc"
471 u1 = "\xc3\x84\xc3\xa4\xc3\x96\xc3\xb6\xc3\x9c\xc3\xbc"
456 u1.force_encoding('UTF-8') if u1.respond_to?(:force_encoding)
472 u1.force_encoding('UTF-8') if u1.respond_to?(:force_encoding)
457 11.times { u << u1 }
473 11.times { u << u1 }
458 attachment = issue.attachments.first
474 attachment = issue.attachments.first
459 assert_equal "#{u}.txt", attachment.filename
475 assert_equal "#{u}.txt", attachment.filename
460 assert_equal 5, attachment.filesize
476 assert_equal 5, attachment.filesize
461 assert File.exist?(attachment.diskfile)
477 assert File.exist?(attachment.diskfile)
462 assert_equal 5, File.size(attachment.diskfile)
478 assert_equal 5, File.size(attachment.diskfile)
463 assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest
479 assert_equal 'd8e8fca2dc0f896fd7cb4cb0031ba249', attachment.digest
464 end
480 end
465
481
466 def test_add_issue_with_iso_8859_1_subject
482 def test_add_issue_with_iso_8859_1_subject
467 issue = submit_email(
483 issue = submit_email(
468 'subject_as_iso-8859-1.eml',
484 'subject_as_iso-8859-1.eml',
469 :issue => {:project => 'ecookbook'}
485 :issue => {:project => 'ecookbook'}
470 )
486 )
471 str = "Testmail from Webmail: \xc3\xa4 \xc3\xb6 \xc3\xbc..."
487 str = "Testmail from Webmail: \xc3\xa4 \xc3\xb6 \xc3\xbc..."
472 str.force_encoding('UTF-8') if str.respond_to?(:force_encoding)
488 str.force_encoding('UTF-8') if str.respond_to?(:force_encoding)
473 assert_kind_of Issue, issue
489 assert_kind_of Issue, issue
474 assert_equal str, issue.subject
490 assert_equal str, issue.subject
475 end
491 end
476
492
477 def test_add_issue_with_japanese_subject
493 def test_add_issue_with_japanese_subject
478 issue = submit_email(
494 issue = submit_email(
479 'subject_japanese_1.eml',
495 'subject_japanese_1.eml',
480 :issue => {:project => 'ecookbook'}
496 :issue => {:project => 'ecookbook'}
481 )
497 )
482 assert_kind_of Issue, issue
498 assert_kind_of Issue, issue
483 ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88"
499 ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88"
484 ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding)
500 ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding)
485 assert_equal ja, issue.subject
501 assert_equal ja, issue.subject
486 end
502 end
487
503
488 def test_add_issue_with_no_subject_header
504 def test_add_issue_with_no_subject_header
489 issue = submit_email(
505 issue = submit_email(
490 'no_subject_header.eml',
506 'no_subject_header.eml',
491 :issue => {:project => 'ecookbook'}
507 :issue => {:project => 'ecookbook'}
492 )
508 )
493 assert_kind_of Issue, issue
509 assert_kind_of Issue, issue
494 assert_equal '(no subject)', issue.subject
510 assert_equal '(no subject)', issue.subject
495 end
511 end
496
512
497 def test_add_issue_with_mixed_japanese_subject
513 def test_add_issue_with_mixed_japanese_subject
498 issue = submit_email(
514 issue = submit_email(
499 'subject_japanese_2.eml',
515 'subject_japanese_2.eml',
500 :issue => {:project => 'ecookbook'}
516 :issue => {:project => 'ecookbook'}
501 )
517 )
502 assert_kind_of Issue, issue
518 assert_kind_of Issue, issue
503 ja = "Re: \xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88"
519 ja = "Re: \xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88"
504 ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding)
520 ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding)
505 assert_equal ja, issue.subject
521 assert_equal ja, issue.subject
506 end
522 end
507
523
508 def test_should_ignore_emails_from_locked_users
524 def test_should_ignore_emails_from_locked_users
509 User.find(2).lock!
525 User.find(2).lock!
510
526
511 MailHandler.any_instance.expects(:dispatch).never
527 MailHandler.any_instance.expects(:dispatch).never
512 assert_no_difference 'Issue.count' do
528 assert_no_difference 'Issue.count' do
513 assert_equal false, submit_email('ticket_on_given_project.eml')
529 assert_equal false, submit_email('ticket_on_given_project.eml')
514 end
530 end
515 end
531 end
516
532
517 def test_should_ignore_emails_from_emission_address
533 def test_should_ignore_emails_from_emission_address
518 Role.anonymous.add_permission!(:add_issues)
534 Role.anonymous.add_permission!(:add_issues)
519 assert_no_difference 'User.count' do
535 assert_no_difference 'User.count' do
520 assert_equal false,
536 assert_equal false,
521 submit_email(
537 submit_email(
522 'ticket_from_emission_address.eml',
538 'ticket_from_emission_address.eml',
523 :issue => {:project => 'ecookbook'},
539 :issue => {:project => 'ecookbook'},
524 :unknown_user => 'create'
540 :unknown_user => 'create'
525 )
541 )
526 end
542 end
527 end
543 end
528
544
529 def test_should_ignore_auto_replied_emails
545 def test_should_ignore_auto_replied_emails
530 MailHandler.any_instance.expects(:dispatch).never
546 MailHandler.any_instance.expects(:dispatch).never
531 [
547 [
532 "X-Auto-Response-Suppress: OOF",
548 "X-Auto-Response-Suppress: OOF",
533 "Auto-Submitted: auto-replied",
549 "Auto-Submitted: auto-replied",
534 "Auto-Submitted: Auto-Replied",
550 "Auto-Submitted: Auto-Replied",
535 "Auto-Submitted: auto-generated"
551 "Auto-Submitted: auto-generated"
536 ].each do |header|
552 ].each do |header|
537 raw = IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml'))
553 raw = IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml'))
538 raw = header + "\n" + raw
554 raw = header + "\n" + raw
539
555
540 assert_no_difference 'Issue.count' do
556 assert_no_difference 'Issue.count' do
541 assert_equal false, MailHandler.receive(raw), "email with #{header} header was not ignored"
557 assert_equal false, MailHandler.receive(raw), "email with #{header} header was not ignored"
542 end
558 end
543 end
559 end
544 end
560 end
545
561
546 def test_add_issue_should_send_email_notification
562 def test_add_issue_should_send_email_notification
547 Setting.notified_events = ['issue_added']
563 Setting.notified_events = ['issue_added']
548 ActionMailer::Base.deliveries.clear
564 ActionMailer::Base.deliveries.clear
549 # This email contains: 'Project: onlinestore'
565 # This email contains: 'Project: onlinestore'
550 issue = submit_email('ticket_on_given_project.eml')
566 issue = submit_email('ticket_on_given_project.eml')
551 assert issue.is_a?(Issue)
567 assert issue.is_a?(Issue)
552 assert_equal 1, ActionMailer::Base.deliveries.size
568 assert_equal 1, ActionMailer::Base.deliveries.size
553 end
569 end
554
570
555 def test_update_issue
571 def test_update_issue
556 journal = submit_email('ticket_reply.eml')
572 journal = submit_email('ticket_reply.eml')
557 assert journal.is_a?(Journal)
573 assert journal.is_a?(Journal)
558 assert_equal User.find_by_login('jsmith'), journal.user
574 assert_equal User.find_by_login('jsmith'), journal.user
559 assert_equal Issue.find(2), journal.journalized
575 assert_equal Issue.find(2), journal.journalized
560 assert_match /This is reply/, journal.notes
576 assert_match /This is reply/, journal.notes
561 assert_equal false, journal.private_notes
577 assert_equal false, journal.private_notes
562 assert_equal 'Feature request', journal.issue.tracker.name
578 assert_equal 'Feature request', journal.issue.tracker.name
563 end
579 end
564
580
565 def test_update_issue_with_attribute_changes
581 def test_update_issue_with_attribute_changes
566 # This email contains: 'Status: Resolved'
582 # This email contains: 'Status: Resolved'
567 journal = submit_email('ticket_reply_with_status.eml')
583 journal = submit_email('ticket_reply_with_status.eml')
568 assert journal.is_a?(Journal)
584 assert journal.is_a?(Journal)
569 issue = Issue.find(journal.issue.id)
585 issue = Issue.find(journal.issue.id)
570 assert_equal User.find_by_login('jsmith'), journal.user
586 assert_equal User.find_by_login('jsmith'), journal.user
571 assert_equal Issue.find(2), journal.journalized
587 assert_equal Issue.find(2), journal.journalized
572 assert_match /This is reply/, journal.notes
588 assert_match /This is reply/, journal.notes
573 assert_equal 'Feature request', journal.issue.tracker.name
589 assert_equal 'Feature request', journal.issue.tracker.name
574 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
590 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
575 assert_equal '2010-01-01', issue.start_date.to_s
591 assert_equal '2010-01-01', issue.start_date.to_s
576 assert_equal '2010-12-31', issue.due_date.to_s
592 assert_equal '2010-12-31', issue.due_date.to_s
577 assert_equal User.find_by_login('jsmith'), issue.assigned_to
593 assert_equal User.find_by_login('jsmith'), issue.assigned_to
578 assert_equal "52.6", issue.custom_value_for(CustomField.find_by_name('Float field')).value
594 assert_equal "52.6", issue.custom_value_for(CustomField.find_by_name('Float field')).value
579 # keywords should be removed from the email body
595 # keywords should be removed from the email body
580 assert !journal.notes.match(/^Status:/i)
596 assert !journal.notes.match(/^Status:/i)
581 assert !journal.notes.match(/^Start Date:/i)
597 assert !journal.notes.match(/^Start Date:/i)
582 end
598 end
583
599
584 def test_update_issue_with_attachment
600 def test_update_issue_with_attachment
585 assert_difference 'Journal.count' do
601 assert_difference 'Journal.count' do
586 assert_difference 'JournalDetail.count' do
602 assert_difference 'JournalDetail.count' do
587 assert_difference 'Attachment.count' do
603 assert_difference 'Attachment.count' do
588 assert_no_difference 'Issue.count' do
604 assert_no_difference 'Issue.count' do
589 journal = submit_email('ticket_with_attachment.eml') do |raw|
605 journal = submit_email('ticket_with_attachment.eml') do |raw|
590 raw.gsub! /^Subject: .*$/, 'Subject: Re: [Cookbook - Feature #2] (New) Add ingredients categories'
606 raw.gsub! /^Subject: .*$/, 'Subject: Re: [Cookbook - Feature #2] (New) Add ingredients categories'
591 end
607 end
592 end
608 end
593 end
609 end
594 end
610 end
595 end
611 end
596 journal = Journal.first(:order => 'id DESC')
612 journal = Journal.first(:order => 'id DESC')
597 assert_equal Issue.find(2), journal.journalized
613 assert_equal Issue.find(2), journal.journalized
598 assert_equal 1, journal.details.size
614 assert_equal 1, journal.details.size
599
615
600 detail = journal.details.first
616 detail = journal.details.first
601 assert_equal 'attachment', detail.property
617 assert_equal 'attachment', detail.property
602 assert_equal 'Paella.jpg', detail.value
618 assert_equal 'Paella.jpg', detail.value
603 end
619 end
604
620
605 def test_update_issue_should_send_email_notification
621 def test_update_issue_should_send_email_notification
606 ActionMailer::Base.deliveries.clear
622 ActionMailer::Base.deliveries.clear
607 journal = submit_email('ticket_reply.eml')
623 journal = submit_email('ticket_reply.eml')
608 assert journal.is_a?(Journal)
624 assert journal.is_a?(Journal)
609 assert_equal 1, ActionMailer::Base.deliveries.size
625 assert_equal 1, ActionMailer::Base.deliveries.size
610 end
626 end
611
627
612 def test_update_issue_should_not_set_defaults
628 def test_update_issue_should_not_set_defaults
613 journal = submit_email(
629 journal = submit_email(
614 'ticket_reply.eml',
630 'ticket_reply.eml',
615 :issue => {:tracker => 'Support request', :priority => 'High'}
631 :issue => {:tracker => 'Support request', :priority => 'High'}
616 )
632 )
617 assert journal.is_a?(Journal)
633 assert journal.is_a?(Journal)
618 assert_match /This is reply/, journal.notes
634 assert_match /This is reply/, journal.notes
619 assert_equal 'Feature request', journal.issue.tracker.name
635 assert_equal 'Feature request', journal.issue.tracker.name
620 assert_equal 'Normal', journal.issue.priority.name
636 assert_equal 'Normal', journal.issue.priority.name
621 end
637 end
622
638
623 def test_replying_to_a_private_note_should_add_reply_as_private
639 def test_replying_to_a_private_note_should_add_reply_as_private
624 private_journal = Journal.create!(:notes => 'Private notes', :journalized => Issue.find(1), :private_notes => true, :user_id => 2)
640 private_journal = Journal.create!(:notes => 'Private notes', :journalized => Issue.find(1), :private_notes => true, :user_id => 2)
625
641
626 assert_difference 'Journal.count' do
642 assert_difference 'Journal.count' do
627 journal = submit_email('ticket_reply.eml') do |email|
643 journal = submit_email('ticket_reply.eml') do |email|
628 email.sub! %r{^In-Reply-To:.*$}, "In-Reply-To: <redmine.journal-#{private_journal.id}.20060719210421@osiris>"
644 email.sub! %r{^In-Reply-To:.*$}, "In-Reply-To: <redmine.journal-#{private_journal.id}.20060719210421@osiris>"
629 end
645 end
630
646
631 assert_kind_of Journal, journal
647 assert_kind_of Journal, journal
632 assert_match /This is reply/, journal.notes
648 assert_match /This is reply/, journal.notes
633 assert_equal true, journal.private_notes
649 assert_equal true, journal.private_notes
634 end
650 end
635 end
651 end
636
652
637 def test_reply_to_a_message
653 def test_reply_to_a_message
638 m = submit_email('message_reply.eml')
654 m = submit_email('message_reply.eml')
639 assert m.is_a?(Message)
655 assert m.is_a?(Message)
640 assert !m.new_record?
656 assert !m.new_record?
641 m.reload
657 m.reload
642 assert_equal 'Reply via email', m.subject
658 assert_equal 'Reply via email', m.subject
643 # The email replies to message #2 which is part of the thread of message #1
659 # The email replies to message #2 which is part of the thread of message #1
644 assert_equal Message.find(1), m.parent
660 assert_equal Message.find(1), m.parent
645 end
661 end
646
662
647 def test_reply_to_a_message_by_subject
663 def test_reply_to_a_message_by_subject
648 m = submit_email('message_reply_by_subject.eml')
664 m = submit_email('message_reply_by_subject.eml')
649 assert m.is_a?(Message)
665 assert m.is_a?(Message)
650 assert !m.new_record?
666 assert !m.new_record?
651 m.reload
667 m.reload
652 assert_equal 'Reply to the first post', m.subject
668 assert_equal 'Reply to the first post', m.subject
653 assert_equal Message.find(1), m.parent
669 assert_equal Message.find(1), m.parent
654 end
670 end
655
671
656 def test_should_strip_tags_of_html_only_emails
672 def test_should_strip_tags_of_html_only_emails
657 issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'})
673 issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'})
658 assert issue.is_a?(Issue)
674 assert issue.is_a?(Issue)
659 assert !issue.new_record?
675 assert !issue.new_record?
660 issue.reload
676 issue.reload
661 assert_equal 'HTML email', issue.subject
677 assert_equal 'HTML email', issue.subject
662 assert_equal 'This is a html-only email.', issue.description
678 assert_equal 'This is a html-only email.', issue.description
663 end
679 end
664
680
665 test "truncate emails with no setting should add the entire email into the issue" do
681 test "truncate emails with no setting should add the entire email into the issue" do
666 with_settings :mail_handler_body_delimiters => '' do
682 with_settings :mail_handler_body_delimiters => '' do
667 issue = submit_email('ticket_on_given_project.eml')
683 issue = submit_email('ticket_on_given_project.eml')
668 assert_issue_created(issue)
684 assert_issue_created(issue)
669 assert issue.description.include?('---')
685 assert issue.description.include?('---')
670 assert issue.description.include?('This paragraph is after the delimiter')
686 assert issue.description.include?('This paragraph is after the delimiter')
671 end
687 end
672 end
688 end
673
689
674 test "truncate emails with a single string should truncate the email at the delimiter for the issue" do
690 test "truncate emails with a single string should truncate the email at the delimiter for the issue" do
675 with_settings :mail_handler_body_delimiters => '---' do
691 with_settings :mail_handler_body_delimiters => '---' do
676 issue = submit_email('ticket_on_given_project.eml')
692 issue = submit_email('ticket_on_given_project.eml')
677 assert_issue_created(issue)
693 assert_issue_created(issue)
678 assert issue.description.include?('This paragraph is before delimiters')
694 assert issue.description.include?('This paragraph is before delimiters')
679 assert issue.description.include?('--- This line starts with a delimiter')
695 assert issue.description.include?('--- This line starts with a delimiter')
680 assert !issue.description.match(/^---$/)
696 assert !issue.description.match(/^---$/)
681 assert !issue.description.include?('This paragraph is after the delimiter')
697 assert !issue.description.include?('This paragraph is after the delimiter')
682 end
698 end
683 end
699 end
684
700
685 test "truncate emails with a single quoted reply should truncate the email at the delimiter with the quoted reply symbols (>)" do
701 test "truncate emails with a single quoted reply should truncate the email at the delimiter with the quoted reply symbols (>)" do
686 with_settings :mail_handler_body_delimiters => '--- Reply above. Do not remove this line. ---' do
702 with_settings :mail_handler_body_delimiters => '--- Reply above. Do not remove this line. ---' do
687 journal = submit_email('issue_update_with_quoted_reply_above.eml')
703 journal = submit_email('issue_update_with_quoted_reply_above.eml')
688 assert journal.is_a?(Journal)
704 assert journal.is_a?(Journal)
689 assert journal.notes.include?('An update to the issue by the sender.')
705 assert journal.notes.include?('An update to the issue by the sender.')
690 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
706 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
691 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
707 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
692 end
708 end
693 end
709 end
694
710
695 test "truncate emails with multiple quoted replies should truncate the email at the delimiter with the quoted reply symbols (>)" do
711 test "truncate emails with multiple quoted replies should truncate the email at the delimiter with the quoted reply symbols (>)" do
696 with_settings :mail_handler_body_delimiters => '--- Reply above. Do not remove this line. ---' do
712 with_settings :mail_handler_body_delimiters => '--- Reply above. Do not remove this line. ---' do
697 journal = submit_email('issue_update_with_multiple_quoted_reply_above.eml')
713 journal = submit_email('issue_update_with_multiple_quoted_reply_above.eml')
698 assert journal.is_a?(Journal)
714 assert journal.is_a?(Journal)
699 assert journal.notes.include?('An update to the issue by the sender.')
715 assert journal.notes.include?('An update to the issue by the sender.')
700 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
716 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
701 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
717 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
702 end
718 end
703 end
719 end
704
720
705 test "truncate emails with multiple strings should truncate the email at the first delimiter found (BREAK)" do
721 test "truncate emails with multiple strings should truncate the email at the first delimiter found (BREAK)" do
706 with_settings :mail_handler_body_delimiters => "---\nBREAK" do
722 with_settings :mail_handler_body_delimiters => "---\nBREAK" do
707 issue = submit_email('ticket_on_given_project.eml')
723 issue = submit_email('ticket_on_given_project.eml')
708 assert_issue_created(issue)
724 assert_issue_created(issue)
709 assert issue.description.include?('This paragraph is before delimiters')
725 assert issue.description.include?('This paragraph is before delimiters')
710 assert !issue.description.include?('BREAK')
726 assert !issue.description.include?('BREAK')
711 assert !issue.description.include?('This paragraph is between delimiters')
727 assert !issue.description.include?('This paragraph is between delimiters')
712 assert !issue.description.match(/^---$/)
728 assert !issue.description.match(/^---$/)
713 assert !issue.description.include?('This paragraph is after the delimiter')
729 assert !issue.description.include?('This paragraph is after the delimiter')
714 end
730 end
715 end
731 end
716
732
717 def test_email_with_long_subject_line
733 def test_email_with_long_subject_line
718 issue = submit_email('ticket_with_long_subject.eml')
734 issue = submit_email('ticket_with_long_subject.eml')
719 assert issue.is_a?(Issue)
735 assert issue.is_a?(Issue)
720 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]
736 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]
721 end
737 end
722
738
723 def test_new_user_from_attributes_should_return_valid_user
739 def test_new_user_from_attributes_should_return_valid_user
724 to_test = {
740 to_test = {
725 # [address, name] => [login, firstname, lastname]
741 # [address, name] => [login, firstname, lastname]
726 ['jsmith@example.net', nil] => ['jsmith@example.net', 'jsmith', '-'],
742 ['jsmith@example.net', nil] => ['jsmith@example.net', 'jsmith', '-'],
727 ['jsmith@example.net', 'John'] => ['jsmith@example.net', 'John', '-'],
743 ['jsmith@example.net', 'John'] => ['jsmith@example.net', 'John', '-'],
728 ['jsmith@example.net', 'John Smith'] => ['jsmith@example.net', 'John', 'Smith'],
744 ['jsmith@example.net', 'John Smith'] => ['jsmith@example.net', 'John', 'Smith'],
729 ['jsmith@example.net', 'John Paul Smith'] => ['jsmith@example.net', 'John', 'Paul Smith'],
745 ['jsmith@example.net', 'John Paul Smith'] => ['jsmith@example.net', 'John', 'Paul Smith'],
730 ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsTheMaximumLength Smith'] => ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsT', 'Smith'],
746 ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsTheMaximumLength Smith'] => ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsT', 'Smith'],
731 ['jsmith@example.net', 'John AVeryLongLastnameThatExceedsTheMaximumLength'] => ['jsmith@example.net', 'John', 'AVeryLongLastnameThatExceedsTh']
747 ['jsmith@example.net', 'John AVeryLongLastnameThatExceedsTheMaximumLength'] => ['jsmith@example.net', 'John', 'AVeryLongLastnameThatExceedsTh']
732 }
748 }
733
749
734 to_test.each do |attrs, expected|
750 to_test.each do |attrs, expected|
735 user = MailHandler.new_user_from_attributes(attrs.first, attrs.last)
751 user = MailHandler.new_user_from_attributes(attrs.first, attrs.last)
736
752
737 assert user.valid?, user.errors.full_messages.to_s
753 assert user.valid?, user.errors.full_messages.to_s
738 assert_equal attrs.first, user.mail
754 assert_equal attrs.first, user.mail
739 assert_equal expected[0], user.login
755 assert_equal expected[0], user.login
740 assert_equal expected[1], user.firstname
756 assert_equal expected[1], user.firstname
741 assert_equal expected[2], user.lastname
757 assert_equal expected[2], user.lastname
742 end
758 end
743 end
759 end
744
760
745 def test_new_user_from_attributes_should_respect_minimum_password_length
761 def test_new_user_from_attributes_should_respect_minimum_password_length
746 with_settings :password_min_length => 15 do
762 with_settings :password_min_length => 15 do
747 user = MailHandler.new_user_from_attributes('jsmith@example.net')
763 user = MailHandler.new_user_from_attributes('jsmith@example.net')
748 assert user.valid?
764 assert user.valid?
749 assert user.password.length >= 15
765 assert user.password.length >= 15
750 end
766 end
751 end
767 end
752
768
753 def test_new_user_from_attributes_should_use_default_login_if_invalid
769 def test_new_user_from_attributes_should_use_default_login_if_invalid
754 user = MailHandler.new_user_from_attributes('foo+bar@example.net')
770 user = MailHandler.new_user_from_attributes('foo+bar@example.net')
755 assert user.valid?
771 assert user.valid?
756 assert user.login =~ /^user[a-f0-9]+$/
772 assert user.login =~ /^user[a-f0-9]+$/
757 assert_equal 'foo+bar@example.net', user.mail
773 assert_equal 'foo+bar@example.net', user.mail
758 end
774 end
759
775
760 def test_new_user_with_utf8_encoded_fullname_should_be_decoded
776 def test_new_user_with_utf8_encoded_fullname_should_be_decoded
761 assert_difference 'User.count' do
777 assert_difference 'User.count' do
762 issue = submit_email(
778 issue = submit_email(
763 'fullname_of_sender_as_utf8_encoded.eml',
779 'fullname_of_sender_as_utf8_encoded.eml',
764 :issue => {:project => 'ecookbook'},
780 :issue => {:project => 'ecookbook'},
765 :unknown_user => 'create'
781 :unknown_user => 'create'
766 )
782 )
767 end
783 end
768
784
769 user = User.first(:order => 'id DESC')
785 user = User.first(:order => 'id DESC')
770 assert_equal "foo@example.org", user.mail
786 assert_equal "foo@example.org", user.mail
771 str1 = "\xc3\x84\xc3\xa4"
787 str1 = "\xc3\x84\xc3\xa4"
772 str2 = "\xc3\x96\xc3\xb6"
788 str2 = "\xc3\x96\xc3\xb6"
773 str1.force_encoding('UTF-8') if str1.respond_to?(:force_encoding)
789 str1.force_encoding('UTF-8') if str1.respond_to?(:force_encoding)
774 str2.force_encoding('UTF-8') if str2.respond_to?(:force_encoding)
790 str2.force_encoding('UTF-8') if str2.respond_to?(:force_encoding)
775 assert_equal str1, user.firstname
791 assert_equal str1, user.firstname
776 assert_equal str2, user.lastname
792 assert_equal str2, user.lastname
777 end
793 end
778
794
779 private
795 private
780
796
781 def submit_email(filename, options={})
797 def submit_email(filename, options={})
782 raw = IO.read(File.join(FIXTURES_PATH, filename))
798 raw = IO.read(File.join(FIXTURES_PATH, filename))
783 yield raw if block_given?
799 yield raw if block_given?
784 MailHandler.receive(raw, options)
800 MailHandler.receive(raw, options)
785 end
801 end
786
802
787 def assert_issue_created(issue)
803 def assert_issue_created(issue)
788 assert issue.is_a?(Issue)
804 assert issue.is_a?(Issue)
789 assert !issue.new_record?
805 assert !issue.new_record?
790 issue.reload
806 issue.reload
791 end
807 end
792 end
808 end
General Comments 0
You need to be logged in to leave comments. Login now