##// END OF EJS Templates
fix non ASCII attachment filename encoding broken (MOJIBAKE) in receiving mail on Ruby 1.8 (#12399)...
Toshi MARUYAMA -
r10625:6740f441c499
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,481 +1,498
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 subject = email.subject.to_s
128 128 if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
129 129 klass, object_id = $1, $2.to_i
130 130 method_name = "receive_#{klass}_reply"
131 131 if self.class.private_instance_methods.collect(&:to_s).include?(method_name)
132 132 send method_name, object_id
133 133 else
134 134 # ignoring it
135 135 end
136 136 elsif m = subject.match(ISSUE_REPLY_SUBJECT_RE)
137 137 receive_issue_reply(m[1].to_i)
138 138 elsif m = subject.match(MESSAGE_REPLY_SUBJECT_RE)
139 139 receive_message_reply(m[1].to_i)
140 140 else
141 141 dispatch_to_default
142 142 end
143 143 rescue ActiveRecord::RecordInvalid => e
144 144 # TODO: send a email to the user
145 145 logger.error e.message if logger
146 146 false
147 147 rescue MissingInformation => e
148 148 logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
149 149 false
150 150 rescue UnauthorizedAction => e
151 151 logger.error "MailHandler: unauthorized attempt from #{user}" if logger
152 152 false
153 153 end
154 154
155 155 def dispatch_to_default
156 156 receive_issue
157 157 end
158 158
159 159 # Creates a new issue
160 160 def receive_issue
161 161 project = target_project
162 162 # check permission
163 163 unless @@handler_options[:no_permission_check]
164 164 raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
165 165 end
166 166
167 167 issue = Issue.new(:author => user, :project => project)
168 168 issue.safe_attributes = issue_attributes_from_keywords(issue)
169 169 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
170 170 issue.subject = cleaned_up_subject
171 171 if issue.subject.blank?
172 172 issue.subject = '(no subject)'
173 173 end
174 174 issue.description = cleaned_up_text_body
175 175
176 176 # add To and Cc as watchers before saving so the watchers can reply to Redmine
177 177 add_watchers(issue)
178 178 issue.save!
179 179 add_attachments(issue)
180 180 logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
181 181 issue
182 182 end
183 183
184 184 # Adds a note to an existing issue
185 185 def receive_issue_reply(issue_id, from_journal=nil)
186 186 issue = Issue.find_by_id(issue_id)
187 187 return unless issue
188 188 # check permission
189 189 unless @@handler_options[:no_permission_check]
190 190 unless user.allowed_to?(:add_issue_notes, issue.project) ||
191 191 user.allowed_to?(:edit_issues, issue.project)
192 192 raise UnauthorizedAction
193 193 end
194 194 end
195 195
196 196 # ignore CLI-supplied defaults for new issues
197 197 @@handler_options[:issue].clear
198 198
199 199 journal = issue.init_journal(user)
200 200 if from_journal && from_journal.private_notes?
201 201 # If the received email was a reply to a private note, make the added note private
202 202 issue.private_notes = true
203 203 end
204 204 issue.safe_attributes = issue_attributes_from_keywords(issue)
205 205 issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
206 206 journal.notes = cleaned_up_text_body
207 207 add_attachments(issue)
208 208 issue.save!
209 209 if logger && logger.info
210 210 logger.info "MailHandler: issue ##{issue.id} updated by #{user}"
211 211 end
212 212 journal
213 213 end
214 214
215 215 # Reply will be added to the issue
216 216 def receive_journal_reply(journal_id)
217 217 journal = Journal.find_by_id(journal_id)
218 218 if journal && journal.journalized_type == 'Issue'
219 219 receive_issue_reply(journal.journalized_id, journal)
220 220 end
221 221 end
222 222
223 223 # Receives a reply to a forum message
224 224 def receive_message_reply(message_id)
225 225 message = Message.find_by_id(message_id)
226 226 if message
227 227 message = message.root
228 228
229 229 unless @@handler_options[:no_permission_check]
230 230 raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
231 231 end
232 232
233 233 if !message.locked?
234 234 reply = Message.new(:subject => cleaned_up_subject.gsub(%r{^.*msg\d+\]}, '').strip,
235 235 :content => cleaned_up_text_body)
236 236 reply.author = user
237 237 reply.board = message.board
238 238 message.children << reply
239 239 add_attachments(reply)
240 240 reply
241 241 else
242 242 if logger && logger.info
243 243 logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic"
244 244 end
245 245 end
246 246 end
247 247 end
248 248
249 249 def add_attachments(obj)
250 250 if email.attachments && email.attachments.any?
251 251 email.attachments.each do |attachment|
252 filename = attachment.filename
253 unless filename.respond_to?(:encoding)
254 # try to reencode to utf8 manually with ruby1.8
255 h = attachment.header['Content-Disposition']
256 unless h.nil?
257 begin
258 if m = h.value.match(/filename\*[0-9\*]*=([^=']+)'/)
259 filename = Redmine::CodesetUtil.to_utf8(filename, m[1])
260 elsif m = h.value.match(/filename=.*=\?([^\?]+)\?[BbQq]\?/)
261 # http://tools.ietf.org/html/rfc2047#section-4
262 filename = Redmine::CodesetUtil.to_utf8(filename, m[1])
263 end
264 rescue
265 # nop
266 end
267 end
268 end
252 269 obj.attachments << Attachment.create(:container => obj,
253 270 :file => attachment.decoded,
254 :filename => attachment.filename,
271 :filename => filename,
255 272 :author => user,
256 273 :content_type => attachment.mime_type)
257 274 end
258 275 end
259 276 end
260 277
261 278 # Adds To and Cc as watchers of the given object if the sender has the
262 279 # appropriate permission
263 280 def add_watchers(obj)
264 281 if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
265 282 addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
266 283 unless addresses.empty?
267 284 watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
268 285 watchers.each {|w| obj.add_watcher(w)}
269 286 end
270 287 end
271 288 end
272 289
273 290 def get_keyword(attr, options={})
274 291 @keywords ||= {}
275 292 if @keywords.has_key?(attr)
276 293 @keywords[attr]
277 294 else
278 295 @keywords[attr] = begin
279 296 if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) &&
280 297 (v = extract_keyword!(plain_text_body, attr, options[:format]))
281 298 v
282 299 elsif !@@handler_options[:issue][attr].blank?
283 300 @@handler_options[:issue][attr]
284 301 end
285 302 end
286 303 end
287 304 end
288 305
289 306 # Destructively extracts the value for +attr+ in +text+
290 307 # Returns nil if no matching keyword found
291 308 def extract_keyword!(text, attr, format=nil)
292 309 keys = [attr.to_s.humanize]
293 310 if attr.is_a?(Symbol)
294 311 if user && user.language.present?
295 312 keys << l("field_#{attr}", :default => '', :locale => user.language)
296 313 end
297 314 if Setting.default_language.present?
298 315 keys << l("field_#{attr}", :default => '', :locale => Setting.default_language)
299 316 end
300 317 end
301 318 keys.reject! {|k| k.blank?}
302 319 keys.collect! {|k| Regexp.escape(k)}
303 320 format ||= '.+'
304 321 keyword = nil
305 322 regexp = /^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i
306 323 if m = text.match(regexp)
307 324 keyword = m[2].strip
308 325 text.gsub!(regexp, '')
309 326 end
310 327 keyword
311 328 end
312 329
313 330 def target_project
314 331 # TODO: other ways to specify project:
315 332 # * parse the email To field
316 333 # * specific project (eg. Setting.mail_handler_target_project)
317 334 target = Project.find_by_identifier(get_keyword(:project))
318 335 raise MissingInformation.new('Unable to determine target project') if target.nil?
319 336 target
320 337 end
321 338
322 339 # Returns a Hash of issue attributes extracted from keywords in the email body
323 340 def issue_attributes_from_keywords(issue)
324 341 assigned_to = (k = get_keyword(:assigned_to, :override => true)) && find_assignee_from_keyword(k, issue)
325 342
326 343 attrs = {
327 344 'tracker_id' => (k = get_keyword(:tracker)) && issue.project.trackers.named(k).first.try(:id),
328 345 'status_id' => (k = get_keyword(:status)) && IssueStatus.named(k).first.try(:id),
329 346 'priority_id' => (k = get_keyword(:priority)) && IssuePriority.named(k).first.try(:id),
330 347 'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.named(k).first.try(:id),
331 348 'assigned_to_id' => assigned_to.try(:id),
332 349 'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) &&
333 350 issue.project.shared_versions.named(k).first.try(:id),
334 351 'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
335 352 'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
336 353 'estimated_hours' => get_keyword(:estimated_hours, :override => true),
337 354 'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0')
338 355 }.delete_if {|k, v| v.blank? }
339 356
340 357 if issue.new_record? && attrs['tracker_id'].nil?
341 358 attrs['tracker_id'] = issue.project.trackers.find(:first).try(:id)
342 359 end
343 360
344 361 attrs
345 362 end
346 363
347 364 # Returns a Hash of issue custom field values extracted from keywords in the email body
348 365 def custom_field_values_from_keywords(customized)
349 366 customized.custom_field_values.inject({}) do |h, v|
350 367 if keyword = get_keyword(v.custom_field.name, :override => true)
351 368 h[v.custom_field.id.to_s] = v.custom_field.value_from_keyword(keyword, customized)
352 369 end
353 370 h
354 371 end
355 372 end
356 373
357 374 # Returns the text/plain part of the email
358 375 # If not found (eg. HTML-only email), returns the body with tags removed
359 376 def plain_text_body
360 377 return @plain_text_body unless @plain_text_body.nil?
361 378
362 379 part = email.text_part || email.html_part || email
363 380 @plain_text_body = Redmine::CodesetUtil.to_utf8(part.body.decoded, part.charset)
364 381
365 382 # strip html tags and remove doctype directive
366 383 @plain_text_body = strip_tags(@plain_text_body.strip)
367 384 @plain_text_body.sub! %r{^<!DOCTYPE .*$}, ''
368 385 @plain_text_body
369 386 end
370 387
371 388 def cleaned_up_text_body
372 389 cleanup_body(plain_text_body)
373 390 end
374 391
375 392 def cleaned_up_subject
376 393 subject = email.subject.to_s
377 394 unless subject.respond_to?(:encoding)
378 395 # try to reencode to utf8 manually with ruby1.8
379 396 begin
380 397 if h = email.header[:subject]
381 398 # http://tools.ietf.org/html/rfc2047#section-4
382 399 if m = h.value.match(/=\?([^\?]+)\?[BbQq]\?/)
383 400 subject = Redmine::CodesetUtil.to_utf8(subject, m[1])
384 401 end
385 402 end
386 403 rescue
387 404 # nop
388 405 end
389 406 end
390 407 subject.strip[0,255]
391 408 end
392 409
393 410 def self.full_sanitizer
394 411 @full_sanitizer ||= HTML::FullSanitizer.new
395 412 end
396 413
397 414 def self.assign_string_attribute_with_limit(object, attribute, value, limit=nil)
398 415 limit ||= object.class.columns_hash[attribute.to_s].limit || 255
399 416 value = value.to_s.slice(0, limit)
400 417 object.send("#{attribute}=", value)
401 418 end
402 419
403 420 # Returns a User from an email address and a full name
404 421 def self.new_user_from_attributes(email_address, fullname=nil)
405 422 user = User.new
406 423
407 424 # Truncating the email address would result in an invalid format
408 425 user.mail = email_address
409 426 assign_string_attribute_with_limit(user, 'login', email_address, User::LOGIN_LENGTH_LIMIT)
410 427
411 428 names = fullname.blank? ? email_address.gsub(/@.*$/, '').split('.') : fullname.split
412 429 assign_string_attribute_with_limit(user, 'firstname', names.shift)
413 430 assign_string_attribute_with_limit(user, 'lastname', names.join(' '))
414 431 user.lastname = '-' if user.lastname.blank?
415 432
416 433 password_length = [Setting.password_min_length.to_i, 10].max
417 434 user.password = Redmine::Utils.random_hex(password_length / 2 + 1)
418 435 user.language = Setting.default_language
419 436
420 437 unless user.valid?
421 438 user.login = "user#{Redmine::Utils.random_hex(6)}" unless user.errors[:login].blank?
422 439 user.firstname = "-" unless user.errors[:firstname].blank?
423 440 user.lastname = "-" unless user.errors[:lastname].blank?
424 441 end
425 442
426 443 user
427 444 end
428 445
429 446 # Creates a User for the +email+ sender
430 447 # Returns the user or nil if it could not be created
431 448 def create_user_from_email
432 449 from = email.header['from'].to_s
433 450 addr, name = from, nil
434 451 if m = from.match(/^"?(.+?)"?\s+<(.+@.+)>$/)
435 452 addr, name = m[2], m[1]
436 453 end
437 454 if addr.present?
438 455 user = self.class.new_user_from_attributes(addr, name)
439 456 if user.save
440 457 user
441 458 else
442 459 logger.error "MailHandler: failed to create User: #{user.errors.full_messages}" if logger
443 460 nil
444 461 end
445 462 else
446 463 logger.error "MailHandler: failed to create User: no FROM address found" if logger
447 464 nil
448 465 end
449 466 end
450 467
451 468 # Removes the email body of text after the truncation configurations.
452 469 def cleanup_body(body)
453 470 delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
454 471 unless delimiters.empty?
455 472 regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
456 473 body = body.gsub(regex, '')
457 474 end
458 475 body.strip
459 476 end
460 477
461 478 def find_assignee_from_keyword(keyword, issue)
462 479 keyword = keyword.to_s.downcase
463 480 assignable = issue.assignable_users
464 481 assignee = nil
465 482 assignee ||= assignable.detect {|a|
466 483 a.mail.to_s.downcase == keyword ||
467 484 a.login.to_s.downcase == keyword
468 485 }
469 486 if assignee.nil? && keyword.match(/ /)
470 487 firstname, lastname = *(keyword.split) # "First Last Throwaway"
471 488 assignee ||= assignable.detect {|a|
472 489 a.is_a?(User) && a.firstname.to_s.downcase == firstname &&
473 490 a.lastname.to_s.downcase == lastname
474 491 }
475 492 end
476 493 if assignee.nil?
477 494 assignee ||= assignable.detect {|a| a.name.downcase == keyword}
478 495 end
479 496 assignee
480 497 end
481 498 end
@@ -1,720 +1,794
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 str = "Testmail from Webmail: \xc3\xa4 \xc3\xb6 \xc3\xbc..."
382 456 str.force_encoding('UTF-8') if str.respond_to?(:force_encoding)
383 457 assert_kind_of Issue, issue
384 458 assert_equal str, issue.subject
385 459 end
386 460
387 461 def test_add_issue_with_japanese_subject
388 462 issue = submit_email(
389 463 'subject_japanese_1.eml',
390 464 :issue => {:project => 'ecookbook'}
391 465 )
392 466 assert_kind_of Issue, issue
393 467 ja = "\xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88"
394 468 ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding)
395 469 assert_equal ja, issue.subject
396 470 end
397 471
398 472 def test_add_issue_with_no_subject_header
399 473 issue = submit_email(
400 474 'no_subject_header.eml',
401 475 :issue => {:project => 'ecookbook'}
402 476 )
403 477 assert_kind_of Issue, issue
404 478 assert_equal '(no subject)', issue.subject
405 479 end
406 480
407 481 def test_add_issue_with_mixed_japanese_subject
408 482 issue = submit_email(
409 483 'subject_japanese_2.eml',
410 484 :issue => {:project => 'ecookbook'}
411 485 )
412 486 assert_kind_of Issue, issue
413 487 ja = "Re: \xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88"
414 488 ja.force_encoding('UTF-8') if ja.respond_to?(:force_encoding)
415 489 assert_equal ja, issue.subject
416 490 end
417 491
418 492 def test_should_ignore_emails_from_locked_users
419 493 User.find(2).lock!
420 494
421 495 MailHandler.any_instance.expects(:dispatch).never
422 496 assert_no_difference 'Issue.count' do
423 497 assert_equal false, submit_email('ticket_on_given_project.eml')
424 498 end
425 499 end
426 500
427 501 def test_should_ignore_emails_from_emission_address
428 502 Role.anonymous.add_permission!(:add_issues)
429 503 assert_no_difference 'User.count' do
430 504 assert_equal false,
431 505 submit_email(
432 506 'ticket_from_emission_address.eml',
433 507 :issue => {:project => 'ecookbook'},
434 508 :unknown_user => 'create'
435 509 )
436 510 end
437 511 end
438 512
439 513 def test_should_ignore_auto_replied_emails
440 514 MailHandler.any_instance.expects(:dispatch).never
441 515 [
442 516 "X-Auto-Response-Suppress: OOF",
443 517 "Auto-Submitted: auto-replied",
444 518 "Auto-Submitted: Auto-Replied",
445 519 "Auto-Submitted: auto-generated"
446 520 ].each do |header|
447 521 raw = IO.read(File.join(FIXTURES_PATH, 'ticket_on_given_project.eml'))
448 522 raw = header + "\n" + raw
449 523
450 524 assert_no_difference 'Issue.count' do
451 525 assert_equal false, MailHandler.receive(raw), "email with #{header} header was not ignored"
452 526 end
453 527 end
454 528 end
455 529
456 530 def test_add_issue_should_send_email_notification
457 531 Setting.notified_events = ['issue_added']
458 532 ActionMailer::Base.deliveries.clear
459 533 # This email contains: 'Project: onlinestore'
460 534 issue = submit_email('ticket_on_given_project.eml')
461 535 assert issue.is_a?(Issue)
462 536 assert_equal 1, ActionMailer::Base.deliveries.size
463 537 end
464 538
465 539 def test_update_issue
466 540 journal = submit_email('ticket_reply.eml')
467 541 assert journal.is_a?(Journal)
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 false, journal.private_notes
472 546 assert_equal 'Feature request', journal.issue.tracker.name
473 547 end
474 548
475 549 def test_update_issue_with_attribute_changes
476 550 # This email contains: 'Status: Resolved'
477 551 journal = submit_email('ticket_reply_with_status.eml')
478 552 assert journal.is_a?(Journal)
479 553 issue = Issue.find(journal.issue.id)
480 554 assert_equal User.find_by_login('jsmith'), journal.user
481 555 assert_equal Issue.find(2), journal.journalized
482 556 assert_match /This is reply/, journal.notes
483 557 assert_equal 'Feature request', journal.issue.tracker.name
484 558 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
485 559 assert_equal '2010-01-01', issue.start_date.to_s
486 560 assert_equal '2010-12-31', issue.due_date.to_s
487 561 assert_equal User.find_by_login('jsmith'), issue.assigned_to
488 562 assert_equal "52.6", issue.custom_value_for(CustomField.find_by_name('Float field')).value
489 563 # keywords should be removed from the email body
490 564 assert !journal.notes.match(/^Status:/i)
491 565 assert !journal.notes.match(/^Start Date:/i)
492 566 end
493 567
494 568 def test_update_issue_with_attachment
495 569 assert_difference 'Journal.count' do
496 570 assert_difference 'JournalDetail.count' do
497 571 assert_difference 'Attachment.count' do
498 572 assert_no_difference 'Issue.count' do
499 573 journal = submit_email('ticket_with_attachment.eml') do |raw|
500 574 raw.gsub! /^Subject: .*$/, 'Subject: Re: [Cookbook - Feature #2] (New) Add ingredients categories'
501 575 end
502 576 end
503 577 end
504 578 end
505 579 end
506 580 journal = Journal.first(:order => 'id DESC')
507 581 assert_equal Issue.find(2), journal.journalized
508 582 assert_equal 1, journal.details.size
509 583
510 584 detail = journal.details.first
511 585 assert_equal 'attachment', detail.property
512 586 assert_equal 'Paella.jpg', detail.value
513 587 end
514 588
515 589 def test_update_issue_should_send_email_notification
516 590 ActionMailer::Base.deliveries.clear
517 591 journal = submit_email('ticket_reply.eml')
518 592 assert journal.is_a?(Journal)
519 593 assert_equal 1, ActionMailer::Base.deliveries.size
520 594 end
521 595
522 596 def test_update_issue_should_not_set_defaults
523 597 journal = submit_email(
524 598 'ticket_reply.eml',
525 599 :issue => {:tracker => 'Support request', :priority => 'High'}
526 600 )
527 601 assert journal.is_a?(Journal)
528 602 assert_match /This is reply/, journal.notes
529 603 assert_equal 'Feature request', journal.issue.tracker.name
530 604 assert_equal 'Normal', journal.issue.priority.name
531 605 end
532 606
533 607 def test_replying_to_a_private_note_should_add_reply_as_private
534 608 private_journal = Journal.create!(:notes => 'Private notes', :journalized => Issue.find(1), :private_notes => true, :user_id => 2)
535 609
536 610 assert_difference 'Journal.count' do
537 611 journal = submit_email('ticket_reply.eml') do |email|
538 612 email.sub! %r{^In-Reply-To:.*$}, "In-Reply-To: <redmine.journal-#{private_journal.id}.20060719210421@osiris>"
539 613 end
540 614
541 615 assert_kind_of Journal, journal
542 616 assert_match /This is reply/, journal.notes
543 617 assert_equal true, journal.private_notes
544 618 end
545 619 end
546 620
547 621 def test_reply_to_a_message
548 622 m = submit_email('message_reply.eml')
549 623 assert m.is_a?(Message)
550 624 assert !m.new_record?
551 625 m.reload
552 626 assert_equal 'Reply via email', m.subject
553 627 # The email replies to message #2 which is part of the thread of message #1
554 628 assert_equal Message.find(1), m.parent
555 629 end
556 630
557 631 def test_reply_to_a_message_by_subject
558 632 m = submit_email('message_reply_by_subject.eml')
559 633 assert m.is_a?(Message)
560 634 assert !m.new_record?
561 635 m.reload
562 636 assert_equal 'Reply to the first post', m.subject
563 637 assert_equal Message.find(1), m.parent
564 638 end
565 639
566 640 def test_should_strip_tags_of_html_only_emails
567 641 issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'})
568 642 assert issue.is_a?(Issue)
569 643 assert !issue.new_record?
570 644 issue.reload
571 645 assert_equal 'HTML email', issue.subject
572 646 assert_equal 'This is a html-only email.', issue.description
573 647 end
574 648
575 649 context "truncate emails based on the Setting" do
576 650 context "with no setting" do
577 651 setup do
578 652 Setting.mail_handler_body_delimiters = ''
579 653 end
580 654
581 655 should "add the entire email into the issue" do
582 656 issue = submit_email('ticket_on_given_project.eml')
583 657 assert_issue_created(issue)
584 658 assert issue.description.include?('---')
585 659 assert issue.description.include?('This paragraph is after the delimiter')
586 660 end
587 661 end
588 662
589 663 context "with a single string" do
590 664 setup do
591 665 Setting.mail_handler_body_delimiters = '---'
592 666 end
593 667 should "truncate the email at the delimiter for the issue" do
594 668 issue = submit_email('ticket_on_given_project.eml')
595 669 assert_issue_created(issue)
596 670 assert issue.description.include?('This paragraph is before delimiters')
597 671 assert issue.description.include?('--- This line starts with a delimiter')
598 672 assert !issue.description.match(/^---$/)
599 673 assert !issue.description.include?('This paragraph is after the delimiter')
600 674 end
601 675 end
602 676
603 677 context "with a single quoted reply (e.g. reply to a Redmine email notification)" do
604 678 setup do
605 679 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
606 680 end
607 681 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
608 682 journal = submit_email('issue_update_with_quoted_reply_above.eml')
609 683 assert journal.is_a?(Journal)
610 684 assert journal.notes.include?('An update to the issue by the sender.')
611 685 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
612 686 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
613 687 end
614 688 end
615 689
616 690 context "with multiple quoted replies (e.g. reply to a reply of a Redmine email notification)" do
617 691 setup do
618 692 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
619 693 end
620 694 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
621 695 journal = submit_email('issue_update_with_multiple_quoted_reply_above.eml')
622 696 assert journal.is_a?(Journal)
623 697 assert journal.notes.include?('An update to the issue by the sender.')
624 698 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
625 699 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
626 700 end
627 701 end
628 702
629 703 context "with multiple strings" do
630 704 setup do
631 705 Setting.mail_handler_body_delimiters = "---\nBREAK"
632 706 end
633 707 should "truncate the email at the first delimiter found (BREAK)" do
634 708 issue = submit_email('ticket_on_given_project.eml')
635 709 assert_issue_created(issue)
636 710 assert issue.description.include?('This paragraph is before delimiters')
637 711 assert !issue.description.include?('BREAK')
638 712 assert !issue.description.include?('This paragraph is between delimiters')
639 713 assert !issue.description.match(/^---$/)
640 714 assert !issue.description.include?('This paragraph is after the delimiter')
641 715 end
642 716 end
643 717 end
644 718
645 719 def test_email_with_long_subject_line
646 720 issue = submit_email('ticket_with_long_subject.eml')
647 721 assert issue.is_a?(Issue)
648 722 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]
649 723 end
650 724
651 725 def test_new_user_from_attributes_should_return_valid_user
652 726 to_test = {
653 727 # [address, name] => [login, firstname, lastname]
654 728 ['jsmith@example.net', nil] => ['jsmith@example.net', 'jsmith', '-'],
655 729 ['jsmith@example.net', 'John'] => ['jsmith@example.net', 'John', '-'],
656 730 ['jsmith@example.net', 'John Smith'] => ['jsmith@example.net', 'John', 'Smith'],
657 731 ['jsmith@example.net', 'John Paul Smith'] => ['jsmith@example.net', 'John', 'Paul Smith'],
658 732 ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsTheMaximumLength Smith'] => ['jsmith@example.net', 'AVeryLongFirstnameThatExceedsT', 'Smith'],
659 733 ['jsmith@example.net', 'John AVeryLongLastnameThatExceedsTheMaximumLength'] => ['jsmith@example.net', 'John', 'AVeryLongLastnameThatExceedsTh']
660 734 }
661 735
662 736 to_test.each do |attrs, expected|
663 737 user = MailHandler.new_user_from_attributes(attrs.first, attrs.last)
664 738
665 739 assert user.valid?, user.errors.full_messages.to_s
666 740 assert_equal attrs.first, user.mail
667 741 assert_equal expected[0], user.login
668 742 assert_equal expected[1], user.firstname
669 743 assert_equal expected[2], user.lastname
670 744 end
671 745 end
672 746
673 747 def test_new_user_from_attributes_should_respect_minimum_password_length
674 748 with_settings :password_min_length => 15 do
675 749 user = MailHandler.new_user_from_attributes('jsmith@example.net')
676 750 assert user.valid?
677 751 assert user.password.length >= 15
678 752 end
679 753 end
680 754
681 755 def test_new_user_from_attributes_should_use_default_login_if_invalid
682 756 user = MailHandler.new_user_from_attributes('foo+bar@example.net')
683 757 assert user.valid?
684 758 assert user.login =~ /^user[a-f0-9]+$/
685 759 assert_equal 'foo+bar@example.net', user.mail
686 760 end
687 761
688 762 def test_new_user_with_utf8_encoded_fullname_should_be_decoded
689 763 assert_difference 'User.count' do
690 764 issue = submit_email(
691 765 'fullname_of_sender_as_utf8_encoded.eml',
692 766 :issue => {:project => 'ecookbook'},
693 767 :unknown_user => 'create'
694 768 )
695 769 end
696 770
697 771 user = User.first(:order => 'id DESC')
698 772 assert_equal "foo@example.org", user.mail
699 773 str1 = "\xc3\x84\xc3\xa4"
700 774 str2 = "\xc3\x96\xc3\xb6"
701 775 str1.force_encoding('UTF-8') if str1.respond_to?(:force_encoding)
702 776 str2.force_encoding('UTF-8') if str2.respond_to?(:force_encoding)
703 777 assert_equal str1, user.firstname
704 778 assert_equal str2, user.lastname
705 779 end
706 780
707 781 private
708 782
709 783 def submit_email(filename, options={})
710 784 raw = IO.read(File.join(FIXTURES_PATH, filename))
711 785 yield raw if block_given?
712 786 MailHandler.receive(raw, options)
713 787 end
714 788
715 789 def assert_issue_created(issue)
716 790 assert issue.is_a?(Issue)
717 791 assert !issue.new_record?
718 792 issue.reload
719 793 end
720 794 end
General Comments 0
You need to be logged in to leave comments. Login now