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