annotate test/unit/mail_handler_test.rb @ 887:f210ac4c5b05 bug_89

Close obsolete branch bug_89
author Chris Cannam
date Wed, 16 Mar 2011 13:44:25 +0000
parents 94944d00e43c
children af80e5618e9b
rev   line source
Chris@0 1 # encoding: utf-8
Chris@0 2 #
Chris@0 3 # Redmine - project management software
Chris@0 4 # Copyright (C) 2006-2009 Jean-Philippe Lang
Chris@0 5 #
Chris@0 6 # This program is free software; you can redistribute it and/or
Chris@0 7 # modify it under the terms of the GNU General Public License
Chris@0 8 # as published by the Free Software Foundation; either version 2
Chris@0 9 # of the License, or (at your option) any later version.
Chris@0 10 #
Chris@0 11 # This program is distributed in the hope that it will be useful,
Chris@0 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
Chris@0 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Chris@0 14 # GNU General Public License for more details.
Chris@0 15 #
Chris@0 16 # You should have received a copy of the GNU General Public License
Chris@0 17 # along with this program; if not, write to the Free Software
Chris@0 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
Chris@0 19
Chris@0 20 require File.dirname(__FILE__) + '/../test_helper'
Chris@0 21
Chris@0 22 class MailHandlerTest < ActiveSupport::TestCase
Chris@0 23 fixtures :users, :projects,
Chris@0 24 :enabled_modules,
Chris@0 25 :roles,
Chris@0 26 :members,
Chris@0 27 :member_roles,
chris@37 28 :users,
Chris@0 29 :issues,
Chris@0 30 :issue_statuses,
Chris@0 31 :workflows,
Chris@0 32 :trackers,
Chris@0 33 :projects_trackers,
chris@37 34 :versions,
Chris@0 35 :enumerations,
Chris@0 36 :issue_categories,
Chris@0 37 :custom_fields,
Chris@0 38 :custom_fields_trackers,
Chris@0 39 :boards,
Chris@0 40 :messages
Chris@0 41
Chris@0 42 FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures/mail_handler'
Chris@0 43
Chris@0 44 def setup
Chris@0 45 ActionMailer::Base.deliveries.clear
chris@37 46 Setting.notified_events = Redmine::Notifiable.all.collect(&:name)
Chris@0 47 end
Chris@0 48
Chris@0 49 def test_add_issue
Chris@0 50 ActionMailer::Base.deliveries.clear
Chris@0 51 # This email contains: 'Project: onlinestore'
Chris@0 52 issue = submit_email('ticket_on_given_project.eml')
Chris@0 53 assert issue.is_a?(Issue)
Chris@0 54 assert !issue.new_record?
Chris@0 55 issue.reload
Chris@0 56 assert_equal 'New ticket on a given project', issue.subject
Chris@0 57 assert_equal User.find_by_login('jsmith'), issue.author
Chris@0 58 assert_equal Project.find(2), issue.project
Chris@0 59 assert_equal IssueStatus.find_by_name('Resolved'), issue.status
Chris@0 60 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
Chris@0 61 assert_equal '2010-01-01', issue.start_date.to_s
Chris@0 62 assert_equal '2010-12-31', issue.due_date.to_s
Chris@0 63 assert_equal User.find_by_login('jsmith'), issue.assigned_to
chris@37 64 assert_equal Version.find_by_name('alpha'), issue.fixed_version
chris@37 65 assert_equal 2.5, issue.estimated_hours
chris@37 66 assert_equal 30, issue.done_ratio
Chris@0 67 # keywords should be removed from the email body
Chris@0 68 assert !issue.description.match(/^Project:/i)
Chris@0 69 assert !issue.description.match(/^Status:/i)
Chris@0 70 # Email notification should be sent
Chris@0 71 mail = ActionMailer::Base.deliveries.last
Chris@0 72 assert_not_nil mail
Chris@0 73 assert mail.subject.include?('New ticket on a given project')
Chris@0 74 end
Chris@0 75
Chris@0 76 def test_add_issue_with_status
Chris@0 77 # This email contains: 'Project: onlinestore' and 'Status: Resolved'
Chris@0 78 issue = submit_email('ticket_on_given_project.eml')
Chris@0 79 assert issue.is_a?(Issue)
Chris@0 80 assert !issue.new_record?
Chris@0 81 issue.reload
Chris@0 82 assert_equal Project.find(2), issue.project
Chris@0 83 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
Chris@0 84 end
Chris@0 85
Chris@0 86 def test_add_issue_with_attributes_override
Chris@0 87 issue = submit_email('ticket_with_attributes.eml', :allow_override => 'tracker,category,priority')
Chris@0 88 assert issue.is_a?(Issue)
Chris@0 89 assert !issue.new_record?
Chris@0 90 issue.reload
Chris@0 91 assert_equal 'New ticket on a given project', issue.subject
Chris@0 92 assert_equal User.find_by_login('jsmith'), issue.author
Chris@0 93 assert_equal Project.find(2), issue.project
Chris@0 94 assert_equal 'Feature request', issue.tracker.to_s
Chris@0 95 assert_equal 'Stock management', issue.category.to_s
Chris@0 96 assert_equal 'Urgent', issue.priority.to_s
Chris@0 97 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
Chris@0 98 end
Chris@0 99
Chris@0 100 def test_add_issue_with_partial_attributes_override
Chris@0 101 issue = submit_email('ticket_with_attributes.eml', :issue => {:priority => 'High'}, :allow_override => ['tracker'])
Chris@0 102 assert issue.is_a?(Issue)
Chris@0 103 assert !issue.new_record?
Chris@0 104 issue.reload
Chris@0 105 assert_equal 'New ticket on a given project', issue.subject
Chris@0 106 assert_equal User.find_by_login('jsmith'), issue.author
Chris@0 107 assert_equal Project.find(2), issue.project
Chris@0 108 assert_equal 'Feature request', issue.tracker.to_s
Chris@0 109 assert_nil issue.category
Chris@0 110 assert_equal 'High', issue.priority.to_s
Chris@0 111 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
Chris@0 112 end
Chris@0 113
Chris@0 114 def test_add_issue_with_spaces_between_attribute_and_separator
Chris@0 115 issue = submit_email('ticket_with_spaces_between_attribute_and_separator.eml', :allow_override => 'tracker,category,priority')
Chris@0 116 assert issue.is_a?(Issue)
Chris@0 117 assert !issue.new_record?
Chris@0 118 issue.reload
Chris@0 119 assert_equal 'New ticket on a given project', issue.subject
Chris@0 120 assert_equal User.find_by_login('jsmith'), issue.author
Chris@0 121 assert_equal Project.find(2), issue.project
Chris@0 122 assert_equal 'Feature request', issue.tracker.to_s
Chris@0 123 assert_equal 'Stock management', issue.category.to_s
Chris@0 124 assert_equal 'Urgent', issue.priority.to_s
Chris@0 125 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
Chris@0 126 end
Chris@0 127
Chris@0 128
Chris@0 129 def test_add_issue_with_attachment_to_specific_project
Chris@0 130 issue = submit_email('ticket_with_attachment.eml', :issue => {:project => 'onlinestore'})
Chris@0 131 assert issue.is_a?(Issue)
Chris@0 132 assert !issue.new_record?
Chris@0 133 issue.reload
Chris@0 134 assert_equal 'Ticket created by email with attachment', issue.subject
Chris@0 135 assert_equal User.find_by_login('jsmith'), issue.author
Chris@0 136 assert_equal Project.find(2), issue.project
Chris@0 137 assert_equal 'This is a new ticket with attachments', issue.description
Chris@0 138 # Attachment properties
Chris@0 139 assert_equal 1, issue.attachments.size
Chris@0 140 assert_equal 'Paella.jpg', issue.attachments.first.filename
Chris@0 141 assert_equal 'image/jpeg', issue.attachments.first.content_type
Chris@0 142 assert_equal 10790, issue.attachments.first.filesize
Chris@0 143 end
Chris@0 144
Chris@0 145 def test_add_issue_with_custom_fields
Chris@0 146 issue = submit_email('ticket_with_custom_fields.eml', :issue => {:project => 'onlinestore'})
Chris@0 147 assert issue.is_a?(Issue)
Chris@0 148 assert !issue.new_record?
Chris@0 149 issue.reload
Chris@0 150 assert_equal 'New ticket with custom field values', issue.subject
Chris@0 151 assert_equal 'Value for a custom field', issue.custom_value_for(CustomField.find_by_name('Searchable field')).value
Chris@0 152 assert !issue.description.match(/^searchable field:/i)
Chris@0 153 end
Chris@0 154
Chris@0 155 def test_add_issue_with_cc
Chris@0 156 issue = submit_email('ticket_with_cc.eml', :issue => {:project => 'ecookbook'})
Chris@0 157 assert issue.is_a?(Issue)
Chris@0 158 assert !issue.new_record?
Chris@0 159 issue.reload
Chris@0 160 assert issue.watched_by?(User.find_by_mail('dlopper@somenet.foo'))
chris@37 161 assert_equal 1, issue.watcher_user_ids.size
Chris@0 162 end
Chris@0 163
Chris@0 164 def test_add_issue_by_unknown_user
Chris@0 165 assert_no_difference 'User.count' do
Chris@0 166 assert_equal false, submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'})
Chris@0 167 end
Chris@0 168 end
Chris@0 169
Chris@0 170 def test_add_issue_by_anonymous_user
Chris@0 171 Role.anonymous.add_permission!(:add_issues)
Chris@0 172 assert_no_difference 'User.count' do
Chris@0 173 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'accept')
Chris@0 174 assert issue.is_a?(Issue)
Chris@0 175 assert issue.author.anonymous?
Chris@0 176 end
Chris@0 177 end
Chris@0 178
Chris@0 179 def test_add_issue_by_anonymous_user_with_no_from_address
Chris@0 180 Role.anonymous.add_permission!(:add_issues)
Chris@0 181 assert_no_difference 'User.count' do
Chris@0 182 issue = submit_email('ticket_by_empty_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'accept')
Chris@0 183 assert issue.is_a?(Issue)
Chris@0 184 assert issue.author.anonymous?
Chris@0 185 end
Chris@0 186 end
Chris@0 187
Chris@0 188 def test_add_issue_by_anonymous_user_on_private_project
Chris@0 189 Role.anonymous.add_permission!(:add_issues)
Chris@0 190 assert_no_difference 'User.count' do
Chris@0 191 assert_no_difference 'Issue.count' do
Chris@0 192 assert_equal false, submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'onlinestore'}, :unknown_user => 'accept')
Chris@0 193 end
Chris@0 194 end
Chris@0 195 end
Chris@0 196
Chris@0 197 def test_add_issue_by_anonymous_user_on_private_project_without_permission_check
Chris@0 198 assert_no_difference 'User.count' do
Chris@0 199 assert_difference 'Issue.count' do
Chris@0 200 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'onlinestore'}, :no_permission_check => '1', :unknown_user => 'accept')
Chris@0 201 assert issue.is_a?(Issue)
Chris@0 202 assert issue.author.anonymous?
Chris@0 203 assert !issue.project.is_public?
Chris@0 204 end
Chris@0 205 end
Chris@0 206 end
Chris@0 207
Chris@0 208 def test_add_issue_by_created_user
Chris@0 209 Setting.default_language = 'en'
Chris@0 210 assert_difference 'User.count' do
Chris@0 211 issue = submit_email('ticket_by_unknown_user.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'create')
Chris@0 212 assert issue.is_a?(Issue)
Chris@0 213 assert issue.author.active?
Chris@0 214 assert_equal 'john.doe@somenet.foo', issue.author.mail
Chris@0 215 assert_equal 'John', issue.author.firstname
Chris@0 216 assert_equal 'Doe', issue.author.lastname
Chris@0 217
Chris@0 218 # account information
Chris@0 219 email = ActionMailer::Base.deliveries.first
Chris@0 220 assert_not_nil email
Chris@0 221 assert email.subject.include?('account activation')
Chris@0 222 login = email.body.match(/\* Login: (.*)$/)[1]
Chris@0 223 password = email.body.match(/\* Password: (.*)$/)[1]
Chris@0 224 assert_equal issue.author, User.try_to_login(login, password)
Chris@0 225 end
Chris@0 226 end
Chris@0 227
Chris@0 228 def test_add_issue_without_from_header
Chris@0 229 Role.anonymous.add_permission!(:add_issues)
Chris@0 230 assert_equal false, submit_email('ticket_without_from_header.eml')
Chris@0 231 end
chris@37 232
chris@37 233 def test_add_issue_with_invalid_attributes
chris@37 234 issue = submit_email('ticket_with_invalid_attributes.eml', :allow_override => 'tracker,category,priority')
chris@37 235 assert issue.is_a?(Issue)
chris@37 236 assert !issue.new_record?
chris@37 237 issue.reload
chris@37 238 assert_nil issue.assigned_to
chris@37 239 assert_nil issue.start_date
chris@37 240 assert_nil issue.due_date
chris@37 241 assert_equal 0, issue.done_ratio
chris@37 242 assert_equal 'Normal', issue.priority.to_s
chris@37 243 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
chris@37 244 end
chris@37 245
chris@37 246 def test_add_issue_with_localized_attributes
chris@37 247 User.find_by_mail('jsmith@somenet.foo').update_attribute 'language', 'fr'
chris@37 248 issue = submit_email('ticket_with_localized_attributes.eml', :allow_override => 'tracker,category,priority')
chris@37 249 assert issue.is_a?(Issue)
chris@37 250 assert !issue.new_record?
chris@37 251 issue.reload
chris@37 252 assert_equal 'New ticket on a given project', issue.subject
chris@37 253 assert_equal User.find_by_login('jsmith'), issue.author
chris@37 254 assert_equal Project.find(2), issue.project
chris@37 255 assert_equal 'Feature request', issue.tracker.to_s
chris@37 256 assert_equal 'Stock management', issue.category.to_s
chris@37 257 assert_equal 'Urgent', issue.priority.to_s
chris@37 258 assert issue.description.include?('Lorem ipsum dolor sit amet, consectetuer adipiscing elit.')
chris@37 259 end
Chris@0 260
Chris@0 261 def test_add_issue_with_japanese_keywords
Chris@0 262 tracker = Tracker.create!(:name => '開発')
Chris@0 263 Project.find(1).trackers << tracker
Chris@0 264 issue = submit_email('japanese_keywords_iso_2022_jp.eml', :issue => {:project => 'ecookbook'}, :allow_override => 'tracker')
Chris@0 265 assert_kind_of Issue, issue
Chris@0 266 assert_equal tracker, issue.tracker
Chris@0 267 end
Chris@0 268
Chris@0 269 def test_should_ignore_emails_from_emission_address
Chris@0 270 Role.anonymous.add_permission!(:add_issues)
Chris@0 271 assert_no_difference 'User.count' do
Chris@0 272 assert_equal false, submit_email('ticket_from_emission_address.eml', :issue => {:project => 'ecookbook'}, :unknown_user => 'create')
Chris@0 273 end
Chris@0 274 end
Chris@0 275
Chris@0 276 def test_add_issue_should_send_email_notification
chris@37 277 Setting.notified_events = ['issue_added']
Chris@0 278 ActionMailer::Base.deliveries.clear
Chris@0 279 # This email contains: 'Project: onlinestore'
Chris@0 280 issue = submit_email('ticket_on_given_project.eml')
Chris@0 281 assert issue.is_a?(Issue)
Chris@0 282 assert_equal 1, ActionMailer::Base.deliveries.size
Chris@0 283 end
Chris@0 284
Chris@0 285 def test_add_issue_note
Chris@0 286 journal = submit_email('ticket_reply.eml')
Chris@0 287 assert journal.is_a?(Journal)
Chris@0 288 assert_equal User.find_by_login('jsmith'), journal.user
Chris@0 289 assert_equal Issue.find(2), journal.journalized
Chris@0 290 assert_match /This is reply/, journal.notes
Chris@0 291 end
Chris@0 292
Chris@0 293 def test_add_issue_note_with_attribute_changes
Chris@0 294 # This email contains: 'Status: Resolved'
Chris@0 295 journal = submit_email('ticket_reply_with_status.eml')
Chris@0 296 assert journal.is_a?(Journal)
Chris@0 297 issue = Issue.find(journal.issue.id)
Chris@0 298 assert_equal User.find_by_login('jsmith'), journal.user
Chris@0 299 assert_equal Issue.find(2), journal.journalized
Chris@0 300 assert_match /This is reply/, journal.notes
Chris@0 301 assert_equal IssueStatus.find_by_name("Resolved"), issue.status
Chris@0 302 assert_equal '2010-01-01', issue.start_date.to_s
Chris@0 303 assert_equal '2010-12-31', issue.due_date.to_s
Chris@0 304 assert_equal User.find_by_login('jsmith'), issue.assigned_to
chris@37 305 assert_equal 'Updated custom value', issue.custom_value_for(CustomField.find_by_name('Searchable field')).value
Chris@0 306 end
Chris@0 307
Chris@0 308 def test_add_issue_note_should_send_email_notification
Chris@0 309 ActionMailer::Base.deliveries.clear
Chris@0 310 journal = submit_email('ticket_reply.eml')
Chris@0 311 assert journal.is_a?(Journal)
Chris@0 312 assert_equal 1, ActionMailer::Base.deliveries.size
Chris@0 313 end
Chris@0 314
Chris@0 315 def test_reply_to_a_message
Chris@0 316 m = submit_email('message_reply.eml')
Chris@0 317 assert m.is_a?(Message)
Chris@0 318 assert !m.new_record?
Chris@0 319 m.reload
Chris@0 320 assert_equal 'Reply via email', m.subject
Chris@0 321 # The email replies to message #2 which is part of the thread of message #1
Chris@0 322 assert_equal Message.find(1), m.parent
Chris@0 323 end
Chris@0 324
Chris@0 325 def test_reply_to_a_message_by_subject
Chris@0 326 m = submit_email('message_reply_by_subject.eml')
Chris@0 327 assert m.is_a?(Message)
Chris@0 328 assert !m.new_record?
Chris@0 329 m.reload
Chris@0 330 assert_equal 'Reply to the first post', m.subject
Chris@0 331 assert_equal Message.find(1), m.parent
Chris@0 332 end
Chris@0 333
Chris@0 334 def test_should_strip_tags_of_html_only_emails
Chris@0 335 issue = submit_email('ticket_html_only.eml', :issue => {:project => 'ecookbook'})
Chris@0 336 assert issue.is_a?(Issue)
Chris@0 337 assert !issue.new_record?
Chris@0 338 issue.reload
Chris@0 339 assert_equal 'HTML email', issue.subject
Chris@0 340 assert_equal 'This is a html-only email.', issue.description
Chris@0 341 end
Chris@0 342
Chris@0 343 context "truncate emails based on the Setting" do
Chris@0 344 context "with no setting" do
Chris@0 345 setup do
Chris@0 346 Setting.mail_handler_body_delimiters = ''
Chris@0 347 end
Chris@0 348
Chris@0 349 should "add the entire email into the issue" do
Chris@0 350 issue = submit_email('ticket_on_given_project.eml')
Chris@0 351 assert_issue_created(issue)
Chris@0 352 assert issue.description.include?('---')
Chris@0 353 assert issue.description.include?('This paragraph is after the delimiter')
Chris@0 354 end
Chris@0 355 end
Chris@0 356
Chris@0 357 context "with a single string" do
Chris@0 358 setup do
Chris@0 359 Setting.mail_handler_body_delimiters = '---'
Chris@0 360 end
Chris@0 361
Chris@0 362 should "truncate the email at the delimiter for the issue" do
Chris@0 363 issue = submit_email('ticket_on_given_project.eml')
Chris@0 364 assert_issue_created(issue)
Chris@0 365 assert issue.description.include?('This paragraph is before delimiters')
Chris@0 366 assert issue.description.include?('--- This line starts with a delimiter')
Chris@0 367 assert !issue.description.match(/^---$/)
Chris@0 368 assert !issue.description.include?('This paragraph is after the delimiter')
Chris@0 369 end
Chris@0 370 end
Chris@0 371
chris@37 372 context "with a single quoted reply (e.g. reply to a Redmine email notification)" do
chris@37 373 setup do
chris@37 374 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
chris@37 375 end
chris@37 376
chris@37 377 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
chris@37 378 journal = submit_email('issue_update_with_quoted_reply_above.eml')
chris@37 379 assert journal.is_a?(Journal)
chris@37 380 assert journal.notes.include?('An update to the issue by the sender.')
chris@37 381 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
chris@37 382 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
chris@37 383
chris@37 384 end
chris@37 385
chris@37 386 end
chris@37 387
chris@37 388 context "with multiple quoted replies (e.g. reply to a reply of a Redmine email notification)" do
chris@37 389 setup do
chris@37 390 Setting.mail_handler_body_delimiters = '--- Reply above. Do not remove this line. ---'
chris@37 391 end
chris@37 392
chris@37 393 should "truncate the email at the delimiter with the quoted reply symbols (>)" do
chris@37 394 journal = submit_email('issue_update_with_multiple_quoted_reply_above.eml')
chris@37 395 assert journal.is_a?(Journal)
chris@37 396 assert journal.notes.include?('An update to the issue by the sender.')
chris@37 397 assert !journal.notes.match(Regexp.escape("--- Reply above. Do not remove this line. ---"))
chris@37 398 assert !journal.notes.include?('Looks like the JSON api for projects was missed.')
chris@37 399
chris@37 400 end
chris@37 401
chris@37 402 end
chris@37 403
Chris@0 404 context "with multiple strings" do
Chris@0 405 setup do
Chris@0 406 Setting.mail_handler_body_delimiters = "---\nBREAK"
Chris@0 407 end
Chris@0 408
Chris@0 409 should "truncate the email at the first delimiter found (BREAK)" do
Chris@0 410 issue = submit_email('ticket_on_given_project.eml')
Chris@0 411 assert_issue_created(issue)
Chris@0 412 assert issue.description.include?('This paragraph is before delimiters')
Chris@0 413 assert !issue.description.include?('BREAK')
Chris@0 414 assert !issue.description.include?('This paragraph is between delimiters')
Chris@0 415 assert !issue.description.match(/^---$/)
Chris@0 416 assert !issue.description.include?('This paragraph is after the delimiter')
Chris@0 417 end
Chris@0 418 end
Chris@0 419 end
Chris@0 420
Chris@0 421 def test_email_with_long_subject_line
Chris@0 422 issue = submit_email('ticket_with_long_subject.eml')
Chris@0 423 assert issue.is_a?(Issue)
Chris@0 424 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]
Chris@0 425 end
Chris@0 426
Chris@0 427 private
Chris@0 428
Chris@0 429 def submit_email(filename, options={})
Chris@0 430 raw = IO.read(File.join(FIXTURES_PATH, filename))
Chris@0 431 MailHandler.receive(raw, options)
Chris@0 432 end
Chris@0 433
Chris@0 434 def assert_issue_created(issue)
Chris@0 435 assert issue.is_a?(Issue)
Chris@0 436 assert !issue.new_record?
Chris@0 437 issue.reload
Chris@0 438 end
Chris@0 439 end