summaryrefslogtreecommitdiff
path: root/rt/t
diff options
context:
space:
mode:
Diffstat (limited to 'rt/t')
-rw-r--r--rt/t/00-compile.t58
-rw-r--r--rt/t/00-mason-syntax.t44
-rw-r--r--rt/t/api/ace.t238
-rw-r--r--rt/t/api/action-createtickets.t240
-rw-r--r--rt/t/api/attachment.t45
-rw-r--r--rt/t/api/attribute-tests.t86
-rw-r--r--rt/t/api/attribute.t42
-rw-r--r--rt/t/api/cf.t224
-rw-r--r--rt/t/api/cf_combo_casacade.t46
-rw-r--r--rt/t/api/cf_external.t56
-rw-r--r--rt/t/api/cf_pattern.t53
-rw-r--r--rt/t/api/cf_single_values.t38
-rw-r--r--rt/t/api/cf_transaction.t60
-rw-r--r--rt/t/api/condition-ownerchange.t51
-rw-r--r--rt/t/api/condition-reject.t45
-rw-r--r--rt/t/api/currentuser.t32
-rw-r--r--rt/t/api/customfield.t74
-rw-r--r--rt/t/api/date.t564
-rw-r--r--rt/t/api/emailparser.t32
-rw-r--r--rt/t/api/group.t99
-rw-r--r--rt/t/api/groups.t139
-rw-r--r--rt/t/api/i18n.t30
-rw-r--r--rt/t/api/link.t24
-rw-r--r--rt/t/api/queue.t92
-rw-r--r--rt/t/api/record.t70
-rw-r--r--rt/t/api/reminders.t88
-rw-r--r--rt/t/api/rights.t142
-rw-r--r--rt/t/api/rt.t18
-rw-r--r--rt/t/api/scrip.t49
-rw-r--r--rt/t/api/scrip_order.t56
-rw-r--r--rt/t/api/searchbuilder.t40
-rw-r--r--rt/t/api/system.t33
-rw-r--r--rt/t/api/template-insert.t26
-rw-r--r--rt/t/api/template.t26
-rw-r--r--rt/t/api/ticket.t257
-rw-r--r--rt/t/api/tickets.t104
-rw-r--r--rt/t/api/tickets_overlay_sql.t73
-rw-r--r--rt/t/api/uri-fsck_com_rt.t28
-rw-r--r--rt/t/api/uri-t.t21
-rw-r--r--rt/t/api/user.t339
-rw-r--r--rt/t/api/users.t80
-rw-r--r--rt/t/approval/basic.t218
-rw-r--r--rt/t/clicky.t119
-rw-r--r--rt/t/cron.t90
-rw-r--r--rt/t/customfields/access_via_queue.t160
-rw-r--r--rt/t/customfields/sort_order.t92
-rw-r--r--rt/t/data/configs/apache2.2+fastcgi.conf44
-rw-r--r--rt/t/data/configs/apache2.2+fastcgi.conf.in44
-rw-r--r--rt/t/data/configs/apache2.2+mod_perl.conf40
-rw-r--r--rt/t/data/configs/apache2.2+mod_perl.conf.in40
-rwxr-xr-xrt/t/data/emails/8859-15-message-series/dir356
-rwxr-xr-xrt/t/data/emails/8859-15-message-series/msg136
-rwxr-xr-xrt/t/data/emails/8859-15-message-series/msg236
-rwxr-xr-xrt/t/data/emails/8859-15-message-series/msg335
-rwxr-xr-xrt/t/data/emails/8859-15-message-series/msg435
-rwxr-xr-xrt/t/data/emails/8859-15-message-series/msg535
-rwxr-xr-xrt/t/data/emails/8859-15-message-series/msg635
-rwxr-xr-xrt/t/data/emails/8859-15-message-series/msg736
-rwxr-xr-xrt/t/data/emails/crashes-file-based-parser193
-rw-r--r--rt/t/data/emails/lorem-ipsum5
-rwxr-xr-xrt/t/data/emails/multipart-alternative-with-umlaut62
-rwxr-xr-xrt/t/data/emails/multipart-report66
-rwxr-xr-xrt/t/data/emails/nested-mime-sample396
-rwxr-xr-xrt/t/data/emails/nested-rfc-822253
-rwxr-xr-xrt/t/data/emails/new-ticket-from-iso-8859-131
-rwxr-xr-xrt/t/data/emails/new-ticket-from-iso-8859-1-full38
-rwxr-xr-xrt/t/data/emails/notes-uuencoded2368
-rw-r--r--rt/t/data/emails/rt-send-cc5
-rwxr-xr-xrt/t/data/emails/russian-subject-no-content-type42
-rw-r--r--rt/t/data/emails/subject-with-folding-ws10
-rwxr-xr-xrt/t/data/emails/text-html-in-russian87
-rwxr-xr-xrt/t/data/emails/text-html-with-umlaut35
-rw-r--r--rt/t/data/emails/very-long-subject12
-rw-r--r--rt/t/data/gnupg/emails/1-signed-MIME-plain.txt38
-rw-r--r--rt/t/data/gnupg/emails/10-encrypted-inline-plain.txt31
-rw-r--r--rt/t/data/gnupg/emails/11-encrypted-inline-attachment.txt80
-rw-r--r--rt/t/data/gnupg/emails/12-encrypted-inline-binary.txt86
-rw-r--r--rt/t/data/gnupg/emails/13-signed-encrypted-MIME-plain.txt49
-rw-r--r--rt/t/data/gnupg/emails/14-signed-encrypted-MIME-attachment.txt51
-rw-r--r--rt/t/data/gnupg/emails/15-signed-encrypted-MIME-binary.txt60
-rw-r--r--rt/t/data/gnupg/emails/16-signed-encrypted-inline-plain.txt33
-rw-r--r--rt/t/data/gnupg/emails/17-signed-encrypted-inline-attachment.txt84
-rw-r--r--rt/t/data/gnupg/emails/18-signed-encrypted-inline-binary.txt89
-rw-r--r--rt/t/data/gnupg/emails/19-signed-inline-plain-nested.txt34
-rwxr-xr-xrt/t/data/gnupg/emails/2-signed-MIME-plain-with-attachment.txt48
-rwxr-xr-xrt/t/data/gnupg/emails/3-signed-MIME-plain-with-binary.txt55
-rw-r--r--rt/t/data/gnupg/emails/4-signed-inline-plain.txt24
-rw-r--r--rt/t/data/gnupg/emails/5-signed-inline-with-attachment.txt48
-rw-r--r--rt/t/data/gnupg/emails/6-signed-inline-with-binary.txt55
-rw-r--r--rt/t/data/gnupg/emails/7-encrypted-MIME-plain.txt47
-rw-r--r--rt/t/data/gnupg/emails/8-encrypted-MIME-with-attachment.txt49
-rw-r--r--rt/t/data/gnupg/emails/9-encrypted-MIME-with-binary.txt57
-rw-r--r--rt/t/data/gnupg/emails/README28
-rw-r--r--rt/t/data/gnupg/keyrings/pubring.gpgbin0 -> 4651 bytes
-rw-r--r--rt/t/data/gnupg/keyrings/secring.gpgbin0 -> 5095 bytes
-rw-r--r--rt/t/data/gnupg/keyrings/signed_old_style_with_attachment.eml48
-rw-r--r--rt/t/data/gnupg/keyrings/trustdb.gpgbin0 -> 1520 bytes
-rw-r--r--rt/t/data/gnupg/keys/general-at-example.com.2.public.key30
-rw-r--r--rt/t/data/gnupg/keys/general-at-example.com.2.secret.key33
-rw-r--r--rt/t/data/gnupg/keys/general-at-example.com.public.key30
-rw-r--r--rt/t/data/gnupg/keys/general-at-example.com.secret.key31
-rw-r--r--rt/t/data/gnupg/keys/recipient-at-example.com.public.key30
-rw-r--r--rt/t/data/gnupg/keys/recipient-at-example.com.secret.key33
-rw-r--r--rt/t/data/gnupg/keys/rt-recipient-at-example.com.public.key30
-rw-r--r--rt/t/data/gnupg/keys/rt-recipient-at-example.com.secret.key33
-rw-r--r--rt/t/data/gnupg/keys/rt-test-at-example.com.2.public.key30
-rw-r--r--rt/t/data/gnupg/keys/rt-test-at-example.com.2.secret.key33
-rw-r--r--rt/t/data/gnupg/keys/rt-test-at-example.com.public.key30
-rw-r--r--rt/t/data/gnupg/keys/rt-test-at-example.com.secret.key33
-rw-r--r--rt/t/delegation/cleanup_stalled.t458
-rw-r--r--rt/t/delegation/revocation.t135
-rw-r--r--rt/t/i18n/default.t19
-rw-r--r--rt/t/mail/charsets-outgoing.t306
-rw-r--r--rt/t/mail/crypt-gnupg.t312
-rw-r--r--rt/t/mail/extractsubjecttag.t98
-rw-r--r--rt/t/mail/gateway.t802
-rw-r--r--rt/t/mail/gnupg-bad.t58
-rw-r--r--rt/t/mail/gnupg-incoming.t320
-rw-r--r--rt/t/mail/gnupg-realmail.t184
-rw-r--r--rt/t/mail/gnupg-reverification.t92
-rw-r--r--rt/t/mail/mime_decoding.t59
-rw-r--r--rt/t/mail/sendmail.t538
-rw-r--r--rt/t/mail/verp.t8
-rw-r--r--rt/t/maildigest/attributes.t168
-rw-r--r--rt/t/pod.t7
-rw-r--r--rt/t/rtname.t34
-rw-r--r--rt/t/savedsearch.t185
-rw-r--r--rt/t/shredder/00load.t29
-rw-r--r--rt/t/shredder/00skeleton.t25
-rw-r--r--rt/t/shredder/01basics.t32
-rw-r--r--rt/t/shredder/01ticket.t86
-rw-r--r--rt/t/shredder/02group_member.t103
-rw-r--r--rt/t/shredder/02queue.t125
-rw-r--r--rt/t/shredder/02template.t76
-rw-r--r--rt/t/shredder/02user.t62
-rw-r--r--rt/t/shredder/03plugin.t46
-rw-r--r--rt/t/shredder/03plugin_summary.t23
-rw-r--r--rt/t/shredder/03plugin_tickets.t150
-rw-r--r--rt/t/shredder/03plugin_users.t40
-rw-r--r--rt/t/shredder/utils.pl435
-rw-r--r--rt/t/ticket/action_linear_escalate.t100
-rw-r--r--rt/t/ticket/add-watchers.t167
-rw-r--r--rt/t/ticket/badlinks.t38
-rw-r--r--rt/t/ticket/batch-upload-csv.t48
-rw-r--r--rt/t/ticket/cfsort-freeform-multiple.t137
-rw-r--r--rt/t/ticket/cfsort-freeform-single.t191
-rw-r--r--rt/t/ticket/deferred_owner.t120
-rw-r--r--rt/t/ticket/link_search.t246
-rw-r--r--rt/t/ticket/linking.t385
-rw-r--r--rt/t/ticket/merge.t92
-rw-r--r--rt/t/ticket/quicksearch.t41
-rw-r--r--rt/t/ticket/requestor-order.t142
-rw-r--r--rt/t/ticket/scrips_batch.t100
-rw-r--r--rt/t/ticket/search.t278
-rw-r--r--rt/t/ticket/search_by_cf_freeform_multiple.t153
-rw-r--r--rt/t/ticket/search_by_cf_freeform_single.t142
-rw-r--r--rt/t/ticket/search_by_links.t132
-rw-r--r--rt/t/ticket/search_by_txn.t35
-rw-r--r--rt/t/ticket/search_by_watcher.t280
-rw-r--r--rt/t/ticket/search_long_cf_values.t79
-rw-r--r--rt/t/ticket/sort-by-custom-ownership.t103
-rw-r--r--rt/t/ticket/sort-by-queue.t100
-rw-r--r--rt/t/ticket/sort-by-user.t152
-rw-r--r--rt/t/ticket/sort_by_cf.t172
-rw-r--r--rt/t/validator/group_members.t178
-rw-r--r--rt/t/web/attachments.t47
-rw-r--r--rt/t/web/basic.t146
-rw-r--r--rt/t/web/cf_access.t191
-rw-r--r--rt/t/web/cf_onqueue.t66
-rw-r--r--rt/t/web/cf_select_one.t159
-rw-r--r--rt/t/web/command_line.t544
-rw-r--r--rt/t/web/command_line_with_unknown_field.t34
-rw-r--r--rt/t/web/compilation_errors.t68
-rw-r--r--rt/t/web/config_tab_right.t41
-rw-r--r--rt/t/web/crypt-gnupg.t446
-rw-r--r--rt/t/web/custom_frontpage.t61
-rw-r--r--rt/t/web/custom_search.t84
-rw-r--r--rt/t/web/dashboard_with_deleted_saved_search.t89
-rw-r--r--rt/t/web/dashboards-groups.t102
-rw-r--r--rt/t/web/dashboards-permissions.t38
-rw-r--r--rt/t/web/dashboards.t250
-rw-r--r--rt/t/web/gnupg-outgoing.t363
-rw-r--r--rt/t/web/gnupg-select-keys-on-create.t325
-rw-r--r--rt/t/web/gnupg-select-keys-on-update.t344
-rw-r--r--rt/t/web/offline_messages_utf8.t67
-rw-r--r--rt/t/web/offline_utf8.t54
-rw-r--r--rt/t/web/query_builder.t249
-rw-r--r--rt/t/web/quicksearch.t51
-rw-r--r--rt/t/web/rest-non-ascii-subject.t55
-rw-r--r--rt/t/web/rest.t71
-rw-r--r--rt/t/web/rights.t85
-rw-r--r--rt/t/web/rights1.t134
-rw-r--r--rt/t/web/saved_search_chart.t86
-rw-r--r--rt/t/web/saved_search_permissions.t34
-rw-r--r--rt/t/web/search_bulk_update_links.t147
-rw-r--r--rt/t/web/ticket-create-utf8.t83
-rw-r--r--rt/t/web/ticket_owner.t356
-rw-r--r--rt/t/web/ticket_seen.t80
-rw-r--r--rt/t/web/ticket_update_without_content.t52
-rw-r--r--rt/t/web/unlimited_search.t41
200 files changed, 24055 insertions, 0 deletions
diff --git a/rt/t/00-compile.t b/rt/t/00-compile.t
new file mode 100644
index 000000000..7257c3e8c
--- /dev/null
+++ b/rt/t/00-compile.t
@@ -0,0 +1,58 @@
+
+use strict;
+use warnings;
+
+use RT::Test nodata => 1, tests => 34;
+
+require_ok("RT");
+require_ok("RT::Test");
+require_ok("RT::ACL");
+require_ok("RT::Handle");
+require_ok("RT::Transaction");
+require_ok("RT::Interface::CLI");
+require_ok("RT::Interface::Email");
+require_ok("RT::Links");
+require_ok("RT::Queues");
+require_ok("RT::Scrips");
+require_ok("RT::Templates");
+require_ok("RT::Principals");
+require_ok("RT::Attachments");
+require_ok("RT::GroupMember");
+require_ok("RT::ScripAction");
+require_ok("RT::CustomFields");
+require_ok("RT::GroupMembers");
+require_ok("RT::ScripActions");
+require_ok("RT::Transactions");
+require_ok("RT::ScripCondition");
+require_ok("RT::Action::Generic");
+require_ok("RT::ScripConditions");
+require_ok("RT::Search::Generic");
+require_ok("RT::Search::Generic");
+require_ok("RT::Search::Generic");
+require_ok("RT::Search::Generic");
+require_ok("RT::Action::SendEmail");
+require_ok("RT::CachedGroupMembers");
+require_ok("RT::Condition::Generic");
+require_ok("RT::Interface::Web");
+require_ok("RT::SavedSearch");
+require_ok("RT::SavedSearches");
+require_ok("RT::Installer");
+require_ok("RT::Util");
+
+
+# no the following doesn't work yet
+__END__
+use File::Find::Rule;
+
+my @files = File::Find::Rule->file()
+ ->name( '*.pm' )
+ ->in( 'lib' );
+
+plan tests => scalar @files;
+
+for (@files) {
+ local $SIG{__WARN__} = sub {};
+ require_ok($_);
+}
+
+1;
diff --git a/rt/t/00-mason-syntax.t b/rt/t/00-mason-syntax.t
new file mode 100644
index 000000000..0584f630f
--- /dev/null
+++ b/rt/t/00-mason-syntax.t
@@ -0,0 +1,44 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use RT::Test tests => 1;
+
+my $ok = 1;
+
+use File::Find;
+find( {
+ no_chdir => 1,
+ wanted => sub {
+ return if /(?:\.(?:jpe?g|png|gif|rej)|\~)$/i;
+ return if m{/\.[^/]+\.swp$}; # vim swap files
+ return unless -f $_;
+ diag "testing $_" if $ENV{'TEST_VERBOSE'};
+ eval { compile_file($_) } and return;
+ $ok = 0;
+ diag "error in ${File::Find::name}:\n$@";
+ },
+}, RT::Test::get_relocatable_dir('../share/html'));
+ok($ok, "mason syntax is ok");
+
+use HTML::Mason;
+use HTML::Mason::Compiler;
+use HTML::Mason::Compiler::ToObject;
+BEGIN { require RT::Test; }
+use Encode qw(decode_utf8);
+
+sub compile_file {
+ my $file = shift;
+
+ my $text = decode_utf8(RT::Test->file_content($file));
+
+ my $compiler = new HTML::Mason::Compiler::ToObject;
+ $compiler->compile(
+ comp_source => $text,
+ name => 'my',
+ $HTML::Mason::VERSION >= 1.36? (comp_path => 'my'): (),
+ );
+ return 1;
+}
+
diff --git a/rt/t/api/ace.t b/rt/t/api/ace.t
new file mode 100644
index 000000000..4031046d9
--- /dev/null
+++ b/rt/t/api/ace.t
@@ -0,0 +1,238 @@
+
+use strict;
+use warnings;
+use RT;
+use RT::Test tests => 76;
+
+
+{
+
+ok(require RT::ACE);
+
+
+}
+
+{
+
+my $Queue = RT::Queue->new($RT::SystemUser);
+
+is ($Queue->AvailableRights->{'DeleteTicket'} , 'Delete tickets', "Found the delete ticket right");
+is ($RT::System->AvailableRights->{'SuperUser'}, 'Do anything and everything', "Found the superuser right");
+
+
+
+}
+
+{
+
+use_ok('RT::User');
+my $user_a = RT::User->new($RT::SystemUser);
+$user_a->Create( Name => 'DelegationA', Privileged => 1);
+ok ($user_a->Id, "Created delegation user a");
+
+my $user_b = RT::User->new($RT::SystemUser);
+$user_b->Create( Name => 'DelegationB', Privileged => 1);
+ok ($user_b->Id, "Created delegation user b");
+
+
+use_ok('RT::Queue');
+my $q = RT::Queue->new($RT::SystemUser);
+$q->Create(Name =>'DelegationTest');
+ok ($q->Id, "Created a delegation test queue");
+
+
+#------ First, we test whether a user can delegate a right that's been granted to him personally
+my ($val, $msg) = $user_a->PrincipalObj->GrantRight(Object => $RT::System, Right => 'AdminOwnPersonalGroups');
+ok($val, $msg);
+
+($val, $msg) = $user_a->PrincipalObj->GrantRight(Object =>$q, Right => 'OwnTicket');
+ok($val, $msg);
+
+ok($user_a->HasRight( Object => $RT::System, Right => 'AdminOwnPersonalGroups') ,"user a has the right 'AdminOwnPersonalGroups' directly");
+
+my $a_delegates = RT::Group->new($user_a);
+$a_delegates->CreatePersonalGroup(Name => 'Delegates');
+ok( $a_delegates->Id ,"user a creates a personal group 'Delegates'");
+ok( $a_delegates->AddMember($user_b->PrincipalId) ,"user a adds user b to personal group 'delegates'");
+
+ok( !$user_b->HasRight(Right => 'OwnTicket', Object => $q) ,"user b does not have the right to OwnTicket' in queue 'DelegationTest'");
+ok( $user_a->HasRight(Right => 'OwnTicket', Object => $q) ,"user a has the right to 'OwnTicket' in queue 'DelegationTest'");
+ok(!$user_a->HasRight( Object => $RT::System, Right => 'DelegateRights') ,"user a does not have the right 'delegate rights'");
+
+
+my $own_ticket_ace = RT::ACE->new($user_a);
+my $user_a_equiv_group = RT::Group->new($user_a);
+$user_a_equiv_group->LoadACLEquivalenceGroup($user_a->PrincipalObj);
+ok ($user_a_equiv_group->Id, "Loaded the user A acl equivalence group");
+my $user_b_equiv_group = RT::Group->new($user_b);
+$user_b_equiv_group->LoadACLEquivalenceGroup($user_b->PrincipalObj);
+ok ($user_b_equiv_group->Id, "Loaded the user B acl equivalence group");
+$own_ticket_ace->LoadByValues( PrincipalType => 'Group', PrincipalId => $user_a_equiv_group->PrincipalId, Object=>$q, RightName => 'OwnTicket');
+
+ok ($own_ticket_ace->Id, "Found the ACE we want to test with for now");
+
+
+($val, $msg) = $own_ticket_ace->Delegate(PrincipalId => $a_delegates->PrincipalId) ;
+ok( !$val ,"user a tries and fails to delegate the right 'ownticket' in queue 'DelegationTest' to personal group 'delegates' - $msg");
+
+
+($val, $msg) = $user_a->PrincipalObj->GrantRight( Right => 'DelegateRights');
+ok($val, "user a is granted the right to 'delegate rights' - $msg");
+
+ok($user_a->HasRight( Object => $RT::System, Right => 'DelegateRights') ,"user a has the right 'DeletgateRights'");
+
+($val, $msg) = $own_ticket_ace->Delegate(PrincipalId => $a_delegates->PrincipalId) ;
+
+ok( $val ,"user a tries and succeeds to delegate the right 'ownticket' in queue 'DelegationTest' to personal group 'delegates' - $msg");
+ok( $user_b->HasRight(Right => 'OwnTicket', Object => $q) ,"user b has the right to own tickets in queue 'DelegationTest'");
+my $delegated_ace = RT::ACE->new($user_a);
+$delegated_ace->LoadByValues ( Object => $q, RightName => 'OwnTicket', PrincipalType => 'Group',
+PrincipalId => $a_delegates->PrincipalId, DelegatedBy => $user_a->PrincipalId, DelegatedFrom => $own_ticket_ace->Id);
+ok ($delegated_ace->Id, "Found the delegated ACE");
+
+ok( $a_delegates->DeleteMember($user_b->PrincipalId) ,"user a removes b from pg 'delegates'");
+ok( !$user_b->HasRight(Right => 'OwnTicket', Object => $q) ,"user b does not have the right to own tickets in queue 'DelegationTest'");
+ok( $a_delegates->AddMember($user_b->PrincipalId) ,"user a adds user b to personal group 'delegates'");
+ok( $user_b->HasRight(Right => 'OwnTicket', Object=> $q) ,"user b has the right to own tickets in queue 'DelegationTest'");
+ok( $delegated_ace->Delete ,"user a revokes pg 'delegates' right to 'OwnTickets' in queue 'DelegationTest'");
+ok( ! $user_b->HasRight(Right => 'OwnTicket', Object => $q) ,"user b does not have the right to own tickets in queue 'DelegationTest'");
+
+($val, $msg) = $own_ticket_ace->Delegate(PrincipalId => $a_delegates->PrincipalId) ;
+ok( $val ,"user a delegates pg 'delegates' right to 'OwnTickets' in queue 'DelegationTest' - $msg");
+
+ok( $user_b->HasRight(Right => 'OwnTicket', Object => $q) ,"user b has the right to own tickets in queue 'DelegationTest'");
+
+($val, $msg) = $user_a->PrincipalObj->RevokeRight(Object=>$q, Right => 'OwnTicket');
+ok($val, "Revoked user a's right to own tickets in queue 'DelegationTest". $msg);
+
+ok( !$user_a->HasRight(Right => 'OwnTicket', Object => $q) ,"user a does not have the right to own tickets in queue 'DelegationTest'");
+
+ ok( !$user_b->HasRight(Right => 'OwnTicket', Object => $q) ,"user b does not have the right to own tickets in queue 'DelegationTest'");
+
+($val, $msg) = $user_a->PrincipalObj->GrantRight(Object=>$q, Right => 'OwnTicket');
+ok($val, $msg);
+
+ ok( $user_a->HasRight(Right => 'OwnTicket', Object => $q) ,"user a has the right to own tickets in queue 'DelegationTest'");
+
+ ok( !$user_b->HasRight(Right => 'OwnTicket', Object => $q) ,"user b does not have the right to own tickets in queue 'DelegationTest'");
+
+# {{{ get back to a known clean state
+($val, $msg) = $user_a->PrincipalObj->RevokeRight( Object => $q, Right => 'OwnTicket');
+ok($val, "Revoked user a's right to own tickets in queue 'DelegationTest -". $msg);
+ok( !$user_a->HasRight(Right => 'OwnTicket', Object => $q) ,"make sure that user a can't own tickets in queue 'DelegationTest'");
+# }}}
+
+
+# {{{ Set up some groups and membership
+my $del1 = RT::Group->new($RT::SystemUser);
+($val, $msg) = $del1->CreateUserDefinedGroup(Name => 'Del1');
+ok( $val ,"create a group del1 - $msg");
+
+my $del2 = RT::Group->new($RT::SystemUser);
+($val, $msg) = $del2->CreateUserDefinedGroup(Name => 'Del2');
+ok( $val ,"create a group del2 - $msg");
+($val, $msg) = $del1->AddMember($del2->PrincipalId);
+ok( $val,"make del2 a member of del1 - $msg");
+
+my $del2a = RT::Group->new($RT::SystemUser);
+($val, $msg) = $del2a->CreateUserDefinedGroup(Name => 'Del2a');
+ok( $val ,"create a group del2a - $msg");
+($val, $msg) = $del2->AddMember($del2a->PrincipalId);
+ok($val ,"make del2a a member of del2 - $msg");
+
+my $del2b = RT::Group->new($RT::SystemUser);
+($val, $msg) = $del2b->CreateUserDefinedGroup(Name => 'Del2b');
+ok( $val ,"create a group del2b - $msg");
+($val, $msg) = $del2->AddMember($del2b->PrincipalId);
+ok($val ,"make del2b a member of del2 - $msg");
+
+($val, $msg) = $del2->AddMember($user_a->PrincipalId) ;
+ok($val,"make 'user a' a member of del2 - $msg");
+
+($val, $msg) = $del2b->AddMember($user_a->PrincipalId) ;
+ok($val,"make 'user a' a member of del2b - $msg");
+
+# }}}
+
+# {{{ Grant a right to a group and make sure that a submember can delegate the right and that it does not get yanked
+# when a user is removed as a submember, when they're a submember through another path
+($val, $msg) = $del1->PrincipalObj->GrantRight( Object=> $q, Right => 'OwnTicket');
+ok( $val ,"grant del1 the right to 'OwnTicket' in queue 'DelegationTest' - $msg");
+
+ok( $user_a->HasRight(Right => 'OwnTicket', Object => $q) ,"make sure that user a can own tickets in queue 'DelegationTest'");
+
+my $group_ace= RT::ACE->new($user_a);
+$group_ace->LoadByValues( PrincipalType => 'Group', PrincipalId => $del1->PrincipalId, Object => $q, RightName => 'OwnTicket');
+
+ok ($group_ace->Id, "Found the ACE we want to test with for now");
+
+($val, $msg) = $group_ace->Delegate(PrincipalId => $a_delegates->PrincipalId);
+
+ok( $val ,"user a tries and succeeds to delegate the right 'ownticket' in queue 'DelegationTest' to personal group 'delegates' - $msg");
+ok( $user_b->HasRight(Right => 'OwnTicket', Object => $q) ,"user b has the right to own tickets in queue 'DelegationTest'");
+
+
+($val, $msg) = $del2b->DeleteMember($user_a->PrincipalId);
+ok( $val ,"remove user a from group del2b - $msg");
+ok( $user_a->HasRight(Right => 'OwnTicket', Object => $q) ,"user a has the right to own tickets in queue 'DelegationTest'");
+ok( $user_b->HasRight(Right => 'OwnTicket', Object => $q) ,"user b has the right to own tickets in queue 'DelegationTest'");
+
+# }}}
+
+# {{{ When a user is removed froom a group by the only path they're in there by, make sure the delegations go away
+($val, $msg) = $del2->DeleteMember($user_a->PrincipalId);
+ok( $val ,"remove user a from group del2 - $msg");
+ok( !$user_a->HasRight(Right => 'OwnTicket', Object => $q) ,"user a does not have the right to own tickets in queue 'DelegationTest' ");
+ok( !$user_b->HasRight(Right => 'OwnTicket', Object => $q) ,"user b does not have the right to own tickets in queue 'DelegationTest' ");
+# }}}
+
+($val, $msg) = $del2->AddMember($user_a->PrincipalId);
+ok( $val ,"make user a a member of group del2 - $msg");
+
+($val, $msg) = $del2->PrincipalObj->GrantRight(Object=>$q, Right => 'OwnTicket');
+ok($val, "grant the right 'own tickets' in queue 'DelegationTest' to group del2 - $msg");
+
+my $del2_right = RT::ACE->new($user_a);
+$del2_right->LoadByValues( PrincipalId => $del2->PrincipalId, PrincipalType => 'Group', Object => $q, RightName => 'OwnTicket');
+ok ($del2_right->Id, "Found the right");
+
+($val, $msg) = $del2_right->Delegate(PrincipalId => $a_delegates->PrincipalId);
+ok( $val ,"user a tries and succeeds to delegate the right 'ownticket' in queue 'DelegationTest' gotten via del2 to personal group 'delegates' - $msg");
+
+# They have it via del1 and del2
+ok( $user_b->HasRight(Right => 'OwnTicket', Object => $q) ,"user b has the right to own tickets in queue 'DelegationTest'");
+
+
+($val, $msg) = $del2->PrincipalObj->RevokeRight(Object=>$q, Right => 'OwnTicket');
+ok($val, "revoke the right 'own tickets' in queue 'DelegationTest' to group del2 - $msg");
+ok( $user_a->HasRight(Right => 'OwnTicket', Object => $q) ,"user a does has the right to own tickets in queue 'DelegationTest' via del1");
+ok( !$user_b->HasRight(Right => 'OwnTicket', Object => $q) ,"user b does not have the right to own tickets in queue 'DelegationTest'");
+
+($val, $msg) = $del2->PrincipalObj->GrantRight(Object=>$q, Right => 'OwnTicket');
+ok($val, "grant the right 'own tickets' in queue 'DelegationTest' to group del2 - $msg");
+
+
+$group_ace= RT::ACE->new($user_a);
+$group_ace->LoadByValues( PrincipalType => 'Group', PrincipalId => $del1->PrincipalId, Object=>$q, RightName => 'OwnTicket');
+
+ok ($group_ace->Id, "Found the ACE we want to test with for now");
+
+($val, $msg) = $group_ace->Delegate(PrincipalId => $a_delegates->PrincipalId);
+
+ok( $val ,"user a tries and succeeds to delegate the right 'ownticket' in queue 'DelegationTest' to personal group 'delegates' - $msg");
+
+ok( $user_b->HasRight(Right => 'OwnTicket', Object => $q) ,"user b has the right to own tickets in queue 'DelegationTest'");
+
+($val, $msg) = $del2->DeleteMember($user_a->PrincipalId);
+ok( $val ,"remove user a from group del2 - $msg");
+
+ok( !$user_a->HasRight(Right => 'OwnTicket', Object => $q) ,"user a does not have the right to own tickets in queue 'DelegationTest'");
+
+ok( !$user_b->HasRight(Right => 'OwnTicket', Object => $q) ,"user b does not have the right to own tickets in queue 'DelegationTest'");
+
+
+
+
+}
+
+1;
diff --git a/rt/t/api/action-createtickets.t b/rt/t/api/action-createtickets.t
new file mode 100644
index 000000000..69ceb8d4d
--- /dev/null
+++ b/rt/t/api/action-createtickets.t
@@ -0,0 +1,240 @@
+
+use strict;
+use warnings;
+use RT;
+use RT::Test tests => 49;
+
+
+{
+
+ok (require RT::Action::CreateTickets);
+use_ok('RT::Scrip');
+use_ok('RT::Template');
+use_ok('RT::ScripAction');
+use_ok('RT::ScripCondition');
+use_ok('RT::Ticket');
+
+my $approvalsq = RT::Queue->new($RT::SystemUser);
+$approvalsq->Create(Name => 'Approvals');
+ok ($approvalsq->Id, "Created Approvals test queue");
+
+
+my $approvals =
+'===Create-Ticket: approval
+Queue: Approvals
+Type: approval
+AdminCc: {join ("\nAdminCc: ",@admins) }
+Depended-On-By: {$Tickets{"TOP"}->Id}
+Refers-To: TOP
+Subject: Approval for ticket: {$Tickets{"TOP"}->Id} - {$Tickets{"TOP"}->Subject}
+Due: {time + 86400}
+Content-Type: text/plain
+Content: Your approval is requested for the ticket {$Tickets{"TOP"}->Id}: {$Tickets{"TOP"}->Subject}
+Blah
+Blah
+ENDOFCONTENT
+===Create-Ticket: two
+Subject: Manager approval.
+Depended-On-By: approval
+Queue: Approvals
+Content-Type: text/plain
+Content:
+Your minion approved ticket {$Tickets{"TOP"}->Id}. you ok with that?
+ENDOFCONTENT
+';
+
+like ($approvals , qr/Content/, "Read in the approvals template");
+
+my $apptemp = RT::Template->new($RT::SystemUser);
+$apptemp->Create( Content => $approvals, Name => "Approvals", Queue => "0");
+
+ok ($apptemp->Id);
+
+my $q = RT::Queue->new($RT::SystemUser);
+$q->Create(Name => 'WorkflowTest');
+ok ($q->Id, "Created workflow test queue");
+
+my $scrip = RT::Scrip->new($RT::SystemUser);
+my ($sval, $smsg) =$scrip->Create( ScripCondition => 'On Transaction',
+ ScripAction => 'Create Tickets',
+ Template => 'Approvals',
+ Queue => $q->Id);
+ok ($sval, $smsg);
+ok ($scrip->Id, "Created the scrip");
+ok ($scrip->TemplateObj->Id, "Created the scrip template");
+ok ($scrip->ConditionObj->Id, "Created the scrip condition");
+ok ($scrip->ActionObj->Id, "Created the scrip action");
+
+my $t = RT::Ticket->new($RT::SystemUser);
+my($tid, $ttrans, $tmsg) = $t->Create(Subject => "Sample workflow test",
+ Owner => "root",
+ Queue => $q->Id);
+
+ok ($tid,$tmsg);
+
+my $deps = $t->DependsOn;
+is ($deps->Count, 1, "The ticket we created depends on one other ticket");
+my $dependson= $deps->First->TargetObj;
+ok ($dependson->Id, "It depends on a real ticket");
+unlike ($dependson->Subject, qr/{/, "The subject doesn't have braces in it. that means we're interpreting expressions");
+is ($t->ReferredToBy->Count,1, "It's only referred to by one other ticket");
+is ($t->ReferredToBy->First->BaseObj->Id,$t->DependsOn->First->TargetObj->Id, "The same ticket that depends on it refers to it.");
+use RT::Action::CreateTickets;
+my $action = RT::Action::CreateTickets->new( CurrentUser => $RT::SystemUser);
+
+# comma-delimited templates
+my $commas = <<"EOF";
+id,Queue,Subject,Owner,Content
+ticket1,General,"foo, bar",root,blah
+ticket2,General,foo bar,root,blah
+ticket3,General,foo' bar,root,blah'boo
+ticket4,General,foo' bar,,blah'boo
+EOF
+
+
+# Comma delimited templates with missing data
+my $sparse_commas = <<"EOF";
+id,Queue,Subject,Owner,Requestor
+ticket14,General,,,bobby
+ticket15,General,,,tommy
+ticket16,General,,suzie,tommy
+ticket17,General,Foo "bar" baz,suzie,tommy
+ticket18,General,'Foo "bar" baz',suzie,tommy
+ticket19,General,'Foo bar' baz,suzie,tommy
+EOF
+
+
+# tab-delimited templates
+my $tabs = <<"EOF";
+id\tQueue\tSubject\tOwner\tContent
+ticket10\tGeneral\t"foo' bar"\troot\tblah'
+ticket11\tGeneral\tfoo, bar\troot\tblah
+ticket12\tGeneral\tfoo' bar\troot\tblah'boo
+ticket13\tGeneral\tfoo' bar\t\tblah'boo
+EOF
+
+my %expected;
+
+$expected{ticket1} = <<EOF;
+Queue: General
+Subject: foo, bar
+Owner: root
+Content: blah
+ENDOFCONTENT
+EOF
+
+$expected{ticket2} = <<EOF;
+Queue: General
+Subject: foo bar
+Owner: root
+Content: blah
+ENDOFCONTENT
+EOF
+
+$expected{ticket3} = <<EOF;
+Queue: General
+Subject: foo' bar
+Owner: root
+Content: blah'boo
+ENDOFCONTENT
+EOF
+
+$expected{ticket4} = <<EOF;
+Queue: General
+Subject: foo' bar
+Owner:
+Content: blah'boo
+ENDOFCONTENT
+EOF
+
+$expected{ticket10} = <<EOF;
+Queue: General
+Subject: foo' bar
+Owner: root
+Content: blah'
+ENDOFCONTENT
+EOF
+
+$expected{ticket11} = <<EOF;
+Queue: General
+Subject: foo, bar
+Owner: root
+Content: blah
+ENDOFCONTENT
+EOF
+
+$expected{ticket12} = <<EOF;
+Queue: General
+Subject: foo' bar
+Owner: root
+Content: blah'boo
+ENDOFCONTENT
+EOF
+
+$expected{ticket13} = <<EOF;
+Queue: General
+Subject: foo' bar
+Owner:
+Content: blah'boo
+ENDOFCONTENT
+EOF
+
+
+$expected{'ticket14'} = <<EOF;
+Queue: General
+Subject:
+Owner:
+Requestor: bobby
+EOF
+$expected{'ticket15'} = <<EOF;
+Queue: General
+Subject:
+Owner:
+Requestor: tommy
+EOF
+$expected{'ticket16'} = <<EOF;
+Queue: General
+Subject:
+Owner: suzie
+Requestor: tommy
+EOF
+$expected{'ticket17'} = <<EOF;
+Queue: General
+Subject: Foo "bar" baz
+Owner: suzie
+Requestor: tommy
+EOF
+$expected{'ticket18'} = <<EOF;
+Queue: General
+Subject: Foo "bar" baz
+Owner: suzie
+Requestor: tommy
+EOF
+$expected{'ticket19'} = <<EOF;
+Queue: General
+Subject: 'Foo bar' baz
+Owner: suzie
+Requestor: tommy
+EOF
+
+
+
+
+$action->Parse(Content =>$commas);
+$action->Parse(Content =>$sparse_commas);
+$action->Parse(Content => $tabs);
+
+my %got;
+foreach (@{ $action->{'create_tickets'} }) {
+ $got{$_} = $action->{'templates'}->{$_};
+}
+
+foreach my $id ( sort keys %expected ) {
+ ok(exists($got{"create-$id"}), "template exists for $id");
+ is($got{"create-$id"}, $expected{$id}, "template is correct for $id");
+}
+
+
+}
+
+1;
diff --git a/rt/t/api/attachment.t b/rt/t/api/attachment.t
new file mode 100644
index 000000000..07c46bad0
--- /dev/null
+++ b/rt/t/api/attachment.t
@@ -0,0 +1,45 @@
+
+use strict;
+use warnings;
+use RT;
+use RT::Test tests => 4;
+
+
+{
+
+ok (require RT::Attachment);
+
+
+}
+
+{
+
+my $test1 = "From: jesse";
+my @headers = RT::Attachment->_SplitHeaders($test1);
+is ($#headers, 0, $test1 );
+
+my $test2 = qq{From: jesse
+To: bobby
+Subject: foo
+};
+
+@headers = RT::Attachment->_SplitHeaders($test2);
+is ($#headers, 2, "testing a bunch of singline multiple headers" );
+
+
+my $test3 = qq{From: jesse
+To: bobby,
+ Suzie,
+ Sally,
+ Joey: bizzy,
+Subject: foo
+};
+
+@headers = RT::Attachment->_SplitHeaders($test3);
+is ($#headers, 2, "testing a bunch of singline multiple headers" );
+
+
+
+}
+
+1;
diff --git a/rt/t/api/attribute-tests.t b/rt/t/api/attribute-tests.t
new file mode 100644
index 000000000..90c3ddb7e
--- /dev/null
+++ b/rt/t/api/attribute-tests.t
@@ -0,0 +1,86 @@
+use strict;
+use warnings;
+use RT;
+use RT::Test tests => 34;
+
+
+
+my $runid = rand(200);
+
+my $attribute = "squelch-$runid";
+
+ok(require RT::Attributes);
+
+my $user = RT::User->new($RT::SystemUser);
+ok (UNIVERSAL::isa($user, 'RT::User'));
+my ($id,$msg) = $user->Create(Name => 'attrtest-'.$runid);
+ok ($id, $msg);
+ok($user->id, "Created a test user");
+
+ok(1, $user->Attributes->BuildSelectQuery);
+my $attr = $user->Attributes;
+# XXX: Order by id as some tests depend on it
+$attr->OrderByCols({ FIELD => 'id' });
+
+ok(1, $attr->BuildSelectQuery);
+
+
+ok (UNIVERSAL::isa($attr,'RT::Attributes'), 'got the attributes object');
+
+($id, $msg) = $user->AddAttribute(Name => 'TestAttr', Content => 'The attribute has content');
+ok ($id, $msg);
+is ($attr->Count,1, " One attr after adding a first one");
+
+my $first_attr = $user->FirstAttribute('TestAttr');
+ok($first_attr, "got some sort of attribute");
+isa_ok($first_attr, 'RT::Attribute');
+is($first_attr->Content, 'The attribute has content', "got the right content back");
+
+($id, $msg) = $attr->DeleteEntry(Name => $runid);
+ok(!$id, "Deleted non-existant entry - $msg");
+is ($attr->Count,1, "1 attr after deleting an empty attr");
+
+my @names = $attr->Names;
+is ("@names", "TestAttr");
+
+
+($id, $msg) = $user->AddAttribute(Name => $runid, Content => "First");
+ok($id, $msg);
+
+my $runid_attr = $user->FirstAttribute($runid);
+ok($runid_attr, "got some sort of attribute");
+isa_ok($runid_attr, 'RT::Attribute');
+is($runid_attr->Content, 'First', "got the right content back");
+
+is ($attr->Count,2, " Two attrs after adding an attribute named $runid");
+($id, $msg) = $user->AddAttribute(Name => $runid, Content => "Second");
+ok($id, $msg);
+
+$runid_attr = $user->FirstAttribute($runid);
+ok($runid_attr, "got some sort of attribute");
+isa_ok($runid_attr, 'RT::Attribute');
+is($runid_attr->Content, 'First', "got the first content back still");
+
+is ($attr->Count,3, " Three attrs after adding a secondvalue to $runid");
+($id, $msg) = $attr->DeleteEntry(Name => $runid, Content => "First");
+ok($id, $msg);
+is ($attr->Count,2);
+
+#$attr->_DoSearch();
+($id, $msg) = $attr->DeleteEntry(Name => $runid, Content => "Second");
+ok($id, $msg);
+is ($attr->Count,1);
+
+#$attr->_DoSearch();
+ok(1, $attr->BuildSelectQuery);
+($id, $msg) = $attr->DeleteEntry(Name => "moose");
+ok(!$id, "Deleted non-existant entry - $msg");
+is ($attr->Count,1);
+
+ok(1, $attr->BuildSelectQuery);
+@names = $attr->Names;
+is("@names", "TestAttr");
+
+
+
+1;
diff --git a/rt/t/api/attribute.t b/rt/t/api/attribute.t
new file mode 100644
index 000000000..cb2626ad8
--- /dev/null
+++ b/rt/t/api/attribute.t
@@ -0,0 +1,42 @@
+
+use strict;
+use warnings;
+use RT;
+use RT::Test tests => 7;
+
+
+{
+
+my $user = $RT::SystemUser;
+my ($id, $msg) = $user->AddAttribute(Name => 'SavedSearch', Content => { Query => 'Foo'} );
+ok ($id, $msg);
+my $attr = RT::Attribute->new($RT::SystemUser);
+$attr->Load($id);
+is($attr->Name , 'SavedSearch');
+$attr->SetSubValues( Format => 'baz');
+
+my $format = $attr->SubValue('Format');
+is ($format , 'baz');
+
+$attr->SetSubValues( Format => 'bar');
+$format = $attr->SubValue('Format');
+is ($format , 'bar');
+
+$attr->DeleteAllSubValues();
+$format = $attr->SubValue('Format');
+is ($format, undef);
+
+$attr->SetSubValues(Format => 'This is a format');
+
+my $attr2 = RT::Attribute->new($RT::SystemUser);
+$attr2->Load($id);
+is ($attr2->SubValue('Format'), 'This is a format');
+$attr2->Delete;
+my $attr3 = RT::Attribute->new($RT::SystemUser);
+($id) = $attr3->Load($id);
+is ($id, 0);
+
+
+}
+
+1;
diff --git a/rt/t/api/cf.t b/rt/t/api/cf.t
new file mode 100644
index 000000000..98114c93f
--- /dev/null
+++ b/rt/t/api/cf.t
@@ -0,0 +1,224 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings FATAL => 'all';
+
+use RT::Test tests => 139;
+
+# Before we get going, ditch all object_cfs; this will remove
+# all custom fields systemwide;
+my $object_cfs = RT::ObjectCustomFields->new($RT::SystemUser);
+$object_cfs->UnLimit();
+while (my $ocf = $object_cfs->Next) {
+ $ocf->Delete();
+}
+
+
+my $queue = RT::Queue->new( $RT::SystemUser );
+$queue->Create( Name => 'RecordCustomFields-'.$$ );
+ok ($queue->id, "Created the queue");
+
+my $queue2 = RT::Queue->new( $RT::SystemUser );
+$queue2->Create( Name => 'RecordCustomFields2' );
+
+my $ticket = RT::Ticket->new( $RT::SystemUser );
+$ticket->Create(
+ Queue => $queue->Id,
+ Requestor => 'root@localhost',
+ Subject => 'RecordCustomFields1',
+);
+
+my $cfs = $ticket->CustomFields;
+is( $cfs->Count, 0 );
+
+# Check that record has no any CF values yet {{{
+my $cfvs = $ticket->CustomFieldValues;
+is( $cfvs->Count, 0 );
+is( $ticket->FirstCustomFieldValue, undef );
+
+my $local_cf1 = RT::CustomField->new( $RT::SystemUser );
+$local_cf1->Create( Name => 'RecordCustomFields1-'.$$, Type => 'SelectSingle', Queue => $queue->id );
+$local_cf1->AddValue( Name => 'RecordCustomFieldValues11' );
+$local_cf1->AddValue( Name => 'RecordCustomFieldValues12' );
+
+my $local_cf2 = RT::CustomField->new( $RT::SystemUser );
+$local_cf2->Create( Name => 'RecordCustomFields2-'.$$, Type => 'SelectSingle', Queue => $queue->id );
+$local_cf2->AddValue( Name => 'RecordCustomFieldValues21' );
+$local_cf2->AddValue( Name => 'RecordCustomFieldValues22' );
+
+my $global_cf3 = RT::CustomField->new( $RT::SystemUser );
+$global_cf3->Create( Name => 'RecordCustomFields3-'.$$, Type => 'SelectSingle', Queue => 0 );
+$global_cf3->AddValue( Name => 'RecordCustomFieldValues31' );
+$global_cf3->AddValue( Name => 'RecordCustomFieldValues32' );
+
+my $local_cf4 = RT::CustomField->new( $RT::SystemUser );
+$local_cf4->Create( Name => 'RecordCustomFields4', Type => 'SelectSingle', Queue => $queue2->id );
+$local_cf4->AddValue( Name => 'RecordCustomFieldValues41' );
+$local_cf4->AddValue( Name => 'RecordCustomFieldValues42' );
+
+
+my @custom_fields = ($local_cf1, $local_cf2, $global_cf3);
+
+
+$cfs = $ticket->CustomFields;
+is( $cfs->Count, 3 );
+
+# Check that record has no any CF values yet {{{
+$cfvs = $ticket->CustomFieldValues;
+is( $cfvs->Count, 0 );
+is( $ticket->FirstCustomFieldValue, undef );
+
+# CF with ID -1 shouldnt exist at all
+$cfvs = $ticket->CustomFieldValues( -1 );
+is( $cfvs->Count, 0 );
+is( $ticket->FirstCustomFieldValue( -1 ), undef );
+
+$cfvs = $ticket->CustomFieldValues( 'SomeUnexpedCustomFieldName' );
+is( $cfvs->Count, 0 );
+is( $ticket->FirstCustomFieldValue( 'SomeUnexpedCustomFieldName' ), undef );
+
+for (@custom_fields) {
+ $cfvs = $ticket->CustomFieldValues( $_->id );
+ is( $cfvs->Count, 0 );
+
+ $cfvs = $ticket->CustomFieldValues( $_->Name );
+ is( $cfvs->Count, 0 );
+ is( $ticket->FirstCustomFieldValue( $_->id ), undef );
+ is( $ticket->FirstCustomFieldValue( $_->Name ), undef );
+}
+# }}}
+
+# try to add field value with fields that do not exist {{{
+my ($status, $msg) = $ticket->AddCustomFieldValue( Field => -1 , Value => 'foo' );
+ok(!$status, "shouldn't add value" );
+($status, $msg) = $ticket->AddCustomFieldValue( Field => 'SomeUnexpedCustomFieldName' , Value => 'foo' );
+ok(!$status, "shouldn't add value" );
+# }}}
+
+# {{{
+SKIP: {
+
+ skip "TODO: We want fields that are not allowed to set unexpected values", 10;
+ for (@custom_fields) {
+ ($status, $msg) = $ticket->AddCustomFieldValue( Field => $_ , Value => 'SomeUnexpectedCFValue' );
+ ok( !$status, 'value doesn\'t exist');
+
+ ($status, $msg) = $ticket->AddCustomFieldValue( Field => $_->id , Value => 'SomeUnexpectedCFValue' );
+ ok( !$status, 'value doesn\'t exist');
+
+ ($status, $msg) = $ticket->AddCustomFieldValue( Field => $_->Name , Value => 'SomeUnexpectedCFValue' );
+ ok( !$status, 'value doesn\'t exist');
+ }
+
+ # Let check that we did not add value to be sure
+ # using only FirstCustomFieldValue sub because
+ # we checked other variants allready
+ for (@custom_fields) {
+ is( $ticket->FirstCustomFieldValue( $_->id ), undef );
+ }
+
+}
+# Add some values to our custom fields
+for (@custom_fields) {
+ # this should be tested elsewhere
+ $_->AddValue( Name => 'Foo' );
+ $_->AddValue( Name => 'Bar' );
+}
+
+my $test_add_delete_cycle = sub {
+ my $cb = shift;
+ for (@custom_fields) {
+ ($status, $msg) = $ticket->AddCustomFieldValue( Field => $cb->($_) , Value => 'Foo' );
+ ok( $status, "message: $msg");
+ }
+
+ # does it exist?
+ $cfvs = $ticket->CustomFieldValues;
+ is( $cfvs->Count, 3, "We found all three custom fields on our ticket" );
+ for (@custom_fields) {
+ $cfvs = $ticket->CustomFieldValues( $_->id );
+ is( $cfvs->Count, 1 , "we found one custom field when searching by id");
+
+ $cfvs = $ticket->CustomFieldValues( $_->Name );
+ is( $cfvs->Count, 1 , " We found one custom field when searching by name for " . $_->Name);
+ is( $ticket->FirstCustomFieldValue( $_->id ), 'Foo' , "first value by id is foo");
+ is( $ticket->FirstCustomFieldValue( $_->Name ), 'Foo' , "first value by name is foo");
+ }
+ # because our CFs are SingleValue then new value addition should override
+ for (@custom_fields) {
+ ($status, $msg) = $ticket->AddCustomFieldValue( Field => $_ , Value => 'Bar' );
+ ok( $status, "message: $msg");
+ }
+ $cfvs = $ticket->CustomFieldValues;
+ is( $cfvs->Count, 3 );
+ for (@custom_fields) {
+ $cfvs = $ticket->CustomFieldValues( $_->id );
+ is( $cfvs->Count, 1 );
+
+ $cfvs = $ticket->CustomFieldValues( $_->Name );
+ is( $cfvs->Count, 1 );
+ is( $ticket->FirstCustomFieldValue( $_->id ), 'Bar' );
+ is( $ticket->FirstCustomFieldValue( $_->Name ), 'Bar' );
+ }
+ # delete it
+ for (@custom_fields ) {
+ ($status, $msg) = $ticket->DeleteCustomFieldValue( Field => $_ , Value => 'Bar' );
+ ok( $status, "Deleted a custom field value 'Bar' for field ".$_->id.": $msg");
+ }
+ $cfvs = $ticket->CustomFieldValues;
+ is( $cfvs->Count, 0, "The ticket (".$ticket->id.") no longer has any custom field values" );
+ for (@custom_fields) {
+ $cfvs = $ticket->CustomFieldValues( $_->id );
+ is( $cfvs->Count, 0, $ticket->id." has no values for cf ".$_->id );
+
+ $cfvs = $ticket->CustomFieldValues( $_->Name );
+ is( $cfvs->Count, 0 , $ticket->id." has no values for cf '".$_->Name. "'" );
+ is( $ticket->FirstCustomFieldValue( $_->id ), undef , "There is no first custom field value when loading by id" );
+ is( $ticket->FirstCustomFieldValue( $_->Name ), undef, "There is no first custom field value when loading by Name" );
+ }
+};
+
+# lets test cycle via CF id
+$test_add_delete_cycle->( sub { return $_[0]->id } );
+# lets test cycle via CF object reference
+$test_add_delete_cycle->( sub { return $_[0] } );
+
+$ticket->AddCustomFieldValue( Field => $local_cf2->id , Value => 'Baz' );
+$ticket->AddCustomFieldValue( Field => $global_cf3->id , Value => 'Baz' );
+# now if we ask for cf values on RecordCustomFields4 we should not get any
+$cfvs = $ticket->CustomFieldValues( 'RecordCustomFields4' );
+is( $cfvs->Count, 0, "No custom field values for non-Queue cf" );
+is( $ticket->FirstCustomFieldValue( 'RecordCustomFields4' ), undef, "No first custom field value for non-Queue cf" );
+
+{
+ my $cfname = $global_cf3->Name;
+ ($status, $msg) = $global_cf3->SetDisabled(1);
+ ok($status, "Disabled CF named $cfname");
+
+ my $load = RT::CustomField->new( $RT::SystemUser );
+ $load->LoadByName( Name => $cfname);
+ ok($load->Id, "Loaded CF named $cfname");
+ is($load->Id, $global_cf3->Id, "Can load disabled CFs");
+
+ my $dup = RT::CustomField->new( $RT::SystemUser );
+ $dup->Create( Name => $cfname, Type => 'SelectSingle', Queue => 0 );
+ ok($dup->Id, "Created CF with duplicate name");
+
+ $load->LoadByName( Name => $cfname);
+ is($load->Id, $dup->Id, "Loading by name gets non-disabled first");
+
+ $dup->SetDisabled(1);
+ $global_cf3->SetDisabled(0);
+
+ $load->LoadByName( Name => $cfname);
+ is($load->Id, $global_cf3->Id, "Loading by name gets non-disabled first, even with order swapped");
+}
+
+#SKIP: {
+# skip "TODO: should we add CF values to objects via CF Name?", 48;
+# names are not unique
+ # lets test cycle via CF Name
+# $test_add_delete_cycle->( sub { return $_[0]->Name } );
+#}
+
+
diff --git a/rt/t/api/cf_combo_casacade.t b/rt/t/api/cf_combo_casacade.t
new file mode 100644
index 000000000..b37345a6a
--- /dev/null
+++ b/rt/t/api/cf_combo_casacade.t
@@ -0,0 +1,46 @@
+#!/usr/bin/perl
+use warnings;
+use strict;
+
+use RT::Test tests => 11;
+
+sub fails { ok(!$_[0], "This should fail: $_[1]") }
+sub works { ok($_[0], $_[1] || 'This works') }
+
+sub new (*) {
+ my $class = shift;
+ return $class->new($RT::SystemUser);
+}
+
+my $q = new(RT::Queue);
+works($q->Create(Name => "CF-Pattern-".$$));
+
+my $cf = new(RT::CustomField);
+my @cf_args = (Name => $q->Name, Type => 'Combobox', Queue => $q->id);
+
+works($cf->Create(@cf_args));
+
+# Set some CFVs with Category markers
+
+my $t = new(RT::Ticket);
+my ($id,undef,$msg) = $t->Create(Queue => $q->id, Subject => 'CF Test');
+works($id,$msg);
+
+sub add_works {
+ works(
+ $cf->AddValue(Name => $_[0], Description => $_[0], Category => $_[1])
+ );
+};
+
+add_works('value1', '1. Category A');
+add_works('value2');
+add_works('value3', '1.1. A-sub one');
+add_works('value4', '1.2. A-sub two');
+add_works('value5', '');
+
+my $cfv = $cf->Values->First;
+is($cfv->Category, '1. Category A');
+works($cfv->SetCategory('1. Category AAA'));
+is($cfv->Category, '1. Category AAA');
+
+1;
diff --git a/rt/t/api/cf_external.t b/rt/t/api/cf_external.t
new file mode 100644
index 000000000..076871240
--- /dev/null
+++ b/rt/t/api/cf_external.t
@@ -0,0 +1,56 @@
+#!/usr/bin/perl
+
+use warnings;
+use strict;
+
+use RT;
+use RT::Test nodata => 1, tests => 11;
+
+sub new (*) {
+ my $class = shift;
+ return $class->new($RT::SystemUser);
+}
+
+use constant VALUES_CLASS => 'RT::CustomFieldValues::Groups';
+
+my $q = new( RT::Queue );
+isa_ok( $q, 'RT::Queue' );
+my ($qid) = $q->Create( Name => "CF-External-". $$ );
+ok( $qid, "created queue" );
+my %arg = ( Name => $q->Name,
+ Type => 'Select',
+ Queue => $q->id,
+ MaxValues => 1,
+ ValuesClass => VALUES_CLASS );
+
+my $cf = new( RT::CustomField );
+isa_ok( $cf, 'RT::CustomField' );
+
+{
+ my ($cfid) = $cf->Create( %arg );
+ ok( $cfid, "created cf" );
+ is( $cf->ValuesClass, VALUES_CLASS, "right values class" );
+ ok( $cf->IsExternalValues, "custom field has external values" );
+}
+
+{
+ # create at least on group for the tests
+ my $group = RT::Group->new( $RT::SystemUser );
+ my ($ret, $msg) = $group->CreateUserDefinedGroup( Name => $q->Name );
+ ok $ret, 'created group' or diag "error: $msg";
+}
+
+{
+ my $values = $cf->Values;
+ isa_ok( $values, VALUES_CLASS );
+ ok( $values->Count, "we have values" );
+ my ($failure, $count) = (0, 0);
+ while( my $value = $values->Next ) {
+ $count++;
+ $failure = 1 unless $value->Name;
+ }
+ ok( !$failure, "all values have name" );
+ is( $values->Count, $count, "count is correct" );
+}
+
+exit(0);
diff --git a/rt/t/api/cf_pattern.t b/rt/t/api/cf_pattern.t
new file mode 100644
index 000000000..89db2fea5
--- /dev/null
+++ b/rt/t/api/cf_pattern.t
@@ -0,0 +1,53 @@
+#!/usr/bin/perl
+use warnings;
+use strict;
+
+use RT;
+use RT::Test tests => 17;
+
+
+sub fails { ok(!$_[0], "This should fail: $_[1]") }
+sub works { ok($_[0], $_[1] || 'This works') }
+
+sub new (*) {
+ my $class = shift;
+ return $class->new($RT::SystemUser);
+}
+
+my $q = new(RT::Queue);
+works($q->Create(Name => "CF-Pattern-".$$));
+
+my $cf = new(RT::CustomField);
+my @cf_args = (Name => $q->Name, Type => 'Freeform', Queue => $q->id, MaxValues => 1);
+
+fails($cf->Create(@cf_args, Pattern => ')))bad!regex((('));
+works($cf->Create(@cf_args, Pattern => 'good regex'));
+
+my $t = new(RT::Ticket);
+my ($id,undef,$msg) = $t->Create(Queue => $q->id, Subject => 'CF Test');
+works($id,$msg);
+
+# OK, I'm thoroughly brain washed by HOP at this point now...
+sub cnt { $t->CustomFieldValues($cf->id)->Count };
+sub add { $t->AddCustomFieldValue(Field => $cf->id, Value => $_[0]) };
+sub del { $t->DeleteCustomFieldValue(Field => $cf->id, Value => $_[0]) };
+
+is(cnt(), 0, "No values yet");
+fails(add('not going to match'));
+is(cnt(), 0, "No values yet");
+works(add('here is a good regex'));
+is(cnt(), 1, "Value filled");
+fails(del('here is a good regex'));
+is(cnt(), 1, "Single CF - Value _not_ deleted");
+
+$cf->SetMaxValues(0); # Unlimited MaxValues
+
+works(del('here is a good regex'));
+is(cnt(), 0, "Multiple CF - Value deleted");
+
+fails($cf->SetPattern('(?{ "insert evil code here" })'));
+works($cf->SetPattern('(?!)')); # reject everything
+fails(add(''));
+fails(add('...'));
+
+1;
diff --git a/rt/t/api/cf_single_values.t b/rt/t/api/cf_single_values.t
new file mode 100644
index 000000000..8e96edd44
--- /dev/null
+++ b/rt/t/api/cf_single_values.t
@@ -0,0 +1,38 @@
+#!/usr/bin/perl
+use warnings;
+use strict;
+
+use RT;
+use RT::Test tests => 8;
+
+
+
+my $q = RT::Queue->new($RT::SystemUser);
+my ($id,$msg) =$q->Create(Name => "CF-Single-".$$);
+ok($id,$msg);
+
+my $cf = RT::CustomField->new($RT::SystemUser);
+($id,$msg) = $cf->Create(Name => 'Single-'.$$, Type => 'Select', MaxValues => '1', Queue => $q->id);
+ok($id,$msg);
+
+
+($id,$msg) =$cf->AddValue(Name => 'First');
+ok($id,$msg);
+
+($id,$msg) =$cf->AddValue(Name => 'Second');
+ok($id,$msg);
+
+
+my $t = RT::Ticket->new($RT::SystemUser);
+($id,undef,$msg) = $t->Create(Queue => $q->id,
+ Subject => 'CF Test');
+
+ok($id,$msg);
+is($t->CustomFieldValues($cf->id)->Count, 0, "No values yet");
+$t->AddCustomFieldValue(Field => $cf->id, Value => 'First');
+is($t->CustomFieldValues($cf->id)->Count, 1, "One now");
+
+$t->AddCustomFieldValue(Field => $cf->id, Value => 'Second');
+is($t->CustomFieldValues($cf->id)->Count, 1, "Still one");
+
+1;
diff --git a/rt/t/api/cf_transaction.t b/rt/t/api/cf_transaction.t
new file mode 100644
index 000000000..1ed2ab932
--- /dev/null
+++ b/rt/t/api/cf_transaction.t
@@ -0,0 +1,60 @@
+#!/usr/bin/perl
+
+use warnings;
+use strict;
+use Data::Dumper;
+
+use RT::Test tests => 14;
+use_ok('RT');
+use_ok('RT::Transactions');
+
+
+my $q = RT::Queue->new($RT::SystemUser);
+my ($id,$msg) = $q->Create( Name => 'TxnCFTest'.$$);
+ok($id,$msg);
+
+my $cf = RT::CustomField->new($RT::SystemUser);
+($id,$msg) = $cf->Create(Name => 'Txnfreeform-'.$$, Type => 'Freeform', MaxValues => '0', LookupType => RT::Transaction->CustomFieldLookupType );
+
+ok($id,$msg);
+
+($id,$msg) = $cf->AddToObject($q);
+
+ok($id,$msg);
+
+
+my $ticket = RT::Ticket->new($RT::SystemUser);
+
+my $transid;
+($id,$transid, $msg) = $ticket->Create(Queue => $q->id,
+ Subject => 'TxnCF test',
+ );
+ok($id,$msg);
+
+my $trans = RT::Transaction->new($RT::SystemUser);
+$trans->Load($transid);
+
+is($trans->ObjectId,$id);
+is ($trans->ObjectType, 'RT::Ticket');
+is ($trans->Type, 'Create');
+my $txncfs = $trans->CustomFields;
+is ($txncfs->Count, 1, "We have one custom field");
+my $txn_cf = $txncfs->First;
+is ($txn_cf->id, $cf->id, "It's the right custom field");
+my $values = $trans->CustomFieldValues($txn_cf->id);
+is ($values->Count, 0, "It has no values");
+
+# Old API
+my %cf_updates = ( 'CustomField-'.$cf->id => 'Testing');
+$trans->UpdateCustomFields( ARGSRef => \%cf_updates);
+
+ $values = $trans->CustomFieldValues($txn_cf->id);
+is ($values->Count, 1, "It has one value");
+
+# New API
+
+$trans->UpdateCustomFields( 'CustomField-'.$cf->id => 'Test two');
+ $values = $trans->CustomFieldValues($txn_cf->id);
+is ($values->Count, 2, "it has two values");
+
+# TODO ok(0, "Should updating custom field values remove old values?");
diff --git a/rt/t/api/condition-ownerchange.t b/rt/t/api/condition-ownerchange.t
new file mode 100644
index 000000000..4c4c49b29
--- /dev/null
+++ b/rt/t/api/condition-ownerchange.t
@@ -0,0 +1,51 @@
+
+use strict;
+use warnings;
+use RT;
+use RT::Test tests => 11;
+
+
+{
+
+my $q = RT::Queue->new($RT::SystemUser);
+$q->Create(Name =>'ownerChangeTest');
+
+ok($q->Id, "Created a scriptest queue");
+
+my $s1 = RT::Scrip->new($RT::SystemUser);
+my ($val, $msg) =$s1->Create( Queue => $q->Id,
+ ScripAction => 'User Defined',
+ ScripCondition => 'On Owner Change',
+ CustomIsApplicableCode => '',
+ CustomPrepareCode => 'return 1',
+ CustomCommitCode => '
+ $self->TicketObj->SetPriority($self->TicketObj->Priority+1);
+ return(1);
+ ',
+ Template => 'Blank'
+ );
+ok($val,$msg);
+
+my $ticket = RT::Ticket->new($RT::SystemUser);
+my ($tv,$ttv,$tm) = $ticket->Create(Queue => $q->Id,
+ Subject => "hair on fire",
+ InitialPriority => '20'
+ );
+ok($tv, $tm);
+ok($ticket->SetOwner('root'));
+is ($ticket->Priority , '21', "Ticket priority is set right");
+ok($ticket->Steal);
+is ($ticket->Priority , '22', "Ticket priority is set right");
+ok($ticket->Untake);
+is ($ticket->Priority , '23', "Ticket priority is set right");
+ok($ticket->Take);
+is ($ticket->Priority , '24', "Ticket priority is set right");
+
+
+
+
+
+
+}
+
+1;
diff --git a/rt/t/api/condition-reject.t b/rt/t/api/condition-reject.t
new file mode 100644
index 000000000..96789509d
--- /dev/null
+++ b/rt/t/api/condition-reject.t
@@ -0,0 +1,45 @@
+#
+# Check that the "On Reject" scrip condition exists and is working
+#
+
+use strict;
+use warnings;
+use RT;
+use RT::Test tests => 7;
+
+
+{
+
+my $q = RT::Queue->new($RT::SystemUser);
+$q->Create(Name =>'rejectTest');
+
+ok($q->Id, "Created a scriptest queue");
+
+my $s1 = RT::Scrip->new($RT::SystemUser);
+my ($val, $msg) =$s1->Create( Queue => $q->Id,
+ ScripAction => 'User Defined',
+ ScripCondition => 'On reject',
+ CustomIsApplicableCode => '',
+ CustomPrepareCode => 'return 1',
+ CustomCommitCode => '
+ $self->TicketObj->SetPriority($self->TicketObj->Priority+1);
+ return(1);
+ ',
+ Template => 'Blank'
+ );
+ok($val,$msg);
+
+my $ticket = RT::Ticket->new($RT::SystemUser);
+my ($tv,$ttv,$tm) = $ticket->Create(Queue => $q->Id,
+ Subject => "hair on fire",
+ InitialPriority => '20'
+ );
+ok($tv, $tm);
+ok($ticket->SetStatus('rejected'), "Status set to \"rejected\"");
+is ($ticket->Priority , '21', "Condition is true, scrip triggered");
+ok($ticket->SetStatus('open'), "Status set to \"open\"");
+is ($ticket->Priority , '21', "Condition is false, scrip skipped");
+
+}
+
+1;
diff --git a/rt/t/api/currentuser.t b/rt/t/api/currentuser.t
new file mode 100644
index 000000000..c15804824
--- /dev/null
+++ b/rt/t/api/currentuser.t
@@ -0,0 +1,32 @@
+
+use strict;
+use warnings;
+use RT;
+use RT::Test tests => 8;
+
+
+{
+
+ok (require RT::CurrentUser);
+
+
+}
+
+{
+
+ok (my $cu = RT::CurrentUser->new('root'));
+ok (my $lh = $cu->LanguageHandle('en-us'));
+isnt ($lh, undef, '$lh is defined');
+ok ($lh->isa('Locale::Maketext'));
+is ($cu->loc('TEST_STRING'), "Concrete Mixer", "Localized TEST_STRING into English");
+SKIP: {
+ skip "French localization is not enabled", 2
+ unless grep $_ && $_ =~ /^(\*|fr)$/, RT->Config->Get('LexiconLanguages');
+ ok ($lh = $cu->LanguageHandle('fr'));
+ is ($cu->loc('before'), "avant", "Localized TEST_STRING into French");
+}
+
+
+}
+
+1;
diff --git a/rt/t/api/customfield.t b/rt/t/api/customfield.t
new file mode 100644
index 000000000..44319c47f
--- /dev/null
+++ b/rt/t/api/customfield.t
@@ -0,0 +1,74 @@
+
+use strict;
+use warnings;
+use RT;
+use RT::Test tests => 29;
+use Test::Warn;
+
+
+{
+
+use_ok('RT::CustomField');
+ok(my $cf = RT::CustomField->new($RT::SystemUser));
+ok(my ($id, $msg)= $cf->Create( Name => 'TestingCF',
+ Queue => '0',
+ SortOrder => '1',
+ Description => 'A Testing custom field',
+ Type=> 'SelectSingle'), 'Created a global CustomField');
+isnt($id , 0, 'Global custom field correctly created');
+ok ($cf->SingleValue);
+is($cf->Type, 'Select');
+is($cf->MaxValues, 1);
+
+(my $val, $msg) = $cf->SetMaxValues('0');
+ok($val, $msg);
+is($cf->Type, 'Select');
+is($cf->MaxValues, 0);
+ok(!$cf->SingleValue );
+ok(my ($bogus_val, $bogus_msg) = $cf->SetType('BogusType') , "Trying to set a custom field's type to a bogus type");
+is($bogus_val , 0, "Unable to set a custom field's type to a bogus type");
+
+ok(my $bad_cf = RT::CustomField->new($RT::SystemUser));
+ok(my ($bad_id, $bad_msg)= $cf->Create( Name => 'TestingCF-bad',
+ Queue => '0',
+ SortOrder => '1',
+ Description => 'A Testing custom field with a bogus Type',
+ Type=> 'SelectSingleton'), 'Created a global CustomField with a bogus type');
+is($bad_id , 0, 'Global custom field correctly decided to not create a cf with a bogus type ');
+
+
+}
+
+{
+
+ok(my $cf = RT::CustomField->new($RT::SystemUser));
+$cf->Load(1);
+is($cf->Id , 1);
+ok(my ($val,$msg) = $cf->AddValue(Name => 'foo' , Description => 'TestCFValue', SortOrder => '6'));
+isnt($val , 0);
+ok (my ($delval, $delmsg) = $cf->DeleteValue($val));
+ok ($delval,"Deleting a cf value: $delmsg");
+
+
+}
+
+{
+
+ok(my $cf = RT::CustomField->new($RT::SystemUser));
+
+warning_like {
+ok($cf->ValidateType('SelectSingle'));
+} qr/deprecated/;
+
+warning_like {
+ok($cf->ValidateType('SelectMultiple'));
+} qr/deprecated/;
+
+warning_like {
+ok(!$cf->ValidateType('SelectFooMultiple'));
+} qr/deprecated/;
+
+
+}
+
+1;
diff --git a/rt/t/api/date.t b/rt/t/api/date.t
new file mode 100644
index 000000000..bc1446f50
--- /dev/null
+++ b/rt/t/api/date.t
@@ -0,0 +1,564 @@
+#!/usr/bin/perl
+
+use Test::MockTime qw(set_fixed_time restore_time);
+
+use Test::More;
+my $tests;
+
+my $localized_datetime_tests;
+BEGIN {
+ $tests = 167;
+ $localized_datetime_tests =
+ eval { require DateTime; 1; } && eval { require DateTime::Locale; 1; } &&
+ DateTime->can('format_cldr') && DateTime::Locale::root->can('date_format_full');
+
+ if ($localized_datetime_tests) {
+
+ # Include RT::Date::LocalizedDateTime tests
+ $tests += 7;
+ }
+}
+
+use warnings; use strict;
+use RT::Test tests => $tests;
+use RT::User;
+use Test::Warn;
+
+use_ok('RT::Date');
+{
+ my $date = RT::Date->new($RT::SystemUser);
+ isa_ok($date, 'RT::Date', "constructor returned RT::Date oject");
+ $date = $date->new($RT::SystemUser);
+ isa_ok($date, 'RT::Date', "constructor returned RT::Date oject");
+}
+
+{
+ # set timezone in all places to UTC
+ $RT::SystemUser->UserObj->__Set(Field => 'Timezone', Value => 'UTC')
+ if $RT::SystemUser->UserObj->Timezone;
+ RT->Config->Set( Timezone => 'UTC' );
+}
+
+my $current_user;
+{
+ my $user = RT::User->new($RT::SystemUser);
+ my($uid, $msg) = $user->Create(
+ Name => "date_api". rand(200),
+ Lang => 'en',
+ Privileged => 1,
+ );
+ ok($uid, "user was created") or diag("error: $msg");
+ $current_user = RT::CurrentUser->new($user);
+}
+
+{
+ my $date = RT::Date->new( $current_user );
+ is($date->Timezone, 'UTC', "dropped all timzones to UTC");
+ is($date->Timezone('user'), 'UTC', "dropped all timzones to UTC");
+ is($date->Timezone('server'), 'UTC', "dropped all timzones to UTC");
+ is($date->Timezone('unknown'), 'UTC', "with wrong context returns UTC");
+
+ $current_user->UserObj->__Set( Field => 'Timezone', Value => 'Europe/Moscow');
+ is($current_user->UserObj->Timezone,
+ 'Europe/Moscow',
+ "successfuly changed user's timezone");
+ is($date->Timezone('user'),
+ 'Europe/Moscow',
+ "in user context returns user's timezone");
+ is($date->Timezone, 'UTC', "the deafult value is always UTC");
+ is($date->Timezone('server'), 'UTC', "wasn't changed");
+
+ RT->Config->Set( Timezone => 'Africa/Ouagadougou' );
+ is($date->Timezone('server'),
+ 'Africa/Ouagadougou',
+ "timezone of the RT server was changed");
+ is($date->Timezone('user'),
+ 'Europe/Moscow',
+ "in user context still returns user's timezone");
+ is($date->Timezone, 'UTC', "the deafult value is always UTC");
+
+ $current_user->UserObj->__Set( Field => 'Timezone', Value => '');
+ is_empty($current_user->UserObj->Timezone,
+ "successfuly changed user's timezone");
+ is($date->Timezone('user'),
+ 'Africa/Ouagadougou',
+ "in user context returns timezone of the server if user's one is not defined");
+ is($date->Timezone, 'UTC', "the deafult value is always UTC");
+
+ RT->Config->Set( Timezone => 'GMT' );
+ is($date->Timezone('server'),
+ 'UTC',
+ "timezone is GMT which one is alias for UTC");
+
+ RT->Config->Set( Timezone => '' );
+ is($date->Timezone, 'UTC', "dropped all timzones to UTC");
+ is($date->Timezone('user'),
+ 'UTC',
+ "user's and server's timzones are not defined, so UTC");
+ is($date->Timezone('server'),
+ 'UTC',
+ "timezone of the server is not defined so UTC");
+
+ RT->Config->Set( Timezone => 'UTC' );
+}
+
+{
+ my $date = RT::Date->new($RT::SystemUser);
+ is($date->Unix, 0, "new date returns 0 in Unix format");
+ is($date->Get, '1970-01-01 00:00:00', "default is ISO format");
+ is($date->Get(Format =>'SomeBadFormat'),
+ '1970-01-01 00:00:00',
+ "don't know format, return ISO format");
+ is($date->Get(Format =>'W3CDTF'),
+ '1970-01-01T00:00:00Z',
+ "W3CDTF format with defaults");
+ is($date->Get(Format =>'RFC2822'),
+ 'Thu, 1 Jan 1970 00:00:00 +0000',
+ "RFC2822 format with defaults");
+ is($date->Get(Format =>'LocalizedDateTime'),
+ 'Thu, Jan 1, 1970 12:00:00 AM',
+ "LocalizedDateTime format with defaults") if ( $localized_datetime_tests );
+
+ is($date->ISO(Time => 0),
+ '1970-01-01',
+ "ISO format without time part");
+ is($date->W3CDTF(Time => 0),
+ '1970-01-01',
+ "W3CDTF format without time part");
+ is($date->RFC2822(Time => 0),
+ 'Thu, 1 Jan 1970',
+ "RFC2822 format without time part");
+ is($date->LocalizedDateTime(Time => 0),
+ 'Thu, Jan 1, 1970',
+ "LocalizedDateTime format without time part") if ( $localized_datetime_tests );
+
+ is($date->ISO(Date => 0),
+ '00:00:00',
+ "ISO format without date part");
+ is($date->W3CDTF(Date => 0),
+ '1970-01-01T00:00:00Z',
+ "W3CDTF format is incorrect without date part");
+ is($date->RFC2822(Date => 0),
+ '00:00:00 +0000',
+ "RFC2822 format without date part");
+ is($date->LocalizedDateTime(Date => 0),
+ '12:00:00 AM',
+ "LocalizedDateTime format without date part") if ( $localized_datetime_tests );
+
+ is($date->ISO(Date => 0, Seconds => 0),
+ '00:00',
+ "ISO format without date part and seconds");
+ is($date->W3CDTF(Date => 0, Seconds => 0),
+ '1970-01-01T00:00Z',
+ "W3CDTF format without seconds, but we ship date part even if Date is false");
+ is($date->RFC2822(Date => 0, Seconds => 0),
+ '00:00 +0000',
+ "RFC2822 format without date part and seconds");
+
+ is($date->RFC2822(DayOfWeek => 0),
+ '1 Jan 1970 00:00:00 +0000',
+ "RFC2822 format without 'day of week' part");
+ is($date->RFC2822(DayOfWeek => 0, Date => 0),
+ '00:00:00 +0000',
+ "RFC2822 format without 'day of week' and date parts(corner case test)");
+
+ is($date->LocalizedDateTime(AbbrDay => 0),
+ 'Thursday, Jan 1, 1970 12:00:00 AM',
+ "LocalizedDateTime format without abbreviation of day") if ( $localized_datetime_tests );
+ is($date->LocalizedDateTime(AbbrMonth => 0),
+ 'Thu, January 1, 1970 12:00:00 AM',
+ "LocalizedDateTime format without abbreviation of month") if ( $localized_datetime_tests );
+ is($date->LocalizedDateTime(DateFormat => 'date_format_short'),
+ '1/1/70 12:00:00 AM',
+ "LocalizedDateTime format with non default DateFormat") if ( $localized_datetime_tests );
+ is($date->LocalizedDateTime(TimeFormat => 'time_format_short'),
+ 'Thu, Jan 1, 1970 12:00 AM',
+ "LocalizedDateTime format with non default TimeFormat") if ( $localized_datetime_tests );
+
+ is($date->Date,
+ '1970-01-01',
+ "the default format for the 'Date' method is ISO");
+ is($date->Date(Format => 'W3CDTF'),
+ '1970-01-01',
+ "'Date' method, W3CDTF format");
+ is($date->Date(Format => 'RFC2822'),
+ 'Thu, 1 Jan 1970',
+ "'Date' method, RFC2822 format");
+ is($date->Date(Time => 1),
+ '1970-01-01',
+ "'Date' method doesn't pass through 'Time' argument");
+ is($date->Date(Date => 0),
+ '1970-01-01',
+ "'Date' method overrides 'Date' argument");
+
+ is($date->Time,
+ '00:00:00',
+ "the default format for the 'Time' method is ISO");
+ is($date->Time(Format => 'W3CDTF'),
+ '1970-01-01T00:00:00Z',
+ "'Time' method, W3CDTF format, date part is required by w3c doc");
+ is($date->Time(Format => 'RFC2822'),
+ '00:00:00 +0000',
+ "'Time' method, RFC2822 format");
+ is($date->Time(Date => 1),
+ '00:00:00',
+ "'Time' method doesn't pass through 'Date' argument");
+ is($date->Time(Time => 0),
+ '00:00:00',
+ "'Time' method overrides 'Time' argument");
+
+ is($date->DateTime,
+ '1970-01-01 00:00:00',
+ "the default format for the 'DateTime' method is ISO");
+ is($date->DateTime(Format =>'W3CDTF'),
+ '1970-01-01T00:00:00Z',
+ "'DateTime' method, W3CDTF format");
+ is($date->DateTime(Format =>'RFC2822'),
+ 'Thu, 1 Jan 1970 00:00:00 +0000',
+ "'DateTime' method, RFC2822 format");
+ is($date->DateTime(Date => 0, Time => 0),
+ '1970-01-01 00:00:00',
+ "the 'DateTime' method overrides both 'Date' and 'Time' arguments");
+}
+
+
+{ # positive timezone
+ $current_user->UserObj->__Set( Field => 'Timezone', Value => 'Europe/Moscow');
+ my $date = RT::Date->new( $current_user );
+ $date->Set( Format => 'ISO', Timezone => 'utc', Value => '2005-01-01 15:10:00' );
+ is($date->ISO( Timezone => 'user' ), '2005-01-01 18:10:00', "ISO");
+ is($date->W3CDTF( Timezone => 'user' ), '2005-01-01T18:10:00+03:00', "W3C DTF");
+ is($date->RFC2822( Timezone => 'user' ), 'Sat, 1 Jan 2005 18:10:00 +0300', "RFC2822");
+
+ # DST
+ $date = RT::Date->new( $current_user );
+ $date->Set( Format => 'ISO', Timezone => 'utc', Value => '2005-07-01 15:10:00' );
+ is($date->ISO( Timezone => 'user' ), '2005-07-01 19:10:00', "ISO");
+ is($date->W3CDTF( Timezone => 'user' ), '2005-07-01T19:10:00+04:00', "W3C DTF");
+ is($date->RFC2822( Timezone => 'user' ), 'Fri, 1 Jul 2005 19:10:00 +0400', "RFC2822");
+}
+
+{ # negative timezone
+ $current_user->UserObj->__Set( Field => 'Timezone', Value => 'America/New_York');
+ my $date = RT::Date->new( $current_user );
+ $date->Set( Format => 'ISO', Timezone => 'utc', Value => '2005-01-01 15:10:00' );
+ is($date->ISO( Timezone => 'user' ), '2005-01-01 10:10:00', "ISO");
+ is($date->W3CDTF( Timezone => 'user' ), '2005-01-01T10:10:00-05:00', "W3C DTF");
+ is($date->RFC2822( Timezone => 'user' ), 'Sat, 1 Jan 2005 10:10:00 -0500', "RFC2822");
+
+ # DST
+ $date = RT::Date->new( $current_user );
+ $date->Set( Format => 'ISO', Timezone => 'utc', Value => '2005-07-01 15:10:00' );
+ is($date->ISO( Timezone => 'user' ), '2005-07-01 11:10:00', "ISO");
+ is($date->W3CDTF( Timezone => 'user' ), '2005-07-01T11:10:00-04:00', "W3C DTF");
+ is($date->RFC2822( Timezone => 'user' ), 'Fri, 1 Jul 2005 11:10:00 -0400', "RFC2822");
+}
+
+warning_like
+{ # bad format
+ my $date = RT::Date->new($RT::SystemUser);
+ $date->Set( Format => 'bad' );
+ is($date->Unix, 0, "bad format");
+} qr'Unknown Date format: bad';
+
+
+{ # setting value via Unix method
+ my $date = RT::Date->new($RT::SystemUser);
+ $date->Unix(1);
+ is($date->ISO, '1970-01-01 00:00:01', "correct value");
+
+ foreach (undef, 0, ''){
+ $date->Unix(1);
+ is($date->ISO, '1970-01-01 00:00:01', "correct value");
+
+ $date->Set(Format => 'unix', Value => $_);
+ is($date->ISO, '1970-01-01 00:00:00', "Set a date to midnight 1/1/1970 GMT due to wrong call");
+ is($date->Unix, 0, "unix is 0 => unset");
+ }
+}
+
+my $year = (localtime(time))[5] + 1900;
+
+{ # set+ISO format
+ my $date = RT::Date->new($RT::SystemUser);
+ warning_like {
+ $date->Set(Format => 'ISO', Value => 'weird date');
+ } qr/Couldn't parse date 'weird date' as a ISO format/;
+ is($date->Unix, 0, "date was wrong => unix == 0");
+
+ # XXX: ISO format has more feature than we suport
+ # http://www.cl.cam.ac.uk/~mgk25/iso-time.html
+
+ $date->Set(Format => 'ISO', Value => '2005-11-28 15:10:00');
+ is($date->ISO, '2005-11-28 15:10:00', "YYYY-DD-MM hh:mm:ss");
+
+ $date->Set(Format => 'ISO', Value => '2005-11-28 15:10:00+00');
+ is($date->ISO, '2005-11-28 15:10:00', "YYYY-DD-MM hh:mm:ss+00");
+
+ $date->Set(Format => 'ISO', Value => '11-28 15:10:00');
+ is($date->ISO, $year .'-11-28 15:10:00', "DD-MM hh:mm:ss");
+
+ $date->Set(Format => 'ISO', Value => '11-28 15:10:00+00');
+ is($date->ISO, $year .'-11-28 15:10:00', "DD-MM hh:mm:ss+00");
+
+ $date->Set(Format => 'ISO', Value => '20051128151000');
+ is($date->ISO, '2005-11-28 15:10:00', "YYYYDDMMhhmmss");
+
+ $date->Set(Format => 'ISO', Value => '1128151000');
+ is($date->ISO, $year .'-11-28 15:10:00', "DDMMhhmmss");
+
+ $date->Set(Format => 'ISO', Value => '2005112815:10:00');
+ is($date->ISO, '2005-11-28 15:10:00', "YYYYDDMMhh:mm:ss");
+
+ $date->Set(Format => 'ISO', Value => '112815:10:00');
+ is($date->ISO, $year .'-11-28 15:10:00', "DDMMhh:mm:ss");
+
+ $date->Set(Format => 'ISO', Value => '2005-13-28 15:10:00');
+ is($date->Unix, 0, "wrong month value");
+
+ $date->Set(Format => 'ISO', Value => '2005-00-28 15:10:00');
+ is($date->Unix, 0, "wrong month value");
+
+ $date->Set(Format => 'ISO', Value => '1960-01-28 15:10:00');
+ is($date->Unix, 0, "too old, we don't support");
+}
+
+{ # set+datemanip format(Time::ParseDate)
+ my $date = RT::Date->new($RT::SystemUser);
+ $date->Set(Format => 'unknown', Value => 'weird date');
+ is($date->Unix, 0, "date was wrong");
+
+ RT->Config->Set( Timezone => 'Europe/Moscow' );
+ $date->Set(Format => 'datemanip', Value => '2005-11-28 15:10:00');
+ is($date->ISO, '2005-11-28 12:10:00', "YYYY-DD-MM hh:mm:ss");
+
+ RT->Config->Set( Timezone => 'UTC' );
+ $date->Set(Format => 'datemanip', Value => '2005-11-28 15:10:00');
+ is($date->ISO, '2005-11-28 15:10:00', "YYYY-DD-MM hh:mm:ss");
+
+ $current_user->UserObj->__Set( Field => 'Timezone', Value => 'Europe/Moscow');
+ $date = RT::Date->new( $current_user );
+ $date->Set(Format => 'datemanip', Value => '2005-11-28 15:10:00');
+ is($date->ISO, '2005-11-28 12:10:00', "YYYY-DD-MM hh:mm:ss");
+}
+
+{ # set+unknown format(Time::ParseDate)
+ my $date = RT::Date->new($RT::SystemUser);
+ $date->Set(Format => 'unknown', Value => 'weird date');
+ is($date->Unix, 0, "date was wrong");
+
+ RT->Config->Set( Timezone => 'Europe/Moscow' );
+ $date->Set(Format => 'unknown', Value => '2005-11-28 15:10:00');
+ is($date->ISO, '2005-11-28 12:10:00', "YYYY-DD-MM hh:mm:ss");
+
+ $date->Set(Format => 'unknown', Value => '2005-11-28 15:10:00', Timezone => 'utc' );
+ is($date->ISO, '2005-11-28 15:10:00', "YYYY-DD-MM hh:mm:ss");
+
+ # test relative dates
+ {
+ set_fixed_time("2005-11-28T15:10:00Z");
+ $date->Set(Format => 'unknown', Value => 'now');
+ is($date->ISO, '2005-11-28 15:10:00', "YYYY-DD-MM hh:mm:ss");
+
+ $date->Set(Format => 'unknown', Value => '1 day ago');
+ is($date->ISO, '2005-11-27 15:10:00', "YYYY-DD-MM hh:mm:ss");
+ restore_time();
+ }
+
+ RT->Config->Set( Timezone => 'UTC' );
+ $date->Set(Format => 'unknown', Value => '2005-11-28 15:10:00');
+ is($date->ISO, '2005-11-28 15:10:00', "YYYY-DD-MM hh:mm:ss");
+
+ $current_user->UserObj->__Set( Field => 'Timezone', Value => 'Europe/Moscow');
+ $date = RT::Date->new( $current_user );
+ $date->Set(Format => 'unknown', Value => '2005-11-28 15:10:00');
+ is($date->ISO, '2005-11-28 12:10:00', "YYYY-DD-MM hh:mm:ss");
+ $date->Set(Format => 'unknown', Value => '2005-11-28 15:10:00', Timezone => 'server' );
+ is($date->ISO, '2005-11-28 15:10:00', "YYYY-DD-MM hh:mm:ss");
+ $date->Set(Format => 'unknown', Value => '2005-11-28 15:10:00', Timezone => 'utc' );
+ is($date->ISO, '2005-11-28 15:10:00', "YYYY-DD-MM hh:mm:ss");
+}
+
+{ # SetToMidnight
+ my $date = RT::Date->new($RT::SystemUser);
+
+ RT->Config->Set( Timezone => 'Europe/Moscow' );
+ $date->Set(Format => 'ISO', Value => '2005-11-28 15:10:00');
+ $date->SetToMidnight;
+ is($date->ISO, '2005-11-28 00:00:00', "default is utc");
+ $date->Set(Format => 'ISO', Value => '2005-11-28 15:10:00');
+ $date->SetToMidnight(Timezone => 'utc');
+ is($date->ISO, '2005-11-28 00:00:00', "utc context");
+ $date->Set(Format => 'ISO', Value => '2005-11-28 15:10:00');
+ $date->SetToMidnight(Timezone => 'user');
+ is($date->ISO, '2005-11-27 21:00:00', "user context, user has no preference, fallback to server");
+ $date->Set(Format => 'ISO', Value => '2005-11-28 15:10:00');
+ $date->SetToMidnight(Timezone => 'server');
+ is($date->ISO, '2005-11-27 21:00:00', "server context");
+
+ $current_user->UserObj->__Set( Field => 'Timezone', Value => 'Europe/Moscow');
+ $date = RT::Date->new( $current_user );
+ $date->Set(Format => 'ISO', Value => '2005-11-28 15:10:00');
+ $date->SetToMidnight;
+ is($date->ISO, '2005-11-28 00:00:00', "default is utc");
+ $date->Set(Format => 'ISO', Value => '2005-11-28 15:10:00');
+ $date->SetToMidnight(Timezone => 'utc');
+ is($date->ISO, '2005-11-28 00:00:00', "utc context");
+ $date->Set(Format => 'ISO', Value => '2005-11-28 15:10:00');
+ $date->SetToMidnight(Timezone => 'user');
+ is($date->ISO, '2005-11-27 21:00:00', "user context");
+ $date->SetToMidnight(Timezone => 'server');
+ is($date->ISO, '2005-11-27 21:00:00', "server context");
+
+ RT->Config->Set( Timezone => 'UTC' );
+}
+
+{ # SetToNow
+ my $date = RT::Date->new($RT::SystemUser);
+ my $time = time;
+ $date->SetToNow;
+ ok($date->Unix >= $time, 'close enough');
+ ok($date->Unix < $time+5, 'difference is less than five seconds');
+}
+
+{
+ my $date = RT::Date->new($RT::SystemUser);
+
+ $date->Unix(0);
+ $date->AddSeconds;
+ is($date->ISO, '1970-01-01 00:00:00', "nothing changed");
+ $date->AddSeconds(0);
+ is($date->ISO, '1970-01-01 00:00:00', "nothing changed");
+
+ $date->Unix(0);
+ $date->AddSeconds(5);
+ is($date->ISO, '1970-01-01 00:00:05', "added five seconds");
+ $date->AddSeconds(-2);
+ is($date->ISO, '1970-01-01 00:00:03', "substracted two seconds");
+
+ $date->Unix(0);
+ $date->AddSeconds(3661);
+ is($date->ISO, '1970-01-01 01:01:01', "added one hour, minute and a second");
+
+# XXX: TODO, doesn't work with Test::Warn
+# TODO: {
+# local $TODO = "BUG or subject to change Date handling to support unix time <= 0";
+# $date->Unix(0);
+# $date->AddSeconds(-2);
+# ok($date->Unix > 0);
+# }
+
+ $date->Unix(0);
+ $date->AddDay;
+ is($date->ISO, '1970-01-02 00:00:00', "added one day");
+ $date->AddDays(2);
+ is($date->ISO, '1970-01-04 00:00:00', "added two days");
+ $date->AddDays(-1);
+ is($date->ISO, '1970-01-03 00:00:00', "substructed one day");
+
+ $date->Unix(0);
+ $date->AddDays(31);
+ is($date->ISO, '1970-02-01 00:00:00', "added one month");
+}
+
+{
+ $current_user->UserObj->__Set( Field => 'Timezone', Value => '');
+ my $date = RT::Date->new( $current_user );
+ is($date->AsString, "Not set", "AsString returns 'Not set'");
+
+ RT->Config->Set( DateTimeFormat => '');
+ $date->Unix(1);
+ is($date->AsString, 'Thu Jan 01 00:00:01 1970', "correct string");
+ is($date->AsString(Date => 0), '00:00:01', "correct string");
+ is($date->AsString(Time => 0), 'Thu Jan 01 1970', "correct string");
+ is($date->AsString(Date => 0, Time => 0), 'Thu Jan 01 00:00:01 1970', "invalid input");
+
+ RT->Config->Set( DateTimeFormat => 'RFC2822' );
+ $date->Unix(1);
+ is($date->AsString, 'Thu, 1 Jan 1970 00:00:01 +0000', "correct string");
+
+ RT->Config->Set( DateTimeFormat => { Format => 'RFC2822', Seconds => 0 } );
+ $date->Unix(1);
+ is($date->AsString, 'Thu, 1 Jan 1970 00:00 +0000', "correct string");
+ is($date->AsString(Seconds => 1), 'Thu, 1 Jan 1970 00:00:01 +0000', "correct string");
+}
+
+{ # DurationAsString
+ my $date = RT::Date->new($RT::SystemUser);
+
+ is($date->DurationAsString(1), '1 sec', '1 sec');
+ is($date->DurationAsString(59), '59 sec', '59 sec');
+ is($date->DurationAsString(60), '1 min', '1 min');
+ is($date->DurationAsString(60*119), '119 min', '119 min');
+ is($date->DurationAsString(60*60*2-1), '120 min', '120 min');
+ is($date->DurationAsString(60*60*2), '2 hours', '2 hours');
+ is($date->DurationAsString(60*60*48-1), '48 hours', '48 hours');
+ is($date->DurationAsString(60*60*48), '2 days', '2 days');
+ is($date->DurationAsString(60*60*24*14-1), '14 days', '14 days');
+ is($date->DurationAsString(60*60*24*14), '2 weeks', '2 weeks');
+ is($date->DurationAsString(60*60*24*7*8-1), '8 weeks', '8 weeks');
+ is($date->DurationAsString(60*60*24*61), '2 months', '2 months');
+ is($date->DurationAsString(60*60*24*365-1), '12 months', '12 months');
+ is($date->DurationAsString(60*60*24*366), '1 years', '1 years');
+
+ is($date->DurationAsString(-1), '1 sec ago', '1 sec ago');
+}
+
+{ # DiffAsString
+ my $date = RT::Date->new($RT::SystemUser);
+ is($date->DiffAsString(1), '', 'no diff, wrong input');
+ is($date->DiffAsString(-1), '', 'no diff, wrong input');
+ is($date->DiffAsString('qwe'), '', 'no diff, wrong input');
+
+ $date->Unix(2);
+ is($date->DiffAsString(-1), '', 'no diff, wrong input');
+
+ is($date->DiffAsString(3), '1 sec ago', 'diff: 1 sec ago');
+ is($date->DiffAsString(1), '1 sec', 'diff: 1 sec');
+
+ my $ndate = RT::Date->new($RT::SystemUser);
+ is($date->DiffAsString($ndate), '', 'no diff, wrong input');
+ $ndate->Unix(3);
+ is($date->DiffAsString($ndate), '1 sec ago', 'diff: 1 sec ago');
+}
+
+{ # Diff
+ my $date = RT::Date->new($RT::SystemUser);
+ $date->SetToNow;
+ my $diff = $date->Diff;
+ ok($diff <= 0, 'close enought');
+ ok($diff > -5, 'close enought');
+}
+
+{ # AgeAsString
+ my $date = RT::Date->new($RT::SystemUser);
+ $date->SetToNow;
+ my $diff = $date->AgeAsString;
+ like($diff, qr/^(0 sec|[1-5] sec ago)$/, 'close enought');
+}
+
+{ # GetWeekday
+ my $date = RT::Date->new($RT::SystemUser);
+ is($date->GetWeekday(7), '', '7 and greater are invalid');
+ is($date->GetWeekday(6), 'Sat', '6 is Saturday');
+ is($date->GetWeekday(0), 'Sun', '0 is Sunday');
+ is($date->GetWeekday(-1), 'Sat', '-1 is Saturday');
+ is($date->GetWeekday(-7), 'Sun', '-7 is Sunday');
+ is($date->GetWeekday(-8), '', '-8 and lesser are invalid');
+}
+
+{ # GetMonth
+ my $date = RT::Date->new($RT::SystemUser);
+ is($date->GetMonth(12), '', '12 and greater are invalid');
+ is($date->GetMonth(11), 'Dec', '11 is December');
+ is($date->GetMonth(0), 'Jan', '0 is January');
+ is($date->GetMonth(-1), 'Dec', '11 is December');
+ is($date->GetMonth(-12), 'Jan', '0 is January');
+ is($date->GetMonth(-13), '', '-13 and lesser are invalid');
+}
+
+#TODO: AsString
+#TODO: RFC2822, W3CDTF with Timezones
+
+exit(0);
+
diff --git a/rt/t/api/emailparser.t b/rt/t/api/emailparser.t
new file mode 100644
index 000000000..4807138cc
--- /dev/null
+++ b/rt/t/api/emailparser.t
@@ -0,0 +1,32 @@
+
+use strict;
+use warnings;
+use RT;
+use RT::Test tests => 4;
+
+
+{
+
+ok(require RT::EmailParser);
+
+
+}
+
+{
+
+is(RT::EmailParser::IsRTAddress("","rt\@example.com"),1, "Regexp matched rt address" );
+is(RT::EmailParser::IsRTAddress("","frt\@example.com"),undef, "Regexp didn't match non-rt address" );
+
+
+}
+
+{
+
+my @before = ("rt\@example.com", "frt\@example.com");
+my @after = ("frt\@example.com");
+ok(eq_array(RT::EmailParser::CullRTAddresses("",@before),@after), "CullRTAddresses only culls RT addresses");
+
+
+}
+
+1;
diff --git a/rt/t/api/group.t b/rt/t/api/group.t
new file mode 100644
index 000000000..551d4f1a0
--- /dev/null
+++ b/rt/t/api/group.t
@@ -0,0 +1,99 @@
+
+use strict;
+use warnings;
+use RT;
+use RT::Test tests => 38;
+
+
+{
+
+# {{{ Tests
+ok (require RT::Group);
+
+ok (my $group = RT::Group->new($RT::SystemUser), "instantiated a group object");
+ok (my ($id, $msg) = $group->CreateUserDefinedGroup( Name => 'TestGroup', Description => 'A test group',
+ ), 'Created a new group');
+isnt ($id , 0, "Group id is $id");
+is ($group->Name , 'TestGroup', "The group's name is 'TestGroup'");
+my $ng = RT::Group->new($RT::SystemUser);
+
+ok($ng->LoadUserDefinedGroup('TestGroup'), "Loaded testgroup");
+is($ng->id , $group->id, "Loaded the right group");
+
+
+ok (($id,$msg) = $ng->AddMember('1'), "Added a member to the group");
+ok($id, $msg);
+ok (($id,$msg) = $ng->AddMember('2' ), "Added a member to the group");
+ok($id, $msg);
+ok (($id,$msg) = $ng->AddMember('3' ), "Added a member to the group");
+ok($id, $msg);
+
+# Group 1 now has members 1, 2 ,3
+
+my $group_2 = RT::Group->new($RT::SystemUser);
+ok (my ($id_2, $msg_2) = $group_2->CreateUserDefinedGroup( Name => 'TestGroup2', Description => 'A second test group'), , 'Created a new group');
+isnt ($id_2 , 0, "Created group 2 ok- $msg_2 ");
+ok (($id,$msg) = $group_2->AddMember($ng->PrincipalId), "Made TestGroup a member of testgroup2");
+ok($id, $msg);
+ok (($id,$msg) = $group_2->AddMember('1' ), "Added member RT_System to the group TestGroup2");
+ok($id, $msg);
+
+# Group 2 how has 1, g1->{1, 2,3}
+
+my $group_3 = RT::Group->new($RT::SystemUser);
+ok (my ($id_3, $msg_3) = $group_3->CreateUserDefinedGroup( Name => 'TestGroup3', Description => 'A second test group'), 'Created a new group');
+isnt ($id_3 , 0, "Created group 3 ok - $msg_3");
+ok (($id,$msg) =$group_3->AddMember($group_2->PrincipalId), "Made TestGroup a member of testgroup2");
+ok($id, $msg);
+
+# g3 now has g2->{1, g1->{1,2,3}}
+
+my $principal_1 = RT::Principal->new($RT::SystemUser);
+$principal_1->Load('1');
+
+my $principal_2 = RT::Principal->new($RT::SystemUser);
+$principal_2->Load('2');
+
+ok (($id,$msg) = $group_3->AddMember('1' ), "Added member RT_System to the group TestGroup2");
+ok($id, $msg);
+
+# g3 now has 1, g2->{1, g1->{1,2,3}}
+
+is($group_3->HasMember($principal_2), undef, "group 3 doesn't have member 2");
+ok($group_3->HasMemberRecursively($principal_2), "group 3 has member 2 recursively");
+ok($ng->HasMember($principal_2) , "group ".$ng->Id." has member 2");
+my ($delid , $delmsg) =$ng->DeleteMember($principal_2->Id);
+isnt ($delid ,0, "Sucessfully deleted it-".$delid."-".$delmsg);
+
+#Gotta reload the group objects, since we've been messing with various internals.
+# we shouldn't need to do this.
+#$ng->LoadUserDefinedGroup('TestGroup');
+#$group_2->LoadUserDefinedGroup('TestGroup2');
+#$group_3->LoadUserDefinedGroup('TestGroup');
+
+# G1 now has 1, 3
+# Group 2 how has 1, g1->{1, 3}
+# g3 now has 1, g2->{1, g1->{1, 3}}
+
+ok(!$ng->HasMember($principal_2) , "group ".$ng->Id." no longer has member 2");
+is($group_3->HasMemberRecursively($principal_2), undef, "group 3 doesn't have member 2");
+is($group_2->HasMemberRecursively($principal_2), undef, "group 2 doesn't have member 2");
+is($ng->HasMember($principal_2), undef, "group 1 doesn't have member 2");
+is($group_3->HasMemberRecursively($principal_2), undef, "group 3 has member 2 recursively");
+
+# }}}
+
+
+}
+
+{
+
+ok(my $u = RT::Group->new($RT::SystemUser));
+ok($u->Load(4), "Loaded the first user");
+is($u->PrincipalObj->ObjectId , 4, "user 4 is the fourth principal");
+is($u->PrincipalObj->PrincipalType , 'Group' , "Principal 4 is a group");
+
+
+}
+
+1;
diff --git a/rt/t/api/groups.t b/rt/t/api/groups.t
new file mode 100644
index 000000000..995c844ba
--- /dev/null
+++ b/rt/t/api/groups.t
@@ -0,0 +1,139 @@
+
+use strict;
+use warnings;
+use RT;
+use RT::Test tests => 28;
+
+
+{
+
+ok (require RT::Groups);
+
+
+}
+
+{
+
+# next had bugs
+# Groups->Limit( FIELD => 'id', OPERATOR => '!=', VALUE => xx );
+my $g = RT::Group->new($RT::SystemUser);
+my ($id, $msg) = $g->CreateUserDefinedGroup(Name => 'GroupsNotEqualTest');
+ok ($id, "created group #". $g->id) or diag("error: $msg");
+
+my $groups = RT::Groups->new($RT::SystemUser);
+$groups->Limit( FIELD => 'id', OPERATOR => '!=', VALUE => $g->id );
+$groups->LimitToUserDefinedGroups();
+my $bug = grep $_->id == $g->id, @{$groups->ItemsArrayRef};
+ok (!$bug, "didn't find group");
+
+
+}
+
+{
+
+my $u = RT::User->new($RT::SystemUser);
+my ($id, $msg) = $u->Create( Name => 'Membertests'. $$ );
+ok ($id, 'created user') or diag "error: $msg";
+
+my $g = RT::Group->new($RT::SystemUser);
+($id, $msg) = $g->CreateUserDefinedGroup(Name => 'Membertests');
+ok ($id, $msg);
+
+my ($aid, $amsg) =$g->AddMember($u->id);
+ok ($aid, $amsg);
+ok($g->HasMember($u->PrincipalObj),"G has member u");
+
+my $groups = RT::Groups->new($RT::SystemUser);
+$groups->LimitToUserDefinedGroups();
+$groups->WithMember(PrincipalId => $u->id);
+is ($groups->Count , 1,"found the 1 group - " . $groups->Count);
+is ($groups->First->Id , $g->Id, "it's the right one");
+
+
+}
+
+{
+ no warnings qw/redefine once/;
+
+my $q = RT::Queue->new($RT::SystemUser);
+my ($id, $msg) =$q->Create( Name => 'GlobalACLTest');
+ok ($id, $msg);
+
+my $testuser = RT::User->new($RT::SystemUser);
+($id,$msg) = $testuser->Create(Name => 'JustAnAdminCc');
+ok ($id,$msg);
+
+my $global_admin_cc = RT::Group->new($RT::SystemUser);
+$global_admin_cc->LoadSystemRoleGroup('AdminCc');
+ok($global_admin_cc->id, "Found the global admincc group");
+my $groups = RT::Groups->new($RT::SystemUser);
+$groups->WithRight(Right => 'OwnTicket', Object => $q);
+is($groups->Count, 1);
+($id, $msg) = $global_admin_cc->PrincipalObj->GrantRight(Right =>'OwnTicket', Object=> $RT::System);
+ok ($id,$msg);
+ok (!$testuser->HasRight(Object => $q, Right => 'OwnTicket') , "The test user does not have the right to own tickets in the test queue");
+($id, $msg) = $q->AddWatcher(Type => 'AdminCc', PrincipalId => $testuser->id);
+ok($id,$msg);
+ok ($testuser->HasRight(Object => $q, Right => 'OwnTicket') , "The test user does have the right to own tickets now. thank god.");
+
+$groups = RT::Groups->new($RT::SystemUser);
+$groups->WithRight(Right => 'OwnTicket', Object => $q);
+ok ($id,$msg);
+is($groups->Count, 3);
+
+my $RTxGroup = RT::Group->new($RT::SystemUser);
+($id, $msg) = $RTxGroup->CreateUserDefinedGroup( Name => 'RTxGroup', Description => "RTx extension group");
+ok ($id,$msg);
+is ($RTxGroup->id, $id, "group loaded");
+
+my $RTxSysObj = {};
+bless $RTxSysObj, 'RTx::System';
+*RTx::System::Id = sub { 1; };
+*RTx::System::id = *RTx::System::Id;
+my $ace = RT::Record->new($RT::SystemUser);
+$ace->Table('ACL');
+$ace->_BuildTableAttributes unless ($RT::Record::_TABLE_ATTR->{ref($ace)});
+($id, $msg) = $ace->Create( PrincipalId => $RTxGroup->id, PrincipalType => 'Group', RightName => 'RTxGroupRight', ObjectType => 'RTx::System', ObjectId => 1);
+ok ($id, "ACL for RTxSysObj created");
+
+my $RTxObj = {};
+bless $RTxObj, 'RTx::System::Record';
+*RTx::System::Record::Id = sub { 4; };
+*RTx::System::Record::id = *RTx::System::Record::Id;
+
+$groups = RT::Groups->new($RT::SystemUser);
+$groups->WithRight(Right => 'RTxGroupRight', Object => $RTxSysObj);
+is($groups->Count, 1, "RTxGroupRight found for RTxSysObj");
+
+$groups = RT::Groups->new($RT::SystemUser);
+$groups->WithRight(Right => 'RTxGroupRight', Object => $RTxObj);
+is($groups->Count, 0, "RTxGroupRight not found for RTxObj");
+
+$groups = RT::Groups->new($RT::SystemUser);
+$groups->WithRight(Right => 'RTxGroupRight', Object => $RTxObj, EquivObjects => [ $RTxSysObj ]);
+is($groups->Count, 1, "RTxGroupRight found for RTxObj using EquivObjects");
+
+$ace = RT::Record->new($RT::SystemUser);
+$ace->Table('ACL');
+$ace->_BuildTableAttributes unless ($RT::Record::_TABLE_ATTR->{ref($ace)});
+($id, $msg) = $ace->Create( PrincipalId => $RTxGroup->id, PrincipalType => 'Group', RightName => 'RTxGroupRight', ObjectType => 'RTx::System::Record', ObjectId => 5 );
+ok ($id, "ACL for RTxObj created");
+
+my $RTxObj2 = {};
+bless $RTxObj2, 'RTx::System::Record';
+*RTx::System::Record::Id = sub { 5; };
+
+$groups = RT::Groups->new($RT::SystemUser);
+$groups->WithRight(Right => 'RTxGroupRight', Object => $RTxObj2);
+is($groups->Count, 1, "RTxGroupRight found for RTxObj2");
+
+$groups = RT::Groups->new($RT::SystemUser);
+$groups->WithRight(Right => 'RTxGroupRight', Object => $RTxObj2, EquivObjects => [ $RTxSysObj ]);
+is($groups->Count, 1, "RTxGroupRight found for RTxObj2");
+
+
+
+
+}
+
+1;
diff --git a/rt/t/api/i18n.t b/rt/t/api/i18n.t
new file mode 100644
index 000000000..17d71b761
--- /dev/null
+++ b/rt/t/api/i18n.t
@@ -0,0 +1,30 @@
+
+use strict;
+use warnings;
+use RT;
+use RT::Test tests => 9;
+
+
+{
+
+use_ok ('RT::I18N');
+ok(RT::I18N->Init);
+
+
+}
+
+{
+
+ok(my $chinese = RT::I18N->get_handle('zh_tw'));
+ok(UNIVERSAL::can($chinese, 'maketext'));
+like($chinese->maketext('__Content-Type') , qr/utf-8/i, "Found the utf-8 charset for traditional chinese in the string ".$chinese->maketext('__Content-Type'));
+is($chinese->encoding , 'utf-8', "The encoding is 'utf-8' -".$chinese->encoding);
+
+ok(my $en = RT::I18N->get_handle('en'));
+ok(UNIVERSAL::can($en, 'maketext'));
+is($en->encoding , 'utf-8', "The encoding ".$en->encoding." is 'utf-8'");
+
+
+}
+
+1;
diff --git a/rt/t/api/link.t b/rt/t/api/link.t
new file mode 100644
index 000000000..1fd66bb64
--- /dev/null
+++ b/rt/t/api/link.t
@@ -0,0 +1,24 @@
+
+use strict;
+use warnings;
+use RT;
+use RT::Test tests => 5;
+
+
+{
+
+
+use RT::Link;
+my $link = RT::Link->new($RT::SystemUser);
+
+
+ok (ref $link);
+ok (UNIVERSAL::isa($link, 'RT::Link'));
+ok (UNIVERSAL::isa($link, 'RT::Base'));
+ok (UNIVERSAL::isa($link, 'RT::Record'));
+ok (UNIVERSAL::isa($link, 'DBIx::SearchBuilder::Record'));
+
+
+}
+
+1;
diff --git a/rt/t/api/queue.t b/rt/t/api/queue.t
new file mode 100644
index 000000000..44d5cafce
--- /dev/null
+++ b/rt/t/api/queue.t
@@ -0,0 +1,92 @@
+
+use strict;
+use warnings;
+use RT;
+use RT::Test tests => 24;
+
+
+{
+
+use RT::Queue;
+
+
+}
+
+{
+
+my $q = RT::Queue->new($RT::SystemUser);
+is($q->IsValidStatus('new'), 1, 'New is a valid status');
+is($q->IsValidStatus('f00'), 0, 'f00 is not a valid status');
+
+
+}
+
+{
+
+my $q = RT::Queue->new($RT::SystemUser);
+is($q->IsActiveStatus('new'), 1, 'New is a Active status');
+is($q->IsActiveStatus('rejected'), 0, 'Rejected is an inactive status');
+is($q->IsActiveStatus('f00'), 0, 'f00 is not a Active status');
+
+
+}
+
+{
+
+my $q = RT::Queue->new($RT::SystemUser);
+is($q->IsInactiveStatus('new'), 0, 'New is a Active status');
+is($q->IsInactiveStatus('rejected'), 1, 'rejeected is an Inactive status');
+is($q->IsInactiveStatus('f00'), 0, 'f00 is not a Active status');
+
+
+}
+
+{
+
+my $queue = RT::Queue->new($RT::SystemUser);
+my ($id, $val) = $queue->Create( Name => 'Test1');
+ok($id, $val);
+
+($id, $val) = $queue->Create( Name => '66');
+ok(!$id, $val);
+
+
+}
+
+{
+
+my $Queue = RT::Queue->new($RT::SystemUser);
+my ($id, $msg) = $Queue->Create(Name => "Foo");
+ok ($id, "Foo $id was created");
+ok(my $group = RT::Group->new($RT::SystemUser));
+ok($group->LoadQueueRoleGroup(Queue => $id, Type=> 'Requestor'));
+ok ($group->Id, "Found the requestors object for this Queue");
+
+{
+ my ($status, $msg) = $Queue->AddWatcher(Type => 'Cc', Email => 'bob@fsck.com');
+ ok ($status, "Added bob at fsck.com as a requestor") or diag "error: $msg";
+}
+
+ok(my $bob = RT::User->new($RT::SystemUser), "Creating a bob rt::user");
+$bob->LoadByEmail('bob@fsck.com');
+ok($bob->Id, "Found the bob rt user");
+ok ($Queue->IsWatcher(Type => 'Cc', PrincipalId => $bob->PrincipalId), "The Queue actually has bob at fsck.com as a requestor");
+
+{
+ my ($status, $msg) = $Queue->DeleteWatcher(Type =>'Cc', Email => 'bob@fsck.com');
+ ok ($status, "Deleted bob from Ccs") or diag "error: $msg";
+ ok (!$Queue->IsWatcher(Type => 'Cc', PrincipalId => $bob->PrincipalId),
+ "The Queue no longer has bob at fsck.com as a requestor");
+}
+
+$group = RT::Group->new($RT::SystemUser);
+ok($group->LoadQueueRoleGroup(Queue => $id, Type=> 'Cc'));
+ok ($group->Id, "Found the cc object for this Queue");
+$group = RT::Group->new($RT::SystemUser);
+ok($group->LoadQueueRoleGroup(Queue => $id, Type=> 'AdminCc'));
+ok ($group->Id, "Found the AdminCc object for this Queue");
+
+
+}
+
+1;
diff --git a/rt/t/api/record.t b/rt/t/api/record.t
new file mode 100644
index 000000000..6bf1af81e
--- /dev/null
+++ b/rt/t/api/record.t
@@ -0,0 +1,70 @@
+
+use strict;
+use warnings;
+use RT;
+use RT::Test tests => 22;
+
+
+{
+
+ok (require RT::Record);
+
+
+}
+
+{
+
+my $ticket = RT::Ticket->new($RT::SystemUser);
+my $group = RT::Group->new($RT::SystemUser);
+is($ticket->ObjectTypeStr, 'Ticket', "Ticket returns correct typestring");
+is($group->ObjectTypeStr, 'Group', "Group returns correct typestring");
+
+
+}
+
+{
+
+my $t1 = RT::Ticket->new($RT::SystemUser);
+my ($id, $trans, $msg) = $t1->Create(Subject => 'DepTest1', Queue => 'general');
+ok($id, "Created dep test 1 - $msg");
+
+my $t2 = RT::Ticket->new($RT::SystemUser);
+(my $id2, $trans, my $msg2) = $t2->Create(Subject => 'DepTest2', Queue => 'general');
+ok($id2, "Created dep test 2 - $msg2");
+my $t3 = RT::Ticket->new($RT::SystemUser);
+(my $id3, $trans, my $msg3) = $t3->Create(Subject => 'DepTest3', Queue => 'general', Type => 'approval');
+ok($id3, "Created dep test 3 - $msg3");
+my ($addid, $addmsg);
+ok (($addid, $addmsg) =$t1->AddLink( Type => 'DependsOn', Target => $t2->id));
+ok ($addid, $addmsg);
+ok (($addid, $addmsg) =$t1->AddLink( Type => 'DependsOn', Target => $t3->id));
+
+ok ($addid, $addmsg);
+my $link = RT::Link->new($RT::SystemUser);
+(my $rv, $msg) = $link->Load($addid);
+ok ($rv, $msg);
+is ($link->LocalTarget , $t3->id, "Link LocalTarget is correct");
+is ($link->LocalBase , $t1->id, "Link LocalBase is correct");
+
+ok ($t1->HasUnresolvedDependencies, "Ticket ".$t1->Id." has unresolved deps");
+ok (!$t1->HasUnresolvedDependencies( Type => 'blah' ), "Ticket ".$t1->Id." has no unresolved blahs");
+ok ($t1->HasUnresolvedDependencies( Type => 'approval' ), "Ticket ".$t1->Id." has unresolved approvals");
+ok (!$t2->HasUnresolvedDependencies, "Ticket ".$t2->Id." has no unresolved deps");
+;
+
+my ($rid, $rmsg)= $t1->Resolve();
+ok(!$rid, $rmsg);
+my ($rid2, $rmsg2) = $t2->Resolve();
+ok ($rid2, $rmsg2);
+($rid, $rmsg)= $t1->Resolve();
+ok(!$rid, $rmsg);
+my ($rid3,$rmsg3) = $t3->Resolve;
+ok ($rid3,$rmsg3);
+($rid, $rmsg)= $t1->Resolve();
+ok($rid, $rmsg);
+
+
+
+}
+
+1;
diff --git a/rt/t/api/reminders.t b/rt/t/api/reminders.t
new file mode 100644
index 000000000..fd1c6a69f
--- /dev/null
+++ b/rt/t/api/reminders.t
@@ -0,0 +1,88 @@
+
+use strict;
+use warnings;
+use RT;
+use RT::Test tests => 20;
+
+
+{
+
+# Create test queues
+use_ok ('RT::Queue');
+
+ok(my $testqueue = RT::Queue->new($RT::SystemUser), 'Instantiate RT::Queue');
+ok($testqueue->Create( Name => 'reminders tests'), 'Create new queue: reminders tests');
+isnt($testqueue->Id , 0, 'Success creating queue');
+
+ok($testqueue->Create( Name => 'reminders tests 2'), 'Create new queue: reminders tests 2');
+isnt($testqueue->Id , 0, 'Success creating queue');
+
+# Create test ticket
+use_ok('RT::Ticket');
+
+my $u = RT::User->new($RT::SystemUser);
+$u->Load("root");
+ok ($u->Id, "Found the root user");
+ok(my $t = RT::Ticket->new($RT::SystemUser), 'Instantiate RT::Ticket');
+ok(my ($id, $msg) = $t->Create( Queue => $testqueue->Id,
+ Subject => 'Testing',
+ Owner => $u->Id
+ ), 'Create sample ticket');
+isnt($id , 0, 'Success creating ticket');
+
+# Add reminder
+my $due_obj = RT::Date->new( $RT::SystemUser );
+$due_obj->SetToNow;
+ok(my ( $add_id, $add_msg, $txnid ) = $t->Reminders->Add(
+ Subject => 'TestReminder',
+ Owner => 'root',
+ Due => $due_obj->ISO
+ ), 'Add reminder');
+
+# Check that the new Reminder is here
+my $reminders = $t->Reminders->Collection;
+ok($reminders, 'Loading reminders for this ticket');
+my $found = 0;
+while ( my $reminder = $reminders->Next ) {
+ next unless $found == 0;
+ $found = 1 if ( $reminder->Subject =~ m/TestReminder/ );
+}
+
+is($found, 1, 'Reminder successfully added');
+
+# Change queue
+ok (my ($move_val, $move_msg) = $t->SetQueue('reminders tests 2'), 'Moving ticket from queue "reminders tests" to "reminders tests 2"');
+
+is ($t->QueueObj->Name, 'reminders tests 2', 'Ticket successfully moved');
+
+# Check that the new reminder is still there and moved to the new queue
+$reminders = $t->Reminders->Collection;
+ok($reminders, 'Loading reminders for this ticket');
+$found = 0;
+my $ok_queue = 0;
+while ( my $reminder = $reminders->Next ) {
+ next unless $found == 0;
+ if ( $reminder->Subject =~ m/TestReminder/ ) {
+ $found = 1;
+ $ok_queue = 1 if ( $reminder->QueueObj->Name eq 'reminders tests 2' );
+ }
+}
+is($found, 1, 'Reminder successfully added');
+
+is($ok_queue, 1, 'Reminder automatically moved to new queue');
+
+# Resolve reminder
+my $r_resolved = 0;
+while ( my $reminder = $reminders->Next ) {
+ if ( $reminder->Subject =~ m/TestReminder/ ) {
+ if ( $reminder->Status ne 'resolved' ) {
+ $t->Reminders->Resolve($reminder);
+ $r_resolved = 1 if ( $reminder->Status eq 'resolved' );
+ }
+ }
+}
+
+is($r_resolved, 1, 'Reminder resolved');
+
+}
+1;
diff --git a/rt/t/api/rights.t b/rt/t/api/rights.t
new file mode 100644
index 000000000..7bd332f13
--- /dev/null
+++ b/rt/t/api/rights.t
@@ -0,0 +1,142 @@
+#!/usr/bin/perl -w
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2007 Best Practical Solutions, LLC
+# <jesse.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/copyleft/gpl.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+
+use RT;
+use RT::Test tests => 26;
+
+use RT::I18N;
+use strict;
+no warnings 'once';
+
+use RT::Queue;
+use RT::ACE;
+use RT::User;
+use RT::Group;
+use RT::Ticket;
+
+
+# clear all global right
+my $acl = RT::ACL->new($RT::SystemUser);
+$acl->Limit( FIELD => 'RightName', OPERATOR => '!=', VALUE => 'SuperUser' );
+$acl->LimitToObject( $RT::System );
+while( my $ace = $acl->Next ) {
+ $ace->Delete;
+}
+
+my $rand_name = "rights". int rand($$);
+# create new queue to be shure we don't mess with rights
+my $queue = RT::Queue->new($RT::SystemUser);
+my ($queue_id) = $queue->Create( Name => $rand_name);
+ok( $queue_id, 'queue created for rights tests' );
+
+# new privileged user to check rights
+my $user = RT::User->new( $RT::SystemUser );
+my ($user_id) = $user->Create( Name => $rand_name,
+ EmailAddress => $rand_name .'@localhost',
+ Privileged => 1,
+ Password => 'qwe123',
+ );
+ok( !$user->HasRight( Right => 'OwnTicket', Object => $queue ), "user can't own ticket" );
+ok( !$user->HasRight( Right => 'ReplyToTicket', Object => $queue ), "user can't reply to ticket" );
+
+my $group = RT::Group->new( $RT::SystemUser );
+ok( $group->LoadQueueRoleGroup( Queue => $queue_id, Type=> 'Owner' ), "load queue owners role group" );
+my $ace = RT::ACE->new( $RT::SystemUser );
+my ($ace_id, $msg) = $group->PrincipalObj->GrantRight( Right => 'ReplyToTicket', Object => $queue );
+ok( $ace_id, "Granted queue owners role group with ReplyToTicket right: $msg" );
+ok( $group->PrincipalObj->HasRight( Right => 'ReplyToTicket', Object => $queue ), "role group can reply to ticket" );
+ok( !$user->HasRight( Right => 'ReplyToTicket', Object => $queue ), "user can't reply to ticket" );
+
+# new ticket
+my $ticket = RT::Ticket->new($RT::SystemUser);
+my ($ticket_id) = $ticket->Create( Queue => $queue_id, Subject => 'test');
+ok( $ticket_id, 'new ticket created' );
+is( $ticket->Owner, $RT::Nobody->Id, 'owner of the new ticket is nobody' );
+
+my $status;
+($status, $msg) = $user->PrincipalObj->GrantRight( Object => $queue, Right => 'OwnTicket' );
+ok( $status, "successfuly granted right: $msg" );
+ok( $user->HasRight( Right => 'OwnTicket', Object => $queue ), "user can own ticket" );
+
+($status, $msg) = $ticket->SetOwner( $user_id );
+ok( $status, "successfuly set owner: $msg" );
+is( $ticket->Owner, $user_id, "set correct owner" );
+
+ok( $user->HasRight( Right => 'ReplyToTicket', Object => $ticket ), "user is owner and can reply to ticket" );
+
+# Testing of EquivObjects
+$group = RT::Group->new( $RT::SystemUser );
+ok( $group->LoadQueueRoleGroup( Queue => $queue_id, Type=> 'AdminCc' ), "load queue AdminCc role group" );
+$ace = RT::ACE->new( $RT::SystemUser );
+($ace_id, $msg) = $group->PrincipalObj->GrantRight( Right => 'ModifyTicket', Object => $queue );
+ok( $ace_id, "Granted queue AdminCc role group with ModifyTicket right: $msg" );
+ok( $group->PrincipalObj->HasRight( Right => 'ModifyTicket', Object => $queue ), "role group can modify ticket" );
+ok( !$user->HasRight( Right => 'ModifyTicket', Object => $ticket ), "user is not AdminCc and can't modify ticket" );
+($status, $msg) = $ticket->AddWatcher(Type => 'AdminCc', PrincipalId => $user->PrincipalId);
+ok( $status, "successfuly added user as AdminCc");
+ok( $user->HasRight( Right => 'ModifyTicket', Object => $ticket ), "user is AdminCc and can modify ticket" );
+
+my $ticket2 = RT::Ticket->new($RT::SystemUser);
+my ($ticket2_id) = $ticket2->Create( Queue => $queue_id, Subject => 'test2');
+ok( $ticket2_id, 'new ticket created' );
+ok( !$user->HasRight( Right => 'ModifyTicket', Object => $ticket2 ), "user is not AdminCc and can't modify ticket2" );
+
+# now we can finally test EquivObjects
+my $equiv = [ $ticket ];
+ok( $user->HasRight( Right => 'ModifyTicket', Object => $ticket2, EquivObjects => $equiv ),
+ "user is not AdminCc but can modify ticket2 because of EquivObjects" );
+
+# the first a third test below are the same, so they should both pass
+my $equiv2 = [];
+ok( !$user->HasRight( Right => 'ModifyTicket', Object => $ticket2, EquivObjects => $equiv2 ),
+ "user is not AdminCc and can't modify ticket2" );
+ok( $user->HasRight( Right => 'ModifyTicket', Object => $ticket, EquivObjects => $equiv2 ),
+ "user is AdminCc and can modify ticket" );
+ok( !$user->HasRight( Right => 'ModifyTicket', Object => $ticket2, EquivObjects => $equiv2 ),
+ "user is not AdminCc and can't modify ticket2 (same question different answer)" );
diff --git a/rt/t/api/rt.t b/rt/t/api/rt.t
new file mode 100644
index 000000000..3c06b5848
--- /dev/null
+++ b/rt/t/api/rt.t
@@ -0,0 +1,18 @@
+
+use strict;
+use warnings;
+use RT;
+use RT::Test tests => 4;
+
+
+{
+
+is ($RT::Nobody->Name() , 'Nobody', "Nobody is nobody");
+isnt ($RT::Nobody->Name() , 'root', "Nobody isn't named root");
+is ($RT::SystemUser->Name() , 'RT_System', "The system user is RT_System");
+isnt ($RT::SystemUser->Name() , 'noname', "The system user isn't noname");
+
+
+}
+
+1;
diff --git a/rt/t/api/scrip.t b/rt/t/api/scrip.t
new file mode 100644
index 000000000..8e8f96213
--- /dev/null
+++ b/rt/t/api/scrip.t
@@ -0,0 +1,49 @@
+
+use strict;
+use warnings;
+use RT;
+use RT::Test tests => 7;
+
+
+{
+
+ok (require RT::Scrip);
+
+
+my $q = RT::Queue->new($RT::SystemUser);
+$q->Create(Name => 'ScripTest');
+ok($q->Id, "Created a scriptest queue");
+
+my $s1 = RT::Scrip->new($RT::SystemUser);
+my ($val, $msg) =$s1->Create( Queue => $q->Id,
+ ScripAction => 'User Defined',
+ ScripCondition => 'User Defined',
+ CustomIsApplicableCode => 'if ($self->TicketObj->Subject =~ /fire/) { return (1);} else { return(0)}',
+ CustomPrepareCode => 'return 1',
+ CustomCommitCode => '$self->TicketObj->SetPriority("87");',
+ Template => 'Blank'
+ );
+ok($val,$msg);
+
+my $ticket = RT::Ticket->new($RT::SystemUser);
+my ($tv,$ttv,$tm) = $ticket->Create(Queue => $q->Id,
+ Subject => "hair on fire",
+ );
+ok($tv, $tm);
+
+is ($ticket->Priority , '87', "Ticket priority is set right");
+
+
+my $ticket2 = RT::Ticket->new($RT::SystemUser);
+my ($t2v,$t2tv,$t2m) = $ticket2->Create(Queue => $q->Id,
+ Subject => "hair in water",
+ );
+ok($t2v, $t2m);
+
+isnt ($ticket2->Priority , '87', "Ticket priority is set right");
+
+
+
+}
+
+1;
diff --git a/rt/t/api/scrip_order.t b/rt/t/api/scrip_order.t
new file mode 100644
index 000000000..9738db9bc
--- /dev/null
+++ b/rt/t/api/scrip_order.t
@@ -0,0 +1,56 @@
+#!/usr/bin/perl -w
+
+use strict;
+
+use RT;
+use RT::Test tests => 7;
+
+
+# {{{ test scrip ordering based on description
+
+my $scrip_queue = RT::Queue->new($RT::SystemUser);
+my ($queue_id, $msg) = $scrip_queue->Create( Name => "ScripOrdering-$$",
+ Description => 'Test scrip ordering by description' );
+ok($queue_id, "Created scrip-ordering test queue? ".$msg);
+
+my $priority_ten_scrip = RT::Scrip->new($RT::SystemUser);
+(my $id, $msg) = $priority_ten_scrip->Create(
+ Description => "10 set priority $$",
+ Queue => $queue_id,
+ ScripCondition => 'On Create',
+ ScripAction => 'User Defined',
+ CustomPrepareCode => '$RT::Logger->debug("Setting priority to 10..."); return 1;',
+ CustomCommitCode => '$self->TicketObj->SetPriority(10);',
+ Template => 'Blank',
+ Stage => 'TransactionCreate',
+);
+ok($id, "Created priority-10 scrip? ".$msg);
+
+my $priority_five_scrip = RT::Scrip->new($RT::SystemUser);
+($id, $msg) = $priority_ten_scrip->Create(
+ Description => "05 set priority $$",
+ Queue => $queue_id,
+ ScripCondition => 'On Create',
+ ScripAction => 'User Defined',
+ CustomPrepareCode => '$RT::Logger->debug("Setting priority to 5..."); return 1;',
+ CustomCommitCode => '$self->TicketObj->SetPriority(5);',
+ Template => 'Blank',
+ Stage => 'TransactionCreate',
+);
+ok($id, "Created priority-5 scrip? ".$msg);
+
+my $ticket = RT::Ticket->new($RT::SystemUser);
+($id, $msg) = $ticket->Create(
+ Queue => $queue_id,
+ Requestor => 'order@example.com',
+ Subject => "Scrip order test $$",
+);
+ok($ticket->id, "Created ticket? id=$id");
+
+isnt($ticket->Priority , 0, "Ticket shouldn't be priority 0");
+isnt($ticket->Priority , 5, "Ticket shouldn't be priority 5");
+is ($ticket->Priority , 10, "Ticket should be priority 10");
+
+# }}}
+
+1;
diff --git a/rt/t/api/searchbuilder.t b/rt/t/api/searchbuilder.t
new file mode 100644
index 000000000..cb118906c
--- /dev/null
+++ b/rt/t/api/searchbuilder.t
@@ -0,0 +1,40 @@
+
+use strict;
+use warnings;
+use RT;
+use RT::Test tests => 11;
+
+
+{
+
+ok (require RT::SearchBuilder);
+
+
+}
+
+{
+
+use_ok('RT::Queues');
+ok(my $queues = RT::Queues->new($RT::SystemUser), 'Created a queues object');
+ok( $queues->UnLimit(),'Unlimited the result set of the queues object');
+my $items = $queues->ItemsArrayRef();
+my @items = @{$items};
+
+ok($queues->NewItem->_Accessible('Name','read'));
+my @sorted = sort {lc($a->Name) cmp lc($b->Name)} @items;
+ok (@sorted, "We have an array of queues, sorted". join(',',map {$_->Name} @sorted));
+
+ok (@items, "We have an array of queues, raw". join(',',map {$_->Name} @items));
+my @sorted_ids = map {$_->id } @sorted;
+my @items_ids = map {$_->id } @items;
+
+is ($#sorted, $#items);
+is ($sorted[0]->Name, $items[0]->Name);
+is ($sorted[-1]->Name, $items[-1]->Name);
+is_deeply(\@items_ids, \@sorted_ids, "ItemsArrayRef sorts alphabetically by name");
+
+
+
+}
+
+1;
diff --git a/rt/t/api/system.t b/rt/t/api/system.t
new file mode 100644
index 000000000..3077115c7
--- /dev/null
+++ b/rt/t/api/system.t
@@ -0,0 +1,33 @@
+
+use strict;
+use warnings;
+use RT;
+use RT::Test tests => 7;
+
+
+{
+
+my $s = RT::System->new($RT::SystemUser);
+my $rights = $s->AvailableRights;
+ok ($rights, "Rights defined");
+ok ($rights->{'AdminUsers'},"AdminUsers right found");
+ok ($rights->{'CreateTicket'},"CreateTicket right found");
+ok ($rights->{'AdminGroupMembership'},"ModifyGroupMembers right found");
+ok (!$rights->{'CasdasdsreateTicket'},"bogus right not found");
+
+
+
+
+}
+
+{
+
+use RT::System;
+my $sys = RT::System->new();
+is( $sys->Id, 1);
+is ($sys->id, 1);
+
+
+}
+
+1;
diff --git a/rt/t/api/template-insert.t b/rt/t/api/template-insert.t
new file mode 100644
index 000000000..47bbd790c
--- /dev/null
+++ b/rt/t/api/template-insert.t
@@ -0,0 +1,26 @@
+#!/usr/bin/perl
+
+use warnings;
+use strict;
+
+
+use RT;
+use RT::Test tests => 7;
+
+
+
+# This tiny little test script triggers an interaction bug between DBD::Oracle 1.16, SB 1.15 and RT 3.4
+
+use_ok('RT::Template');
+my $template = RT::Template->new($RT::SystemUser);
+
+isa_ok($template, 'RT::Template');
+my ($val,$msg) = $template->Create(Queue => 1,
+ Name => 'InsertTest',
+ Content => 'This is template content');
+ok($val,$msg);
+is($template->Name, 'InsertTest');
+is($template->Content, 'This is template content', "We created the object right");
+($val, $msg) = $template->SetContent( 'This is new template content');
+ok($val,$msg);
+is($template->Content, 'This is new template content', "We managed to _Set_ the content");
diff --git a/rt/t/api/template.t b/rt/t/api/template.t
new file mode 100644
index 000000000..1612b8ffd
--- /dev/null
+++ b/rt/t/api/template.t
@@ -0,0 +1,26 @@
+
+use strict;
+use warnings;
+use RT;
+use RT::Test tests => 2;
+
+
+{
+
+ok(require RT::Template);
+
+
+}
+
+{
+
+my $t = RT::Template->new($RT::SystemUser);
+$t->Create(Name => "Foo", Queue => 1);
+my $t2 = RT::Template->new($RT::Nobody);
+$t2->Load($t->Id);
+ok($t2->QueueObj->id, "Got the template's queue objet");
+
+
+}
+
+1;
diff --git a/rt/t/api/ticket.t b/rt/t/api/ticket.t
new file mode 100644
index 000000000..2ca0997bd
--- /dev/null
+++ b/rt/t/api/ticket.t
@@ -0,0 +1,257 @@
+
+use strict;
+use warnings;
+use RT;
+use RT::Test tests => 87;
+
+
+{
+
+use_ok ('RT::Queue');
+ok(my $testqueue = RT::Queue->new($RT::SystemUser));
+ok($testqueue->Create( Name => 'ticket tests'));
+isnt($testqueue->Id , 0);
+use_ok('RT::CustomField');
+ok(my $testcf = RT::CustomField->new($RT::SystemUser));
+my ($ret, $cmsg) = $testcf->Create( Name => 'selectmulti',
+ Queue => $testqueue->id,
+ Type => 'SelectMultiple');
+ok($ret,"Created the custom field - ".$cmsg);
+($ret,$cmsg) = $testcf->AddValue ( Name => 'Value1',
+ SortOrder => '1',
+ Description => 'A testing value');
+
+ok($ret, "Added a value - ".$cmsg);
+
+ok($testcf->AddValue ( Name => 'Value2',
+ SortOrder => '2',
+ Description => 'Another testing value'));
+ok($testcf->AddValue ( Name => 'Value3',
+ SortOrder => '3',
+ Description => 'Yet Another testing value'));
+
+is($testcf->Values->Count , 3);
+
+use_ok('RT::Ticket');
+
+my $u = RT::User->new($RT::SystemUser);
+$u->Load("root");
+ok ($u->Id, "Found the root user");
+ok(my $t = RT::Ticket->new($RT::SystemUser));
+ok(my ($id, $msg) = $t->Create( Queue => $testqueue->Id,
+ Subject => 'Testing',
+ Owner => $u->Id
+ ));
+isnt($id , 0);
+is ($t->OwnerObj->Id , $u->Id, "Root is the ticket owner");
+ok(my ($cfv, $cfm) =$t->AddCustomFieldValue(Field => $testcf->Id,
+ Value => 'Value1'));
+isnt($cfv , 0, "Custom field creation didn't return an error: $cfm");
+is($t->CustomFieldValues($testcf->Id)->Count , 1);
+ok($t->CustomFieldValues($testcf->Id)->First &&
+ $t->CustomFieldValues($testcf->Id)->First->Content eq 'Value1');
+
+ok(my ($cfdv, $cfdm) = $t->DeleteCustomFieldValue(Field => $testcf->Id,
+ Value => 'Value1'));
+isnt ($cfdv , 0, "Deleted a custom field value: $cfdm");
+is($t->CustomFieldValues($testcf->Id)->Count , 0);
+
+ok(my $t2 = RT::Ticket->new($RT::SystemUser));
+ok($t2->Load($id));
+is($t2->Subject, 'Testing');
+is($t2->QueueObj->Id, $testqueue->id);
+is($t2->OwnerObj->Id, $u->Id);
+
+my $t3 = RT::Ticket->new($RT::SystemUser);
+my ($id3, $msg3) = $t3->Create( Queue => $testqueue->Id,
+ Subject => 'Testing',
+ Owner => $u->Id);
+my ($cfv1, $cfm1) = $t->AddCustomFieldValue(Field => $testcf->Id,
+ Value => 'Value1');
+isnt($cfv1 , 0, "Adding a custom field to ticket 1 is successful: $cfm");
+my ($cfv2, $cfm2) = $t3->AddCustomFieldValue(Field => $testcf->Id,
+ Value => 'Value2');
+isnt($cfv2 , 0, "Adding a custom field to ticket 2 is successful: $cfm");
+my ($cfv3, $cfm3) = $t->AddCustomFieldValue(Field => $testcf->Id,
+ Value => 'Value3');
+isnt($cfv3 , 0, "Adding a custom field to ticket 1 is successful: $cfm");
+is($t->CustomFieldValues($testcf->Id)->Count , 2,
+ "This ticket has 2 custom field values");
+is($t3->CustomFieldValues($testcf->Id)->Count , 1,
+ "This ticket has 1 custom field value");
+
+
+}
+
+{
+
+
+ok(require RT::Ticket, "Loading the RT::Ticket library");
+
+
+}
+
+{
+
+my $t = RT::Ticket->new($RT::SystemUser);
+
+ok( $t->Create(Queue => 'General', Due => '2002-05-21 00:00:00', ReferredToBy => 'http://www.cpan.org', RefersTo => 'http://fsck.com', Subject => 'This is a subject'), "Ticket Created");
+
+ok ( my $id = $t->Id, "Got ticket id");
+like ($t->RefersTo->First->Target , qr/fsck.com/, "Got refers to");
+like ($t->ReferredToBy->First->Base , qr/cpan.org/, "Got referredtoby");
+is ($t->ResolvedObj->Unix, 0, "It hasn't been resolved - ". $t->ResolvedObj->Unix);
+
+
+}
+
+{
+
+my $ticket = RT::Ticket->new($RT::SystemUser);
+my ($id, $msg) = $ticket->Create(Subject => "Foo",
+ Owner => $RT::SystemUser->Id,
+ Status => 'open',
+ Requestor => ['jesse@example.com'],
+ Queue => '1'
+ );
+ok ($id, "Ticket $id was created");
+ok(my $group = RT::Group->new($RT::SystemUser));
+ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'Requestor'));
+ok ($group->Id, "Found the requestors object for this ticket");
+
+ok(my $jesse = RT::User->new($RT::SystemUser), "Creating a jesse rt::user");
+$jesse->LoadByEmail('jesse@example.com');
+ok($jesse->Id, "Found the jesse rt user");
+
+
+ok ($ticket->IsWatcher(Type => 'Requestor', PrincipalId => $jesse->PrincipalId), "The ticket actually has jesse at fsck.com as a requestor");
+ok (my ($add_id, $add_msg) = $ticket->AddWatcher(Type => 'Requestor', Email => 'bob@fsck.com'), "Added bob at fsck.com as a requestor");
+ok ($add_id, "Add succeeded: ($add_msg)");
+ok(my $bob = RT::User->new($RT::SystemUser), "Creating a bob rt::user");
+$bob->LoadByEmail('bob@fsck.com');
+ok($bob->Id, "Found the bob rt user");
+ok ($ticket->IsWatcher(Type => 'Requestor', PrincipalId => $bob->PrincipalId), "The ticket actually has bob at fsck.com as a requestor");
+ok ( ($add_id, $add_msg) = $ticket->DeleteWatcher(Type =>'Requestor', Email => 'bob@fsck.com'), "Added bob at fsck.com as a requestor");
+ok (!$ticket->IsWatcher(Type => 'Requestor', PrincipalId => $bob->PrincipalId), "The ticket no longer has bob at fsck.com as a requestor");
+
+
+$group = RT::Group->new($RT::SystemUser);
+ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'Cc'));
+ok ($group->Id, "Found the cc object for this ticket");
+$group = RT::Group->new($RT::SystemUser);
+ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'AdminCc'));
+ok ($group->Id, "Found the AdminCc object for this ticket");
+$group = RT::Group->new($RT::SystemUser);
+ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'Owner'));
+ok ($group->Id, "Found the Owner object for this ticket");
+ok($group->HasMember($RT::SystemUser->UserObj->PrincipalObj), "the owner group has the member 'RT_System'");
+
+
+}
+
+{
+
+my $t = RT::Ticket->new($RT::SystemUser);
+ok($t->Create(Queue => 'general', Subject => 'SquelchTest', SquelchMailTo => 'nobody@example.com'));
+
+my @returned = $t->SquelchMailTo();
+is($#returned, 0, "The ticket has one squelched recipients");
+
+my ($ret, $msg) = $t->UnsquelchMailTo('nobody@example.com');
+ok($ret, "Removed nobody as a squelched recipient - ".$msg);
+@returned = $t->SquelchMailTo();
+is($#returned, -1, "The ticket has no squelched recipients". join(',',@returned));
+
+
+
+@returned = $t->SquelchMailTo('nobody@example.com');
+is($#returned, 0, "The ticket has one squelched recipients");
+
+my @names = $t->Attributes->Names;
+is(shift @names, 'SquelchMailTo', "The attribute we have is SquelchMailTo");
+@returned = $t->SquelchMailTo('nobody@example.com');
+
+
+is($#returned, 0, "The ticket has one squelched recipients");
+
+@names = $t->Attributes->Names;
+is(shift @names, 'SquelchMailTo', "The attribute we have is SquelchMailTo");
+
+
+($ret, $msg) = $t->UnsquelchMailTo('nobody@example.com');
+ok($ret, "Removed nobody as a squelched recipient - ".$msg);
+@returned = $t->SquelchMailTo();
+is($#returned, -1, "The ticket has no squelched recipients". join(',',@returned));
+
+
+
+}
+
+{
+
+my $t1 = RT::Ticket->new($RT::SystemUser);
+$t1->Create ( Subject => 'Merge test 1', Queue => 'general', Requestor => 'merge1@example.com');
+my $t1id = $t1->id;
+my $t2 = RT::Ticket->new($RT::SystemUser);
+$t2->Create ( Subject => 'Merge test 2', Queue => 'general', Requestor => 'merge2@example.com');
+my $t2id = $t2->id;
+my ($msg, $val) = $t1->MergeInto($t2->id);
+ok ($msg,$val);
+$t1 = RT::Ticket->new($RT::SystemUser);
+is ($t1->id, undef, "ok. we've got a blank ticket1");
+$t1->Load($t1id);
+
+is ($t1->id, $t2->id);
+
+is ($t1->Requestors->MembersObj->Count, 2);
+
+
+
+}
+
+{
+
+my $root = RT::User->new($RT::SystemUser);
+$root->Load('root');
+ok ($root->Id, "Loaded the root user");
+my $t = RT::Ticket->new($RT::SystemUser);
+$t->Load(1);
+$t->SetOwner('root');
+is ($t->OwnerObj->Name, 'root' , "Root owns the ticket");
+$t->Steal();
+is ($t->OwnerObj->id, $RT::SystemUser->id , "SystemUser owns the ticket");
+my $txns = RT::Transactions->new($RT::SystemUser);
+$txns->OrderBy(FIELD => 'id', ORDER => 'DESC');
+$txns->Limit(FIELD => 'ObjectId', VALUE => '1');
+$txns->Limit(FIELD => 'ObjectType', VALUE => 'RT::Ticket');
+$txns->Limit(FIELD => 'Type', OPERATOR => '!=', VALUE => 'EmailRecord');
+
+my $steal = $txns->First;
+is($steal->OldValue , $root->Id , "Stolen from root");
+is($steal->NewValue , $RT::SystemUser->Id , "Stolen by the systemuser");
+
+
+}
+
+{
+
+my $tt = RT::Ticket->new($RT::SystemUser);
+my ($id, $tid, $msg)= $tt->Create(Queue => 'general',
+ Subject => 'test');
+ok($id, $msg);
+is($tt->Status, 'new', "New ticket is created as new");
+
+($id, $msg) = $tt->SetStatus('open');
+ok($id, $msg);
+like($msg, qr/open/i, "Status message is correct");
+($id, $msg) = $tt->SetStatus('resolved');
+ok($id, $msg);
+like($msg, qr/resolved/i, "Status message is correct");
+($id, $msg) = $tt->SetStatus('resolved');
+ok(!$id,$msg);
+
+
+
+}
+
+1;
diff --git a/rt/t/api/tickets.t b/rt/t/api/tickets.t
new file mode 100644
index 000000000..9148a8899
--- /dev/null
+++ b/rt/t/api/tickets.t
@@ -0,0 +1,104 @@
+
+use strict;
+use warnings;
+use RT;
+use RT::Test tests => 16;
+
+
+{
+
+ok (require RT::Tickets);
+ok( my $testtickets = RT::Tickets->new( $RT::SystemUser ) );
+ok( $testtickets->LimitStatus( VALUE => 'deleted' ) );
+# Should be zero until 'allow_deleted_search'
+is( $testtickets->Count , 0 );
+
+
+}
+
+{
+
+# Test to make sure that you can search for tickets by requestor address and
+# by requestor name.
+
+my ($id,$msg);
+my $u1 = RT::User->new($RT::SystemUser);
+($id, $msg) = $u1->Create( Name => 'RequestorTestOne', EmailAddress => 'rqtest1@example.com');
+ok ($id,$msg);
+my $u2 = RT::User->new($RT::SystemUser);
+($id, $msg) = $u2->Create( Name => 'RequestorTestTwo', EmailAddress => 'rqtest2@example.com');
+ok ($id,$msg);
+
+my $t1 = RT::Ticket->new($RT::SystemUser);
+my ($trans);
+($id,$trans,$msg) =$t1->Create (Queue => 'general', Subject => 'Requestor test one', Requestor => [$u1->EmailAddress]);
+ok ($id, $msg);
+
+my $t2 = RT::Ticket->new($RT::SystemUser);
+($id,$trans,$msg) =$t2->Create (Queue => 'general', Subject => 'Requestor test one', Requestor => [$u2->EmailAddress]);
+ok ($id, $msg);
+
+
+my $t3 = RT::Ticket->new($RT::SystemUser);
+($id,$trans,$msg) =$t3->Create (Queue => 'general', Subject => 'Requestor test one', Requestor => [$u2->EmailAddress, $u1->EmailAddress]);
+ok ($id, $msg);
+
+
+my $tix1 = RT::Tickets->new($RT::SystemUser);
+$tix1->FromSQL('Requestor.EmailAddress LIKE "rqtest1" OR Requestor.EmailAddress LIKE "rqtest2"');
+
+is ($tix1->Count, 3);
+
+my $tix2 = RT::Tickets->new($RT::SystemUser);
+$tix2->FromSQL('Requestor.Name LIKE "TestOne" OR Requestor.Name LIKE "TestTwo"');
+
+is ($tix2->Count, 3);
+
+
+my $tix3 = RT::Tickets->new($RT::SystemUser);
+$tix3->FromSQL('Requestor.EmailAddress LIKE "rqtest1"');
+
+is ($tix3->Count, 2);
+
+my $tix4 = RT::Tickets->new($RT::SystemUser);
+$tix4->FromSQL('Requestor.Name LIKE "TestOne" ');
+
+is ($tix4->Count, 2);
+
+# Searching for tickets that have two requestors isn't supported
+# There's no way to differentiate "one requestor name that matches foo and bar"
+# and "two requestors, one matching foo and one matching bar"
+
+# my $tix5 = RT::Tickets->new($RT::SystemUser);
+# $tix5->FromSQL('Requestor.Name LIKE "TestOne" AND Requestor.Name LIKE "TestTwo"');
+#
+# is ($tix5->Count, 1);
+#
+# my $tix6 = RT::Tickets->new($RT::SystemUser);
+# $tix6->FromSQL('Requestor.EmailAddress LIKE "rqtest1" AND Requestor.EmailAddress LIKE "rqtest2"');
+#
+# is ($tix6->Count, 1);
+
+
+
+}
+
+{
+
+my $t1 = RT::Ticket->new($RT::SystemUser);
+$t1->Create(Queue => 'general', Subject => "LimitWatchers test", Requestors => \['requestor1@example.com']);
+
+
+}
+
+{
+
+# We assume that we've got some tickets hanging around from before.
+ok( my $unlimittickets = RT::Tickets->new( $RT::SystemUser ) );
+ok( $unlimittickets->UnLimit );
+ok( $unlimittickets->Count > 0, "UnLimited tickets object should return tickets" );
+
+
+}
+
+1;
diff --git a/rt/t/api/tickets_overlay_sql.t b/rt/t/api/tickets_overlay_sql.t
new file mode 100644
index 000000000..798088664
--- /dev/null
+++ b/rt/t/api/tickets_overlay_sql.t
@@ -0,0 +1,73 @@
+
+use RT;
+use RT::Test tests => 7;
+
+
+{
+
+use RT::Tickets;
+use strict;
+
+my $tix = RT::Tickets->new($RT::SystemUser);
+{
+ my $query = "Status = 'open'";
+ my ($status, $msg) = $tix->FromSQL($query);
+ ok ($status, "correct query") or diag("error: $msg");
+}
+
+
+my (@created,%created);
+my $string = 'subject/content SQL test';
+{
+ my $t = RT::Ticket->new($RT::SystemUser);
+ ok( $t->Create(Queue => 'General', Subject => $string), "Ticket Created");
+ $created{ $t->Id }++; push @created, $t->Id;
+}
+
+{
+ my $Message = MIME::Entity->build(
+ Subject => 'this is my subject',
+ From => 'jesse@example.com',
+ Data => [ $string ],
+ );
+
+ my $t = RT::Ticket->new($RT::SystemUser);
+ ok( $t->Create( Queue => 'General',
+ Subject => 'another ticket',
+ MIMEObj => $Message,
+ MemberOf => $created[0]
+ ),
+ "Ticket Created"
+ );
+ $created{ $t->Id }++; push @created, $t->Id;
+}
+
+{
+ my $query = ("Subject LIKE '$string' OR Content LIKE '$string'");
+ my ($status, $msg) = $tix->FromSQL($query);
+ ok ($status, "correct query") or diag("error: $msg");
+
+ my $count = 0;
+ while (my $tick = $tix->Next) {
+ $count++ if $created{ $tick->id };
+ }
+ is ($count, scalar @created, "number of returned tickets same as entered");
+}
+
+{
+ my $query = "id = $created[0] OR MemberOf = $created[0]";
+ my ($status, $msg) = $tix->FromSQL($query);
+ ok ($status, "correct query") or diag("error: $msg");
+
+ my $count = 0;
+ while (my $tick = $tix->Next) {
+ $count++ if $created{ $tick->id };
+ }
+ is ($count, scalar @created, "number of returned tickets same as entered");
+}
+
+
+
+}
+
+1;
diff --git a/rt/t/api/uri-fsck_com_rt.t b/rt/t/api/uri-fsck_com_rt.t
new file mode 100644
index 000000000..d62e58022
--- /dev/null
+++ b/rt/t/api/uri-fsck_com_rt.t
@@ -0,0 +1,28 @@
+use strict;
+use warnings;
+use RT;
+use RT::Test tests => 8;
+
+use_ok("RT::URI::fsck_com_rt");
+my $uri = RT::URI::fsck_com_rt->new($RT::SystemUser);
+
+my $t1 = RT::Ticket->new($RT::SystemUser);
+my ($id,$trans,$msg) =$t1->Create (Queue => 'general', Subject => 'Requestor test one', );
+ok ($id, $msg);
+
+ok(ref($uri));
+
+ok (UNIVERSAL::isa($uri,"RT::URI::fsck_com_rt"), "It's an RT::URI::fsck_com_rt");
+
+ok ($uri->isa('RT::URI::base'), "It's an RT::URI::base");
+ok ($uri->isa('RT::Base'), "It's an RT::Base");
+
+is ($uri->LocalURIPrefix , 'fsck.com-rt://'.RT->Config->Get('Organization'));
+
+
+my $ticket = RT::Ticket->new($RT::SystemUser);
+$ticket->Load(1);
+$uri = RT::URI::fsck_com_rt->new($ticket->CurrentUser);
+is($uri->LocalURIPrefix. "/ticket/1" , $uri->URIForObject($ticket));
+
+1;
diff --git a/rt/t/api/uri-t.t b/rt/t/api/uri-t.t
new file mode 100644
index 000000000..4695629bb
--- /dev/null
+++ b/rt/t/api/uri-t.t
@@ -0,0 +1,21 @@
+use strict;
+use warnings;
+use RT;
+use RT::Test tests => 6;
+
+my $t1 = RT::Ticket->new($RT::SystemUser);
+my ($id,$trans,$msg) =$t1->Create (Queue => 'general', Subject => 'Requestor test one', );
+ok ($id, $msg);
+
+use_ok("RT::URI::t");
+my $uri = RT::URI::t->new($RT::SystemUser);
+ok(ref($uri), "URI object exists");
+
+my $uristr = "t:1";
+$uri->ParseURI($uristr);
+is(ref($uri->Object), "RT::Ticket", "Object loaded is a ticket");
+is($uri->Object->Id, 1, "Object loaded has correct ID");
+is($uri->URI, 'fsck.com-rt://'.RT->Config->Get('Organization').'/ticket/1',
+ "URI object has correct URI string");
+
+1;
diff --git a/rt/t/api/user.t b/rt/t/api/user.t
new file mode 100644
index 000000000..25cf74773
--- /dev/null
+++ b/rt/t/api/user.t
@@ -0,0 +1,339 @@
+
+use strict;
+use warnings;
+use RT;
+use RT::Test tests => 108;
+
+
+{
+
+ok(require RT::User);
+
+
+}
+
+{
+
+# Make sure we can create a user
+
+my $u1 = RT::User->new($RT::SystemUser);
+is(ref($u1), 'RT::User');
+my ($id, $msg) = $u1->Create(Name => 'CreateTest1'.$$, EmailAddress => $$.'create-test-1@example.com');
+ok ($id, "Creating user CreateTest1 - " . $msg );
+
+# Make sure we can't create a second user with the same name
+my $u2 = RT::User->new($RT::SystemUser);
+($id, $msg) = $u2->Create(Name => 'CreateTest1'.$$, EmailAddress => $$.'create-test-2@example.com');
+ok (!$id, $msg);
+
+
+# Make sure we can't create a second user with the same EmailAddress address
+my $u3 = RT::User->new($RT::SystemUser);
+($id, $msg) = $u3->Create(Name => 'CreateTest2'.$$, EmailAddress => $$.'create-test-1@example.com');
+ok (!$id, $msg);
+
+# Make sure we can create a user with no EmailAddress address
+my $u4 = RT::User->new($RT::SystemUser);
+($id, $msg) = $u4->Create(Name => 'CreateTest3'.$$);
+ok ($id, $msg);
+
+# make sure we can create a second user with no EmailAddress address
+my $u5 = RT::User->new($RT::SystemUser);
+($id, $msg) = $u5->Create(Name => 'CreateTest4'.$$);
+ok ($id, $msg);
+
+# make sure we can create a user with a blank EmailAddress address
+my $u6 = RT::User->new($RT::SystemUser);
+($id, $msg) = $u6->Create(Name => 'CreateTest6'.$$, EmailAddress => '');
+ok ($id, $msg);
+# make sure we can create a second user with a blankEmailAddress address
+my $u7 = RT::User->new($RT::SystemUser);
+($id, $msg) = $u7->Create(Name => 'CreateTest7'.$$, EmailAddress => '');
+ok ($id, $msg);
+
+# Can we change the email address away from from "";
+($id,$msg) = $u7->SetEmailAddress('foo@bar'.$$);
+ok ($id, $msg);
+# can we change the address back to "";
+($id,$msg) = $u7->SetEmailAddress('');
+ok ($id, $msg);
+is_empty ($u7->EmailAddress);
+
+RT->Config->Set('ValidateUserEmailAddresses' => 1);
+# Make sur we can't create a user with multiple email adresses separated by comma
+my $u8 = RT::User->new($RT::SystemUser);
+($id, $msg) = $u8->Create(Name => 'CreateTest8'.$$, EmailAddress => $$.'create-test-81@example.com, '.$$.'create-test-82@example.com');
+ok (!$id, $msg);
+
+# Make sur we can't create a user with multiple email adresses separated by space
+my $u9 = RT::User->new($RT::SystemUser);
+($id, $msg) = $u9->Create(Name => 'CreateTest9'.$$, EmailAddress => $$.'create-test-91@example.com '.$$.'create-test-92@example.com');
+ok (!$id, $msg);
+
+# Make sur we can't create a user with invalid email address
+my $u10 = RT::User->new($RT::SystemUser);
+($id, $msg) = $u10->Create(Name => 'CreateTest10'.$$, EmailAddress => $$.'create-test10}@[.com');
+ok (!$id, $msg);
+RT->Config->Set('ValidateUserEmailAddresses' => undef);
+
+}
+
+{
+
+
+ok(my $user = RT::User->new($RT::SystemUser));
+ok($user->Load('root'), "Loaded user 'root'");
+ok($user->Privileged, "User 'root' is privileged");
+ok(my ($v,$m) = $user->SetPrivileged(0));
+is ($v ,1, "Set unprivileged suceeded ($m)");
+ok(!$user->Privileged, "User 'root' is no longer privileged");
+ok(my ($v2,$m2) = $user->SetPrivileged(1));
+is ($v2 ,1, "Set privileged suceeded ($m2");
+ok($user->Privileged, "User 'root' is privileged again");
+
+
+}
+
+{
+
+ok(my $u = RT::User->new($RT::SystemUser));
+ok($u->Load(1), "Loaded the first user");
+is($u->PrincipalObj->ObjectId , 1, "user 1 is the first principal");
+is($u->PrincipalObj->PrincipalType, 'User' , "Principal 1 is a user, not a group");
+
+
+}
+
+{
+
+my $root = RT::User->new($RT::SystemUser);
+$root->Load('root');
+ok($root->Id, "Found the root user");
+my $rootq = RT::Queue->new($root);
+$rootq->Load(1);
+ok($rootq->Id, "Loaded the first queue");
+
+ok ($rootq->CurrentUser->HasRight(Right=> 'CreateTicket', Object => $rootq), "Root can create tickets");
+
+my $new_user = RT::User->new($RT::SystemUser);
+my ($id, $msg) = $new_user->Create(Name => 'ACLTest'.$$);
+
+ok ($id, "Created a new user for acl test $msg");
+
+my $q = RT::Queue->new($new_user);
+$q->Load(1);
+ok($q->Id, "Loaded the first queue");
+
+
+ok (!$q->CurrentUser->HasRight(Right => 'CreateTicket', Object => $q), "Some random user doesn't have the right to create tickets");
+ok (my ($gval, $gmsg) = $new_user->PrincipalObj->GrantRight( Right => 'CreateTicket', Object => $q), "Granted the random user the right to create tickets");
+ok ($gval, "Grant succeeded - $gmsg");
+
+
+ok ($q->CurrentUser->HasRight(Right => 'CreateTicket', Object => $q), "The user can create tickets after we grant him the right");
+ok ( ($gval, $gmsg) = $new_user->PrincipalObj->RevokeRight( Right => 'CreateTicket', Object => $q), "revoked the random user the right to create tickets");
+ok ($gval, "Revocation succeeded - $gmsg");
+ok (!$q->CurrentUser->HasRight(Right => 'CreateTicket', Object => $q), "The user can't create tickets anymore");
+
+
+
+
+
+# Create a ticket in the queue
+my $new_tick = RT::Ticket->new($RT::SystemUser);
+my ($tickid, $tickmsg) = $new_tick->Create(Subject=> 'ACL Test', Queue => 'General');
+ok($tickid, "Created ticket: $tickid");
+# Make sure the user doesn't have the right to modify tickets in the queue
+ok (!$new_user->HasRight( Object => $new_tick, Right => 'ModifyTicket'), "User can't modify the ticket without group membership");
+# Create a new group
+my $group = RT::Group->new($RT::SystemUser);
+$group->CreateUserDefinedGroup(Name => 'ACLTest'.$$);
+ok($group->Id, "Created a new group Ok");
+# Grant a group the right to modify tickets in a queue
+ok(my ($gv,$gm) = $group->PrincipalObj->GrantRight( Object => $q, Right => 'ModifyTicket'),"Granted the group the right to modify tickets");
+ok($gv,"Grant succeeed - $gm");
+# Add the user to the group
+ok( my ($aid, $amsg) = $group->AddMember($new_user->PrincipalId), "Added the member to the group");
+ok ($aid, "Member added to group: $amsg");
+# Make sure the user does have the right to modify tickets in the queue
+ok ($new_user->HasRight( Object => $new_tick, Right => 'ModifyTicket'), "User can modify the ticket with group membership");
+
+
+# Remove the user from the group
+ok( my ($did, $dmsg) = $group->DeleteMember($new_user->PrincipalId), "Deleted the member from the group");
+ok ($did,"Deleted the group member: $dmsg");
+# Make sure the user doesn't have the right to modify tickets in the queue
+ok (!$new_user->HasRight( Object => $new_tick, Right => 'ModifyTicket'), "User can't modify the ticket without group membership");
+
+
+my $q_as_system = RT::Queue->new($RT::SystemUser);
+$q_as_system->Load(1);
+ok($q_as_system->Id, "Loaded the first queue");
+
+# Create a ticket in the queue
+my $new_tick2 = RT::Ticket->new($RT::SystemUser);
+(my $tick2id, $tickmsg) = $new_tick2->Create(Subject=> 'ACL Test 2', Queue =>$q_as_system->Id);
+ok($tick2id, "Created ticket: $tick2id");
+is($new_tick2->QueueObj->id, $q_as_system->Id, "Created a new ticket in queue 1");
+
+
+# make sure that the user can't do this without subgroup membership
+ok (!$new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User can't modify the ticket without group membership");
+
+# Create a subgroup
+my $subgroup = RT::Group->new($RT::SystemUser);
+$subgroup->CreateUserDefinedGroup(Name => 'Subgrouptest'.$$);
+ok($subgroup->Id, "Created a new group ".$subgroup->Id."Ok");
+#Add the subgroup as a subgroup of the group
+my ($said, $samsg) = $group->AddMember($subgroup->PrincipalId);
+ok ($said, "Added the subgroup as a member of the group");
+# Add the user to a subgroup of the group
+
+my ($usaid, $usamsg) = $subgroup->AddMember($new_user->PrincipalId);
+ok($usaid,"Added the user ".$new_user->Id."to the subgroup");
+# Make sure the user does have the right to modify tickets in the queue
+ok ($new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User can modify the ticket with subgroup membership");
+
+# {{{ Deal with making sure that members of subgroups of a disabled group don't have rights
+
+($id, $msg) = $group->SetDisabled(1);
+ok ($id,$msg);
+ok (!$new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User can't modify the ticket when the group ".$group->Id. " is disabled");
+ ($id, $msg) = $group->SetDisabled(0);
+ok($id,$msg);
+# Test what happens when we disable the group the user is a member of directly
+
+($id, $msg) = $subgroup->SetDisabled(1);
+ ok ($id,$msg);
+ok (!$new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User can't modify the ticket when the group ".$subgroup->Id. " is disabled");
+ ($id, $msg) = $subgroup->SetDisabled(0);
+ ok ($id,$msg);
+ok ($new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User can modify the ticket without group membership");
+
+# }}}
+
+
+my ($usrid, $usrmsg) = $subgroup->DeleteMember($new_user->PrincipalId);
+ok($usrid,"removed the user from the group - $usrmsg");
+# Make sure the user doesn't have the right to modify tickets in the queue
+ok (!$new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User can't modify the ticket without group membership");
+
+#revoke the right to modify tickets in a queue
+ok(($gv,$gm) = $group->PrincipalObj->RevokeRight( Object => $q, Right => 'ModifyTicket'),"Granted the group the right to modify tickets");
+ok($gv,"revoke succeeed - $gm");
+
+# {{{ Test the user's right to modify a ticket as a _queue_ admincc for a right granted at the _queue_ level
+
+# Grant queue admin cc the right to modify ticket in the queue
+ok(my ($qv,$qm) = $q_as_system->AdminCc->PrincipalObj->GrantRight( Object => $q_as_system, Right => 'ModifyTicket'),"Granted the queue adminccs the right to modify tickets");
+ok($qv, "Granted the right successfully - $qm");
+
+# Add the user as a queue admincc
+ok (my ($add_id, $add_msg) = $q_as_system->AddWatcher(Type => 'AdminCc', PrincipalId => $new_user->PrincipalId) , "Added the new user as a queue admincc");
+ok ($add_id, "the user is now a queue admincc - $add_msg");
+
+# Make sure the user does have the right to modify tickets in the queue
+ok ($new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User can modify the ticket as an admincc");
+# Remove the user from the role group
+ok (my ($del_id, $del_msg) = $q_as_system->DeleteWatcher(Type => 'AdminCc', PrincipalId => $new_user->PrincipalId) , "Deleted the new user as a queue admincc");
+
+# Make sure the user doesn't have the right to modify tickets in the queue
+ok (!$new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User can't modify the ticket without group membership");
+
+# }}}
+
+# {{{ Test the user's right to modify a ticket as a _ticket_ admincc with the right granted at the _queue_ level
+
+# Add the user as a ticket admincc
+ok (my( $uadd_id, $uadd_msg) = $new_tick2->AddWatcher(Type => 'AdminCc', PrincipalId => $new_user->PrincipalId) , "Added the new user as a queue admincc");
+ok ($add_id, "the user is now a queue admincc - $add_msg");
+
+# Make sure the user does have the right to modify tickets in the queue
+ok ($new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User can modify the ticket as an admincc");
+
+# Remove the user from the role group
+ok (( $del_id, $del_msg) = $new_tick2->DeleteWatcher(Type => 'AdminCc', PrincipalId => $new_user->PrincipalId) , "Deleted the new user as a queue admincc");
+
+# Make sure the user doesn't have the right to modify tickets in the queue
+ok (!$new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User can't modify the ticket without group membership");
+
+
+# Revoke the right to modify ticket in the queue
+ok(my ($rqv,$rqm) = $q_as_system->AdminCc->PrincipalObj->RevokeRight( Object => $q_as_system, Right => 'ModifyTicket'),"Revokeed the queue adminccs the right to modify tickets");
+ok($rqv, "Revoked the right successfully - $rqm");
+
+# }}}
+
+
+
+# {{{ Test the user's right to modify a ticket as a _queue_ admincc for a right granted at the _system_ level
+
+# Before we start Make sure the user does not have the right to modify tickets in the queue
+ok (!$new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User can not modify the ticket without it being granted");
+ok (!$new_user->HasRight( Object => $new_tick2->QueueObj, Right => 'ModifyTicket'), "User can not modify tickets in the queue without it being granted");
+
+# Grant queue admin cc the right to modify ticket in the queue
+ok(($qv,$qm) = $q_as_system->AdminCc->PrincipalObj->GrantRight( Object => $RT::System, Right => 'ModifyTicket'),"Granted the queue adminccs the right to modify tickets");
+ok($qv, "Granted the right successfully - $qm");
+
+# Make sure the user can't modify the ticket before they're added as a watcher
+ok (!$new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User can not modify the ticket without being an admincc");
+ok (!$new_user->HasRight( Object => $new_tick2->QueueObj, Right => 'ModifyTicket'), "User can not modify tickets in the queue without being an admincc");
+
+# Add the user as a queue admincc
+ok (($add_id, $add_msg) = $q_as_system->AddWatcher(Type => 'AdminCc', PrincipalId => $new_user->PrincipalId) , "Added the new user as a queue admincc");
+ok ($add_id, "the user is now a queue admincc - $add_msg");
+
+# Make sure the user does have the right to modify tickets in the queue
+ok ($new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User can modify the ticket as an admincc");
+ok ($new_user->HasRight( Object => $new_tick2->QueueObj, Right => 'ModifyTicket'), "User can modify tickets in the queue as an admincc");
+# Remove the user from the role group
+ok (($del_id, $del_msg) = $q_as_system->DeleteWatcher(Type => 'AdminCc', PrincipalId => $new_user->PrincipalId) , "Deleted the new user as a queue admincc");
+
+# Make sure the user doesn't have the right to modify tickets in the queue
+ok (!$new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User can't modify the ticket without group membership");
+ok (!$new_user->HasRight( Object => $new_tick2->QueueObj, Right => 'ModifyTicket'), "User can't modify tickets in the queue without group membership");
+
+# }}}
+
+# {{{ Test the user's right to modify a ticket as a _ticket_ admincc with the right granted at the _queue_ level
+
+ok (!$new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User can not modify the ticket without being an admincc");
+ok (!$new_user->HasRight( Object => $new_tick2->QueueObj, Right => 'ModifyTicket'), "User can not modify tickets in the queue obj without being an admincc");
+
+
+# Add the user as a ticket admincc
+ok ( ($uadd_id, $uadd_msg) = $new_tick2->AddWatcher(Type => 'AdminCc', PrincipalId => $new_user->PrincipalId) , "Added the new user as a queue admincc");
+ok ($add_id, "the user is now a queue admincc - $add_msg");
+
+# Make sure the user does have the right to modify tickets in the queue
+ok ($new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User can modify the ticket as an admincc");
+ok (!$new_user->HasRight( Object => $new_tick2->QueueObj, Right => 'ModifyTicket'), "User can not modify tickets in the queue obj being only a ticket admincc");
+
+# Remove the user from the role group
+ok ( ($del_id, $del_msg) = $new_tick2->DeleteWatcher(Type => 'AdminCc', PrincipalId => $new_user->PrincipalId) , "Deleted the new user as a queue admincc");
+
+# Make sure the user doesn't have the right to modify tickets in the queue
+ok (!$new_user->HasRight( Object => $new_tick2, Right => 'ModifyTicket'), "User can't modify the ticket without being an admincc");
+ok (!$new_user->HasRight( Object => $new_tick2->QueueObj, Right => 'ModifyTicket'), "User can not modify tickets in the queue obj without being an admincc");
+
+
+# Revoke the right to modify ticket in the queue
+ok(($rqv,$rqm) = $q_as_system->AdminCc->PrincipalObj->RevokeRight( Object => $RT::System, Right => 'ModifyTicket'),"Revokeed the queue adminccs the right to modify tickets");
+ok($rqv, "Revoked the right successfully - $rqm");
+
+# }}}
+
+
+
+
+# Grant "privileged users" the system right to create users
+# Create a privileged user.
+# have that user create another user
+# Revoke the right for privileged users to create users
+# have the privileged user try to create another user and fail the ACL check
+
+
+}
+
+1;
diff --git a/rt/t/api/users.t b/rt/t/api/users.t
new file mode 100644
index 000000000..d1ff174e1
--- /dev/null
+++ b/rt/t/api/users.t
@@ -0,0 +1,80 @@
+
+use strict;
+use warnings;
+use RT;
+use RT::Test tests => 11;
+
+
+{
+
+ok(require RT::Users);
+
+
+}
+
+{
+ no warnings qw(redefine once);
+
+ok(my $users = RT::Users->new($RT::SystemUser));
+$users->WhoHaveRight(Object =>$RT::System, Right =>'SuperUser');
+is($users->Count , 1, "There is one privileged superuser - Found ". $users->Count );
+# TODO: this wants more testing
+
+my $RTxUser = RT::User->new($RT::SystemUser);
+my ($id, $msg) = $RTxUser->Create( Name => 'RTxUser', Comments => "RTx extension user", Privileged => 1);
+ok ($id,$msg);
+
+my $group = RT::Group->new($RT::SystemUser);
+$group->LoadACLEquivalenceGroup($RTxUser->PrincipalObj);
+
+my $RTxSysObj = {};
+bless $RTxSysObj, 'RTx::System';
+*RTx::System::Id = sub { 1; };
+*RTx::System::id = *RTx::System::Id;
+my $ace = RT::Record->new($RT::SystemUser);
+$ace->Table('ACL');
+$ace->_BuildTableAttributes unless ($RT::Record::_TABLE_ATTR->{ref($ace)});
+($id, $msg) = $ace->Create( PrincipalId => $group->id, PrincipalType => 'Group', RightName => 'RTxUserRight', ObjectType => 'RTx::System', ObjectId => 1 );
+ok ($id, "ACL for RTxSysObj created");
+
+my $RTxObj = {};
+bless $RTxObj, 'RTx::System::Record';
+*RTx::System::Record::Id = sub { 4; };
+*RTx::System::Record::id = *RTx::System::Record::Id;
+
+$users = RT::Users->new($RT::SystemUser);
+$users->WhoHaveRight(Right => 'RTxUserRight', Object => $RTxSysObj);
+is($users->Count, 1, "RTxUserRight found for RTxSysObj");
+
+$users = RT::Users->new($RT::SystemUser);
+$users->WhoHaveRight(Right => 'RTxUserRight', Object => $RTxObj);
+is($users->Count, 0, "RTxUserRight not found for RTxObj");
+
+$users = RT::Users->new($RT::SystemUser);
+$users->WhoHaveRight(Right => 'RTxUserRight', Object => $RTxObj, EquivObjects => [ $RTxSysObj ]);
+is($users->Count, 1, "RTxUserRight found for RTxObj using EquivObjects");
+
+$ace = RT::Record->new($RT::SystemUser);
+$ace->Table('ACL');
+$ace->_BuildTableAttributes unless ($RT::Record::_TABLE_ATTR->{ref($ace)});
+($id, $msg) = $ace->Create( PrincipalId => $group->id, PrincipalType => 'Group', RightName => 'RTxUserRight', ObjectType => 'RTx::System::Record', ObjectId => 5 );
+ok ($id, "ACL for RTxObj created");
+
+my $RTxObj2 = {};
+bless $RTxObj2, 'RTx::System::Record';
+*RTx::System::Record::Id = sub { 5; };
+*RTx::System::Record::id = sub { 5; };
+
+$users = RT::Users->new($RT::SystemUser);
+$users->WhoHaveRight(Right => 'RTxUserRight', Object => $RTxObj2);
+is($users->Count, 1, "RTxUserRight found for RTxObj2");
+
+$users = RT::Users->new($RT::SystemUser);
+$users->WhoHaveRight(Right => 'RTxUserRight', Object => $RTxObj2, EquivObjects => [ $RTxSysObj ]);
+is($users->Count, 1, "RTxUserRight found for RTxObj2");
+
+
+
+}
+
+1;
diff --git a/rt/t/approval/basic.t b/rt/t/approval/basic.t
new file mode 100644
index 000000000..597459125
--- /dev/null
+++ b/rt/t/approval/basic.t
@@ -0,0 +1,218 @@
+
+use strict;
+use warnings;
+use Test::More;
+BEGIN {
+ eval { require Email::Abstract; require Test::Email; 1 }
+ or plan skip_all => 'require Email::Abstract and Test::Email';
+}
+
+
+use RT;
+use RT::Test tests => 39;
+use RT::Test::Email;
+
+RT->Config->Set( LogToScreen => 'debug' );
+RT->Config->Set( UseTransactionBatch => 1 );
+my ($baseurl, $m) = RT::Test->started_ok;
+
+my $q = RT::Queue->new($RT::SystemUser);
+$q->Load('___Approvals');
+$q->SetDisabled(0);
+
+my %users;
+for my $user_name (qw(minion cfo ceo )) {
+ my $user = $users{$user_name} = RT::User->new($RT::SystemUser);
+ $user->Create( Name => uc($user_name),
+ Privileged => 1,
+ EmailAddress => $user_name.'@company.com');
+ my ($val, $msg);
+ ($val, $msg) = $user->PrincipalObj->GrantRight(Object =>$q, Right => $_)
+ for qw(ModifyTicket OwnTicket ShowTicket);
+}
+
+# XXX: we need to make the first approval ticket open so notification is sent.
+my $approvals =
+'===Create-Ticket: for-CFO
+Queue: ___Approvals
+Type: approval
+Owner: CFO
+Requestors: {$Tickets{"TOP"}->Requestors}
+Refers-To: TOP
+Subject: CFO Approval for PO: {$Tickets{"TOP"}->Id} - {$Tickets{"TOP"}->Subject}
+Due: {time + 86400}
+Content-Type: text/plain
+Content: Your approval is requested for the PO ticket {$Tickets{"TOP"}->Id}: {$Tickets{"TOP"}->Subject}
+Blah
+Blah
+ENDOFCONTENT
+===Create-Ticket: for-CEO
+Queue: ___Approvals
+Type: approval
+Owner: CEO
+Requestors: {$Tickets{"TOP"}->Requestors}
+Subject: PO approval request for {$Tickets{"TOP"}->Subject}
+Refers-To: TOP
+Depends-On: for-CFO
+Depended-On-By: {$Tickets{"TOP"}->Id}
+Content-Type: text/plain
+Content:
+Your CFO approved PO ticket {$Tickets{"TOP"}->Id} for minion. you ok with that?
+ENDOFCONTENT
+';
+
+my $apptemp = RT::Template->new($RT::SystemUser);
+$apptemp->Create( Content => $approvals, Name => "PO Approvals", Queue => "0");
+
+ok($apptemp->Id);
+
+$q = RT::Queue->new($RT::SystemUser);
+$q->Create(Name => 'PO');
+ok ($q->Id, "Created PO queue");
+
+my $scrip = RT::Scrip->new($RT::SystemUser);
+my ($sval, $smsg) =$scrip->Create( ScripCondition => 'On Create',
+ ScripAction => 'Create Tickets',
+ Template => 'PO Approvals',
+ Queue => $q->Id,
+ Description => 'Create Approval Tickets');
+ok ($sval, $smsg);
+ok ($scrip->Id, "Created the scrip");
+ok ($scrip->TemplateObj->Id, "Created the scrip template");
+ok ($scrip->ConditionObj->Id, "Created the scrip condition");
+ok ($scrip->ActionObj->Id, "Created the scrip action");
+
+my $t = RT::Ticket->new($RT::SystemUser);
+my ($tid, $ttrans, $tmsg);
+
+mail_ok {
+ ($tid, $ttrans, $tmsg) =
+ $t->Create(Subject => "PO for stationary",
+ Owner => "root", Requestor => 'minion',
+ Queue => $q->Id);
+} { from => qr/RT System/,
+ to => 'cfo@company.com',
+ subject => qr/New Pending Approval: CFO Approval/,
+ body => qr/pending your approval.*Your approval is requested.*Blah/s
+},{ from => qr/PO via RT/,
+ to => 'minion@company.com',
+ subject => qr/PO for stationary/,
+ body => qr/automatically generated in response/
+};
+
+ok ($tid,$tmsg);
+
+is ($t->ReferredToBy->Count,2, "referred to by the two tickets");
+
+my $deps = $t->DependsOn;
+is ($deps->Count, 1, "The ticket we created depends on one other ticket");
+my $dependson_ceo= $deps->First->TargetObj;
+ok ($dependson_ceo->Id, "It depends on a real ticket");
+like($dependson_ceo->Subject, qr/PO approval request.*stationary/);
+
+$deps = $dependson_ceo->DependsOn;
+is ($deps->Count, 1, "The ticket we created depends on one other ticket");
+my $dependson_cfo = $deps->First->TargetObj;
+ok ($dependson_cfo->Id, "It depends on a real ticket");
+
+like($dependson_cfo->Subject, qr/CFO Approval for PO.*stationary/);
+
+is_deeply([ $t->Status, $dependson_cfo->Status, $dependson_ceo->Status ],
+ [ 'new', 'open', 'new'], 'tickets in correct state');
+
+mail_ok {
+ my $cfo = RT::CurrentUser->new;
+ $cfo->Load( $users{cfo} );
+
+ $dependson_cfo->CurrentUser($cfo);
+ my $notes = MIME::Entity->build(
+ Data => [ 'Resources exist to be consumed.' ]
+ );
+ RT::I18N::SetMIMEEntityToUTF8($notes); # convert text parts into utf-8
+
+ my ( $notesval, $notesmsg ) = $dependson_cfo->Correspond( MIMEObj => $notes );
+ ok($notesval, $notesmsg);
+
+ my ($ok, $msg) = $dependson_cfo->SetStatus( Status => 'resolved' );
+ ok($ok, "cfo can approve - $msg");
+
+} { from => qr/RT System/,
+ to => 'ceo@company.com',
+ subject => qr/New Pending Approval: PO approval request for PO/,
+ body => qr/pending your approval.*CFO approved.*ok with that\?/s
+},{ from => qr/RT System/,
+ to => 'minion@company.com',
+ subject => qr/Ticket Approved:/,
+ body => qr/approved by CFO.*notes: Resources exist to be consumed/s
+};
+
+is ($t->DependsOn->Count, 1, "still depends only on the CEO approval");
+is ($t->ReferredToBy->Count,2, "referred to by the two tickets");
+
+is_deeply([ $t->Status, $dependson_cfo->Status, $dependson_ceo->Status ],
+ [ 'new', 'resolved', 'open'], 'ticket state after cfo approval');
+
+mail_ok {
+ my $ceo = RT::CurrentUser->new;
+ $ceo->Load( $users{ceo} );
+
+ $dependson_ceo->CurrentUser($ceo);
+ my $notes = MIME::Entity->build(
+ Data => [ 'And consumed they will be.' ]
+ );
+ RT::I18N::SetMIMEEntityToUTF8($notes); # convert text parts into utf-8
+
+ my ( $notesval, $notesmsg ) = $dependson_ceo->Correspond( MIMEObj => $notes );
+ ok($notesval, $notesmsg);
+
+ my ($ok, $msg) = $dependson_ceo->SetStatus( Status => 'resolved' );
+ ok($ok, "ceo can approve - $msg");
+
+} { from => qr/RT System/,
+ to => 'minion@company.com',
+ subject => qr/Ticket Approved:/,
+ body => qr/approved by CEO.*Its Owner may now start to act on it.*notes: And consumed they will be/s,
+}, { from => qr'CEO via RT',
+ to => 'root@localhost',
+ subject => qr/Ticket Approved/,
+ body => qr/The ticket has been approved, you may now start to act on it/,
+};
+
+
+is_deeply([ $t->Status, $dependson_cfo->Status, $dependson_ceo->Status ],
+ [ 'new', 'resolved', 'resolved'], 'ticket state after ceo approval');
+
+$dependson_cfo->_Set(
+ Field => 'Status',
+ Value => 'open');
+
+$dependson_ceo->_Set(
+ Field => 'Status',
+ Value => 'new');
+
+mail_ok {
+ my $cfo = RT::CurrentUser->new;
+ $cfo->Load( $users{cfo} );
+
+ $dependson_cfo->CurrentUser($cfo);
+ my $notes = MIME::Entity->build(
+ Data => [ 'sorry, out of resources.' ]
+ );
+ RT::I18N::SetMIMEEntityToUTF8($notes); # convert text parts into utf-8
+
+ my ( $notesval, $notesmsg ) = $dependson_cfo->Correspond( MIMEObj => $notes );
+ ok($notesval, $notesmsg);
+
+ my ($ok, $msg) = $dependson_cfo->SetStatus( Status => 'rejected' );
+ ok($ok, "cfo can approve - $msg");
+
+} { from => qr/RT System/,
+ to => 'minion@company.com',
+ subject => qr/Ticket Rejected: PO for stationary/,
+ body => qr/rejected by CFO.*out of resources/s,
+};
+
+$t->Load($t->id);$dependson_ceo->Load($dependson_ceo->id);
+is_deeply([ $t->Status, $dependson_cfo->Status, $dependson_ceo->Status ],
+ [ 'rejected', 'rejected', 'deleted'], 'ticket state after cfo rejection');
+
diff --git a/rt/t/clicky.t b/rt/t/clicky.t
new file mode 100644
index 000000000..8d5227e04
--- /dev/null
+++ b/rt/t/clicky.t
@@ -0,0 +1,119 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+use Test::More;
+use RT::Test tests => 14;
+my %clicky;
+
+BEGIN {
+
+ %clicky = map { $_ => 1 } grep $_, RT->Config->Get('Active_MakeClicky');
+
+# this's hack: we have to use RT::Test first to get RT->Config work, this
+# results in the fact that we can't plan any more
+ unless ( keys %clicky ) {
+ SKIP: {
+ skip "No active Make Clicky actions", 14;
+ }
+ exit 0;
+ }
+}
+
+my ($baseurl, $m) = RT::Test->started_ok;
+
+use_ok('MIME::Entity');
+
+my $CurrentUser = $RT::SystemUser;
+
+my $queue = new RT::Queue($CurrentUser);
+$queue->Load('General') || Abort(loc("Queue could not be loaded."));
+
+my $message = MIME::Entity->build(
+ Subject => 'test',
+ Data => <<END,
+If you have some problems with RT you could find help
+on http://wiki.bestpractical.com or subscribe to
+the rt-users\@lists.bestpractical.com.
+
+--
+Best regards. BestPractical Team.
+END
+);
+
+my $ticket = new RT::Ticket( $CurrentUser );
+my ($id) = $ticket->Create(
+ Subject => 'test',
+ Queue => $queue->Id,
+ MIMEObj => $message,
+);
+ok($id, "We created a ticket #$id");
+ok($ticket->Transactions->First->Content, "Has some content");
+
+ok $m->login, 'logged in';
+ok $m->goto_ticket($id), 'opened diplay page of the ticket';
+
+SKIP: {
+ skip "httpurl action disabled", 1 unless $clicky{'httpurl'};
+ my @links = $m->find_link(
+ tag => 'a',
+ url => 'http://wiki.bestpractical.com',
+ text => 'Open URL',
+ );
+ ok( scalar @links, 'found clicky link' );
+}
+
+SKIP: {
+ skip "httpurl_overwrite action disabled", 1 unless $clicky{'httpurl_overwrite'};
+ my @links = $m->find_link(
+ tag => 'a',
+ url => 'http://wiki.bestpractical.com',
+ text => 'http://wiki.bestpractical.com',
+ );
+ ok( scalar @links, 'found clicky link' );
+}
+
+{
+
+my $message = MIME::Entity->build(
+ Type => 'text/html',
+ Subject => 'test',
+ Data => <<END,
+If you have some problems with RT you could find help
+on <a href="http://wiki.bestpractical.com">wiki</a>
+or find known bugs on http://rt3.fsck.com
+--
+Best regards. BestPractical Team.
+END
+);
+
+my $ticket = new RT::Ticket($CurrentUser);
+my ($id) = $ticket->Create(
+ Subject => 'test',
+ Queue => $queue->Id,
+ MIMEObj => $message,
+);
+ok( $id, "We created a ticket #$id" );
+ok( $ticket->Transactions->First->Content, "Has some content" );
+
+ok $m->login, 'logged in';
+ok $m->goto_ticket($id), 'opened diplay page of the ticket';
+
+SKIP: {
+ skip "httpurl action disabled", 2 unless $clicky{'httpurl'};
+ my @links = $m->find_link(
+ tag => 'a',
+ url => 'http://wiki.bestpractical.com',
+ text => 'Open URL',
+ );
+ ok( @links == 0, 'not make clicky links clicky twice' );
+
+ @links = $m->find_link(
+ tag => 'a',
+ url => 'http://rt3.fsck.com',
+ text => 'Open URL',
+ );
+ ok( scalar @links, 'found clicky link' );
+}
+
+}
diff --git a/rt/t/cron.t b/rt/t/cron.t
new file mode 100644
index 000000000..29d7bd7f6
--- /dev/null
+++ b/rt/t/cron.t
@@ -0,0 +1,90 @@
+#!/usr/bin/perl -w
+
+use strict;
+
+use RT;
+use RT::Test tests => 18;
+
+
+### Set up some testing data. Test the testing data because why not?
+
+# Create a user with rights, a queue, and some tickets.
+my $user_obj = RT::User->new($RT::SystemUser);
+my ($ret, $msg) = $user_obj->LoadOrCreateByEmail('tara@example.com');
+ok($ret, 'record test user creation');
+$user_obj->SetName('tara');
+$user_obj->PrincipalObj->GrantRight(Right => 'SuperUser');
+my $CurrentUser = RT::CurrentUser->new('tara');
+
+# Create our template, which will be used for tests of RT::Action::Record*.
+
+my $template_content = 'RT-Send-Cc: tla@example.com
+RT-Send-Bcc: jesse@example.com
+
+This is a content string with no content.';
+
+my $template_obj = RT::Template->new($CurrentUser);
+$template_obj->Create(Queue => '0',
+ Name => 'recordtest',
+ Description => 'testing Record actions',
+ Content => $template_content,
+ );
+
+# Create a queue and some tickets.
+
+my $queue_obj = RT::Queue->new($CurrentUser);
+($ret, $msg) = $queue_obj->Create(Name => 'recordtest', Description => 'queue for Action::Record testing');
+ok($ret, 'record test queue creation');
+
+my $ticket1 = RT::Ticket->new($CurrentUser);
+my ($id, $tobj, $msg2) = $ticket1->Create(Queue => $queue_obj,
+ Requestor => ['tara@example.com'],
+ Subject => 'bork bork bork',
+ Priority => 22,
+ );
+ok($id, 'record test ticket creation 1');
+my $ticket2 = RT::Ticket->new($CurrentUser);
+($id, $tobj, $msg2) = $ticket2->Create(Queue => $queue_obj,
+ Requestor => ['root@localhost'],
+ Subject => 'hurdy gurdy'
+ );
+ok($id, 'record test ticket creation 2');
+
+
+### OK. Have data, will travel.
+
+# First test the search.
+
+ok(require RT::Search::FromSQL, "Search::FromSQL loaded");
+my $ticketsqlstr = "Requestor.EmailAddress = '" . $CurrentUser->EmailAddress .
+ "' AND Priority > '20'";
+my $search = RT::Search::FromSQL->new(Argument => $ticketsqlstr, TicketsObj => RT::Tickets->new($CurrentUser),
+ );
+is(ref($search), 'RT::Search::FromSQL', "search created");
+ok($search->Prepare(), "fromsql search run");
+my $counter = 0;
+while(my $t = $search->TicketsObj->Next() ) {
+ is($t->Id(), $ticket1->Id(), "fromsql search results 1");
+ $counter++;
+}
+is ($counter, 1, "fromsql search results 2");
+
+# Right. Now test the actions.
+
+ok(require RT::Action::RecordComment);
+ok(require RT::Action::RecordCorrespondence);
+
+my ($comment_act, $correspond_act);
+ok($comment_act = RT::Action::RecordComment->new(TicketObj => $ticket1, TemplateObj => $template_obj, CurrentUser => $CurrentUser), "RecordComment created");
+ok($correspond_act = RT::Action::RecordCorrespondence->new(TicketObj => $ticket2, TemplateObj => $template_obj, CurrentUser => $CurrentUser), "RecordCorrespondence created");
+ok($comment_act->Prepare(), "Comment prepared");
+ok($correspond_act->Prepare(), "Correspond prepared");
+ok($comment_act->Commit(), "Comment committed");
+ok($correspond_act->Commit(), "Correspondence committed");
+
+# Now test for loop suppression.
+my ($trans, $desc, $transaction) = $ticket2->Comment(MIMEObj => $template_obj->MIMEObj);
+my $bogus_action = RT::Action::RecordComment->new(TicketObj => $ticket1, TemplateObj => $template_obj, TransactionObj => $transaction, CurrentUser => $CurrentUser);
+ok(!$bogus_action->Prepare(), "Comment aborted to prevent loop");
+
+1;
diff --git a/rt/t/customfields/access_via_queue.t b/rt/t/customfields/access_via_queue.t
new file mode 100644
index 000000000..c291860ea
--- /dev/null
+++ b/rt/t/customfields/access_via_queue.t
@@ -0,0 +1,160 @@
+#!/usr/bin/perl -w
+
+use strict;
+use warnings;
+
+use RT::Test tests => 35;
+use RT::Ticket;
+use RT::CustomField;
+
+my $queue_name = "CFSortQueue-$$";
+my $queue = RT::Test->load_or_create_queue( Name => $queue_name );
+ok($queue && $queue->id, "$queue_name - test queue creation");
+
+diag "create a CF\n" if $ENV{TEST_VERBOSE};
+my $cf_name = "Rights$$";
+my $cf;
+{
+ $cf = RT::CustomField->new( $RT::SystemUser );
+ my ($ret, $msg) = $cf->Create(
+ Name => $cf_name,
+ Queue => $queue->id,
+ Type => 'FreeformSingle',
+ );
+ ok($ret, "Custom Field Order created");
+}
+
+my $tester = RT::Test->load_or_create_user(
+ Name => 'tester', Password => 'password',
+);
+ok $tester && $tester->id, 'loaded or created user';
+
+my $cc_role = RT::Group->new( $queue->CurrentUser );
+$cc_role->LoadQueueRoleGroup( Type => 'Cc', Queue => $queue->id );
+
+my $owner_role = RT::Group->new( $queue->CurrentUser );
+$owner_role->LoadQueueRoleGroup( Type => 'Owner', Queue => $queue->id );
+
+ok( RT::Test->set_rights(
+ { Principal => $tester, Right => [qw(SeeQueue ShowTicket CreateTicket ReplyToTicket Watch OwnTicket TakeTicket)] },
+ { Principal => $cc_role, Object => $queue, Right => [qw(SeeCustomField)] },
+ { Principal => $owner_role, Object => $queue, Right => [qw(ModifyCustomField)] },
+), 'set rights');
+
+{
+ my $ticket = RT::Ticket->new( $tester );
+ my ($tid, $msg) = $ticket->Create( Queue => $queue, Subject => 'test' );
+ ok $tid, "created ticket";
+
+ ok !$ticket->CustomFields->First, "see no fields";
+}
+
+{
+ my $ticket = RT::Ticket->new( $tester );
+ my ($tid, $msg) = $ticket->Create( Queue => $queue, Subject => 'test', Cc => $tester->id );
+ ok $tid, "created ticket";
+
+ my $cf = $ticket->CustomFields->First;
+ ok $cf, "Ccs see cf";
+}
+
+{
+ my $ticket = RT::Ticket->new( $tester );
+ my ($tid, $msg) = $ticket->Create( Queue => $queue, Subject => 'test', Cc => $tester->id );
+ ok $tid, "created ticket";
+
+ (my $status, $msg) = $ticket->AddCustomFieldValue( Field => $cf->Name, Value => 'test' );
+ ok !$status, "Can not change CF";
+}
+
+{
+ my $ticket = RT::Ticket->new( $tester );
+ my ($tid, $msg) = $ticket->Create( Queue => $queue, Subject => 'test', Cc => $tester->id, Owner => $tester->id );
+ ok $tid, "created ticket";
+
+ (my $status, $msg) = $ticket->AddCustomFieldValue( Field => $cf->Name, Value => 'test' );
+ ok $status, "Changed CF";
+ is $ticket->FirstCustomFieldValue( $cf->Name ), 'test';
+
+ ($status, $msg) = $ticket->DeleteCustomFieldValue( Field => $cf->Name, Value => 'test' );
+ ok $status, "Changed CF";
+ is $ticket->FirstCustomFieldValue( $cf->Name ), undef;
+}
+
+{
+ my $ticket = RT::Ticket->new( $tester );
+ my ($tid, $msg) = $ticket->Create( Queue => $queue, Subject => 'test', Cc => $tester->id, Owner => $tester->id );
+ ok $tid, "created ticket";
+
+ (my $status, $msg) = $ticket->AddCustomFieldValue( Field => $cf->id, Value => 'test' );
+ ok $status, "Changed CF";
+ is $ticket->FirstCustomFieldValue( $cf->id ), 'test';
+
+ ($status, $msg) = $ticket->DeleteCustomFieldValue( Field => $cf->id, Value => 'test' );
+ ok $status, "Changed CF";
+ is $ticket->FirstCustomFieldValue( $cf->id ), undef;
+}
+
+my ($baseurl, $m) = RT::Test->started_ok;
+ok $m->login( tester => 'password' ), 'logged in';
+
+diag "check that we have no the CF on the create" if $ENV{'TEST_VERBOSE'};
+{
+ $m->submit_form(
+ form_name => "CreateTicketInQueue",
+ fields => { Queue => $queue->Name },
+ );
+
+ my $form = $m->form_name("TicketCreate");
+ my $cf_field = "Object-RT::Ticket--CustomField-". $cf->id ."-Value";
+ ok !$form->find_input( $cf_field ), 'no form field on the page';
+
+ $m->submit_form(
+ form_name => "TicketCreate",
+ fields => { Subject => 'test' },
+ );
+ my ($tid) = ($m->content =~ /Ticket (\d+) created/i);
+ ok $tid, "created a ticket succesfully";
+ $m->content_unlike(qr/$cf_name/, "don't see CF");
+
+ $m->follow_link( text => 'Custom Fields' );
+ $form = $m->form_number(3);
+ $cf_field = "Object-RT::Ticket-$tid-CustomField-". $cf->id ."-Value";
+ ok !$form->find_input( $cf_field ), 'no form field on the page';
+}
+
+diag "check that we see CF as Cc" if $ENV{'TEST_VERBOSE'};
+{
+ my $ticket = RT::Ticket->new( $tester );
+ my ($tid, $msg) = $ticket->Create( Queue => $queue, Subject => 'test', Cc => $tester->id );
+ ok $tid, "created ticket";
+
+ ok $m->goto_ticket( $tid ), "opened ticket";
+ $m->content_like(qr/$cf_name/, "see CF");
+}
+
+diag "check that owner can see and edit CF" if $ENV{'TEST_VERBOSE'};
+{
+ my $ticket = RT::Ticket->new( $tester );
+ my ($tid, $msg) = $ticket->Create( Queue => $queue, Subject => 'test', Cc => $tester->id, Owner => $tester->id );
+ ok $tid, "created ticket";
+
+ ok $m->goto_ticket( $tid ), "opened ticket";
+ $m->content_like(qr/$cf_name/, "see CF");
+
+ $m->follow_link( text => 'Custom Fields' );
+ my $form = $m->form_number(3);
+ my $cf_field = "Object-RT::Ticket-$tid-CustomField-". $cf->id ."-Value";
+ ok $form->find_input( $cf_field ), 'form field on the page';
+
+ $m->submit_form(
+ form_number => 3,
+ fields => {
+ $cf_field => "changed cf",
+ },
+ );
+
+ ok $m->goto_ticket( $tid ), "opened ticket";
+ $m->content_like(qr/$cf_name/, "changed cf");
+}
+
diff --git a/rt/t/customfields/sort_order.t b/rt/t/customfields/sort_order.t
new file mode 100644
index 000000000..472eb6bf3
--- /dev/null
+++ b/rt/t/customfields/sort_order.t
@@ -0,0 +1,92 @@
+#!/usr/bin/perl -w
+
+use strict;
+use warnings;
+
+use RT::Test tests => 18;
+use RT::Ticket;
+use RT::CustomField;
+
+my $queue_name = "CFSortQueue-$$";
+my $queue = RT::Test->load_or_create_queue( Name => $queue_name );
+ok($queue && $queue->id, "$queue_name - test queue creation");
+
+diag "create multiple CFs: B, A and C" if $ENV{TEST_VERBOSE};
+my @cfs = ();
+{
+ my $cf = RT::CustomField->new( $RT::SystemUser );
+ my ($ret, $msg) = $cf->Create(
+ Name => "CF B",
+ Queue => $queue->id,
+ Type => 'FreeformSingle',
+ );
+ ok($ret, "Custom Field Order created");
+ push @cfs, $cf;
+}
+{
+ my $cf = RT::CustomField->new( $RT::SystemUser );
+ my ($ret, $msg) = $cf->Create(
+ Name => "CF A",
+ Queue => $queue->id,
+ Type => 'FreeformSingle',
+ );
+ ok($ret, "Custom Field Order created");
+ push @cfs, $cf;
+}
+{
+ my $cf = RT::CustomField->new( $RT::SystemUser );
+ my ($ret, $msg) = $cf->Create(
+ Name => "CF C",
+ Queue => $queue->id,
+ Type => 'FreeformSingle',
+ );
+ ok($ret, "Custom Field Order created");
+ push @cfs, $cf;
+}
+
+my ($baseurl, $m) = RT::Test->started_ok;
+ok $m->login( root => 'password' ), 'logged in';
+
+diag "reorder CFs: C, A and B" if $ENV{TEST_VERBOSE};
+{
+ $m->get( '/Admin/Queues/' );
+ $m->follow_link_ok( {text => $queue->id} );
+ $m->follow_link_ok( {text => 'Ticket Custom Fields'} );
+
+ my @tmp = ($m->content =~ /(CF [ABC])/g);
+ is_deeply(\@tmp, ['CF B', 'CF A', 'CF C']);
+
+ $m->follow_link_ok( {text => 'Move up', n => 2} );
+ $m->follow_link_ok( {text => 'Move up', n => 1} );
+ $m->follow_link_ok( {text => 'Move up', n => 2} );
+
+ @tmp = ($m->content =~ /(CF [ABC])/g);
+ is_deeply(\@tmp, ['CF C', 'CF A', 'CF B']);
+}
+
+diag "check ticket create, display and edit pages" if $ENV{TEST_VERBOSE};
+{
+ $m->submit_form(
+ form_name => "CreateTicketInQueue",
+ fields => { Queue => $queue->Name },
+ );
+
+ my @tmp = ($m->content =~ /(CF [ABC])/g);
+ is_deeply(\@tmp, ['CF C', 'CF A', 'CF B']);
+
+ $m->submit_form(
+ form_name => "TicketCreate",
+ fields => { Subject => 'test' },
+ );
+ my ($tid) = ($m->content =~ /Ticket (\d+) created/i);
+ ok $tid, "created a ticket succesfully";
+
+ @tmp = ($m->content =~ /(CF [ABC])/g);
+ is_deeply(\@tmp, ['CF C', 'CF A', 'CF B']);
+
+ $m->follow_link_ok( {text => 'Custom Fields'} );
+
+ @tmp = ($m->content =~ /(CF [ABC])/g);
+ is_deeply(\@tmp, ['CF C', 'CF A', 'CF B']);
+}
+
diff --git a/rt/t/data/configs/apache2.2+fastcgi.conf b/rt/t/data/configs/apache2.2+fastcgi.conf
new file mode 100644
index 000000000..f9b3b1034
--- /dev/null
+++ b/rt/t/data/configs/apache2.2+fastcgi.conf
@@ -0,0 +1,44 @@
+ServerRoot %%SERVER_ROOT%%
+PidFile %%PID_FILE%%
+ServerAdmin root@localhost
+
+%%LOAD_MODULES%%
+
+TypesConfig /etc/mime.types
+FastCgiIpcDir /tmp
+
+<IfModule !mpm_netware_module>
+<IfModule !mpm_winnt_module>
+User www
+Group www
+</IfModule>
+</IfModule>
+
+Listen %%LISTEN%%
+
+ErrorLog "%%LOG_FILE%%"
+LogLevel debug
+
+<Directory />
+ Options FollowSymLinks
+ AllowOverride None
+ Order deny,allow
+ Deny from all
+</Directory>
+
+AddDefaultCharset UTF-8
+
+FastCgiServer %%RT_BIN_PATH%%/mason_handler.fcgi -processes 1 -idle-timeout 180 -initial-env RT_SITE_CONFIG=%%RT_SITE_CONFIG%%
+
+Alias /NoAuth/images/ %%DOCUMENT_ROOT%%/NoAuth/images/
+ScriptAlias / %%RT_BIN_PATH%%/mason_handler.fcgi/
+
+DocumentRoot "%%DOCUMENT_ROOT%%"
+<Location />
+ Order allow,deny
+ Allow from all
+
+ Options +ExecCGI
+ AddHandler fastcgi-script fcgi
+</Location>
+
diff --git a/rt/t/data/configs/apache2.2+fastcgi.conf.in b/rt/t/data/configs/apache2.2+fastcgi.conf.in
new file mode 100644
index 000000000..c0c01d1a5
--- /dev/null
+++ b/rt/t/data/configs/apache2.2+fastcgi.conf.in
@@ -0,0 +1,44 @@
+ServerRoot %%SERVER_ROOT%%
+PidFile %%PID_FILE%%
+ServerAdmin root@localhost
+
+%%LOAD_MODULES%%
+
+TypesConfig /etc/mime.types
+FastCgiIpcDir /tmp
+
+<IfModule !mpm_netware_module>
+<IfModule !mpm_winnt_module>
+User @WEB_USER@
+Group @WEB_GROUP@
+</IfModule>
+</IfModule>
+
+Listen %%LISTEN%%
+
+ErrorLog "%%LOG_FILE%%"
+LogLevel debug
+
+<Directory />
+ Options FollowSymLinks
+ AllowOverride None
+ Order deny,allow
+ Deny from all
+</Directory>
+
+AddDefaultCharset UTF-8
+
+FastCgiServer %%RT_BIN_PATH%%/mason_handler.fcgi -processes 1 -idle-timeout 180 -initial-env RT_SITE_CONFIG=%%RT_SITE_CONFIG%%
+
+Alias /NoAuth/images/ %%DOCUMENT_ROOT%%/NoAuth/images/
+ScriptAlias / %%RT_BIN_PATH%%/mason_handler.fcgi/
+
+DocumentRoot "%%DOCUMENT_ROOT%%"
+<Location />
+ Order allow,deny
+ Allow from all
+
+ Options +ExecCGI
+ AddHandler fastcgi-script fcgi
+</Location>
+
diff --git a/rt/t/data/configs/apache2.2+mod_perl.conf b/rt/t/data/configs/apache2.2+mod_perl.conf
new file mode 100644
index 000000000..341ae5024
--- /dev/null
+++ b/rt/t/data/configs/apache2.2+mod_perl.conf
@@ -0,0 +1,40 @@
+ServerRoot %%SERVER_ROOT%%
+PidFile %%PID_FILE%%
+ServerAdmin root@localhost
+
+%%LOAD_MODULES%%
+
+<IfModule !mpm_netware_module>
+<IfModule !mpm_winnt_module>
+User www
+Group www
+</IfModule>
+</IfModule>
+
+Listen %%LISTEN%%
+
+ErrorLog "%%LOG_FILE%%"
+LogLevel debug
+
+<Directory />
+ Options FollowSymLinks
+ AllowOverride None
+ Order deny,allow
+ Deny from all
+</Directory>
+
+AddDefaultCharset UTF-8
+PerlSetEnv RT_SITE_CONFIG %%RT_SITE_CONFIG%%
+PerlRequire %%RT_BIN_PATH%%/webmux.pl
+
+RedirectMatch permanent (.*)/$ $1/index.html
+
+DocumentRoot "%%DOCUMENT_ROOT%%"
+<Directory "%%DOCUMENT_ROOT%%">
+ Order allow,deny
+ Allow from all
+
+ SetHandler perl-script
+ PerlResponseHandler RT::Mason
+</Directory>
+
diff --git a/rt/t/data/configs/apache2.2+mod_perl.conf.in b/rt/t/data/configs/apache2.2+mod_perl.conf.in
new file mode 100644
index 000000000..ed12c8690
--- /dev/null
+++ b/rt/t/data/configs/apache2.2+mod_perl.conf.in
@@ -0,0 +1,40 @@
+ServerRoot %%SERVER_ROOT%%
+PidFile %%PID_FILE%%
+ServerAdmin root@localhost
+
+%%LOAD_MODULES%%
+
+<IfModule !mpm_netware_module>
+<IfModule !mpm_winnt_module>
+User @WEB_USER@
+Group @WEB_GROUP@
+</IfModule>
+</IfModule>
+
+Listen %%LISTEN%%
+
+ErrorLog "%%LOG_FILE%%"
+LogLevel debug
+
+<Directory />
+ Options FollowSymLinks
+ AllowOverride None
+ Order deny,allow
+ Deny from all
+</Directory>
+
+AddDefaultCharset UTF-8
+PerlSetEnv RT_SITE_CONFIG %%RT_SITE_CONFIG%%
+PerlRequire %%RT_BIN_PATH%%/webmux.pl
+
+RedirectMatch permanent (.*)/$ $1/index.html
+
+DocumentRoot "%%DOCUMENT_ROOT%%"
+<Directory "%%DOCUMENT_ROOT%%">
+ Order allow,deny
+ Allow from all
+
+ SetHandler perl-script
+ PerlResponseHandler RT::Mason
+</Directory>
+
diff --git a/rt/t/data/emails/8859-15-message-series/dir b/rt/t/data/emails/8859-15-message-series/dir
new file mode 100755
index 000000000..b9f8ec3ba
--- /dev/null
+++ b/rt/t/data/emails/8859-15-message-series/dir
@@ -0,0 +1,356 @@
+Return-Path: <rt-users-admin@lists.fsck.com>
+Delivered-To: j@pallas.eruditorum.org
+Received: from pallas.eruditorum.org (localhost [127.0.0.1])
+ by pallas.eruditorum.org (Postfix) with ESMTP
+ id 72E3A111B3; Mon, 26 May 2003 14:50:14 -0400 (EDT)
+Delivered-To: rt-users@pallas.eruditorum.org
+Received: from mail-in-02.arcor-online.net (mail-in-02.arcor-online.net [151.189.21.42])
+ by pallas.eruditorum.org (Postfix) with ESMTP id 15E761118D
+ for <rt-users@lists.fsck.com>; Mon, 26 May 2003 14:49:56 -0400 (EDT)
+Received: from otdial-212-144-012-186.arcor-ip.net (otdial-212-144-011-024.arcor-ip.net [212.144.11.24])
+ by mail-in-02.arcor-online.net (Postfix) with ESMTP
+ id 745EE15E87; Mon, 26 May 2003 20:53:15 +0200 (CEST)
+From: Dirk Pape <pape-rt@inf.fu-berlin.de>
+To: Jesse Vincent <jesse@bestpractical.com>,
+ rt-users <rt-users@lists.fsck.com>
+Subject: Re: [rt-users] [rt-announce] Development Snapshot 3.0.2++
+Message-ID: <2147483647.1053982235@otdial-212-144-011-024.arcor-ip.net>
+In-Reply-To: <2147483647.1053974498@[10.0.255.35]>
+References: <20030523202405.GF23719@fsck.com>
+ <2147483647.1053974498@[10.0.255.35]>
+X-Mailer: Mulberry/3.0.3 (Mac OS X)
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="==========2147500486=========="
+Sender: rt-users-admin@lists.fsck.com
+Errors-To: rt-users-admin@lists.fsck.com
+X-BeenThere: rt-users@lists.fsck.com
+X-Mailman-Version: 2.0.12
+Precedence: bulk
+List-Help: <mailto:rt-users-request@lists.fsck.com?subject=help>
+List-Post: <mailto:rt-users@lists.fsck.com>
+List-Subscribe: <http://lists.fsck.com/mailman/listinfo/rt-users>,
+ <mailto:rt-users-request@lists.fsck.com?subject=subscribe>
+List-Id: For users of RT: Request Tracker <rt-users.lists.fsck.com>
+List-Unsubscribe: <http://lists.fsck.com/mailman/listinfo/rt-users>,
+ <mailto:rt-users-request@lists.fsck.com?subject=unsubscribe>
+List-Archive: <http://lists.fsck.com/pipermail/rt-users/>
+Date: Mon, 26 May 2003 20:50:36 +0200
+X-Spam-Status: No, hits=-2.5 required=5.0
+ tests=AWL,IN_REP_TO,KNOWN_MAILING_LIST,QUOTED_EMAIL_TEXT,
+ REFERENCES,REPLY_WITH_QUOTES
+ autolearn=ham version=2.55
+X-Spam-Level:
+X-Spam-Checker-Version: SpamAssassin 2.55 (1.174.2.19-2003-05-19-exp)
+
+--==========2147500486==========
+Content-Type: text/plain; charset=us-ascii; format=flowed
+Content-Transfer-Encoding: 7bit
+Content-Disposition: inline
+
+Hello,
+
+here is the digest I forgot to attach. And I also forgot to say, that these
+were the only messages after a restart of apache.
+
+The messages in the digest are the copies which I - for testing purpose -
+allways queue into a mailbox just befor it is queued via rt-mailgate into
+the rt-system.
+
+--Am Montag, 26. Mai 2003 18:41 Uhr +0200 schrieb Dirk Pape
+<pape-rt@inf.fu-berlin.de>:
+
+> I attach a digest with mails I send one after another to the rt-system
+> and they get queued into one queue, each as a new ticket.
+
+
+
+
+--==========2147500486==========
+Content-Type: multipart/digest; boundary="==========2147489407=========="
+
+--==========2147489407==========
+Content-Type: message/rfc822; name="test _________"
+
+Return-Path: <pape@inf.fu-berlin.de>
+Delivered-To: pape-rtdoublecheck@mi.fu-berlin.de
+Received: (qmail 27591 invoked by uid 9804); 26 May 2003 18:10:50 +0200
+Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1)
+ by localhost with SMTP; 26 May 2003 18:10:46 +0200
+Received: (Qmail 27575 invoked from network); 26 May 2003 18:10:46 +0200
+Received: From es.inf.fu-berlin.de (160.45.110.22)
+ by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:10:46 +0200
+Received: from leibniz ([160.45.40.10] helo=math.fu-berlin.de)
+ by es.inf.fu-berlin.de with smtp (Exim 3.35 #1 (Debian))
+ id 19KKYe-0000Yi-00
+ for <staff@tec.mi.fu-berlin.de>; Mon, 26 May 2003 18:10:44 +0200
+Received: (qmail 27557 invoked by uid 9804); 26 May 2003 18:10:44 +0200
+Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1)
+ by localhost with SMTP; 26 May 2003 18:10:40 +0200
+Received: (Qmail 27540 invoked from network); 26 May 2003 18:10:40 +0200
+Received: From eremix.inf.fu-berlin.de (HELO eremix) (160.45.113.36)
+ by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:10:40 +0200
+Date: Mon, 26 May 2003 18:11:00 +0200
+From: Dirk Pape <pape@inf.fu-berlin.de>
+To: staff@tec.mi.fu-berlin.de
+Subject: =?ISO-8859-15?Q?test_=E4=F6=FC=DF=C4=D6=DC=DF=A4?=
+Message-ID: <2147483647.1053972660@[10.0.255.35]>
+X-Mailer: Mulberry/3.0.3 (Mac OS X)
+X-Envelope-Sender: pape@inf.fu-berlin.de
+X-Envelope-Sender: pape@inf.fu-berlin.de
+X-Virus-Scanned: by AMaViS 0.3.12pre7-U23 [27578] (NAI-uvscan@math.fu-berlin.de)
+X-Remote-IP: 160.45.110.22
+MIME-Version: 1.0
+Content-Type: text/plain; charset=iso-8859-15; FORMAT=flowed
+Content-Transfer-Encoding: quoted-printable
+Content-Disposition: inline
+
+test nochmal in anderer Queue
+test =E4=F6=FC=DF=C4=D6=DC=DF=A4
+
+--==========2147489407==========
+Content-Type: message/rfc822; name="test _________"
+
+Return-Path: <pape@inf.fu-berlin.de>
+Delivered-To: pape-rtdoublecheck@mi.fu-berlin.de
+Received: (qmail 27754 invoked by uid 9804); 26 May 2003 18:11:24 +0200
+Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1)
+ by localhost with SMTP; 26 May 2003 18:11:20 +0200
+Received: (Qmail 27704 invoked from network); 26 May 2003 18:11:19 +0200
+Received: From es.inf.fu-berlin.de (160.45.110.22)
+ by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:11:19 +0200
+Received: from leibniz ([160.45.40.10] helo=math.fu-berlin.de)
+ by es.inf.fu-berlin.de with smtp (Exim 3.35 #1 (Debian))
+ id 19KKZA-0000Yy-00
+ for <staff@tec.mi.fu-berlin.de>; Mon, 26 May 2003 18:11:16 +0200
+Received: (qmail 27690 invoked by uid 9804); 26 May 2003 18:11:16 +0200
+Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1)
+ by localhost with SMTP; 26 May 2003 18:11:13 +0200
+Received: (Qmail 27677 invoked from network); 26 May 2003 18:11:13 +0200
+Received: From eremix.inf.fu-berlin.de (HELO eremix) (160.45.113.36)
+ by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:11:13 +0200
+Date: Mon, 26 May 2003 18:11:32 +0200
+From: Dirk Pape <pape@inf.fu-berlin.de>
+To: staff@tec.mi.fu-berlin.de
+Subject: =?ISO-8859-15?Q?test_=E4=F6=FC=DF=C4=D6=DC=DF=A4?=
+Message-ID: <2147483647.1053972692@[10.0.255.35]>
+X-Mailer: Mulberry/3.0.3 (Mac OS X)
+X-Envelope-Sender: pape@inf.fu-berlin.de
+X-Envelope-Sender: pape@inf.fu-berlin.de
+X-Virus-Scanned: by AMaViS 0.3.12pre7-U23 [27711] (NAI-uvscan@math.fu-berlin.de)
+X-Remote-IP: 160.45.110.22
+MIME-Version: 1.0
+Content-Type: text/plain; charset=iso-8859-15; FORMAT=flowed
+Content-Transfer-Encoding: quoted-printable
+Content-Disposition: inline
+
+test nochmal in anderer Queue
+test =E4=F6=FC=DF=C4=D6=DC=DF=A4
+
+--==========2147489407==========
+Content-Type: message/rfc822; name="test _________"
+
+Return-Path: <pape@inf.fu-berlin.de>
+Delivered-To: pape-rtdoublecheck@mi.fu-berlin.de
+Received: (qmail 27971 invoked by uid 9804); 26 May 2003 18:12:02 +0200
+Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1)
+ by localhost with SMTP; 26 May 2003 18:11:52 +0200
+Received: (Qmail 27908 invoked from network); 26 May 2003 18:11:52 +0200
+Received: From es.inf.fu-berlin.de (160.45.110.22)
+ by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:11:52 +0200
+Received: from leibniz ([160.45.40.10] helo=math.fu-berlin.de)
+ by es.inf.fu-berlin.de with smtp (Exim 3.35 #1 (Debian))
+ id 19KKZj-0000ZC-00
+ for <staff@tec.mi.fu-berlin.de>; Mon, 26 May 2003 18:11:51 +0200
+Received: (qmail 27848 invoked by uid 9804); 26 May 2003 18:11:50 +0200
+Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1)
+ by localhost with SMTP; 26 May 2003 18:11:46 +0200
+Received: (Qmail 27809 invoked from network); 26 May 2003 18:11:45 +0200
+Received: From eremix.inf.fu-berlin.de (HELO eremix) (160.45.113.36)
+ by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:11:45 +0200
+Date: Mon, 26 May 2003 18:12:05 +0200
+From: Dirk Pape <pape@inf.fu-berlin.de>
+To: staff@tec.mi.fu-berlin.de
+Subject: =?ISO-8859-15?Q?test_=E4=F6=FC=DF=C4=D6=DC=DF=A4?=
+Message-ID: <2147483647.1053972725@[10.0.255.35]>
+X-Mailer: Mulberry/3.0.3 (Mac OS X)
+X-Envelope-Sender: pape@inf.fu-berlin.de
+X-Envelope-Sender: pape@inf.fu-berlin.de
+X-Virus-Scanned: by AMaViS 0.3.12pre7-U23 [27911] (NAI-uvscan@math.fu-berlin.de)
+X-Remote-IP: 160.45.110.22
+MIME-Version: 1.0
+Content-Type: text/plain; charset=iso-8859-15; FORMAT=flowed
+Content-Transfer-Encoding: quoted-printable
+Content-Disposition: inline
+
+test nochmal in anderer Queue
+test =E4=F6=FC=DF=C4=D6=DC=DF=A4
+
+--==========2147489407==========
+Content-Type: message/rfc822; name="test _________"
+
+Return-Path: <pape@inf.fu-berlin.de>
+Delivered-To: pape-rtdoublecheck@mi.fu-berlin.de
+Received: (qmail 28283 invoked by uid 9804); 26 May 2003 18:12:39 +0200
+Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1)
+ by localhost with SMTP; 26 May 2003 18:12:36 +0200
+Received: (Qmail 28256 invoked from network); 26 May 2003 18:12:35 +0200
+Received: From es.inf.fu-berlin.de (160.45.110.22)
+ by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:12:35 +0200
+Received: from leibniz ([160.45.40.10] helo=math.fu-berlin.de)
+ by es.inf.fu-berlin.de with smtp (Exim 3.35 #1 (Debian))
+ id 19KKaQ-0000ZQ-00
+ for <staff@tec.mi.fu-berlin.de>; Mon, 26 May 2003 18:12:34 +0200
+Received: (qmail 28236 invoked by uid 9804); 26 May 2003 18:12:34 +0200
+Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1)
+ by localhost with SMTP; 26 May 2003 18:12:30 +0200
+Received: (Qmail 28224 invoked from network); 26 May 2003 18:12:30 +0200
+Received: From eremix.inf.fu-berlin.de (HELO eremix) (160.45.113.36)
+ by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:12:30 +0200
+Date: Mon, 26 May 2003 18:12:50 +0200
+From: Dirk Pape <pape@inf.fu-berlin.de>
+To: staff@tec.mi.fu-berlin.de
+Subject: =?ISO-8859-15?Q?test_=E4=F6=FC=DF=C4=D6=DC=DF=A4?=
+Message-ID: <2147483647.1053972770@[10.0.255.35]>
+X-Mailer: Mulberry/3.0.3 (Mac OS X)
+X-Envelope-Sender: pape@inf.fu-berlin.de
+X-Envelope-Sender: pape@inf.fu-berlin.de
+X-Virus-Scanned: by AMaViS 0.3.12pre7-U23 [28259] (NAI-uvscan@math.fu-berlin.de)
+X-Remote-IP: 160.45.110.22
+MIME-Version: 1.0
+Content-Type: text/plain; charset=iso-8859-15; FORMAT=flowed
+Content-Transfer-Encoding: quoted-printable
+Content-Disposition: inline
+
+test nochmal in anderer Queue
+test =E4=F6=FC=DF=C4=D6=DC=DF=A4
+
+--==========2147489407==========
+Content-Type: message/rfc822; name="test _________"
+
+Return-Path: <pape@inf.fu-berlin.de>
+Delivered-To: pape-rtdoublecheck@mi.fu-berlin.de
+Received: (qmail 28578 invoked by uid 9804); 26 May 2003 18:13:20 +0200
+Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1)
+ by localhost with SMTP; 26 May 2003 18:13:15 +0200
+Received: (Qmail 28534 invoked from network); 26 May 2003 18:13:14 +0200
+Received: From es.inf.fu-berlin.de (160.45.110.22)
+ by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:13:14 +0200
+Received: from leibniz ([160.45.40.10] helo=math.fu-berlin.de)
+ by es.inf.fu-berlin.de with smtp (Exim 3.35 #1 (Debian))
+ id 19KKb1-0000Ze-00
+ for <staff@tec.mi.fu-berlin.de>; Mon, 26 May 2003 18:13:11 +0200
+Received: (qmail 28516 invoked by uid 9804); 26 May 2003 18:13:11 +0200
+Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1)
+ by localhost with SMTP; 26 May 2003 18:13:08 +0200
+Received: (Qmail 28479 invoked from network); 26 May 2003 18:13:07 +0200
+Received: From eremix.inf.fu-berlin.de (HELO eremix) (160.45.113.36)
+ by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:13:07 +0200
+Date: Mon, 26 May 2003 18:13:27 +0200
+From: Dirk Pape <pape@inf.fu-berlin.de>
+To: staff@tec.mi.fu-berlin.de
+Subject: =?ISO-8859-15?Q?test_=E4=F6=FC=DF=C4=D6=DC=DF=A4?=
+Message-ID: <2147483647.1053972807@[10.0.255.35]>
+X-Mailer: Mulberry/3.0.3 (Mac OS X)
+X-Envelope-Sender: pape@inf.fu-berlin.de
+X-Envelope-Sender: pape@inf.fu-berlin.de
+X-Virus-Scanned: by AMaViS 0.3.12pre7-U23 [28540] (NAI-uvscan@math.fu-berlin.de)
+X-Remote-IP: 160.45.110.22
+MIME-Version: 1.0
+Content-Type: text/plain; charset=iso-8859-15; FORMAT=flowed
+Content-Transfer-Encoding: quoted-printable
+Content-Disposition: inline
+
+test nochmal in anderer Queue
+test =E4=F6=FC=DF=C4=D6=DC=DF=A4
+
+
+--==========2147489407==========
+Content-Type: message/rfc822; name="test _________"
+
+Return-Path: <pape@inf.fu-berlin.de>
+Delivered-To: pape-rtdoublecheck@mi.fu-berlin.de
+Received: (qmail 29108 invoked by uid 9804); 26 May 2003 18:14:15 +0200
+Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1)
+ by localhost with SMTP; 26 May 2003 18:14:10 +0200
+Received: (Qmail 29066 invoked from network); 26 May 2003 18:14:10 +0200
+Received: From es.inf.fu-berlin.de (160.45.110.22)
+ by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:14:10 +0200
+Received: from leibniz ([160.45.40.10] helo=math.fu-berlin.de)
+ by es.inf.fu-berlin.de with smtp (Exim 3.35 #1 (Debian))
+ id 19KKbw-0000Zr-00
+ for <staff@tec.mi.fu-berlin.de>; Mon, 26 May 2003 18:14:08 +0200
+Received: (qmail 29054 invoked by uid 9804); 26 May 2003 18:14:08 +0200
+Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1)
+ by localhost with SMTP; 26 May 2003 18:14:04 +0200
+Received: (Qmail 29036 invoked from network); 26 May 2003 18:14:04 +0200
+Received: From eremix.inf.fu-berlin.de (HELO eremix) (160.45.113.36)
+ by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:14:04 +0200
+Date: Mon, 26 May 2003 18:14:24 +0200
+From: Dirk Pape <pape@inf.fu-berlin.de>
+To: staff@tec.mi.fu-berlin.de
+Subject: =?ISO-8859-15?Q?test_=E4=F6=FC=DF=C4=D6=DC=DF=A4?=
+Message-ID: <2147483647.1053972864@[10.0.255.35]>
+X-Mailer: Mulberry/3.0.3 (Mac OS X)
+X-Envelope-Sender: pape@inf.fu-berlin.de
+X-Envelope-Sender: pape@inf.fu-berlin.de
+X-Virus-Scanned: by AMaViS 0.3.12pre7-U23 [29069] (NAI-uvscan@math.fu-berlin.de)
+X-Remote-IP: 160.45.110.22
+MIME-Version: 1.0
+Content-Type: text/plain; charset=iso-8859-15; FORMAT=flowed
+Content-Transfer-Encoding: quoted-printable
+Content-Disposition: inline
+
+test nochmal in anderer Queue
+test =E4=F6=FC=DF=C4=D6=DC=DF=A4
+
+--==========2147489407==========
+Content-Type: message/rfc822; name="test _________"
+
+Return-Path: <pape@inf.fu-berlin.de>
+Delivered-To: pape-rtdoublecheck@mi.fu-berlin.de
+Received: (qmail 29551 invoked by uid 9804); 26 May 2003 18:15:16 +0200
+Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1)
+ by localhost with SMTP; 26 May 2003 18:15:12 +0200
+Received: (Qmail 29521 invoked from network); 26 May 2003 18:15:12 +0200
+Received: From es.inf.fu-berlin.de (160.45.110.22)
+ by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:15:12 +0200
+Received: from leibniz ([160.45.40.10] helo=math.fu-berlin.de)
+ by es.inf.fu-berlin.de with smtp (Exim 3.35 #1 (Debian))
+ id 19KKcx-0000a4-00
+ for <staff@tec.mi.fu-berlin.de>; Mon, 26 May 2003 18:15:11 +0200
+Received: (qmail 29511 invoked by uid 9804); 26 May 2003 18:15:10 +0200
+Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1)
+ by localhost with SMTP; 26 May 2003 18:15:07 +0200
+Received: (Qmail 29465 invoked from network); 26 May 2003 18:15:06 +0200
+Received: From eremix.inf.fu-berlin.de (HELO eremix) (160.45.113.36)
+ by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:15:06 +0200
+Date: Mon, 26 May 2003 18:15:26 +0200
+From: Dirk Pape <pape@inf.fu-berlin.de>
+To: staff@tec.mi.fu-berlin.de
+Subject: =?ISO-8859-15?Q?test_=E4=F6=FC=DF=C4=D6=DC=DF=A4?=
+Message-ID: <2147483647.1053972926@[10.0.255.35]>
+X-Mailer: Mulberry/3.0.3 (Mac OS X)
+X-Envelope-Sender: pape@inf.fu-berlin.de
+X-Envelope-Sender: pape@inf.fu-berlin.de
+X-Virus-Scanned: by AMaViS 0.3.12pre7-U23 [29524] (NAI-uvscan@math.fu-berlin.de)
+X-Remote-IP: 160.45.110.22
+MIME-Version: 1.0
+Content-Type: text/plain; charset=iso-8859-15; FORMAT=flowed
+Content-Transfer-Encoding: quoted-printable
+Content-Disposition: inline
+
+test nochmal in anderer Queue
+test =E4=F6=FC=DF=C4=D6=DC=DF=A4
+
+
+--==========2147489407==========--
+
+--==========2147500486==========--
+
+_______________________________________________
+rt-users mailing list
+rt-users@lists.fsck.com
+http://lists.fsck.com/mailman/listinfo/rt-users
+
+Have you read the FAQ? The RT FAQ Manager lives at http://fsck.com/rtfm
+
diff --git a/rt/t/data/emails/8859-15-message-series/msg1 b/rt/t/data/emails/8859-15-message-series/msg1
new file mode 100755
index 000000000..cc99c406c
--- /dev/null
+++ b/rt/t/data/emails/8859-15-message-series/msg1
@@ -0,0 +1,36 @@
+Return-Path: <pape@inf.fu-berlin.de>
+Delivered-To: pape-rtdoublecheck@mi.fu-berlin.de
+Received: (qmail 27591 invoked by uid 9804); 26 May 2003 18:10:50 +0200
+Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1)
+ by localhost with SMTP; 26 May 2003 18:10:46 +0200
+Received: (Qmail 27575 invoked from network); 26 May 2003 18:10:46 +0200
+Received: From es.inf.fu-berlin.de (160.45.110.22)
+ by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:10:46 +0200
+Received: from leibniz ([160.45.40.10] helo=math.fu-berlin.de)
+ by es.inf.fu-berlin.de with smtp (Exim 3.35 #1 (Debian))
+ id 19KKYe-0000Yi-00
+ for <staff@tec.mi.fu-berlin.de>; Mon, 26 May 2003 18:10:44 +0200
+Received: (qmail 27557 invoked by uid 9804); 26 May 2003 18:10:44 +0200
+Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1)
+ by localhost with SMTP; 26 May 2003 18:10:40 +0200
+Received: (Qmail 27540 invoked from network); 26 May 2003 18:10:40 +0200
+Received: From eremix.inf.fu-berlin.de (HELO eremix) (160.45.113.36)
+ by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:10:40 +0200
+Date: Mon, 26 May 2003 18:11:00 +0200
+From: Dirk Pape <pape@inf.fu-berlin.de>
+To: staff@tec.mi.fu-berlin.de
+Subject: =?ISO-8859-15?Q?test_=E4=F6=FC=DF=C4=D6=DC=DF=A4?=
+Message-ID: <2147483647.1053972660@[10.0.255.35]>
+X-Mailer: Mulberry/3.0.3 (Mac OS X)
+X-Envelope-Sender: pape@inf.fu-berlin.de
+X-Envelope-Sender: pape@inf.fu-berlin.de
+X-Virus-Scanned: by AMaViS 0.3.12pre7-U23 [27578] (NAI-uvscan@math.fu-berlin.de)
+X-Remote-IP: 160.45.110.22
+MIME-Version: 1.0
+Content-Type: text/plain; charset=iso-8859-15; FORMAT=flowed
+Content-Transfer-Encoding: quoted-printable
+Content-Disposition: inline
+
+test nochmal in anderer Queue
+test =E4=F6=FC=DF=C4=D6=DC=DF=A4
+
diff --git a/rt/t/data/emails/8859-15-message-series/msg2 b/rt/t/data/emails/8859-15-message-series/msg2
new file mode 100755
index 000000000..dc442cfc3
--- /dev/null
+++ b/rt/t/data/emails/8859-15-message-series/msg2
@@ -0,0 +1,36 @@
+Return-Path: <pape@inf.fu-berlin.de>
+Delivered-To: pape-rtdoublecheck@mi.fu-berlin.de
+Received: (qmail 27754 invoked by uid 9804); 26 May 2003 18:11:24 +0200
+Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1)
+ by localhost with SMTP; 26 May 2003 18:11:20 +0200
+Received: (Qmail 27704 invoked from network); 26 May 2003 18:11:19 +0200
+Received: From es.inf.fu-berlin.de (160.45.110.22)
+ by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:11:19 +0200
+Received: from leibniz ([160.45.40.10] helo=math.fu-berlin.de)
+ by es.inf.fu-berlin.de with smtp (Exim 3.35 #1 (Debian))
+ id 19KKZA-0000Yy-00
+ for <staff@tec.mi.fu-berlin.de>; Mon, 26 May 2003 18:11:16 +0200
+Received: (qmail 27690 invoked by uid 9804); 26 May 2003 18:11:16 +0200
+Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1)
+ by localhost with SMTP; 26 May 2003 18:11:13 +0200
+Received: (Qmail 27677 invoked from network); 26 May 2003 18:11:13 +0200
+Received: From eremix.inf.fu-berlin.de (HELO eremix) (160.45.113.36)
+ by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:11:13 +0200
+Date: Mon, 26 May 2003 18:11:32 +0200
+From: Dirk Pape <pape@inf.fu-berlin.de>
+To: staff@tec.mi.fu-berlin.de
+Subject: =?ISO-8859-15?Q?test_=E4=F6=FC=DF=C4=D6=DC=DF=A4?=
+Message-ID: <2147483647.1053972692@[10.0.255.35]>
+X-Mailer: Mulberry/3.0.3 (Mac OS X)
+X-Envelope-Sender: pape@inf.fu-berlin.de
+X-Envelope-Sender: pape@inf.fu-berlin.de
+X-Virus-Scanned: by AMaViS 0.3.12pre7-U23 [27711] (NAI-uvscan@math.fu-berlin.de)
+X-Remote-IP: 160.45.110.22
+MIME-Version: 1.0
+Content-Type: text/plain; charset=iso-8859-15; FORMAT=flowed
+Content-Transfer-Encoding: quoted-printable
+Content-Disposition: inline
+
+test nochmal in anderer Queue
+test =E4=F6=FC=DF=C4=D6=DC=DF=A4
+
diff --git a/rt/t/data/emails/8859-15-message-series/msg3 b/rt/t/data/emails/8859-15-message-series/msg3
new file mode 100755
index 000000000..e23866d5f
--- /dev/null
+++ b/rt/t/data/emails/8859-15-message-series/msg3
@@ -0,0 +1,35 @@
+Return-Path: <pape@inf.fu-berlin.de>
+Delivered-To: pape-rtdoublecheck@mi.fu-berlin.de
+Received: (qmail 27971 invoked by uid 9804); 26 May 2003 18:12:02 +0200
+Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1)
+ by localhost with SMTP; 26 May 2003 18:11:52 +0200
+Received: (Qmail 27908 invoked from network); 26 May 2003 18:11:52 +0200
+Received: From es.inf.fu-berlin.de (160.45.110.22)
+ by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:11:52 +0200
+Received: from leibniz ([160.45.40.10] helo=math.fu-berlin.de)
+ by es.inf.fu-berlin.de with smtp (Exim 3.35 #1 (Debian))
+ id 19KKZj-0000ZC-00
+ for <staff@tec.mi.fu-berlin.de>; Mon, 26 May 2003 18:11:51 +0200
+Received: (qmail 27848 invoked by uid 9804); 26 May 2003 18:11:50 +0200
+Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1)
+ by localhost with SMTP; 26 May 2003 18:11:46 +0200
+Received: (Qmail 27809 invoked from network); 26 May 2003 18:11:45 +0200
+Received: From eremix.inf.fu-berlin.de (HELO eremix) (160.45.113.36)
+ by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:11:45 +0200
+Date: Mon, 26 May 2003 18:12:05 +0200
+From: Dirk Pape <pape@inf.fu-berlin.de>
+To: staff@tec.mi.fu-berlin.de
+Subject: =?ISO-8859-15?Q?test_=E4=F6=FC=DF=C4=D6=DC=DF=A4?=
+Message-ID: <2147483647.1053972725@[10.0.255.35]>
+X-Mailer: Mulberry/3.0.3 (Mac OS X)
+X-Envelope-Sender: pape@inf.fu-berlin.de
+X-Envelope-Sender: pape@inf.fu-berlin.de
+X-Virus-Scanned: by AMaViS 0.3.12pre7-U23 [27911] (NAI-uvscan@math.fu-berlin.de)
+X-Remote-IP: 160.45.110.22
+MIME-Version: 1.0
+Content-Type: text/plain; charset=iso-8859-15; FORMAT=flowed
+Content-Transfer-Encoding: quoted-printable
+Content-Disposition: inline
+
+test nochmal in anderer Queue
+test =E4=F6=FC=DF=C4=D6=DC=DF=A4
diff --git a/rt/t/data/emails/8859-15-message-series/msg4 b/rt/t/data/emails/8859-15-message-series/msg4
new file mode 100755
index 000000000..831695cc7
--- /dev/null
+++ b/rt/t/data/emails/8859-15-message-series/msg4
@@ -0,0 +1,35 @@
+Return-Path: <pape@inf.fu-berlin.de>
+Delivered-To: pape-rtdoublecheck@mi.fu-berlin.de
+Received: (qmail 28283 invoked by uid 9804); 26 May 2003 18:12:39 +0200
+Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1)
+ by localhost with SMTP; 26 May 2003 18:12:36 +0200
+Received: (Qmail 28256 invoked from network); 26 May 2003 18:12:35 +0200
+Received: From es.inf.fu-berlin.de (160.45.110.22)
+ by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:12:35 +0200
+Received: from leibniz ([160.45.40.10] helo=math.fu-berlin.de)
+ by es.inf.fu-berlin.de with smtp (Exim 3.35 #1 (Debian))
+ id 19KKaQ-0000ZQ-00
+ for <staff@tec.mi.fu-berlin.de>; Mon, 26 May 2003 18:12:34 +0200
+Received: (qmail 28236 invoked by uid 9804); 26 May 2003 18:12:34 +0200
+Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1)
+ by localhost with SMTP; 26 May 2003 18:12:30 +0200
+Received: (Qmail 28224 invoked from network); 26 May 2003 18:12:30 +0200
+Received: From eremix.inf.fu-berlin.de (HELO eremix) (160.45.113.36)
+ by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:12:30 +0200
+Date: Mon, 26 May 2003 18:12:50 +0200
+From: Dirk Pape <pape@inf.fu-berlin.de>
+To: staff@tec.mi.fu-berlin.de
+Subject: =?ISO-8859-15?Q?test_=E4=F6=FC=DF=C4=D6=DC=DF=A4?=
+Message-ID: <2147483647.1053972770@[10.0.255.35]>
+X-Mailer: Mulberry/3.0.3 (Mac OS X)
+X-Envelope-Sender: pape@inf.fu-berlin.de
+X-Envelope-Sender: pape@inf.fu-berlin.de
+X-Virus-Scanned: by AMaViS 0.3.12pre7-U23 [28259] (NAI-uvscan@math.fu-berlin.de)
+X-Remote-IP: 160.45.110.22
+MIME-Version: 1.0
+Content-Type: text/plain; charset=iso-8859-15; FORMAT=flowed
+Content-Transfer-Encoding: quoted-printable
+Content-Disposition: inline
+
+test nochmal in anderer Queue
+test =E4=F6=FC=DF=C4=D6=DC=DF=A4
diff --git a/rt/t/data/emails/8859-15-message-series/msg5 b/rt/t/data/emails/8859-15-message-series/msg5
new file mode 100755
index 000000000..272c93c4f
--- /dev/null
+++ b/rt/t/data/emails/8859-15-message-series/msg5
@@ -0,0 +1,35 @@
+Return-Path: <pape@inf.fu-berlin.de>
+Delivered-To: pape-rtdoublecheck@mi.fu-berlin.de
+Received: (qmail 28578 invoked by uid 9804); 26 May 2003 18:13:20 +0200
+Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1)
+ by localhost with SMTP; 26 May 2003 18:13:15 +0200
+Received: (Qmail 28534 invoked from network); 26 May 2003 18:13:14 +0200
+Received: From es.inf.fu-berlin.de (160.45.110.22)
+ by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:13:14 +0200
+Received: from leibniz ([160.45.40.10] helo=math.fu-berlin.de)
+ by es.inf.fu-berlin.de with smtp (Exim 3.35 #1 (Debian))
+ id 19KKb1-0000Ze-00
+ for <staff@tec.mi.fu-berlin.de>; Mon, 26 May 2003 18:13:11 +0200
+Received: (qmail 28516 invoked by uid 9804); 26 May 2003 18:13:11 +0200
+Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1)
+ by localhost with SMTP; 26 May 2003 18:13:08 +0200
+Received: (Qmail 28479 invoked from network); 26 May 2003 18:13:07 +0200
+Received: From eremix.inf.fu-berlin.de (HELO eremix) (160.45.113.36)
+ by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:13:07 +0200
+Date: Mon, 26 May 2003 18:13:27 +0200
+From: Dirk Pape <pape@inf.fu-berlin.de>
+To: staff@tec.mi.fu-berlin.de
+Subject: =?ISO-8859-15?Q?test_=E4=F6=FC=DF=C4=D6=DC=DF=A4?=
+Message-ID: <2147483647.1053972807@[10.0.255.35]>
+X-Mailer: Mulberry/3.0.3 (Mac OS X)
+X-Envelope-Sender: pape@inf.fu-berlin.de
+X-Envelope-Sender: pape@inf.fu-berlin.de
+X-Virus-Scanned: by AMaViS 0.3.12pre7-U23 [28540] (NAI-uvscan@math.fu-berlin.de)
+X-Remote-IP: 160.45.110.22
+MIME-Version: 1.0
+Content-Type: text/plain; charset=iso-8859-15; FORMAT=flowed
+Content-Transfer-Encoding: quoted-printable
+Content-Disposition: inline
+
+test nochmal in anderer Queue
+test =E4=F6=FC=DF=C4=D6=DC=DF=A4
diff --git a/rt/t/data/emails/8859-15-message-series/msg6 b/rt/t/data/emails/8859-15-message-series/msg6
new file mode 100755
index 000000000..3ae9d3b69
--- /dev/null
+++ b/rt/t/data/emails/8859-15-message-series/msg6
@@ -0,0 +1,35 @@
+Return-Path: <pape@inf.fu-berlin.de>
+Delivered-To: pape-rtdoublecheck@mi.fu-berlin.de
+Received: (qmail 29108 invoked by uid 9804); 26 May 2003 18:14:15 +0200
+Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1)
+ by localhost with SMTP; 26 May 2003 18:14:10 +0200
+Received: (Qmail 29066 invoked from network); 26 May 2003 18:14:10 +0200
+Received: From es.inf.fu-berlin.de (160.45.110.22)
+ by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:14:10 +0200
+Received: from leibniz ([160.45.40.10] helo=math.fu-berlin.de)
+ by es.inf.fu-berlin.de with smtp (Exim 3.35 #1 (Debian))
+ id 19KKbw-0000Zr-00
+ for <staff@tec.mi.fu-berlin.de>; Mon, 26 May 2003 18:14:08 +0200
+Received: (qmail 29054 invoked by uid 9804); 26 May 2003 18:14:08 +0200
+Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1)
+ by localhost with SMTP; 26 May 2003 18:14:04 +0200
+Received: (Qmail 29036 invoked from network); 26 May 2003 18:14:04 +0200
+Received: From eremix.inf.fu-berlin.de (HELO eremix) (160.45.113.36)
+ by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:14:04 +0200
+Date: Mon, 26 May 2003 18:14:24 +0200
+From: Dirk Pape <pape@inf.fu-berlin.de>
+To: staff@tec.mi.fu-berlin.de
+Subject: =?ISO-8859-15?Q?test_=E4=F6=FC=DF=C4=D6=DC=DF=A4?=
+Message-ID: <2147483647.1053972864@[10.0.255.35]>
+X-Mailer: Mulberry/3.0.3 (Mac OS X)
+X-Envelope-Sender: pape@inf.fu-berlin.de
+X-Envelope-Sender: pape@inf.fu-berlin.de
+X-Virus-Scanned: by AMaViS 0.3.12pre7-U23 [29069] (NAI-uvscan@math.fu-berlin.de)
+X-Remote-IP: 160.45.110.22
+MIME-Version: 1.0
+Content-Type: text/plain; charset=iso-8859-15; FORMAT=flowed
+Content-Transfer-Encoding: quoted-printable
+Content-Disposition: inline
+
+test nochmal in anderer Queue
+test =E4=F6=FC=DF=C4=D6=DC=DF=A4
diff --git a/rt/t/data/emails/8859-15-message-series/msg7 b/rt/t/data/emails/8859-15-message-series/msg7
new file mode 100755
index 000000000..6149dd644
--- /dev/null
+++ b/rt/t/data/emails/8859-15-message-series/msg7
@@ -0,0 +1,36 @@
+Return-Path: <pape@inf.fu-berlin.de>
+Delivered-To: pape-rtdoublecheck@mi.fu-berlin.de
+Received: (qmail 29551 invoked by uid 9804); 26 May 2003 18:15:16 +0200
+Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1)
+ by localhost with SMTP; 26 May 2003 18:15:12 +0200
+Received: (Qmail 29521 invoked from network); 26 May 2003 18:15:12 +0200
+Received: From es.inf.fu-berlin.de (160.45.110.22)
+ by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:15:12 +0200
+Received: from leibniz ([160.45.40.10] helo=math.fu-berlin.de)
+ by es.inf.fu-berlin.de with smtp (Exim 3.35 #1 (Debian))
+ id 19KKcx-0000a4-00
+ for <staff@tec.mi.fu-berlin.de>; Mon, 26 May 2003 18:15:11 +0200
+Received: (qmail 29511 invoked by uid 9804); 26 May 2003 18:15:10 +0200
+Received: from localhost (HELO math.fu-berlin.de) (127.0.0.1)
+ by localhost with SMTP; 26 May 2003 18:15:07 +0200
+Received: (Qmail 29465 invoked from network); 26 May 2003 18:15:06 +0200
+Received: From eremix.inf.fu-berlin.de (HELO eremix) (160.45.113.36)
+ by leibniz.math.fu-berlin.de with SMTP; 26 May 2003 18:15:06 +0200
+Date: Mon, 26 May 2003 18:15:26 +0200
+From: Dirk Pape <pape@inf.fu-berlin.de>
+To: staff@tec.mi.fu-berlin.de
+Subject: =?ISO-8859-15?Q?test_=E4=F6=FC=DF=C4=D6=DC=DF=A4?=
+Message-ID: <2147483647.1053972926@[10.0.255.35]>
+X-Mailer: Mulberry/3.0.3 (Mac OS X)
+X-Envelope-Sender: pape@inf.fu-berlin.de
+X-Envelope-Sender: pape@inf.fu-berlin.de
+X-Virus-Scanned: by AMaViS 0.3.12pre7-U23 [29524] (NAI-uvscan@math.fu-berlin.de)
+X-Remote-IP: 160.45.110.22
+MIME-Version: 1.0
+Content-Type: text/plain; charset=iso-8859-15; FORMAT=flowed
+Content-Transfer-Encoding: quoted-printable
+Content-Disposition: inline
+
+test nochmal in anderer Queue
+test =E4=F6=FC=DF=C4=D6=DC=DF=A4
+
diff --git a/rt/t/data/emails/crashes-file-based-parser b/rt/t/data/emails/crashes-file-based-parser
new file mode 100755
index 000000000..da1913eb9
--- /dev/null
+++ b/rt/t/data/emails/crashes-file-based-parser
@@ -0,0 +1,193 @@
+X-Real-To: <mitya@example.com>
+Received: from [194.87.5.31] (HELO sinbin.d-s.example.com)
+ by cgp.example.com (CommuniGate Pro SMTP 4.0.6/D4)
+ with ESMTP-TLS id 125035761 for mitya@example.com; Thu, 11 Dec 2003 15:17:46 +0300
+Received: (from daemon@localhost)
+ by sinbin.d-s.example.com (8.12.9p1/8.11.6) id hBBCHjN0031595
+ for mitya@example.com; Thu, 11 Dec 2003 15:17:45 +0300 (MSK)
+ (envelope-from noc@rt3.mx.example.com)
+Received: from d-s.example.com by sinbin.d-s.example.com with ESMTP id hBBCHjar031575;
+ (8.12.9p2/D) Thu, 11 Dec 2003 15:17:45 +0300 (MSK)
+X-Real-To: <mitya@example.com>
+Sender: <noc@rt3.mx.example.com> (Network Operation Center)
+To: mitya@example.com
+Date: Thu, 11 Dec 2003 15:17:45 +0300
+Message-ID: <redirect-137509289@d-s.example.com>
+X-Original-Return-Path: <vox19@b92.d-s.example.com>
+Received: from [194.87.0.16] (HELO mail.d-s.example.com)
+ by d-s.example.com (CommuniGate Pro SMTP 4.1.5/D1)
+ with ESMTP id 120757484 for noc@rt3.mx.example.com; Mon, 27 Oct 2003 09:40:53 +0300
+Received: from [194.87.0.22] (HELO moscvax.d-s.example.com)
+ by mail.d-s.example.com (CommuniGate Pro SMTP 4.1.5/D)
+ with ESMTP-TLS id 107945800 for noc@rt3.mx.example.com; Mon, 27 Oct 2003 09:40:53 +0300
+Received: from d-s.example.com (mx.d-s.example.com [194.87.0.32])
+ by moscvax.d-s.example.com (8.12.9/8.12.9) with ESMTP id h9R6erFm062621
+ for <security@d.example.com>; Mon, 27 Oct 2003 09:40:53 +0300 (MSK)
+ (envelope-from vox19@b92.d-s.example.com)
+Received: by d-s.example.com (CommuniGate Pro PIPE 4.1.5/D1)
+ with PIPE id 120757490; Mon, 27 Oct 2003 09:40:53 +0300
+Received: from [194.87.2.108] (HELO b92.d-s.example.com)
+ by d-s.example.com (CommuniGate Pro SMTP 4.1.5/D1)
+ with ESMTP-TLS id 120757480 for security@d.example.com; Mon, 27 Oct 2003 09:40:52 +0300
+Received: from b92.d-s.example.com (localhost [127.0.0.1])
+ by b92.d-s.example.com (8.12.8p1/8.12.3) with ESMTP id h9R6eqIe014669
+ for <security@d.example.com>; Mon, 27 Oct 2003 09:40:52 +0300 (MSK)
+ (envelope-from vox19@b92.d-s.example.com)
+Received: from localhost (localhost [[UNIX: localhost]])
+ by b92.d-s.example.com (8.12.8p1/8.12.3/Submit) id h9R6epst014668
+ for security@d.example.com; Mon, 27 Oct 2003 09:40:51 +0300 (MSK)
+From: "Stanislav" <drstas@d.example.com>
+Subject: Fwd: scanning my ports
+X-Original-Date: Mon, 27 Oct 2003 10:40:51 +0400
+User-Agent: KMail/1.5.4
+X-Original-To: security@d.example.com
+MIME-Version: 1.0
+Content-Type: Multipart/Mixed;
+ boundary="Boundary-00=_z3Ln/tUeUBipHgx"
+X-Original-Message-Id: <200310270940.51758.vox19@d.example.com>
+X-Spam-Checker-Version: SpamAssassin 2.60-jumbo.demos (1.212-2003-09-23-exp)
+X-Spam-Level:
+X-Spam-Status: No, hits=-6.8 required=5.0 tests=BAYES_00,FROM_ENDS_IN_NUMS,
+ HTML_MESSAGE,SUBJECT_RT autolearn=ham version=2.60-jumbo.demos
+X-Spam-Report: -6.8 points, 5.0 required;
+ * -3.0 SUBJECT_RT Tracking system
+ * 1.0 FROM_ENDS_IN_NUMS From: ends in numbers
+ * 0.1 HTML_MESSAGE BODY: HTML included in message
+ * -4.9 BAYES_00 BODY: Bayesian spam probability is 0 to 1%
+ * [score: 0.0000]
+
+
+--Boundary-00=_z3Ln/tUeUBipHgx
+Content-Type: text/plain;
+ charset="koi8-r"
+Content-Transfer-Encoding: 7bit
+Content-Disposition: inline
+
+
+FYI
+
+
+---------- Forwarded Message ----------
+
+Subject: [DEMOS #12148] scanning my ports
+Date: Sunday 26 October 2003 20:19
+From: 1stwizard@isp.example.com
+To: no-reply@d-r.example.com
+
+This transaction appears to have no content
+
+-------------------------------------------------------
+
+
+
+--
+best wishes,
+
+Stanislav A. Mushkat
+http://www.di.example.com
+
+--Boundary-00=_z3Ln/tUeUBipHgx
+Content-Type: text/plain;
+ charset="iso-8859-1";
+ name=" "
+Content-Transfer-Encoding: 7bit
+Content-Disposition: inline
+
+Somebody at IP 127.0.0.1 scanned my ports.
+--Boundary-00=_z3Ln/tUeUBipHgx
+Content-Type: text/html;
+ charset="iso-8859-1";
+ name=" "
+Content-Transfer-Encoding: 7bit
+Content-Disposition: inline
+
+<HTML><HEAD>
+<META http-equiv=Content-Type content="text/html; charset=iso-8859-1">
+<META content="IncrediMail 1.0" name=GENERATOR>
+<!--IncrdiXMLRemarkStart>
+<IncrdiX-Info>
+<X-FID>BA285063-5BCE-11D4-AF8D-0050DAC67E11</X-FID>
+<X-FVER>2.0</X-FVER>
+<X-FIT>Letter</X-FIT>
+<X-FCOL>Elegant Paper</X-FCOL>
+<X-FCAT>Stationery</X-FCAT>
+<X-FDIS>Rice Fields</X-FDIS>
+<X-Extensions>SU1CTDEsNDEsgUmBSTAkkcGNgZmVTY0wNCxNhYUoiU0kOMEoTYGBjYEoJDSZnSyFhUksSU1CTDIsMCwsSU1CTDMsMCwsVHlwZVZlcnNpb24sMywxLjAs</X-Extensions>
+<X-BG>8E549F43-079D-11D8-B0F9-00B0D0B65B96</X-BG>
+<X-BGT>repeat</X-BGT>
+<X-BGC>#eff3f7</X-BGC>
+<X-BGPX>left</X-BGPX>
+<X-BGPY>0px</X-BGPY>
+<X-ASN>ANIM3D00-NONE-0000-0000-000000000000</X-ASN>
+<X-ASNF>0</X-ASNF>
+<X-ASH>ANIM3D00-NONE-0000-0000-000000000000</X-ASH>
+<X-ASHF>1</X-ASHF>
+<X-AN>6486DDE0-3EFD-11D4-BA3D-0050DAC68030</X-AN>
+<X-ANF>0</X-ANF>
+<X-AP>6486DDE0-3EFD-11D4-BA3D-0050DAC68030</X-AP>
+<X-APF>1</X-APF>
+<X-AD>C3C52140-4147-11D4-BA3D-0050DAC68030</X-AD>
+<X-ADF>0</X-ADF>
+<X-AUTO>X-ASN,X-ASH,X-AN,X-AP,X-AD</X-AUTO>
+<X-CNT>;</X-CNT>
+</IncrdiX-Info>
+<IncrdiXMLRemarkEnd-->
+</HEAD>
+<BODY style="BACKGROUND-POSITION: left 0px; FONT-SIZE: 12pt; MARGIN: 0px 10px 10px; COLOR: #00005b; BACKGROUND-REPEAT: repeat; FONT-FAMILY: Arial" text=#00005b bgColor=#eff3f7 background=cid:8E549F43-079D-11D8-B0F9-00B0D0B65B96 scroll=yes SIGCOLOR="0" X-ADF="0" X-AD="C3C52140-4147-11D4-BA3D-0050DAC68030" X-APF="1" X-AP="6486DDE0-3EFD-11D4-BA3D-0050DAC68030" X-ANF="0" X-AN="6486DDE0-3EFD-11D4-BA3D-0050DAC68030" X-ASHF="1" X-ASH="ANIM3D00-NONE-0000-0000-000000000000" X-ASNF="0" X-ASN="ANIM3D00-NONE-0000-0000-000000000000" X-FVER="2.0" X-FID="BA285063-5BCE-11D4-AF8D-0050DAC67E11" X-FIT="Letter" X-FCOL="Elegant Paper" X-FCAT="Elegant Paper" X-FDIS="Rice Fields" ORGYPOS="0">
+<TABLE id=INCREDIMAINTABLE cellSpacing=0 cellPadding=2 width="100%" border=0>
+<TBODY>
+<TR>
+<TD id=INCREDITEXTREGION style="PADDING-RIGHT: 0px; PADDING-LEFT: 0px; FONT-SIZE: 12pt; PADDING-BOTTOM: 0px; CURSOR: auto; PADDING-TOP: 0px" vAlign=top width="100%">
+<DIV>Somebody at IP 127.0.0.1 scanned my ports. </DIV>
+<DIV>&nbsp;</DIV>
+<DIV>&nbsp;</DIV></TD></TR>
+<TR>
+<TD id=INCREDIFOOTER width="100%">
+<TABLE cellSpacing=0 cellPadding=0 width="100%">
+<TBODY>
+<TR>
+<TD width="100%"></TD>
+<TD id=INCREDISOUND vAlign=bottom align=middle></TD>
+<TD id=INCREDIANIM vAlign=bottom align=middle></TD></TR></TBODY></TABLE></TD></TR></TBODY></TABLE></BODY></HTML>
+--Boundary-00=_z3Ln/tUeUBipHgx
+Content-Type: image/jpeg;
+ charset="iso-8859-1";
+ name="BackGrnd.jpg"
+Content-Transfer-Encoding: base64
+Content-Disposition: inline; filename="BackGrnd.jpg"
+
+/9j/4AAQSkZJRgABAgAAZABkAAD/7AARRHVja3kAAQAEAAAAHgAA/+4AIUFk
+b2JlAGTAAAAAAQMAEAMCAwYAAAHbAAAC1gAABZX/2wCEABALCwsMCxAMDBAX
+Dw0PFxsUEBAUGx8XFxcXFx8eFxoaGhoXHh4jJSclIx4vLzMzLy9AQEBAQEBA
+QEBAQEBAQEABEQ8PERMRFRISFRQRFBEUGhQWFhQaJhoaHBoaJjAjHh4eHiMw
+Ky4nJycuKzU1MDA1NUBAP0BAQEBAQEBAQEBAQP/CABEIAGUAcwMBIgACEQED
+EQH/xACAAAEBAQEAAAAAAAAAAAAAAAAAAQIGAQEBAAAAAAAAAAAAAAAAAAAA
+ARABAAICAwEAAgMAAAAAAAAAAQARIQIxQRIiQDIQMFARAAICAgIBBAIDAQEA
+AAAAAAERACExQVFhcYGRobECEsHhMtHxEgEAAAAAAAAAAAAAAAAAAABQ/9oA
+DAMBAAIRAxEAAADtRZYE1ASghQFgUZoCkKSwLmhcllAEqkSkqFAlhUomoAS3
+IoJqFlDNpFEAQFE1AIVYAWIVKAJRNZpYCwVmmshKACA0CBAUCBYGwf/aAAgB
+AgABBQD8B/yP/9oACAEDAAEFAPz6/or8H//aAAgBAQABBQC2+ZeHjbD+saX6
+hwXeDW1Rg4xLLTa+m7ZiIEsI1MTiHP1dYpvFADiFM1/X6nq9byuwdPPz5oFo
+fWlEMQ9ULKrWq2ppG9Y2J6INQma9lVTRdlUKgHzXXSEECw1SYu5WsGoJPkis
+ZYpx31GvXZQ/JM3VwShzVTsp1EZbBI8LcaUSih86+s2Zl4Wp6+lAZnVsDkjd
+ku5m+lJTdXDG2SHM9M2wKX1YxsaZTTwmoVrYnqsMrM652yjs01K0mtbGAz6Y
+5dpfqNz06qpq5QNjiIjiZtbhtceNuf0jyeqGgu6rXMvI4omPWbPMYzEfMI+a
+xHnFvOP4/9oACAECAgY/AGP/2gAIAQMCBj8AY//aAAgBAQEGPwB72Yucb1Bf
+IhFEaeZ+xRXFQELN+HEUQdjU0Xn4g9gRCQcpw1yajGYsP/kFvUzvjUBWrIMF
+HI2OJQNEAjiEEFdTmfG/MTHq5RFOnpTV3kzCBx7x4YOD1AV5uYJvnqMA0hep
+jfwpYCwC4Bx3q55zeZRBCw9TkoIuHw78RdczSNH2mgqcLpRC+RASAkA3B13m
+cYd5mR84c/yOx4lWtRAZ6mGDhiP9WgXVyhWA+xDgMOWGMsTg/wBTz8SjjXrP
+8hHIlX1MZ6mDzgc/cIV/iyN1GBR0MQMKjnEzvvMz8mUkErKlfqU63iV+IKNH
+7mNZBLFQEpEDeDOV32IVn8WR4caoywqI2p695mbZzNUQIcKfk0bo+0NpCqn7
+CiQiNGXkdQen1DpjGeZ7WNw3pK+I93maCPc16+Zkf6XxMCsFwAkaiIB57vc/
+IAhZ/HqZBBbB0ZokAEOGxsYqBgPp8agQBu4VSMJdqx6SwDsGBrTmAR93uZGX
+6KePowEADAIjoX8gw459CICaW/MLGvodQfkDW71zBxRHtB3j3jC4PMIYoAgK
+NfPMCQNN7jCzvlzXPopzhQvNZY3CRya9ZrEFfRE0iCB5mscZuVYfKmAi94uE
+3Q8qfytQ7xD0svmFcmaxNPI8iMjh3pmF2HbzqeUi+YkiD/MrOl5LmbwPuWVf
+mXpv3hDH8qAjPpiZHXkRnSd6ZhB53mejzKV6US0K9TCCLyCeIhtETX5MsHBG
+JkD/ANiFkMCE2qGoCdZ8Q8AMGpYFqEhdhRIYH3CF3d1M/Mexma+4CwdQ2Ddc
+x0exAlmj04QUQd8QWLB/iB5GxmEg5TENVZqPYzFV8eHAy9T/AEc8a4n3Ov6g
+/VwvE6lpQ4VNysXzhS8esOO8w/rlF/rypjV3B5H1Knr8T//Z
+
+--Boundary-00=_z3Ln/tUeUBipHgx--
+
diff --git a/rt/t/data/emails/lorem-ipsum b/rt/t/data/emails/lorem-ipsum
new file mode 100644
index 000000000..1aceb1464
--- /dev/null
+++ b/rt/t/data/emails/lorem-ipsum
@@ -0,0 +1,5 @@
+Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut
+labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris
+nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit
+esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
+culpa qui officia deserunt mollit anim id est laborum.
diff --git a/rt/t/data/emails/multipart-alternative-with-umlaut b/rt/t/data/emails/multipart-alternative-with-umlaut
new file mode 100755
index 000000000..1ad4fe323
--- /dev/null
+++ b/rt/t/data/emails/multipart-alternative-with-umlaut
@@ -0,0 +1,62 @@
+Return-Path: <gst@example.com>
+Delivered-To: j@pallas.eruditorum.org
+Received: from vis.example.com (vis.example.com [212.68.68.251])
+ by pallas.eruditorum.org (Postfix) with SMTP id 59236111C3
+ for <jesse@example.com>; Thu, 12 Jun 2003 02:14:44 -0400 (EDT)
+Received: (qmail 29541 invoked by uid 502); 12 Jun 2003 06:14:42 -0000
+Received: from sivd.example.com (HELO example.com) (192.168.42.1)
+ by 192.168.42.42 with SMTP; 12 Jun 2003 06:14:42 -0000
+Received: received from 172.20.72.174 by odie.example.com; Thu, 12 Jun 2003 08:14:27 +0200
+Received: by mailserver.example.com with Internet Mail Service (5.5.2653.19) id <LJSB7T54>; Thu, 12 Jun 2003 08:14:39 +0200
+Message-ID: <50362EC956CBD411A339009027F6257E013DD495@mailserver.example.com>
+Date: Thu, 12 Jun 2003 08:14:39 +0200
+From: "Stever, Gregor" <gst@example.com>
+MIME-Version: 1.0
+X-Mailer: Internet Mail Service (5.5.2653.19)
+To: "'jesse@example.com'" <jesse@example.com>
+Subject: RE: [rt-users] HTML-encoded mails with umlaute
+Date: Thu, 12 Jun 2003 08:14:39 +0200
+Content-Type: multipart/alternative;
+ boundary="----_=_NextPart_001_01C330A9.E7BDD590"
+X-Spam-Status: No, hits=0.0 required=5.0
+ tests=AWL,HTML_50_60,HTML_MESSAGE,INVALID_DATE
+ version=2.55
+X-Spam-Level:
+X-Spam-Checker-Version: SpamAssassin 2.55 (1.174.2.19-2003-05-19-exp)
+
+------_=_NextPart_001_01C330A9.E7BDD590
+Content-Type: text/plain;
+ charset="iso-8859-1"
+Content-Transfer-Encoding: quoted-printable
+
+Hello,
+
+ist this kind of Messages, that causes rt to crash.=20
+
+Mit freundlichen Gr=FC=DFen
+Gregor Stever ^^causes Error!!
+
+
+------_=_NextPart_001_01C330A9.E7BDD590
+Content-Type: text/html;
+ charset="iso-8859-1"
+Content-Transfer-Encoding: quoted-printable
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+<HTML><HEAD>
+<META HTTP-EQUIV=3D"Content-Type" CONTENT=3D"text/html; charset=3Diso-8859-=
+1">
+
+
+<META content=3D"MSHTML 6.00.2800.1170" name=3DGENERATOR></HEAD>
+<BODY>
+<DIV><FONT face=3DArial><FONT size=3D2>Hello,<BR><BR>ist this kind of Messa=
+ges, that=20
+causes rt to crash.<BR><BR>Mit freundlichen Gr=FC=DFen<BR>Gregor=20
+Stever&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ^^causes Error<SPAN=20
+class=3D975501206-12062003>!!</SPAN></FONT></FONT></DIV></BODY></HTML>
+
+
+------_=_NextPart_001_01C330A9.E7BDD590--
+
+
diff --git a/rt/t/data/emails/multipart-report b/rt/t/data/emails/multipart-report
new file mode 100755
index 000000000..538e0c880
--- /dev/null
+++ b/rt/t/data/emails/multipart-report
@@ -0,0 +1,66 @@
+Return-Path: <mailnull@example.com>
+Date: Sat, 23 Aug 2003 00:15:18 +0800 (SGT)
+From: Mail Delivery Subsystem <MAILER-DAEMON@other.example.com>
+Message-Id: <200308221615.CGA36111@mailbox.other.example.com>
+To: support@example.com
+MIME-Version: 1.0
+Content-Type: multipart/report; report-type=delivery-status;
+ boundary="CGA36111.1061568918/mailbox.other.example.com"
+Subject: Returned mail: User unknown
+Auto-Submitted: auto-generated (failure)
+
+This is a MIME-encapsulated message
+
+--CGA36111.1061568918/mailbox.other.example.com
+
+The original message was received at Sat, 23 Aug 2003 00:15:18 +0800 (SGT)
+from mx12.mcis.other.example.com [10.1.1.232]
+
+ ----- The following addresses had permanent delivery errors -----
+<jesmund>
+
+
+--CGA36111.1061568918/mailbox.other.example.com
+Content-Type: message/delivery-status
+
+Reporting-MTA: dns; mailbox.other.example.com
+Arrival-Date: Sat, 23 Aug 2003 00:15:18 +0800 (SGT)
+
+Final-Recipient: RFC822; jesmund@mailbox.other.example.com
+Action: failed
+Status: 5.1.1
+Remote-MTA: DNS; mail.mcis.other.example.com
+Diagnostic-Code: SMTP; 550 5.1.1 <jesmund>... User unknown
+Last-Attempt-Date: Sat, 23 Aug 2003 00:15:18 +0800 (SGT)
+
+--CGA36111.1061568918/mailbox.other.example.com
+Content-Type: message/rfc822
+
+Return-Path: <support@example.com>
+Received: from mx12.other.example.com (mx12.mcis.other.example.com [10.1.1.232])
+ by mailbox.other.example.com (Mirapoint Messaging Server MOS 3.3.3-GR)
+ with ESMTP id CGA36101;
+ Sat, 23 Aug 2003 00:15:17 +0800 (SGT)
+Received: from STATION13 (rhala.dsl.pe.net [64.38.69.104])
+ by mx12.other.example.com (8.12.9/8.12.9) with ESMTP id h7MGFGac020135
+ for <jesmund@other.example.com>; Sat, 23 Aug 2003 00:15:17 +0800
+Message-Id: <200308221615.h7MGFGac020135@mx12.other.example.com>
+From: <support@example.com>
+To: <jesmund@other.example.com>
+Subject: Thank you!
+Date: Fri, 22 Aug 2003 9:15:19 --0700
+X-MailScanner: Found to be clean
+Importance: Normal
+X-Mailer: Microsoft Outlook Express 6.00.2600.0000
+X-MSMail-Priority: Normal
+X-Priority: 3 (Normal)
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="_NextPart_000_05684DA4"
+
+
+
+--_NextPart_000_05684DA4--
+
+--CGA36111.1061568918/mailbox.other.example.com--
+
diff --git a/rt/t/data/emails/nested-mime-sample b/rt/t/data/emails/nested-mime-sample
new file mode 100755
index 000000000..8b85d948c
--- /dev/null
+++ b/rt/t/data/emails/nested-mime-sample
@@ -0,0 +1,396 @@
+Return-Path: <Xxxxxx_Yyyyyyy@some.net>
+Delivered-To: jesse@pallas.eruditorum.org
+Received: by pallas.eruditorum.org (Postfix)
+ id B5D3E1123A; Fri, 12 Jul 2002 11:35:27 -0400 (EDT)
+Delivered-To: rt-2.0-bugs@pallas.eruditorum.org
+Received: from postman.some.net (postman.some.net [193.0.0.199])
+ by pallas.eruditorum.org (Postfix) with SMTP id 2736011234
+ for <rt-2.0-bugs@fsck.com>; Fri, 12 Jul 2002 11:35:27 -0400 (EDT)
+Received: (qmail 11615 invoked by uid 0); 12 Jul 2002 15:35:26 -0000
+Received: from x22.some.net (HELO x22.some.net.some.net) (193.0.1.22)
+ by postman.some.net with SMTP; 12 Jul 2002 15:35:26 -0000
+Date: Fri, 12 Jul 2002 17:35:26 +0200 (CEST)
+From: Xxxxxx Yyyyyyy <Xxxxxx_Yyyyyyy@some.net>
+To: rt-0.0-bugs@fsck.com
+Subject: Example MIME within MIME within MIME message
+Message-ID: <Pine.LNX.4.44.0207121734250.25020-120000@x22.some.net>
+MIME-Version: 1.0
+Content-Type: MULTIPART/MIXED; BOUNDARY="12654081-192303556-1026488126=:25020"
+X-Spam-Status: No, hits=4.0 required=7.0
+ tests=DOUBLE_CAPSWORD,MIME_NULL_BLOCK,MIME_MISSING_BOUNDARY
+ version=2.31
+Content-Length: 11478
+
+ This message is in MIME format. The first part should be readable text,
+ while the remaining parts are likely unreadable without MIME-aware tools.
+ Send mail to mime@docserver.cac.washington.edu for more info.
+
+--12654081-192303556-1026488126=:25020
+Content-Type: TEXT/PLAIN; charset=US-ASCII
+
+
+MIME is fun at times.
+
+
+--
+ Xxxxxx Yyyyyyy SOME
+ Systems/Network Engineer NCC
+ www.some.net - PGP000C8B1B Operations/Security
+
+--12654081-192303556-1026488126=:25020
+Content-Type: MULTIPART/Digest; BOUNDARY="12654081-2102091261-1026488126=:25020"
+Content-ID: <Pine.LNX.4.44.0207121734322.25020@x22.some.net>
+Content-Description: Digest of 2 messages
+
+ This message is in MIME format. The first part should be readable text,
+ while the remaining parts are likely unreadable without MIME-aware tools.
+ Send mail to mime@docserver.cac.washington.edu for more info.
+
+--12654081-2102091261-1026488126=:25020
+Content-Type: MESSAGE/RFC822; CHARSET=US-ASCII
+Content-ID: <Pine.LNX.4.44.0207121734320.25020@x22.some.net>
+Content-Description: first outer message (fwd)
+
+Date: Fri, 12 Jul 2002 17:32:37 +0200 (CEST)
+From: Xxxxxx Yyyyyyy <Xxxxxx_Yyyyyyy@some.net>
+X-X-Sender: bc@x22.some.net
+To: Xxxxxx_Yyyyyyy@some.net
+Subject: first outer message
+Message-ID: <Pine.LNX.4.44.0207121732180.25020-120000@x22.some.net>
+MIME-Version: 1.0
+Content-Type: MULTIPART/MIXED; BOUNDARY="12654081-113777422-1026487957=:25020"
+
+
+--12654081-113777422-1026487957=:25020
+Content-Type: TEXT/PLAIN; charset=US-ASCII
+
+
+first outer message
+
+--12654081-113777422-1026487957=:25020
+Content-Type: MULTIPART/Digest; BOUNDARY="12654081-387266385-1026487957=:25020"
+Content-ID: <Pine.LNX.4.44.0207121732222.25020@x22.some.net>
+Content-Description: Digest of 2 messages
+
+--12654081-387266385-1026487957=:25020
+Content-Type: MESSAGE/RFC822; CHARSET=US-ASCII
+Content-ID: <Pine.LNX.4.44.0207121732220.25020@x22.some.net>
+Content-Description: middle message (fwd)
+
+Date: Fri, 12 Jul 2002 17:31:45 +0200 (CEST)
+From: Xxxxxx Yyyyyyy <Xxxxxx_Yyyyyyy@some.net>
+X-X-Sender: bc@x22.some.net
+To: Xxxxxx_Yyyyyyy@some.net
+Subject: middle message
+Message-ID: <Pine.LNX.4.44.0207121731190.25020-120000@x22.some.net>
+MIME-Version: 1.0
+Content-Type: MULTIPART/MIXED; BOUNDARY="12654081-1711788944-1026487905=:25020"
+
+
+--12654081-1711788944-1026487905=:25020
+Content-Type: TEXT/PLAIN; charset=US-ASCII
+
+
+This is the first middle message
+
+
+--12654081-1711788944-1026487905=:25020
+Content-Type: MULTIPART/Digest; BOUNDARY="12654081-1221085552-1026487905=:25020"
+Content-ID: <Pine.LNX.4.44.0207121731262.25020@x22.some.net>
+Content-Description: Digest of 2 messages
+
+--12654081-1221085552-1026487905=:25020
+Content-Type: MESSAGE/RFC822; CHARSET=US-ASCII
+Content-ID: <Pine.LNX.4.44.0207121731260.25020@x22.some.net>
+Content-Description: This is the inner-most message (fwd)
+
+Date: Fri, 12 Jul 2002 17:30:31 +0200 (CEST)
+From: Xxxxxx Yyyyyyy <Xxxxxx_Yyyyyyy@some.net>
+X-X-Sender: bc@x22.some.net
+To: Xxxxxx_Yyyyyyy@some.net
+Subject: This is the inner-most message
+Message-ID: <Pine.LNX.4.44.0207121730070.25020-100000@x22.some.net>
+MIME-Version: 1.0
+Content-Type: TEXT/PLAIN; charset=US-ASCII
+
+
+
+inner-msg
+
+
+
+--12654081-1221085552-1026487905=:25020
+Content-Type: MESSAGE/RFC822; CHARSET=US-ASCII
+Content-ID: <Pine.LNX.4.44.0207121731261.25020@x22.some.net>
+Content-Description: another inner message (need two before pine will do the mime-digest thing) (fwd)
+
+Date: Fri, 12 Jul 2002 17:31:12 +0200 (CEST)
+From: Xxxxxx Yyyyyyy <Xxxxxx_Yyyyyyy@some.net>
+X-X-Sender: bc@x22.some.net
+To: Xxxxxx_Yyyyyyy@some.net
+Subject: another inner message (need two before pine will do the mime-digest
+ thing)
+Message-ID: <Pine.LNX.4.44.0207121730480.25020-100000@x22.some.net>
+MIME-Version: 1.0
+Content-Type: TEXT/PLAIN; charset=US-ASCII
+
+
+
+again
+
+
+
+--12654081-1221085552-1026487905=:25020--
+--12654081-1711788944-1026487905=:25020--
+
+--12654081-387266385-1026487957=:25020
+Content-Type: MESSAGE/RFC822; CHARSET=US-ASCII
+Content-ID: <Pine.LNX.4.44.0207121732221.25020@x22.some.net>
+Content-Description: middle message (fwd)
+
+Date: Fri, 12 Jul 2002 17:32:05 +0200 (CEST)
+From: Xxxxxx Yyyyyyy <Xxxxxx_Yyyyyyy@some.net>
+X-X-Sender: bc@x22.some.net
+To: Xxxxxx_Yyyyyyy@some.net
+Subject: middle message
+Message-ID: <Pine.LNX.4.44.0207121731470.25020-120000@x22.some.net>
+MIME-Version: 1.0
+Content-Type: MULTIPART/MIXED; BOUNDARY="12654081-1731270459-1026487925=:25020"
+
+
+--12654081-1731270459-1026487925=:25020
+Content-Type: TEXT/PLAIN; charset=US-ASCII
+
+
+
+This is the second middle message
+
+
+--12654081-1731270459-1026487925=:25020
+Content-Type: MULTIPART/Digest; BOUNDARY="12654081-128832654-1026487925=:25020"
+Content-ID: <Pine.LNX.4.44.0207121731502.25020@x22.some.net>
+Content-Description: Digest of 2 messages
+
+--12654081-128832654-1026487925=:25020
+Content-Type: MESSAGE/RFC822; CHARSET=US-ASCII
+Content-ID: <Pine.LNX.4.44.0207121731500.25020@x22.some.net>
+Content-Description: This is the inner-most message (fwd)
+
+Date: Fri, 12 Jul 2002 17:30:31 +0200 (CEST)
+From: Xxxxxx Yyyyyyy <Xxxxxx_Yyyyyyy@some.net>
+X-X-Sender: bc@x22.some.net
+To: Xxxxxx_Yyyyyyy@some.net
+Subject: This is the inner-most message
+Message-ID: <Pine.LNX.4.44.0207121730070.25020-100000@x22.some.net>
+MIME-Version: 1.0
+Content-Type: TEXT/PLAIN; charset=US-ASCII
+
+
+
+inner-msg
+
+
+
+--12654081-128832654-1026487925=:25020
+Content-Type: MESSAGE/RFC822; CHARSET=US-ASCII
+Content-ID: <Pine.LNX.4.44.0207121731501.25020@x22.some.net>
+Content-Description: another inner message (need two before pine will do the mime-digest thing) (fwd)
+
+Date: Fri, 12 Jul 2002 17:31:12 +0200 (CEST)
+From: Xxxxxx Yyyyyyy <Xxxxxx_Yyyyyyy@some.net>
+X-X-Sender: bc@x22.some.net
+To: Xxxxxx_Yyyyyyy@some.net
+Subject: another inner message (need two before pine will do the mime-digest
+ thing)
+Message-ID: <Pine.LNX.4.44.0207121730480.25020-100000@x22.some.net>
+MIME-Version: 1.0
+Content-Type: TEXT/PLAIN; charset=US-ASCII
+
+
+
+again
+
+
+
+--12654081-128832654-1026487925=:25020--
+--12654081-1731270459-1026487925=:25020--
+
+--12654081-387266385-1026487957=:25020--
+--12654081-113777422-1026487957=:25020--
+
+--12654081-2102091261-1026488126=:25020
+Content-Type: MESSAGE/RFC822; CHARSET=US-ASCII
+Content-ID: <Pine.LNX.4.44.0207121734321.25020@x22.some.net>
+Content-Description: 2nd outer message (fwd)
+
+Date: Fri, 12 Jul 2002 17:32:54 +0200 (CEST)
+From: Xxxxxx Yyyyyyy <Xxxxxx_Yyyyyyy@some.net>
+X-X-Sender: bc@x22.some.net
+To: Xxxxxx_Yyyyyyy@some.net
+Subject: 2nd outer message
+Message-ID: <Pine.LNX.4.44.0207121732380.25020-120000@x22.some.net>
+MIME-Version: 1.0
+Content-Type: MULTIPART/MIXED; BOUNDARY="12654081-1955637437-1026487974=:25020"
+
+
+--12654081-1955637437-1026487974=:25020
+Content-Type: TEXT/PLAIN; charset=US-ASCII
+
+
+2nd outer message
+
+
+--12654081-1955637437-1026487974=:25020
+Content-Type: MULTIPART/Digest; BOUNDARY="12654081-362457126-1026487974=:25020"
+Content-ID: <Pine.LNX.4.44.0207121732412.25020@x22.some.net>
+Content-Description: Digest of 2 messages
+
+--12654081-362457126-1026487974=:25020
+Content-Type: MESSAGE/RFC822; CHARSET=US-ASCII
+Content-ID: <Pine.LNX.4.44.0207121732410.25020@x22.some.net>
+Content-Description: middle message (fwd)
+
+Date: Fri, 12 Jul 2002 17:31:45 +0200 (CEST)
+From: Xxxxxx Yyyyyyy <Xxxxxx_Yyyyyyy@some.net>
+X-X-Sender: bc@x22.some.net
+To: Xxxxxx_Yyyyyyy@some.net
+Subject: middle message
+Message-ID: <Pine.LNX.4.44.0207121731190.25020-120000@x22.some.net>
+MIME-Version: 1.0
+Content-Type: MULTIPART/MIXED; BOUNDARY="12654081-1711788944-1026487905=:25020"
+
+
+--12654081-1711788944-1026487905=:25020
+Content-Type: TEXT/PLAIN; charset=US-ASCII
+
+
+This is the first middle message
+
+
+--12654081-1711788944-1026487905=:25020
+Content-Type: MULTIPART/Digest; BOUNDARY="12654081-1221085552-1026487905=:25020"
+Content-ID: <Pine.LNX.4.44.0207121731262.25020@x22.some.net>
+Content-Description: Digest of 2 messages
+
+--12654081-1221085552-1026487905=:25020
+Content-Type: MESSAGE/RFC822; CHARSET=US-ASCII
+Content-ID: <Pine.LNX.4.44.0207121731260.25020@x22.some.net>
+Content-Description: This is the inner-most message (fwd)
+
+Date: Fri, 12 Jul 2002 17:30:31 +0200 (CEST)
+From: Xxxxxx Yyyyyyy <Xxxxxx_Yyyyyyy@some.net>
+X-X-Sender: bc@x22.some.net
+To: Xxxxxx_Yyyyyyy@some.net
+Subject: This is the inner-most message
+Message-ID: <Pine.LNX.4.44.0207121730070.25020-100000@x22.some.net>
+MIME-Version: 1.0
+Content-Type: TEXT/PLAIN; charset=US-ASCII
+
+
+
+inner-msg
+
+
+
+--12654081-1221085552-1026487905=:25020
+Content-Type: MESSAGE/RFC822; CHARSET=US-ASCII
+Content-ID: <Pine.LNX.4.44.0207121731261.25020@x22.some.net>
+Content-Description: another inner message (need two before pine will do the mime-digest thing) (fwd)
+
+Date: Fri, 12 Jul 2002 17:31:12 +0200 (CEST)
+From: Xxxxxx Yyyyyyy <Xxxxxx_Yyyyyyy@some.net>
+X-X-Sender: bc@x22.some.net
+To: Xxxxxx_Yyyyyyy@some.net
+Subject: another inner message (need two before pine will do the mime-digest
+ thing)
+Message-ID: <Pine.LNX.4.44.0207121730480.25020-100000@x22.some.net>
+MIME-Version: 1.0
+Content-Type: TEXT/PLAIN; charset=US-ASCII
+
+
+
+again
+
+
+
+--12654081-1221085552-1026487905=:25020--
+--12654081-1711788944-1026487905=:25020--
+
+--12654081-362457126-1026487974=:25020
+Content-Type: MESSAGE/RFC822; CHARSET=US-ASCII
+Content-ID: <Pine.LNX.4.44.0207121732411.25020@x22.some.net>
+Content-Description: middle message (fwd)
+
+Date: Fri, 12 Jul 2002 17:32:05 +0200 (CEST)
+From: Xxxxxx Yyyyyyy <Xxxxxx_Yyyyyyy@some.net>
+X-X-Sender: bc@x22.some.net
+To: Xxxxxx_Yyyyyyy@some.net
+Subject: middle message
+Message-ID: <Pine.LNX.4.44.0207121731470.25020-120000@x22.some.net>
+MIME-Version: 1.0
+Content-Type: MULTIPART/MIXED; BOUNDARY="12654081-1731270459-1026487925=:25020"
+
+
+--12654081-1731270459-1026487925=:25020
+Content-Type: TEXT/PLAIN; charset=US-ASCII
+
+
+
+This is the second middle message
+
+
+--12654081-1731270459-1026487925=:25020
+Content-Type: MULTIPART/Digest; BOUNDARY="12654081-128832654-1026487925=:25020"
+Content-ID: <Pine.LNX.4.44.0207121731502.25020@x22.some.net>
+Content-Description: Digest of 2 messages
+
+--12654081-128832654-1026487925=:25020
+Content-Type: MESSAGE/RFC822; CHARSET=US-ASCII
+Content-ID: <Pine.LNX.4.44.0207121731500.25020@x22.some.net>
+Content-Description: This is the inner-most message (fwd)
+
+Date: Fri, 12 Jul 2002 17:30:31 +0200 (CEST)
+From: Xxxxxx Yyyyyyy <Xxxxxx_Yyyyyyy@some.net>
+X-X-Sender: bc@x22.some.net
+To: Xxxxxx_Yyyyyyy@some.net
+Subject: This is the inner-most message
+Message-ID: <Pine.LNX.4.44.0207121730070.25020-100000@x22.some.net>
+MIME-Version: 1.0
+Content-Type: TEXT/PLAIN; charset=US-ASCII
+
+
+
+inner-msg
+
+
+
+--12654081-128832654-1026487925=:25020
+Content-Type: MESSAGE/RFC822; CHARSET=US-ASCII
+Content-ID: <Pine.LNX.4.44.0207121731501.25020@x22.some.net>
+Content-Description: another inner message (need two before pine will do the mime-digest thing) (fwd)
+
+Date: Fri, 12 Jul 2002 17:31:12 +0200 (CEST)
+From: Xxxxxx Yyyyyyy <Xxxxxx_Yyyyyyy@some.net>
+X-X-Sender: bc@x22.some.net
+To: Xxxxxx_Yyyyyyy@some.net
+Subject: another inner message (need two before pine will do the mime-digest
+ thing)
+Message-ID: <Pine.LNX.4.44.0207121730480.25020-100000@x22.some.net>
+MIME-Version: 1.0
+Content-Type: TEXT/PLAIN; charset=US-ASCII
+
+
+
+again
+
+
+
+--12654081-128832654-1026487925=:25020--
+--12654081-1731270459-1026487925=:25020--
+
+--12654081-362457126-1026487974=:25020--
+--12654081-1955637437-1026487974=:25020--
+
+--12654081-2102091261-1026488126=:25020--
+--12654081-192303556-1026488126=:25020--
+
diff --git a/rt/t/data/emails/nested-rfc-822 b/rt/t/data/emails/nested-rfc-822
new file mode 100755
index 000000000..d4f118df2
--- /dev/null
+++ b/rt/t/data/emails/nested-rfc-822
@@ -0,0 +1,253 @@
+Return-Path: <jonas@astral.example.com>
+Delivered-To: j@pallas.eruditorum.org
+Received: from example.com (example.com [213.88.137.35])
+ by pallas.eruditorum.org (Postfix) with ESMTP id 869591115E
+ for <jesse@bestpractical.com>; Sun, 29 Jun 2003 18:04:04 -0400 (EDT)
+Received: from jonas by example.com with local (Exim 4.20)
+ id 19WkLK-0004Vr-0I
+ for jesse@bestpractical.com; Mon, 30 Jun 2003 00:08:18 +0200
+Resent-To: jesse@bestpractical.com
+Resent-From: Jonas Liljegren <jonas@example.com>
+Resent-Date: Mon, 30 Jun 2003 00:08:17 +0200
+Received: from mail by example.com with spam-scanned (Exim 4.20)
+ id 19Wayz-00068j-KO
+ for jonas@astral.example.com; Sun, 29 Jun 2003 14:08:42 +0200
+Received: from jonas by example.com with local (Exim 4.20)
+ id 19Wayz-00068g-FY
+ for red@example.com; Sun, 29 Jun 2003 14:08:37 +0200
+To: Redaktionen <red@example.com>
+Subject: [Jonas Liljegren] Re: [Para] =?iso-8859-1?q?Niv=E5er=3F?=
+From: Jonas Liljegren <jonas@example.com>
+Date: Sun, 29 Jun 2003 14:08:37 +0200
+Message-ID: <87d6gxt7ay.fsf@example.com>
+User-Agent: Gnus/5.1002 (Gnus v5.10.2) Emacs/21.2 (gnu/linux)
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="=-=-="
+Sender: Jonas Liljegren <jonas@astral.example.com>
+Resent-Message-Id: <E19WkLK-0004Vr-0I@example.com>
+Resent-Sender: Jonas Liljegren <jonas@astral.example.com>
+Resent-Date: Mon, 30 Jun 2003 00:08:18 +0200
+X-Spam-Status: No, hits=-5.7 required=5.0
+ tests=AWL,BAYES_10,EMAIL_ATTRIBUTION,MAILTO_WITH_SUBJ,
+ QUOTED_EMAIL_TEXT,USER_AGENT_GNUS_UA
+ version=2.55
+X-Spam-Level:
+X-Spam-Checker-Version: SpamAssassin 2.55 (1.174.2.19-2003-05-19-exp)
+
+--=-=-=
+Content-Type: text/plain; charset=iso-8859-1
+Content-Transfer-Encoding: quoted-printable
+
+Material f=F6r att uppdatera texten om niv=E5er.
+
+Denna text b=F6r dessutom ligga som ett uppslagsord och inte d=E4r den ligg=
+er nu.
+
+
+--=-=-=
+Content-Type: message/rfc822
+Content-Disposition: inline
+
+Return-path: <list-bounces@example.com>
+Received: from mail by example.com with spam-scanned (Exim 4.20)
+ id 19WFzq-0005i1-WE
+ for jonas@example.com; Sat, 28 Jun 2003 15:44:13 +0200
+Received: from localhost
+ ([127.0.0.1] helo=example.com ident=list)
+ by example.com with esmtp (Exim 4.20)
+ id 19WFzp-0005hf-Tz; Sat, 28 Jun 2003 15:44:05 +0200
+Received: from mail by example.com with spam-scanned (Exim 4.20)
+ id 19WFzh-0005hR-Bu
+ for list@example.com; Sat, 28 Jun 2003 15:44:03 +0200
+Received: from jonas by example.com with local (Exim 4.20)
+ id 19WFzh-0005hO-AO
+ for list@example.com; Sat, 28 Jun 2003 15:43:57 +0200
+To: list@example.com
+Subject: Re: [Para] =?iso-8859-1?q?Niv=E5er=3F?=
+References: <002701c33d62$170fb2e0$a33740d5@TELIA.COM>
+ <002301c33d66$bf6483e0$d97864d5@remotel2tu76c8>
+ <64753.217.210.4.156.1056801224.squirrel@example.com>
+From: Jonas Liljegren <jonas@example.com>
+Date: Sat, 28 Jun 2003 15:43:57 +0200
+In-Reply-To: <64753.217.210.4.156.1056801224.squirrel@example.com> (Jakob
+ Carlsson's message of "Sat, 28 Jun 2003 13:53:44 +0200 (CEST)")
+Message-ID: <877k76uxk2.fsf@example.com>
+User-Agent: Gnus/5.1002 (Gnus v5.10.2) Emacs/21.2 (gnu/linux)
+X-BeenThere: list@example.com
+X-Mailman-Version: 2.1.2
+Precedence: list
+List-Id: &#214;ppen lista f&#246;r alla medlemmar <list.example.com>
+List-Unsubscribe: <http://example.com/cgi-bin/mailman/listinfo/list>,
+ <mailto:list-request@example.com?subject=unsubscribe>
+List-Archive: <http://example.com/pipermail/list>
+List-Post: <mailto:list@example.com>
+List-Help: <mailto:list-request@example.com?subject=help>
+List-Subscribe: <http://example.com/cgi-bin/mailman/listinfo/list>,
+ <mailto:list-request@example.com?subject=subscribe>
+Sender: list-bounces@example.com
+Errors-To: list-bounces@example.com
+X-Spam-Status: No, hits=-7.0 required=5.0
+ tests=BAYES_00,EMAIL_ATTRIBUTION,IN_REP_TO,QUOTED_EMAIL_TEXT,
+ REFERENCES,REPLY_WITH_QUOTES,USER_AGENT_GNUS_UA
+ version=2.55
+X-Spam-Level:
+X-Spam-Checker-Version: SpamAssassin 2.55 (1.174.2.19-2003-05-19-exp)
+MIME-Version: 1.0
+Content-Type: text/plain; charset=iso-8859-1
+Content-Transfer-Encoding: quoted-printable
+
+"Jakob Carlsson" <esrange@example.com> writes:
+
+>> Om du g=E5r in p=E5 Hemsidan och sen p=E5 Torget.
+>> D=E4r ser du att det st=E5r ditt anv=E4ndarnamn och
+>> bredvid det Niv=E5 5.
+>> Klicka p=E5 niv=E5 5 s=E5 kommer du in p=E5 en sida som
+>> f=F6rklarar allt om niv=E5systemet.
+>
+> Bra svar. Men jag k=E4nner f=F6r att ge en kort f=F6rklaring av niv=E5-sy=
+stemet.
+
+Jag skulle kunna l=E4gga en massa tid p=E5 at skriva samma sak om och om
+igen. Fliker in h=E4r f=F6r att s=E4ga detta =E4nnu en g=E5ng...:
+
+ * Det =E4r jag som hittat p=E5 det h=E4r med niv=E5system
+
+ * Det =E4r en stor skillnad p=E5 hur det =E4r t=E4nkt att vara och hur det=
+ =E4r
+ nu. Jag har stora planer och en massa id=E9er jag vill genomf=F6ra.
+
+ * Niv=E5systemet =E4r en =E5terkoppling f=F6r vad man gjort f=F6r webbplat=
+sen.
+ Som ett tack g=F6r hj=E4lpen.
+
+ * Systemet finns som en inspiration f=F6r de som d=E5 k=E4nner f=F6r att g=
+=F6ra
+ mer. Men jag vill inte att det ska ge en negativ influens f=F6r de
+ som inte gillar niv=E5er. Var och en ska kunna v=E4lja att ignorera
+ niv=E5n. Speciellt b=F6r de f=F6rst=E5 att det inte har att g=F6ra med
+ graden av andlig utveckling, esoteriska kunskaper eller n=E5got
+ s=E5dant.
+
+ * Inspirationen till niv=E5erna kommer fr=E5n spel, hemliga ordenssystem,
+ kosmska hiearkier, skr=E5v=E4sen, akademier, politisk administration,
+ osv. Det =E4r ett element av rollspel. En lek.
+
+ * Olika niv=E5er motsvarar olika roller p=E5 webbplatsen. Webbplatsen
+ webbmaster och ansvbariga har en viss niv=E5, bes=F6kare och g=E4ster har
+ en annan niv=E5.
+
+ * Alla datorsystem har administrat=F6rssystem f=F6r dem som sk=F6ter
+ systemet. Jag har valt att arrangera dessa funktioner i en skala.
+ Niv=E5n anger hur mycket av systemet du har r=E4tt att administrera.
+
+ * Att ha ett niv=E5system f=F6r access g=F6r att man kan g=F6ra som p=E5 f=
+ilm;
+ att l=E5ta de med h=F6gre access komma =E5t mer information. De med
+ riktigt h=F6g niv=E5 kan n=E5 topphemlig information. P=E5 denna webbpl=
+ats
+ kan varje anv=E4ndae v=E4lja att h=E5lla vissa personliga uppgifter. Har
+ du h=F6g niv=E5 har du rollen som anv=E4ndaradministrat=F6r och har
+ tillg=E5ng till dessa uppgifter. Just nu =E4r vi tre personer med
+ denna niv=E5n.
+
+ * Niv=E5systemet =E4r ett m=E5tt p=E5 p=E5litlighet. Vi ger dig h=F6gre n=
+iv=E5 n=E4r
+ vi litar p=E5 att du inte kommer att f=F6rst=F6ra f=F6r oss. F=F6r ju h=
+=F6gre
+ niv=E5, desto l=E4ttare kan du sabbotera inneh=E5llet.
+
+ * P=E5 en h=F6gre niv=E5 beh=F6vs det inte bara att vi litar p=E5 att du v=
+ill
+ v=E4l. Du m=E5ste =E4ven ha ett gott omd=F6me, teknisk f=F6rst=E5else,
+ intresse och logiskt t=E4nkande. Utan detta =E4r det l=E4tt h=E4nt att =
+du
+ f=F6rst=F6r saker av misstag.
+
+ * Vi vill uppmuntra medlemmarna att g=F6ra det som =E4r bra f=F6r
+ webbplatsen. Tilldelandet av h=F6gre niv=E5 ska uppmuntra till att
+ g=F6ra det som =E4r bra.
+
+ * F=F6r att minska missbruk av e-postadresser visar vi e-postadresser
+ bara f=F6r de med lite h=F6gre niv=E5. P=E5 s=E5 vis vill vi undvika att
+ n=E5gon g=E5r med som medlem bara f=F6r att samla e-postadresser f=F6r a=
+tt
+ sedan g=F6ra reklamutskick.
+
+ * Idag n=E5r du olika niv=E5er p=E5 detta vis:
+
+ 0. Kom in p=E5 webbplatsen som g=E4st
+
+ 1. V=E4lj anv=E4ndarnamn och ange e-postadress
+
+ 2. Logga in med det l=F6senord som skickats till dig
+
+ 3. Skrivit en presentation
+
+ 5. Presentationen har godk=E4nts
+
+ 6. Du har svarat p=E5 ett f=E5tal fr=E5gor om dina intressen
+
+ 7. Du har svarat p=E5 en massa fr=E5gor om intressen och beskrivit dem
+ detaljerat
+
+ 10. N=E5gon v=E4ktare tycker du f=F6rtj=E4nar h=F6gre niv=E5 f=F6r att du=
+ =E4r s=E5
+ trevlig och engagerad i webbplatsen.
+
+ 11. Du har gjort ett antal kopplingar mellan =E4mnen och =F6verv=E4gande
+ delan av dem har godk=E4nts av en v=E4ktare, och du accepterar att
+ b=F6rja som l=E4rling i v=E4ktarakademin (jobbet som
+ systemadministrat=F6r)
+
+ 12-39. D=E5 och d=E5 tittar jag p=E5 vad du gjort i form av skrivande av
+ texter och arbetande med uppslagsverkets =E4mnen, och justerar din
+ niv=E5 i f=F6rh=E5llande till m=E4ngd och kvalit=E9 p=E5 arbetet
+
+ 40. Du har full=E4ndat ett helt =E4mnesomr=E5de. En m=E4ngd sammanl=E4nk=
+ade
+ =E4mnen med bra textinneh=E5ll.
+
+ 41. F=F6rtroende att arbeta med adminstration av medlemsregistret.
+
+ 42. Delaktig i utvecklandet av webbplatsens prgrammering.
+
+
+ * Allts=E5. Automatik tar dig till niv=E5 7.
+
+ * Men som sagt. Jag har en massa andra planer d=E4r mycket mer kopplas
+ till niv=E5er och d=E4r det finns systemautomatik f=F6r hela v=E4gen till
+ niv=E5 40.
+
+ * 41 och 42 ligger utanf=F6r niv=E5systemet i =F6vrigt. Den som har de
+ niv=E5erna har inte n=F6dv=E4ndigtvis tagit sig till niv=E5 40 innan. De
+ motsvaras av anv=E4ndaradministrat=F6r och systemadministrat=F6r och
+ niv=E5n speglar maktbefogenheterna snarare =E4n vad man i =F6vrigt gjort
+ f=F6r webbplatsen.
+
+ * Alla texter. Allt inneh=E5ll =E4r =F6ppet f=F6r alla. =C4ven f=F6r bes=
+=F6kare som
+ inte loggar in. Du kan till och med g=E5 in p=E5
+ administrationssidorna utan att logga in. Vi g=F6mmer inte inneh=E5ll.
+ Det vi g=F6r =E4r att hindra dig fr=E5n att =E4ndra inneh=E5llet. Vi d=
+=F6ljer
+ dock en del information om andra medlemmar i syfte att f=E5 s=E5 m=E5nga
+ som m=F6jligt att sj=E4lv skriva in sig och skriva en presentation.
+
+--=20
+/ Jonas - http://jonas.example.com/myself/en/index.html
+
+_______________________________________________
+List mailing list
+List@example.com
+http://example.com/cgi-bin/mailman/listinfo/list
+
+
+--=-=-=
+
+
+
+--
+/ Jonas - http://jonas.example.com/myself/en/index.html
+
+--=-=-=--
+
diff --git a/rt/t/data/emails/new-ticket-from-iso-8859-1 b/rt/t/data/emails/new-ticket-from-iso-8859-1
new file mode 100755
index 000000000..299392d26
--- /dev/null
+++ b/rt/t/data/emails/new-ticket-from-iso-8859-1
@@ -0,0 +1,31 @@
+Return-Path: <hw@nordkapp.net>
+Delivered-To: j@pallas.eruditorum.org
+Received: from sm1.nordkapp.net (sm1.nordkapp.net [62.70.54.150])
+ by pallas.eruditorum.org (Postfix) with ESMTP id 48F4E11112
+ for <jesse@bestpractical.com>; Mon, 2 Jun 2003 14:58:37 -0400 (EDT)
+Received: (qmail 3612 invoked by uid 1009); 2 Jun 2003 18:58:36 -0000
+Received: from unknown (HELO office.nordkapp.net) (213.161.186.83)
+ by 0 with SMTP; 2 Jun 2003 18:58:36 -0000
+Message-Id: <5.2.1.1.0.20030602205708.0314c5f8@mail.nordkapp.net>
+X-Sender: hw@nordkapp.net@mail.nordkapp.net
+X-Mailer: QUALCOMM Windows Eudora Version 5.2.1
+Date: Mon, 02 Jun 2003 20:58:30 +0200
+To: Jesse Vincent <jesse@bestpractical.com>
+From: Wilhelmsen Håvard <hw@nordkapp.net>
+Subject: Re: rt-3.0.3pre1
+In-Reply-To: <20030602185607.GN10811@fsck.com>
+References: <5.2.1.1.0.20030602204834.031406d8@mail.nordkapp.net>
+ <5.2.1.1.0.20030530194214.0371c988@mail.nordkapp.net>
+ <5.2.1.1.0.20030530194214.0371c988@mail.nordkapp.net>
+ <5.2.1.1.0.20030602204834.031406d8@mail.nordkapp.net>
+Mime-Version: 1.0
+Content-Type: text/plain; charset="iso-8859-1"; format=flowed
+Content-Transfer-Encoding: 8bit
+X-Spam-Status: No, hits=-1.9 required=5.0
+ tests=AWL,EMAIL_ATTRIBUTION,IN_REP_TO,QUOTED_EMAIL_TEXT,
+ REFERENCES,REPLY_WITH_QUOTES
+ autolearn=ham version=2.55
+X-Spam-Level:
+X-Spam-Checker-Version: SpamAssassin 2.55 (1.174.2.19-2003-05-19-exp)
+
+Håvard
diff --git a/rt/t/data/emails/new-ticket-from-iso-8859-1-full b/rt/t/data/emails/new-ticket-from-iso-8859-1-full
new file mode 100755
index 000000000..493ca1591
--- /dev/null
+++ b/rt/t/data/emails/new-ticket-from-iso-8859-1-full
@@ -0,0 +1,38 @@
+X-Mailer: QUALCOMM Windows Eudora Version 5.2.1
+To: Jesse Vincent <jesse@bestpractical.com>
+From: Wilhelmsen Håvard <hw@nordkapp.net>
+Subject: Re: rt-3.0.3pre1
+X-Spam-Status: No, hits=-1.9 required=5.0
+ tests=AWL,EMAIL_ATTRIBUTION,IN_REP_TO,QUOTED_EMAIL_TEXT,
+ REFERENCES,REPLY_WITH_QUOTES
+ autolearn=ham version=2.55
+X-Spam-Level:
+X-Spam-Checker-Version: SpamAssassin 2.55 (1.174.2.19-2003-05-19-exp)
+
+At 14:56 02.06.2003 -0400, you wrote:
+>> This patch didn't help us out.
+>> We still got problems with auto responding e-mails sent from the system
+>> when a new ticket is created.
+>> The same problem appears when one of the staff replays to an new ticket.
+>> All Norwegian letters is converted to strange letters like ø
+>>
+>> We would love if this bug could be fixed. On our mail server we are
+>running
+>> perl 5.6.1 since we are using debian stabel packet lists.
+>
+>I'd love it too. I just can't find it. Can you send me
+>(jesse@bestpractical.com) a couple of email messages containing
+>characters that break your RT?
+
+Hello again,
+
+Thanks for your fast replay!
+
+I don't know how this looks at your end but it is letters like: ø æ å
+If your want to make this in html it will be &oslash; &aring; and &aerlig;
+
+
+--
+HÃ¥vard
+
+
diff --git a/rt/t/data/emails/notes-uuencoded b/rt/t/data/emails/notes-uuencoded
new file mode 100755
index 000000000..f27fdf8c0
--- /dev/null
+++ b/rt/t/data/emails/notes-uuencoded
@@ -0,0 +1,2368 @@
+Return-Path: <mhenrion@example.com>
+Delivered-To: j@pallas.eruditorum.org
+Received: from serveurlotus.example.com (unknown [213.56.193.67])
+ by pallas.eruditorum.org (Postfix) with SMTP id C21DB113AA
+ for <jesse@vendor.example.com>; Thu, 27 Nov 2003 10:55:58 -0500 (EST)
+Received: by serveurlotus.example.com(Lotus SMTP MTA v4.6.1 (569.2 2-6-1998)) id C1256DEB.00578401 ; Thu, 27 Nov 2003 16:55:54 +0100
+X-Lotus-FromDomain: DOMAINEQZ
+From: "Maxime HENRION" <mhenrion@example.com>
+To: jesse@vendor.example.com
+Cc: support@example.com
+Message-ID: <C1256DEB.005717B5.00@serveurlotus.example.com>
+Date: Thu, 27 Nov 2003 16:55:50 +0100
+Subject: Test e-mail which exhibits problems with RT
+X-Spam-Status: No, hits=-2.6 required=7.0
+ tests=BAYES_20
+ version=2.55
+X-Spam-Level:
+X-Spam-Checker-Version: SpamAssassin 2.55 (1.174.2.19-2003-05-19-exp)
+Content-Length: 144905
+
+I send you this mail from Lotus Notes to make sure it'll exhibit the
+reported symptoms (lost attachment and body). I Cc: it to our RT address
+to verify it does cause the reported problems. Could you please mail me
+any replies to my personal e-mail, mux@example.org ?
+
+Thanks in advance,
+Maxime
+
+(See attached file: Naz_Head.jpg)
+
+(UUEncoded file named: Naz_Head.jpg follows)
+(Its format is: JPEG File Interchange )
+
+begin 644 Naz_Head.jpg
+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
+I("O4!#X(DJA<_P#YE_\`B6G2_P">U9ZNFXE:#I_,$^EW^,*4;)(T_]EK
+`
+end
+
+
diff --git a/rt/t/data/emails/rt-send-cc b/rt/t/data/emails/rt-send-cc
new file mode 100644
index 000000000..da8c4daff
--- /dev/null
+++ b/rt/t/data/emails/rt-send-cc
@@ -0,0 +1,5 @@
+From: rt@example.com
+subject: testing send-cc headers
+RT-Send-Cc: this-is-a-sample-test1e@example.com, second-this-is-a-sample-test2@example.com, test-sample-sample-sample-test3@example.com,
+ afourthtest4@example.com,
+ test5@example.com
diff --git a/rt/t/data/emails/russian-subject-no-content-type b/rt/t/data/emails/russian-subject-no-content-type
new file mode 100755
index 000000000..03d95b8c4
--- /dev/null
+++ b/rt/t/data/emails/russian-subject-no-content-type
@@ -0,0 +1,42 @@
+Return-Path: <mitya@fling-wing.example.com>
+X-Real-To: <mitya@second.example.com>
+Received: from [194.87.5.31] (HELO sinbin.example.com)
+ by cgp.second.example.com (CommuniGate Pro SMTP 4.0.5/D)
+ with ESMTP-TLS id 69661026 for mitya@second.example.com; Wed, 18 Jun 2003 11:14:49 +0400
+Received: (from daemon@localhost)
+ by sinbin.example.com (8.12.8/8.11.6) id h5I7EfOj096595
+ for mitya@second.example.com; Wed, 18 Jun 2003 11:14:41 +0400 (MSD)
+ (envelope-from mitya@fling-wing.example.com)
+Received: from example.com by sinbin.example.com with ESMTP id h5I7Ee8K096580;
+ (8.12.9/D) Wed, 18 Jun 2003 11:14:40 +0400 (MSD)
+X-Real-To: <mitya@second.example.com>
+Received: from [194.87.0.31] (HELO mail.example.com)
+ by example.com (CommuniGate Pro SMTP 4.1b7/D)
+ with ESMTP id 76217696 for mitya@example.com; Wed, 18 Jun 2003 11:14:40 +0400
+Received: by mail.example.com (CommuniGate Pro PIPE 4.1b7/D)
+ with PIPE id 63920083; Wed, 18 Jun 2003 11:14:40 +0400
+Received: from [194.87.5.69] (HELO fling-wing.example.com)
+ by mail.example.com (CommuniGate Pro SMTP 4.1b7/D)
+ with ESMTP-TLS id 63920055 for mitya@example.com; Wed, 18 Jun 2003 11:14:38 +0400
+Received: from fling-wing.example.com (localhost [127.0.0.1])
+ by fling-wing.example.com (8.12.9/8.12.6) with ESMTP id h5I7Ec5R000153
+ for <mitya@example.com>; Wed, 18 Jun 2003 11:14:38 +0400 (MSD)
+ (envelope-from mitya@fling-wing.example.com)
+Received: (from mitya@localhost)
+ by fling-wing.example.com (8.12.9/8.12.6/Submit) id h5I7Ec0J000152
+ for mitya@example.com; Wed, 18 Jun 2003 11:14:38 +0400 (MSD)
+Date: Wed, 18 Jun 2003 11:14:38 +0400 (MSD)
+From: "Dmitry S. Sivachenko" <mitya@fling-wing.example.com>
+Message-Id: <200306180714.h5I7Ec0J000152@fling-wing.example.com>
+To: mitya@example.com
+Subject: ÔÅÓÔ ÔÅÓÔ
+X-Spam-Checker-Version: SpamAssassin 2.60-cvs-mail.demos (1.193-2003-06-13-exp)
+X-Spam-Level: +
+X-Spam-Status: No, hits=1.0 required=5.0 tests=SUBJ_ILLEGAL_CHARS autolearn=no
+ version=2.60-cvs-mail.demos
+X-Spam-Report: * SUBJ_ILLEGAL_CHARS 1.0 (Subject contains too many raw illegal characters)
+
+Content-Length: 6
+
+ôåóô
+
diff --git a/rt/t/data/emails/subject-with-folding-ws b/rt/t/data/emails/subject-with-folding-ws
new file mode 100644
index 000000000..c0826325e
--- /dev/null
+++ b/rt/t/data/emails/subject-with-folding-ws
@@ -0,0 +1,10 @@
+Subject: =?ISO-8859-1?Q?te?=
+ =?ISO-8859-1?Q?st?=
+Date: Mon, 02 Jun 2003 20:58:30 +0200
+To: rt@example.com
+From: foo@example.com
+Mime-Version: 1.0
+Content-Type: text/plain; charset="iso-8859-1"
+Content-Transfer-Encoding: 8bit
+
+test
diff --git a/rt/t/data/emails/text-html-in-russian b/rt/t/data/emails/text-html-in-russian
new file mode 100755
index 000000000..b965b1b59
--- /dev/null
+++ b/rt/t/data/emails/text-html-in-russian
@@ -0,0 +1,87 @@
+From rickt@other-example.com Tue Jun 17 20:39:13 2003
+Return-Path: <rickt@other-example.com>
+X-Original-To: info
+Delivered-To: mitya@vh.example.com
+Received: from example.com (mx.example.com [194.87.0.32])
+ by vh.example.com (Postfix) with ESMTP id 8D77B16E6BD
+ for <info>; Tue, 17 Jun 2003 20:39:05 +0400 (MSD)
+Received: from hotline@example.com
+ by example.com (CommuniGate Pro GROUP 4.1b7/D)
+ with GROUP id 76033026; Tue, 17 Jun 2003 20:38:00 +0400
+Received: by example.com (CommuniGate Pro PIPE 4.1b7/D)
+ with PIPE id 76033052; Tue, 17 Jun 2003 20:38:00 +0400
+Received: from [217.132.49.75] (HELO compuserve.com)
+ by example.com (CommuniGate Pro SMTP 4.1b7/D)
+ with SMTP id 76032971 for info@example.com; Tue, 17 Jun 2003 20:37:41 +0400
+Date: Wed, 18 Jun 2003 01:41:01 +0000
+From: Ó÷åáíûé Öåíòð <rickt@other-example.com>
+Subject: Ïðèãëàøàåì ðóêîâîäèòåëÿ, íà÷àëüíèêîâ ïîäðàçäåëåíèé íà òðåíèíã YXLWLJ3LPT9UHuLyGTzyuKQc06eIZ96Y6RVTCZFt
+To: Info <info@example.com>
+References: <0ID97EGL951H1907@example.com>
+In-Reply-To: <0ID97EGL951H1907@example.com>
+Message-ID: <HDE46LIK8GGJJ72I@other-example.com>
+MIME-Version: 1.0
+Content-Type: text/html; charset=Windows-1251
+Content-Transfer-Encoding: 8bit
+X-Spam-Flag: YES
+X-Spam-Checker-Version: SpamAssassin 2.60-cvs-jumbo.demos (1.190-2003-06-01-exp)
+X-Spam-Level: ++++++++++++++
+X-Spam-Status: Yes, hits=14.9 required=5.0 tests=BAYES_99,DATE_IN_FUTURE_06_12
+ FROM_ILLEGAL_CHARS,HTML_10_20,HTML_FONTCOLOR_UNKNOWN,HTML_FONT_BIG
+ MIME_HTML_ONLY,RCVD_IN_NJABL,SUBJ_HAS_SPACES,SUBJ_HAS_UNIQ_ID
+ SUBJ_ILLEGAL_CHARS autolearn=no version=2.60-cvs-jumbo.demos
+X-Spam-Report: 14.9 points, 5.0 required;
+ * 2.3 -- Subject contains lots of white space
+ * 1.0 -- BODY: HTML font color is unknown to us
+ * 0.3 -- BODY: FONT Size +2 and up or 3 and up
+ [score: 1.0000]
+ * 2.8 -- BODY: Bayesian classifier spam probability is 99 to 100%
+ * 1.0 -- BODY: Message is 10% to 20% HTML
+ * 1.0 -- From contains too many raw illegal characters
+ * 1.0 -- Subject contains a unique ID
+ * 1.0 -- Subject contains too many raw illegal characters
+ * 1.2 -- Date: is 6 to 12 hours after Received: date
+ [217.132.49.75 listed in dnsbl.njabl.org]
+ * 1.2 -- RBL: Received via a relay in dnsbl.njabl.org
+ * 2.0 -- Message only has text/html MIME parts
+Status: RO
+Content-Length: 2743
+Lines: 36
+
+<html><body><basefont face="times new roman, times, serif" size="2">
+<center>Ó÷eáíûé Öeíòp "ÊÀÄÐÛ ÄÅËÎÂÎÃÎ ÌÈÐÀ" ïpèãëaøaeò ía òpeíèíã:<br>
+<font size="5"><b>ÌÎÒÈÂÀÖÈß ÊÀÊ ÈÍÑÒÐÓÌÅÍÒ ÓÏÐÀÂËÅÍÈß ÏÅÐÑÎÍÀËÎÌ</b></font><br>
+<font color="red"><b>19 èþíÿ 2003 ã.</b></font><br>
+<b><i>Òpeíèíã ïpeäíaçía÷eí äëÿ âûcøeão è cpeäíeão óïpaâëeí÷ecêoão ïepcoíaëa.</i></b><br></center><br>
+<p align="justify"><b>Òpeíep: Áopìoòoâ Ïaâeë.</b> Ïpaêòè÷ecêèé ïcèõoëoã, oïûò paáoòû áoëee 10 ëeò â oáëacòè ïcèõoëoãèè è áèçíec-òpeíèíãoâ. Àâòop pÿäa ïóáëèêaöèé è ìeòoäè÷ecêèõ ïocoáèé paçëè÷íûõ íaïpaâëeíèé ïcèõoëoãèè, â òoì ÷ècëe: “Òeõíoëoãèÿ äeëoâoão oáùeíèÿ”, “Òeõíèêè è ïpèeìû ýôôeêòèâíûõ ïepeãoâopoâ”, “Ñòpaòeãèè ôopìèpoâaíèÿ êopïopaòèâíoão èìèäæa” è äp. Çaêoí÷èë ËÃÓ ôaêóëüòeò coöèaëüíoé ïcèõoëoãèè, Ðoccèécêóþ Àêaäeìèþ ãocóäapcòâeííoé cëóæáû ïpè Ïpeçèäeíòe ÐÔ, êópcû MBA.<br><br>
+<b><u>Öeëè òpeíèíãa:</u></b><br>
+1. Îcâoèòü ïpèeìû óïpaâëeíèÿ ìoòèâaöèeé;<br>
+2. Ïoëó÷èòü ïpaêòè÷ecêèe íaâûêè ìoòèâaöèè ïepcoíaëa ê paáoòe;<br>
+3. Îcâoèòü ocíoâíûe íaâûêè êoìaíäooápaçoâaíèÿ;<br>
+4. Îâëaäeòü ïpaêòè÷ecêèìè ìeòoäaìè coçäaíèÿ è ócèëeíèÿ paáo÷eé ìoòèâaöèè, êoìaíäooápaçoâaíèÿ.<br><br>
+<b><u>Çaäa÷è òpeíèíãa:</u></b><br>
+&nbsp;- Îcâoèòü ìeòoäû ïoáóæäeíèÿ äpóãèõ ëþäeé ê âûïoëíeíèþ oïpeäeëeííoé äeÿòeëüíocòè;<br>
+&nbsp;- Íaó÷èòücÿ íaïpaâëÿòü ïoáóæäeíèÿ coòpóäíèêoâ â cooòâeòcòâèe c çaäa÷aìè opãaíèçaöèè.<br><br>
+<b><u>Ñoäepæaíèe ïpoãpaììû:</u></b><br>
+<b>I. Ìaòepèaëüíûe è íeìaòepèaëüíûe ôopìû ìoòèâaöèè:</b><br>
+1. Ìecòo è poëü ìoòèâaöèè â óïpaâëeíèè ïepcoíaëoì;<br>
+2. Ïpaêòèêa óïpaâëeíèÿ opãaíèçaöèÿìè.<br>
+<b>II. Ïpaêòè÷ecêoe ïpèìeíeíèe ìoòèâaöèè â óïpaâëeíèè ïepcoíaëoì:</b><br>
+1. Àíòèìoòèâèpóþùèe pacïopÿæeíèÿ;<br>
+2. Ìoòèâaöèÿ è oöeíêa äeÿòeëüíocòè (poëü aòòecòaöèè coòpóäíèêoâ);<br>
+3. Ìoòèâaöèÿ è ïpaêòèêa íaêaçaíèé.<br><br>
+<b><u>Â çaâepøeíèè ïpoãpaììû ó÷acòíèêè cìoãóò:</u></b><br>
+1. Îpèeíòèpoâaòü coòpóäíèêoâ ía äocòèæeíèe oïpeäeëeííoão peçóëüòaòa;<br>
+2. Îâëaäeòü íeoáõoäèìûìè íaâûêaìè óïpaâëeíèÿ ìoòèâaöèeé ïepcoíaëa;<br>
+3. Ïpèìeíÿòü ïoëó÷eííûe çíaíèÿ â ïpaêòèêe óïpaâëeíèÿ ïepcoíaëoì;<br>
+4. Îïpeäeëÿòü èíäèâèäóaëüíûe ocoáeííocòè (ïpeäïo÷òeíèÿ) ìoòèâaöèè coòpóäíèêoâ â opãaíèçaöèè.<br>
+<i> õoäe òpeíèíãa ècïoëüçóeòcÿ paáo÷èé è cïpaâo÷íûé ìaòepèaë ïo ìoòèâaöèè è còèìóëèpoâaíèþ ïepcoíaëa poccèécêèõ êoìïaíèé. Ïo oêoí÷aíèè âûäaeòcÿ cepòèôèêaò.</i><br><br>
+<center>Ïpoäoëæèòeëüíocòü: 1 äeíü, 8 ÷acoâ (äâa ïepepûâa, oáeä)<br>
+<b>Ñòoèìocòü ó÷acòèÿ: 4 700 póáëeé áeç ÍÄÑ.</b><br>
+921-5862, 928-4156, 928-4200, 928-5321</center><br>
+<font size=1> Åcëè èíôopìaöèÿ ïoäoáíoão poäa Âac íe èíòepecóeò è ïo äpóãèì âoïpocaì - ïèøèòe: <a href="mailto:motiv@mailje.nl">seminar</a></font>
+<br><font size="1" color="#ffffff">3ZkRPb60QBbiHef1IRVl</font>
+</body></html>
+
+
+
diff --git a/rt/t/data/emails/text-html-with-umlaut b/rt/t/data/emails/text-html-with-umlaut
new file mode 100755
index 000000000..90e5d3fa9
--- /dev/null
+++ b/rt/t/data/emails/text-html-with-umlaut
@@ -0,0 +1,35 @@
+Return-Path: <gst@example.com>
+Delivered-To: j@pallas.eruditorum.org
+Received: from vis.example.com (vis.example.com [212.68.68.251])
+ by pallas.eruditorum.org (Postfix) with SMTP id 59236111C3
+ for <jesse@example.com>; Thu, 12 Jun 2003 02:14:44 -0400 (EDT)
+Received: (qmail 29541 invoked by uid 502); 12 Jun 2003 06:14:42 -0000
+Received: from sivd.example.com (HELO example.com) (192.168.42.1)
+ by 192.168.42.42 with SMTP; 12 Jun 2003 06:14:42 -0000
+Received: received from 172.20.72.174 by odie.example.com; Thu, 12 Jun 2003 08:14:27 +0200
+Received: by mailserver.example.com with Internet Mail Service (5.5.2653.19) id <LJSB7T54>; Thu, 12 Jun 2003 08:14:39 +0200
+Message-ID: <50362EC956CBD411A339009027F6257E013DD495@mailserver.example.com>
+Date: Thu, 12 Jun 2003 08:14:39 +0200
+From: "Stever, Gregor" <gst@example.com>
+MIME-Version: 1.0
+X-Mailer: Internet Mail Service (5.5.2653.19)
+To: "'jesse@example.com'" <jesse@example.com>
+Subject: An example of mail containing text-html with an umlaut in the content
+Date: Thu, 12 Jun 2003 08:14:39 +0200
+Content-Type: text/html;
+ charset="iso-8859-1"
+Content-Transfer-Encoding: quoted-printable
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+<HTML><HEAD>
+<META HTTP-EQUIV=3D"Content-Type" CONTENT=3D"text/html; charset=3Diso-8859-=
+1">
+
+
+<META content=3D"MSHTML 6.00.2800.1170" name=3DGENERATOR></HEAD>
+<BODY>
+<DIV><FONT face=3DArial><FONT size=3D2>Hello,<BR><BR>ist this kind of Messa=
+ges, that=20
+causes rt to crash.<BR><BR>Mit freundlichen Gr=FC=DFen<BR>Gregor=20
+Stever&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ^^causes Error<SPAN=20
+class=3D975501206-12062003>!!</SPAN></FONT></FONT></DIV></BODY></HTML>
diff --git a/rt/t/data/emails/very-long-subject b/rt/t/data/emails/very-long-subject
new file mode 100644
index 000000000..ad420d0a6
--- /dev/null
+++ b/rt/t/data/emails/very-long-subject
@@ -0,0 +1,12 @@
+Subject: 012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789
+Date: Mon, 02 Jun 2003 20:58:30 +0200
+To: rt@example.com
+From: foo@example.com
+Mime-Version: 1.0
+Content-Type: text/plain; charset="utf-8"
+Content-Transfer-Encoding: 8bit
+
+This email has a very long subject. Our DB allows you to use subject
+no longer than 200 chars, but we creat ticket, don't generate an
+error and trancate long line.
+
diff --git a/rt/t/data/gnupg/emails/1-signed-MIME-plain.txt b/rt/t/data/gnupg/emails/1-signed-MIME-plain.txt
new file mode 100644
index 000000000..bbf316b59
--- /dev/null
+++ b/rt/t/data/gnupg/emails/1-signed-MIME-plain.txt
@@ -0,0 +1,38 @@
+Message-ID: <46D7309C.9040804@example.com>
+Date: Fri, 31 Aug 2007 01:03:24 +0400
+From: rt-test@example.com
+User-Agent: Thunderbird 2.0.0.6 (X11/20070804)
+MIME-Version: 1.0
+To: rt-recipient@example.com
+Subject: Test Email ID:1
+X-Enigmail-Version: 0.95.3
+Content-Type: multipart/signed; micalg=pgp-sha1;
+ protocol="application/pgp-signature";
+ boundary="------------enigF67974ED650702891ACEBB10"
+
+This is an OpenPGP/MIME signed message (RFC 2440 and 3156)
+--------------enigF67974ED650702891ACEBB10
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: quoted-printable
+
+This is a test email with MIME signature.
+ID:1
+
+
+
+--------------enigF67974ED650702891ACEBB10
+Content-Type: application/pgp-signature; name="signature.asc"
+Content-Description: OpenPGP digital signature
+Content-Disposition: attachment; filename="signature.asc"
+
+-----BEGIN PGP SIGNATURE-----
+Version: GnuPG v1.4.7 (GNU/Linux)
+Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org
+
+iD8DBQFG1zCi0ygDXYSIHxsRAqEmAJ9mKiEdoWg9IFGlUlhPrzo9+1tKSwCfdqRG
++HKnj+81jWpBEhj6D00uNrQ=
+=ZFav
+-----END PGP SIGNATURE-----
+
+--------------enigF67974ED650702891ACEBB10--
+
diff --git a/rt/t/data/gnupg/emails/10-encrypted-inline-plain.txt b/rt/t/data/gnupg/emails/10-encrypted-inline-plain.txt
new file mode 100644
index 000000000..93cc19516
--- /dev/null
+++ b/rt/t/data/gnupg/emails/10-encrypted-inline-plain.txt
@@ -0,0 +1,31 @@
+Received: by anduril (Postfix, from userid 1000)
+ id 3AAFC37F5A; Fri, 10 Aug 2007 15:52:03 -0400 (EDT)
+Date: Fri, 10 Aug 2007 15:52:03 -0400
+To: rt-recipient@example.com
+Subject: Test Email ID:10
+Message-ID: <20070810195203.GB5815@mit.edu>
+MIME-Version: 1.0
+Content-Type: text/plain; charset=us-ascii; x-action=pgp-encrypted
+Content-Disposition: inline
+User-Agent: Mutt/1.5.13 (2006-08-11)
+From: ternus@mit.edu (Christian Ternus)
+
+-----BEGIN PGP MESSAGE-----
+Version: GnuPG v1.4.6 (GNU/Linux)
+
+hQIOA076g5OuwfIOEAgAyB2wkXAxcNw6+iKwzSlXlyZCsdZFSstJbhIr/i+ujIrk
++NLUAIgPfgIeg1nGVVK1z/hszrHRo3FT2b+jSrPF2Ox/wsUqpSvDf641hFbDCIlx
+bbqNmUEOCP5Q64Bw+RHoDoGLx9CPgeXLbciXvPZtlC3MPqG9w8lQdJlhTM1lqwCs
+1kqyvQ8YiFrvCNxU6x/81O9wWwiGOVELVnwX62crUK8howCpmeGx28Uo3HKG7NMv
+vTsdO6vwh38tNRz0kX05+AlxWP+3Vs4se6YwePc+XnhgWQoHqrYcxTOA0OrivJja
+1ty5CAHzGudBdXBcDncg0+6d1Ih60d5JP7nhl3lyzAgArA8CO/iv/kFOkQN9qm4O
+4zq189niju7mVCmcTLgBoxN2U1AeAlSl/JacPw4b3CGDQKmj1L/SMKOfUvOvCUlU
+o6XB7fHPxsFrJag9Yp5snenMRrvoUbypRljebHYsjtkCOsLqK7KUYLx2JQ/pHJjs
+AXwuyytBpLVxwEy0xkujpAQ4rsYT+z410zdH8hAthFo9FwFGKsT95WzfGcOa2+B3
+Qi1LX2uav8q5PAQgbjatp/aiHn3mHZSkRtLbOPr9A8GkmjQaNqIFgUMfaKx2hu8z
+nIZmAjpZ8CDvyKoFU0g9Z2KqNPagOqHgq6sRKuAp/5nz62sMyl4Hc7UfeYMTHWn/
+tNKCAYtyAHAGuvwLiRCN3M5h5Y1DdgEyIri7RtH/LW2QLCqYt9MLvmVRsPIEtuEh
+tZAb+KQzr9W6+fOhpW/1zkBCGxw2PEhv0HafXsVxEjOITbc2Hfwn9kySZ1aeSxL8
+DXzE9fSwcCOXQE1YY7TVNKuqqm34BADCnO7jw/b8EaOHRWgElw==
+=MYSm
+-----END PGP MESSAGE-----
diff --git a/rt/t/data/gnupg/emails/11-encrypted-inline-attachment.txt b/rt/t/data/gnupg/emails/11-encrypted-inline-attachment.txt
new file mode 100644
index 000000000..e3695f776
--- /dev/null
+++ b/rt/t/data/gnupg/emails/11-encrypted-inline-attachment.txt
@@ -0,0 +1,80 @@
+Message-ID: <46BCDCA0.4000205@mit.edu>
+Date: Fri, 10 Aug 2007 17:46:08 -0400
+From: Christian Ternus <ternus@mit.edu>
+User-Agent: Thunderbird 1.5.0.12 (X11/20070604)
+MIME-Version: 1.0
+To: rt-recipient@example.com
+Subject: Test Email ID:11
+X-Enigmail-Version: 0.94.2.0
+Content-Type: multipart/mixed;
+ boundary="------------010900070701080408060501"
+
+This is a multi-part message in MIME format.
+--------------010900070701080408060501
+Content-Type: text/plain; charset=ISO-8859-1
+Content-Transfer-Encoding: 8bit
+
+-----BEGIN PGP MESSAGE-----
+Charset: ISO-8859-1
+Version: GnuPG v1.4.6 (GNU/Linux)
+
+hQIOA076g5OuwfIOEAf/dbKZrl//1Q5r/FjPmqjp2+NDajR7Fj2afjKtkR8ySKni
+gc8kTvx2ceQ8bdOMfdn2AngPlKKyADp8xLDge3hwZg/mBRpOtk5B6eqGZDSwDxgQ
+Q2+mUVilhDBqLB5uv4rXoclT/JqR/WBWW1SjZgVVaf/lB9mI5amcmOPuY5vBq/iY
+q+qBAlWc27AfafGPSz0nifohc2qmq/pj2G6YDkKBE01EZRwbwvm35bTjI6rFL6dp
+Zzx2kGhAu928orL5Ft/KtyOEqdGwGlrGApLVV2X6+G1X6ASdgR24OAqnPxYMnhoT
+I3uYidSE6DSmUAm4R0rGRc1yMG5rBgRDDOYumRJFHAgA9WsNlQTkSuzypFzW7xT1
+zAzurHA1OYttjjWSAFb/eOnLxc27r1mBS0JVoEz/9+HtHOgGcsRe/sym6uwOOMT1
+znpgRGtQ+vL0bwBK3HyLqQzY9o5BGphf3ipCpI6HWwBDdGUJT+K7S8J9Og4AmV9V
+iq6V1PCdGwWS/XD4tulHIPUiA7Z6osVqtbHVTuQNWwsFU06SEHOmhNEGDSmO7Q7i
+bUS1YvC1STmJttzSsh+A/dh7uN/mfrPG1BJEAi4iZRGM6GFoKS5CQnpyQ4Az222n
+w1iB3u170ijcgLvDNR1Nz5DVnXdJcbVg46GvAGjctI7qWmFIKuazCA/AA0hwRjfz
+j4UCDgOxaIPydPr+7xAH/AzvNqeXEU9n7Y7K6rALWKwETU98ltzyp0FdO0YCbN/Z
+HWAyazGpPL6VqECWdc9xAYa1zYRf9HEW25yi5BMqbPav7oxfkEMbfLEl2OJr+EfO
+/GeTijv8riPFazcXdj7CKuumsld/GQgVLjwmRdoVtGN8WMRZfM/CII6uhg2wVbxq
+iThRc03DSNwLr0N0aNr66rySOnvTZNEARljPA1VaRgy1YkHXAyj6Z+s5OBZgj3yh
+3f6KZXaRBq9r+7iXJFuOpQ35k/pykHL6wwaTYMmEvlPZ9EO1zgvBxS6NWm4Ct4+X
+l9anXjRRPS9aSEUZQRjP6VfQPrrlhBNIgAEoKHlIJe4H/RVbTUAqRlTf3k4XBwkR
+HoTk/b/vM5grRQTR12DLXKB+NeNEpQPQFbdFOhfX22ZJbVzl3CVxaOEYHB29KT4D
+2frp99spiO8gksYdC5kviXeXXlXINtqsAWCEQr96wOlE03NuKh9TLF3ykAKageqr
+yDVdFjmeuMwL5XWVdWFUCDKROQz0gbX3WglCVhccrB4aMPx1vP4HiQIn7R2Q9/GJ
+I0lrn/se5GuT078BnhpApZkA0zU+Fi32NWTY7TLXYKxK1BpHWREEFIwrPe1D3Sot
+tLh+E65rDp2dt9LrZgGerSQqP1V8z50reNcGdRuWXfJSBwWJfVVo6ApEqD17bPo1
+Y8HSfAGGTN7euRDBuov2eMPKlLTYqQAqOE7DJK1IWQtw9aIMxLuTBCORy+DHh8v3
+elFBb1PxNZ64BJrOK4+gUm09eizynTdtHYhvhe8Gplu6JO5U28djXVqlIiHtEhF1
+VltikOnWlztV/2M8XoUKZneB9MBqhJ3W0lqN5Xzw7Jk=
+=bB9/
+-----END PGP MESSAGE-----
+
+--------------010900070701080408060501
+Content-Type: application/octet-stream;
+ name="text-attachment.pgp"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="text-attachment.pgp"
+
+hQIOA076g5OuwfIOEAf+KE8JLsuGPug17A+1JJwCliRR8Iid4CxXyFplTXYgeFGKh2EN0UJw
+Rj0k10BKnefbhL2gRM90aQaAF4gBF9mCh4H4Rg1reZ5ohoinNPk1Qgkotx1JnyVxIEk7mzKE
+tDIU4BGdcaN6LPhlBRBsSA/nzA19oM8HwecBstws9fbS6ZRwV8XLXKEWsQASEfmJxSYWpdkJ
+GEWIprcwkKClXf9RO+FXhFeg9i1ucFC05/1053w1CZtBAB9ohqs/GhILiCzVc8RZtQ+Xig6c
+Pdv7Q7zZBRy3lFz3o6bAjefxZfNbvVZmuJGMLPrevXF02v4aGazCtzqm7wR5cH0sxaaW0maD
+Fgf8DAeE4A+GqG16s6GGodVllO2JsDWYlyVnf+kfE4FU94uspYteDeGBf4zXhd9ccRYOnDWO
+o6yBwgonzgrZHicF6kcYhJLQS0uM4C1cK9S5nh6CP8cZEUUmEar9g5TKQjWivNGl4qNdzm10
+4dZOK1/u5xFb6dkrqOZ1a1M2DfrZRxgdz2RwZZbwre88wGKP3taBtvJvFXk26QNEa78fuoGL
+wqo3jvsn1evswD/JL4bVJl6VGRDNAsqSxkR6209W8qseFA1tw1aFM8XhpZzhchOlshg+ri60
+r7fMKln4t1hiaBstROGCCtdLomb4ezxJQddp/SH4K3XdHBJg+4ttjsUmkoUCDgOxaIPydPr+
+7xAH/RbwjSq8A8C7lJVXcwnmEIIpQfaKNaFkSRhp9+/y8e8skhUz6JRopeLnMv0u4o6jos+2
+9VGQ8ei+wzA8NgIbWtnxktT4aobI+lWpy7fyC13aMcG64lYbDUjktW7YNxkMYD9+Pk5bBpwJ
+hf9cXhfdSPPofqUM1DmA1LcrkkO+d/qvTCnHjrrjwvTFGBmvKi+iKUk1pltTvOdc5rkK7oI4
+6U6JTYWp9HfvNt7oYCMuILYEftvY0XhBCiUIWUClsQ7WhfqoGpix1peOv4ajzDYeDPo18cMi
+oyDrAHAeWAOjYWtuDXOtwpdaWkAoxgZOJgcu2ZVUWdGyFYvSCwOxfZYzv5IH/2a3xIjO21OC
+M5ql+lXb1OFfqsqXYXAhOP5+JhBOa5UN+VBIKG9pxVL48mWCK1GwHN5RX5aB+ky6tvkrK1QD
+XjCZRxj8o+le03bZ16QiWSEuqxTg2e4/zWY1ZUXBqkxpBVRwT5hzEbLVays0Bju6vKV8CyX9
+apFxFf/vWZ347x+/cK6jc/Bpc8u9PSdOBwGC1ReZixmvgL3fI9ozLOlkNXGvjKxx2Ui6v6LB
+e6SBQsD0cnhRGgfWXgJQsmmPDVPbcmM9+pU/p4JsqdA15lNUILXeeiieFNGHjUORgZtjhY45
+Z1Hw8EOfZWBquFbNt5tRaR6UmroviHO4kNx5N39DtfvSegEma6N5QIsTxrpw0IP2NMVXJ9qa
+4in+Orrc1EQfljFTLYSyK3zowghdHlZsMUo7d5+AMjDlsjKf9H0f1gMB8hWebQQNlyNOmKa/
+uHMcJVlbpGFCOspBgN4N2+s/Ldz+c9hy9W9x7J5U2bcdsdbEKlMF0EkgUE+f3Jhc
+--------------010900070701080408060501--
+
+
diff --git a/rt/t/data/gnupg/emails/12-encrypted-inline-binary.txt b/rt/t/data/gnupg/emails/12-encrypted-inline-binary.txt
new file mode 100644
index 000000000..e07163bcc
--- /dev/null
+++ b/rt/t/data/gnupg/emails/12-encrypted-inline-binary.txt
@@ -0,0 +1,86 @@
+Message-ID: <46BCDCF2.3080704@mit.edu>
+Date: Fri, 10 Aug 2007 17:47:30 -0400
+From: Christian Ternus <ternus@mit.edu>
+User-Agent: Thunderbird 1.5.0.12 (X11/20070604)
+MIME-Version: 1.0
+To: rt-recipient@example.com
+Subject: Test Email ID:12
+X-Enigmail-Version: 0.94.2.0
+Content-Type: multipart/mixed;
+ boundary="------------090206040704060905090502"
+
+This is a multi-part message in MIME format.
+--------------090206040704060905090502
+Content-Type: text/plain; charset=ISO-8859-1
+Content-Transfer-Encoding: 8bit
+
+-----BEGIN PGP MESSAGE-----
+Charset: ISO-8859-1
+Version: GnuPG v1.4.6 (GNU/Linux)
+
+hQIOA076g5OuwfIOEAf/bD4qivq3N/0qZZnLucbOARr1mypqomT+gvm1It/DllR/
+F3RRLlm0wcBoVQp52aNecT9OJiZNh32oxPW78XBG2OCZv+jATd2vMf4ESOX1Pj4+
+1DATM9V3lOJ8XYJqAX9dVe3MdUntpwqmGvlv2bYmbrc1o6IVQroPm6eV+ttB0BKc
+ELj+1NjpaXDGERoV2P300JcJNR6tu9pXo56kz1vAjAQj/xQBNCoSOxBB1TJA1sKF
+iNuQVObWYG2cE63PRZFTlzvTofeHo9kt9ykfyfNMnslwAaXPI5LJP3KGjUOZiomq
+XA+FYLzMuFzHaOgRGcpJGKTeZ578N7WZ21zmNOgS8ggA0/RNae8CHklYRd9sySdo
+0vxoAO6pokK9HSCaxeX96nS4JJfij/uTMRyR1cbwFBvJSQHUtGNGL1Tn9vhgGfqL
+TjrkvkC/HhBqu4QDYH3ekobV/tu/uQjGhxAASTFB5vtYSQvZtmCR1COTmO559aDz
+oO+93N4x3AOhC1Nhcmyds9mSCKCt4TCWE3gmHuPKYuLpSwj5Ndi5er6jCsq7RpsJ
+W0GzGOGg8wUUPDgBWJbXS5CVAkiJs/SlP0yUqeVR18mf9r/oKUt9DL3GCp230/o+
+0wyVkzqOBPNiIHpFRN1NhikXdKRVOL4ewJeCc9nrlBtLwqpEyZ1g4Ht7SKkE3rl1
+64UCDgOxaIPydPr+7xAH+gNwaV49PwLd8WUJpPuL+YmH5RRsAasWvX+VlnxM+g/7
+cn1BrvpXekq7vctMByxHaV2WnaC42n4M8xC+V1B/7Wk1rmhRMZKpIVjM6X9eNsPg
+FvOX5DhJdhgNOhjwT7rk29P24p9TQj06TwPnvLYjQJ6LViiX5l4T1YpG0mMb6Sc2
+vmp1MbGeV8zZ3hVbSpWOmwlotvfiGa4LjLGNduXIiOFF93pviV2xApYNt+FgJHLc
+p63GfqUXuj/K2iqKwFIbo7nssx/ceWBRdlgPrE/MSStYhBm/e9mw55nwDsUIB4Pg
+tkle/6XgRsOgvHydiTD+mxng/1bx9cmvVtNfvOq82wMH/3JI/rfmQUQGRJ/JFufk
+Kl26lQcbd/Vyxff3MVww7oUFBY2oUPwYL/5N3TjhI7HwiCkMP6S9bL1AfHvkLenu
+n5GKjSLJmyx7BNQMEIIKWD4tNSjA0CCk3+QVGr9sRIVIVD44Be9I8jlcpkn2r5Qs
+9djSXzEBpM+Aiqv5cCBFq2rgYyd9m2c2iHsI6EBaxCzYFz1qobsfvXk4a7ax0/Ck
+ug/jPEq8GK36UTp+51G8JhxzgJKSm4Fo7N3Os8emIlZ2QymPrzPkxEQj9eRsMmp3
+qIQ0rOPjTC8847s4lIlAx7di80OYQ1dV2J8NLL/qxF09rr6f978oH59IfSHRKJ2b
+aCnSfAGfFBr68Zm20se6/0mzrYY2MDljPrKSDQPmNW7nJ761/YjMgVAeZlnKW7JM
+hTdZciuHMTX7+3BDTGCMbAnmYYWurX0jKGJ4X0UJLV8m4nsXvqHvsG6FBzVC5z2w
+kytey5Bqi2gXRjk6xqckH4O79kEJw0kLkxJbT9IPBKw=
+=vGEb
+-----END PGP MESSAGE-----
+
+--------------090206040704060905090502
+Content-Type: application/octet-stream;
+ name="favicon.png.pgp"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="favicon.png.pgp"
+
+hQIOA076g5OuwfIOEAf+P0Qp/k1B0WDRr9bNcEANStTaiefYoLrUrtMJv+aFtkiSqKfft0A9
+okrYkVUKs6kxfgxueuqNMFQh58nl8+d7Z2qGIgVEXxC8rRxexEQ8mXu5LXzzBbc6Dq8Jsa7B
+bXzwGty51culYcKeMEjpEY8Qx76qoNQDNCuvth1JJxJ6xQix/pVyZZbJRu/nLrv1i3Z4KRFY
+qafnJlcsUTVj2o9dLfeU13z8nd0uBPY+hiCgYJHSPDLr+mkA+c6YK0m4a88r/wjLmsVHMkn2
+N5nCjuqP4tzT8SCjhoICGTbu+fFdks9NhQjvsW7MHBi9HFFzm6SoEvquFHThzwMl3hAhTLpi
+Jwf/a6unMP/swAxoFTJ2GRXBmQOH4sJHR/M31rEVkLZGJixhU94Tpx8ptgLXqme5VCXgl+M3
+Oh0GHRXqFYjR/HGUTZokRKR/BgCEpOGlH5FcabHiu/Gy8UBezPbuC+BNvxuCbuwODMp9R5DE
+F8RSCAQ1hrRoJjeHT2wyE7HdCvN/xx7NyenA3GdJa5Z6W7Y0gshr2fAOFL39jKXw4WwCh/Yq
+XnyG8uOyPgFrnHI3WpO24VpQHp3MBKebWNhQ/Opy/cABunCSwWQpDB9Ar4GeS3R1WGtMNC4r
+ph4afmTHJcQQkoa0VfvHL0hEzycwysYD46O9QhZfhxtKKShgX356oCeMEIUCDgOxaIPydPr+
+7xAH+gMnbi5OLPf5xMeZydvNWdHE/wJTub2rrWFtzvj0Aa5Ne/KFhcDDqSjaL3MXP1WfIJr1
+/ANe1eWcM2hlYVDpEn6YOh0bz6BASE4kbHA5nGMyUrgH0hWfcOgkMUloRZdf1q32j80mchCJ
+rF4YsQ6EndnUYzAiXKHGJRUy/6IA2qBH2n/fRiyC2FmmQPtWO4c6t15Vgh4fB3QXSTri8J5r
+577yIiHRE+dq6gg1BfyqCtw0DW56lSFQ7dxyMXeLyTGyjTGPlUDc+FbP23CRK7zDIVujARmm
+mX3bP2lMfCK326FwBZf2Q4Zl/ac1BN8Mcb4wwcnKvRzfEw8d1Y6pkphe7KYH/0MDDqtmuEw2
+D/xdw4FHB16/HW32bcPaMVvFuseczEfrwPGCrCiPHPm++edAoY0rWoBtzHVpgN+s5bset5OR
+snhjuWceuCb+Ga0QV0s/xmIPIQ8VYaXyD5hob6nHEIeskS68Vbni0BpY3nejDPoV3dNHY2Tp
+2fjYNHCpsdTz8yyavQVixoQjZQH9hUb48zZDHCt0Af9Rfq6Et5/Qr8iJqAyEU8JzZtrkpO4O
+HgLU7JTPLxOGzYtOj8JkLmLguVA5kOafAuU4OTEU43utQfS3KYbdEWT2jJ1QaJVS8CjFJrqH
+V99FUsDvgKWSTy5hA9gkQAQE1QdwkoQKpkCWm18KZqTSwNkBfrsEuvHC3Cz4Sy+cJmhr4Hvx
+dyZY8DuuWExUcVCjkeuASLgjLEgahnCbMkyKATazswwTEdfzjcOowjLTdaWFEN/Cg22nF/px
+9MXMtzBkrTkjYPhfywETKoMVH/Nw7rRNZhkOSb5WJV5ynF1BlbzXI7Z8rA/KrIn8aydzwJUU
+qFr8Dw1C7kbE76+SFVWX8fqpwGmQhDAO+kos6ivgN9HDHtuXmwfGeROi2U0WcmFGbAyLo5fT
+LCcNPOMflU27WDXm8m+tjq9naUynqvwg5zBBz/xY67L1R8uOwfZplvRi35iZAJjzMHGirkiB
+W3ZDXbDqEfKl4aCXqU+XhQZsku2z3OtKZOBVVI5p8nGVEfavg6QECRUNUS7qbtMxlj5IwCGl
+babK3W5YVuERjklrrLUYZjqFIZ2yLK3Z2VmSn7yKAb/eRvdEeha+9PKcN11pXPkS/M3t+Vpr
+G+4TqNgqwLVWMvbENp08dS3OAPpZLDnqG9CJV0qacDMjv69X26V3Xp6vuZoKqAPxMG9QKAfX
+E9LInR1Kd0cpRUkb
+--------------090206040704060905090502--
+
+
diff --git a/rt/t/data/gnupg/emails/13-signed-encrypted-MIME-plain.txt b/rt/t/data/gnupg/emails/13-signed-encrypted-MIME-plain.txt
new file mode 100644
index 000000000..f0bd624a2
--- /dev/null
+++ b/rt/t/data/gnupg/emails/13-signed-encrypted-MIME-plain.txt
@@ -0,0 +1,49 @@
+Received: by anduril (Postfix, from userid 1000)
+ id 19FD037F61; Fri, 10 Aug 2007 16:03:35 -0400 (EDT)
+Date: Fri, 10 Aug 2007 16:03:35 -0400
+To: rt-recipient@example.com
+Subject: Test Email ID:13
+Message-ID: <20070810200335.GC5815@mit.edu>
+MIME-Version: 1.0
+Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";
+ boundary="+nBD6E3TurpgldQp"
+Content-Disposition: inline
+User-Agent: Mutt/1.5.13 (2006-08-11)
+From: ternus@mit.edu (Christian Ternus)
+
+
+--+nBD6E3TurpgldQp
+Content-Type: application/pgp-encrypted
+Content-Disposition: attachment
+
+Version: 1
+
+--+nBD6E3TurpgldQp
+Content-Type: application/octet-stream
+Content-Disposition: inline; filename="msg.asc"
+
+-----BEGIN PGP MESSAGE-----
+Version: GnuPG v1.4.6 (GNU/Linux)
+
+hQIOA076g5OuwfIOEAgAhFFX4adckICRP4VEhrElZ8rRYAwJVbTLd7S5yOnrovcG
+sRAIZTjEmb8CRPX926hm3fJ/5O+pm9ZPE1wzvEBQGcJ+6oZPD4GMwuMqJpaZLw4v
+6zj4ogzac0i32vkwLYDLHXle9ytgiBwTFHuOKllCvBmTe5PrFAHqhA0EPJ824gD9
+H93LJUyvn/elOHASKVcUhm1XLV0RxdNTh8Afc7dEAIr0uPaWu1rVatdnc9JnQjsB
+luYLP8M+UD9u/sZwSBv7x/PVIOE9QsaFQGQZTYEzKb/4zeujcUyM0+4rjJ+45QUX
+cDsrFVmXoQfts2nw0BN5mERZdbOIwvkhZMZzsf+EKgf/dA7x9rguO/eGy/keQf0f
+sBHAz24WGWRqcmRNdBsaecVgAsygAEi564RQYvxM7eJxqKl+1TsjUmGUAmacShN7
+JjpqTH9HRrV522UNvVXFitel4Ri3UItP3zI+951x7YvkzUtIz8gfaCOHC71NmPBO
+RdKDDBYDEajJkYN6mhL+QpX9fIIP5ALkfVz0JmdHN1e81Z5myuEWnCSuxeyZHCgl
+Xw8PuV/Af/+GHqjNUaXRDxN4SWm82pKkK4rxioMI4liI5zuR73GH8wd1RjspCkKd
+gPEhmqDxvkxykjhtJt5Izj+iQsyZNRDHRkGJA4BLOLUnvFtwpNCg6DaRDUOBjBmI
+P9LAXwGbhZU7uZLtSzwn1gr8TD+cqkpQrFlKRiUCdh6sTwfp+HE3JDmKAwX6t8bM
+ndBMYwIPddYdkFFpWOQbl1G72zR3SSuwgyCC5+xZxDWPHT97iCKbvCYAmdN3cGHZ
+8Lqu56ulz54pIkBZzSsu8TWzZner2eax1MqITH3WNYYuH57yiMSaUK1DSdFrg9Mc
+o/aK0QUaW6pCFbYGZRS1cqUBaN11Z2debMC60JiNFA/htzLwgSwmZiEdAIt4lvU6
+xMdA7dFYuVLmVh9+VQYp3KspNDWsILiPsKbV8oomlcLiZ+g8cRY/NzLKXVqX2a+7
+Jl3hLlEuVKdcWe+XCfWt07Y2Ibwtpkq/vph61cPdus0dgV8U+QITQ4kt1ky8xNBc
+L51c
+=zSIe
+-----END PGP MESSAGE-----
+
+--+nBD6E3TurpgldQp--
diff --git a/rt/t/data/gnupg/emails/14-signed-encrypted-MIME-attachment.txt b/rt/t/data/gnupg/emails/14-signed-encrypted-MIME-attachment.txt
new file mode 100644
index 000000000..8a70384a2
--- /dev/null
+++ b/rt/t/data/gnupg/emails/14-signed-encrypted-MIME-attachment.txt
@@ -0,0 +1,51 @@
+Received: by anduril (Postfix, from userid 1000)
+ id 061F037F56; Fri, 10 Aug 2007 16:12:29 -0400 (EDT)
+Date: Fri, 10 Aug 2007 16:12:29 -0400
+To: rt-recipient@example.com
+Subject: Test Email ID:14
+Message-ID: <20070810201229.GD5815@mit.edu>
+MIME-Version: 1.0
+Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";
+ boundary="N1GIdlSm9i+YlY4t"
+Content-Disposition: inline
+User-Agent: Mutt/1.5.13 (2006-08-11)
+From: ternus@mit.edu (Christian Ternus)
+
+
+--N1GIdlSm9i+YlY4t
+Content-Type: application/pgp-encrypted
+Content-Disposition: attachment
+
+Version: 1
+
+--N1GIdlSm9i+YlY4t
+Content-Type: application/octet-stream
+Content-Disposition: inline; filename="msg.asc"
+
+-----BEGIN PGP MESSAGE-----
+Version: GnuPG v1.4.6 (GNU/Linux)
+
+hQIOA076g5OuwfIOEAf+MEje2ZVc5yRFxINYBmz8l5oRdrDkldSFISKNlcFogKeA
+ZAwvP0Y2Utxjs+aT25gqR72uftGzCLhKdIb8X/i8CswKHX/1NnLKFTFB+T3Mmwtg
+3a9jiB6kQ5haWvcd9y1DBcm1Wnw5B51QmiHZzHu209dphl7qnoak79M0NK4KPSqr
+NKqn7yTKFOjJ3qIlCLqawQbQy++IFmtJVKMQSNZawqSX/JSGMNPz9hioyOgyjxtE
+XZlrdgV1jumnZ3nVF1bQFMGUMGpi6NAA7+TiPjSZgFkKp2zvyOXwFKjKqkVgAgUh
++295JO9kjFmRYOxpWcSfBaibX4qFSsWvJbvltNceDAf/WCCcvP6+JYHmZL32YL6+
+oSM3SnHuG3PIqaT0/99pMVIhHzhrdrmWsyR6ijcK7PRpQg3mIZDd5pzAgcae3c4M
+mYsImADyUC6CX0PvSOYYvG7CskDUiGk76fjDKTkKPkgqEFJXz1v9TA+ptx9v2PTd
+CW8cES9Du5PdwPLbltfC54ax/tCKcyyHbtgozVQJB6RfkXKhUjYI632qnXa7BDqO
+c5Pcpn7gKKik4MkGEWnf50aqQ6ph4Q9MfOZ+5hc/EBUQd2Genqn8iCTPa2rkrI49
+6sKJ9FSdXL4Xdlat09MxnD5Dw6DKF6G6Ig2SAQA/WBG5+sZxWC5NMd5oeJWHVnCz
+VNLA5QEIo0BNtY+wm2W/YH3hSHV5K+Mz+MEKEUaimYSLWKl1p6vboBjHuk0XY1Tn
+FDGmLVcO/jLBkiPOhNyibRXaCW/x3iws+xCHngjX4rDeidEtphHCxihVtS1Sh1wT
+IHHE+Kl6jlateIeBpug79RsGKcdwYTUg2SuaZXEf5A2tQtUhU6TEWYqhFc0g2dCV
+u/lkhrVRt4S+En4DYJ7myQYdMBeHym52Mz9tFZKKQ54dMWDhDsRZIe0Zy8TKBwXd
+cwc5kMpkwFK3drbhFuf3WnQUUR80oyGKCffJw3n40ET91b+nAYJ8bPvYFE+gDB+S
+vHktXvM+vk4PTAKGN21TyZfB9n8VVtIHq3DUPpStrFFQQUDKjMeZf0TDtEWVd1zl
+7jVJ2d+Su/KcAXUITXAQMl4ECXMZ3VD3fnbyRAHI375+wcBh08rM1DQRdhWd40XZ
+55rzTy6T0RVa3JcQBCtM8FYubYQvy8iU5XcA2LEDWdDn8D89IJ+UAnFHIYoeipj8
+qbYjl9JetF1gcmm3QlUwtec6nb5VSmWdQrdfnvdxrW54J3gqlGnzCQg=
+=7/EE
+-----END PGP MESSAGE-----
+
+--N1GIdlSm9i+YlY4t--
diff --git a/rt/t/data/gnupg/emails/15-signed-encrypted-MIME-binary.txt b/rt/t/data/gnupg/emails/15-signed-encrypted-MIME-binary.txt
new file mode 100644
index 000000000..55f69a19d
--- /dev/null
+++ b/rt/t/data/gnupg/emails/15-signed-encrypted-MIME-binary.txt
@@ -0,0 +1,60 @@
+Received: by anduril (Postfix, from userid 1000)
+ id 171AA37F6B; Fri, 10 Aug 2007 16:13:42 -0400 (EDT)
+Date: Fri, 10 Aug 2007 16:13:42 -0400
+To: rt-recipient@example.com
+Subject: Test Email ID:15
+Message-ID: <20070810201341.GE5815@mit.edu>
+MIME-Version: 1.0
+Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";
+ boundary="QWpDgw58+k1mSFBj"
+Content-Disposition: inline
+User-Agent: Mutt/1.5.13 (2006-08-11)
+From: ternus@mit.edu (Christian Ternus)
+
+
+--QWpDgw58+k1mSFBj
+Content-Type: application/pgp-encrypted
+Content-Disposition: attachment
+
+Version: 1
+
+--QWpDgw58+k1mSFBj
+Content-Type: application/octet-stream
+Content-Disposition: inline; filename="msg.asc"
+
+-----BEGIN PGP MESSAGE-----
+Version: GnuPG v1.4.6 (GNU/Linux)
+
+hQIOA076g5OuwfIOEAgAiH54xEwaxc/DL+nyd5f/77c6Vk27bdxRz1eXHVU0ofqs
+oeZHkEWpkub80YQL1HOQTabAF3ZUIGfyfGufnqXpWWNjhnxP/Bi/Itxk4p3oq6Pj
+RDoDsrO6SPcM1wlbz7M/8rTmpjTk0cYl3nA9Bc7ZDP7jRWRRKFp3Jl63fvK2BWG3
+x0yA1WwJX/gzgsRlFSBBFukaffCqaeQZJ2GSKGbuHTH8EYvGDjqALlEwE/iPYRYq
+OoSa27KIzDCwPFJf8/ZK+03qX2D2LrEViflbFlZMcMGrIFmU0BLrlBx8Obimu3lm
+8bCDX5zQsOIptLQqrhQ5E1HIeEFHqJAxJwHu5v7AFggAqSNH4adYAxz3vZyQ3HdV
+IiDYdwwGh4usb7R7afc8uiFGzxmcc9ktyQ8bBAEpGHMU+ahcUMcfg1svcSKKFQqS
+xNJb5rl3Q9D7qqsoFnNdVFGIxhnUpjt16TdFEtRHDUnC6dWjxKw2fzdR7YsclzkP
+IjUP1yHRK0ees3r7LGU90zQUcUXGnuX4xbMi3+t8NFPqpi9DIkOKS77XEpLFg+Yg
+qrMi3/p72jGBcgbchyS5JJyrVfZjz3igEDy4A2rEw9lJvyemahpccYf9v8LjVCn8
+ofdeLujIyDV1T+wFuB2gIY8fbnvP2UC+ffJ2qGA4uDiz9T1t+IapBjwwrhG/P541
+KdLpAfXAJzKb/gnXdicy/TyGnMguVXryCVZXAx392jA5se/OVjtVkUV8hMcqBPFC
+6zMEPZDXVaCZ7r4VX4xK1y7MGTrq+t4v8UhmfxckiwDVrKZkt6MOwdhTq1YBwFwq
+DEDN391R2fFr08i+3ogKv30gNlqs1gfqh55uCsZMsrtjMAEnM7JcB6pnptND/mRm
+9MYsz5Sxra5L59SNNhJS0GjuTVGIaEhzwusWdFTAeE358AqTOTwdSNe/Fa3qEXBr
+YzmrCgTn4x4YTn8IDQkh80It7ENEc7tkgRN/FcG+Q6cwHWgPOj7QDBJsN/lm8V3K
+cWKVxwvp+hoP8isigZxi/7nOD1EZkQkMAWyZSFp+iCZH14GcAKwqxp9Y8bgSuF/5
+jzASoge+POvEIpsbS7lmfalU8qR2kCYg17fEvDH+6olqRCT6Bq30cVUYY+LVAHgz
+KXgJjK3WYue2XQieGaNYjtwr9AsVl4fxpjXmb+QwL6H0K4JMdXc9tabF5j5YFeTQ
+rWYzED39EiXst9dAFomVhXDHD1OdpAbNh5F9/9wlccgZ7co+tK3bdcD18k2G4XGq
+1AX27utveaSU8M1jeHG4a9//f6NLn4cqJ8Qryv0uiY7N0iiwWHsoHrci0doMQI0U
+glPhkk7qjHf7YAhsRsybfdNrum4jPHMpk1wqY4GR4xhV3JLAbDfKN17yHJWR3c2w
+TQ8sOLMPKMibyo/KRBLCz3CpnuSvpc7A5tCenDJLYtDhmUMofTN1ki3gBW3OFQpd
+zH6pCuVBQDQ3iLO/lg6Y434fPz3cuKnxBdN/QvdbeiX4H7tGzC/q+qXpu/8Yv2x0
+AVSQkrXcc4CQXAvLzqNMXa6NgKrVtVNXUgHyIxvOgGyVxULKDQo+3bByccCOjNOZ
+3gN/JnU2HvEk3iYDYPa+VOJzS1i6itZOCeCBF+NDaaTvG30owmINGCGl/nxv+yIO
+9nlCF3QYdaod2TVYfxdp2X7hlPEhv6nHYt2r3/pXYW4Hjy5M/mT7sR+OVAgknpiJ
+yOzeNy/dVoxpAAlOuzwl+sYI6TkDnF0vduJO0jxWP5+oa+Al9sWr4x4E59OkGAg/
+lQ==
+=NbQY
+-----END PGP MESSAGE-----
+
+--QWpDgw58+k1mSFBj--
diff --git a/rt/t/data/gnupg/emails/16-signed-encrypted-inline-plain.txt b/rt/t/data/gnupg/emails/16-signed-encrypted-inline-plain.txt
new file mode 100644
index 000000000..5686a12bf
--- /dev/null
+++ b/rt/t/data/gnupg/emails/16-signed-encrypted-inline-plain.txt
@@ -0,0 +1,33 @@
+Received: by anduril (Postfix, from userid 1000)
+ id 98F6C37F69; Fri, 10 Aug 2007 16:17:42 -0400 (EDT)
+Date: Fri, 10 Aug 2007 16:17:42 -0400
+To: rt-recipient@example.com
+Subject: Test Email ID:16
+Message-ID: <20070810201742.GF5815@mit.edu>
+MIME-Version: 1.0
+Content-Type: text/plain; charset=us-ascii; x-action=pgp-encrypted
+Content-Disposition: inline
+User-Agent: Mutt/1.5.13 (2006-08-11)
+From: ternus@mit.edu (Christian Ternus)
+
+-----BEGIN PGP MESSAGE-----
+Version: GnuPG v1.4.6 (GNU/Linux)
+
+hQIOA076g5OuwfIOEAgAsnA+yHerSZnvxR6TIiWSIRlohPBIQHUhoeHBH+9Q0DFJ
+r6in5UmN1luVGtT8kl1dbLAnoVu2Yyf/d57IWgam41MbWi1EmGLQaaLHcIbQ3JXR
+MmXXwG7PTHWdZJismmqCOuT3svTlkqerIBicsHXvfKE209y+jP3lruJ7cVpxZyCI
+M75H8d313r3MwBpoRuEiNMBOjG6MXOATFmgRw93J6pzjWirxwmawhaSNeghkO8tN
+3vdDkzZlLmM/Pq4jQrkWbIbGH/EwdchbWNnhd91o/Lll77fshkXNMYQlgyU154sk
+3wCY5IFGJTUdR1hrETz2nOASLDHIdamhhz1xMItClwgA2uXkVhG0Fslp/A7z4a09
+ivX/gM6a4KuFSNVJtHrlc6Z8/WKe1LNdLiulbFMtbppvMtIkzgZfv/DBavZJBqVI
+EOI+9VzLb6IdqybbNp54nRbniU6aiboEk90waSjqHggCnk5qnOUxrxp2ZCIn9pwP
+KUisxy6cAKGCEeFdvtXIBqfC6uITAu9kNq78rPN64TUaZbJs8VshIj1zpGi6DUYa
+uPvGPIhlLi5xN/oK3Yu2TDrIPxO4m1ZdHQJH8TBn6l6EifAVsDeskSQ4nCZJzc0H
+0D+ofkCzxqRkjDY0IOcl7hI79cRxO5tagtf6od0vcK883wUHxe6Kfv26tKm8IsX1
+ANLAFQFi867dLG+X9wc/QyBwNXZ1hPSE8MulfAYyzT/rXno4nkmQrw6zFLf9q4gA
+Z7aZsgxXPAvWwhwLGyinNgi0ua5LHZL17CWOfG8/GlIK59mesnk8tC9Gyj8aLU3/
+ROteGBBGfk0UJbfcQ+reAjmORofZdHSMCsGYZ5DJEy3KIUrNHzW4yYAlfzLWYfX8
+k9R41E0xGBuooe578MTBtVOPZKY4gurQZdfrHdYnsUXgfpV1w6WYvEM31n90Px6m
+aXWFWq9JVxX/JFOmWJV38fw+EhNgApncTw==
+=J8xa
+-----END PGP MESSAGE-----
diff --git a/rt/t/data/gnupg/emails/17-signed-encrypted-inline-attachment.txt b/rt/t/data/gnupg/emails/17-signed-encrypted-inline-attachment.txt
new file mode 100644
index 000000000..6991f0b7f
--- /dev/null
+++ b/rt/t/data/gnupg/emails/17-signed-encrypted-inline-attachment.txt
@@ -0,0 +1,84 @@
+Message-ID: <46D73CE2.8060202@example.com>
+Date: Fri, 31 Aug 2007 01:55:46 +0400
+From: rt-test@example.com
+User-Agent: Thunderbird 2.0.0.6 (X11/20070804)
+MIME-Version: 1.0
+To: rt-recipient@example.com
+Subject: Test Email ID:17
+X-Enigmail-Version: 0.95.3
+Content-Type: multipart/mixed;
+ boundary="------------070807070206050202070908"
+
+This is a multi-part message in MIME format.
+--------------070807070206050202070908
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+-----BEGIN PGP MESSAGE-----
+Charset: UTF-8
+Version: GnuPG v1.4.7 (GNU/Linux)
+Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org
+
+hQIOA076g5OuwfIOEAgAn0IWP59DHAWRYz8MG09D5vp+V3rfVdwv8ud2hp/jAUDZ
+ogXobK3KkUH5CIDaohqgrxobAtU9D7XhxO1ti6G9ana7+u2GiTIQmC7F1THSipNJ
+GZajUNG4E0WQXjvkWvDgx2cLdjn+L/i4Y5lQvP6CEmzZpdfKQk4DZdmMJf/Fz0Ag
+jqPI0weYSEr8YBz/p0bEy2Xh7UPw8rwC6ajk/v/E/SfZXI+TpWnFxLt9OUN+o0E/
+o8RQ+5LTpPvkR4RTFFnqZAKu8CU2LqNIWYlzhm67pi+QqeepMuhbW+Ix8tt6oBbN
+EXFrwBYfjLeLcmMJcm2fEwE7otDqKHHW5/G/lu8RBwf9E0TjxnGbpTH3ERrj172E
+T/LT0LD2qRitQQCdFyeDvnq6KKyoUtkyhwrrfpDfB1ZYBSjgIB8rgolbO6OxnY8s
+O6dob+07mQrC3EsbrTQhRjwtLCWB/4kaI0d/9Y4/InStq14AvW0wZWX3kukUq8Us
+ReJhrDA/fOV+duOQgcEc6ZsMjLE/snQv6KMN7ey7iOe10ejLs9qHFZClqHhpjbn7
+zTPRapgTOV1hwBWq9603NDP1EQM/wAOFCw1TDUnhFOzUocpBfSwajWY1bKB4pz94
+Nf/U4BBUFq3aQVX3g4mh4sBesYInZ8wMq7fw9fegyiaLL7YzTbxg/YSXHWkTaYCk
+YIUCDgOxaIPydPr+7xAH/1BIgIWDNzXHWbrmi2pxg547LWoJ3EpJQ46fk2ryfIll
+6Ot1xpCXGStZA1bUnRB0KTZlTNVfXkIy2TuFKJ0xP5JgkeeVQ+Vn8wCXyPB1nYte
+WJ9aKHIk9UhKHhW6FHIjWs3CYjfmpJPaI3AXu4hwT/W1yPIFbAb2UYwuvRn5XABk
+RDxauFRDoHKPvy4IsorGBPIa5ZkJxuBsP7lxp03CfgnUHX7HYehlNp+rKzp82uLg
+fXJdwQAE3OcrnB7rbD9AirIyJpy4q/sYUOnC2M137PF+HYA5zCxktpnkVDtWd6Te
+DyQy7T5Ey5/NzP8IbJpS3Wv+bYOiozv1zNCZ4ZbopbcH/RKMlLN78fSTZmgC6Snb
+dcwZqi8lvL6vx83VBwUAaQeNDLvtlyjojt0UuDWUI3JQJpv5vcGdLjP21+5f/INt
+lua80hg/pKwoKijzQPPtnPBJQOiRkzIEYlaqMkmPi+jgkosf0kcAWxnQGt0L74tD
+Q4ENpgCK6jJVkQFN9Gd2efT7EFUc2acY7/YfFcTOBVm6xi0FXoa0HzL4NzMhbrsx
+NhlC7/Yp34y3NHjzVlMIKiU9kbHrVCyZyXKuSSV2em0a1SqbJu2wj2/qjK6Ibtts
+0aowVn5HafWR6jm8sEEyX/v8c1BdR5Ibmup7Z2DPd0j+fY65GK0nNYZ5QeYq7PxR
+rSbSwA0B+Ot9P47q0c2vGrQF7EKvug2sgnE/9K8DX+Q5IVwMQEoJUZvKf66x7DTA
+YIMnWxlJeeU6fkURzeBpv0TzidU6/KRa/2xg/QoUTZvftAoaQidsajzFSIZTBEqC
+xjzLnkqlDMtlZYBtdvVYDxrBvj8u5whGnhe6GhO8GUfIe/N+bWM5N1kbUAsE7Tau
+ify77kszQdf20rLVO9eU0MjXyvKpWcT/Dpk/Nzth3iLACEtjr/dXo5pMwK0mofx1
+I4lLS7P5PtTz4Oat7aV1up1o
+=LNkV
+-----END PGP MESSAGE-----
+
+--------------070807070206050202070908
+Content-Type: application/octet-stream;
+ name="text-attachment.pgp"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="text-attachment.pgp"
+
+hQIOA076g5OuwfIOEAf/fLTYMB41pbPXfa0LaNlQ5PVkAa8i9W/RVfA8s+BGgFYz/herlLi+
+eCE9Ls1RoiEmh3e3ZOG0M/iPA0qBMryH4fcYWqx3R7uaZDVIOEcUM3lR9Q0ffNfGRyVMKX+N
+nUUI/HAaSLCl1NWF6Qnh+/S8tV97D6FDdqQqJKeG7HtPcXMJ3daqHcCl5mMrL/OgcwEcqdHl
+dw8VnESjnkQ3NAvzyNJEpyB7fHN6HtaynxmZbU262ez9Ywh8XsfBbKsDhnLRf/rEUt7X3RQz
+YjXL2C6scGT9ctZ6i1yxPQJOP1+Z+UxijfSd8S59n51/SATsdjmhNFst1JZTbX9Xm/Gv//mj
+aQgAuzhrqVBvyOtX1fMNxGACBhMuJKMLvb9bCltnl4V8IN76d1HP65bw9FvFC7JNcLnbeLBk
+6J6CGhwZYcQHuLBiTAfycM636RotiszjLTx1/7j/Jp3vvzToLm3sNNb4INVmoRBrmVqqP2wl
+Ykl7mxdHAq0Lr6NMiTnk9/TVbJnXdCFseLONJaX6ZPrPZUbw6jhgDMeyAjoBkHJjGqfbde2S
+TsHvPvT53g8d2RgZx5ATptTphU5QsBbS0eYEIiKoU4lUh7augb6EratU63xTsKc9pJ8DJOqB
+l7Ic1ujUDQqlccTVJgYyIJJVO/GUR/AkIYrzWcvEwhtk+PW5CMwrcbEo5IUCDgOxaIPydPr+
+7xAH/RYdKbPnEo/0R8mZls6xuycjGoGNWYuhhaze7NiDPuflz/oMkPL9f/o/7a+9t5mBbdQ9
+XXnNKuhclocbDg6N/TLXWWw9011Ba2ko2l8LpvzlLo7oARaMYZu4kmMYZIkIeQ2GW97gIV4w
+yXLJOuVnH5cudt36/P4QqJM0PxSqmQA5sqAMB7cvgK3RFKbDvvrMvmyrdoTFmrcFhKMWbTq+
+g51hVLm7HEHjicP6LEhHv7m4ooe+rojS4D+0HofvkPou4XWJ+R2T4CVKOSfobrp0RjMqjRuJ
+PvdzhyUNjP1qhtvUyM0JBgYHd7biFKOKjYG7E7j9+5EhHG0uSG+xRWq9NtgH+QF2Mhb1WB24
+lpXZM2KPM6OdStgEJFEzjr9DwULJpEzQhvOAUI45Jd7oqLQjEdmzuPmkEBdNfcUBKkCNdADp
+JBn/0Ak2C4OQImtZX6kyJ2afF7zwMQ5J1eH2e/eOYJTOAVnt29EH51UZHi6OCWosYQhDMNZW
+8AXoza4H3474bPzlxw7utSRm/CJwetrmp3L9BahuEheYv3PZCmy/1aHpt5OHIJDmrDVMzaAH
+DhWNkRuY4/CLvg/+r4W21Y7tsPllPGNfxLJt9kilu8F3M/alhNK8CxbM3sskNnxSPPid5vE2
+e88iO74uhwvQzTrPKlXfsUDpnGacAwG3uNS3x3CbimjSwAkBjvCRETU1DziOU5xVTCGEQFPv
+gF3YtRG7Bmvm0d1zeAlPDuflRhadYCdNvy17gA8LI+YBuk74XavAqKURN6FEevULYaSPZCx8
+HXgjE0uyo8U7l2D9hrYjGuvON84Cwp2O6+jsiLk4oVvdBVbb0PRz4xj/Egnlu+yjGD88Swge
+zpQzBdaqE8m6sizWBlYyPGh8amL8/PfS3OC6hZcg4LOiLhj+h+h3eeamTyAE62zjJjC4UMKc
+ZEI9ZhQ95BxH9bTS+JxP6UySLnQ=
+--------------070807070206050202070908--
+
diff --git a/rt/t/data/gnupg/emails/18-signed-encrypted-inline-binary.txt b/rt/t/data/gnupg/emails/18-signed-encrypted-inline-binary.txt
new file mode 100644
index 000000000..315ba5898
--- /dev/null
+++ b/rt/t/data/gnupg/emails/18-signed-encrypted-inline-binary.txt
@@ -0,0 +1,89 @@
+Message-ID: <46BCDC0C.6090400@mit.edu>
+Date: Fri, 10 Aug 2007 17:43:40 -0400
+From: Christian Ternus <ternus@mit.edu>
+User-Agent: Thunderbird 1.5.0.12 (X11/20070604)
+MIME-Version: 1.0
+To: rt-recipient@example.com
+Subject: Test Email ID:18
+X-Enigmail-Version: 0.94.2.0
+Content-Type: multipart/mixed;
+ boundary="------------090909060406090905060708"
+
+This is a multi-part message in MIME format.
+--------------090909060406090905060708
+Content-Type: text/plain; charset=ISO-8859-1
+Content-Transfer-Encoding: 8bit
+
+-----BEGIN PGP MESSAGE-----
+Charset: ISO-8859-1
+Version: GnuPG v1.4.6 (GNU/Linux)
+
+hQIOA076g5OuwfIOEAf+KwEEEyzyWahb5iwjSff0D3MfGimfD7AJTEBjjDgtxcC4
+8xKSJsXhZ2feCi3Wx3TYAoFedoiR0DkyNd//B6dE51bpDpoUX1R8M4W0tOKH03ZZ
+N3sIBYSnJBF66GdLfjSvyEGD9sdi+OnWTv9DbQ4x9Sq2d3u8PBLu3krwk75hJEH7
+z9Q4rhqzpRU9ymhWpS/QPd+4SvuhlJ0ciUGeYLmOFc1YAUJNWzTtnja/jZptFY7l
+8oDXtHTapeJPB9M0WaDf6R60Evkl9DcvGA6FRBMCzWRjqMSdAPGYiceeMiDYcwCq
+hJWSzbDKAHDSY2qwlHZ+2y7mIze3x05qcBHYCIOKewgA5mer22WKZIiEP++p4TZF
+kJbwFxfCqf7mB/nsRGDWsfThGED4KSr18feba8ychNLbVx83FEtTZ5tei96N6NrK
+eS6BdjedTCbvrq7lhIFmC/qPXy/5cjNxr50RBT0sLUEdnY9dhrwLxo69Rqr5qHQT
+luYu7/NhQwOV8OlMTpvy/AfUXGiICLa1iBxtVK8UQ9YMLe1GOyCmseeF3UyfTmKN
+YfEylY17YRsmYZerulSwsHnEoCipjEHOwzksR5zXDHmnv7cqVq1gV1SnjSXOd1Cd
+QuJFdXY5fBdugcrAAJsiB8Iq4t0B7Ai1Lou6x+kKQoigF4i3zhSmH6blZmc99B1r
+jYUCDgOxaIPydPr+7xAH/j7bst78EmNmr4RJvQ4A5bss5BJeOdG36MsTkQ4rXTKv
+tA+chOccB3irMYqpWpKoDMWRz8VgAu9MVVc8SBUr6XRIhLRyCpuZxlwLA+EJJUk6
+yeKBHZZ8KgQ+PgC6WYMaTcRRLeOWbTxhhIrZmT2EbEEDE1jbeeLNrEu1wmrdMBBb
+fhtQTjOHKb7iau3LOGTbbV+F9llnPHrdy2WTp8ozFbsruEqdUG85zQ8X58sy/iC4
+hS9mj+vRs8nanbYABfDHUhoBk8VWUWPlpWTUfzDX0Wuai7LWo5wn7mK0p2i84vMU
+IghS5OGRJfrGVK/1giklqUrmPuiz8M2bG9voLoOVnVIH/izycMW2zZh12TD+YySt
+D1NP6OPy/5PgYgVZvzTYtOW7Y5EI1eTYoC0cMgjITokq6NKqN9aI7sReVV9P24El
+J/LxLvv9Nk7/8Jq6z1CjF0THtFg1mQYTWn52mrYwBr7aPpv/UDDFA0dmIancXwf0
+CHwFMMHFlYSBChy/vwx8+QiE60pLiz6EnajSq8lEQCf6nSFIvmkWn8y3IxmYRgr/
+CMn0BZc+VLgyzIJYG1Ygll10vXDdmjV4Y4f33stWr2Qse5Wp2Pyg/Lsfqw3C7+H1
+1BoxYJma2NmJ6sQu8xDNKPt8dsyOCYEyJqf8KGhi8eslRPuilKOreZ9b/KZh6OE2
+cNTSwA4Buvhbv4626sdq/BJxEi4Cxrhu3dMPCyeyl1540BOSMpThXdKa5pheJTs1
+78UDi87cJBZx8eQ5Gcg2VfwXkolc/dOvIRBEvnuIvA/3DaqH4gxMZ90UCFSzZ9Jm
+By83H0GVB0l8UbneSKtbr4wms5qjSUUzT7NTmqOdZwFmxJIbB0hVZtC1ve00WlkG
+qSmK2BXyG2bccsjeC/XOKO2WmZm9Gfuvtr6KcOnHFQYj08ZSfDPzJCuY7PK4/egc
+TR4wApehk6BzQwbz66uN2PlHIw==
+=2ZQG
+-----END PGP MESSAGE-----
+
+--------------090909060406090905060708
+Content-Type: application/octet-stream;
+ name="favicon.png.pgp"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="favicon.png.pgp"
+
+hQIOA076g5OuwfIOEAf/fVq/LJs3vrFnybm6thFjpDte1mawn7xw9op6UIFqEFRjkesreTn2
+b4vzYgi/8HUKJj6A/b9OBBobsOuHkKddZ+wRnTnMTc6ZGahJOFdOc3C84hEJYvyEgRqJ9hBh
+F6awMfPYHIfN3y6PpeJ8jbtWN2QxYvy9a9dsb7oUGi9FuBC7nI0qIqdxeLwjK4MkVGzDhdJU
+N4OCCe3LhYyD+Ev8Ote2NiWFdPnCkFiO6vwYI1cEyzltJXeOXYe/7YCKDb3FcTG9UszG2nDl
+jz7hIXF4NtYVQjIxxtRxPfxKgkzboXIoAD1enJpBkNtTdVS3jtdbFvSlypDhG3+mfMlUo/k4
+vwf/RF+30fL1kZjue/Jecd4oh6NcOokKRRScIwxTwittYPVYtR5W+swTRyF+YA5SLfygmmYb
+QHDjFX+cOx8u8WzmVgK4szOBgh921p6DWxMdD6aLK2wDT//ZVdRDqNV/0AbApexlDriCP/CH
+8BQxHKTCsentMtdVDf66Y/44K6HYceeU8iZT3pvQKXfAmetiMau7G/pN+BvkN2HXFhBPa8id
+dHYIpixvjk7Mg3PBzt6mJa4SOSf8vzProFx4UmnkexOQcRxpS0Zoep1mB16oWqm7tXEUjBJx
+39BwDg8e9sU4Yrri2WzUkIPU0pG3ub5sxbCTMSxMJnZkRk8ul8s9GMtWYIUCDgOxaIPydPr+
+7xAIAIu4kheO0n6eNJeng1XE/giRvZodwsO27kA00KIW8mi6WGLvehlmFoxntck1r2oCNVvH
+Oj94nfWsG2i1zvjQDvMp2Cnp3KTmhTrOss37dFayWVODF/Q9Kx+7WXnh9zCEjQe0eCd6PiKF
+fvb4zCU3ANAY8dTmqRcDDB9TK39nafpWHEfzhjClHrQhuQDh+yb4ayXAvUXkNLIYzAvuwvku
+9x46MPLHf/4VVQAplymRvsMy/Vj3R9triHoE9tD42EOWgbo525IwLQ590x/AZoZhkCOeffWv
+6fq29Z6om8TqFTHFGPb7I88s5ihlpvUgvXkAA7ZYdF6q06pDldWqVOCUm98H/0LZKiiURX1P
+30EAe7DpKi0bAUvHj2Qc5GCU+Hf1ASGJmeENFNMwY+eDwYA6wuBf0tN5zitvncUj5SBZjXjV
+o8Uz7JJ9/6BuUcfAa4/O2qTIFdctBI9bwzzzuXYF3LbYXfjB4WmoaR0nRh8kTlpgFij7WQlg
+l+C1G630ynSm4bVHSmD0J/kBDfNUF7Kyr/riPuqnyqf2wVffbLIJYe/axb4slACZ+F4FhfsZ
+0NLYL2+pvGW05sv+k/kEyhSQeQZwmu8X4iZdfFK6kOpkHeS7K/yFX7BYRD9nMnZ5Zwekh8l7
+eVno90OdJ4rVJkp4+c2N3kEPPLgrEVlaut9gybv553PSwSIBeo8TwJX0khartEsmJRh4fuQq
+Ni8JhsMDG5Mr8DMvmjhWy5Awp+uQ+neNcnk11wQ+OYDFw4JopRED0m+igEK0i/737ANzEhLN
+ssbKnPSivQCBo7Yzv9o3XUx+UU4c3E2bE3MQRGDgDmB1SS2h+wyLDWDoBHGXZ17Hxcb83huJ
+HxFt+5e2K8T6hL3Rwghtg1DHt9eVS05v35xomDn1zPR+EWTEEhvWrPEw00CGpsq3ub4vJajJ
+Aa+4ClaBivaZVKE4rC87cdLNkiBFbXR3ROvkzoSnrA7/ZUgupDLfagDQKAj91hC1hmF1nRix
+SOIYGGeM1IsKm806Ah58IOUWsE5vAzoFqCMB4kYNZLbXU1ccxZVTU6QnpE8DzHoDqOtyN5y3
+2nyRLH9jBwKfECR3YHH3NxRbbI317fhmU5pAvRWeHpRp1yzGrCf21lgx12Ot1EWxHuXrd23p
+0O2EiQAcwadItgkfKW+UwAxGsVX+vueqSBYmYViCTFWKGqMXmgXgirR4nLt7L20WqrXBFKBH
+ms+7e9LwLHStfAzilr1deHnriNfwz0b9UPAvCV3n6FN3uz2asBWZKjOxlEr2VmaQYLiiFWe0
+UhX685wevk7AOX2LZ7iq7SkVmMLSIMgOVI8vmt2PEw==
+--------------090909060406090905060708--
+
+
diff --git a/rt/t/data/gnupg/emails/19-signed-inline-plain-nested.txt b/rt/t/data/gnupg/emails/19-signed-inline-plain-nested.txt
new file mode 100644
index 000000000..eb763bdc1
--- /dev/null
+++ b/rt/t/data/gnupg/emails/19-signed-inline-plain-nested.txt
@@ -0,0 +1,34 @@
+Received: by anduril (Postfix, from userid 1000)
+ id 3EDA537F80; Mon, 13 Aug 2007 15:34:17 -0400 (EDT)
+Date: Mon, 13 Aug 2007 15:34:17 -0400
+To: rt-recipient@example.com
+Subject: Test Email ID:19
+Message-ID: <20070813193417.GD6392@mit.edu>
+MIME-Version: 1.0
+Content-Type: text/plain; charset=us-ascii; x-action=pgp-signed
+Content-Disposition: inline
+User-Agent: Mutt/1.5.13 (2006-08-11)
+From: ternus@mit.edu (Christian Ternus)
+
+-----BEGIN PGP SIGNED MESSAGE-----
+Hash: SHA1
+
+- -----BEGIN PGP SIGNED MESSAGE-----
+Hash: SHA1
+
+This is a test email with inline nested signature.
+ID:19
+- -----BEGIN PGP SIGNATURE-----
+Version: GnuPG v1.4.9 (Darwin)
+
+iEYEARECAAYFAkmETNkACgkQ0ygDXYSIHxv3ewCgijZQyL5vWIOfk+06XjqTXdrN
+VDcAnj13TCHvhas6rMtxcljNNGvPidw6
+=VMc6
+- -----END PGP SIGNATURE-----
+-----BEGIN PGP SIGNATURE-----
+Version: GnuPG v1.4.9 (Darwin)
+
+iEYEARECAAYFAkmETOIACgkQ0ygDXYSIHxvvvQCfZlRPNjt77jJ7ANxwOpkHkwCY
+wsIAn0PzLhCKhIcAm+hk8CpduzYcY0xW
+=Xy5t
+-----END PGP SIGNATURE-----
diff --git a/rt/t/data/gnupg/emails/2-signed-MIME-plain-with-attachment.txt b/rt/t/data/gnupg/emails/2-signed-MIME-plain-with-attachment.txt
new file mode 100755
index 000000000..851549c02
--- /dev/null
+++ b/rt/t/data/gnupg/emails/2-signed-MIME-plain-with-attachment.txt
@@ -0,0 +1,48 @@
+Received: by anduril (Postfix, from userid 1000)
+ id 74BF637F75; Mon, 13 Aug 2007 15:23:57 -0400 (EDT)
+Date: Mon, 13 Aug 2007 15:23:57 -0400
+To: rt-recipient@example.com
+Subject: Test Email ID:2
+Message-ID: <20070813192357.GB6392@mit.edu>
+MIME-Version: 1.0
+Content-Type: multipart/signed; micalg=pgp-sha1;
+ protocol="application/pgp-signature"; boundary="bKyqfOwhbdpXa4YI"
+Content-Disposition: inline
+User-Agent: Mutt/1.5.13 (2006-08-11)
+From: ternus@mit.edu (Christian Ternus)
+
+
+--bKyqfOwhbdpXa4YI
+Content-Type: multipart/mixed; boundary="DKU6Jbt7q3WqK7+M"
+Content-Disposition: inline
+
+
+--DKU6Jbt7q3WqK7+M
+Content-Type: text/plain; charset=us-ascii
+Content-Disposition: inline
+
+This is a test email with a text attachment.
+ID:2
+
+--DKU6Jbt7q3WqK7+M
+Content-Type: text/plain; charset=us-ascii
+Content-Disposition: attachment; filename=text-attachment
+
+This is a test attachment. The magic word is: zanzibar.
+
+--DKU6Jbt7q3WqK7+M--
+
+--bKyqfOwhbdpXa4YI
+Content-Type: application/pgp-signature; name="signature.asc"
+Content-Description: Digital signature
+Content-Disposition: inline
+
+-----BEGIN PGP SIGNATURE-----
+Version: GnuPG v1.4.6 (GNU/Linux)
+
+iD8DBQFGwK/N0ygDXYSIHxsRAlbuAJ4wxUVCNerg6dLm+w7llCj51YYbFACgvNJR
+ajbUy9MMkljajl6Of3IlqRA=
+=R6Gi
+-----END PGP SIGNATURE-----
+
+--bKyqfOwhbdpXa4YI--
diff --git a/rt/t/data/gnupg/emails/3-signed-MIME-plain-with-binary.txt b/rt/t/data/gnupg/emails/3-signed-MIME-plain-with-binary.txt
new file mode 100755
index 000000000..82ef7e9f3
--- /dev/null
+++ b/rt/t/data/gnupg/emails/3-signed-MIME-plain-with-binary.txt
@@ -0,0 +1,55 @@
+Received: by anduril (Postfix, from userid 1000)
+ id 9745537F7B; Mon, 13 Aug 2007 15:29:41 -0400 (EDT)
+Date: Mon, 13 Aug 2007 15:29:41 -0400
+To: rt-recipient@example.com
+Subject: Test Email ID:3
+Message-ID: <20070813192941.GC6392@mit.edu>
+MIME-Version: 1.0
+Content-Type: multipart/signed; micalg=pgp-sha1;
+ protocol="application/pgp-signature"; boundary="pY3vCvL1qV+PayAL"
+Content-Disposition: inline
+User-Agent: Mutt/1.5.13 (2006-08-11)
+From: ternus@mit.edu (Christian Ternus)
+
+
+--pY3vCvL1qV+PayAL
+Content-Type: multipart/mixed; boundary="at6+YcpfzWZg/htY"
+Content-Disposition: inline
+
+
+--at6+YcpfzWZg/htY
+Content-Type: text/plain; charset=us-ascii
+Content-Disposition: inline
+
+This is a test email with binary attachment and MIME signature.
+ID:3
+
+--at6+YcpfzWZg/htY
+Content-Type: image/png
+Content-Disposition: attachment; filename="favicon.png"
+Content-Transfer-Encoding: base64
+
+iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABmJLR0QAAAAAAAD5Q7t/AAAB
+BElEQVR42u1WWw6DMAwz0+5FbzbvZuZk2cfUritpea77wVIRIBQ7dhsBdIQkM8AMMJImyW6d
+BXweyJ7UAMnUvQFGwHp2bizIJfUTUHZO8j/k1pt8lntvchbdH8ndtqyS+Gj3fyVPAtZAkm3N
+ffCyi/chBIQQ3iqs3cQ0TZCERzbhngDocOS4z94wXTCmu2V45LuQW8hsSWpaP8v9sy+2IRZj
+ZTP5ububbp8Az4ly5W6QqJ33YwKSkIYbZVy5uNMFsOJGLaLTBMRC8Yy7bmR/OD8TUB00DvkW
+AcPSB7FIPoji0AGQBtU4jt+Fh1R6Dcc6B2Znv4HTHTiAJkfXv+ILFy5c8PACgtsiPj7qOgAA
+AAAASUVORK5CYII=
+
+--at6+YcpfzWZg/htY--
+
+--pY3vCvL1qV+PayAL
+Content-Type: application/pgp-signature; name="signature.asc"
+Content-Description: Digital signature
+Content-Disposition: inline
+
+-----BEGIN PGP SIGNATURE-----
+Version: GnuPG v1.4.6 (GNU/Linux)
+
+iD8DBQFGwLEl0ygDXYSIHxsRAuYxAKDQeRS40bRiW5jmrwHNsCDN67vu7wCfV0Pd
+7T/gCO7TrbuGaJ0BVsJnJsY=
+=Pjdg
+-----END PGP SIGNATURE-----
+
+--pY3vCvL1qV+PayAL--
diff --git a/rt/t/data/gnupg/emails/4-signed-inline-plain.txt b/rt/t/data/gnupg/emails/4-signed-inline-plain.txt
new file mode 100644
index 000000000..1dcecaedb
--- /dev/null
+++ b/rt/t/data/gnupg/emails/4-signed-inline-plain.txt
@@ -0,0 +1,24 @@
+Received: by anduril (Postfix, from userid 1000)
+ id 3EDA537F80; Mon, 13 Aug 2007 15:34:17 -0400 (EDT)
+Date: Mon, 13 Aug 2007 15:34:17 -0400
+To: rt-recipient@example.com
+Subject: Test Email ID:4
+Message-ID: <20070813193417.GD6392@mit.edu>
+MIME-Version: 1.0
+Content-Type: text/plain; charset=us-ascii; x-action=pgp-signed
+Content-Disposition: inline
+User-Agent: Mutt/1.5.13 (2006-08-11)
+From: ternus@mit.edu (Christian Ternus)
+
+-----BEGIN PGP SIGNED MESSAGE-----
+Hash: SHA1
+
+This is a test email with inline signature.
+ID:4
+-----BEGIN PGP SIGNATURE-----
+Version: GnuPG v1.4.6 (GNU/Linux)
+
+iD8DBQFGwLI50ygDXYSIHxsRAp40AJ9ErYdLH2SVRXtgRtx7n/FVFOmKDwCgl/0T
+BeRSaF4Xbi8uGhVIkmU+YCs=
+=e4u6
+-----END PGP SIGNATURE-----
diff --git a/rt/t/data/gnupg/emails/5-signed-inline-with-attachment.txt b/rt/t/data/gnupg/emails/5-signed-inline-with-attachment.txt
new file mode 100644
index 000000000..638f0fda5
--- /dev/null
+++ b/rt/t/data/gnupg/emails/5-signed-inline-with-attachment.txt
@@ -0,0 +1,48 @@
+Message-ID: <46BCDA81.3030308@mit.edu>
+Date: Fri, 10 Aug 2007 17:37:05 -0400
+From: Christian Ternus <ternus@mit.edu>
+User-Agent: Thunderbird 1.5.0.12 (X11/20070604)
+MIME-Version: 1.0
+To: rt-recipient@example.com
+Subject: Test Email ID:5
+X-Enigmail-Version: 0.94.2.0
+Content-Type: multipart/mixed;
+ boundary="------------010302000403000103080306"
+
+This is a multi-part message in MIME format.
+--------------010302000403000103080306
+Content-Type: text/plain; charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+
+-----BEGIN PGP SIGNED MESSAGE-----
+Hash: SHA1
+
+This is a test email with a text attachment and inline signature.
+ID:5
+-----BEGIN PGP SIGNATURE-----
+Version: GnuPG v1.4.6 (GNU/Linux)
+
+iD8DBQFGvNqA0ygDXYSIHxsRAuCHAKC0HnduLWqihY5wzGYDFGbFtA4chwCgr6+t
+mQo4oXIqu+kIZ0ExWyiUENs=
+=3ABp
+-----END PGP SIGNATURE-----
+
+--------------010302000403000103080306
+Content-Type: text/plain;
+ name="text-attachment"
+Content-Transfer-Encoding: 7bit
+Content-Disposition: inline;
+ filename="text-attachment"
+
+This is a test attachment. The magic word is: zanzibar.
+
+--------------010302000403000103080306
+Content-Type: application/octet-stream;
+ name="text-attachment.sig"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="text-attachment.sig"
+
+iD8DBQBGvNqB0ygDXYSIHxsRAmkbAJ0esGNEDMr01u45ZHIIKZpCFSE8tgCfXBedq0Yu5mnZ
+zOZyASZYUIf9wSE=
+--------------010302000403000103080306--
diff --git a/rt/t/data/gnupg/emails/6-signed-inline-with-binary.txt b/rt/t/data/gnupg/emails/6-signed-inline-with-binary.txt
new file mode 100644
index 000000000..2c725aa58
--- /dev/null
+++ b/rt/t/data/gnupg/emails/6-signed-inline-with-binary.txt
@@ -0,0 +1,55 @@
+Message-ID: <46BCDAE6.4090803@mit.edu>
+Date: Fri, 10 Aug 2007 17:38:46 -0400
+From: Christian Ternus <ternus@mit.edu>
+User-Agent: Thunderbird 1.5.0.12 (X11/20070604)
+MIME-Version: 1.0
+To: rt-recipient@example.com
+Subject: Test Email ID:6
+X-Enigmail-Version: 0.94.2.0
+Content-Type: multipart/signed; micalg=pgp-sha1;
+ protocol="application/pgp-signature";
+ boundary="------------enigAEEA002E4229CA8E5445ED73"
+
+This is an OpenPGP/MIME signed message (RFC 2440 and 3156)
+--------------enigAEEA002E4229CA8E5445ED73
+Content-Type: multipart/mixed;
+ boundary="------------000104020205010403010301"
+
+This is a multi-part message in MIME format.
+--------------000104020205010403010301
+Content-Type: text/plain; charset=ISO-8859-1
+Content-Transfer-Encoding: quoted-printable
+
+This is a email signed inline with a binary attachment.
+ID:6
+
+--------------000104020205010403010301
+Content-Type: image/png;
+ name="favicon.png"
+Content-Transfer-Encoding: base64
+Content-Disposition: inline;
+ filename="favicon.png"
+
+iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABmJLR0QAAAAAAAD5Q7t/AAAB
+BElEQVR42u1WWw6DMAwz0+5FbzbvZuZk2cfUritpea77wVIRIBQ7dhsBdIQkM8AMMJImyW6d
+BXweyJ7UAMnUvQFGwHp2bizIJfUTUHZO8j/k1pt8lntvchbdH8ndtqyS+Gj3fyVPAtZAkm3N
+ffCyi/chBIQQ3iqs3cQ0TZCERzbhngDocOS4z94wXTCmu2V45LuQW8hsSWpaP8v9sy+2IRZj
+ZTP5ububbp8Az4ly5W6QqJ33YwKSkIYbZVy5uNMFsOJGLaLTBMRC8Yy7bmR/OD8TUB00DvkW
+AcPSB7FIPoji0AGQBtU4jt+Fh1R6Dcc6B2Znv4HTHTiAJkfXv+ILFy5c8PACgtsiPj7qOgAA
+AAAASUVORK5CYII=
+--------------000104020205010403010301--
+
+--------------enigAEEA002E4229CA8E5445ED73
+Content-Type: application/pgp-signature; name="signature.asc"
+Content-Description: OpenPGP digital signature
+Content-Disposition: attachment; filename="signature.asc"
+
+-----BEGIN PGP SIGNATURE-----
+Version: GnuPG v1.4.6 (GNU/Linux)
+
+iD8DBQFGvNrm0ygDXYSIHxsRAvwSAKC4d3U6SjfhYpUHu2V/vXtgxGFa1QCfeK6p
+dyDDlvlqP9Ns4EExvHXfHuY=
+=sX3V
+-----END PGP SIGNATURE-----
+
+--------------enigAEEA002E4229CA8E5445ED73--
diff --git a/rt/t/data/gnupg/emails/7-encrypted-MIME-plain.txt b/rt/t/data/gnupg/emails/7-encrypted-MIME-plain.txt
new file mode 100644
index 000000000..11e3b7cda
--- /dev/null
+++ b/rt/t/data/gnupg/emails/7-encrypted-MIME-plain.txt
@@ -0,0 +1,47 @@
+Received: by anduril (Postfix, from userid 1000)
+ id CEA9137F51; Fri, 10 Aug 2007 15:27:49 -0400 (EDT)
+Date: Fri, 10 Aug 2007 15:27:49 -0400
+To: rt-recipient@example.com
+Subject: Test Email ID:7
+Message-ID: <20070810192749.GA5572@mit.edu>
+MIME-Version: 1.0
+Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";
+ boundary="YiEDa0DAkWCtVeE4"
+Content-Disposition: inline
+User-Agent: Mutt/1.5.13 (2006-08-11)
+From: ternus@mit.edu (Christian Ternus)
+
+
+--YiEDa0DAkWCtVeE4
+Content-Type: application/pgp-encrypted
+Content-Disposition: attachment
+
+Version: 1
+
+--YiEDa0DAkWCtVeE4
+Content-Type: application/octet-stream
+Content-Disposition: inline; filename="msg.asc"
+
+-----BEGIN PGP MESSAGE-----
+Version: GnuPG v1.4.6 (GNU/Linux)
+
+hQIOA076g5OuwfIOEAf9FtBMrkLWUKK5BtwuUsYXV9Mbe/YkkmK61MuysAtLcX/M
+DiXPzngjL62Dr9l88R3imf2kPmY36yx5WNeXUrFFVmFPaeZrHEJiMNvPGVVQCqRK
+uar2vsYRK9th4msZnn0hBYnA8+8kZ8rWefWHpszOcJ1YZpyyEMLf8Vnstyf0Pebp
+Wxixr99+mn3MVH38CrhoErI6CMiCFJgPAl5wtGWd0lT3+657dLJCsNI0cT3AY/JC
+IJwWD2sdOXOzDo7tdC3l7/YuGsXvd4jGu4A8PdoBMOgPx/N4KT3+uPhp2sRD3PMg
+LU59613xT8/FOYxQSib9hGqNZPqXS3ryC3ZvY4Sp8AgApCMocKsN7vm8N+6Yh7Nc
+Jjy/kuf8tjuTTs32Yk0ACU3y2SFXKSBZo6cVXgJhUvmG2Dq4O/A8mtP0cjqeBFqp
++vZOb7xhtxxTE6HWWThvx5qxcwjpijzDMS9uzfGaHwLvewdGVLODCup06MJmeAmj
+N1WEZqc/cqFZvZ9omCpcvTGoELpOzcUY1MxAq1IVkMzAk7dPIHYuyPSFQK6Y8IPl
+xsfSMcq9gjth8qautnriB0ohwkUebGnxgM4CjGjnSmLmUFXkndUOXKbM7R7E3QdL
++6TKMr2pvLl8U3OJrCiyyPVyVi3in4TYi4uegXJl05CAEjEXRf5RFhaWRnn66EYN
+WdLAAQHpkESfESVUaWvJwI+JB+LMBoKZjWgvIQ7AQKqLAvIsAqs9PKM4mYOMaawl
+en9XNRkW0dSGUxjW4K8u7fLS/xzWrZeCrafEkvCowVv/nR+Wxm296bxX+7z2R52/
+j+J0zms1fRdVxEs+rOI6JuXg4xWxUdLTav7cqvQ5c/izM+jU4yWEa3y0fHma6Jeh
+o4+1NerQby8Yzxszh9XVfkbYnQilhP8qCVxYe4HGjKlNi5v/xOgCznCKsqkGYMPU
+S32K6lg=
+=xeKr
+-----END PGP MESSAGE-----
+
+--YiEDa0DAkWCtVeE4--
diff --git a/rt/t/data/gnupg/emails/8-encrypted-MIME-with-attachment.txt b/rt/t/data/gnupg/emails/8-encrypted-MIME-with-attachment.txt
new file mode 100644
index 000000000..3781a62e1
--- /dev/null
+++ b/rt/t/data/gnupg/emails/8-encrypted-MIME-with-attachment.txt
@@ -0,0 +1,49 @@
+Received: by anduril (Postfix, from userid 1000)
+ id C962637F57; Fri, 10 Aug 2007 15:39:14 -0400 (EDT)
+Date: Fri, 10 Aug 2007 15:39:14 -0400
+To: rt-recipient@example.com
+Subject: Test Email ID:8
+Message-ID: <20070810193914.GC5572@mit.edu>
+MIME-Version: 1.0
+Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";
+ boundary="FFoLq8A0u+X9iRU8"
+Content-Disposition: inline
+User-Agent: Mutt/1.5.13 (2006-08-11)
+From: ternus@mit.edu (Christian Ternus)
+
+
+--FFoLq8A0u+X9iRU8
+Content-Type: application/pgp-encrypted
+Content-Disposition: attachment
+
+Version: 1
+
+--FFoLq8A0u+X9iRU8
+Content-Type: application/octet-stream
+Content-Disposition: inline; filename="msg.asc"
+
+-----BEGIN PGP MESSAGE-----
+Version: GnuPG v1.4.6 (GNU/Linux)
+
+hQIOA076g5OuwfIOEAgA4dZa3t55kB5pY4Q7+h4thTJVZ+wV2QxI5wZFRvMs7yw3
+MEIWqHusoe7R2zeXLxaT0gMSQj3osO+GrGJRDk4qCGjcjSneFmYKkELx8KRML59N
+X7/HeX7v8nSGpyqM5foaky38uAGxj2xBpswfvsY8qdQ37a4mNH2atESnkxYLomZb
+4vTRTSE1XtvhaG7oHaXxfmeNln0JezfbfpDGTktZJOupEX99j6/bvloimXyBT6SZ
+3wbcP/EYoqC8DQOQTtFTAisWwveTVp6FDFvi5L2BCJ2NzYZSDOBNeXrzf1AJ04dN
+9lTzbtejdE/AgwDclDnZYZByiGLM43X6nTk7c9gH1AgAlrxxvO1yz4sZchGM2rrn
+yzNJpcvcgbJLTt54gqie/BdQHSnvlCBLZzplx2xV9HIwJB7Kf29Ka2gg2p6mjyID
+YUd1451K1hDUmBHgya8jb6g2c6bBxiusUODlGfmv+c+kq/yG0tKws5d0/IGanL+Z
+h7XkcEYq3its0acCbcKizgzLSmxSqu6NK7rnDvioUOHHKG8uC9fk2JetdTdYOTBn
+AWYDxa3D+kRvOAiOWGMqtvOWAC1BZJ8hssBpesuG1+sucTh7W4ZROtQfEbqC8W51
+s6e17x1YUqE8QIs/aUYkUkX2JqObKsZPJAiVYCQkqJceIk/lT4rXNOfUwl5dAynk
+WNLAZQGKuVghSfSoRyKbFMRDimigisWN2JUudaV0Ld6E/5iO7EP7XhjqkzTlwsaN
+wui2n0Omxye2dNVMKz3q76fVp79XbznaI1ckeVjiiDmkiaQOp1Au/Sx2Bj0/wolJ
+OXnp27oqc9THy3RANLRXIQuRaYOyoxUIjVvhbOVfM3U7MlcAW3jT2kMTI7H83HWc
+ezxKfqM7nJUKkIVmRY4J/6X4uo5c8DdIxLeG+ioj+3I7BRkLfIAPYDGaeB5+BD1k
+EPcVGd3u1EW7D8f5CRARnU8aaC8DCPk5YYN6wM9JY9n0FEgIkwoniTpqHLs/UMa8
+DWGer1UCGp4qElmHvWHVV8b82nw6Ta2BhWKjA9pphf96KOTt3y4jqxfU4GbrOYDJ
+786/cggItsc0
+=qJrM
+-----END PGP MESSAGE-----
+
+--FFoLq8A0u+X9iRU8--
diff --git a/rt/t/data/gnupg/emails/9-encrypted-MIME-with-binary.txt b/rt/t/data/gnupg/emails/9-encrypted-MIME-with-binary.txt
new file mode 100644
index 000000000..cafc88077
--- /dev/null
+++ b/rt/t/data/gnupg/emails/9-encrypted-MIME-with-binary.txt
@@ -0,0 +1,57 @@
+Received: by anduril (Postfix, from userid 1000)
+ id 215C737F5D; Fri, 10 Aug 2007 15:46:27 -0400 (EDT)
+Date: Fri, 10 Aug 2007 15:46:27 -0400
+To: rt-recipient@example.com
+Subject: Test Email ID:9
+Message-ID: <20070810194627.GA5815@mit.edu>
+MIME-Version: 1.0
+Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";
+ boundary="GID0FwUMdk1T2AWN"
+Content-Disposition: inline
+User-Agent: Mutt/1.5.13 (2006-08-11)
+From: ternus@mit.edu (Christian Ternus)
+
+
+--GID0FwUMdk1T2AWN
+Content-Type: application/pgp-encrypted
+Content-Disposition: attachment
+
+Version: 1
+
+--GID0FwUMdk1T2AWN
+Content-Type: application/octet-stream
+Content-Disposition: inline; filename="msg.asc"
+
+-----BEGIN PGP MESSAGE-----
+Version: GnuPG v1.4.6 (GNU/Linux)
+
+hQIOA076g5OuwfIOEAgAqwVRwpeOIRMvrgswuYagJfk57HvSvrAg7tVzXHaHW5LI
+zJeWpKg0PVwmJUYTG9cZN0gkfpr33SULruvniZRSX650E/vv9H03P1dVYhBbcP01
+AyfPoaySinipoqG5OK7aTJVEnPZqqsCKgzFbqEg+FdF3YjNVa1etfmKV/JvoXTxR
+il6pOqv+xUJ66GpPUoP7mCZJ/cJEF1zz/EbZoYcS9MgxoP42bOIYBhyqIv3fUh7X
+fuDpup5xnJwM3FRjkNQPlqKhgGHCgk04OGgLUw8wzRwYQ5jSyij45J/UcGoNWePi
+eVTZignbYIfrlwa5GH6776U9QA9ei7PFP3RqO2CB2Af6AiIdkeN9MfPFfiQ/ASwR
+/zePg+OOmKC6locoUHWTEuBCZhgty7XarfPZufBHlA3Z3f369Pxz9nMV07KkO22t
+thhltpsxan2pFZL4oPfwA9OdDRGeWIISn4jGc+foNfKTmERY7EZv9ruDC0lxaSvs
+HEWJC38sJ3xGdlS439qaddSbm0Lft8JbNwi6FEpThE4abBdITu+BxbkoFrjIy6aC
+UJgyJ3YtPTpU8JuT49Ocn+51YPXZTSc/ePnvlzSqRXHeJrOKp5Oyoa9001242MRM
+mpaC1lw/sfGIIAXv5t2B569RXDsZ2jIvPjFIleIdyKOG17m8qsP82nWKbN2N96am
+p9LpAb5gqid4Hw1PjjjCB0A9w7G9orQeEQuF+2V9MGWAEWOHoVbGWTRjfh4uXASz
+4PlazbNDIZZB09ACaDfxG3Y3/TI2X0sl9KJNRALFtHm4+DNbWVeWe+aKrG9ZY9Os
+xjD/UQFFQrchpNPD7xV98WWEg4+s8pkk9Mwv+Q/gNf2xBXJXDOhIm2LlY0VlhETq
+fk4YS3EUS78Ti4V99w/L9PTnoaZY/8kkI6NNjb+bqldhd9AQHXEkpV5Gh/PrsgFn
+FzBZxEwcY8wKP2yKMTp+A3rsjHwM4OChjtkDShf4KDwGS5D+E4o1RJVcM3jfh2SG
+RRKge5ewErBmIDIqeU8Wpj5cuyqS77CIB3aplSKLqUu2bC2EiLFZxs2UTg+cp5WK
+WFzt+YmH6/p0y9eyaAGOleSh3dnIZHv2BDtzF9x5fHFesjaVp3jpmMsHQ1ol53TR
+8N4fUuO140oE0Gnci61EXzLjkGCSBhzZqy/+K8PR0d1hGv+vlwfadpzsPfX8WcxZ
+DBuI1E7OcqVaXemR90/C2AbLB8qGuwQ+wTIHPqXlm8s0+6wZ4YUcUoa/F+2VLSxN
+VWGji1yioKrSaBjlBBUpHfoC/Q/0hLk2FgIrAZFIPvhHl9nF3Vq/HqlxtGZQ7gnL
+TjQtRuI+oU7IhJRR1VZbhr8xn1pirueiiwJUPub6w4XfMGyvcvlIOVpUImW5Hab7
+kg20iwrcJNLCcPJsR5iEbhJBBHLsViMtfdRbstlEV8I4wTY2tNAkfMtUtWdUhwOA
+fdx/UmHjbNZFSS6cpsGHL8+QJ2Bo1urkHIz6z1w4f1vTL31NAPeyvZ4kNstSqkNt
+6fjdrt8mBRVGQfqRQsK2b/R61ErJoFULboeqOT/ed/Dufu2Wxf/wuuRvG77S8xoq
+OiDT8nhyHh7ljjdHZZ2uLro8gg==
+=BIke
+-----END PGP MESSAGE-----
+
+--GID0FwUMdk1T2AWN--
diff --git a/rt/t/data/gnupg/emails/README b/rt/t/data/gnupg/emails/README
new file mode 100644
index 000000000..634f53794
--- /dev/null
+++ b/rt/t/data/gnupg/emails/README
@@ -0,0 +1,28 @@
+Set of emails signed and/or encrypted using real MUAs(mutt, thunderbird).
+
+All messages have subejct 'Test Email ID:\d+'.
+ID matches number in name of the corresponding file.
+Top most entity of a message as well contains 'ID:\d+' text.
+
+Emails may have either text plain or binary attachment.
+Text of plain attachment is
+"This is a test attachment. The magic word is: zanzibar."
+
+Content of binary attachment is RT's favicon.png.
+
+Encrypted messages encrypted for rt-recipient@example.com.
+
+Messages signed using rt-test@example.com key.
+
+Name of a file may contain the following parts separated by dash:
+* signed
+* encrypted
+* MIME - RFC format
+* inline - inline format
+* with-attachment - text plain attachment
+* with-binary - binary attachment
+
+In total 18 emails using all possible combinations.
+18 = 3 (signed/encrypted/both) * 2 (MIME/inline)
+ * 3 (no/text/binary)
+
diff --git a/rt/t/data/gnupg/keyrings/pubring.gpg b/rt/t/data/gnupg/keyrings/pubring.gpg
new file mode 100644
index 000000000..f993bf2db
--- /dev/null
+++ b/rt/t/data/gnupg/keyrings/pubring.gpg
Binary files differ
diff --git a/rt/t/data/gnupg/keyrings/secring.gpg b/rt/t/data/gnupg/keyrings/secring.gpg
new file mode 100644
index 000000000..eda64ae94
--- /dev/null
+++ b/rt/t/data/gnupg/keyrings/secring.gpg
Binary files differ
diff --git a/rt/t/data/gnupg/keyrings/signed_old_style_with_attachment.eml b/rt/t/data/gnupg/keyrings/signed_old_style_with_attachment.eml
new file mode 100644
index 000000000..0bae0d4af
--- /dev/null
+++ b/rt/t/data/gnupg/keyrings/signed_old_style_with_attachment.eml
@@ -0,0 +1,48 @@
+Message-ID: <45A10003.1090705@bestpractical.com>
+Date: Sun, 07 Jan 2007 17:13:23 +0300
+From: "Ruslan U. Zakirov" <ruz@bestpractical.com>
+User-Agent: Thunderbird 1.5.0.9 (X11/20061221)
+MIME-Version: 1.0
+To: rt@example.com
+Subject: test
+X-Enigmail-Version: 0.94.1.0
+Content-Type: multipart/mixed;
+ boundary="------------030903040907010006050500"
+
+This is a multi-part message in MIME format.
+--------------030903040907010006050500
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 7bit
+
+-----BEGIN PGP SIGNED MESSAGE-----
+Hash: SHA1
+
+inline
+
+-----BEGIN PGP SIGNATURE-----
+Version: GnuPG v1.4.6 (GNU/Linux)
+Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org
+
+iD8DBQFFoQADtaRiGUNF96URAmHSAKCDdVnRJ2gb1idhE1ZXEg1JARalsQCgkaU8
+74cnNxVyp/0XwBA73qzkvx0=
+=UmxP
+-----END PGP SIGNATURE-----
+
+--------------030903040907010006050500
+Content-Type: text/plain;
+ name="test.txt"
+Content-Transfer-Encoding: base64
+Content-Disposition: inline;
+ filename="test.txt"
+
+YXR0YWNobWVudAo=
+--------------030903040907010006050500
+Content-Type: application/pgp-signature;
+ name="test.txt.sig"
+Content-Transfer-Encoding: base64
+Content-Disposition: inline;
+ filename="test.txt.sig"
+
+iD8DBQBFoQADtaRiGUNF96URAqmBAJ42zyr06nK6R4dNpZD5067DNDgjRwCgkR+SKgz7OdAq
+p11D7PQGCR1Wuvg=
+--------------030903040907010006050500--
diff --git a/rt/t/data/gnupg/keyrings/trustdb.gpg b/rt/t/data/gnupg/keyrings/trustdb.gpg
new file mode 100644
index 000000000..9f2ae63a2
--- /dev/null
+++ b/rt/t/data/gnupg/keyrings/trustdb.gpg
Binary files differ
diff --git a/rt/t/data/gnupg/keys/general-at-example.com.2.public.key b/rt/t/data/gnupg/keys/general-at-example.com.2.public.key
new file mode 100644
index 000000000..04fbe9666
--- /dev/null
+++ b/rt/t/data/gnupg/keys/general-at-example.com.2.public.key
@@ -0,0 +1,30 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: GnuPG v1.4.7 (Darwin)
+
+mQGiBEbgjJQRBACx8uMHcl9JlKCUU5yh0rBw636MXhKr7cH/+zBTeUVQyfs+J4fo
+HlI3fwJzZOxwXKTwmXsHSKxomeBJxPpFCnwHvn7xTYh6Wtis1Rm2Vr4lsw4gxrFj
+bpk1ISkHo4tO3dPTNx6Jhz+gYzoVUyfclz/byUmEbe2HJkBCQNmC9/lcuwCg477S
+siOdObqTKtQhDqXzFfOHKmsEAKVB8RImtLAO/HPY5+rxiVkfKjsmZovi0PfioGDI
+3o1jcSwq/RwWPZTNB0vnlEx1aD3zedUn1T7ZPnKoRttIv0xYlg9wYhX/xvOA9mKy
+G5aXSypiJiNwJSQfcChlyVHX3R88pURIzPiwWv2OBOvRagE8Bmgz0DFa0/AQfmvI
+vpUmA/4qZVzOJ5hC6fVP+ESKEC6fStvHAEZM3sQK5AnZsuUls3+tgfkD9T6ei5YF
+MXDLMGz3thue1M0QEu3IL7cLoTMalWQpjpyDuqXS8UAsd5cG2eP2iA6Uf2VoKQ11
+w6Q1FyMwTC9B0F8JijwdLF77ERSCtIG+TOA1EtH9HfTV5+BfHbQfZ2VuZXJhbCAy
+IDxnZW5lcmFsQGV4YW1wbGUuY29tPohgBBMRAgAgBQJG4IyUAhsDBgsJCAcDAgQV
+AggDBBYCAwECHgECF4AACgkQ32UfoGMsT1De7QCgu2Gws7EHllWWhJC9mHgDHC45
+Z8YAn2mBLO7ZBi5lptavTfQlR05RvTSKuQINBEbgjJQQCAD55mgtF494BvjhumUE
+Zu15W9QL99772EntAHnGd6tx6r2GlqWmIHrze/jguIdyXqVGEVWAosDAu08vF1EC
+DDJoCz/tkyjhXOT4MBKWL0Gpetk61Pi2Qv0GsxQSYG+FwKjTk+pE4qxnT932n3U8
+7KxUtgIxzH7Y/oFxgb0LGQxgokSPuq/E+jtglYxey9fzeFWjr2T77g5oWu9PXPCF
+jg+km3f9rQxxRgDuM4bPr5tWjhLTpNB0xKZMbx6YUVUP/DyK/DhqaMZ5Qt1vZ0QI
+/COSPy9Kq76wR1WW6E0SDFU49xAbtMm2MZgDh5uj/lFp95yDGmBSZzwxY0qy/BbN
+c4HDAAMFCADbiPty42xieGtEtQdvjqejEsE15Zhna+zf/4Fch/Ee9laOmtW9Nekg
+2ltUMHw+Nc5VseWIKtFqlnOZtcutO6yYgVH+GJ287gnXw3gK8zEcbQ2RUxYEdiuw
+ORuMqowmVlOCFxgPW6Vv5Em8E3kb1dEMx+Ec/SVmeXpOcGV5kS9PS9veO3ruoXqx
+6l3Gq3BU3xvDhJ3FRRlsUYtGOBuJl0/oXk/1TUjAuQinsxM+Nh4VljeI3eiT2Ygb
+ypdXjH6zmTDy7PlEy5RDYdFOsKW1sZXKWoY6y2TU4IO9JBWehS8Lhn3pMTv65FME
+6Ow9W5+hLTBYo9E/kUZXWvPlJgGA1439iEkEGBECAAkFAkbgjJQCGwwACgkQ32Uf
+oGMsT1BE+gCeOni/yGg3+IIPpIoTCVsf8jUcJXgAn3nsf/TTUli9XyftXRiJbua9
+CM0m
+=bGLz
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/rt/t/data/gnupg/keys/general-at-example.com.2.secret.key b/rt/t/data/gnupg/keys/general-at-example.com.2.secret.key
new file mode 100644
index 000000000..6a8510e58
--- /dev/null
+++ b/rt/t/data/gnupg/keys/general-at-example.com.2.secret.key
@@ -0,0 +1,33 @@
+-----BEGIN PGP PRIVATE KEY BLOCK-----
+Version: GnuPG v1.4.7 (Darwin)
+
+lQHhBEbgjJQRBACx8uMHcl9JlKCUU5yh0rBw636MXhKr7cH/+zBTeUVQyfs+J4fo
+HlI3fwJzZOxwXKTwmXsHSKxomeBJxPpFCnwHvn7xTYh6Wtis1Rm2Vr4lsw4gxrFj
+bpk1ISkHo4tO3dPTNx6Jhz+gYzoVUyfclz/byUmEbe2HJkBCQNmC9/lcuwCg477S
+siOdObqTKtQhDqXzFfOHKmsEAKVB8RImtLAO/HPY5+rxiVkfKjsmZovi0PfioGDI
+3o1jcSwq/RwWPZTNB0vnlEx1aD3zedUn1T7ZPnKoRttIv0xYlg9wYhX/xvOA9mKy
+G5aXSypiJiNwJSQfcChlyVHX3R88pURIzPiwWv2OBOvRagE8Bmgz0DFa0/AQfmvI
+vpUmA/4qZVzOJ5hC6fVP+ESKEC6fStvHAEZM3sQK5AnZsuUls3+tgfkD9T6ei5YF
+MXDLMGz3thue1M0QEu3IL7cLoTMalWQpjpyDuqXS8UAsd5cG2eP2iA6Uf2VoKQ11
+w6Q1FyMwTC9B0F8JijwdLF77ERSCtIG+TOA1EtH9HfTV5+BfHf4DAwJaviEcavOA
+YWAyNs+6FnvZb5JMpdZWxKrmidVFbPUx7sHk2vEW0UTzcnqnlxmoJ/tKeWwiNl1j
+ztCVFbQfZ2VuZXJhbCAyIDxnZW5lcmFsQGV4YW1wbGUuY29tPohgBBMRAgAgBQJG
+4IyUAhsDBgsJCAcDAgQVAggDBBYCAwECHgECF4AACgkQ32UfoGMsT1De7QCgu2Gw
+s7EHllWWhJC9mHgDHC45Z8YAn2mBLO7ZBi5lptavTfQlR05RvTSKnQJjBEbgjJQQ
+CAD55mgtF494BvjhumUEZu15W9QL99772EntAHnGd6tx6r2GlqWmIHrze/jguIdy
+XqVGEVWAosDAu08vF1ECDDJoCz/tkyjhXOT4MBKWL0Gpetk61Pi2Qv0GsxQSYG+F
+wKjTk+pE4qxnT932n3U87KxUtgIxzH7Y/oFxgb0LGQxgokSPuq/E+jtglYxey9fz
+eFWjr2T77g5oWu9PXPCFjg+km3f9rQxxRgDuM4bPr5tWjhLTpNB0xKZMbx6YUVUP
+/DyK/DhqaMZ5Qt1vZ0QI/COSPy9Kq76wR1WW6E0SDFU49xAbtMm2MZgDh5uj/lFp
+95yDGmBSZzwxY0qy/BbNc4HDAAMFCADbiPty42xieGtEtQdvjqejEsE15Zhna+zf
+/4Fch/Ee9laOmtW9Nekg2ltUMHw+Nc5VseWIKtFqlnOZtcutO6yYgVH+GJ287gnX
+w3gK8zEcbQ2RUxYEdiuwORuMqowmVlOCFxgPW6Vv5Em8E3kb1dEMx+Ec/SVmeXpO
+cGV5kS9PS9veO3ruoXqx6l3Gq3BU3xvDhJ3FRRlsUYtGOBuJl0/oXk/1TUjAuQin
+sxM+Nh4VljeI3eiT2YgbypdXjH6zmTDy7PlEy5RDYdFOsKW1sZXKWoY6y2TU4IO9
+JBWehS8Lhn3pMTv65FME6Ow9W5+hLTBYo9E/kUZXWvPlJgGA1439/gMDAlq+IRxq
+84BhYLhZWMP6gRz3MuuJr0YX10x+bx3/96Wkh505MRMLqefr0J8WgzjIJS1aIUqA
+nusWttcVsQZS3ZHIZk/tp5dq54CICn09Rl+UNySISQQYEQIACQUCRuCMlAIbDAAK
+CRDfZR+gYyxPUET6AJwNpkgxKEdjvIbdB7Y+IPgA1wyt+ACdGc0py11j1RLa2gn0
+1+nPnBIS7R8=
+=oSXs
+-----END PGP PRIVATE KEY BLOCK-----
diff --git a/rt/t/data/gnupg/keys/general-at-example.com.public.key b/rt/t/data/gnupg/keys/general-at-example.com.public.key
new file mode 100644
index 000000000..c274c8df9
--- /dev/null
+++ b/rt/t/data/gnupg/keys/general-at-example.com.public.key
@@ -0,0 +1,30 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: GnuPG v1.4.7 (Darwin)
+
+mQGiBEYrdhQRBACpTwAjSJchxV9rgWJj/4GUe92xZ2wWHVrkv7cELO5GD1ie8wtA
+nh57oXfcFhuSmtLTyT/C1Mbzo/tz4Sigf33bZlEMXusp0bLsSz1S/5mslBGRdApJ
+Dz5jETEcakpzWsHA5sHfv8HLn+o8WUDtlGZf+Edi0DqUKSiLRkjWMAdcJwCg7cN1
+IIhhFFAf9Lr3Ny7ngJDwn/sEAJoZVUmhBHo9TipR9lZY1si5U0hA8Yn4XghLp4z9
+0rm8dAgLSZwFI2/zoU5u9qjW0UAo8Sp2SO9F03wQpfUGnpQtea/HVNuwiZVU42bB
+E5gn5EIYrHYT8X7cd+ZpWVGYu2117uoJtRHwnfuh857ocs7M7xeo7IQUZArqeHOG
+i3hABACV9mqnZoPyCOtaBogdXtDlEbqDvYclONJTsSKAfPsNRjJi8lzvJL9ZhtS8
+YKIUvxFu+XX0UVXWoNnzte8Ip/0hwupJu9jIcBJpI9dVEK3H2tWr+NElzML/uch+
+VO7UUmk2H/hF8+a3wXkdEN45FnJCyqC0Kk59OcY3bJIrI56SZrQdZ2VuZXJhbCA8
+Z2VuZXJhbEBleGFtcGxlLmNvbT6IYAQTEQIAIAUCRit2FAIbAwYLCQgHAwIEFQII
+AwQWAgMBAh4BAheAAAoJEJ+mYsBt4i/CNxwAoMEYt4mJlH9rCoqc8kkaE2qJslG+
+AJ9plkUk5LWv7ncWcrUpMxVo5J3Eh7kCDQRGK3YwEAgAqxHhoyoViGJsImMKG2XQ
+wxHIBJc21zaLwG8MAn6xNXIZRDrjlgy0ItteGB0Hs1VqibE70fwZ9q5O/Ev32M/s
+fxtEgJMyfZzOVPMxY0AZu7sUdsfDiTqV/0FwKMjGM1aV2ulqaVZLJr4hpdxjgynf
+TUqtkJjYtUMAwMcsee/ORnyuSBGcaG3SjWyOyrI+oDm4e2QZ/jGmINP4b3PHJu9C
+JXq3dR/vJGlYxr9uApaXHjVksa5+uPA6iqjyCoRo/Sv+V3SANSOYJYI9tnKFHuLM
+YAqwOufWNnvHjb2zS0hCAJJ6p0CMcBhmaNieXcOgmt8MIJe/b5W4ItfgS/2D+yI/
+iwADBQf+ISTkh2pY2QExXtGmhjO1LSxB/sO5n0zJOkQvf/I5aOgWxXDP8dYcc9mp
+2sV5okB+S4Dol/ppGaxas+kisdwTNja12Er21Lrm6CGfyqp214EKZcM44KmIb1HA
+utLlujBJsWgf6LDggH4s785GKzwtA9NHfiWeWkXWGXrGFsFK6OWDcdSH0GcHus2F
+9FHLyOh5jpkd4Xczx5lEAn1wvUhQtRn9bBU7FLGUind352vbKegeFjUvhGCLp+YF
+IxSB2OXeVdzUbMjiUM0zJu3VNDsq9IP5EBvV3cwixuOF/XzYiDIPqezDDU9P5H2R
+d1IQBgMIPl9WXAFRkCIJUnsmvQOINIhJBBgRAgAJBQJGK3YwAhsMAAoJEJ+mYsBt
+4i/CEuwAn1QHO3umF9MHhpWQIBaacJUfBboqAKCYPpflpFvDNn0ioLA7Dw5qVSix
+5A==
+=CF0O
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/rt/t/data/gnupg/keys/general-at-example.com.secret.key b/rt/t/data/gnupg/keys/general-at-example.com.secret.key
new file mode 100644
index 000000000..36f9ba3e3
--- /dev/null
+++ b/rt/t/data/gnupg/keys/general-at-example.com.secret.key
@@ -0,0 +1,31 @@
+-----BEGIN PGP PRIVATE KEY BLOCK-----
+Version: GnuPG v1.4.7 (Darwin)
+
+lQG7BEYrdhQRBACpTwAjSJchxV9rgWJj/4GUe92xZ2wWHVrkv7cELO5GD1ie8wtA
+nh57oXfcFhuSmtLTyT/C1Mbzo/tz4Sigf33bZlEMXusp0bLsSz1S/5mslBGRdApJ
+Dz5jETEcakpzWsHA5sHfv8HLn+o8WUDtlGZf+Edi0DqUKSiLRkjWMAdcJwCg7cN1
+IIhhFFAf9Lr3Ny7ngJDwn/sEAJoZVUmhBHo9TipR9lZY1si5U0hA8Yn4XghLp4z9
+0rm8dAgLSZwFI2/zoU5u9qjW0UAo8Sp2SO9F03wQpfUGnpQtea/HVNuwiZVU42bB
+E5gn5EIYrHYT8X7cd+ZpWVGYu2117uoJtRHwnfuh857ocs7M7xeo7IQUZArqeHOG
+i3hABACV9mqnZoPyCOtaBogdXtDlEbqDvYclONJTsSKAfPsNRjJi8lzvJL9ZhtS8
+YKIUvxFu+XX0UVXWoNnzte8Ip/0hwupJu9jIcBJpI9dVEK3H2tWr+NElzML/uch+
+VO7UUmk2H/hF8+a3wXkdEN45FnJCyqC0Kk59OcY3bJIrI56SZgAAn2a5NRggeMB1
+vXJiR6c1NjYO2+hHCRC0HWdlbmVyYWwgPGdlbmVyYWxAZXhhbXBsZS5jb20+iGAE
+ExECACAFAkYrdhQCGwMGCwkIBwMCBBUCCAMEFgIDAQIeAQIXgAAKCRCfpmLAbeIv
+wjccAKDBGLeJiZR/awqKnPJJGhNqibJRvgCfaZZFJOS1r+53FnK1KTMVaOSdxIed
+Aj0ERit2MBAIAKsR4aMqFYhibCJjChtl0MMRyASXNtc2i8BvDAJ+sTVyGUQ645YM
+tCLbXhgdB7NVaomxO9H8GfauTvxL99jP7H8bRICTMn2czlTzMWNAGbu7FHbHw4k6
+lf9BcCjIxjNWldrpamlWSya+IaXcY4Mp301KrZCY2LVDAMDHLHnvzkZ8rkgRnGht
+0o1sjsqyPqA5uHtkGf4xpiDT+G9zxybvQiV6t3Uf7yRpWMa/bgKWlx41ZLGufrjw
+Ooqo8gqEaP0r/ld0gDUjmCWCPbZyhR7izGAKsDrn1jZ7x429s0tIQgCSeqdAjHAY
+ZmjYnl3DoJrfDCCXv2+VuCLX4Ev9g/siP4sAAwUH/iEk5IdqWNkBMV7RpoYztS0s
+Qf7DuZ9MyTpEL3/yOWjoFsVwz/HWHHPZqdrFeaJAfkuA6Jf6aRmsWrPpIrHcEzY2
+tdhK9tS65ughn8qqdteBCmXDOOCpiG9RwLrS5bowSbFoH+iw4IB+LO/ORis8LQPT
+R34lnlpF1hl6xhbBSujlg3HUh9BnB7rNhfRRy8joeY6ZHeF3M8eZRAJ9cL1IULUZ
+/WwVOxSxlIp3d+dr2ynoHhY1L4Rgi6fmBSMUgdjl3lXc1GzI4lDNMybt1TQ7KvSD
++RAb1d3MIsbjhf182IgyD6nsww1PT+R9kXdSEAYDCD5fVlwBUZAiCVJ7Jr0DiDQA
+AVQKdEMomLc49M9nDgc3mQkmkS1Ce0xI+BBEf0LhdiUYRU9mclN7HkCj3fl8EUCI
+SQQYEQIACQUCRit2MAIbDAAKCRCfpmLAbeIvwhLsAKCoZAIqS3Bp2WndQqZJvHBS
+u4f0VACgslT4IpJF1BdbMvA+oyvYVgv3g/c=
+=linj
+-----END PGP PRIVATE KEY BLOCK-----
diff --git a/rt/t/data/gnupg/keys/recipient-at-example.com.public.key b/rt/t/data/gnupg/keys/recipient-at-example.com.public.key
new file mode 100644
index 000000000..3d3032ffa
--- /dev/null
+++ b/rt/t/data/gnupg/keys/recipient-at-example.com.public.key
@@ -0,0 +1,30 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: GnuPG v1.4.7 (Darwin)
+
+mQGiBEWOOBARBAClfdK23heqW39sO3K0p+KtZkDxWhgpjjRfMSWQWwY++eDFhDr5
+BGG3zxsB2R8MOaHVKetuDjWjmfDi/LvDR8br8+eaLt94F4PBcLa+vVjboYyjvTFs
+2t3leyXjNd+mBTZmUcY6WLM0E+biIJdlDhVOv95n8VwvT10R0J/mGO7hTwCg20NC
+vJJ/fpWzPItFfKHnKg3gO3MD/RhG6CxmsZs+1bdzY07UwABQG8NhoR5Veqg2+uBr
+xiYjemtC+8fAtthEojJ334BE7qDuXEO7eq1R+JOtEj/Hx8gtgWQfDNZlgLA4NUSc
+aU3PthtXD4CnY5MsTrxjpD+bjTde6ziEJ3RHPQQSq2S1fKqo5Bf6H7GYWueRiSGS
+cNK2A/4/WAYFQbj9Jm7zgvPrLRRnk7RP3A4+ABaWEtGMRbpCaFEHd63gjYEH7P3i
+/O6y9kXsYr3SkDbhk6h1Cx8+4fjpZHAd4XbgabZhp5u7Nq3m/TIzQiXMYXrZGleB
+CGoQrERbM9mavEgOHGZEwO1I/JKSHdmAg/adTRAbG+AxZK79u7QhVGVzdCBVc2Vy
+IDxyZWNpcGllbnRAZXhhbXBsZS5jb20+iGAEExECACAFAkWOOBACGwMGCwkIBwMC
+BBUCCAMEFgIDAQIeAQIXgAAKCRBIVe2Ik+ud58m2AKCAO8NDxsU8m6ahhfwrPaQG
+O7MjQwCgtDn5aYx4G93pv5UKpGzkHKh2T1G5Ag0ERY44FBAIALAc3V9FsEK3pxdc
+ILYVjV85rrL58+hGcsgeNqF2TIgneJ3Dwi0zA82K1gchfvBT7Ab+A7WGi6E0rXnO
+s/rMoDV+5Qoac4M9hW1qb1seBt+0au72npIMSqF5V/4nZr4L21g6vE4/cgrLd3BK
+bA1hCDdIGeqG3Ljiy++RGnhIkiY+FGpNYYAI9bkXltC9BYtl5DBvMrpTqlDUfSzq
+rz3zZYaD30FUGbTTqfISU1RC4sY7aEmgFvB6vvia/s9XyldngPFwuTELCAG/JkFd
+ZvodkwlTdv/vIY0SFkHJSjmT2a797wuhxuaC17eWrQhfF0sxsZhJ1Ac3osrGQQvr
+8dZCd2cAAwUH/icHkxwmYKa7UPQZYexD4QsGS+rq7TbYzbSWcxz2l5J11/pHdD6m
+tdGbtEn5mQPjBnOI3GYwvtqbzG4WO5y5qssYoW22ZL9ov67QpJyKJILNLInWwiqx
+oRgB3Kgk5J5vDNw7CZLxrEvQNKE1gTEqfmQGUAiXipJ2VXbTWenPN6fDv5vdKesF
+dnDmk+jfjbL0/G8jUwt6vnQXMVZnIuxTMxs+4tTQfK1qh5iMdaC3wy15pg2wZoky
+OzjVEJywIQmqAA8lcvD0S/l4+JM6Epfx8gd9IjOVULUd2/TiPSvGM/gpJdop3bUc
+UmlWeF59Xx9uZPo7twDubKB6grMWOZwGgM2ISQQYEQIACQUCRY44FAIbDAAKCRBI
+Ve2Ik+ud5+EVAJ0TZHopcAmCANc26gPWWDfTaZovmQCcC5jl3o/yXvdaAhfbqKMr
+cts0+T8=
+=wrrX
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/rt/t/data/gnupg/keys/recipient-at-example.com.secret.key b/rt/t/data/gnupg/keys/recipient-at-example.com.secret.key
new file mode 100644
index 000000000..620a4327e
--- /dev/null
+++ b/rt/t/data/gnupg/keys/recipient-at-example.com.secret.key
@@ -0,0 +1,33 @@
+-----BEGIN PGP PRIVATE KEY BLOCK-----
+Version: GnuPG v1.4.7 (Darwin)
+
+lQHhBEWOOBARBAClfdK23heqW39sO3K0p+KtZkDxWhgpjjRfMSWQWwY++eDFhDr5
+BGG3zxsB2R8MOaHVKetuDjWjmfDi/LvDR8br8+eaLt94F4PBcLa+vVjboYyjvTFs
+2t3leyXjNd+mBTZmUcY6WLM0E+biIJdlDhVOv95n8VwvT10R0J/mGO7hTwCg20NC
+vJJ/fpWzPItFfKHnKg3gO3MD/RhG6CxmsZs+1bdzY07UwABQG8NhoR5Veqg2+uBr
+xiYjemtC+8fAtthEojJ334BE7qDuXEO7eq1R+JOtEj/Hx8gtgWQfDNZlgLA4NUSc
+aU3PthtXD4CnY5MsTrxjpD+bjTde6ziEJ3RHPQQSq2S1fKqo5Bf6H7GYWueRiSGS
+cNK2A/4/WAYFQbj9Jm7zgvPrLRRnk7RP3A4+ABaWEtGMRbpCaFEHd63gjYEH7P3i
+/O6y9kXsYr3SkDbhk6h1Cx8+4fjpZHAd4XbgabZhp5u7Nq3m/TIzQiXMYXrZGleB
+CGoQrERbM9mavEgOHGZEwO1I/JKSHdmAg/adTRAbG+AxZK79u/4DAwKKjpV+74Fq
+wGCggxQNXbWP1fy2s8C5f3k/jsyGcPyDv2qcVoo9f1cLdb6Alu3f6kSAGWpUink9
+dHErTbQhVGVzdCBVc2VyIDxyZWNpcGllbnRAZXhhbXBsZS5jb20+iGAEExECACAF
+AkWOOBACGwMGCwkIBwMCBBUCCAMEFgIDAQIeAQIXgAAKCRBIVe2Ik+ud58m2AKCA
+O8NDxsU8m6ahhfwrPaQGO7MjQwCgtDn5aYx4G93pv5UKpGzkHKh2T1GdAmMERY44
+FBAIALAc3V9FsEK3pxdcILYVjV85rrL58+hGcsgeNqF2TIgneJ3Dwi0zA82K1gch
+fvBT7Ab+A7WGi6E0rXnOs/rMoDV+5Qoac4M9hW1qb1seBt+0au72npIMSqF5V/4n
+Zr4L21g6vE4/cgrLd3BKbA1hCDdIGeqG3Ljiy++RGnhIkiY+FGpNYYAI9bkXltC9
+BYtl5DBvMrpTqlDUfSzqrz3zZYaD30FUGbTTqfISU1RC4sY7aEmgFvB6vvia/s9X
+yldngPFwuTELCAG/JkFdZvodkwlTdv/vIY0SFkHJSjmT2a797wuhxuaC17eWrQhf
+F0sxsZhJ1Ac3osrGQQvr8dZCd2cAAwUH/icHkxwmYKa7UPQZYexD4QsGS+rq7TbY
+zbSWcxz2l5J11/pHdD6mtdGbtEn5mQPjBnOI3GYwvtqbzG4WO5y5qssYoW22ZL9o
+v67QpJyKJILNLInWwiqxoRgB3Kgk5J5vDNw7CZLxrEvQNKE1gTEqfmQGUAiXipJ2
+VXbTWenPN6fDv5vdKesFdnDmk+jfjbL0/G8jUwt6vnQXMVZnIuxTMxs+4tTQfK1q
+h5iMdaC3wy15pg2wZokyOzjVEJywIQmqAA8lcvD0S/l4+JM6Epfx8gd9IjOVULUd
+2/TiPSvGM/gpJdop3bUcUmlWeF59Xx9uZPo7twDubKB6grMWOZwGgM3+AwMCio6V
+fu+BasBgNB3AutvANzxcWBPZSJFs3uN0m4ii+g7eu8fYaiIJO7GFk1WD5UORHs9w
+0C4JIqUj10yUzvnHBZ6DdkV2aZE9FZljLLpRs+iiwYhJBBgRAgAJBQJFjjgUAhsM
+AAoJEEhV7YiT653n4RUAoJd1DuGANpd7VGGwNfoMCCpAk/4TAKCrbh6NpoJr0Ab4
+1toDA5lM/EYYPw==
+=5O4y
+-----END PGP PRIVATE KEY BLOCK-----
diff --git a/rt/t/data/gnupg/keys/rt-recipient-at-example.com.public.key b/rt/t/data/gnupg/keys/rt-recipient-at-example.com.public.key
new file mode 100644
index 000000000..9952c6f99
--- /dev/null
+++ b/rt/t/data/gnupg/keys/rt-recipient-at-example.com.public.key
@@ -0,0 +1,30 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: GnuPG v1.4.7 (GNU/Linux)
+
+mQGiBEa8u6QRBADCqPh8w3cO51hPVb1Sttqq5UhCeB5t2dAL8aVEDkpPfV7LItDi
+pN4VqHo2zbGE8q2bCoqW06Ogn0R4xsxEeD9Jq9/k3dHReFL2gbA5F/el1PKXVxG8
+62BnjLkDub8yCdWsg0QDJ6ah7LC7vukTMlJj+3HhoXWEqBrTBKjtFkNIrwCg/LtU
+CEyj+z/cl6NQGZUw2A6+5DUD/2DfcLeSir7xrlcidqO4BxtxdWkEBDAnmARKrqaw
+zSATIK11+HO3Gteovfa08J1XXU2+IFqi2Ssyaqss1kteJE8DmOAcllSXqmCfOmPm
+xoW4gXOQfEv6tkTvF9JST1OZRj5w+ecyxn0282XrzKcxNeLjc+JcLfzPmmuhw4lA
+s/nJA/4tBqT0V7QiwaznBo8Bh7N3sz75x0vgSdZLUA0e2VzHKh9mAfK/FeVS1mcJ
+04iHWvxOGMqEfXnpxUrogME7f/TWNBVfT4M2JW0sHLvaiJhTtIhn+Q67awQ1f0qG
+mGQLIo9OAWZnIfBZ8e2tBwJ3ajiSZ2LIPWFv4Q1hKxOclODpf7QmUlQgVXNlciBC
+b2IgPHJ0LXJlY2lwaWVudEBleGFtcGxlLmNvbT6IYAQTEQIAIAUCRry7pAIbAwYL
+CQgHAwIEFQIIAwQWAgMBAh4BAheAAAoJEM4wxB7CJ0omPPUAoL3A7jiEZX6xSyXE
+duAtnmMplqHrAKDB8mrNols8/ni0VOv0QletwEwbVrkCDQRGvLuvEAgA/qDyDeDP
+FrDhh757tpgvJp2CmIx8fyv+i9nLEBVCZjtkLqgrcvtNh8l+xu3y8vjGB6+ToPvG
+ZE3FRxyLWNPGIlq1pQSREC9faEDWDrN7yA8miaikLIlfMnGwwzb5bEXWsmXzctTv
+DgxTCufDj8T66TKv+cCqc9T956XY6q49Z/p6yZDiY7LZ0N6GkHSoT8o6ZCOvl87n
+IjwKR8AXDWBxL5+SeenNkZ8e30pSVDJTOe4u6W/MKK3RBD0FKYr+DOMh5BQtE7yT
+QEhzmDTPfGe9m52FV8FbSLpimMnIFM2hGRf6jynoR10s0tk2DVADXDycwNYarRYG
+AxV6XafLCPDv4wADBQgAtpM7zhVch/NsL56aIG0QZmSaKCdk6UPsJua91eLEHJFo
+zOzethsAWED5KHD5ThsYBKPGq+mFz7QQtw8/DBmcajtBxMv2fvVOE7SrWfeHyMVl
+RgidJc3O6HlPPnA/v8lQhsYTxpUddYqB4lC0ktpncxCzX/VNr62YkmrpJx2Yvyd0
+L/lK5fiko65gQC1v/XQ/QI9kpGbOFXFnEgQXmFcDTX4kzTgpJ3cOBrM9GAO/hcwH
+82eC0j8fYw8mLYR8yQG0jsXJKCvHxTgkOh0nSkLaeLoq1maLp+NbJKCqgpsmeV4n
+QmEJE4Ye7I/L077BtJLv1tk0G0Jh3F2WeSzEvB7cS4hJBBgRAgAJBQJGvLuvAhsM
+AAoJEM4wxB7CJ0om/FcAn2tCGofP7IPmw6VxGBZNPHal4sIBAJ9UCgpOaGtX2fRl
++vvcvfcuIys27g==
+=mo7N
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/rt/t/data/gnupg/keys/rt-recipient-at-example.com.secret.key b/rt/t/data/gnupg/keys/rt-recipient-at-example.com.secret.key
new file mode 100644
index 000000000..853801fd6
--- /dev/null
+++ b/rt/t/data/gnupg/keys/rt-recipient-at-example.com.secret.key
@@ -0,0 +1,33 @@
+-----BEGIN PGP PRIVATE KEY BLOCK-----
+Version: GnuPG v1.4.7 (GNU/Linux)
+
+lQHhBEa8u6QRBADCqPh8w3cO51hPVb1Sttqq5UhCeB5t2dAL8aVEDkpPfV7LItDi
+pN4VqHo2zbGE8q2bCoqW06Ogn0R4xsxEeD9Jq9/k3dHReFL2gbA5F/el1PKXVxG8
+62BnjLkDub8yCdWsg0QDJ6ah7LC7vukTMlJj+3HhoXWEqBrTBKjtFkNIrwCg/LtU
+CEyj+z/cl6NQGZUw2A6+5DUD/2DfcLeSir7xrlcidqO4BxtxdWkEBDAnmARKrqaw
+zSATIK11+HO3Gteovfa08J1XXU2+IFqi2Ssyaqss1kteJE8DmOAcllSXqmCfOmPm
+xoW4gXOQfEv6tkTvF9JST1OZRj5w+ecyxn0282XrzKcxNeLjc+JcLfzPmmuhw4lA
+s/nJA/4tBqT0V7QiwaznBo8Bh7N3sz75x0vgSdZLUA0e2VzHKh9mAfK/FeVS1mcJ
+04iHWvxOGMqEfXnpxUrogME7f/TWNBVfT4M2JW0sHLvaiJhTtIhn+Q67awQ1f0qG
+mGQLIo9OAWZnIfBZ8e2tBwJ3ajiSZ2LIPWFv4Q1hKxOclODpf/4DAwIc+jyN96r4
+cWCKJH3rJKMiam7fzkjUhawkIXBXWlau1oZeQKvQCxCpj02aFks9bSTK9wseazjU
+JRF6D7QmUlQgVXNlciBCb2IgPHJ0LXJlY2lwaWVudEBleGFtcGxlLmNvbT6IYAQT
+EQIAIAUCRry7pAIbAwYLCQgHAwIEFQIIAwQWAgMBAh4BAheAAAoJEM4wxB7CJ0om
+PPUAoL3A7jiEZX6xSyXEduAtnmMplqHrAKDB8mrNols8/ni0VOv0QletwEwbVp0C
+YwRGvLuvEAgA/qDyDeDPFrDhh757tpgvJp2CmIx8fyv+i9nLEBVCZjtkLqgrcvtN
+h8l+xu3y8vjGB6+ToPvGZE3FRxyLWNPGIlq1pQSREC9faEDWDrN7yA8miaikLIlf
+MnGwwzb5bEXWsmXzctTvDgxTCufDj8T66TKv+cCqc9T956XY6q49Z/p6yZDiY7LZ
+0N6GkHSoT8o6ZCOvl87nIjwKR8AXDWBxL5+SeenNkZ8e30pSVDJTOe4u6W/MKK3R
+BD0FKYr+DOMh5BQtE7yTQEhzmDTPfGe9m52FV8FbSLpimMnIFM2hGRf6jynoR10s
+0tk2DVADXDycwNYarRYGAxV6XafLCPDv4wADBQgAtpM7zhVch/NsL56aIG0QZmSa
+KCdk6UPsJua91eLEHJFozOzethsAWED5KHD5ThsYBKPGq+mFz7QQtw8/DBmcajtB
+xMv2fvVOE7SrWfeHyMVlRgidJc3O6HlPPnA/v8lQhsYTxpUddYqB4lC0ktpncxCz
+X/VNr62YkmrpJx2Yvyd0L/lK5fiko65gQC1v/XQ/QI9kpGbOFXFnEgQXmFcDTX4k
+zTgpJ3cOBrM9GAO/hcwH82eC0j8fYw8mLYR8yQG0jsXJKCvHxTgkOh0nSkLaeLoq
+1maLp+NbJKCqgpsmeV4nQmEJE4Ye7I/L077BtJLv1tk0G0Jh3F2WeSzEvB7cS/4D
+AwIc+jyN96r4cWBgxgN9v8Z6ySQrlQfJWkB3LDcYVugGb3Ht6vIMaFMnW9KFwOgd
+/nuf5uqyuy2/jQ0SZT1fUC3skhxT1BXKewfmmlnzoLJu3rz6iEkEGBECAAkFAka8
+u68CGwwACgkQzjDEHsInSib8VwCeLdgy5axSbYZ8Ez42Kcj8Ku2Q4ZUAnjJ92jFc
+fC7XWcM/6AX5IyU0jtEW
+=DS2U
+-----END PGP PRIVATE KEY BLOCK-----
diff --git a/rt/t/data/gnupg/keys/rt-test-at-example.com.2.public.key b/rt/t/data/gnupg/keys/rt-test-at-example.com.2.public.key
new file mode 100644
index 000000000..d63d4dd48
--- /dev/null
+++ b/rt/t/data/gnupg/keys/rt-test-at-example.com.2.public.key
@@ -0,0 +1,30 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: GnuPG v1.4.7 (GNU/Linux)
+
+mQGiBEbl4D0RBADu+s2KTTSMl2/aK3Jkhy9ZTBMFOOCPeleidjV8z7RVGEwTjcby
+B0DbPFC/eK0ot9m/F9CojE6QHK0hqjCKPfARptjG6C/Iqxql0DaRWdo4UYTgT6WW
+hhoKK5DUN57Eu0essy1qTyzcXVIRsQdfkn2ldRKC1XSXnKAiL0vODLtL7wCgoDgj
+tDtOHdi0vlSHvRhPD1F9P3sEAMvSaiEMN/3AlAWQLqrg0rQRr4dpRZqahoffBIeX
+OZGzDSrWtIshMQLLA0HmkphPtRe/y74GBWfpwr6Hs6yl5tP2PSXsAAl+W92st2Vp
+lKJWsLZtvW39nS5cwmv0Etz6j0F9tn7Ah4+x89egzIg9GwU14cS2GNqxYsK3+YMY
+jSXzA/4zEDOQkrRuSEm9JNG5JCFKexAvjLzhYQQRCOI1PrX3iAMzbYFFIgTpr26h
+sPfOb5SMy2OGeECXGd0rxF4+rMCbp0jrQ8B18CWuho7HJK97WuT6NFoaPZCh/pYK
+OQkKGnJCUQNSm5u4uWY6yNs/+U4kvYJvIGw7F7uNWuXcpfREabQmUlQgVGVzdCB0
+aGUgc2FtZSA8cnQtdGVzdEBleGFtcGxlLmNvbT6IYAQTEQIAIAUCRuXgPQIbAwYL
+CQgHAwIEFQIIAwQWAgMBAh4BAheAAAoJEMeYWRqoMdv7v1oAnR9bV/4nbzizkEAm
+691AuqGLFyryAJ9WjLWviRXuiEKMR6LMIn9HC0POarkCDQRG5eA9EAgAoexx9eOo
+hJpX0VJ8gNVSlFsLLq60ugiWzfwzrGkL2v4o5QoCwj6XFhK/xtDmqhWu3USBVtqW
+vrMuq2VCWWPiezZ/8Zl8NY7GjxDyLgeuEotfBkS8qFLQH7TGqNFLoJOIi3UjNFX8
+cRx29CSQyc3jj88HiC1InuMMwDXf8ukpkYNG0n7E3lZ3dWOadCXY2+kAxJ5qGV0U
+WmEBoux9TU8hFAq4DtRmf2x/Mt3k+e5ZXpbnbKGxAAImMV8WR0SGAd5OcgeE+rB3
+ziAtD8YbM+WDxbihloje7YN0hVSFSTsGvypIC/jPSNTVgkJdG7N0NepGh/T3wbAV
+eDebMYDmUQYLiwADBQgAmrAkyor1V/M21ERoZdoNFMlbdxuuQ5QhBIkkiygn2dq1
+d7CaOF1Hi5s3o6DsD9ER2YENlPYgPtVPH/agGuITHHugGKitj9sRM/QCGMKf/IKa
+zxcTQfcHmOUMXrM6GWzPZiiTrIBpOZyRDyFN1x0y9G9z7lVt507AkyF3aS2da6yc
+l2knSX3vPtFkHCUVlvKz6yXGE4xLm8/ACH/zPDjuQ2X/Yz+FfpJAeNihz6/J88Ga
+O7sgU1jx3zdRaH5hZXJRlyO7AW1KdOtI8xZi3SBNXKBnfCbUUSBbxa2spXst0Eep
+ijEDtsDhK+imdFjX8ul/JE0XhJooRrcdevEbohMn2IhJBBgRAgAJBQJG5eA9AhsM
+AAoJEMeYWRqoMdv7OI4AnR5PPGNNdhVSTle3B9cV2Vy11gEjAJ0QHe4iFf7wfASF
+jybMsNKYwFb5FQ==
+=XFiw
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/rt/t/data/gnupg/keys/rt-test-at-example.com.2.secret.key b/rt/t/data/gnupg/keys/rt-test-at-example.com.2.secret.key
new file mode 100644
index 000000000..83207f100
--- /dev/null
+++ b/rt/t/data/gnupg/keys/rt-test-at-example.com.2.secret.key
@@ -0,0 +1,33 @@
+-----BEGIN PGP PRIVATE KEY BLOCK-----
+Version: GnuPG v1.4.7 (GNU/Linux)
+
+lQHhBEbl4D0RBADu+s2KTTSMl2/aK3Jkhy9ZTBMFOOCPeleidjV8z7RVGEwTjcby
+B0DbPFC/eK0ot9m/F9CojE6QHK0hqjCKPfARptjG6C/Iqxql0DaRWdo4UYTgT6WW
+hhoKK5DUN57Eu0essy1qTyzcXVIRsQdfkn2ldRKC1XSXnKAiL0vODLtL7wCgoDgj
+tDtOHdi0vlSHvRhPD1F9P3sEAMvSaiEMN/3AlAWQLqrg0rQRr4dpRZqahoffBIeX
+OZGzDSrWtIshMQLLA0HmkphPtRe/y74GBWfpwr6Hs6yl5tP2PSXsAAl+W92st2Vp
+lKJWsLZtvW39nS5cwmv0Etz6j0F9tn7Ah4+x89egzIg9GwU14cS2GNqxYsK3+YMY
+jSXzA/4zEDOQkrRuSEm9JNG5JCFKexAvjLzhYQQRCOI1PrX3iAMzbYFFIgTpr26h
+sPfOb5SMy2OGeECXGd0rxF4+rMCbp0jrQ8B18CWuho7HJK97WuT6NFoaPZCh/pYK
+OQkKGnJCUQNSm5u4uWY6yNs/+U4kvYJvIGw7F7uNWuXcpfREaf4DAwJXViZBji9w
+O2Bhhip5V1QOR7FbE8SLAJVVPoX4Lv8iXOMm4jaXAqTPkvWmWZDiUKfDf6Dv5wT1
+aj8N8LQmUlQgVGVzdCB0aGUgc2FtZSA8cnQtdGVzdEBleGFtcGxlLmNvbT6IYAQT
+EQIAIAUCRuXgPQIbAwYLCQgHAwIEFQIIAwQWAgMBAh4BAheAAAoJEMeYWRqoMdv7
+v1oAnR9bV/4nbzizkEAm691AuqGLFyryAJ9WjLWviRXuiEKMR6LMIn9HC0POap0C
+YwRG5eA9EAgAoexx9eOohJpX0VJ8gNVSlFsLLq60ugiWzfwzrGkL2v4o5QoCwj6X
+FhK/xtDmqhWu3USBVtqWvrMuq2VCWWPiezZ/8Zl8NY7GjxDyLgeuEotfBkS8qFLQ
+H7TGqNFLoJOIi3UjNFX8cRx29CSQyc3jj88HiC1InuMMwDXf8ukpkYNG0n7E3lZ3
+dWOadCXY2+kAxJ5qGV0UWmEBoux9TU8hFAq4DtRmf2x/Mt3k+e5ZXpbnbKGxAAIm
+MV8WR0SGAd5OcgeE+rB3ziAtD8YbM+WDxbihloje7YN0hVSFSTsGvypIC/jPSNTV
+gkJdG7N0NepGh/T3wbAVeDebMYDmUQYLiwADBQgAmrAkyor1V/M21ERoZdoNFMlb
+dxuuQ5QhBIkkiygn2dq1d7CaOF1Hi5s3o6DsD9ER2YENlPYgPtVPH/agGuITHHug
+GKitj9sRM/QCGMKf/IKazxcTQfcHmOUMXrM6GWzPZiiTrIBpOZyRDyFN1x0y9G9z
+7lVt507AkyF3aS2da6ycl2knSX3vPtFkHCUVlvKz6yXGE4xLm8/ACH/zPDjuQ2X/
+Yz+FfpJAeNihz6/J88GaO7sgU1jx3zdRaH5hZXJRlyO7AW1KdOtI8xZi3SBNXKBn
+fCbUUSBbxa2spXst0EepijEDtsDhK+imdFjX8ul/JE0XhJooRrcdevEbohMn2P4D
+AwJXViZBji9wO2DcFJ5JiQd3X934uG+AeTFIWQG75qOWS3j8BpRn50H73tUn9dxB
+Z+V9tI7sMFW3cQTnt6c+FSNJBTUAfLTExQO0BqPSrVwVmKImiEkEGBECAAkFAkbl
+4D0CGwwACgkQx5hZGqgx2/s4jgCfeukl8vl9mMuariu08MsuywQ77y4An1cIwl1x
+997LwJrR5WF/WoGvPQ61
+=E3hO
+-----END PGP PRIVATE KEY BLOCK-----
diff --git a/rt/t/data/gnupg/keys/rt-test-at-example.com.public.key b/rt/t/data/gnupg/keys/rt-test-at-example.com.public.key
new file mode 100644
index 000000000..db359a6eb
--- /dev/null
+++ b/rt/t/data/gnupg/keys/rt-test-at-example.com.public.key
@@ -0,0 +1,30 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: GnuPG v1.4.7 (GNU/Linux)
+
+mQGiBEa4o04RBADc+2sFcnuTTaqcKRTmSBQKdXvumT4GoATk194UYMghwprrNb1/
+flXQRk9zLkc0YENFHLMoRUmXKEF+WFxzXrZgHJS096tGn+Ud2FXQbSL47Vl3EHng
+c+jSvvVaZRcEySaCyQrsDR7gWlQtCbxbe96Y2x9jX3Zbih9UYnRvWBeczwCg4tgz
+EOmScnWiwUdyZNQsvXDqvKUD/REf0WjWDaykQvXYZ0aTpc/WMBsDS16nl8GNz5eD
+lCB/JJHKh5QDu89p0557AbVDSi5LCOYAM+v4oi8k5zgiO/7HJptirDkZ27Ichyes
+kzhu3Xr9rPLawie/o4FCfncNLbOAEE4EjEGDGRlyowAaXlW7DWT+TLbxY0qL0uHy
+AQPGA/9AmYHBJQqHTfQ4/QXdCnp+UwYs+rhPh7YHymBLn8Saa14heE9SZcYfSerL
+FAE7KKeBx96+RplgsiaqfWrliUwrV3KnnJICMyqWmn2OyMYiV9iFWqAHFTCsitS2
+q1COv5/Lg1a+XkAwEfoIuLrAXT8buIxXs/BhLc1PD1t9My8srbQdUlQgVGVzdCA8
+cnQtdGVzdEBleGFtcGxlLmNvbT6IYAQTEQIAIAUCRrijTgIbAwYLCQgHAwIEFQII
+AwQWAgMBAh4BAheAAAoJENMoA12EiB8bMZoAoIxLNWQ9d9+W4ImPMpUmjLl9ttxW
+AJ9ELlhkfdhPukRe508p5fZqKUfl/rkCDQRGuKNhEAgAjSKFedFcU9RjLmSEGo7E
+4qMQWOtiNooW4NtRsKC2cbJXnGJUOT+GBzGCxjBZt89T6MVsOy7DoAzs+xWKA5Cf
+gFEX5xZWM1c6EA4f7LhC1hawtGQkMQIyHzEy9b7NPEcMlkdOebjjhZ4Ob8svGily
+Q9jN6zpR2c36i0sLaZ5gORIHJ9DOX1k5lUzEhkogEYoYof48VQwHt/5xUURli2kL
+Daqi+X2+6j/vNp96EQ3sbFifmNejWNaDyyrlyGUvx9g/Eh5wMRospmFA/oE1kSws
+tKiBxAPs11OJGBRre2Q6QVW2ULAhxZOFgkCq0DNb8TMnhJOY4jhOP57rrvpMyu9q
+4wADBQgAhRl4aiej9lX+YpZUcyhBkqIB/cDwYemmtIWzo6mVWuDuVcyLl//sJsBi
+pwJF6O5nr0ZC5CT+GRgjBmh9rQjv/UtWBldJ7og/HfuSMG6xIfljO2FxKjabDhGa
+iKzgTk75LnPqfx0FeRNbN78dPy4hV/iIvHPANuyUlmbBsx9hSGqMc78FIDwwfZtB
+im5XUJbpHsahu4/8agQLBu+PFK+5CIVWskrYVL1R66nPHCzsfYcOv+1CYCsuFldG
+tFNPCegGxE7T7CFs7m7aYeSdgycNaR9wuBZeV17JnOJ2z/mtsR+8p4vZZzSMcTkj
+f1j7UCgiKS/ioTD03pU/OZEy9+l1SYhJBBgRAgAJBQJGuKNhAhsMAAoJENMoA12E
+iB8bOvUAoMbxSLj4OsaYEQ1FxOfsXPRmSPVWAJ91PvFOwLkhsm88kjPAIc8oymjd
+7A==
+=ABM8
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/rt/t/data/gnupg/keys/rt-test-at-example.com.secret.key b/rt/t/data/gnupg/keys/rt-test-at-example.com.secret.key
new file mode 100644
index 000000000..e76fe8428
--- /dev/null
+++ b/rt/t/data/gnupg/keys/rt-test-at-example.com.secret.key
@@ -0,0 +1,33 @@
+-----BEGIN PGP PRIVATE KEY BLOCK-----
+Version: GnuPG v1.4.7 (Darwin)
+
+lQHhBEa4o04RBADc+2sFcnuTTaqcKRTmSBQKdXvumT4GoATk194UYMghwprrNb1/
+flXQRk9zLkc0YENFHLMoRUmXKEF+WFxzXrZgHJS096tGn+Ud2FXQbSL47Vl3EHng
+c+jSvvVaZRcEySaCyQrsDR7gWlQtCbxbe96Y2x9jX3Zbih9UYnRvWBeczwCg4tgz
+EOmScnWiwUdyZNQsvXDqvKUD/REf0WjWDaykQvXYZ0aTpc/WMBsDS16nl8GNz5eD
+lCB/JJHKh5QDu89p0557AbVDSi5LCOYAM+v4oi8k5zgiO/7HJptirDkZ27Ichyes
+kzhu3Xr9rPLawie/o4FCfncNLbOAEE4EjEGDGRlyowAaXlW7DWT+TLbxY0qL0uHy
+AQPGA/9AmYHBJQqHTfQ4/QXdCnp+UwYs+rhPh7YHymBLn8Saa14heE9SZcYfSerL
+FAE7KKeBx96+RplgsiaqfWrliUwrV3KnnJICMyqWmn2OyMYiV9iFWqAHFTCsitS2
+q1COv5/Lg1a+XkAwEfoIuLrAXT8buIxXs/BhLc1PD1t9My8srf4DAwKhnHYPLWS2
+9GBnewzagq2czolDuKHrmtb1Eiv4mb4S8X6HhSn4gQSUJ59mVsn7L1TwK7yWJgK0
++Ix66LQdUlQgVGVzdCA8cnQtdGVzdEBleGFtcGxlLmNvbT6IYAQTEQIAIAUCRrij
+TgIbAwYLCQgHAwIEFQIIAwQWAgMBAh4BAheAAAoJENMoA12EiB8bMZoAoIxLNWQ9
+d9+W4ImPMpUmjLl9ttxWAJ9ELlhkfdhPukRe508p5fZqKUfl/p0CYwRGuKNhEAgA
+jSKFedFcU9RjLmSEGo7E4qMQWOtiNooW4NtRsKC2cbJXnGJUOT+GBzGCxjBZt89T
+6MVsOy7DoAzs+xWKA5CfgFEX5xZWM1c6EA4f7LhC1hawtGQkMQIyHzEy9b7NPEcM
+lkdOebjjhZ4Ob8svGilyQ9jN6zpR2c36i0sLaZ5gORIHJ9DOX1k5lUzEhkogEYoY
+of48VQwHt/5xUURli2kLDaqi+X2+6j/vNp96EQ3sbFifmNejWNaDyyrlyGUvx9g/
+Eh5wMRospmFA/oE1kSwstKiBxAPs11OJGBRre2Q6QVW2ULAhxZOFgkCq0DNb8TMn
+hJOY4jhOP57rrvpMyu9q4wADBQgAhRl4aiej9lX+YpZUcyhBkqIB/cDwYemmtIWz
+o6mVWuDuVcyLl//sJsBipwJF6O5nr0ZC5CT+GRgjBmh9rQjv/UtWBldJ7og/HfuS
+MG6xIfljO2FxKjabDhGaiKzgTk75LnPqfx0FeRNbN78dPy4hV/iIvHPANuyUlmbB
+sx9hSGqMc78FIDwwfZtBim5XUJbpHsahu4/8agQLBu+PFK+5CIVWskrYVL1R66nP
+HCzsfYcOv+1CYCsuFldGtFNPCegGxE7T7CFs7m7aYeSdgycNaR9wuBZeV17JnOJ2
+z/mtsR+8p4vZZzSMcTkjf1j7UCgiKS/ioTD03pU/OZEy9+l1Sf4DAwKhnHYPLWS2
+9GBUcgG+SE35K+ynz0mpxRRx4kbgN9Ap6oxzhDYVGRbfDpVxE8hgJuc7zJ27pmPr
+VwmdzDROCDy1W9bwjfrV8yhln81npumXxndSiEkEGBECAAkFAka4o2ECGwwACgkQ
+0ygDXYSIHxs69QCgxvFIuPg6xpgRDUXE5+xc9GZI9VYAn3U+8U7AuSGybzySM8Ah
+zyjKaN3s
+=cv/+
+-----END PGP PRIVATE KEY BLOCK-----
diff --git a/rt/t/delegation/cleanup_stalled.t b/rt/t/delegation/cleanup_stalled.t
new file mode 100644
index 000000000..f108eccf2
--- /dev/null
+++ b/rt/t/delegation/cleanup_stalled.t
@@ -0,0 +1,458 @@
+#!/usr/bin/perl -w
+use strict;
+use warnings;
+
+# Regression test suite for http://rt3.fsck.com/Ticket/Display.html?id=6184
+# and related corner cases related to cleanup of delegated ACEs when
+# the delegator loses the right to delegate. This causes complexities
+# due to the fact that multiple ACEs can grant different delegation
+# rights to a principal, and because DelegateRights and SuperUser can
+# themselves be delegated.
+
+# The case where the "parent" delegated ACE is removed is handled in
+# the embedded regression tests in lib/RT/ACE_Overlay.pm .
+
+
+use RT;
+
+use RT::Test tests => 98;
+
+my ($u1, $u2, $g1, $g2, $g3, $pg1, $pg2, $ace, @groups, @users, @principals);
+@groups = (\$g1, \$g2, \$g3, \$pg1, \$pg2);
+@users = (\$u1, \$u2);
+@principals = (@groups, @users);
+
+my($ret, $msg);
+
+$u1 = RT::User->new($RT::SystemUser);
+( $ret, $msg ) = $u1->LoadOrCreateByEmail('delegtest1@example.com');
+ok( $ret, "Load / Create test user 1: $msg" );
+$u1->SetPrivileged(1);
+$u2 = RT::User->new($RT::SystemUser);
+( $ret, $msg ) = $u2->LoadOrCreateByEmail('delegtest2@example.com');
+ok( $ret, "Load / Create test user 2: $msg" );
+$u2->SetPrivileged(1);
+$g1 = RT::Group->new($RT::SystemUser);
+( $ret, $msg) = $g1->LoadUserDefinedGroup('dg1');
+unless ($ret) {
+ ( $ret, $msg ) = $g1->CreateUserDefinedGroup( Name => 'dg1' );
+}
+ok( $ret, "Load / Create test group 1: $msg" );
+$g2 = RT::Group->new($RT::SystemUser);
+( $ret, $msg) = $g2->LoadUserDefinedGroup('dg2');
+unless ($ret) {
+ ( $ret, $msg ) = $g2->CreateUserDefinedGroup( Name => 'dg2' );
+}
+ok( $ret, "Load / Create test group 2: $msg" );
+$g3 = RT::Group->new($RT::SystemUser);
+( $ret, $msg) = $g3->LoadUserDefinedGroup('dg3');
+unless ($ret) {
+ ( $ret, $msg ) = $g3->CreateUserDefinedGroup( Name => 'dg3' );
+}
+ok( $ret, "Load / Create test group 3: $msg" );
+$pg1 = RT::Group->new($RT::SystemUser);
+( $ret, $msg ) = $pg1->LoadPersonalGroup( Name => 'dpg1',
+ User => $u1->PrincipalId );
+unless ($ret) {
+ ( $ret, $msg ) = $pg1->CreatePersonalGroup( Name => 'dpg1',
+ PrincipalId => $u1->PrincipalId );
+}
+ok( $ret, "Load / Create test personal group 1: $msg" );
+$pg2 = RT::Group->new($RT::SystemUser);
+( $ret, $msg ) = $pg2->LoadPersonalGroup( Name => 'dpg2',
+ User => $u2->PrincipalId );
+unless ($ret) {
+ ( $ret, $msg ) = $pg2->CreatePersonalGroup( Name => 'dpg2',
+ PrincipalId => $u2->PrincipalId );
+}
+ok( $ret, "Load / Create test personal group 2: $msg" );
+
+
+
+# Basic case: u has global DelegateRights through g1 and ShowConfigTab
+# through g2; then u is removed from g1.
+
+clear_acls_and_groups();
+
+( $ret, $msg ) = $g1->PrincipalObj->GrantRight( Right => 'DelegateRights' );
+ok( $ret, "Grant DelegateRights to g1: $msg" );
+( $ret, $msg ) = $g2->PrincipalObj->GrantRight( Right => 'ShowConfigTab' );
+ok( $ret, "Grant ShowConfigTab to g2: $msg" );
+( $ret, $msg ) = $g1->AddMember( $u1->PrincipalId );
+ok( $ret, "Add test user 1 to g1: $msg" );
+ok(
+ $u1->PrincipalObj->HasRight(
+ Right => 'DelegateRights',
+ Object => $RT::System
+ ),
+ "test user 1 has DelegateRights after joining g1"
+);
+( $ret, $msg ) = $g2->AddMember( $u1->PrincipalId );
+ok( $ret, "Add test user 1 to g2: $msg" );
+ok(
+ $u1->PrincipalObj->HasRight(
+ Right => 'ShowConfigTab',
+ Object => $RT::System
+ ),
+ "test user 1 has ShowConfigTab after joining g2"
+);
+
+$ace = RT::ACE->new($u1);
+( $ret, $msg ) = $ace->LoadByValues(
+ RightName => 'ShowConfigTab',
+ Object => $RT::System,
+ PrincipalType => 'Group',
+ PrincipalId => $g2->PrincipalId
+);
+ok( $ret, "Look up ACE to be delegated: $msg" );
+( $ret, $msg ) = $ace->Delegate( PrincipalId => $pg1->PrincipalId );
+ok( $ret, "Delegate ShowConfigTab to pg1: $msg" );
+ok(
+ $pg1->PrincipalObj->HasRight(
+ Right => 'ShowConfigTab',
+ Object => $RT::System
+ ),
+ "Test personal group 1 has ShowConfigTab right after delegation"
+);
+
+( $ret, $msg ) = $g1->DeleteMember( $u1->PrincipalId );
+ok( $ret, "Delete test user 1 from g1: $msg" );
+ok(
+ not(
+ $pg1->PrincipalObj->HasRight(
+ Right => 'ShowConfigTab',
+ Object => $RT::System
+ )
+ ),
+ "Test personal group 1 lacks ShowConfigTab right after user removed from g1"
+);
+
+# Basic case: u has global DelegateRights through g1 and ShowConfigTab
+# through g2; then DelegateRights revoked from g1.
+
+( $ret, $msg ) = $g1->AddMember( $u1->PrincipalId );
+ok( $ret, "Add test user 1 to g1: $msg" );
+( $ret, $msg ) = $ace->Delegate( PrincipalId => $pg1->PrincipalId );
+ok( $ret, "Delegate ShowConfigTab to pg1: $msg" );
+( $ret, $msg ) = $g1->PrincipalObj->RevokeRight( Right => 'DelegateRights' );
+ok( $ret, "Revoke DelegateRights from g1: $msg" );
+ok(
+ not(
+ $pg1->PrincipalObj->HasRight(
+ Right => 'ShowConfigTab',
+ Object => $RT::System
+ )
+ ),
+ "Test personal group 1 lacks ShowConfigTab right after DelegateRights revoked from g1"
+);
+
+
+
+# Corner case - restricted delegation: u has DelegateRights on pg1
+# through g1 and AdminGroup on pg1 through g2; then DelegateRights
+# revoked from g1.
+
+clear_acls_and_groups();
+
+( $ret, $msg ) = $g1->PrincipalObj->GrantRight( Right => 'DelegateRights',
+ Object => $pg1);
+ok( $ret, "Grant DelegateRights on pg1 to g1: $msg" );
+( $ret, $msg ) = $g2->PrincipalObj->GrantRight( Right => 'AdminGroup',
+ Object => $pg1);
+ok( $ret, "Grant AdminGroup on pg1 to g2: $msg" );
+( $ret, $msg ) = $g1->AddMember( $u1->PrincipalId );
+ok( $ret, "Add test user 1 to g1: $msg" );
+( $ret, $msg ) = $g2->AddMember( $u1->PrincipalId );
+ok( $ret, "Add test user 1 to g2: $msg" );
+ok( $u1->PrincipalObj->HasRight(
+ Right => 'DelegateRights',
+ Object => $pg1 ),
+ "test user 1 has DelegateRights on pg1 after joining g1" );
+ok( not( $u1->PrincipalObj->HasRight(
+ Right => 'DelegateRights',
+ Object => $RT::System )),
+ "Test personal group 1 lacks global DelegateRights after joining g1" );
+$ace = RT::ACE->new($u1);
+( $ret, $msg ) = $ace->LoadByValues(
+ RightName => 'AdminGroup',
+ Object => $pg1,
+ PrincipalType => 'Group',
+ PrincipalId => $g2->PrincipalId
+);
+ok( $ret, "Look up ACE to be delegated: $msg" );
+( $ret, $msg ) = $ace->Delegate( PrincipalId => $pg1->PrincipalId );
+ok( $ret, "Delegate AdminGroup on pg1 to pg1: $msg" );
+ok( $pg1->PrincipalObj->HasRight(
+ Right => 'AdminGroup',
+ Object => $pg1 ),
+ "Test personal group 1 has AdminGroup right on pg1 after delegation" );
+( $ret, $msg ) = $g1->PrincipalObj->RevokeRight ( Right => 'DelegateRights',
+ Object => $pg1 );
+ok( $ret, "Revoke DelegateRights on pg1 from g1: $msg" );
+ok( not( $pg1->PrincipalObj->HasRight(
+ Right => 'AdminGroup',
+ Object => $pg1 )),
+ "Test personal group 1 lacks AdminGroup right on pg1 after DelegateRights revoked from g1" );
+( $ret, $msg ) = $g1->PrincipalObj->GrantRight( Right => 'DelegateRights',
+ Object => $pg1);
+
+# Corner case - restricted delegation: u has DelegateRights on pg1
+# through g1 and AdminGroup on pg1 through g2; then u removed from g1.
+
+ok( $ret, "Grant DelegateRights on pg1 to g1: $msg" );
+( $ret, $msg ) = $ace->Delegate( PrincipalId => $pg1->PrincipalId );
+ok( $ret, "Delegate AdminGroup on pg1 to pg1: $msg" );
+ok( $pg1->PrincipalObj->HasRight(
+ Right => 'AdminGroup',
+ Object => $pg1 ),
+ "Test personal group 1 has AdminGroup right on pg1 after delegation" );
+( $ret, $msg ) = $g1->DeleteMember( $u1->PrincipalId );
+ok( $ret, "Delete test user 1 from g1: $msg" );
+ok( not( $pg1->PrincipalObj->HasRight(
+ Right => 'AdminGroup',
+ Object => $pg1 )),
+ "Test personal group 1 lacks AdminGroup right on pg1 after user removed from g1" );
+
+clear_acls_and_groups();
+
+
+
+# Corner case - multiple delegation rights: u has global
+# DelegateRights directly and DelegateRights on pg1 through g1, and
+# AdminGroup on pg1 through g2; then u removed from g1 (delegation
+# should remain); then DelegateRights revoked from u (delegation
+# should not remain).
+
+( $ret, $msg ) = $g1->PrincipalObj->GrantRight( Right => 'DelegateRights',
+ Object => $pg1);
+ok( $ret, "Grant DelegateRights on pg1 to g1: $msg" );
+( $ret, $msg ) = $g2->PrincipalObj->GrantRight( Right => 'AdminGroup',
+ Object => $pg1);
+ok( $ret, "Grant AdminGroup on pg1 to g2: $msg" );
+( $ret, $msg ) = $u1->PrincipalObj->GrantRight( Right => 'DelegateRights',
+ Object => $RT::System);
+ok( $ret, "Grant DelegateRights to user: $msg" );
+( $ret, $msg ) = $g1->AddMember( $u1->PrincipalId );
+ok( $ret, "Add test user 1 to g1: $msg" );
+( $ret, $msg ) = $g2->AddMember( $u1->PrincipalId );
+ok( $ret, "Add test user 1 to g2: $msg" );
+$ace = RT::ACE->new($u1);
+( $ret, $msg ) = $ace->LoadByValues(
+ RightName => 'AdminGroup',
+ Object => $pg1,
+ PrincipalType => 'Group',
+ PrincipalId => $g2->PrincipalId
+);
+ok( $ret, "Look up ACE to be delegated: $msg" );
+( $ret, $msg ) = $ace->Delegate( PrincipalId => $pg1->PrincipalId );
+ok( $ret, "Delegate AdminGroup on pg1 to pg1: $msg" );
+( $ret, $msg ) = $g1->DeleteMember( $u1->PrincipalId );
+ok( $ret, "Delete test user 1 from g1: $msg" );
+ok( $pg1->PrincipalObj->HasRight(Right => 'AdminGroup',
+ Object => $pg1),
+ "Test personal group 1 retains AdminGroup right on pg1 after user removed from g1" );
+( $ret, $msg ) = $u1->PrincipalObj->RevokeRight( Right => 'DelegateRights',
+ Object => $RT::System );
+ok( not ($pg1->PrincipalObj->HasRight(Right => 'AdminGroup',
+ Object => $pg1)),
+ "Test personal group 1 lacks AdminGroup right on pg1 after DelegateRights revoked");
+
+# Corner case - multiple delegation rights and selectivity: u has
+# DelegateRights globally and on g2 directly and DelegateRights on pg1
+# through g1, and AdminGroup on pg1 through g2; then global
+# DelegateRights revoked from u (delegation should remain),
+# DelegateRights on g2 revoked from u (delegation should remain), and
+# u removed from g1 (delegation should not remain).
+
+( $ret, $msg ) = $g1->AddMember( $u1->PrincipalId );
+ok( $ret, "Add test user 1 to g1: $msg" );
+( $ret, $msg ) = $u1->PrincipalObj->GrantRight( Right => 'DelegateRights',
+ Object => $RT::System);
+ok( $ret, "Grant DelegateRights to user: $msg" );
+( $ret, $msg ) = $u1->PrincipalObj->GrantRight( Right => 'DelegateRights',
+ Object => $g2);
+ok( $ret, "Grant DelegateRights on g2 to user: $msg" );
+( $ret, $msg ) = $ace->Delegate( PrincipalId => $pg1->PrincipalId );
+ok( $ret, "Delegate AdminGroup on pg1 to pg1: $msg" );
+( $ret, $msg ) = $u1->PrincipalObj->RevokeRight( Right => 'DelegateRights',
+ Object => $RT::System );
+ok( $pg1->PrincipalObj->HasRight(Right => 'AdminGroup',
+ Object => $pg1),
+ "Test personal group 1 retains AdminGroup right on pg1 after global DelegateRights revoked" );
+( $ret, $msg ) = $u1->PrincipalObj->RevokeRight( Right => 'DelegateRights',
+ Object => $g2 );
+ok( $pg1->PrincipalObj->HasRight(Right => 'AdminGroup',
+ Object => $pg1),
+ "Test personal group 1 retains AdminGroup right on pg1 after DelegateRights on g2 revoked" );
+( $ret, $msg ) = $g1->DeleteMember( $u1->PrincipalId );
+ok( $ret, "Delete test user 1 from g1: $msg" );
+ok( not ($pg1->PrincipalObj->HasRight(Right => 'AdminGroup',
+ Object => $pg1)),
+ "Test personal group 1 lacks AdminGroup right on pg1 after user removed from g1");
+
+
+
+# Corner case - indirect delegation rights: u has DelegateRights
+# through g1 via g3, and ShowConfigTab via g2; then g3 removed from
+# g1.
+
+clear_acls_and_groups();
+
+( $ret, $msg ) = $g1->PrincipalObj->GrantRight( Right => 'DelegateRights' );
+ok( $ret, "Grant DelegateRights to g1: $msg" );
+( $ret, $msg ) = $g2->PrincipalObj->GrantRight( Right => 'ShowConfigTab' );
+ok( $ret, "Grant ShowConfigTab to g2: $msg" );
+( $ret, $msg ) = $g1->AddMember( $g3->PrincipalId );
+ok( $ret, "Add g3 to g1: $msg" );
+( $ret, $msg ) = $g3->AddMember( $u1->PrincipalId );
+ok( $ret, "Add test user 1 to g3: $msg" );
+( $ret, $msg ) = $g2->AddMember( $u1->PrincipalId );
+ok( $ret, "Add test user 1 to g2: $msg" );
+
+$ace = RT::ACE->new($u1);
+( $ret, $msg ) = $ace->LoadByValues(
+ RightName => 'ShowConfigTab',
+ Object => $RT::System,
+ PrincipalType => 'Group',
+ PrincipalId => $g2->PrincipalId
+);
+ok( $ret, "Look up ACE to be delegated: $msg" );
+( $ret, $msg ) = $ace->Delegate( PrincipalId => $pg1->PrincipalId );
+ok( $ret, "Delegate ShowConfigTab to pg1: $msg" );
+
+( $ret, $msg ) = $g1->DeleteMember( $g3->PrincipalId );
+ok( $ret, "Delete g3 from g1: $msg" );
+ok( not ($pg1->PrincipalObj->HasRight(Right => 'ShowConfigTab',
+ Object => $RT::System)),
+ "Test personal group 1 lacks ShowConfigTab right after g3 removed from g1");
+
+# Corner case - indirect delegation rights: u has DelegateRights
+# through g1 via g3, and ShowConfigTab via g2; then DelegateRights
+# revoked from g1.
+
+( $ret, $msg ) = $g1->AddMember( $g3->PrincipalId );
+ok( $ret, "Add g3 to g1: $msg" );
+( $ret, $msg ) = $ace->Delegate( PrincipalId => $pg1->PrincipalId );
+ok( $ret, "Delegate ShowConfigTab to pg1: $msg" );
+( $ret, $msg ) = $g1->PrincipalObj->RevokeRight ( Right => 'DelegateRights' );
+ok( $ret, "Revoke DelegateRights from g1: $msg" );
+
+ok( not ($pg1->PrincipalObj->HasRight(Right => 'ShowConfigTab',
+ Object => $RT::System)),
+ "Test personal group 1 lacks ShowConfigTab right after DelegateRights revoked from g1");
+
+
+
+# Corner case - delegation of DelegateRights: u1 has DelegateRights
+# via g1 and delegates DelegateRights to pg1; u2 has DelegateRights
+# via pg1 and ShowConfigTab via g2; then u1 removed from g1.
+
+clear_acls_and_groups();
+
+( $ret, $msg ) = $g1->PrincipalObj->GrantRight( Right => 'DelegateRights' );
+ok( $ret, "Grant DelegateRights to g1: $msg" );
+( $ret, $msg ) = $g2->PrincipalObj->GrantRight( Right => 'ShowConfigTab' );
+ok( $ret, "Grant ShowConfigTab to g2: $msg" );
+( $ret, $msg ) = $g1->AddMember( $u1->PrincipalId );
+ok( $ret, "Add test user 1 to g1: $msg" );
+$ace = RT::ACE->new($u1);
+( $ret, $msg ) = $ace->LoadByValues(
+ RightName => 'DelegateRights',
+ Object => $RT::System,
+ PrincipalType => 'Group',
+ PrincipalId => $g1->PrincipalId
+);
+ok( $ret, "Look up ACE to be delegated: $msg" );
+( $ret, $msg ) = $ace->Delegate( PrincipalId => $pg1->PrincipalId );
+ok( $ret, "Delegate DelegateRights to pg1: $msg" );
+
+( $ret, $msg ) = $pg1->AddMember( $u2->PrincipalId );
+ok( $ret, "Add test user 2 to pg1: $msg" );
+( $ret, $msg ) = $g2->AddMember( $u2->PrincipalId );
+ok( $ret, "Add test user 2 to g2: $msg" );
+$ace = RT::ACE->new($u2);
+( $ret, $msg ) = $ace->LoadByValues(
+ RightName => 'ShowConfigTab',
+ Object => $RT::System,
+ PrincipalType => 'Group',
+ PrincipalId => $g2->PrincipalId
+);
+ok( $ret, "Look up ACE to be delegated: $msg" );
+( $ret, $msg ) = $ace->Delegate( PrincipalId => $pg2->PrincipalId );
+ok( $ret, "Delegate ShowConfigTab to pg2: $msg" );
+
+ok( $pg2->PrincipalObj->HasRight(Right => 'ShowConfigTab',
+ Object => $RT::System),
+ "Test personal group 2 has ShowConfigTab right after delegation");
+( $ret, $msg ) = $g1->DeleteMember( $u1->PrincipalId );
+ok( $ret, "Delete u1 from g1: $msg" );
+ok( not ($pg2->PrincipalObj->HasRight(Right => 'ShowConfigTab',
+ Object => $RT::System)),
+ "Test personal group 2 lacks ShowConfigTab right after u1 removed from g1");
+
+# Corner case - delegation of DelegateRights: u1 has DelegateRights
+# via g1 and delegates DelegateRights to pg1; u2 has DelegateRights
+# via pg1 and ShowConfigTab via g2; then DelegateRights revoked from
+# g1.
+
+( $ret, $msg ) = $g1->AddMember( $u1->PrincipalId );
+ok( $ret, "Add u1 to g1: $msg" );
+$ace = RT::ACE->new($u1);
+( $ret, $msg ) = $ace->LoadByValues(
+ RightName => 'DelegateRights',
+ Object => $RT::System,
+ PrincipalType => 'Group',
+ PrincipalId => $g1->PrincipalId
+);
+ok( $ret, "Look up ACE to be delegated: $msg" );
+( $ret, $msg ) = $ace->Delegate( PrincipalId => $pg1->PrincipalId );
+ok( $ret, "Delegate DelegateRights to pg1: $msg" );
+$ace = RT::ACE->new($u2);
+( $ret, $msg ) = $ace->LoadByValues(
+ RightName => 'ShowConfigTab',
+ Object => $RT::System,
+ PrincipalType => 'Group',
+ PrincipalId => $g2->PrincipalId
+);
+ok( $ret, "Look up ACE to be delegated: $msg" );
+( $ret, $msg ) = $ace->Delegate( PrincipalId => $pg2->PrincipalId );
+ok( $ret, "Delegate ShowConfigTab to pg2: $msg" );
+
+( $ret, $msg ) = $g1->PrincipalObj->RevokeRight ( Right => 'DelegateRights' );
+ok( $ret, "Revoke DelegateRights from g1: $msg" );
+ok( not ($pg2->PrincipalObj->HasRight(Right => 'ShowConfigTab',
+ Object => $RT::System)),
+ "Test personal group 2 lacks ShowConfigTab right after DelegateRights revoked from g1");
+
+
+
+
+#######
+
+sub clear_acls_and_groups {
+ # Revoke all rights granted to our cast
+ my $acl = RT::ACL->new($RT::SystemUser);
+ foreach (@principals) {
+ $acl->LimitToPrincipal(Type => $$_->PrincipalObj->PrincipalType,
+ Id => $$_->PrincipalObj->Id);
+ }
+ while (my $ace = $acl->Next()) {
+ $ace->Delete();
+ }
+
+ # Remove all group memberships
+ my $members = RT::GroupMembers->new($RT::SystemUser);
+ foreach (@groups) {
+ $members->LimitToMembersOfGroup( $$_->PrincipalId );
+ }
+ while (my $member = $members->Next()) {
+ $member->Delete();
+ }
+
+ $acl->RedoSearch();
+ is( $acl->Count() , 0,
+ "All principals have no rights after clearing ACLs" );
+ $members->RedoSearch();
+ is( $members->Count() , 0,
+ "All groups have no members after clearing groups" );
+}
diff --git a/rt/t/delegation/revocation.t b/rt/t/delegation/revocation.t
new file mode 100644
index 000000000..151525edf
--- /dev/null
+++ b/rt/t/delegation/revocation.t
@@ -0,0 +1,135 @@
+#!/usr/bin/perl -w
+
+use strict;
+use warnings;
+
+use RT;
+
+use RT::Test tests => 22;
+
+my ($u1, $g1, $pg1, $pg2, $ace, @groups, @users, @principals);
+@groups = (\$g1, \$pg1, \$pg2);
+@users = (\$u1);
+@principals = (@groups, @users);
+
+my($ret, $msg);
+
+$u1 = RT::User->new($RT::SystemUser);
+( $ret, $msg ) = $u1->LoadOrCreateByEmail('delegtest1@example.com');
+ok( $ret, "Load / Create test user 1: $msg" );
+$u1->SetPrivileged(1);
+
+$g1 = RT::Group->new($RT::SystemUser);
+( $ret, $msg) = $g1->LoadUserDefinedGroup('dg1');
+unless ($ret) {
+ ( $ret, $msg ) = $g1->CreateUserDefinedGroup( Name => 'dg1' );
+}
+$pg1 = RT::Group->new($RT::SystemUser);
+( $ret, $msg ) = $pg1->LoadPersonalGroup( Name => 'dpg1',
+ User => $u1->PrincipalId );
+unless ($ret) {
+ ( $ret, $msg ) = $pg1->CreatePersonalGroup( Name => 'dpg1',
+ PrincipalId => $u1->PrincipalId );
+}
+ok( $ret, "Load / Create test personal group 1: $msg" );
+$pg2 = RT::Group->new($RT::SystemUser);
+( $ret, $msg ) = $pg2->LoadPersonalGroup( Name => 'dpg2',
+ User => $u1->PrincipalId );
+unless ($ret) {
+ ( $ret, $msg ) = $pg2->CreatePersonalGroup( Name => 'dpg2',
+ PrincipalId => $u1->PrincipalId );
+}
+ok( $ret, "Load / Create test personal group 2: $msg" );
+
+clear_acls_and_groups();
+
+( $ret, $msg ) = $u1->PrincipalObj->GrantRight( Right => 'DelegateRights' );
+ok( $ret, "Grant DelegateRights to u1: $msg" );
+( $ret, $msg ) = $g1->PrincipalObj->GrantRight( Right => 'ShowConfigTab' );
+ok( $ret, "Grant ShowConfigTab to g1: $msg" );
+( $ret, $msg ) = $g1->AddMember( $u1->PrincipalId );
+ok( $ret, "Add test user 1 to g1: $msg" );
+
+$ace = RT::ACE->new($u1);
+( $ret, $msg ) = $ace->LoadByValues(
+ RightName => 'ShowConfigTab',
+ Object => $RT::System,
+ PrincipalType => 'Group',
+ PrincipalId => $g1->PrincipalId
+);
+ok( $ret, "Look up ACE to be delegated: $msg" );
+( $ret, $msg ) = $ace->Delegate( PrincipalId => $pg1->PrincipalId );
+ok( $ret, "Delegate ShowConfigTab to pg1: $msg" );
+( $ret, $msg ) = $ace->Delegate( PrincipalId => $pg2->PrincipalId );
+ok( $ret, "Delegate ShowConfigTab to pg2: $msg" );
+
+ok(( $pg1->PrincipalObj->HasRight( Right => 'ShowConfigTab',
+ Object => $RT::System ) and
+ $pg2->PrincipalObj->HasRight( Right => 'ShowConfigTab',
+ Object => $RT::System )),
+ "Test personal groups have ShowConfigTab right after delegation" );
+
+( $ret, $msg ) = $g1->DeleteMember( $u1->PrincipalId );
+ok( $ret, "Delete test user 1 from g1: $msg" );
+
+ok( not( $pg1->PrincipalObj->HasRight( Right => 'ShowConfigTab',
+ Object => $RT::System )),
+ "Test personal group 1 lacks ShowConfigTab after user removed from g1" );
+ok( not( $pg2->PrincipalObj->HasRight( Right => 'ShowConfigTab',
+ Object => $RT::System )),
+ "Test personal group 2 lacks ShowConfigTab after user removed from g1" );
+
+( $ret, $msg ) = $g1->AddMember( $u1->PrincipalId );
+ok( $ret, "Add test user 1 to g1: $msg" );
+( $ret, $msg ) = $ace->Delegate( PrincipalId => $pg1->PrincipalId );
+ok( $ret, "Delegate ShowConfigTab to pg1: $msg" );
+( $ret, $msg ) = $ace->Delegate( PrincipalId => $pg2->PrincipalId );
+ok( $ret, "Delegate ShowConfigTab to pg2: $msg" );
+
+ok(( $pg1->PrincipalObj->HasRight( Right => 'ShowConfigTab',
+ Object => $RT::System ) and
+ $pg2->PrincipalObj->HasRight( Right => 'ShowConfigTab',
+ Object => $RT::System )),
+ "Test personal groups have ShowConfigTab right after delegation" );
+
+( $ret, $msg ) = $g1->PrincipalObj->RevokeRight( Right => 'ShowConfigTab' );
+ok( $ret, "Revoke ShowConfigTab from g1: $msg" );
+
+ok( not( $pg1->PrincipalObj->HasRight( Right => 'ShowConfigTab',
+ Object => $RT::System )),
+ "Test personal group 1 lacks ShowConfigTab after user removed from g1" );
+ok( not( $pg2->PrincipalObj->HasRight( Right => 'ShowConfigTab',
+ Object => $RT::System )),
+ "Test personal group 2 lacks ShowConfigTab after user removed from g1" );
+
+
+
+#######
+
+sub clear_acls_and_groups {
+ # Revoke all rights granted to our cast
+ my $acl = RT::ACL->new($RT::SystemUser);
+ foreach (@principals) {
+ $acl->LimitToPrincipal(Type => $$_->PrincipalObj->PrincipalType,
+ Id => $$_->PrincipalObj->Id);
+ }
+ while (my $ace = $acl->Next()) {
+ $ace->Delete();
+ }
+
+ # Remove all group memberships
+ my $members = RT::GroupMembers->new($RT::SystemUser);
+ foreach (@groups) {
+ $members->LimitToMembersOfGroup( $$_->PrincipalId );
+ }
+ while (my $member = $members->Next()) {
+ $member->Delete();
+ }
+
+ $acl->RedoSearch();
+ is( $acl->Count() , 0,
+ "All principals have no rights after clearing ACLs" );
+ $members->RedoSearch();
+ is( $members->Count() , 0,
+ "All groups have no members after clearing groups" );
+}
diff --git a/rt/t/i18n/default.t b/rt/t/i18n/default.t
new file mode 100644
index 000000000..6c9842a4c
--- /dev/null
+++ b/rt/t/i18n/default.t
@@ -0,0 +1,19 @@
+#!/usr/bin/perl -w
+use strict;
+use warnings;
+
+use RT::Test tests => 8;
+
+my ($baseurl, $m) = RT::Test->started_ok;
+$m->get_ok('/');
+$m->title_is('Login');
+
+$m->get_ok('/', { 'Accept-Language' => 'x-klingon' });
+$m->title_is('Login', 'unavailable language fallback to en');
+
+$m->add_header('Accept-Language' => 'zh-tw,zh;q=0.8,en-gb;q=0.5,en;q=0.3');
+$m->get_ok('/');
+use utf8;
+Encode::_utf8_on($m->{content});
+$m->title_is('登入', 'Page title properly translated to chinese');
+$m->content_contains('密碼','Password properly translated');
diff --git a/rt/t/mail/charsets-outgoing.t b/rt/t/mail/charsets-outgoing.t
new file mode 100644
index 000000000..ca44bbd27
--- /dev/null
+++ b/rt/t/mail/charsets-outgoing.t
@@ -0,0 +1,306 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+use utf8;
+
+use RT::Test tests => 30;
+
+
+RT::Test->set_mail_catcher;
+
+my $queue = RT::Test->load_or_create_queue(
+ Name => 'Regression',
+ CorrespondAddress => 'rt-recipient@example.com',
+ CommentAddress => 'rt-recipient@example.com',
+);
+ok $queue && $queue->id, 'loaded or created queue';
+
+diag "make sure queue has no subject tag" if $ENV{'TEST_VERBOSE'};
+{
+ my ($status, $msg) = $queue->SetSubjectTag( undef );
+ ok $status, "set subject tag for the queue" or diag "error: $msg";
+}
+
+diag "set intial simple autoreply template" if $ENV{'TEST_VERBOSE'};
+{
+ my $template = RT::Template->new( $RT::SystemUser );
+ $template->Load('Autoreply');
+ ok $template->id, "loaded autoreply tempalte";
+
+ my ($status, $msg) = $template->SetContent(
+ "Subject: Autreply { \$Ticket->Subject }\n"
+ ."\n"
+ ."hi there it's an autoreply.\n"
+ ."\n"
+ );
+ ok $status, "changed content of the template"
+ or diag "error: $msg";
+}
+
+diag "basic test of autoreply" if $ENV{'TEST_VERBOSE'};
+{
+ my $ticket = RT::Ticket->new( $RT::SystemUser );
+ $ticket->Create(
+ Queue => $queue->id,
+ Subject => 'test',
+ Requestor => 'root@localhost',
+ );
+ my @mails = RT::Test->fetch_caught_mails;
+ ok @mails, "got some outgoing emails";
+}
+
+my $str_ru_test = "\x{442}\x{435}\x{441}\x{442}";
+my $str_ru_autoreply = "\x{410}\x{432}\x{442}\x{43e}\x{43e}\x{442}\x{432}\x{435}\x{442}";
+my $str_ru_support = "\x{43f}\x{43e}\x{434}\x{434}\x{435}\x{440}\x{436}\x{43a}\x{430}";
+
+diag "non-ascii Subject with ascii prefix set in the template"
+ if $ENV{'TEST_VERBOSE'};
+{
+ my $ticket = RT::Ticket->new( $RT::SystemUser );
+ $ticket->Create(
+ Queue => $queue->id,
+ Subject => $str_ru_test,
+ Requestor => 'root@localhost',
+ );
+ my @mails = RT::Test->fetch_caught_mails;
+ ok @mails, "got some outgoing emails";
+
+ my $status = 1;
+ foreach my $mail ( @mails ) {
+ my $entity = parse_mail( $mail );
+ my $subject = Encode::decode_utf8( $entity->head->get('Subject') );
+ $subject =~ /$str_ru_test/
+ or do { $status = 0; diag "wrong subject: $subject" };
+ }
+ ok $status, "all mails have correct data";
+}
+
+diag "set non-ascii subject tag for the queue" if $ENV{'TEST_VERBOSE'};
+{
+ my ($status, $msg) = $queue->SetSubjectTag( $str_ru_support );
+ ok $status, "set subject tag for the queue" or diag "error: $msg";
+}
+
+diag "ascii subject with non-ascii subject tag" if $ENV{'TEST_VERBOSE'};
+{
+ my $ticket = RT::Ticket->new( $RT::SystemUser );
+ $ticket->Create(
+ Queue => $queue->id,
+ Subject => 'test',
+ Requestor => 'root@localhost',
+ );
+ my @mails = RT::Test->fetch_caught_mails;
+ ok @mails, "got some outgoing emails";
+
+ my $status = 1;
+ foreach my $mail ( @mails ) {
+ my $entity = parse_mail( $mail );
+ my $subject = Encode::decode_utf8( $entity->head->get('Subject') );
+ $subject =~ /$str_ru_support/
+ or do { $status = 0; diag "wrong subject: $subject" };
+ }
+ ok $status, "all mails have correct data";
+}
+
+diag "non-ascii subject with non-ascii subject tag" if $ENV{'TEST_VERBOSE'};
+{
+ my $ticket = RT::Ticket->new( $RT::SystemUser );
+ $ticket->Create(
+ Queue => $queue->id,
+ Subject => $str_ru_test,
+ Requestor => 'root@localhost',
+ );
+ my @mails = RT::Test->fetch_caught_mails;
+ ok @mails, "got some outgoing emails";
+
+ my $status = 1;
+ foreach my $mail ( @mails ) {
+ my $entity = parse_mail( $mail );
+ my $subject = Encode::decode_utf8( $entity->head->get('Subject') );
+ $subject =~ /$str_ru_support/
+ or do { $status = 0; diag "wrong subject: $subject" };
+ $subject =~ /$str_ru_test/
+ or do { $status = 0; diag "wrong subject: $subject" };
+ }
+ ok $status, "all mails have correct data";
+}
+
+diag "return back the empty subject tag" if $ENV{'TEST_VERBOSE'};
+{
+ my ($status, $msg) = $queue->SetSubjectTag( undef );
+ ok $status, "set subject tag for the queue" or diag "error: $msg";
+}
+
+diag "add non-ascii subject prefix in the autoreply template" if $ENV{'TEST_VERBOSE'};
+{
+ my $template = RT::Template->new( $RT::SystemUser );
+ $template->Load('Autoreply');
+ ok $template->id, "loaded autoreply tempalte";
+
+ my ($status, $msg) = $template->SetContent(
+ "Subject: $str_ru_autoreply { \$Ticket->Subject }\n"
+ ."\n"
+ ."hi there it's an autoreply.\n"
+ ."\n"
+ );
+ ok $status, "changed content of the template" or diag "error: $msg";
+}
+
+diag "ascii subject with non-ascii subject prefix in template" if $ENV{'TEST_VERBOSE'};
+{
+ my $ticket = RT::Ticket->new( $RT::SystemUser );
+ $ticket->Create(
+ Queue => $queue->id,
+ Subject => 'test',
+ Requestor => 'root@localhost',
+ );
+ my @mails = RT::Test->fetch_caught_mails;
+ ok @mails, "got some outgoing emails";
+
+ my $status = 1;
+ foreach my $mail ( @mails ) {
+ my $entity = parse_mail( $mail );
+ my $subject = Encode::decode_utf8( $entity->head->get('Subject') );
+ $subject =~ /$str_ru_autoreply/
+ or do { $status = 0; diag "wrong subject: $subject" };
+ }
+ ok $status, "all mails have correct data";
+}
+
+diag "non-ascii subject with non-ascii subject prefix in template"
+ if $ENV{'TEST_VERBOSE'};
+{
+ my $ticket = RT::Ticket->new( $RT::SystemUser );
+ $ticket->Create(
+ Queue => $queue->id,
+ Subject => $str_ru_test,
+ Requestor => 'root@localhost',
+ );
+ my @mails = RT::Test->fetch_caught_mails;
+ ok @mails, "got some outgoing emails";
+
+ my $status = 1;
+ foreach my $mail ( @mails ) {
+ my $entity = parse_mail( $mail );
+ my $subject = Encode::decode_utf8( $entity->head->get('Subject') );
+ $subject =~ /$str_ru_autoreply/
+ or do { $status = 0; diag "wrong subject: $subject" };
+ $subject =~ /$str_ru_autoreply/
+ or do { $status = 0; diag "wrong subject: $subject" };
+ }
+ ok $status, "all mails have correct data";
+}
+
+diag "set non-ascii subject tag for the queue" if $ENV{'TEST_VERBOSE'};
+{
+ my ($status, $msg) = $queue->SetSubjectTag( $str_ru_support );
+ ok $status, "set subject tag for the queue" or diag "error: $msg";
+}
+
+diag "non-ascii subject, non-ascii prefix in template and non-ascii tag"
+ if $ENV{'TEST_VERBOSE'};
+{
+ my $ticket = RT::Ticket->new( $RT::SystemUser );
+ $ticket->Create(
+ Queue => $queue->id,
+ Subject => $str_ru_test,
+ Requestor => 'root@localhost',
+ );
+ my @mails = RT::Test->fetch_caught_mails;
+ ok @mails, "got some outgoing emails";
+
+ my $status = 1;
+ foreach my $mail ( @mails ) {
+ my $entity = parse_mail( $mail );
+ my $subject = Encode::decode_utf8( $entity->head->get('Subject') );
+ $subject =~ /$str_ru_autoreply/
+ or do { $status = 0; diag "wrong subject: $subject" };
+ $subject =~ /$str_ru_autoreply/
+ or do { $status = 0; diag "wrong subject: $subject" };
+ $subject =~ /$str_ru_autoreply/
+ or do { $status = 0; diag "wrong subject: $subject" };
+ }
+ ok $status, "all mails have correct data";
+}
+
+diag "flush subject tag of the queue" if $ENV{'TEST_VERBOSE'};
+{
+ my ($status, $msg) = $queue->SetSubjectTag( undef );
+ ok $status, "set subject tag for the queue" or diag "error: $msg";
+}
+
+
+diag "don't change subject via template" if $ENV{'TEST_VERBOSE'};
+{
+ my $template = RT::Template->new( $RT::SystemUser );
+ $template->Load('Autoreply');
+ ok $template->id, "loaded autoreply tempalte";
+
+ my ($status, $msg) = $template->SetContent(
+ "\n"
+ ."\n"
+ ."hi there it's an autoreply.\n"
+ ."\n"
+ );
+ ok $status, "changed content of the template" or diag "error: $msg";
+}
+
+diag "non-ascii Subject without changes in template" if $ENV{'TEST_VERBOSE'};
+{
+ my $ticket = RT::Ticket->new( $RT::SystemUser );
+ $ticket->Create(
+ Queue => $queue->id,
+ Subject => $str_ru_test,
+ Requestor => 'root@localhost',
+ );
+ my @mails = RT::Test->fetch_caught_mails;
+ ok @mails, "got some outgoing emails";
+
+ my $status = 1;
+ foreach my $mail ( @mails ) {
+ my $entity = parse_mail( $mail );
+ my $subject = Encode::decode_utf8( $entity->head->get('Subject') );
+ $subject =~ /$str_ru_test/
+ or do { $status = 0; diag "wrong subject: $subject" };
+ }
+ ok $status, "all mails have correct data";
+}
+
+diag "set non-ascii subject tag for the queue" if $ENV{'TEST_VERBOSE'};
+{
+ my ($status, $msg) = $queue->SetSubjectTag( $str_ru_support );
+ ok $status, "set subject tag for the queue" or diag "error: $msg";
+}
+
+diag "non-ascii Subject without changes in template and with non-ascii subject tag"
+ if $ENV{'TEST_VERBOSE'};
+{
+ my $ticket = RT::Ticket->new( $RT::SystemUser );
+ $ticket->Create(
+ Queue => $queue->id,
+ Subject => $str_ru_test,
+ Requestor => 'root@localhost',
+ );
+ my @mails = RT::Test->fetch_caught_mails;
+ ok @mails, "got some outgoing emails";
+
+ my $status = 1;
+ foreach my $mail ( @mails ) {
+ my $entity = parse_mail( $mail );
+ my $subject = Encode::decode_utf8( $entity->head->get('Subject') );
+ $subject =~ /$str_ru_test/
+ or do { $status = 0; diag "wrong subject: $subject" };
+ $subject =~ /$str_ru_support/
+ or do { $status = 0; diag "wrong subject: $subject" };
+ }
+ ok $status, "all mails have correct data";
+}
+
+sub parse_mail {
+ my $mail = shift;
+ require RT::EmailParser;
+ my $parser = new RT::EmailParser;
+ $parser->ParseMIMEEntityFromScalar( $mail );
+ return $parser->Entity;
+}
+
diff --git a/rt/t/mail/crypt-gnupg.t b/rt/t/mail/crypt-gnupg.t
new file mode 100644
index 000000000..f33fbab1c
--- /dev/null
+++ b/rt/t/mail/crypt-gnupg.t
@@ -0,0 +1,312 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use RT::Test nodata => 1, tests => 92;
+plan skip_all => 'GnuPG required.'
+ unless eval 'use GnuPG::Interface; 1';
+plan skip_all => 'gpg executable is required.'
+ unless RT::Test->find_executable('gpg');
+
+
+use File::Spec ();
+use Cwd;
+
+my $homedir = RT::Test::get_abs_relocatable_dir(File::Spec->updir(),
+ qw(data gnupg keyrings) );
+
+mkdir $homedir;
+
+use_ok('RT::Crypt::GnuPG');
+use_ok('MIME::Entity');
+
+RT->Config->Set( 'GnuPG',
+ Enable => 1,
+ OutgoingMessagesFormat => 'RFC' );
+
+RT->Config->Set( 'GnuPGOptions',
+ homedir => $homedir,
+ 'no-permission-warning' => undef,
+);
+
+
+diag 'only signing. correct passphrase' if $ENV{'TEST_VERBOSE'};
+{
+ my $entity = MIME::Entity->build(
+ From => 'rt@example.com',
+ Subject => 'test',
+ Data => ['test'],
+ );
+ my %res = RT::Crypt::GnuPG::SignEncrypt( Entity => $entity, Encrypt => 0, Passphrase => 'test' );
+ ok( $entity, 'signed entity');
+ ok( !$res{'logger'}, "log is here as well" ) or diag $res{'logger'};
+ my @status = RT::Crypt::GnuPG::ParseStatus( $res{'status'} );
+ is( scalar @status, 2, 'two records: passphrase, signing');
+ is( $status[0]->{'Operation'}, 'PassphraseCheck', 'operation is correct');
+ is( $status[0]->{'Status'}, 'DONE', 'good passphrase');
+ is( $status[1]->{'Operation'}, 'Sign', 'operation is correct');
+ is( $status[1]->{'Status'}, 'DONE', 'done');
+ is( $status[1]->{'User'}->{'EmailAddress'}, 'rt@example.com', 'correct email');
+
+ ok( $entity->is_multipart, 'signed message is multipart' );
+ is( $entity->parts, 2, 'two parts' );
+
+ my @parts = RT::Crypt::GnuPG::FindProtectedParts( Entity => $entity );
+ is( scalar @parts, 1, 'one protected part' );
+ is( $parts[0]->{'Type'}, 'signed', "have signed part" );
+ is( $parts[0]->{'Format'}, 'RFC3156', "RFC3156 format" );
+ is( $parts[0]->{'Top'}, $entity, "it's the same entity" );
+
+ my @res = RT::Crypt::GnuPG::VerifyDecrypt( Entity => $entity );
+ is scalar @res, 1, 'one operation';
+ @status = RT::Crypt::GnuPG::ParseStatus( $res[0]{'status'} );
+ is( scalar @status, 1, 'one record');
+ is( $status[0]->{'Operation'}, 'Verify', 'operation is correct');
+ is( $status[0]->{'Status'}, 'DONE', 'good passphrase');
+ is( $status[0]->{'Trust'}, 'ULTIMATE', 'have trust value');
+}
+
+diag 'only signing. missing passphrase' if $ENV{'TEST_VERBOSE'};
+{
+ my $entity = MIME::Entity->build(
+ From => 'rt@example.com',
+ Subject => 'test',
+ Data => ['test'],
+ );
+ my %res = RT::Crypt::GnuPG::SignEncrypt( Entity => $entity, Encrypt => 0, Passphrase => '' );
+ ok( $res{'exit_code'}, "couldn't sign without passphrase");
+ ok( $res{'error'} || $res{'logger'}, "error is here" );
+
+ my @status = RT::Crypt::GnuPG::ParseStatus( $res{'status'} );
+ is( scalar @status, 1, 'one record');
+ is( $status[0]->{'Operation'}, 'PassphraseCheck', 'operation is correct');
+ is( $status[0]->{'Status'}, 'MISSING', 'missing passphrase');
+}
+
+diag 'only signing. wrong passphrase' if $ENV{'TEST_VERBOSE'};
+{
+ my $entity = MIME::Entity->build(
+ From => 'rt@example.com',
+ Subject => 'test',
+ Data => ['test'],
+ );
+ my %res = RT::Crypt::GnuPG::SignEncrypt( Entity => $entity, Encrypt => 0, Passphrase => 'wrong' );
+ ok( $res{'exit_code'}, "couldn't sign with bad passphrase");
+ ok( $res{'error'} || $res{'logger'}, "error is here" );
+
+ my @status = RT::Crypt::GnuPG::ParseStatus( $res{'status'} );
+ is( scalar @status, 1, 'one record');
+ is( $status[0]->{'Operation'}, 'PassphraseCheck', 'operation is correct');
+ is( $status[0]->{'Status'}, 'BAD', 'wrong passphrase');
+}
+
+diag 'encryption only' if $ENV{'TEST_VERBOSE'};
+{
+ my $entity = MIME::Entity->build(
+ From => 'rt@example.com',
+ To => 'rt@example.com',
+ Subject => 'test',
+ Data => ['test'],
+ );
+ my %res = RT::Crypt::GnuPG::SignEncrypt( Entity => $entity, Sign => 0 );
+ ok( !$res{'exit_code'}, "successful encryption" );
+ ok( !$res{'logger'}, "no records in logger" );
+
+ my @status = RT::Crypt::GnuPG::ParseStatus( $res{'status'} );
+ is( scalar @status, 1, 'one record');
+ is( $status[0]->{'Operation'}, 'Encrypt', 'operation is correct');
+ is( $status[0]->{'Status'}, 'DONE', 'done');
+
+ ok($entity, 'get an encrypted part');
+
+ my @parts = RT::Crypt::GnuPG::FindProtectedParts( Entity => $entity );
+ is( scalar @parts, 1, 'one protected part' );
+ is( $parts[0]->{'Type'}, 'encrypted', "have encrypted part" );
+ is( $parts[0]->{'Format'}, 'RFC3156', "RFC3156 format" );
+ is( $parts[0]->{'Top'}, $entity, "it's the same entity" );
+}
+
+diag 'encryption only, bad recipient' if $ENV{'TEST_VERBOSE'};
+{
+ my $entity = MIME::Entity->build(
+ From => 'rt@example.com',
+ To => 'keyless@example.com',
+ Subject => 'test',
+ Data => ['test'],
+ );
+ my %res = RT::Crypt::GnuPG::SignEncrypt( Entity => $entity, Sign => 0 );
+ ok( $res{'exit_code'}, 'no way to encrypt without keys of recipients');
+ ok( $res{'logger'}, "errors are in logger" );
+
+ my @status = RT::Crypt::GnuPG::ParseStatus( $res{'status'} );
+ is( scalar @status, 1, 'one record');
+ is( $status[0]->{'Keyword'}, 'INV_RECP', 'invalid recipient');
+}
+
+diag 'encryption and signing with combined method' if $ENV{'TEST_VERBOSE'};
+{
+ my $entity = MIME::Entity->build(
+ From => 'rt@example.com',
+ To => 'rt@example.com',
+ Subject => 'test',
+ Data => ['test'],
+ );
+ my %res = RT::Crypt::GnuPG::SignEncrypt( Entity => $entity, Passphrase => 'test' );
+ ok( !$res{'exit_code'}, "successful encryption with signing" );
+ ok( !$res{'logger'}, "no records in logger" );
+
+ my @status = RT::Crypt::GnuPG::ParseStatus( $res{'status'} );
+ is( scalar @status, 3, 'three records: passphrase, sign and encrypt');
+ is( $status[0]->{'Operation'}, 'PassphraseCheck', 'operation is correct');
+ is( $status[0]->{'Status'}, 'DONE', 'done');
+ is( $status[1]->{'Operation'}, 'Sign', 'operation is correct');
+ is( $status[1]->{'Status'}, 'DONE', 'done');
+ is( $status[2]->{'Operation'}, 'Encrypt', 'operation is correct');
+ is( $status[2]->{'Status'}, 'DONE', 'done');
+
+ ok($entity, 'get an encrypted and signed part');
+
+ my @parts = RT::Crypt::GnuPG::FindProtectedParts( Entity => $entity );
+ is( scalar @parts, 1, 'one protected part' );
+ is( $parts[0]->{'Type'}, 'encrypted', "have encrypted part" );
+ is( $parts[0]->{'Format'}, 'RFC3156', "RFC3156 format" );
+ is( $parts[0]->{'Top'}, $entity, "it's the same entity" );
+}
+
+diag 'encryption and signing with cascading, sign on encrypted' if $ENV{'TEST_VERBOSE'};
+{
+ my $entity = MIME::Entity->build(
+ From => 'rt@example.com',
+ To => 'rt@example.com',
+ Subject => 'test',
+ Data => ['test'],
+ );
+ my %res = RT::Crypt::GnuPG::SignEncrypt( Entity => $entity, Sign => 0 );
+ ok( !$res{'exit_code'}, 'successful encryption' );
+ ok( !$res{'logger'}, "no records in logger" );
+ %res = RT::Crypt::GnuPG::SignEncrypt( Entity => $entity, Encrypt => 0, Passphrase => 'test' );
+ ok( !$res{'exit_code'}, 'successful signing' );
+ ok( !$res{'logger'}, "no records in logger" );
+
+ my @parts = RT::Crypt::GnuPG::FindProtectedParts( Entity => $entity );
+ is( scalar @parts, 1, 'one protected part, top most' );
+ is( $parts[0]->{'Type'}, 'signed', "have signed part" );
+ is( $parts[0]->{'Format'}, 'RFC3156', "RFC3156 format" );
+ is( $parts[0]->{'Top'}, $entity, "it's the same entity" );
+}
+
+diag 'find signed/encrypted part deep inside' if $ENV{'TEST_VERBOSE'};
+{
+ my $entity = MIME::Entity->build(
+ From => 'rt@example.com',
+ To => 'rt@example.com',
+ Subject => 'test',
+ Data => ['test'],
+ );
+ my %res = RT::Crypt::GnuPG::SignEncrypt( Entity => $entity, Sign => 0 );
+ ok( !$res{'exit_code'}, "success" );
+ $entity->make_multipart( 'mixed', Force => 1 );
+ $entity->attach(
+ Type => 'text/plain',
+ Data => ['-'x76, 'this is mailing list'],
+ );
+
+ my @parts = RT::Crypt::GnuPG::FindProtectedParts( Entity => $entity );
+ is( scalar @parts, 1, 'one protected part' );
+ is( $parts[0]->{'Type'}, 'encrypted', "have encrypted part" );
+ is( $parts[0]->{'Format'}, 'RFC3156', "RFC3156 format" );
+ is( $parts[0]->{'Top'}, $entity->parts(0), "it's the same entity" );
+}
+
+diag 'wrong signed/encrypted parts: no protocol' if $ENV{'TEST_VERBOSE'};
+{
+ my $entity = MIME::Entity->build(
+ From => 'rt@example.com',
+ To => 'rt@example.com',
+ Subject => 'test',
+ Data => ['test'],
+ );
+ my %res = RT::Crypt::GnuPG::SignEncrypt( Entity => $entity, Sign => 0 );
+ ok( !$res{'exit_code'}, 'success' );
+ $entity->head->mime_attr( 'Content-Type.protocol' => undef );
+
+ my @parts = RT::Crypt::GnuPG::FindProtectedParts( Entity => $entity );
+ is( scalar @parts, 0, 'no protected parts' );
+}
+
+diag 'wrong signed/encrypted parts: not enought parts' if $ENV{'TEST_VERBOSE'};
+{
+ my $entity = MIME::Entity->build(
+ From => 'rt@example.com',
+ To => 'rt@example.com',
+ Subject => 'test',
+ Data => ['test'],
+ );
+ my %res = RT::Crypt::GnuPG::SignEncrypt( Entity => $entity, Sign => 0 );
+ ok( !$res{'exit_code'}, 'success' );
+ $entity->parts([ $entity->parts(0) ]);
+
+ my @parts = RT::Crypt::GnuPG::FindProtectedParts( Entity => $entity );
+ is( scalar @parts, 0, 'no protected parts' );
+}
+
+diag 'wrong signed/encrypted parts: wrong proto' if $ENV{'TEST_VERBOSE'};
+{
+ my $entity = MIME::Entity->build(
+ From => 'rt@example.com',
+ To => 'rt@example.com',
+ Subject => 'test',
+ Data => ['test'],
+ );
+ my %res = RT::Crypt::GnuPG::SignEncrypt( Entity => $entity, Sign => 0 );
+ ok( !$res{'exit_code'}, 'success' );
+ $entity->head->mime_attr( 'Content-Type.protocol' => 'application/bad-proto' );
+
+ my @parts = RT::Crypt::GnuPG::FindProtectedParts( Entity => $entity );
+ is( scalar @parts, 0, 'no protected parts' );
+}
+
+diag 'wrong signed/encrypted parts: wrong proto' if $ENV{'TEST_VERBOSE'};
+{
+ my $entity = MIME::Entity->build(
+ From => 'rt@example.com',
+ To => 'rt@example.com',
+ Subject => 'test',
+ Data => ['test'],
+ );
+ my %res = RT::Crypt::GnuPG::SignEncrypt( Entity => $entity, Encrypt => 0, Passphrase => 'test' );
+ ok( !$res{'exit_code'}, 'success' );
+ $entity->head->mime_attr( 'Content-Type.protocol' => 'application/bad-proto' );
+
+ my @parts = RT::Crypt::GnuPG::FindProtectedParts( Entity => $entity );
+ is( scalar @parts, 0, 'no protected parts' );
+}
+
+diag 'verify inline and in attachment signatures' if $ENV{'TEST_VERBOSE'};
+{
+ open my $fh, "$homedir/signed_old_style_with_attachment.eml";
+ my $parser = new MIME::Parser;
+ my $entity = $parser->parse( $fh );
+
+ my @parts = RT::Crypt::GnuPG::FindProtectedParts( Entity => $entity );
+ is( scalar @parts, 2, 'two protected parts' );
+ is( $parts[1]->{'Type'}, 'signed', "have signed part" );
+ is( $parts[1]->{'Format'}, 'Inline', "inline format" );
+ is( $parts[1]->{'Data'}, $entity->parts(0), "it's first part" );
+
+ is( $parts[0]->{'Type'}, 'signed', "have signed part" );
+ is( $parts[0]->{'Format'}, 'Attachment', "attachment format" );
+ is( $parts[0]->{'Data'}, $entity->parts(1), "data in second part" );
+ is( $parts[0]->{'Signature'}, $entity->parts(2), "file's signature in third part" );
+
+ my @res = RT::Crypt::GnuPG::VerifyDecrypt( Entity => $entity );
+ my @status = RT::Crypt::GnuPG::ParseStatus( $res[0]->{'status'} );
+ is( scalar @status, 1, 'one record');
+ is( $status[0]->{'Operation'}, 'Verify', 'operation is correct');
+ is( $status[0]->{'Status'}, 'DONE', 'good passphrase');
+ is( $status[0]->{'Trust'}, 'ULTIMATE', 'have trust value');
+
+ $parser->filer->purge();
+}
+
diff --git a/rt/t/mail/extractsubjecttag.t b/rt/t/mail/extractsubjecttag.t
new file mode 100644
index 000000000..5a2548883
--- /dev/null
+++ b/rt/t/mail/extractsubjecttag.t
@@ -0,0 +1,98 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+use utf8;
+
+use RT::Test tests => 14;
+
+
+my ($baseurl, $m) = RT::Test->started_ok;
+RT::Test->set_mail_catcher;
+
+my $queue = RT::Test->load_or_create_queue(
+ Name => 'Regression',
+ CorrespondAddress => 'rt-recipient@example.com',
+ CommentAddress => 'rt-recipient@example.com',
+);
+my $subject_tag = 'Windows/Servers-Desktops';
+ok $queue && $queue->id, 'loaded or created queue';
+
+diag "Set Subject Tag" if $ENV{'TEST_VERBOSE'};
+{
+ is(RT->System->SubjectTag($queue), undef, 'No Subject Tag yet');
+ my ($status, $msg) = $queue->SetSubjectTag( $subject_tag );
+ ok $status, "set subject tag for the queue" or diag "error: $msg";
+ is(RT->System->SubjectTag($queue), $subject_tag, "Set Subject Tag to $subject_tag");
+}
+
+my $original_ticket = RT::Ticket->new( $RT::SystemUser );
+diag "Create a ticket and make sure it has the subject tag" if $ENV{'TEST_VERBOSE'};
+{
+ $original_ticket->Create(
+ Queue => $queue->id,
+ Subject => 'test',
+ Requestor => 'root@localhost'
+ );
+ my @mails = RT::Test->fetch_caught_mails;
+ ok @mails, "got some outgoing emails";
+
+ my $status = 1;
+ foreach my $mail ( @mails ) {
+ my $entity = parse_mail( $mail );
+ my $subject = $entity->head->get('Subject');
+ $subject =~ /\[\Q$subject_tag\E #\d+\]/
+ or do { $status = 0; diag "wrong subject: $subject" };
+ }
+ ok $status, "Correctly added subject tag to ticket";
+}
+
+
+diag "Test that a reply with a Subject Tag doesn't change the subject" if $ENV{'TEST_VERBOSE'};
+{
+ my $ticketid = $original_ticket->Id;
+ my $text = <<EOF;
+From: root\@localhost
+To: general\@$RT::rtname
+Subject: [$subject_tag #$ticketid] test
+
+reply with subject tag
+EOF
+ my ($status, $id) = RT::Test->send_via_mailgate($text, queue => $queue->Name);
+ is ($status >> 8, 0, "The mail gateway exited normally");
+ is ($id, $ticketid, "Replied to ticket $id correctly");
+
+ my $freshticket = RT::Ticket->new( $RT::SystemUser );
+ $freshticket->LoadById($id);
+ is($original_ticket->Subject,$freshticket->Subject,'Stripped Queue Subject Tag correctly');
+
+}
+
+diag "Test that a reply with another RT's subject tag changes the subject" if $ENV{'TEST_VERBOSE'};
+{
+ my $ticketid = $original_ticket->Id;
+ my $text = <<EOF;
+From: root\@localhost
+To: general\@$RT::rtname
+Subject: [$subject_tag #$ticketid] [remote-rt-system #79] test
+
+reply with subject tag and remote rt subject tag
+EOF
+ diag($text);
+ my ($status, $id) = RT::Test->send_via_mailgate($text, queue => $queue->Name);
+ is ($status >> 8, 0, "The mail gateway exited normally");
+ is ($id, $ticketid, "Replied to ticket $id correctly");
+
+ my $freshticket = RT::Ticket->new( $RT::SystemUser );
+ $freshticket->LoadById($id);
+ like($freshticket->Subject,qr/\[remote-rt-system #79\]/,"Kept remote rt's subject tag");
+ unlike($freshticket->Subject,qr/\[\Q$subject_tag\E #$ticketid\]/,'Stripped Queue Subject Tag correctly');
+
+}
+
+sub parse_mail {
+ my $mail = shift;
+ require RT::EmailParser;
+ my $parser = new RT::EmailParser;
+ $parser->ParseMIMEEntityFromScalar( $mail );
+ return $parser->Entity;
+}
diff --git a/rt/t/mail/gateway.t b/rt/t/mail/gateway.t
new file mode 100644
index 000000000..00de1ec7f
--- /dev/null
+++ b/rt/t/mail/gateway.t
@@ -0,0 +1,802 @@
+#!/usr/bin/perl -w
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2004 Best Practical Solutions, LLC
+# <jesse.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/copyleft/gpl.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+
+=head1 NAME
+
+rt-mailgate - Mail interface to RT3.
+
+=cut
+
+use strict;
+use warnings;
+
+
+use RT::Test config => 'Set( $UnsafeEmailCommands, 1);', tests => 159;
+my ($baseurl, $m) = RT::Test->started_ok;
+
+use RT::Tickets;
+
+use MIME::Entity;
+use Digest::MD5 qw(md5_base64);
+use LWP::UserAgent;
+
+# TODO: --extension queue
+
+my $url = $m->rt_base_url;
+
+diag "Make sure that when we call the mailgate without URL, it fails" if $ENV{'TEST_VERBOSE'};
+{
+ my $text = <<EOF;
+From: root\@localhost
+To: rt\@@{[RT->Config->Get('rtname')]}
+Subject: This is a test of new ticket creation
+
+Foob!
+EOF
+ my ($status, $id) = RT::Test->send_via_mailgate($text, url => undef);
+ is ($status >> 8, 1, "The mail gateway exited with a failure");
+ ok (!$id, "No ticket id") or diag "by mistake ticket #$id";
+}
+
+diag "Make sure that when we call the mailgate with wrong URL, it tempfails" if $ENV{'TEST_VERBOSE'};
+{
+ my $text = <<EOF;
+From: root\@localhost
+To: rt\@@{[RT->Config->Get('rtname')]}
+Subject: This is a test of new ticket creation
+
+Foob!
+EOF
+ my ($status, $id) = RT::Test->send_via_mailgate($text, url => 'http://this.test.for.non-connection.is.expected.to.generate.an.error');
+ is ($status >> 8, 75, "The mail gateway exited with a failure");
+ ok (!$id, "No ticket id");
+}
+
+my $everyone_group;
+diag "revoke rights tests depend on" if $ENV{'TEST_VERBOSE'};
+{
+ $everyone_group = RT::Group->new( $RT::SystemUser );
+ $everyone_group->LoadSystemInternalGroup( 'Everyone' );
+ ok ($everyone_group->Id, "Found group 'everyone'");
+
+ foreach( qw(CreateTicket ReplyToTicket CommentOnTicket) ) {
+ $everyone_group->PrincipalObj->RevokeRight(Right => $_);
+ }
+}
+
+diag "Test new ticket creation by root who is privileged and superuser" if $ENV{'TEST_VERBOSE'};
+{
+ my $text = <<EOF;
+From: root\@localhost
+To: rt\@@{[RT->Config->Get('rtname')]}
+Subject: This is a test of new ticket creation
+
+Blah!
+Foob!
+EOF
+
+ my ($status, $id) = RT::Test->send_via_mailgate($text);
+ is ($status >> 8, 0, "The mail gateway exited normally");
+ ok ($id, "Created ticket");
+
+ my $tick = RT::Test->last_ticket;
+ isa_ok ($tick, 'RT::Ticket');
+ is ($tick->Id, $id, "correct ticket id");
+ is ($tick->Subject , 'This is a test of new ticket creation', "Created the ticket");
+}
+
+diag "Test the 'X-RT-Mail-Extension' field in the header of a ticket" if $ENV{'TEST_VERBOSE'};
+{
+ my $text = <<EOF;
+From: root\@localhost
+To: rt\@@{[RT->Config->Get('rtname')]}
+Subject: This is a test of the X-RT-Mail-Extension field
+Blah!
+Foob!
+EOF
+ local $ENV{'EXTENSION'} = "bad value with\nnewlines\n";
+ my ($status, $id) = RT::Test->send_via_mailgate($text);
+ is ($status >> 8, 0, "The mail gateway exited normally");
+ ok ($id, "Created ticket #$id");
+
+ my $tick = RT::Test->last_ticket;
+ isa_ok ($tick, 'RT::Ticket');
+ is ($tick->Id, $id, "correct ticket id");
+ is ($tick->Subject, 'This is a test of the X-RT-Mail-Extension field', "Created the ticket");
+
+ my $transactions = $tick->Transactions;
+ $transactions->OrderByCols({ FIELD => 'id', ORDER => 'DESC' });
+ $transactions->Limit( FIELD => 'Type', OPERATOR => '!=', VALUE => 'EmailRecord');
+ my $txn = $transactions->First;
+ isa_ok ($txn, 'RT::Transaction');
+ is ($txn->Type, 'Create', "correct type");
+
+ my $attachment = $txn->Attachments->First;
+ isa_ok ($attachment, 'RT::Attachment');
+ # XXX: We eat all newlines in header, that's not what RFC's suggesting
+ is (
+ $attachment->GetHeader('X-RT-Mail-Extension'),
+ "bad value with newlines",
+ 'header is in place, without trailing newline char'
+ );
+}
+
+diag "Make sure that not standard --extension is passed" if $ENV{'TEST_VERBOSE'};
+{
+ my $text = <<EOF;
+From: root\@localhost
+To: rt\@@{[RT->Config->Get('rtname')]}
+Subject: This is a test of new ticket creation
+
+Foob!
+EOF
+ my ($status, $id) = RT::Test->send_via_mailgate($text, extension => 'some-extension-arg' );
+ is ($status >> 8, 0, "The mail gateway exited normally");
+ ok ($id, "Created ticket #$id");
+
+ my $tick = RT::Test->last_ticket;
+ isa_ok ($tick, 'RT::Ticket');
+ is ($tick->Id, $id, "correct ticket id");
+
+ my $transactions = $tick->Transactions;
+ $transactions->OrderByCols({ FIELD => 'id', ORDER => 'DESC' });
+ $transactions->Limit( FIELD => 'Type', OPERATOR => '!=', VALUE => 'EmailRecord');
+ my $txn = $transactions->First;
+ isa_ok ($txn, 'RT::Transaction');
+ is ($txn->Type, 'Create', "correct type");
+
+ my $attachment = $txn->Attachments->First;
+ isa_ok ($attachment, 'RT::Attachment');
+ is (
+ $attachment->GetHeader('X-RT-Mail-Extension'),
+ 'some-extension-arg',
+ 'header is in place'
+ );
+}
+
+diag "Test new ticket creation without --action argument" if $ENV{'TEST_VERBOSE'};
+{
+ my $text = <<EOF;
+From: root\@localhost
+To: rt\@$RT::rtname
+Subject: using mailgate without --action arg
+
+Blah!
+Foob!
+EOF
+ my ($status, $id) = RT::Test->send_via_mailgate($text, extension => 'some-extension-arg' );
+ is ($status >> 8, 0, "The mail gateway exited normally");
+ ok ($id, "Created ticket #$id");
+
+ my $tick = RT::Test->last_ticket;
+ isa_ok ($tick, 'RT::Ticket');
+ is ($tick->Id, $id, "correct ticket id");
+ is ($tick->Subject, 'using mailgate without --action arg', "using mailgate without --action arg");
+}
+
+diag "This is a test of new ticket creation as an unknown user" if $ENV{'TEST_VERBOSE'};
+{
+ my $text = <<EOF;
+From: doesnotexist\@@{[RT->Config->Get('rtname')]}
+To: rt\@@{[RT->Config->Get('rtname')]}
+Subject: This is a test of new ticket creation as an unknown user
+
+Blah!
+Foob!
+EOF
+ my ($status, $id) = RT::Test->send_via_mailgate($text);
+ is ($status >> 8, 0, "The mail gateway exited normally");
+ ok (!$id, "no ticket created");
+
+ my $tick = RT::Test->last_ticket;
+ isa_ok ($tick, 'RT::Ticket');
+ ok ($tick->Id, "found ticket ".$tick->Id);
+ isnt ($tick->Subject , 'This is a test of new ticket creation as an unknown user', "failed to create the new ticket from an unprivileged account");
+
+ my $u = RT::User->new($RT::SystemUser);
+ $u->Load("doesnotexist\@@{[RT->Config->Get('rtname')]}");
+ ok( !$u->Id, "user does not exist and was not created by failed ticket submission");
+}
+
+diag "grant everybody with CreateTicket right" if $ENV{'TEST_VERBOSE'};
+{
+ ok( RT::Test->set_rights(
+ { Principal => $everyone_group->PrincipalObj,
+ Right => [qw(CreateTicket)],
+ },
+ ), "Granted everybody the right to create tickets");
+}
+
+my $ticket_id;
+diag "now everybody can create tickets. can a random unkown user create tickets?" if $ENV{'TEST_VERBOSE'};
+{
+ my $text = <<EOF;
+From: doesnotexist\@@{[RT->Config->Get('rtname')]}
+To: rt\@@{[RT->Config->Get('rtname')]}
+Subject: This is a test of new ticket creation as an unknown user
+
+Blah!
+Foob!
+EOF
+ my ($status, $id) = RT::Test->send_via_mailgate($text);
+ is ($status >> 8, 0, "The mail gateway exited normally");
+ ok ($id, "ticket created");
+
+ my $tick = RT::Test->last_ticket;
+ isa_ok ($tick, 'RT::Ticket');
+ ok ($tick->Id, "found ticket ".$tick->Id);
+ is ($tick->Id, $id, "correct ticket id");
+ is ($tick->Subject , 'This is a test of new ticket creation as an unknown user', "failed to create the new ticket from an unprivileged account");
+
+ my $u = RT::User->new( $RT::SystemUser );
+ $u->Load( "doesnotexist\@@{[RT->Config->Get('rtname')]}" );
+ ok ($u->Id, "user does not exist and was created by ticket submission");
+ $ticket_id = $id;
+}
+
+diag "can another random reply to a ticket without being granted privs? answer should be no." if $ENV{'TEST_VERBOSE'};
+{
+ my $text = <<EOF;
+From: doesnotexist-2\@@{[RT->Config->Get('rtname')]}
+To: rt\@@{[RT->Config->Get('rtname')]}
+Subject: [@{[RT->Config->Get('rtname')]} #$ticket_id] This is a test of a reply as an unknown user
+
+Blah! (Should not work.)
+Foob!
+EOF
+ my ($status, $id) = RT::Test->send_via_mailgate($text);
+ is ($status >> 8, 0, "The mail gateway exited normally");
+ ok (!$id, "no way to reply to the ticket");
+
+ my $u = RT::User->new($RT::SystemUser);
+ $u->Load('doesnotexist-2@'.RT->Config->Get('rtname'));
+ ok( !$u->Id, " user does not exist and was not created by ticket correspondence submission");
+}
+
+diag "grant everyone 'ReplyToTicket' right" if $ENV{'TEST_VERBOSE'};
+{
+ ok( RT::Test->set_rights(
+ { Principal => $everyone_group->PrincipalObj,
+ Right => [qw(CreateTicket ReplyToTicket)],
+ },
+ ), "Granted everybody the right to reply to tickets" );
+}
+
+diag "can another random reply to a ticket after being granted privs? answer should be yes" if $ENV{'TEST_VERBOSE'};
+{
+ my $text = <<EOF;
+From: doesnotexist-2\@@{[RT->Config->Get('rtname')]}
+To: rt\@@{[RT->Config->Get('rtname')]}
+Subject: [@{[RT->Config->Get('rtname')]} #$ticket_id] This is a test of a reply as an unknown user
+
+Blah!
+Foob!
+EOF
+ my ($status, $id) = RT::Test->send_via_mailgate($text);
+ is ($status >> 8, 0, "The mail gateway exited normally");
+ is ($id, $ticket_id, "replied to the ticket");
+
+ my $u = RT::User->new($RT::SystemUser);
+ $u->Load('doesnotexist-2@'.RT->Config->Get('rtname'));
+ ok ($u->Id, "user exists and was created by ticket correspondence submission");
+}
+
+diag "add a reply to the ticket using '--extension ticket' feature" if $ENV{'TEST_VERBOSE'};
+{
+ my $text = <<EOF;
+From: doesnotexist-2\@@{[RT->Config->Get('rtname')]}
+To: rt\@@{[RT->Config->Get('rtname')]}
+Subject: This is a test of a reply as an unknown user
+
+Blah!
+Foob!
+EOF
+ local $ENV{'EXTENSION'} = $ticket_id;
+ my ($status, $id) = RT::Test->send_via_mailgate($text, extension => 'ticket');
+ is ($status >> 8, 0, "The mail gateway exited normally");
+ is ($id, $ticket_id, "replied to the ticket");
+
+ my $tick = RT::Test->last_ticket;
+ isa_ok ($tick, 'RT::Ticket');
+ ok ($tick->Id, "found ticket ".$tick->Id);
+ is ($tick->Id, $id, "correct ticket id");
+
+ my $transactions = $tick->Transactions;
+ $transactions->OrderByCols({ FIELD => 'id', ORDER => 'DESC' });
+ $transactions->Limit( FIELD => 'Type', OPERATOR => '!=', VALUE => 'EmailRecord');
+ my $txn = $transactions->First;
+ isa_ok ($txn, 'RT::Transaction');
+ is ($txn->Type, 'Correspond', "correct type");
+
+ my $attachment = $txn->Attachments->First;
+ isa_ok ($attachment, 'RT::Attachment');
+ is ($attachment->GetHeader('X-RT-Mail-Extension'), $id, 'header is in place');
+}
+
+diag "can another random comment on a ticket without being granted privs? answer should be no" if $ENV{'TEST_VERBOSE'};
+{
+ my $text = <<EOF;
+From: doesnotexist-3\@@{[RT->Config->Get('rtname')]}
+To: rt\@@{[RT->Config->Get('rtname')]}
+Subject: [@{[RT->Config->Get('rtname')]} #$ticket_id] This is a test of a comment as an unknown user
+
+Blah! (Should not work.)
+Foob!
+EOF
+ my ($status, $id) = RT::Test->send_via_mailgate($text, action => 'comment');
+ is ($status >> 8, 0, "The mail gateway exited normally");
+ ok (!$id, "no way to comment on the ticket");
+
+ my $u = RT::User->new($RT::SystemUser);
+ $u->Load('doesnotexist-3@'.RT->Config->Get('rtname'));
+ ok( !$u->Id, " user does not exist and was not created by ticket comment submission");
+}
+
+
+diag "grant everyone 'CommentOnTicket' right" if $ENV{'TEST_VERBOSE'};
+{
+ ok( RT::Test->set_rights(
+ { Principal => $everyone_group->PrincipalObj,
+ Right => [qw(CreateTicket ReplyToTicket CommentOnTicket)],
+ },
+ ), "Granted everybody the right to comment on tickets");
+}
+
+diag "can another random reply to a ticket after being granted privs? answer should be yes" if $ENV{'TEST_VERBOSE'};
+{
+ my $text = <<EOF;
+From: doesnotexist-3\@@{[RT->Config->Get('rtname')]}
+To: rt\@@{[RT->Config->Get('rtname')]}
+Subject: [@{[RT->Config->Get('rtname')]} #$ticket_id] This is a test of a comment as an unknown user
+
+Blah!
+Foob!
+EOF
+ my ($status, $id) = RT::Test->send_via_mailgate($text, action => 'comment');
+ is ($status >> 8, 0, "The mail gateway exited normally");
+ is ($id, $ticket_id, "replied to the ticket");
+
+ my $u = RT::User->new($RT::SystemUser);
+ $u->Load('doesnotexist-3@'.RT->Config->Get('rtname'));
+ ok ($u->Id, " user exists and was created by ticket comment submission");
+}
+
+diag "add comment to the ticket using '--extension action' feature" if $ENV{'TEST_VERBOSE'};
+{
+ my $text = <<EOF;
+From: doesnotexist-3\@@{[RT->Config->Get('rtname')]}
+To: rt\@@{[RT->Config->Get('rtname')]}
+Subject: [@{[RT->Config->Get('rtname')]} #$ticket_id] This is a test of a comment via '--extension action'
+
+Blah!
+Foob!
+EOF
+ local $ENV{'EXTENSION'} = 'comment';
+ my ($status, $id) = RT::Test->send_via_mailgate($text, extension => 'action');
+ is ($status >> 8, 0, "The mail gateway exited normally");
+ is ($id, $ticket_id, "added comment to the ticket");
+
+ my $tick = RT::Test->last_ticket;
+ isa_ok ($tick, 'RT::Ticket');
+ ok ($tick->Id, "found ticket ".$tick->Id);
+ is ($tick->Id, $id, "correct ticket id");
+
+ my $transactions = $tick->Transactions;
+ $transactions->OrderByCols({ FIELD => 'id', ORDER => 'DESC' });
+ $transactions->Limit(
+ FIELD => 'Type',
+ OPERATOR => 'NOT ENDSWITH',
+ VALUE => 'EmailRecord',
+ ENTRYAGGREGATOR => 'AND',
+ );
+ my $txn = $transactions->First;
+ isa_ok ($txn, 'RT::Transaction');
+ is ($txn->Type, 'Comment', "correct type");
+
+ my $attachment = $txn->Attachments->First;
+ isa_ok ($attachment, 'RT::Attachment');
+ is ($attachment->GetHeader('X-RT-Mail-Extension'), 'comment', 'header is in place');
+}
+
+diag "Testing preservation of binary attachments" if $ENV{'TEST_VERBOSE'};
+{
+ # Get a binary blob (Best Practical logo)
+ my $LOGO_FILE = $RT::MasonComponentRoot .'/NoAuth/images/bplogo.gif';
+
+ # Create a mime entity with an attachment
+ my $entity = MIME::Entity->build(
+ From => 'root@localhost',
+ To => 'rt@localhost',
+ Subject => 'binary attachment test',
+ Data => ['This is a test of a binary attachment'],
+ );
+
+ $entity->attach(
+ Path => $LOGO_FILE,
+ Type => 'image/gif',
+ Encoding => 'base64',
+ );
+ # Create a ticket with a binary attachment
+ my ($status, $id) = RT::Test->send_via_mailgate($entity);
+ is ($status >> 8, 0, "The mail gateway exited normally");
+ ok ($id, "created ticket");
+
+ my $tick = RT::Test->last_ticket;
+ isa_ok ($tick, 'RT::Ticket');
+ ok ($tick->Id, "found ticket ".$tick->Id);
+ is ($tick->Id, $id, "correct ticket id");
+ is ($tick->Subject , 'binary attachment test', "Created the ticket - ".$tick->Id);
+
+ my $file = `cat $LOGO_FILE`;
+ ok ($file, "Read in the logo image");
+ diag "for the raw file the md5 hex is ". Digest::MD5::md5_hex($file) if $ENV{'TEST_VERBOSE'};
+
+ # Verify that the binary attachment is valid in the database
+ my $attachments = RT::Attachments->new($RT::SystemUser);
+ $attachments->Limit(FIELD => 'ContentType', VALUE => 'image/gif');
+ my $txn_alias = $attachments->Join(
+ ALIAS1 => 'main',
+ FIELD1 => 'TransactionId',
+ TABLE2 => 'Transactions',
+ FIELD2 => 'id',
+ );
+ $attachments->Limit( ALIAS => $txn_alias, FIELD => 'ObjectType', VALUE => 'RT::Ticket' );
+ $attachments->Limit( ALIAS => $txn_alias, FIELD => 'ObjectId', VALUE => $id );
+ is ($attachments->Count, 1, 'Found only one gif attached to the ticket');
+ my $attachment = $attachments->First;
+ ok ($attachment->Id, 'loaded attachment object');
+ my $acontent = $attachment->Content;
+
+ diag "coming from the database, md5 hex is ".Digest::MD5::md5_hex($acontent) if $ENV{'TEST_VERBOSE'};
+ is ($acontent, $file, 'The attachment isn\'t screwed up in the database.');
+
+ # Grab the binary attachment via the web ui
+ my $ua = new LWP::UserAgent;
+ my $full_url = "$url/Ticket/Attachment/". $attachment->TransactionId
+ ."/". $attachment->id. "/bplogo.gif?&user=root&pass=password";
+ my $r = $ua->get( $full_url );
+
+ # Verify that the downloaded attachment is the same as what we uploaded.
+ is ($file, $r->content, 'The attachment isn\'t screwed up in download');
+}
+
+diag "Simple I18N testing" if $ENV{'TEST_VERBOSE'};
+{
+ my $text = <<EOF;
+From: root\@localhost
+To: rtemail\@@{[RT->Config->Get('rtname')]}
+Subject: This is a test of I18N ticket creation
+Content-Type: text/plain; charset="utf-8"
+
+2 accented lines
+\303\242\303\252\303\256\303\264\303\273
+\303\241\303\251\303\255\303\263\303\272
+bye
+EOF
+ my ($status, $id) = RT::Test->send_via_mailgate($text);
+ is ($status >> 8, 0, "The mail gateway exited normally");
+ ok ($id, "created ticket");
+
+ my $tick = RT::Test->last_ticket;
+ isa_ok ($tick, 'RT::Ticket');
+ ok ($tick->Id, "found ticket ". $tick->Id);
+ is ($tick->Id, $id, "correct ticket");
+ is ($tick->Subject , 'This is a test of I18N ticket creation', "Created the ticket - ". $tick->Subject);
+
+ my $unistring = "\303\241\303\251\303\255\303\263\303\272";
+ Encode::_utf8_on($unistring);
+ is (
+ $tick->Transactions->First->Content,
+ $tick->Transactions->First->Attachments->First->Content,
+ "Content is ". $tick->Transactions->First->Attachments->First->Content
+ );
+ ok (
+ $tick->Transactions->First->Content =~ /$unistring/i,
+ $tick->Id." appears to be unicode ". $tick->Transactions->First->Attachments->First->Id
+ );
+}
+
+diag "supposedly I18N fails on the second message sent in." if $ENV{'TEST_VERBOSE'};
+{
+ my $text = <<EOF;
+From: root\@localhost
+To: rtemail\@@{[RT->Config->Get('rtname')]}
+Subject: This is a test of I18N ticket creation
+Content-Type: text/plain; charset="utf-8"
+
+2 accented lines
+\303\242\303\252\303\256\303\264\303\273
+\303\241\303\251\303\255\303\263\303\272
+bye
+EOF
+ my ($status, $id) = RT::Test->send_via_mailgate($text);
+ is ($status >> 8, 0, "The mail gateway exited normally");
+ ok ($id, "created ticket");
+
+ my $tick = RT::Test->last_ticket;
+ isa_ok ($tick, 'RT::Ticket');
+ ok ($tick->Id, "found ticket ". $tick->Id);
+ is ($tick->Id, $id, "correct ticket");
+ is ($tick->Subject , 'This is a test of I18N ticket creation', "Created the ticket");
+
+ my $unistring = "\303\241\303\251\303\255\303\263\303\272";
+ Encode::_utf8_on($unistring);
+
+ ok (
+ $tick->Transactions->First->Content =~ $unistring,
+ "It appears to be unicode - ". $tick->Transactions->First->Content
+ );
+}
+
+diag "check that mailgate doesn't suffer from empty Reply-To:" if $ENV{'TEST_VERBOSE'};
+{
+ my $text = <<EOF;
+From: root\@localhost
+Reply-To:
+To: rtemail\@@{[RT->Config->Get('rtname')]}
+Subject: test
+Content-Type: text/plain; charset="utf-8"
+
+test
+EOF
+ my ($status, $id) = RT::Test->send_via_mailgate($text);
+ is ($status >> 8, 0, "The mail gateway exited normally");
+ ok ($id, "created ticket");
+
+ my $tick = RT::Test->last_ticket;
+ isa_ok ($tick, 'RT::Ticket');
+ ok ($tick->Id, "found ticket ". $tick->Id);
+ is ($tick->Id, $id, "correct ticket");
+
+ like $tick->RequestorAddresses, qr/root\@localhost/, 'correct requestor';
+}
+
+
+my ($val,$msg) = $everyone_group->PrincipalObj->RevokeRight(Right => 'CreateTicket');
+ok ($val, $msg);
+
+SKIP: {
+skip "Advanced mailgate actions require an unsafe configuration", 47
+ unless RT->Config->Get('UnsafeEmailCommands');
+
+# create new queue to be shure we don't mess with rights
+use RT::Queue;
+my $queue = RT::Queue->new($RT::SystemUser);
+my ($qid) = $queue->Create( Name => 'ext-mailgate');
+ok( $qid, 'queue created for ext-mailgate tests' );
+
+# {{{ Check take and resolve actions
+
+# create ticket that is owned by nobody
+use RT::Ticket;
+my $tick = RT::Ticket->new($RT::SystemUser);
+my ($id) = $tick->Create( Queue => 'ext-mailgate', Subject => 'test');
+ok( $id, 'new ticket created' );
+is( $tick->Owner, $RT::Nobody->Id, 'owner of the new ticket is nobody' );
+
+$! = 0;
+ok(open(MAIL, "|$RT::BinPath/rt-mailgate --url $url --queue ext-mailgate --action take"), "Opened the mailgate - $!");
+print MAIL <<EOF;
+From: root\@localhost
+Subject: [@{[RT->Config->Get('rtname')]} \#$id] test
+
+EOF
+close (MAIL);
+is ($? >> 8, 0, "The mail gateway exited normally");
+
+$tick = RT::Ticket->new($RT::SystemUser);
+$tick->Load( $id );
+is( $tick->Id, $id, 'load correct ticket');
+is( $tick->OwnerObj->EmailAddress, 'root@localhost', 'successfuly take ticket via email');
+
+# check that there is no text transactions writen
+is( $tick->Transactions->Count, 2, 'no superfluous transactions');
+
+my $status;
+($status, $msg) = $tick->SetOwner( $RT::Nobody->Id, 'Force' );
+ok( $status, 'successfuly changed owner: '. ($msg||'') );
+is( $tick->Owner, $RT::Nobody->Id, 'set owner back to nobody');
+
+
+$! = 0;
+ok(open(MAIL, "|$RT::BinPath/rt-mailgate --url $url --queue ext-mailgate --action take-correspond"), "Opened the mailgate - $@");
+print MAIL <<EOF;
+From: root\@localhost
+Subject: [@{[RT->Config->Get('rtname')]} \#$id] correspondence
+
+test
+EOF
+close (MAIL);
+is ($? >> 8, 0, "The mail gateway exited normally");
+
+DBIx::SearchBuilder::Record::Cachable->FlushCache;
+
+$tick = RT::Ticket->new($RT::SystemUser);
+$tick->Load( $id );
+is( $tick->Id, $id, "load correct ticket #$id");
+is( $tick->OwnerObj->EmailAddress, 'root@localhost', 'successfuly take ticket via email');
+my $txns = $tick->Transactions;
+$txns->Limit( FIELD => 'Type', VALUE => 'Correspond');
+$txns->OrderBy( FIELD => 'id', ORDER => 'DESC' );
+# +1 because of auto open
+is( $tick->Transactions->Count, 6, 'no superfluous transactions');
+is( $txns->First->Subject, "[$RT::rtname \#$id] correspondence", 'successfuly add correspond within take via email' );
+
+$! = 0;
+ok(open(MAIL, "|$RT::BinPath/rt-mailgate --url $url --queue ext-mailgate --action resolve"), "Opened the mailgate - $!");
+print MAIL <<EOF;
+From: root\@localhost
+Subject: [@{[RT->Config->Get('rtname')]} \#$id] test
+
+EOF
+close (MAIL);
+is ($? >> 8, 0, "The mail gateway exited normally");
+
+DBIx::SearchBuilder::Record::Cachable->FlushCache;
+
+$tick = RT::Ticket->new($RT::SystemUser);
+$tick->Load( $id );
+is( $tick->Id, $id, 'load correct ticket');
+is( $tick->Status, 'resolved', 'successfuly resolved ticket via email');
+is( $tick->Transactions->Count, 7, 'no superfluous transactions');
+
+use RT::User;
+my $user = RT::User->new( $RT::SystemUser );
+my ($uid) = $user->Create( Name => 'ext-mailgate',
+ EmailAddress => 'ext-mailgate@localhost',
+ Privileged => 1,
+ Password => 'qwe123',
+ );
+ok( $uid, 'user created for ext-mailgate tests' );
+ok( !$user->HasRight( Right => 'OwnTicket', Object => $queue ), "User can't own ticket" );
+
+$tick = RT::Ticket->new($RT::SystemUser);
+($id) = $tick->Create( Queue => $qid, Subject => 'test' );
+ok( $id, 'create new ticket' );
+
+my $rtname = RT->Config->Get('rtname');
+
+$! = 0;
+ok(open(MAIL, "|$RT::BinPath/rt-mailgate --url $url --queue ext-mailgate --action take"), "Opened the mailgate - $!");
+print MAIL <<EOF;
+From: ext-mailgate\@localhost
+Subject: [$rtname \#$id] test
+
+EOF
+close (MAIL);
+is ( $? >> 8, 0, "mailgate exited normally" );
+DBIx::SearchBuilder::Record::Cachable->FlushCache;
+
+cmp_ok( $tick->Owner, '!=', $user->id, "we didn't change owner" );
+
+($status, $msg) = $user->PrincipalObj->GrantRight( Object => $queue, Right => 'ReplyToTicket' );
+ok( $status, "successfuly granted right: $msg" );
+my $ace_id = $status;
+ok( $user->HasRight( Right => 'ReplyToTicket', Object => $tick ), "User can reply to ticket" );
+
+$! = 0;
+ok(open(MAIL, "|$RT::BinPath/rt-mailgate --url $url --queue ext-mailgate --action correspond-take"), "Opened the mailgate - $!");
+print MAIL <<EOF;
+From: ext-mailgate\@localhost
+Subject: [$rtname \#$id] test
+
+correspond-take
+EOF
+close (MAIL);
+is ( $? >> 8, 0, "mailgate exited normally" );
+DBIx::SearchBuilder::Record::Cachable->FlushCache;
+
+cmp_ok( $tick->Owner, '!=', $user->id, "we didn't change owner" );
+is( $tick->Transactions->Count, 3, "one transactions added" );
+
+$! = 0;
+ok(open(MAIL, "|$RT::BinPath/rt-mailgate --url $url --queue ext-mailgate --action take-correspond"), "Opened the mailgate - $!");
+print MAIL <<EOF;
+From: ext-mailgate\@localhost
+Subject: [$rtname \#$id] test
+
+correspond-take
+EOF
+close (MAIL);
+is ( $? >> 8, 0, "mailgate exited normally" );
+DBIx::SearchBuilder::Record::Cachable->FlushCache;
+
+cmp_ok( $tick->Owner, '!=', $user->id, "we didn't change owner" );
+is( $tick->Transactions->Count, 3, "no transactions added, user can't take ticket first" );
+
+# revoke ReplyToTicket right
+use RT::ACE;
+my $ace = RT::ACE->new($RT::SystemUser);
+$ace->Load( $ace_id );
+$ace->Delete;
+my $acl = RT::ACL->new($RT::SystemUser);
+$acl->Limit( FIELD => 'RightName', VALUE => 'ReplyToTicket' );
+$acl->LimitToObject( $RT::System );
+while( my $ace = $acl->Next ) {
+ $ace->Delete;
+}
+
+ok( !$user->HasRight( Right => 'ReplyToTicket', Object => $tick ), "User can't reply to ticket any more" );
+
+
+my $group = RT::Group->new( $RT::SystemUser );
+ok( $group->LoadQueueRoleGroup( Queue => $qid, Type=> 'Owner' ), "load queue owners role group" );
+$ace = RT::ACE->new( $RT::SystemUser );
+($ace_id, $msg) = $group->PrincipalObj->GrantRight( Right => 'ReplyToTicket', Object => $queue );
+ok( $ace_id, "Granted queue owners role group with ReplyToTicket right" );
+
+($status, $msg) = $user->PrincipalObj->GrantRight( Object => $queue, Right => 'OwnTicket' );
+ok( $status, "successfuly granted right: $msg" );
+($status, $msg) = $user->PrincipalObj->GrantRight( Object => $queue, Right => 'TakeTicket' );
+ok( $status, "successfuly granted right: $msg" );
+
+$! = 0;
+ok(open(MAIL, "|$RT::BinPath/rt-mailgate --url $url --queue ext-mailgate --action take-correspond"), "Opened the mailgate - $!");
+print MAIL <<EOF;
+From: ext-mailgate\@localhost
+Subject: [$rtname \#$id] test
+
+take-correspond with reply right granted to owner role
+EOF
+close (MAIL);
+is ( $? >> 8, 0, "mailgate exited normally" );
+DBIx::SearchBuilder::Record::Cachable->FlushCache;
+
+$tick->Load( $id );
+is( $tick->Owner, $user->id, "we changed owner" );
+ok( $user->HasRight( Right => 'ReplyToTicket', Object => $tick ), "owner can reply to ticket" );
+is( $tick->Transactions->Count, 5, "transactions added" );
+
+
+# }}}
+};
+
+
+1;
+
diff --git a/rt/t/mail/gnupg-bad.t b/rt/t/mail/gnupg-bad.t
new file mode 100644
index 000000000..2d8e03575
--- /dev/null
+++ b/rt/t/mail/gnupg-bad.t
@@ -0,0 +1,58 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+use RT::Test tests => 6;
+
+plan skip_all => 'GnuPG required.'
+ unless eval 'use GnuPG::Interface; 1';
+plan skip_all => 'gpg executable is required.'
+ unless RT::Test->find_executable('gpg');
+
+
+use Cwd 'getcwd';
+
+my $homedir = RT::Test::get_abs_relocatable_dir(File::Spec->updir(),
+ qw(data gnupg keyrings));
+
+RT->Config->Set( 'GnuPG',
+ Enable => 1,
+ OutgoingMessagesFormat => 'RFC' );
+
+RT->Config->Set( 'GnuPGOptions',
+ homedir => $homedir,
+ passphrase => 'test',
+ 'no-permission-warning' => undef);
+
+RT->Config->Set( 'MailPlugins' => 'Auth::MailFrom', 'Auth::GnuPG' );
+
+my ($baseurl, $m) = RT::Test->started_ok;
+
+$m->get( $baseurl."?user=root;pass=password" );
+$m->content_like(qr/Logout/, 'we did log in');
+$m->get( $baseurl.'/Admin/Queues/');
+$m->follow_link_ok( {text => 'General'} );
+$m->submit_form( form_number => 3,
+ fields => { CorrespondAddress => 'rt@example.com' } );
+$m->content_like(qr/rt\@example.com.* - never/, 'has key info.');
+
+ok(my $user = RT::User->new($RT::SystemUser));
+ok($user->Load('root'), "Loaded user 'root'");
+$user->SetEmailAddress('rt@example.com');
+
+if (0) {
+ # XXX: need to generate these mails
+ diag "no signature" if $ENV{TEST_VERBOSE};
+ diag "no encryption on encrypted queue" if $ENV{TEST_VERBOSE};
+ diag "mismatched signature" if $ENV{TEST_VERBOSE};
+ diag "unknown public key" if $ENV{TEST_VERBOSE};
+ diag "unknown private key" if $ENV{TEST_VERBOSE};
+ diag "signer != sender" if $ENV{TEST_VERBOSE};
+ diag "encryption to user whose pubkey is not signed" if $ENV{TEST_VERBOSE};
+ diag "no encryption of attachment on encrypted queue" if $ENV{TEST_VERBOSE};
+ diag "no signature of attachment" if $ENV{TEST_VERBOSE};
+ diag "revoked key" if $ENV{TEST_VERBOSE};
+ diag "expired key" if $ENV{TEST_VERBOSE};
+ diag "unknown algorithm" if $ENV{TEST_VERBOSE};
+}
+
diff --git a/rt/t/mail/gnupg-incoming.t b/rt/t/mail/gnupg-incoming.t
new file mode 100644
index 000000000..ec313330a
--- /dev/null
+++ b/rt/t/mail/gnupg-incoming.t
@@ -0,0 +1,320 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+use RT::Test tests => 39;
+
+plan skip_all => 'GnuPG required.'
+ unless eval 'use GnuPG::Interface; 1';
+plan skip_all => 'gpg executable is required.'
+ unless RT::Test->find_executable('gpg');
+
+
+use File::Temp;
+use Cwd 'getcwd';
+use String::ShellQuote 'shell_quote';
+use IPC::Run3 'run3';
+
+my $homedir = RT::Test::get_abs_relocatable_dir(File::Spec->updir(),
+ qw(data gnupg keyrings));
+
+# catch any outgoing emails
+RT::Test->set_mail_catcher;
+
+RT->Config->Set( 'GnuPG',
+ Enable => 1,
+ OutgoingMessagesFormat => 'RFC' );
+
+RT->Config->Set( 'GnuPGOptions',
+ homedir => $homedir,
+ 'no-permission-warning' => undef);
+
+RT->Config->Set( 'MailPlugins' => 'Auth::MailFrom', 'Auth::GnuPG' );
+
+my ($baseurl, $m) = RT::Test->started_ok;
+
+# configure key for General queue
+$m->get( $baseurl."?user=root;pass=password" );
+$m->content_like(qr/Logout/, 'we did log in');
+$m->get( $baseurl.'/Admin/Queues/');
+$m->follow_link_ok( {text => 'General'} );
+$m->submit_form( form_number => 3,
+ fields => { CorrespondAddress => 'general@example.com' } );
+$m->content_like(qr/general\@example.com.* - never/, 'has key info.');
+
+ok(my $user = RT::User->new($RT::SystemUser));
+ok($user->Load('root'), "Loaded user 'root'");
+$user->SetEmailAddress('recipient@example.com');
+
+# test simple mail. supposedly this should fail when
+# 1. the queue requires signature
+# 2. the from is not what the key is associated with
+my $mail = RT::Test->open_mailgate_ok($baseurl);
+print $mail <<EOF;
+From: recipient\@example.com
+To: general\@$RT::rtname
+Subject: This is a test of new ticket creation as root
+
+Blah!
+Foob!
+EOF
+RT::Test->close_mailgate_ok($mail);
+
+{
+ my $tick = RT::Test->last_ticket;
+ is( $tick->Subject,
+ 'This is a test of new ticket creation as root',
+ "Created the ticket"
+ );
+ my $txn = $tick->Transactions->First;
+ like(
+ $txn->Attachments->First->Headers,
+ qr/^X-RT-Incoming-Encryption: Not encrypted/m,
+ 'recorded incoming mail that is not encrypted'
+ );
+ like( $txn->Attachments->First->Content, qr'Blah');
+}
+
+# test for signed mail
+my $buf = '';
+
+run3(
+ shell_quote(
+ qw(gpg --armor --sign),
+ '--default-key' => 'recipient@example.com',
+ '--homedir' => $homedir,
+ '--passphrase' => 'recipient',
+ ),
+ \"fnord\r\n",
+ \$buf,
+ \*STDOUT
+);
+
+$mail = RT::Test->open_mailgate_ok($baseurl);
+print $mail <<"EOF";
+From: recipient\@example.com
+To: general\@$RT::rtname
+Subject: signed message for queue
+
+$buf
+EOF
+RT::Test->close_mailgate_ok($mail);
+
+{
+ my $tick = RT::Test->last_ticket;
+ is( $tick->Subject, 'signed message for queue',
+ "Created the ticket"
+ );
+
+ my $txn = $tick->Transactions->First;
+ my ($msg, $attach) = @{$txn->Attachments->ItemsArrayRef};
+
+ is( $msg->GetHeader('X-RT-Incoming-Encryption'),
+ 'Not encrypted',
+ 'recorded incoming mail that is encrypted'
+ );
+ # test for some kind of PGP-Signed-By: Header
+ like( $attach->Content, qr'fnord');
+}
+
+# test for clear-signed mail
+$buf = '';
+
+run3(
+ shell_quote(
+ qw(gpg --armor --sign --clearsign),
+ '--default-key' => 'recipient@example.com',
+ '--homedir' => $homedir,
+ '--passphrase' => 'recipient',
+ ),
+ \"clearfnord\r\n",
+ \$buf,
+ \*STDOUT
+);
+
+$mail = RT::Test->open_mailgate_ok($baseurl);
+print $mail <<"EOF";
+From: recipient\@example.com
+To: general\@$RT::rtname
+Subject: signed message for queue
+
+$buf
+EOF
+RT::Test->close_mailgate_ok($mail);
+
+{
+ my $tick = RT::Test->last_ticket;
+ is( $tick->Subject, 'signed message for queue',
+ "Created the ticket"
+ );
+
+ my $txn = $tick->Transactions->First;
+ my ($msg, $attach) = @{$txn->Attachments->ItemsArrayRef};
+ is( $msg->GetHeader('X-RT-Incoming-Encryption'),
+ 'Not encrypted',
+ 'recorded incoming mail that is encrypted'
+ );
+ # test for some kind of PGP-Signed-By: Header
+ like( $attach->Content, qr'clearfnord');
+}
+
+# test for signed and encrypted mail
+$buf = '';
+
+run3(
+ shell_quote(
+ qw(gpg --encrypt --armor --sign),
+ '--recipient' => 'general@example.com',
+ '--default-key' => 'recipient@example.com',
+ '--homedir' => $homedir,
+ '--passphrase' => 'recipient',
+ ),
+ \"orzzzzzz\r\n",
+ \$buf,
+ \*STDOUT
+);
+
+$mail = RT::Test->open_mailgate_ok($baseurl);
+print $mail <<"EOF";
+From: recipient\@example.com
+To: general\@$RT::rtname
+Subject: Encrypted message for queue
+
+$buf
+EOF
+RT::Test->close_mailgate_ok($mail);
+
+{
+ my $tick = RT::Test->last_ticket;
+ is( $tick->Subject, 'Encrypted message for queue',
+ "Created the ticket"
+ );
+
+ my $txn = $tick->Transactions->First;
+ my ($msg, $attach, $orig) = @{$txn->Attachments->ItemsArrayRef};
+
+ is( $msg->GetHeader('X-RT-Incoming-Encryption'),
+ 'Success',
+ 'recorded incoming mail that is encrypted'
+ );
+ is( $msg->GetHeader('X-RT-Privacy'),
+ 'PGP',
+ 'recorded incoming mail that is encrypted'
+ );
+ like( $attach->Content, qr'orz');
+
+ is( $orig->GetHeader('Content-Type'), 'application/x-rt-original-message');
+ ok(index($orig->Content, $buf) != -1, 'found original msg');
+}
+
+# test for signed mail by other key
+$buf = '';
+
+run3(
+ shell_quote(
+ qw(gpg --armor --sign),
+ '--default-key' => 'rt@example.com',
+ '--homedir' => $homedir,
+ '--passphrase' => 'test',
+ ),
+ \"alright\r\n",
+ \$buf,
+ \*STDOUT
+);
+
+$mail = RT::Test->open_mailgate_ok($baseurl);
+print $mail <<"EOF";
+From: recipient\@example.com
+To: general\@$RT::rtname
+Subject: signed message for queue
+
+$buf
+EOF
+RT::Test->close_mailgate_ok($mail);
+
+{
+ my $tick = RT::Test->last_ticket;
+ my $txn = $tick->Transactions->First;
+ my ($msg, $attach) = @{$txn->Attachments->ItemsArrayRef};
+ # XXX: in this case, which credential should we be using?
+ is( $msg->GetHeader('X-RT-Incoming-Signature'),
+ 'Test User <rt@example.com>',
+ 'recorded incoming mail signed by others'
+ );
+}
+
+# test for encrypted mail with key not associated to the queue
+$buf = '';
+
+run3(
+ shell_quote(
+ qw(gpg --armor --encrypt),
+ '--recipient' => 'random@localhost',
+ '--homedir' => $homedir,
+ ),
+ \"should not be there either\r\n",
+ \$buf,
+ \*STDOUT
+);
+
+$mail = RT::Test->open_mailgate_ok($baseurl);
+print $mail <<"EOF";
+From: recipient\@example.com
+To: general\@$RT::rtname
+Subject: encrypted message for queue
+
+$buf
+EOF
+RT::Test->close_mailgate_ok($mail);
+
+{
+ my $tick = RT::Test->last_ticket;
+ my $txn = $tick->Transactions->First;
+ my ($msg, $attach) = @{$txn->Attachments->ItemsArrayRef};
+
+ TODO:
+ {
+ local $TODO = "this test requires keys associated with queues";
+ unlike( $attach->Content, qr'should not be there either');
+ }
+}
+
+# test for badly encrypted mail
+{
+$buf = '';
+
+run3(
+ shell_quote(
+ qw(gpg --armor --encrypt),
+ '--recipient' => 'rt@example.com',
+ '--homedir' => $homedir,
+ ),
+ \"really should not be there either\r\n",
+ \$buf,
+ \*STDOUT
+);
+
+$buf =~ s/PGP MESSAGE/SCREWED UP/g;
+
+RT::Test->fetch_caught_mails;
+
+$mail = RT::Test->open_mailgate_ok($baseurl);
+print $mail <<"EOF";
+From: recipient\@example.com
+To: general\@$RT::rtname
+Subject: encrypted message for queue
+
+$buf
+EOF
+RT::Test->close_mailgate_ok($mail);
+my @mail = RT::Test->fetch_caught_mails;
+is(@mail, 1, 'caught outgoing mail.');
+}
+
+{
+ my $tick = RT::Test->last_ticket;
+ my $txn = $tick->Transactions->First;
+ my ($msg, $attach) = @{$txn->Attachments->ItemsArrayRef};
+ unlike( ($attach ? $attach->Content : ''), qr'really should not be there either');
+}
+
diff --git a/rt/t/mail/gnupg-realmail.t b/rt/t/mail/gnupg-realmail.t
new file mode 100644
index 000000000..de1d95815
--- /dev/null
+++ b/rt/t/mail/gnupg-realmail.t
@@ -0,0 +1,184 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+use RT::Test tests => 197;
+
+plan skip_all => 'GnuPG required.'
+ unless eval 'use GnuPG::Interface; 1';
+plan skip_all => 'gpg executable is required.'
+ unless RT::Test->find_executable('gpg');
+
+
+use Digest::MD5 qw(md5_hex);
+
+use File::Temp qw(tempdir);
+my $homedir = tempdir( CLEANUP => 1 );
+
+RT->Config->Set( 'GnuPG',
+ Enable => 1,
+ OutgoingMessagesFormat => 'RFC' );
+
+RT->Config->Set( 'GnuPGOptions',
+ homedir => $homedir,
+ passphrase => 'rt-test',
+ 'no-permission-warning' => undef);
+
+RT->Config->Set( 'MailPlugins' => 'Auth::MailFrom', 'Auth::GnuPG' );
+
+RT::Test->import_gnupg_key('rt-recipient@example.com');
+RT::Test->import_gnupg_key('rt-test@example.com', 'public');
+
+my ($baseurl, $m) = RT::Test->started_ok;
+ok $m->login, 'we did log in';
+$m->get_ok( '/Admin/Queues/');
+$m->follow_link_ok( {text => 'General'} );
+$m->submit_form( form_number => 3,
+ fields => { CorrespondAddress => 'rt-recipient@example.com' } );
+$m->content_like(qr/rt-recipient\@example.com.* - never/, 'has key info.');
+
+diag "load Everyone group" if $ENV{'TEST_VERBOSE'};
+my $everyone;
+{
+ $everyone = RT::Group->new( $RT::SystemUser );
+ $everyone->LoadSystemInternalGroup('Everyone');
+ ok $everyone->id, "loaded 'everyone' group";
+}
+
+RT::Test->set_rights(
+ Principal => $everyone,
+ Right => ['CreateTicket'],
+);
+
+
+my $eid = 0;
+for my $usage (qw/signed encrypted signed&encrypted/) {
+ for my $format (qw/MIME inline/) {
+ for my $attachment (qw/plain text-attachment binary-attachment/) {
+ ++$eid;
+ diag "Email $eid: $usage, $attachment email with $format format" if $ENV{TEST_VERBOSE};
+ eval { email_ok($eid, $usage, $format, $attachment) };
+ }
+ }
+}
+
+$eid = 18;
+{
+ my ($usage, $format, $attachment) = ('signed', 'inline', 'plain');
+ ++$eid;
+ diag "Email $eid: $usage, $attachment email with $format format" if $ENV{TEST_VERBOSE};
+ eval { email_ok($eid, $usage, $format, $attachment) };
+}
+
+sub email_ok {
+ my ($eid, $usage, $format, $attachment) = @_;
+ diag "email_ok $eid: $usage, $format, $attachment" if $ENV{'TEST_VERBOSE'};
+
+ my $emaildatadir = RT::Test::get_relocatable_dir(File::Spec->updir(),
+ qw(data gnupg emails));
+ my ($file) = glob("$emaildatadir/$eid-*");
+ my $mail = RT::Test->file_content($file);
+
+ my ($status, $id) = RT::Test->send_via_mailgate($mail);
+ is ($status >> 8, 0, "$eid: The mail gateway exited normally");
+ ok ($id, "$eid: got id of a newly created ticket - $id");
+
+ my $tick = RT::Ticket->new( $RT::SystemUser );
+ $tick->Load( $id );
+ ok ($tick->id, "$eid: loaded ticket #$id");
+
+ is ($tick->Subject,
+ "Test Email ID:$eid",
+ "$eid: Created the ticket"
+ );
+
+ my $txn = $tick->Transactions->First;
+ my ($msg, @attachments) = @{$txn->Attachments->ItemsArrayRef};
+
+ is( $msg->GetHeader('X-RT-Privacy'),
+ 'PGP',
+ "$eid: recorded incoming mail that is encrypted"
+ );
+
+ if ($usage =~ /encrypted/) {
+ if ( $format eq 'MIME' || $attachment eq 'plain' ) {
+ is( $msg->GetHeader('X-RT-Incoming-Encryption'),
+ 'Success',
+ "$eid: recorded incoming mail that is encrypted"
+ );
+ } else {
+ is( $attachments[0]->GetHeader('X-RT-Incoming-Encryption'),
+ 'Success',
+ "$eid: recorded incoming mail that is encrypted"
+ );
+ is( $attachments[1]->GetHeader('X-RT-Incoming-Encryption'),
+ 'Success',
+ "$eid: recorded incoming mail that is encrypted"
+ );
+ }
+ like( $attachments[0]->Content, qr/ID:$eid/,
+ "$eid: incoming mail did NOT have original body"
+ );
+ }
+ else {
+ is( $msg->GetHeader('X-RT-Incoming-Encryption'),
+ 'Not encrypted',
+ "$eid: recorded incoming mail that is not encrypted"
+ );
+ like( $msg->Content || $attachments[0]->Content, qr/ID:$eid/,
+ "$eid: got original content"
+ );
+ }
+
+ if ($usage =~ /signed/) {
+# XXX: FIXME: TODO: 6-signed-inline-with-attachment should be re-generated as it's actually RFC format
+ if ( $format eq 'MIME' || $attachment eq 'plain' || ($format eq 'inline' && $attachment =~ /binary/ && $usage !~ /encrypted/) ) {
+ is( $msg->GetHeader('X-RT-Incoming-Signature'),
+ 'RT Test <rt-test@example.com>',
+ "$eid: recorded incoming mail that is signed"
+ );
+ }
+ else {
+ is( $attachments[0]->GetHeader('X-RT-Incoming-Signature'),
+ 'RT Test <rt-test@example.com>',
+ "$eid: recorded incoming mail that is signed"
+ );
+ is( $attachments[1]->GetHeader('X-RT-Incoming-Signature'),
+ 'RT Test <rt-test@example.com>',
+ "$eid: recorded incoming mail that is signed"
+ );
+ }
+ }
+ else {
+ is( $msg->GetHeader('X-RT-Incoming-Signature'),
+ undef,
+ "$eid: recorded incoming mail that is not signed"
+ );
+ }
+
+ if ($attachment =~ /attachment/) {
+ # signed messages should sign each attachment too
+ if ($usage =~ /signed/) {
+ my $sig = pop @attachments;
+ ok ($sig->Id, "$eid: loaded attachment.sig object");
+ my $acontent = $sig->Content;
+ }
+
+ my ($a) = grep $_->Filename, @attachments;
+ ok ($a && $a->Id, "$eid: found attachment with filename");
+
+ my $acontent = $a->Content;
+ if ($attachment =~ /binary/)
+ {
+ is(md5_hex($acontent), '1e35f1aa90c98ca2bab85c26ae3e1ba7', "$eid: The binary attachment's md5sum matches");
+ }
+ else
+ {
+ like($acontent, qr/zanzibar/, "$eid: The attachment isn't screwed up in the database.");
+ }
+
+ }
+
+ return 0;
+}
+
diff --git a/rt/t/mail/gnupg-reverification.t b/rt/t/mail/gnupg-reverification.t
new file mode 100644
index 000000000..f116d9380
--- /dev/null
+++ b/rt/t/mail/gnupg-reverification.t
@@ -0,0 +1,92 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+use RT::Test tests => 120;
+
+plan skip_all => 'GnuPG required.'
+ unless eval 'use GnuPG::Interface; 1';
+plan skip_all => 'gpg executable is required.'
+ unless RT::Test->find_executable('gpg');
+
+
+use File::Temp qw(tempdir);
+my $homedir = tempdir( CLEANUP => 1 );
+
+RT->Config->Set( 'GnuPG',
+ Enable => 1,
+ OutgoingMessagesFormat => 'RFC' );
+
+RT->Config->Set( 'GnuPGOptions',
+ homedir => $homedir,
+ passphrase => 'rt-test',
+ 'no-permission-warning' => undef);
+
+RT->Config->Set( 'MailPlugins' => 'Auth::MailFrom', 'Auth::GnuPG' );
+
+
+diag "load Everyone group" if $ENV{'TEST_VERBOSE'};
+my $everyone;
+{
+ $everyone = RT::Group->new( $RT::SystemUser );
+ $everyone->LoadSystemInternalGroup('Everyone');
+ ok $everyone->id, "loaded 'everyone' group";
+}
+
+RT::Test->set_rights(
+ Principal => $everyone,
+ Right => ['CreateTicket'],
+);
+
+
+my ($baseurl, $m) = RT::Test->started_ok;
+ok $m->login, 'we get log in';
+
+RT::Test->import_gnupg_key('rt-recipient@example.com');
+
+my @ticket_ids;
+
+my $emaildatadir = RT::Test::get_relocatable_dir(File::Spec->updir(),
+ qw(data gnupg emails));
+my @files = glob("$emaildatadir/*-signed-*");
+foreach my $file ( @files ) {
+ diag "testing $file" if $ENV{'TEST_VERBOSE'};
+
+ my ($eid) = ($file =~ m{(\d+)[^/\\]+$});
+ ok $eid, 'figured id of a file';
+
+ my $email_content = RT::Test->file_content( $file );
+ ok $email_content, "$eid: got content of email";
+
+ my ($status, $id) = RT::Test->send_via_mailgate( $email_content );
+ is $status >> 8, 0, "$eid: the mail gateway exited normally";
+ ok $id, "$eid: got id of a newly created ticket - $id";
+
+ my $ticket = RT::Ticket->new( $RT::SystemUser );
+ $ticket->Load( $id );
+ ok $ticket->id, "$eid: loaded ticket #$id";
+ is $ticket->Subject, "Test Email ID:$eid", "$eid: correct subject";
+
+ $m->goto_ticket( $id );
+ $m->content_like(
+ qr/Not possible to check the signature, the reason is missing public key/is,
+ "$eid: signature is not verified",
+ );
+ $m->content_like(qr/This is .*ID:$eid/ims, "$eid: content is there and message is decrypted");
+
+ push @ticket_ids, $id;
+}
+
+diag "import key into keyring" if $ENV{'TEST_VERBOSE'};
+RT::Test->import_gnupg_key('rt-test@example.com', 'public');
+
+foreach my $id ( @ticket_ids ) {
+ diag "testing ticket #$id" if $ENV{'TEST_VERBOSE'};
+
+ $m->goto_ticket( $id );
+ $m->content_like(
+ qr/The signature is good/is,
+ "signature is re-verified and now good",
+ );
+}
+
diff --git a/rt/t/mail/mime_decoding.t b/rt/t/mail/mime_decoding.t
new file mode 100644
index 000000000..8257aee80
--- /dev/null
+++ b/rt/t/mail/mime_decoding.t
@@ -0,0 +1,59 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+use RT::Test nodata => 1, tests => 6;
+
+use_ok('RT::I18N');
+
+diag q{'=' char in a leading part before an encoded part} if $ENV{TEST_VERBOSE};
+{
+ my $str = 'key="plain"; key="=?UTF-8?B?0LzQvtC5X9GE0LDQudC7LmJpbg==?="';
+ is(
+ RT::I18N::DecodeMIMEWordsToUTF8($str),
+ 'key="plain"; key="мой_файл.bin"',
+ "right decoding"
+ );
+}
+
+diag q{not compliant with standards, but MUAs send such field when attachment has non-ascii in name}
+ if $ENV{TEST_VERBOSE};
+{
+ my $str = 'attachment; filename="=?UTF-8?B?0LzQvtC5X9GE0LDQudC7LmJpbg==?="';
+ is(
+ RT::I18N::DecodeMIMEWordsToUTF8($str),
+ 'attachment; filename="мой_файл.bin"',
+ "right decoding"
+ );
+}
+
+diag q{'=' char in a trailing part after an encoded part} if $ENV{TEST_VERBOSE};
+{
+ my $str = 'attachment; filename="=?UTF-8?B?0LzQvtC5X9GE0LDQudC7LmJpbg==?="; some_prop="value"';
+ is(
+ RT::I18N::DecodeMIMEWordsToUTF8($str),
+ 'attachment; filename="мой_файл.bin"; some_prop="value"',
+ "right decoding"
+ );
+}
+
+diag q{regression test for #5248 from rt3.fsck.com} if $ENV{TEST_VERBOSE};
+{
+ my $str = qq{Subject: =?ISO-8859-1?Q?Re=3A_=5BXXXXXX=23269=5D_=5BComment=5D_Frag?=}
+ . qq{\n =?ISO-8859-1?Q?e_zu_XXXXXX--xxxxxx_/_Xxxxx=FCxxxxxxxxxx?=};
+ is(
+ RT::I18N::DecodeMIMEWordsToUTF8($str),
+ qq{Subject: Re: [XXXXXX#269] [Comment] Frage zu XXXXXX--xxxxxx / Xxxxxüxxxxxxxxxx},
+ "right decoding"
+ );
+}
+
+diag q{newline and encoded file name} if $ENV{TEST_VERBOSE};
+{
+ my $str = qq{application/vnd.ms-powerpoint;\n\tname="=?ISO-8859-1?Q?Main_presentation.ppt?="};
+ is(
+ RT::I18N::DecodeMIMEWordsToUTF8($str),
+ qq{application/vnd.ms-powerpoint;\tname="Main presentation.ppt"},
+ "right decoding"
+ );
+}
+
diff --git a/rt/t/mail/sendmail.t b/rt/t/mail/sendmail.t
new file mode 100644
index 000000000..1f97bbb9f
--- /dev/null
+++ b/rt/t/mail/sendmail.t
@@ -0,0 +1,538 @@
+#!/usr/bin/perl -w
+
+use strict;
+use File::Spec ();
+
+use RT::Test tests => 137;
+
+use RT::EmailParser;
+use RT::Tickets;
+use RT::Action::SendEmail;
+
+my @_outgoing_messages;
+my @scrips_fired;
+
+#We're not testing acls here.
+my $everyone = RT::Group->new($RT::SystemUser);
+$everyone->LoadSystemInternalGroup('Everyone');
+$everyone->PrincipalObj->GrantRight( Right =>'SuperUser' );
+
+
+is (__PACKAGE__, 'main', "We're operating in the main package");
+
+{
+ no warnings qw/redefine/;
+ sub RT::Action::SendEmail::SendMessage {
+ my $self = shift;
+ my $MIME = shift;
+
+ main::_fired_scrip($self->ScripObj);
+ main::is(ref($MIME) , 'MIME::Entity', "hey, look. it's a mime entity");
+ }
+}
+
+# some utils
+sub first_txn { return $_[0]->Transactions->First }
+sub first_attach { return first_txn($_[0])->Attachments->First }
+
+sub count_txns { return $_[0]->Transactions->Count }
+sub count_attachs { return first_txn($_[0])->Attachments->Count }
+
+# instrument SendEmail to pass us what it's about to send.
+# create a regular ticket
+
+my $parser = RT::EmailParser->new();
+
+# Let's test to make sure a multipart/report is processed correctly
+my $multipart_report_email = RT::Test::get_relocatable_file('multipart-report',
+ (File::Spec->updir(), 'data', 'emails'));
+my $content = RT::Test->file_content($multipart_report_email);
+# be as much like the mail gateway as possible.
+use RT::Interface::Email;
+my %args = (message => $content, queue => 1, action => 'correspond');
+my ($status, $msg) = RT::Interface::Email::Gateway(\%args);
+ok($status, "successfuly used Email::Gateway interface") or diag("error: $msg");
+my $tickets = RT::Tickets->new($RT::SystemUser);
+$tickets->OrderBy(FIELD => 'id', ORDER => 'DESC');
+$tickets->Limit(FIELD => 'id' ,OPERATOR => '>', VALUE => '0');
+my $tick= $tickets->First();
+isa_ok($tick, "RT::Ticket", "got a ticket object");
+ok ($tick->Id, "found ticket ".$tick->Id);
+like (first_txn($tick)->Content , qr/The original message was received/, "It's the bounce");
+
+
+# make sure it fires scrips.
+is ($#scrips_fired, 1, "Fired 2 scrips on ticket creation");
+
+undef @scrips_fired;
+
+
+
+
+$parser->ParseMIMEEntityFromScalar('From: root@localhost
+To: rt@example.com
+Subject: This is a test of new ticket creation as an unknown user
+
+Blah!
+Foob!');
+
+
+use Data::Dumper;
+
+my $ticket = RT::Ticket->new($RT::SystemUser);
+my ($id, undef, $create_msg ) = $ticket->Create(Requestor => ['root@localhost'], Queue => 'general', Subject => 'I18NTest', MIMEObj => $parser->Entity);
+ok ($id,$create_msg);
+$tickets = RT::Tickets->new($RT::SystemUser);
+$tickets->OrderBy(FIELD => 'id', ORDER => 'DESC');
+$tickets->Limit(FIELD => 'id' ,OPERATOR => '>', VALUE => '0');
+ $tick = $tickets->First();
+ok ($tick->Id, "found ticket ".$tick->Id);
+is ($tick->Subject , 'I18NTest', "failed to create the new ticket from an unprivileged account");
+
+# make sure it fires scrips.
+is ($#scrips_fired, 1, "Fired 2 scrips on ticket creation");
+# make sure it sends an autoreply
+# make sure it sends a notification to adminccs
+
+
+# we need to swap out SendMessage to test the new things we care about;
+&utf8_redef_sendmessage;
+
+# create an iso 8859-1 ticket
+@scrips_fired = ();
+
+my $iso_8859_1_ticket_email = RT::Test::get_relocatable_file(
+ 'new-ticket-from-iso-8859-1', (File::Spec->updir(), 'data', 'emails'));
+$content = RT::Test->file_content($iso_8859_1_ticket_email);
+
+
+
+$parser->ParseMIMEEntityFromScalar($content);
+
+
+# be as much like the mail gateway as possible.
+use RT::Interface::Email;
+
+ %args = (message => $content, queue => 1, action => 'correspond');
+ RT::Interface::Email::Gateway(\%args);
+ $tickets = RT::Tickets->new($RT::SystemUser);
+$tickets->OrderBy(FIELD => 'id', ORDER => 'DESC');
+$tickets->Limit(FIELD => 'id' ,OPERATOR => '>', VALUE => '0');
+ $tick = $tickets->First();
+ok ($tick->Id, "found ticket ".$tick->Id);
+
+like (first_txn($tick)->Content , qr/H\x{e5}vard/, "It's signed by havard. yay");
+
+
+# make sure it fires scrips.
+is ($#scrips_fired, 1, "Fired 2 scrips on ticket creation");
+# make sure it sends an autoreply
+
+
+# make sure it sends a notification to adminccs
+
+# If we correspond, does it do the right thing to the outbound messages?
+
+$parser->ParseMIMEEntityFromScalar($content);
+ ($id, $msg) = $tick->Comment(MIMEObj => $parser->Entity);
+ok ($id, $msg);
+
+$parser->ParseMIMEEntityFromScalar($content);
+($id, $msg) = $tick->Correspond(MIMEObj => $parser->Entity);
+ok ($id, $msg);
+
+
+
+
+
+# we need to swap out SendMessage to test the new things we care about;
+&iso8859_redef_sendmessage;
+RT->Config->Set( EmailOutputEncoding => 'iso-8859-1' );
+# create an iso 8859-1 ticket
+@scrips_fired = ();
+
+ $content = RT::Test->file_content($iso_8859_1_ticket_email);
+# be as much like the mail gateway as possible.
+use RT::Interface::Email;
+
+ %args = (message => $content, queue => 1, action => 'correspond');
+ RT::Interface::Email::Gateway(\%args);
+$tickets = RT::Tickets->new($RT::SystemUser);
+$tickets->OrderBy(FIELD => 'id', ORDER => 'DESC');
+$tickets->Limit(FIELD => 'id' ,OPERATOR => '>', VALUE => '0');
+ $tick = $tickets->First();
+ok ($tick->Id, "found ticket ".$tick->Id);
+
+like (first_txn($tick)->Content , qr/H\x{e5}vard/, "It's signed by havard. yay");
+
+
+# make sure it fires scrips.
+is ($#scrips_fired, 1, "Fired 2 scrips on ticket creation");
+# make sure it sends an autoreply
+
+
+# make sure it sends a notification to adminccs
+
+
+# If we correspond, does it do the right thing to the outbound messages?
+
+$parser->ParseMIMEEntityFromScalar($content);
+ ($id, $msg) = $tick->Comment(MIMEObj => $parser->Entity);
+ok ($id, $msg);
+
+$parser->ParseMIMEEntityFromScalar($content);
+($id, $msg) = $tick->Correspond(MIMEObj => $parser->Entity);
+ok ($id, $msg);
+
+
+sub _fired_scrip {
+ my $scrip = shift;
+ push @scrips_fired, $scrip;
+}
+
+sub utf8_redef_sendmessage {
+ no warnings qw/redefine/;
+ eval '
+ sub RT::Action::SendEmail::SendMessage {
+ my $self = shift;
+ my $MIME = shift;
+
+ my $scrip = $self->ScripObj->id;
+ ok(1, $self->ScripObj->ConditionObj->Name . " ".$self->ScripObj->ActionObj->Name);
+ main::_fired_scrip($self->ScripObj);
+ $MIME->make_singlepart;
+ main::is( ref($MIME) , \'MIME::Entity\',
+ "hey, look. it\'s a mime entity" );
+ main::is( ref( $MIME->head ) , \'MIME::Head\',
+ "its mime header is a mime header. yay" );
+ main::like( $MIME->head->get(\'Content-Type\') , qr/utf-8/,
+ "Its content type is utf-8" );
+ my $message_as_string = $MIME->bodyhandle->as_string();
+ use Encode;
+ $message_as_string = Encode::decode_utf8($message_as_string);
+ main::like(
+ $message_as_string , qr/H\x{e5}vard/,
+"The message\'s content contains havard\'s name. this will fail if it\'s not utf8 out");
+
+ }';
+}
+
+sub iso8859_redef_sendmessage {
+ no warnings qw/redefine/;
+ eval '
+ sub RT::Action::SendEmail::SendMessage {
+ my $self = shift;
+ my $MIME = shift;
+
+ my $scrip = $self->ScripObj->id;
+ ok(1, $self->ScripObj->ConditionObj->Name . " ".$self->ScripObj->ActionObj->Name);
+ main::_fired_scrip($self->ScripObj);
+ $MIME->make_singlepart;
+ main::is( ref($MIME) , \'MIME::Entity\',
+ "hey, look. it\'s a mime entity" );
+ main::is( ref( $MIME->head ) , \'MIME::Head\',
+ "its mime header is a mime header. yay" );
+ main::like( $MIME->head->get(\'Content-Type\') , qr/iso-8859-1/,
+ "Its content type is iso-8859-1 - " . $MIME->head->get("Content-Type") );
+ my $message_as_string = $MIME->bodyhandle->as_string();
+ use Encode;
+ $message_as_string = Encode::decode("iso-8859-1",$message_as_string);
+ main::like(
+ $message_as_string , qr/H\x{e5}vard/, "The message\'s content contains havard\'s name. this will fail if it\'s not utf8 out");
+
+ }';
+}
+
+# {{{ test a multipart alternative containing a text-html part with an umlaut
+
+ my $alt_umlaut_email = RT::Test::get_relocatable_file(
+ 'multipart-alternative-with-umlaut', (File::Spec->updir(), 'data', 'emails'));
+ $content = RT::Test->file_content($alt_umlaut_email);
+
+$parser->ParseMIMEEntityFromScalar($content);
+
+
+# be as much like the mail gateway as possible.
+{
+ no warnings qw/redefine/;
+ local *RT::Action::SendEmail::SendMessage = sub { return 1};
+
+ %args = (message => $content, queue => 1, action => 'correspond');
+ RT::Interface::Email::Gateway(\%args);
+ # TODO: following 5 lines should replaced by get_latest_ticket_ok()
+ $tickets = RT::Tickets->new($RT::SystemUser);
+ $tickets->OrderBy(FIELD => 'id', ORDER => 'DESC');
+ $tickets->Limit(FIELD => 'id' ,OPERATOR => '>', VALUE => '0');
+ $tick = $tickets->First();
+
+ ok ($tick->Id, "found ticket ".$tick->Id);
+
+ like (first_txn($tick)->Content , qr/causes Error/, "We recorded the content right as text-plain");
+ is (count_attachs($tick) , 3 , "Has three attachments, presumably a text-plain, a text-html and a multipart alternative");
+
+}
+
+# }}}
+
+# {{{ test a text-html message with an umlaut
+ my $text_html_email = RT::Test::get_relocatable_file('text-html-with-umlaut',
+ (File::Spec->updir(), 'data', 'emails'));
+ $content = RT::Test->file_content($text_html_email);
+
+$parser->ParseMIMEEntityFromScalar($content);
+
+
+# be as much like the mail gateway as possible.
+&text_html_redef_sendmessage;
+
+ %args = (message => $content, queue => 1, action => 'correspond');
+ RT::Interface::Email::Gateway(\%args);
+ $tickets = RT::Tickets->new($RT::SystemUser);
+$tickets->OrderBy(FIELD => 'id', ORDER => 'DESC');
+$tickets->Limit(FIELD => 'id' ,OPERATOR => '>', VALUE => '0');
+ $tick = $tickets->First();
+ok ($tick->Id, "found ticket ".$tick->Id);
+
+like (first_attach($tick)->Content , qr/causes Error/, "We recorded the content as containing 'causes error'") or diag( first_attach($tick)->Content );
+like (first_attach($tick)->ContentType , qr/text\/html/, "We recorded the content as text/html");
+is (count_attachs($tick), 1 , "Has one attachment, presumably a text-html and a multipart alternative");
+
+sub text_html_redef_sendmessage {
+ no warnings qw/redefine/;
+ eval 'sub RT::Action::SendEmail::SendMessage {
+ my $self = shift;
+ my $MIME = shift;
+ return (1) unless ($self->ScripObj->ScripActionObj->Name eq "Notify AdminCcs" );
+ is ($MIME->parts, 0, "generated correspondence mime entity
+ does not have parts");
+ is ($MIME->head->mime_type , "text/plain", "The mime type is a plain");
+ }';
+}
+
+# }}}
+
+# {{{ test a text-html message with russian characters
+ my $russian_email = RT::Test::get_relocatable_file('text-html-in-russian',
+ (File::Spec->updir(), 'data', 'emails'));
+ $content = RT::Test->file_content($russian_email);
+
+$parser->ParseMIMEEntityFromScalar($content);
+
+
+# be as much like the mail gateway as possible.
+&text_html_redef_sendmessage;
+
+ %args = (message => $content, queue => 1, action => 'correspond');
+ RT::Interface::Email::Gateway(\%args);
+ $tickets = RT::Tickets->new($RT::SystemUser);
+$tickets->OrderBy(FIELD => 'id', ORDER => 'DESC');
+$tickets->Limit(FIELD => 'id' ,OPERATOR => '>', VALUE => '0');
+ $tick = $tickets->First();
+ok ($tick->Id, "found ticket ".$tick->Id);
+
+like (first_attach($tick)->ContentType , qr/text\/html/, "We recorded the content right as text-html");
+
+is (count_attachs($tick) ,1 , "Has one attachment, presumably a text-html and a multipart alternative");
+
+# }}}
+
+# {{{ test a message containing a russian subject and NO content type
+
+RT->Config->Set( EmailInputEncodings => 'koi8-r', RT->Config->Get('EmailInputEncodings') );
+RT->Config->Set( EmailOutputEncoding => 'koi8-r' );
+my $russian_subject_email = RT::Test::get_relocatable_file(
+ 'russian-subject-no-content-type', (File::Spec->updir(), 'data', 'emails'));
+$content = RT::Test->file_content($russian_subject_email);
+
+$parser->ParseMIMEEntityFromScalar($content);
+
+
+# be as much like the mail gateway as possible.
+&text_plain_russian_redef_sendmessage;
+ %args = (message => $content, queue => 1, action => 'correspond');
+ RT::Interface::Email::Gateway(\%args);
+ $tickets = RT::Tickets->new($RT::SystemUser);
+$tickets->OrderBy(FIELD => 'id', ORDER => 'DESC');
+$tickets->Limit(FIELD => 'id' ,OPERATOR => '>', VALUE => '0');
+$tick= $tickets->First();
+ok ($tick->Id, "found ticket ".$tick->Id);
+
+like (first_attach($tick)->ContentType , qr/text\/plain/, "We recorded the content type right");
+is (count_attachs($tick) ,1 , "Has one attachment, presumably a text-plain");
+is ($tick->Subject, "\x{442}\x{435}\x{441}\x{442} \x{442}\x{435}\x{441}\x{442}", "Recorded the subject right");
+sub text_plain_russian_redef_sendmessage {
+ no warnings qw/redefine/;
+ eval 'sub RT::Action::SendEmail::SendMessage {
+ my $self = shift;
+ my $MIME = shift;
+ return (1) unless ($self->ScripObj->ScripActionObj->Name eq "Notify AdminCcs" );
+ is ($MIME->head->mime_type , "text/plain", "The only part is text/plain ");
+ my $subject = $MIME->head->get("subject");
+ chomp($subject);
+ #is( $subject , /^=\?KOI8-R\?B\?W2V4YW1wbGUuY39tICM3XSDUxdPUINTF09Q=\?=/ , "The $subject is encoded correctly");
+ };
+ ';
+}
+
+my @input_encodings = RT->Config->Get( 'EmailInputEncodings' );
+shift @input_encodings;
+RT->Config->Set(EmailInputEncodings => @input_encodings );
+RT->Config->Set(EmailOutputEncoding => 'utf-8');
+# }}}
+
+
+# {{{ test a message containing a nested RFC 822 message
+
+my $nested_rfc822_email = RT::Test::get_relocatable_file('nested-rfc-822',
+ (File::Spec->updir(), 'data', 'emails'));
+$content = RT::Test->file_content($nested_rfc822_email);
+ok ($content, "Loaded nested-rfc-822 to test");
+
+$parser->ParseMIMEEntityFromScalar($content);
+
+
+# be as much like the mail gateway as possible.
+&text_plain_nested_redef_sendmessage;
+ %args = (message => $content, queue => 1, action => 'correspond');
+ RT::Interface::Email::Gateway(\%args);
+ $tickets = RT::Tickets->new($RT::SystemUser);
+$tickets->OrderBy(FIELD => 'id', ORDER => 'DESC');
+$tickets->Limit(FIELD => 'id' ,OPERATOR => '>', VALUE => '0');
+$tick= $tickets->First();
+ok ($tick->Id, "found ticket ".$tick->Id);
+is ($tick->Subject, "[Jonas Liljegren] Re: [Para] Niv\x{e5}er?");
+like (first_attach($tick)->ContentType , qr/multipart\/mixed/, "We recorded the content type right");
+is (count_attachs($tick) , 5 , "Has one attachment, presumably a text-plain and a message RFC 822 and another plain");
+sub text_plain_nested_redef_sendmessage {
+ no warnings qw/redefine/;
+ eval 'sub RT::Action::SendEmail::SendMessage {
+ my $self = shift;
+ my $MIME = shift;
+ return (1) unless ($self->ScripObj->ScripActionObj->Name eq "Notify AdminCcs" );
+ is ($MIME->head->mime_type , "multipart/mixed", "It is a mixed multipart");
+ my $subject = $MIME->head->get("subject");
+ $subject = MIME::Base64::decode_base64( $subject);
+ chomp($subject);
+ # TODO, why does this test fail
+ #ok($subject =~ qr{Niv\x{e5}er}, "The subject matches the word - $subject");
+ 1;
+ }';
+}
+
+# }}}
+
+
+# {{{ test a multipart alternative containing a uuencoded mesage generated by lotus notes
+
+ my $uuencoded_email = RT::Test::get_relocatable_file('notes-uuencoded',
+ (File::Spec->updir(), 'data', 'emails'));
+ $content = RT::Test->file_content($uuencoded_email);
+
+$parser->ParseMIMEEntityFromScalar($content);
+
+
+# be as much like the mail gateway as possible.
+{
+ no warnings qw/redefine/;
+ local *RT::Action::SendEmail::SendMessage = sub { return 1};
+ %args = (message => $content, queue => 1, action => 'correspond');
+ RT::Interface::Email::Gateway(\%args);
+ $tickets = RT::Tickets->new($RT::SystemUser);
+ $tickets->OrderBy(FIELD => 'id', ORDER => 'DESC');
+ $tickets->Limit(FIELD => 'id' ,OPERATOR => '>', VALUE => '0');
+ $tick= $tickets->First();
+ ok ($tick->Id, "found ticket ".$tick->Id);
+
+ like (first_txn($tick)->Content , qr/from Lotus Notes/, "We recorded the content right");
+ is (count_attachs($tick) , 3 , "Has three attachments");
+}
+
+# }}}
+
+# {{{ test a multipart that crashes the file-based mime-parser works
+
+ my $crashes_file_based_parser_email = RT::Test::get_relocatable_file(
+ 'crashes-file-based-parser', (File::Spec->updir(), 'data', 'emails'));
+ $content = RT::Test->file_content($crashes_file_based_parser_email);
+
+$parser->ParseMIMEEntityFromScalar($content);
+
+
+# be as much like the mail gateway as possible.
+
+no warnings qw/redefine/;
+local *RT::Action::SendEmail::SendMessage = sub { return 1};
+ %args = (message => $content, queue => 1, action => 'correspond');
+ RT::Interface::Email::Gateway(\%args);
+ $tickets = RT::Tickets->new($RT::SystemUser);
+$tickets->OrderBy(FIELD => 'id', ORDER => 'DESC');
+$tickets->Limit(FIELD => 'id' ,OPERATOR => '>', VALUE => '0');
+$tick= $tickets->First();
+ok ($tick->Id, "found ticket ".$tick->Id);
+
+like (first_txn($tick)->Content , qr/FYI/, "We recorded the content right");
+is (count_attachs($tick) , 5 , "Has three attachments");
+
+
+
+
+# }}}
+
+# {{{ test a multi-line RT-Send-CC header
+
+ my $rt_send_cc_email = RT::Test::get_relocatable_file('rt-send-cc',
+ (File::Spec->updir(), 'data', 'emails'));
+ $content = RT::Test->file_content($rt_send_cc_email);
+
+$parser->ParseMIMEEntityFromScalar($content);
+
+
+
+ %args = (message => $content, queue => 1, action => 'correspond');
+ RT::Interface::Email::Gateway(\%args);
+ $tickets = RT::Tickets->new($RT::SystemUser);
+$tickets->OrderBy(FIELD => 'id', ORDER => 'DESC');
+$tickets->Limit(FIELD => 'id' ,OPERATOR => '>', VALUE => '0');
+$tick= $tickets->First();
+ok ($tick->Id, "found ticket ".$tick->Id);
+
+my $cc = first_attach($tick)->GetHeader('RT-Send-Cc');
+like ($cc , qr/test1/, "Found test 1");
+like ($cc , qr/test2/, "Found test 2");
+like ($cc , qr/test3/, "Found test 3");
+like ($cc , qr/test4/, "Found test 4");
+like ($cc , qr/test5/, "Found test 5");
+
+# }}}
+
+diag q{regression test for #5248 from rt3.fsck.com} if $ENV{TEST_VERBOSE};
+{
+ my $subject_folding_email = RT::Test::get_relocatable_file(
+ 'subject-with-folding-ws', (File::Spec->updir(), 'data', 'emails'));
+ my $content = RT::Test->file_content($subject_folding_email);
+ my ($status, $msg, $ticket) = RT::Interface::Email::Gateway(
+ { message => $content, queue => 1, action => 'correspond' }
+ );
+ ok ($status, 'created ticket') or diag "error: $msg";
+ ok ($ticket->id, "found ticket ". $ticket->id);
+ is ($ticket->Subject, 'test', 'correct subject');
+}
+
+diag q{regression test for #5248 from rt3.fsck.com} if $ENV{TEST_VERBOSE};
+{
+ my $long_subject_email = RT::Test::get_relocatable_file('very-long-subject',
+ (File::Spec->updir(), 'data', 'emails'));
+ my $content = RT::Test->file_content($long_subject_email);
+ my ($status, $msg, $ticket) = RT::Interface::Email::Gateway(
+ { message => $content, queue => 1, action => 'correspond' }
+ );
+ ok ($status, 'created ticket') or diag "error: $msg";
+ ok ($ticket->id, "found ticket ". $ticket->id);
+ is ($ticket->Subject, '0123456789'x20, 'correct subject');
+}
+
+
+
+# Don't taint the environment
+$everyone->PrincipalObj->RevokeRight(Right =>'SuperUser');
+1;
diff --git a/rt/t/mail/verp.t b/rt/t/mail/verp.t
new file mode 100644
index 000000000..79ede90ab
--- /dev/null
+++ b/rt/t/mail/verp.t
@@ -0,0 +1,8 @@
+#!/usr/bin/perl -w
+
+use strict;
+use RT::Test tests => 1;
+TODO: {
+ todo_skip "No tests written for VERP yet", 1;
+ ok(1,"a test to skip");
+}
diff --git a/rt/t/maildigest/attributes.t b/rt/t/maildigest/attributes.t
new file mode 100644
index 000000000..ba2a58566
--- /dev/null
+++ b/rt/t/maildigest/attributes.t
@@ -0,0 +1,168 @@
+#!/usr/bin/perl -w
+
+use warnings;
+use strict;
+use RT;
+use RT::Test tests => 31;
+my @users = qw/ emailnormal@example.com emaildaily@example.com emailweekly@example.com emailsusp@example.com /;
+
+my( $ret, $msg );
+my $user_n = RT::User->new( $RT::SystemUser );
+( $ret, $msg ) = $user_n->LoadOrCreateByEmail( $users[0] );
+ok( $ret, "user with default email prefs created: $msg" );
+$user_n->SetPrivileged( 1 );
+
+my $user_d = RT::User->new( $RT::SystemUser );
+( $ret, $msg ) = $user_d->LoadOrCreateByEmail( $users[1] );
+ok( $ret, "user with daily digest email prefs created: $msg" );
+# Set a username & password for testing the interface.
+$user_d->SetPrivileged( 1 );
+$user_d->SetPreferences($RT::System => { %{ $user_d->Preferences( $RT::System ) || {}}, EmailFrequency => 'Daily digest'});
+
+
+
+my $user_w = RT::User->new( $RT::SystemUser );
+( $ret, $msg ) = $user_w->LoadOrCreateByEmail( $users[2] );
+ok( $ret, "user with weekly digest email prefs created: $msg" );
+$user_w->SetPrivileged( 1 );
+$user_w->SetPreferences($RT::System => { %{ $user_w->Preferences( $RT::System ) || {}}, EmailFrequency => 'Weekly digest'});
+
+my $user_s = RT::User->new( $RT::SystemUser );
+( $ret, $msg ) = $user_s->LoadOrCreateByEmail( $users[3] );
+ok( $ret, "user with suspended email prefs created: $msg" );
+$user_s->SetPreferences($RT::System => { %{ $user_s->Preferences( $RT::System ) || {}}, EmailFrequency => 'Suspended'});
+$user_s->SetPrivileged( 1 );
+
+
+is(RT::Config->Get('EmailFrequency' => $user_s), 'Suspended');
+
+# Make a testing queue for ourselves.
+my $testq = RT::Queue->new( $RT::SystemUser );
+if( $testq->ValidateName( 'EmailDigest-testqueue' ) ) {
+ ( $ret, $msg ) = $testq->Create( Name => 'EmailDigest-testqueue' );
+ ok( $ret, "Our test queue is created: $msg" );
+} else {
+ $testq->Load( 'EmailDigest-testqueue' );
+ ok( $testq->id, "Our test queue is loaded" );
+}
+
+# Allow anyone to open a ticket on the test queue.
+my $everyone = RT::Group->new( $RT::SystemUser );
+( $ret, $msg ) = $everyone->LoadSystemInternalGroup( 'Everyone' );
+ok( $ret, "Loaded 'everyone' group: $msg" );
+
+( $ret, $msg ) = $everyone->PrincipalObj->GrantRight( Right => 'CreateTicket',
+ Object => $testq );
+ok( $ret || $msg =~ /already has/, "Granted everyone CreateTicket on testq: $msg" );
+
+# Make user_d an admincc for the queue.
+( $ret, $msg ) = $user_d->PrincipalObj->GrantRight( Right => 'AdminQueue',
+ Object => $testq );
+ok( $ret || $msg =~ /already has/, "Granted dduser AdminQueue on testq: $msg" );
+( $ret, $msg ) = $testq->AddWatcher( Type => 'AdminCc',
+ PrincipalId => $user_d->PrincipalObj->id );
+ok( $ret || $msg =~ /already/, "dduser added as a queue watcher: $msg" );
+
+# Give the others queue rights.
+( $ret, $msg ) = $user_n->PrincipalObj->GrantRight( Right => 'AdminQueue',
+ Object => $testq );
+ok( $ret || $msg =~ /already has/, "Granted emailnormal right on testq: $msg" );
+( $ret, $msg ) = $user_w->PrincipalObj->GrantRight( Right => 'AdminQueue',
+ Object => $testq );
+ok( $ret || $msg =~ /already has/, "Granted emailweekly right on testq: $msg" );
+( $ret, $msg ) = $user_s->PrincipalObj->GrantRight( Right => 'AdminQueue',
+ Object => $testq );
+ok( $ret || $msg =~ /already has/, "Granted emailsusp right on testq: $msg" );
+
+# Create a ticket with To: Cc: Bcc: fields using our four users.
+my $id;
+my $ticket = RT::Ticket->new( $RT::SystemUser );
+( $id, $ret, $msg ) = $ticket->Create( Queue => $testq->Name,
+ Requestor => [ $user_w->Name ],
+ Subject => 'Test ticket for RT::Extension::EmailDigest',
+ );
+ok( $ret, "Ticket $id created: $msg" );
+
+# Make the other users ticket watchers.
+( $ret, $msg ) = $ticket->AddWatcher( Type => 'Cc',
+ PrincipalId => $user_n->PrincipalObj->id );
+ok( $ret, "Added user_n as a ticket watcher: $msg" );
+( $ret, $msg ) = $ticket->AddWatcher( Type => 'Cc',
+ PrincipalId => $user_s->PrincipalObj->id );
+ok( $ret, "Added user_s as a ticket watcher: $msg" );
+
+my $obj;
+($id, $msg, $obj ) = $ticket->Correspond(
+ Content => "This is a ticket response for CC action" );
+ok( $ret, "Transaction created: $msg" );
+
+# Get the deferred notifications that should result. Should be two for
+# email daily, and one apiece for emailweekly and emailsusp.
+my @notifications;
+
+my $txns = RT::Transactions->new( $RT::SystemUser );
+$txns->LimitToTicket( $ticket->id );
+my( $c_daily, $c_weekly, $c_susp ) = ( 0, 0, 0 );
+while( my $txn = $txns->Next ) {
+ my @daily_rcpt = $txn->DeferredRecipients( 'daily' );
+ my @weekly_rcpt = $txn->DeferredRecipients('weekly' );
+ my @susp_rcpt = $txn->DeferredRecipients( 'susp' );
+
+ $c_daily++ if @daily_rcpt;
+ $c_weekly++ if @weekly_rcpt;
+ $c_susp++ if @susp_rcpt;
+
+ # If the transaction has content...
+ if( $txn->ContentObj ) {
+ # ...none of the deferred folk should be in the header.
+ my $headerstr = $txn->ContentObj->Headers;
+ foreach my $rcpt( @daily_rcpt, @weekly_rcpt, @susp_rcpt ) {
+ ok( $headerstr !~ /$rcpt/, "Deferred recipient $rcpt not found in header" );
+ }
+ }
+}
+
+# Finally, check to see that we got the correct number of each sort of
+# deferred recipient.
+is( $c_daily, 2, "correct number of daily-sent messages" );
+is( $c_weekly, 2, "correct number of weekly-sent messages" );
+is( $c_susp, 1, "correct number of suspended messages" );
+
+
+
+
+
+# Now let's actually run the daily and weekly digest tool to make sure we generate those
+
+# the first time get the content
+email_digest_like( '--mode daily --print', qr/in the last day/ );
+# The second time run it for real so we make sure that we get RT to mark the txn as sent
+email_digest_like( '--mode daily', qr/maildaily\@/ );
+# now we should have nothing to do, so no content.
+email_digest_like( '--mode daily --print', '' );
+
+# the first time get the content
+email_digest_like( '--mode weekly --print', qr/in the last seven days/ );
+# The second time run it for real so we make sure that we get RT to mark the txn as sent
+email_digest_like( '--mode weekly', qr/mailweekly\@/ );
+# now we should have nothing to do, so no content.
+email_digest_like( '--mode weekly --print', '' );
+
+sub email_digest_like {
+ my $arg = shift;
+ my $pattern = shift;
+
+ local $Test::Builder::Level = $Test::Builder::Level + 1;
+
+ my $perl = $^X . ' ' . join ' ', map { "-I$_" } @INC;
+ open my $digester, "-|", "$perl $RT::SbinPath/rt-email-digest $arg";
+ my @results = <$digester>;
+ my $content = join '', @results;
+ if ( ref $pattern && ref $pattern eq 'Regexp' ) {
+ like($content, $pattern);
+ }
+ else {
+ is( $content, $pattern );
+ }
+ close $digester;
+}
diff --git a/rt/t/pod.t b/rt/t/pod.t
new file mode 100644
index 000000000..d11a497eb
--- /dev/null
+++ b/rt/t/pod.t
@@ -0,0 +1,7 @@
+use strict;
+use warnings;
+
+use Test::More;
+eval "use Test::Pod 1.14";
+plan skip_all => "Test::Pod 1.14 required for testing POD" if $@;
+all_pod_files_ok();
diff --git a/rt/t/rtname.t b/rt/t/rtname.t
new file mode 100644
index 000000000..ef6092bb2
--- /dev/null
+++ b/rt/t/rtname.t
@@ -0,0 +1,34 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+use RT::Test nodata => 1, tests => 9;
+
+use RT::Interface::Email;
+
+# normal use case, regexp set to rtname
+RT->Config->Set( rtname => "site" );
+RT->Config->Set( EmailSubjectTagRegex => qr/site/ );
+RT->Config->Set( rtname => undef );
+is(RT::Interface::Email::ParseTicketId("[site #123] test"), 123);
+is(RT::Interface::Email::ParseTicketId("[othersite #123] test"), undef);
+
+# oops usecase, where the regexp is scragged
+RT->Config->Set( rtname => "site" );
+RT->Config->Set( EmailSubjectTagRegex => undef );
+is(RT::Interface::Email::ParseTicketId("[site #123] test"), 123);
+is(RT::Interface::Email::ParseTicketId("[othersite #123] test"), undef);
+
+# set to a simple regexp. NOTE: we no longer match "site"
+RT->Config->Set( rtname => "site");
+RT->Config->Set( EmailSubjectTagRegex => qr/newsite/);
+is(RT::Interface::Email::ParseTicketId("[site #123] test"), undef);
+is(RT::Interface::Email::ParseTicketId("[newsite #123] test"), 123);
+
+# set to a more complex regexp
+RT->Config->Set( rtname => "site" );
+RT->Config->Set( EmailSubjectTagRegex => qr/newsite|site/ );
+is(RT::Interface::Email::ParseTicketId("[site #123] test"), 123);
+is(RT::Interface::Email::ParseTicketId("[newsite #123] test"), 123);
+is(RT::Interface::Email::ParseTicketId("[othersite #123] test"), undef);
+
diff --git a/rt/t/savedsearch.t b/rt/t/savedsearch.t
new file mode 100644
index 000000000..5798f79cb
--- /dev/null
+++ b/rt/t/savedsearch.t
@@ -0,0 +1,185 @@
+use strict;
+use warnings;
+BEGIN { $ENV{'LANG'} = 'C' }
+use RT;
+use RT::User;
+use RT::Group;
+use RT::Ticket;
+use RT::Queue;
+
+use RT::Test tests => 27;
+use_ok('RT::SavedSearch');
+use_ok('RT::SavedSearches');
+
+use Test::Warn;
+
+# Set up some infrastructure. These calls are tested elsewhere.
+
+my $searchuser = RT::User->new($RT::SystemUser);
+my ($ret, $msg) = $searchuser->Create(Name => 'searchuser'.$$,
+ Privileged => 1,
+ EmailAddress => "searchuser\@p$$.example.com",
+ RealName => 'Search user');
+ok($ret, "created searchuser: $msg");
+$searchuser->PrincipalObj->GrantRight(Right => 'LoadSavedSearch');
+$searchuser->PrincipalObj->GrantRight(Right => 'CreateSavedSearch');
+$searchuser->PrincipalObj->GrantRight(Right => 'ModifySelf');
+
+# This is the group whose searches searchuser should be able to see.
+my $ingroup = RT::Group->new($RT::SystemUser);
+$ingroup->CreateUserDefinedGroup(Name => 'searchgroup1'.$$);
+$ingroup->AddMember($searchuser->Id);
+$searchuser->PrincipalObj->GrantRight(Right => 'EditSavedSearches',
+ Object => $ingroup);
+$searchuser->PrincipalObj->GrantRight(Right => 'ShowSavedSearches',
+ Object => $ingroup);
+
+# This is the group whose searches searchuser should not be able to see.
+my $outgroup = RT::Group->new($RT::SystemUser);
+$outgroup->CreateUserDefinedGroup(Name => 'searchgroup2'.$$);
+$outgroup->AddMember($RT::SystemUser->Id);
+
+my $queue = RT::Queue->new($RT::SystemUser);
+$queue->Create(Name => 'SearchQueue'.$$);
+$searchuser->PrincipalObj->GrantRight(Right => 'SeeQueue', Object => $queue);
+$searchuser->PrincipalObj->GrantRight(Right => 'ShowTicket', Object => $queue);
+$searchuser->PrincipalObj->GrantRight(Right => 'OwnTicket', Object => $queue);
+
+
+my $ticket = RT::Ticket->new($RT::SystemUser);
+$ticket->Create(Queue => $queue->Id,
+ Requestor => [ $searchuser->Name ],
+ Owner => $searchuser,
+ Subject => 'saved search test');
+
+
+# Now start the search madness.
+my $curruser = RT::CurrentUser->new($searchuser);
+my $format = '\' <b><a href="/Ticket/Display.html?id=__id__">__id__</a></b>/TITLE:#\',
+\'<b><a href="/Ticket/Display.html?id=__id__">__Subject__</a></b>/TITLE:Subject\',
+\'__Status__\',
+\'__QueueName__\',
+\'__OwnerName__\',
+\'__Priority__\',
+\'__NEWLINE__\',
+\'\',
+\'<small>__Requestors__</small>\',
+\'<small>__CreatedRelative__</small>\',
+\'<small>__ToldRelative__</small>\',
+\'<small>__LastUpdatedRelative__</small>\',
+\'<small>__TimeLeft__</small>\'';
+
+my $mysearch = RT::SavedSearch->new($curruser);
+($ret, $msg) = $mysearch->Save(Privacy => 'RT::User-' . $searchuser->Id,
+ Type => 'Ticket',
+ Name => 'owned by me',
+ SearchParams => {'Format' => $format,
+ 'Query' => "Owner = '"
+ . $searchuser->Name
+ . "'"});
+ok($ret, "mysearch was created");
+
+
+my $groupsearch = RT::SavedSearch->new($curruser);
+($ret, $msg) = $groupsearch->Save(Privacy => 'RT::Group-' . $ingroup->Id,
+ Type => 'Ticket',
+ Name => 'search queue',
+ SearchParams => {'Format' => $format,
+ 'Query' => "Queue = '"
+ . $queue->Name . "'"});
+ok($ret, "groupsearch was created");
+
+my $othersearch = RT::SavedSearch->new($curruser);
+($ret, $msg) = $othersearch->Save(Privacy => 'RT::Group-' . $outgroup->Id,
+ Type => 'Ticket',
+ Name => 'searchuser requested',
+ SearchParams => {'Format' => $format,
+ 'Query' =>
+ "Requestor.Name LIKE 'search'"});
+ok(!$ret, "othersearch NOT created");
+like($msg, qr/Failed to load object for/, "...for the right reason");
+
+$othersearch = RT::SavedSearch->new($RT::SystemUser);
+($ret, $msg) = $othersearch->Save(Privacy => 'RT::Group-' . $outgroup->Id,
+ Type => 'Ticket',
+ Name => 'searchuser requested',
+ SearchParams => {'Format' => $format,
+ 'Query' =>
+ "Requestor.Name LIKE 'search'"});
+ok($ret, "othersearch created by systemuser");
+
+# Now try to load some searches.
+
+# This should work.
+my $loadedsearch1 = RT::SavedSearch->new($curruser);
+$loadedsearch1->Load('RT::User-'.$curruser->Id, $mysearch->Id);
+is($loadedsearch1->Id, $mysearch->Id, "Loaded mysearch");
+like($loadedsearch1->GetParameter('Query'), qr/Owner/,
+ "Retrieved query of mysearch");
+# Check through the other accessor methods.
+is($loadedsearch1->Privacy, 'RT::User-' . $curruser->Id,
+ "Privacy of mysearch correct");
+is($loadedsearch1->Name, 'owned by me', "Name of mysearch correct");
+is($loadedsearch1->Type, 'Ticket', "Type of mysearch correct");
+
+# See if it can be used to search for tickets.
+my $tickets = RT::Tickets->new($curruser);
+$tickets->FromSQL($loadedsearch1->GetParameter('Query'));
+is($tickets->Count, 1, "Found a ticket");
+
+# This should fail -- wrong object.
+# my $loadedsearch2 = RT::SavedSearch->new($curruser);
+# $loadedsearch2->Load('RT::User-'.$curruser->Id, $groupsearch->Id);
+# isnt($loadedsearch2->Id, $othersearch->Id, "Didn't load groupsearch as mine");
+# ...but this should succeed.
+my $loadedsearch3 = RT::SavedSearch->new($curruser);
+$loadedsearch3->Load('RT::Group-'.$ingroup->Id, $groupsearch->Id);
+is($loadedsearch3->Id, $groupsearch->Id, "Loaded groupsearch");
+like($loadedsearch3->GetParameter('Query'), qr/Queue/,
+ "Retrieved query of groupsearch");
+# Can it get tickets?
+$tickets = RT::Tickets->new($curruser);
+$tickets->FromSQL($loadedsearch3->GetParameter('Query'));
+is($tickets->Count, 1, "Found a ticket");
+
+# This should fail -- no permission.
+my $loadedsearch4 = RT::SavedSearch->new($curruser);
+
+warning_like {
+ $loadedsearch4->Load($othersearch->Privacy, $othersearch->Id);
+} qr/Could not load object RT::Group-\d+ when loading search/;
+
+isnt($loadedsearch4->Id, $othersearch->Id, "Did not load othersearch");
+
+# Try to update an existing search.
+$loadedsearch1->Update( SearchParams => {'Format' => $format,
+ 'Query' => "Queue = '" . $queue->Name . "'" } );
+like($loadedsearch1->GetParameter('Query'), qr/Queue/,
+ "Updated mysearch parameter");
+is($loadedsearch1->Type, 'Ticket', "mysearch is still for tickets");
+is($loadedsearch1->Privacy, 'RT::User-'.$curruser->Id,
+ "mysearch still belongs to searchuser");
+like($mysearch->GetParameter('Query'), qr/Queue/, "other mysearch object updated");
+
+
+## Right ho. Test the pseudo-collection object.
+
+my $genericsearch = RT::SavedSearch->new($curruser);
+$genericsearch->Save(Name => 'generic search',
+ Type => 'all',
+ SearchParams => {'Query' => "Queue = 'General'"});
+
+my $ticketsearches = RT::SavedSearches->new($curruser);
+$ticketsearches->LimitToPrivacy('RT::User-'.$curruser->Id, 'Ticket');
+is($ticketsearches->Count, 1, "Found searchuser's ticket searches");
+
+my $allsearches = RT::SavedSearches->new($curruser);
+$allsearches->LimitToPrivacy('RT::User-'.$curruser->Id);
+is($allsearches->Count, 2, "Found all searchuser's searches");
+
+# Delete a search.
+($ret, $msg) = $genericsearch->Delete;
+ok($ret, "Deleted genericsearch");
+$allsearches->LimitToPrivacy('RT::User-'.$curruser->Id);
+is($allsearches->Count, 1, "Found all searchuser's searches after deletion");
+
diff --git a/rt/t/shredder/00load.t b/rt/t/shredder/00load.t
new file mode 100644
index 000000000..1e06261bc
--- /dev/null
+++ b/rt/t/shredder/00load.t
@@ -0,0 +1,29 @@
+use strict;
+use warnings;
+use File::Spec;
+use Test::More tests => 11;
+use RT::Test ();
+
+BEGIN {
+ my $shredder_utils = RT::Test::get_relocatable_file('utils.pl',
+ File::Spec->curdir());
+ require $shredder_utils;
+}
+
+use_ok("RT::Shredder");
+
+use_ok("RT::Shredder::Plugin");
+use_ok("RT::Shredder::Plugin::Base");
+
+# search plugins
+use_ok("RT::Shredder::Plugin::Base::Search");
+use_ok("RT::Shredder::Plugin::Objects");
+use_ok("RT::Shredder::Plugin::Attachments");
+use_ok("RT::Shredder::Plugin::Tickets");
+use_ok("RT::Shredder::Plugin::Users");
+
+# dump plugins
+use_ok("RT::Shredder::Plugin::Base::Dump");
+use_ok("RT::Shredder::Plugin::SQLDump");
+use_ok("RT::Shredder::Plugin::Summary");
+
diff --git a/rt/t/shredder/00skeleton.t b/rt/t/shredder/00skeleton.t
new file mode 100644
index 000000000..eab9433cd
--- /dev/null
+++ b/rt/t/shredder/00skeleton.t
@@ -0,0 +1,25 @@
+#!/usr/bin/perl -w
+
+use strict;
+use warnings;
+
+use Test::Deep;
+use File::Spec;
+use Test::More tests => 1;
+use RT::Test ();
+BEGIN {
+ my $shredder_utils = RT::Test::get_relocatable_file('utils.pl',
+ File::Spec->curdir());
+ require $shredder_utils;
+}
+init_db();
+
+
+create_savepoint('clean'); # backup of the clean RT DB
+my $shredder = shredder_new(); # new shredder object
+
+# ....
+# create and wipe RT objects
+#
+
+cmp_deeply( dump_current_and_savepoint('clean'), "current DB equal to savepoint");
diff --git a/rt/t/shredder/01basics.t b/rt/t/shredder/01basics.t
new file mode 100644
index 000000000..450f2df8c
--- /dev/null
+++ b/rt/t/shredder/01basics.t
@@ -0,0 +1,32 @@
+#!/usr/bin/perl -w
+
+use strict;
+use warnings;
+
+use Test::Deep;
+use File::Spec;
+use Test::More tests => 3;
+use RT::Test ();
+BEGIN {
+ my $shredder_utils = RT::Test::get_relocatable_file('utils.pl',
+ File::Spec->curdir());
+ require $shredder_utils;
+}
+init_db();
+
+
+create_savepoint();
+
+use RT::Tickets;
+my $ticket = RT::Ticket->new( $RT::SystemUser );
+my ($id) = $ticket->Create( Subject => 'test', Queue => 1 );
+ok( $id, "created new ticket" );
+
+$ticket = RT::Ticket->new( $RT::SystemUser );
+my ($status, $msg) = $ticket->Load( $id );
+ok( $id, "load ticket" ) or diag( "error: $msg" );
+
+my $shredder = shredder_new();
+$shredder->Wipeout( Object => $ticket );
+
+cmp_deeply( dump_current_and_savepoint(), "current DB equal to savepoint");
diff --git a/rt/t/shredder/01ticket.t b/rt/t/shredder/01ticket.t
new file mode 100644
index 000000000..5625b985d
--- /dev/null
+++ b/rt/t/shredder/01ticket.t
@@ -0,0 +1,86 @@
+#!/usr/bin/perl -w
+
+use strict;
+use warnings;
+
+use Test::Deep;
+use File::Spec;
+use Test::More tests => 15;
+use RT::Test ();
+
+
+BEGIN {
+ my $shredder_utils = RT::Test::get_relocatable_file('utils.pl',
+ File::Spec->curdir());
+ require $shredder_utils;
+}
+
+init_db();
+create_savepoint('clean');
+
+use RT::Ticket;
+use RT::Tickets;
+
+{
+ my $ticket = RT::Ticket->new( $RT::SystemUser );
+ my ($id) = $ticket->Create( Subject => 'test', Queue => 1 );
+ ok( $id, "created new ticket" );
+ $ticket->Delete;
+ is( $ticket->Status, 'deleted', "successfuly changed status" );
+
+ my $tickets = RT::Tickets->new( $RT::SystemUser );
+ $tickets->{'allow_deleted_search'} = 1;
+ $tickets->LimitStatus( VALUE => 'deleted' );
+ is( $tickets->Count, 1, "found one deleted ticket" );
+
+ my $shredder = shredder_new();
+ $shredder->PutObjects( Objects => $tickets );
+ $shredder->WipeoutAll;
+}
+cmp_deeply( dump_current_and_savepoint('clean'), "current DB equal to savepoint");
+
+{
+ my $parent = RT::Ticket->new( $RT::SystemUser );
+ my ($pid) = $parent->Create( Subject => 'test', Queue => 1 );
+ ok( $pid, "created new ticket" );
+ create_savepoint('parent_ticket');
+
+ my $child = RT::Ticket->new( $RT::SystemUser );
+ my ($cid) = $child->Create( Subject => 'test', Queue => 1 );
+ ok( $cid, "created new ticket" );
+
+ my ($status, $msg) = $parent->AddLink( Type => 'MemberOf', Target => $cid );
+ ok( $status, "Added link between tickets") or diag("error: $msg");
+ my $shredder = shredder_new();
+ $shredder->PutObjects( Objects => $child );
+ $shredder->WipeoutAll;
+ cmp_deeply( dump_current_and_savepoint('parent_ticket'), "current DB equal to savepoint");
+
+ $shredder->PutObjects( Objects => $parent );
+ $shredder->WipeoutAll;
+}
+cmp_deeply( dump_current_and_savepoint('clean'), "current DB equal to savepoint");
+
+{
+ my $parent = RT::Ticket->new( $RT::SystemUser );
+ my ($pid) = $parent->Create( Subject => 'test', Queue => 1 );
+ ok( $pid, "created new ticket" );
+ my ($status, $msg) = $parent->Delete;
+ ok( $status, 'deleted parent ticket');
+ create_savepoint('parent_ticket');
+
+ my $child = RT::Ticket->new( $RT::SystemUser );
+ my ($cid) = $child->Create( Subject => 'test', Queue => 1 );
+ ok( $cid, "created new ticket" );
+
+ ($status, $msg) = $parent->AddLink( Type => 'DependsOn', Target => $cid );
+ ok( $status, "Added link between tickets") or diag("error: $msg");
+ my $shredder = shredder_new();
+ $shredder->PutObjects( Objects => $child );
+ $shredder->WipeoutAll;
+ cmp_deeply( dump_current_and_savepoint('parent_ticket'), "current DB equal to savepoint");
+
+ $shredder->PutObjects( Objects => $parent );
+ $shredder->WipeoutAll;
+}
+cmp_deeply( dump_current_and_savepoint('clean'), "current DB equal to savepoint");
diff --git a/rt/t/shredder/02group_member.t b/rt/t/shredder/02group_member.t
new file mode 100644
index 000000000..b68557a8b
--- /dev/null
+++ b/rt/t/shredder/02group_member.t
@@ -0,0 +1,103 @@
+#!/usr/bin/perl -w
+
+use strict;
+use warnings;
+
+use Test::Deep;
+use File::Spec;
+use Test::More tests => 22;
+use RT::Test ();
+BEGIN {
+ my $shredder_utils = RT::Test::get_relocatable_file('utils.pl',
+ File::Spec->curdir());
+ require $shredder_utils;
+}
+init_db();
+
+
+### nested membership check
+{
+ create_savepoint('clean');
+ my $pgroup = RT::Group->new( $RT::SystemUser );
+ my ($pgid) = $pgroup->CreateUserDefinedGroup( Name => 'Parent group' );
+ ok( $pgid, "created parent group" );
+ is( $pgroup->id, $pgid, "id is correct" );
+
+ my $cgroup = RT::Group->new( $RT::SystemUser );
+ my ($cgid) = $cgroup->CreateUserDefinedGroup( Name => 'Child group' );
+ ok( $cgid, "created child group" );
+ is( $cgroup->id, $cgid, "id is correct" );
+
+ my ($status, $msg) = $pgroup->AddMember( $cgroup->id );
+ ok( $status, "added child group to parent") or diag "error: $msg";
+
+ create_savepoint('bucreate'); # before user create
+ my $user = RT::User->new( $RT::SystemUser );
+ my $uid;
+ ($uid, $msg) = $user->Create( Name => 'new user', Privileged => 1, Disabled => 0 );
+ ok( $uid, "created new user" ) or diag "error: $msg";
+ is( $user->id, $uid, "id is correct" );
+
+ create_savepoint('buadd'); # before group add
+ ($status, $msg) = $cgroup->AddMember( $user->id );
+ ok( $status, "added user to child group") or diag "error: $msg";
+
+ my $members = RT::GroupMembers->new( $RT::SystemUser );
+ $members->Limit( FIELD => 'MemberId', VALUE => $uid );
+ $members->Limit( FIELD => 'GroupId', VALUE => $cgid );
+ is( $members->Count, 1, "find membership record" );
+
+ my $shredder = shredder_new();
+ $shredder->PutObjects( Objects => $members );
+ $shredder->WipeoutAll();
+ cmp_deeply( dump_current_and_savepoint('buadd'), "current DB equal to savepoint");
+
+ $shredder->PutObjects( Objects => $user );
+ $shredder->WipeoutAll();
+ cmp_deeply( dump_current_and_savepoint('bucreate'), "current DB equal to savepoint");
+
+ $shredder->PutObjects( Objects => [$pgroup, $cgroup] );
+ $shredder->WipeoutAll();
+ cmp_deeply( dump_current_and_savepoint('clean'), "current DB equal to savepoint");
+}
+
+### deleting member of the ticket Owner role group
+{
+ restore_savepoint('clean');
+
+ my $user = RT::User->new( $RT::SystemUser );
+ my ($uid, $msg) = $user->Create( Name => 'new user', Privileged => 1, Disabled => 0 );
+ ok( $uid, "created new user" ) or diag "error: $msg";
+ is( $user->id, $uid, "id is correct" );
+
+ use RT::Queue;
+ my $queue = new RT::Queue( $RT::SystemUser );
+ $queue->Load('general');
+ ok( $queue->id, "queue loaded succesfully" );
+
+ $user->PrincipalObj->GrantRight( Right => 'OwnTicket', Object => $queue );
+
+ use RT::Tickets;
+ my $ticket = RT::Ticket->new( $RT::SystemUser );
+ my ($id) = $ticket->Create( Subject => 'test', Queue => $queue->id );
+ ok( $id, "created new ticket" );
+ $ticket = RT::Ticket->new( $RT::SystemUser );
+ my $status;
+ ($status, $msg) = $ticket->Load( $id );
+ ok( $id, "load ticket" ) or diag( "error: $msg" );
+
+ ($status, $msg) = $ticket->SetOwner( $user->id );
+ ok( $status, "owner successfuly set") or diag( "error: $msg" );
+ is( $ticket->Owner, $user->id, "owner successfuly set") or diag( "error: $msg" );
+
+ my $member = $ticket->OwnerGroup->MembersObj->First;
+ my $shredder = shredder_new();
+ $shredder->PutObjects( Objects => $member );
+ $shredder->WipeoutAll();
+
+ $ticket = RT::Ticket->new( $RT::SystemUser );
+ ($status, $msg) = $ticket->Load( $id );
+ ok( $id, "load ticket" ) or diag( "error: $msg" );
+ is( $ticket->Owner, $RT::Nobody->id, "owner switched back to nobody" );
+ is( $ticket->OwnerGroup->MembersObj->First->MemberId, $RT::Nobody->id, "and owner role group member is nobody");
+}
diff --git a/rt/t/shredder/02queue.t b/rt/t/shredder/02queue.t
new file mode 100644
index 000000000..197cf63c8
--- /dev/null
+++ b/rt/t/shredder/02queue.t
@@ -0,0 +1,125 @@
+#!/usr/bin/perl -w
+
+use strict;
+use warnings;
+
+use Test::Deep;
+use File::Spec;
+use Test::More tests => 16;
+use RT::Test ();
+BEGIN {
+ my $shredder_utils = RT::Test::get_relocatable_file('utils.pl',
+ File::Spec->curdir());
+ require $shredder_utils;
+}
+init_db();
+
+
+diag 'simple queue' if $ENV{'TEST_VERBOSE'};
+{
+ create_savepoint('clean');
+ my $queue = RT::Queue->new( $RT::SystemUser );
+ my ($id, $msg) = $queue->Create( Name => 'my queue' );
+ ok($id, 'created queue') or diag "error: $msg";
+
+ my $shredder = shredder_new();
+ $shredder->PutObjects( Objects => $queue );
+ $shredder->WipeoutAll;
+ cmp_deeply( dump_current_and_savepoint('clean'), "current DB equal to savepoint");
+}
+
+diag 'queue with scrip' if $ENV{'TEST_VERBOSE'};
+{
+ create_savepoint('clean');
+ my $queue = RT::Queue->new( $RT::SystemUser );
+ my ($id, $msg) = $queue->Create( Name => 'my queue' );
+ ok($id, 'created queue') or diag "error: $msg";
+
+ my $scrip = RT::Scrip->new( $RT::SystemUser );
+ ($id, $msg) = $scrip->Create(
+ Description => 'my scrip',
+ Queue => $queue->id,
+ ScripCondition => 'On Create',
+ ScripAction => 'Open Tickets',
+ Template => 'Blank',
+ );
+ ok($id, 'created scrip') or diag "error: $msg";
+
+ my $shredder = shredder_new();
+ $shredder->PutObjects( Objects => $queue );
+ $shredder->WipeoutAll;
+ cmp_deeply( dump_current_and_savepoint('clean'), "current DB equal to savepoint");
+}
+
+diag 'queue with template' if $ENV{'TEST_VERBOSE'};
+{
+ create_savepoint('clean');
+ my $queue = RT::Queue->new( $RT::SystemUser );
+ my ($id, $msg) = $queue->Create( Name => 'my queue' );
+ ok($id, 'created queue') or diag "error: $msg";
+
+ my $template = RT::Template->new( $RT::SystemUser );
+ ($id, $msg) = $template->Create(
+ Name => 'my template',
+ Queue => $queue->id,
+ Content => "\nsome content",
+ );
+ ok($id, 'created template') or diag "error: $msg";
+
+ my $shredder = shredder_new();
+ $shredder->PutObjects( Objects => $queue );
+ $shredder->WipeoutAll;
+ cmp_deeply( dump_current_and_savepoint('clean'), "current DB equal to savepoint");
+}
+
+diag 'queue with a right granted' if $ENV{'TEST_VERBOSE'};
+{
+ create_savepoint('clean');
+ my $queue = RT::Queue->new( $RT::SystemUser );
+ my ($id, $msg) = $queue->Create( Name => 'my queue' );
+ ok($id, 'created queue') or diag "error: $msg";
+
+ my $group = RT::Group->new( $RT::SystemUser );
+ $group->LoadSystemInternalGroup('Everyone');
+ ok($group->id, 'loaded group');
+
+ ($id, $msg) = $group->PrincipalObj->GrantRight(
+ Right => 'CreateTicket',
+ Object => $queue,
+ );
+ ok($id, 'granted right') or diag "error: $msg";
+
+ my $shredder = shredder_new();
+ $shredder->PutObjects( Objects => $queue );
+ $shredder->WipeoutAll;
+ cmp_deeply( dump_current_and_savepoint('clean'), "current DB equal to savepoint");
+}
+
+diag 'queue with a watcher' if $ENV{'TEST_VERBOSE'};
+{
+# XXX, FIXME: if uncomment these lines then we'll get 'Bizarre...'
+# create_savepoint('clean');
+ my $group = RT::Group->new( $RT::SystemUser );
+ my ($id, $msg) = $group->CreateUserDefinedGroup(Name => 'my group');
+ ok($id, 'created group') or diag "error: $msg";
+
+ create_savepoint('bqcreate');
+ my $queue = RT::Queue->new( $RT::SystemUser );
+ ($id, $msg) = $queue->Create( Name => 'my queue' );
+ ok($id, 'created queue') or diag "error: $msg";
+
+ ($id, $msg) = $queue->AddWatcher(
+ Type => 'Cc',
+ PrincipalId => $group->id,
+ );
+ ok($id, 'added watcher') or diag "error: $msg";
+
+ my $shredder = shredder_new();
+ $shredder->PutObjects( Objects => $queue );
+ $shredder->WipeoutAll;
+ cmp_deeply( dump_current_and_savepoint('bqcreate'), "current DB equal to savepoint");
+
+# $shredder->PutObjects( Objects => $group );
+# $shredder->WipeoutAll;
+# cmp_deeply( dump_current_and_savepoint('clean'), "current DB equal to savepoint");
+}
diff --git a/rt/t/shredder/02template.t b/rt/t/shredder/02template.t
new file mode 100644
index 000000000..d4c323e09
--- /dev/null
+++ b/rt/t/shredder/02template.t
@@ -0,0 +1,76 @@
+#!/usr/bin/perl -w
+
+use strict;
+use warnings;
+
+use Test::Deep;
+use File::Spec;
+use Test::More tests => 7;
+use RT::Test ();
+BEGIN {
+ my $shredder_utils = RT::Test::get_relocatable_file('utils.pl',
+ File::Spec->curdir());
+ require $shredder_utils;
+}
+init_db();
+
+
+diag 'global template' if $ENV{'TEST_VERBOSE'};
+{
+ create_savepoint('clean');
+ my $template = RT::Template->new( $RT::SystemUser );
+ my ($id, $msg) = $template->Create(
+ Name => 'my template',
+ Content => "\nsome content",
+ );
+ ok($id, 'created template') or diag "error: $msg";
+
+ my $shredder = shredder_new();
+ $shredder->PutObjects( Objects => $template );
+ $shredder->WipeoutAll;
+ cmp_deeply( dump_current_and_savepoint('clean'), "current DB equal to savepoint");
+}
+
+diag 'local template' if $ENV{'TEST_VERBOSE'};
+{
+ create_savepoint('clean');
+ my $template = RT::Template->new( $RT::SystemUser );
+ my ($id, $msg) = $template->Create(
+ Name => 'my template',
+ Queue => 'General',
+ Content => "\nsome content",
+ );
+ ok($id, 'created template') or diag "error: $msg";
+
+ my $shredder = shredder_new();
+ $shredder->PutObjects( Objects => $template );
+ $shredder->WipeoutAll;
+ cmp_deeply( dump_current_and_savepoint('clean'), "current DB equal to savepoint");
+}
+
+diag 'template used in scrip' if $ENV{'TEST_VERBOSE'};
+{
+ create_savepoint('clean');
+ my $template = RT::Template->new( $RT::SystemUser );
+ my ($id, $msg) = $template->Create(
+ Name => 'my template',
+ Queue => 'General',
+ Content => "\nsome content",
+ );
+ ok($id, 'created template') or diag "error: $msg";
+
+ my $scrip = RT::Scrip->new( $RT::SystemUser );
+ ($id, $msg) = $scrip->Create(
+ Description => 'my scrip',
+ Queue => 'General',
+ ScripCondition => 'On Create',
+ ScripAction => 'Open Tickets',
+ Template => $template->id,
+ );
+ ok($id, 'created scrip') or diag "error: $msg";
+
+ my $shredder = shredder_new();
+ $shredder->PutObjects( Objects => $template );
+ $shredder->WipeoutAll;
+ cmp_deeply( dump_current_and_savepoint('clean'), "current DB equal to savepoint");
+}
diff --git a/rt/t/shredder/02user.t b/rt/t/shredder/02user.t
new file mode 100644
index 000000000..03abd6c69
--- /dev/null
+++ b/rt/t/shredder/02user.t
@@ -0,0 +1,62 @@
+#!/usr/bin/perl -w
+
+use strict;
+use warnings;
+
+use Test::Deep;
+use File::Spec;
+use Test::More tests => 8;
+use RT::Test ();
+BEGIN {
+ my $shredder_utils = RT::Test::get_relocatable_file('utils.pl',
+ File::Spec->curdir());
+ require $shredder_utils;
+}
+init_db();
+
+
+create_savepoint('clean');
+
+my $queue = RT::Queue->new( $RT::SystemUser );
+my ($qid) = $queue->Load( 'General' );
+ok( $qid, "loaded queue" );
+
+my $ticket = RT::Ticket->new( $RT::SystemUser );
+my ($tid) = $ticket->Create( Queue => $qid, Subject => 'test' );
+ok( $tid, "ticket created" );
+
+create_savepoint('bucreate'); # berfore user create
+my $user = RT::User->new( $RT::SystemUser );
+my ($uid, $msg) = $user->Create( Name => 'new user', Privileged => 1, Disabled => 0 );
+ok( $uid, "created new user" ) or diag "error: $msg";
+is( $user->id, $uid, "id is correct" );
+# HACK: set ticket props to enable VARIABLE dependencies
+$ticket->__Set( Field => 'LastUpdatedBy', Value => $uid );
+create_savepoint('aucreate'); # after user create
+
+{
+ my $resolver = sub {
+ my %args = (@_);
+ my $t = $args{'TargetObject'};
+ my $resolver_uid = $RT::SystemUser->id;
+ foreach my $method ( qw(Creator LastUpdatedBy) ) {
+ next unless $t->_Accessible( $method => 'read' );
+ $t->__Set( Field => $method, Value => $resolver_uid );
+ }
+ };
+ my $shredder = shredder_new();
+ $shredder->PutResolver( BaseClass => 'RT::User', Code => $resolver );
+ $shredder->Wipeout( Object => $user );
+ cmp_deeply( dump_current_and_savepoint('bucreate'), "current DB equal to savepoint");
+}
+
+{
+ restore_savepoint('aucreate');
+ my $user = RT::User->new( $RT::SystemUser );
+ $user->Load($uid);
+ ok($user->id, "loaded user after restore");
+ my $shredder = shredder_new();
+ eval { $shredder->Wipeout( Object => $user ) };
+ ok($@, "wipeout throw exception if no resolvers");
+ cmp_deeply( dump_current_and_savepoint('aucreate'), "current DB equal to savepoint");
+}
diff --git a/rt/t/shredder/03plugin.t b/rt/t/shredder/03plugin.t
new file mode 100644
index 000000000..190f40acf
--- /dev/null
+++ b/rt/t/shredder/03plugin.t
@@ -0,0 +1,46 @@
+#!/usr/bin/perl -w
+
+use strict;
+use warnings;
+
+use Test::Deep;
+use File::Spec;
+use Test::More tests => 28;
+use RT::Test ();
+BEGIN {
+ my $shredder_utils = RT::Test::get_relocatable_file('utils.pl',
+ File::Spec->curdir());
+ require $shredder_utils;
+}
+
+my @PLUGINS = sort qw(Attachments Base Objects SQLDump Summary Tickets Users);
+
+use_ok('RT::Shredder::Plugin');
+{
+ my $plugin = new RT::Shredder::Plugin;
+ isa_ok($plugin, 'RT::Shredder::Plugin');
+ my %plugins = $plugin->List;
+ cmp_deeply( [sort keys %plugins], [@PLUGINS], "correct plugins" );
+}
+{ # test ->List as class method
+ my %plugins = RT::Shredder::Plugin->List;
+ cmp_deeply( [sort keys %plugins], [@PLUGINS], "correct plugins" );
+}
+{ # reblessing on LoadByName
+ foreach (@PLUGINS) {
+ my $plugin = new RT::Shredder::Plugin;
+ isa_ok($plugin, 'RT::Shredder::Plugin');
+ my ($status, $msg) = $plugin->LoadByName( $_ );
+ ok($status, "loaded plugin by name") or diag("error: $msg");
+ isa_ok($plugin, "RT::Shredder::Plugin::$_" );
+ }
+}
+{ # error checking in LoadByName
+ my $plugin = new RT::Shredder::Plugin;
+ isa_ok($plugin, 'RT::Shredder::Plugin');
+ my ($status, $msg) = $plugin->LoadByName;
+ ok(!$status, "not loaded plugin - empty name");
+ ($status, $msg) = $plugin->LoadByName('Foo');
+ ok(!$status, "not loaded plugin - not exist");
+}
+
diff --git a/rt/t/shredder/03plugin_summary.t b/rt/t/shredder/03plugin_summary.t
new file mode 100644
index 000000000..30606af41
--- /dev/null
+++ b/rt/t/shredder/03plugin_summary.t
@@ -0,0 +1,23 @@
+#!/usr/bin/perl -w
+
+use strict;
+use warnings;
+
+use Test::Deep;
+use File::Spec;
+use Test::More tests => 4;
+use RT::Test ();
+BEGIN {
+ my $shredder_utils = RT::Test::get_relocatable_file('utils.pl',
+ File::Spec->curdir());
+ require $shredder_utils;
+}
+
+
+use_ok('RT::Shredder::Plugin');
+my $plugin_obj = new RT::Shredder::Plugin;
+isa_ok($plugin_obj, 'RT::Shredder::Plugin');
+my ($status, $msg) = $plugin_obj->LoadByName('Summary');
+ok($status, 'loaded summary plugin') or diag "error: $msg";
+isa_ok($plugin_obj, 'RT::Shredder::Plugin::Summary');
+
diff --git a/rt/t/shredder/03plugin_tickets.t b/rt/t/shredder/03plugin_tickets.t
new file mode 100644
index 000000000..3d742ff83
--- /dev/null
+++ b/rt/t/shredder/03plugin_tickets.t
@@ -0,0 +1,150 @@
+#!/usr/bin/perl -w
+
+use strict;
+use warnings;
+
+use Test::Deep;
+use File::Spec;
+use Test::More tests => 44;
+use RT::Test ();
+BEGIN {
+ my $shredder_utils = RT::Test::get_relocatable_file('utils.pl',
+ File::Spec->curdir());
+ require $shredder_utils;
+}
+
+
+use_ok('RT::Shredder::Plugin::Tickets');
+{
+ my $plugin = new RT::Shredder::Plugin::Tickets;
+ isa_ok($plugin, 'RT::Shredder::Plugin::Tickets');
+
+ is(lc $plugin->Type, 'search', 'correct type');
+}
+
+init_db();
+create_savepoint('clean');
+use_ok('RT::Ticket');
+use_ok('RT::Tickets');
+
+{ # create parent and child and check functionality of 'with_linked' arg
+ my $parent = RT::Ticket->new( $RT::SystemUser );
+ my ($pid) = $parent->Create( Subject => 'parent', Queue => 1 );
+ ok( $pid, "created new ticket" );
+
+ my $child = RT::Ticket->new( $RT::SystemUser );
+ my ($cid) = $child->Create( Subject => 'child', Queue => 1, MemberOf => $pid );
+ ok( $cid, "created new ticket" );
+
+ my $plugin = new RT::Shredder::Plugin::Tickets;
+ isa_ok($plugin, 'RT::Shredder::Plugin::Tickets');
+
+ my ($status, $msg, @objs);
+ ($status, $msg) = $plugin->TestArgs( query => 'Subject = "parent"' );
+ ok($status, "plugin arguments are ok") or diag "error: $msg";
+
+ ($status, @objs) = $plugin->Run;
+ ok($status, "executed plugin successfully") or diag "error: @objs";
+ @objs = RT::Shredder->CastObjectsToRecords( Objects => \@objs );
+ is(scalar @objs, 1, "only one object in result set");
+ is($objs[0]->id, $pid, "parent is in result set");
+
+ ($status, $msg) = $plugin->TestArgs( query => 'Subject = "parent"', with_linked => 1 );
+ ok($status, "plugin arguments are ok") or diag "error: $msg";
+
+ ($status, @objs) = $plugin->Run;
+ ok($status, "executed plugin successfully") or diag "error: @objs";
+ @objs = RT::Shredder->CastObjectsToRecords( Objects => \@objs );
+ my %has = map { $_->id => 1 } @objs;
+ is(scalar @objs, 2, "two objects in the result set");
+ ok($has{$pid}, "parent is in the result set");
+ ok($has{$cid}, "child is in the result set");
+
+ my $shredder = shredder_new();
+ $shredder->PutObjects( Objects => \@objs );
+ $shredder->WipeoutAll;
+}
+cmp_deeply( dump_current_and_savepoint('clean'), "current DB equal to savepoint");
+
+{ # create parent and child and link them reqursively to check that we don't hang
+ my $parent = RT::Ticket->new( $RT::SystemUser );
+ my ($pid) = $parent->Create( Subject => 'parent', Queue => 1 );
+ ok( $pid, "created new ticket" );
+
+ my $child = RT::Ticket->new( $RT::SystemUser );
+ my ($cid) = $child->Create( Subject => 'child', Queue => 1, MemberOf => $pid );
+ ok( $cid, "created new ticket" );
+
+ my ($status, $msg) = $child->AddLink( Target => $pid, Type => 'DependsOn' );
+ ok($status, "added reqursive link") or diag "error: $msg";
+
+ my $plugin = new RT::Shredder::Plugin::Tickets;
+ isa_ok($plugin, 'RT::Shredder::Plugin::Tickets');
+
+ my (@objs);
+ ($status, $msg) = $plugin->TestArgs( query => 'Subject = "parent"' );
+ ok($status, "plugin arguments are ok") or diag "error: $msg";
+
+ ($status, @objs) = $plugin->Run;
+ ok($status, "executed plugin successfully") or diag "error: @objs";
+ @objs = RT::Shredder->CastObjectsToRecords( Objects => \@objs );
+ is(scalar @objs, 1, "only one object in result set");
+ is($objs[0]->id, $pid, "parent is in result set");
+
+ ($status, $msg) = $plugin->TestArgs( query => 'Subject = "parent"', with_linked => 1 );
+ ok($status, "plugin arguments are ok") or diag "error: $msg";
+
+ ($status, @objs) = $plugin->Run;
+ ok($status, "executed plugin successfully") or diag "error: @objs";
+ @objs = RT::Shredder->CastObjectsToRecords( Objects => \@objs );
+ is(scalar @objs, 2, "two objects in the result set");
+ my %has = map { $_->id => 1 } @objs;
+ ok($has{$pid}, "parent is in the result set");
+ ok($has{$cid}, "child is in the result set");
+
+ my $shredder = shredder_new();
+ $shredder->PutObjects( Objects => \@objs );
+ $shredder->WipeoutAll;
+}
+cmp_deeply( dump_current_and_savepoint('clean'), "current DB equal to savepoint");
+
+{ # create parent and child and check functionality of 'apply_query_to_linked' arg
+ my $parent = RT::Ticket->new( $RT::SystemUser );
+ my ($pid) = $parent->Create( Subject => 'parent', Queue => 1, Status => 'resolved' );
+ ok( $pid, "created new ticket" );
+
+ my $child1 = RT::Ticket->new( $RT::SystemUser );
+ my ($cid1) = $child1->Create( Subject => 'child', Queue => 1, MemberOf => $pid );
+ ok( $cid1, "created new ticket" );
+ my $child2 = RT::Ticket->new( $RT::SystemUser );
+ my ($cid2) = $child2->Create( Subject => 'child', Queue => 1, MemberOf => $pid, Status => 'resolved' );
+ ok( $cid2, "created new ticket" );
+
+ my $plugin = new RT::Shredder::Plugin::Tickets;
+ isa_ok($plugin, 'RT::Shredder::Plugin::Tickets');
+
+ my ($status, $msg) = $plugin->TestArgs( query => 'Status = "resolved"', apply_query_to_linked => 1 );
+ ok($status, "plugin arguments are ok") or diag "error: $msg";
+
+ my @objs;
+ ($status, @objs) = $plugin->Run;
+ ok($status, "executed plugin successfully") or diag "error: @objs";
+ @objs = RT::Shredder->CastObjectsToRecords( Objects => \@objs );
+ is(scalar @objs, 2, "two objects in the result set");
+ my %has = map { $_->id => 1 } @objs;
+ ok($has{$pid}, "parent is in the result set");
+ ok(!$has{$cid1}, "first child is in the result set");
+ ok($has{$cid2}, "second child is in the result set");
+
+ my $shredder = shredder_new();
+ $shredder->PutObjects( Objects => \@objs );
+ $shredder->WipeoutAll;
+
+ my $ticket = RT::Ticket->new( $RT::SystemUser );
+ $ticket->Load( $cid1 );
+ is($ticket->id, $cid1, 'loaded ticket');
+
+ $shredder->PutObjects( Objects => $ticket );
+ $shredder->WipeoutAll;
+}
+cmp_deeply( dump_current_and_savepoint('clean'), "current DB equal to savepoint");
diff --git a/rt/t/shredder/03plugin_users.t b/rt/t/shredder/03plugin_users.t
new file mode 100644
index 000000000..45fc8a27e
--- /dev/null
+++ b/rt/t/shredder/03plugin_users.t
@@ -0,0 +1,40 @@
+#!/usr/bin/perl -w
+
+use strict;
+use warnings;
+
+use Test::Deep;
+use File::Spec;
+use Test::More tests => 9;
+use RT::Test ();
+BEGIN {
+ my $shredder_utils = RT::Test::get_relocatable_file('utils.pl',
+ File::Spec->curdir());
+ require $shredder_utils;
+}
+
+
+my @ARGS = sort qw(limit status name member_of email replace_relations no_tickets);
+
+use_ok('RT::Shredder::Plugin::Users');
+{
+ my $plugin = new RT::Shredder::Plugin::Users;
+ isa_ok($plugin, 'RT::Shredder::Plugin::Users');
+
+ is(lc $plugin->Type, 'search', 'correct type');
+
+ my @args = sort $plugin->SupportArgs;
+ cmp_deeply(\@args, \@ARGS, "support all args");
+
+
+ my ($status, $msg) = $plugin->TestArgs( name => 'r??t*' );
+ ok($status, "arg name = 'r??t*'") or diag("error: $msg");
+
+ for (qw(any disabled enabled)) {
+ my ($status, $msg) = $plugin->TestArgs( status => $_ );
+ ok($status, "arg status = '$_'") or diag("error: $msg");
+ }
+ ($status, $msg) = $plugin->TestArgs( status => '!@#' );
+ ok(!$status, "bad 'status' arg value");
+}
+
diff --git a/rt/t/shredder/utils.pl b/rt/t/shredder/utils.pl
new file mode 100644
index 000000000..54243318e
--- /dev/null
+++ b/rt/t/shredder/utils.pl
@@ -0,0 +1,435 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use File::Spec;
+use File::Temp 0.19 ();
+require File::Path;
+require File::Copy;
+require Cwd;
+
+BEGIN {
+### after: push @INC, qw(@RT_LIB_PATH@);
+ push @INC, qw(/opt/rt3/local/lib /opt/rt3/lib);
+}
+use RT::Shredder;
+
+# where to keep temporary generated test data
+my $tmpdir = '';
+
+=head1 DESCRIPTION
+
+RT::Shredder test suite utilities
+
+=head1 TESTING
+
+Since RT:Shredder 0.01_03 we have a test suite. You
+can run tests and see if everything works as expected
+before you try shredder on your actual data.
+Tests also help in the development process.
+
+The test suite uses SQLite databases to store data in individual files,
+so you could sun tests on your production servers without risking
+damage to your production data.
+
+You'll want to run the test suite almost every time you install or update
+the shredder distribution, especialy if you have local customizations of
+the DB schema and/or RT code.
+
+Tests are one thing you can write even if you don't know much perl,
+but want to learn more about RT's internals. New tests are very welcome.
+
+=head2 WRITING TESTS
+
+The shredder distribution has several files to help write new tests.
+
+ t/shredder/utils.pl - this file, utilities
+ t/00skeleton.t - skeleteton .t file for new tests
+
+All tests follow this algorithm:
+
+ require "t/shredder/utils.pl"; # plug in utilities
+ init_db(); # create new tmp RT DB and init RT API
+ # create RT data you want to be always in the RT DB
+ # ...
+ create_savepoint('mysp'); # create DB savepoint
+ # create data you want delete with shredder
+ # ...
+ # run shredder on the objects you've created
+ # ...
+ # check that shredder deletes things you want
+ # this command will compare savepoint DB with current
+ cmp_deeply( dump_current_and_savepoint('mysp'), "current DB equal to savepoint");
+ # then you can create another object and delete it, then check again
+
+Savepoints are named and you can create two or more savepoints.
+
+=head1 FUNCTIONS
+
+=head2 RT CONFIG
+
+=head3 rewrite_rtconfig
+
+Call this sub after C<RT::LoadConfig>. It changes the RT config
+options necessary to switch to a local SQLite database.
+
+=cut
+
+sub rewrite_rtconfig
+{
+ # database
+ config_set( '$DatabaseType' , 'SQLite' );
+ config_set( '$DatabaseHost' , 'localhost' );
+ config_set( '$DatabaseRTHost' , 'localhost' );
+ config_set( '$DatabasePort' , '' );
+ config_set( '$DatabaseUser' , 'rt_user' );
+ config_set( '$DatabasePassword' , 'rt_pass' );
+ config_set( '$DatabaseRequireSSL' , undef );
+ # database file name
+ config_set( '$DatabaseName' , db_name() );
+
+ # generic logging
+ config_set( '$LogToSyslog' , undef );
+ config_set( '$LogToScreen' , 'error' );
+ config_set( '$LogStackTraces' , 'crit' );
+ # logging to standalone file
+ config_set( '$LogToFile' , 'debug' );
+ my $fname = File::Spec->catfile(create_tmpdir(), test_name() .".log");
+ config_set( '$LogToFileNamed' , $fname );
+}
+
+=head3 config_set
+
+This sub is a helper used by C<rewrite_rtconfig>. You shouldn't
+need to use it elsewhere unless you need to change other RT
+configuration variables.
+
+=cut
+
+sub config_set {
+ my $opt = shift;
+ $opt =~ s/^[\$\%\@]//;
+ RT->Config->Set($opt, @_)
+}
+
+=head2 DATABASES
+
+=head3 init_db
+
+Creates a new RT DB with initial data in a new test tmp dir.
+Also runs RT::Init() and RT::InitLogging().
+
+This is all you need to call to setup a testing environment
+in most situations.
+
+=cut
+
+sub init_db
+{
+ create_tmpdir();
+ RT::LoadConfig();
+ rewrite_rtconfig();
+ RT::InitLogging();
+
+ _init_db();
+
+ RT::Init();
+ $SIG{__WARN__} = sub { $RT::Logger->warning( @_ ); warn @_ };
+ $SIG{__DIE__} = sub { $RT::Logger->crit( @_ ) unless $^S; die @_ };
+}
+
+use IPC::Open2;
+sub _init_db
+{
+
+
+ foreach ( qw(Type Host Port Name User Password) ) {
+ $ENV{ "RT_DB_". uc $_ } = RT->Config->Get("Database$_");
+ }
+ my $rt_setup_database = RT::Test::get_relocatable_file(
+ 'rt-setup-database', (File::Spec->updir(), File::Spec->updir(), 'sbin'));
+ my $cmd = "$^X $rt_setup_database --action init 2>&1";
+
+ my ($child_out, $child_in);
+ my $pid = open2($child_out, $child_in, $cmd);
+ close $child_in;
+ my $result = do { local $/; <$child_out> };
+ return $result;
+}
+
+=head3 db_name
+
+Returns the absolute file path to the current DB.
+It is <$tmpdir . test_name() .'.db'>.
+
+See also the C<test_name> function.
+
+=cut
+
+sub db_name { return File::Spec->catfile(create_tmpdir(), test_name() .".db") }
+
+=head3 connect_sqlite
+
+Returns connected DBI DB handle.
+
+Takes path to sqlite db.
+
+=cut
+
+sub connect_sqlite
+{
+ return DBI->connect("dbi:SQLite:dbname=". shift, "", "");
+}
+
+=head2 SHREDDER
+
+=head3 shredder_new
+
+Creates and returns a new RT::Shredder object.
+
+=cut
+
+sub shredder_new
+{
+ my $obj = new RT::Shredder;
+
+ my $file = File::Spec->catfile( create_tmpdir(), test_name() .'.XXXX.sql' );
+ $obj->AddDumpPlugin( Arguments => {
+ file_name => $file,
+ from_storage => 0,
+ } );
+
+ return $obj;
+}
+
+
+=head2 TEST FILES
+
+=head3 test_name
+
+Returns name of the test file running now with file extension and
+directory names stripped.
+
+For example, it returns '00load' for the test file 't/00load.t'.
+
+=cut
+
+sub test_name
+{
+ my $name = $0;
+ $name =~ s/^.*[\\\/]//;
+ $name =~ s/\..*$//;
+ return $name;
+}
+
+=head2 TEMPORARY DIRECTORY
+
+=head3 tmpdir
+
+Returns the absolute path to a tmp dir used in tests.
+
+=cut
+
+sub tmpdir {
+ if (-d $tmpdir) {
+ return $tmpdir;
+ } else {
+ $tmpdir = File::Temp->newdir(TEMPLATE => 'shredderXXXXX', CLEANUP => 0);
+ return $tmpdir;
+ }
+}
+
+=head2 create_tmpdir
+
+Creates a tmp dir if one doesn't exist already. Returns tmpdir path.
+
+=cut
+
+sub create_tmpdir { my $n = tmpdir(); File::Path::mkpath( [$n] ); return $n }
+
+=head3 cleanup_tmp
+
+Deletes all the tmp dir used in the tests.
+See also the C<test_name> function.
+
+=cut
+
+sub cleanup_tmp
+{
+ my $dir = File::Spec->catdir(tmpdir(), test_name());
+ return File::Path::rmtree( File::Spec->catdir( tmpdir(), test_name() ));
+}
+
+=head2 SAVEPOINTS
+
+=head3 savepoint_name
+
+Returns the absolute path to the named savepoint DB file.
+Takes one argument - savepoint name, by default C<sp>.
+
+=cut
+
+sub savepoint_name
+{
+ my $name = shift || 'sp';
+ return File::Spec->catfile( create_tmpdir(), test_name() .".$name.db" );
+}
+
+=head3 create_savepoint
+
+Creates savepoint DB from the current DB.
+Takes name of the savepoint as argument.
+
+=head3 restore_savepoint
+
+Restores current DB to savepoint state.
+Takes name of the savepoint as argument.
+
+=cut
+
+sub create_savepoint { return __cp_db( db_name() => savepoint_name( shift ) ) }
+sub restore_savepoint { return __cp_db( savepoint_name( shift ) => db_name() ) }
+sub __cp_db
+{
+ my( $orig, $dest ) = @_;
+ $RT::Handle->dbh->disconnect;
+ # DIRTY HACK: undef Handles to force reconnect
+ $RT::Handle = undef;
+ %DBIx::SearchBuilder::DBIHandle = ();
+ $DBIx::SearchBuilder::PrevHandle = undef;
+
+ File::Copy::copy( $orig, $dest ) or die "Couldn't copy '$orig' => '$dest': $!";
+ RT::ConnectToDatabase();
+ return;
+}
+
+
+=head2 DUMPS
+
+=head3 dump_sqlite
+
+Returns DB dump as a complex hash structure:
+ {
+ TableName => {
+ #id => {
+ lc_field => 'value',
+ }
+ }
+ }
+
+Takes named argument C<CleanDates>. If true, clean all date fields from
+dump. True by default.
+
+=cut
+
+sub dump_sqlite
+{
+ my $dbh = shift;
+ my %args = ( CleanDates => 1, @_ );
+
+ my $old_fhkn = $dbh->{'FetchHashKeyName'};
+ $dbh->{'FetchHashKeyName'} = 'NAME_lc';
+
+ my $sth = $dbh->table_info( '', '', '%', 'TABLE' ) || die $DBI::err;
+ my @tables = keys %{$sth->fetchall_hashref( 'table_name' )};
+
+ my $res = {};
+ foreach my $t( @tables ) {
+ next if lc($t) eq 'sessions';
+ $res->{$t} = $dbh->selectall_hashref("SELECT * FROM $t", 'id');
+ clean_dates( $res->{$t} ) if $args{'CleanDates'};
+ die $DBI::err if $DBI::err;
+ }
+
+ $dbh->{'FetchHashKeyName'} = $old_fhkn;
+ return $res;
+}
+
+=head3 dump_current_and_savepoint
+
+Returns dump of the current DB and of the named savepoint.
+Takes one argument - savepoint name.
+
+=cut
+
+sub dump_current_and_savepoint
+{
+ my $orig = savepoint_name( shift );
+ die "Couldn't find savepoint file" unless -f $orig && -r _;
+ my $odbh = connect_sqlite( $orig );
+ return ( dump_sqlite( $RT::Handle->dbh, @_ ), dump_sqlite( $odbh, @_ ) );
+}
+
+=head3 dump_savepoint_and_current
+
+Returns the same data as C<dump_current_and_savepoint> function,
+but in reversed order.
+
+=cut
+
+sub dump_savepoint_and_current { return reverse dump_current_and_savepoint(@_) }
+
+sub clean_dates
+{
+ my $h = shift;
+ my $date_re = qr/^\d\d\d\d\-\d\d\-\d\d\s*\d\d\:\d\d(\:\d\d)?$/i;
+ foreach my $id ( keys %{ $h } ) {
+ next unless $h->{ $id };
+ foreach ( keys %{ $h->{ $id } } ) {
+ delete $h->{$id}{$_} if $h->{$id}{$_} &&
+ $h->{$id}{$_} =~ /$date_re/;
+ }
+ }
+}
+
+=head2 NOTES
+
+Function that returns debug notes.
+
+=head3 note_on_fail
+
+Returns a note about debug info that you can display if tests fail.
+
+=cut
+
+sub note_on_fail
+{
+ my $name = test_name();
+ my $tmpdir = tmpdir();
+ return <<END;
+Some tests in '$0' file failed.
+You can find debug info in '$tmpdir' dir.
+There should be:
+ $name.log - RT debug log file
+ $name.db - latest RT DB used while testing
+ $name.*.db - savepoint databases
+See also perldoc t/shredder/utils.pl for how to use this info.
+END
+}
+
+=head2 OTHER
+
+=head3 all_were_successful
+
+Returns true if all tests that have already run were successful.
+
+=cut
+
+sub all_were_successful
+{
+ use Test::Builder;
+ my $Test = Test::Builder->new;
+ return grep( !$_, $Test->summary )? 0: 1;
+}
+
+END {
+ return unless -e tmpdir();
+ if ( all_were_successful() ) {
+ cleanup_tmp();
+ } else {
+ diag( note_on_fail() );
+ }
+}
+
+1;
diff --git a/rt/t/ticket/action_linear_escalate.t b/rt/t/ticket/action_linear_escalate.t
new file mode 100644
index 000000000..38cd47ded
--- /dev/null
+++ b/rt/t/ticket/action_linear_escalate.t
@@ -0,0 +1,100 @@
+#!/usr/bin/perl -w
+
+use strict;
+use warnings;
+
+use RT;
+use RT::Test tests => 17;
+
+my ($id, $msg);
+my $RecordTransaction;
+my $UpdateLastUpdated;
+
+
+use_ok('RT::Action::LinearEscalate');
+
+my $q = RT::Test->load_or_create_queue( Name => 'Regression' );
+ok $q && $q->id, 'loaded or created queue';
+
+# rt-cron-tool uses Gecos name to get rt user, so we'd better create one
+my $gecos = RT::Test->load_or_create_user(
+ Name => 'gecos',
+ Password => 'password',
+ Gecos => ($^O eq 'MSWin32') ? Win32::LoginName() : (getpwuid($<))[0],
+);
+ok $gecos && $gecos->id, 'loaded or created gecos user';
+
+# get rid of all right permissions
+$gecos->PrincipalObj->GrantRight( Right => 'SuperUser' );
+
+
+my $user = RT::Test->load_or_create_user(
+ Name => 'user', Password => 'password',
+);
+ok $user && $user->id, 'loaded or created user';
+
+$user->PrincipalObj->GrantRight( Right => 'SuperUser' );
+my $current_user = RT::CurrentUser->new($RT::SystemUser);
+($id, $msg) = $current_user->Load($user->id);
+ok( $id, "Got current user? $msg" );
+
+#defaults
+$RecordTransaction = 0;
+$UpdateLastUpdated = 1;
+my $ticket2 = create_ticket_as_ok($current_user);
+escalate_ticket_ok($ticket2);
+ok( $ticket2->LastUpdatedBy != $user->id, "Set LastUpdated" );
+ok( $ticket2->Transactions->Last->Type =~ /Create/i, "Did not record a transaction" );
+
+$RecordTransaction = 1;
+$UpdateLastUpdated = 1;
+my $ticket1 = create_ticket_as_ok($current_user);
+escalate_ticket_ok($ticket1);
+ok( $ticket1->LastUpdatedBy != $user->id, "Set LastUpdated" );
+ok( $ticket1->Transactions->Last->Type !~ /Create/i, "Recorded a transaction" );
+
+$RecordTransaction = 0;
+$UpdateLastUpdated = 0;
+my $ticket3 = create_ticket_as_ok($current_user);
+escalate_ticket_ok($ticket3);
+ok( $ticket3->LastUpdatedBy == $user->id, "Did not set LastUpdated" );
+ok( $ticket3->Transactions->Last->Type =~ /Create/i, "Did not record a transaction" );
+
+1;
+
+
+sub create_ticket_as_ok {
+ my $user = shift;
+
+ my $created = RT::Date->new($RT::SystemUser);
+ $created->Unix(time() - ( 7 * 24 * 60**2 ));
+ my $due = RT::Date->new($RT::SystemUser);
+ $due->Unix(time() + ( 7 * 24 * 60**2 ));
+
+ my $ticket = RT::Ticket->new($user);
+ ($id, $msg) = $ticket->Create( Queue => $q->id,
+ Subject => "Escalation test",
+ Priority => 0,
+ InitialPriority => 0,
+ FinalPriority => 50,
+ );
+ ok($id, "Created ticket? ".$id);
+ $ticket->__Set( Field => 'Created',
+ Value => $created->ISO,
+ );
+ $ticket->__Set( Field => 'Due',
+ Value => $due->ISO,
+ );
+
+ return $ticket;
+}
+
+sub escalate_ticket_ok {
+ my $ticket = shift;
+ my $id = $ticket->id;
+ print "$RT::BinPath/rt-crontool --search RT::Search::FromSQL --search-arg \"id = @{[$id]}\" --action RT::Action::LinearEscalate --action-arg \"RecordTransaction:$RecordTransaction; UpdateLastUpdated:$UpdateLastUpdated\"\n";
+ print STDERR `$RT::BinPath/rt-crontool --search RT::Search::FromSQL --search-arg "id = @{[$id]}" --action RT::Action::LinearEscalate --action-arg "RecordTransaction:$RecordTransaction; UpdateLastUpdated:$UpdateLastUpdated"`;
+
+ $ticket->Load($id); # reload, because otherwise we get the cached value
+ ok( $ticket->Priority != 0, "Escalated ticket" );
+}
diff --git a/rt/t/ticket/add-watchers.t b/rt/t/ticket/add-watchers.t
new file mode 100644
index 000000000..ae993a936
--- /dev/null
+++ b/rt/t/ticket/add-watchers.t
@@ -0,0 +1,167 @@
+#!/usr/bin/perl -w
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2005 Best Practical Solutions, LLC
+# <jesse.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+
+use RT::Test tests => 32;
+
+use strict;
+use warnings;
+no warnings 'once';
+
+use RT::Queue;
+use RT::User;
+use RT::Group;
+use RT::Ticket;
+use RT::CurrentUser;
+
+
+# clear all global right
+my $acl = RT::ACL->new($RT::SystemUser);
+$acl->Limit( FIELD => 'RightName', OPERATOR => '!=', VALUE => 'SuperUser' );
+$acl->LimitToObject( $RT::System );
+while( my $ace = $acl->Next ) {
+ $ace->Delete;
+}
+
+# create new queue to be sure we do not mess with rights
+my $queue = RT::Queue->new($RT::SystemUser);
+my ($queue_id) = $queue->Create( Name => 'watcher tests '.$$);
+ok( $queue_id, 'queue created for watcher tests' );
+
+# new privileged user to check rights
+my $user = RT::User->new( $RT::SystemUser );
+my ($user_id) = $user->Create( Name => 'watcher'.$$,
+ EmailAddress => "watcher$$".'@localhost',
+ Privileged => 1,
+ Password => 'qwe123',
+ );
+my $cu= RT::CurrentUser->new($user);
+
+# make sure user can see tickets in the queue
+my $principal = $user->PrincipalObj;
+ok( $principal, "principal loaded" );
+$principal->GrantRight( Right => 'ShowTicket', Object => $queue );
+$principal->GrantRight( Right => 'SeeQueue' , Object => $queue );
+
+ok( $user->HasRight( Right => 'SeeQueue', Object => $queue ), "user can see queue" );
+ok( $user->HasRight( Right => 'ShowTicket', Object => $queue ), "user can show queue tickets" );
+ok( !$user->HasRight( Right => 'ModifyTicket', Object => $queue ), "user can't modify queue tickets" );
+ok( !$user->HasRight( Right => 'Watch', Object => $queue ), "user can't watch queue tickets" );
+
+my $ticket = RT::Ticket->new( $RT::SystemUser );
+my ($rv, $msg) = $ticket->Create( Subject => 'watcher tests', Queue => $queue->Name );
+ok( $ticket->id, "ticket created" );
+
+my $ticket2 = RT::Ticket->new( $cu );
+$ticket2->Load( $ticket->id );
+ok( $ticket2->Subject, "ticket load by user" );
+
+# user can add self to ticket only after getting Watch right
+($rv, $msg) = $ticket2->AddWatcher( Type => 'Cc', PrincipalId => $user->PrincipalId );
+ok( !$rv, "user can't add self as Cc" );
+($rv, $msg) = $ticket2->AddWatcher( Type => 'Requestor', PrincipalId => $user->PrincipalId );
+ok( !$rv, "user can't add self as Requestor" );
+$principal->GrantRight( Right => 'Watch' , Object => $queue );
+ok( $user->HasRight( Right => 'Watch', Object => $queue ), "user can watch queue tickets" );
+($rv, $msg) = $ticket2->AddWatcher( Type => 'Cc', PrincipalId => $user->PrincipalId );
+ok( $rv, "user can add self as Cc by PrincipalId" );
+($rv, $msg) = $ticket2->AddWatcher( Type => 'Requestor', PrincipalId => $user->PrincipalId );
+ok( $rv, "user can add self as Requestor by PrincipalId" );
+
+# remove user and try adding with Email address
+($rv, $msg) = $ticket->DeleteWatcher( Type => 'Cc', PrincipalId => $user->PrincipalId );
+ok( $rv, "watcher removed by PrincipalId" );
+($rv, $msg) = $ticket->DeleteWatcher( Type => 'Requestor', Email => $user->EmailAddress );
+ok( $rv, "watcher removed by Email" );
+
+($rv, $msg) = $ticket2->AddWatcher( Type => 'Cc', Email => $user->EmailAddress );
+ok( $rv, "user can add self as Cc by Email" );
+($rv, $msg) = $ticket2->AddWatcher( Type => 'Requestor', Email => $user->EmailAddress );
+ok( $rv, "user can add self as Requestor by Email" );
+
+# remove user and try adding by username
+# This worked in 3.6 and is a regression in 3.8
+($rv, $msg) = $ticket->DeleteWatcher( Type => 'Cc', Email => $user->EmailAddress );
+ok( $rv, "watcher removed by Email" );
+($rv, $msg) = $ticket->DeleteWatcher( Type => 'Requestor', Email => $user->EmailAddress );
+ok( $rv, "watcher removed by Email" );
+
+($rv, $msg) = $ticket2->AddWatcher( Type => 'Cc', Email => $user->Name );
+ok( $rv, "user can add self as Cc by username" );
+($rv, $msg) = $ticket2->AddWatcher( Type => 'Requestor', Email => $user->Name );
+ok( $rv, "user can add self as Requestor by username" );
+
+# Queue watcher tests
+$principal->RevokeRight( Right => 'Watch' , Object => $queue );
+ok( !$user->HasRight( Right => 'Watch', Object => $queue ), "user queue watch right revoked" );
+
+my $queue2 = RT::Queue->new( $cu );
+($rv, $msg) = $queue2->Load( $queue->id );
+ok( $rv, "user loaded queue" );
+
+# user can add self to queue only after getting Watch right
+($rv, $msg) = $queue2->AddWatcher( Type => 'Cc', PrincipalId => $user->PrincipalId );
+ok( !$rv, "user can't add self as Cc" );
+($rv, $msg) = $queue2->AddWatcher( Type => 'Requestor', PrincipalId => $user->PrincipalId );
+ok( !$rv, "user can't add self as Requestor" );
+$principal->GrantRight( Right => 'Watch' , Object => $queue );
+ok( $user->HasRight( Right => 'Watch', Object => $queue ), "user can watch queue queues" );
+($rv, $msg) = $queue2->AddWatcher( Type => 'Cc', PrincipalId => $user->PrincipalId );
+ok( $rv, "user can add self as Cc by PrincipalId" );
+($rv, $msg) = $queue2->AddWatcher( Type => 'Requestor', PrincipalId => $user->PrincipalId );
+ok( $rv, "user can add self as Requestor by PrincipalId" );
+
+# remove user and try adding with Email address
+($rv, $msg) = $queue->DeleteWatcher( Type => 'Cc', PrincipalId => $user->PrincipalId );
+ok( $rv, "watcher removed by PrincipalId" );
+($rv, $msg) = $queue->DeleteWatcher( Type => 'Requestor', Email => $user->EmailAddress );
+ok( $rv, "watcher removed by Email" );
+
+($rv, $msg) = $queue2->AddWatcher( Type => 'Cc', Email => $user->EmailAddress );
+ok( $rv, "user can add self as Cc by Email" );
+($rv, $msg) = $queue2->AddWatcher( Type => 'Requestor', Email => $user->EmailAddress );
+ok( $rv, "user can add self as Requestor by Email" );
+
diff --git a/rt/t/ticket/badlinks.t b/rt/t/ticket/badlinks.t
new file mode 100644
index 000000000..408e6b67c
--- /dev/null
+++ b/rt/t/ticket/badlinks.t
@@ -0,0 +1,38 @@
+#!/usr/bin/env perl
+use strict;
+use warnings;
+use RT::Test tests => 12;
+
+my ($baseurl, $m) = RT::Test->started_ok;
+ok($m->login, "Logged in");
+
+my $queue = RT::Test->load_or_create_queue(Name => 'General');
+ok($queue->Id, "loaded the General queue");
+
+my $ticket = RT::Ticket->new($RT::SystemUser);
+my ($tid, $txn, $msg) = $ticket->Create(
+ Queue => $queue->id,
+ Subject => 'test links',
+ );
+ok $tid, 'created a ticket #'. $tid or diag "error: $msg";
+
+$m->goto_ticket($tid);
+
+$m->follow_link_ok( { text => 'Links' }, "Followed link to Links" );
+
+ok $m->form_with_fields("$tid-DependsOn"), "found the form";
+my $not_a_ticket_url = "http://example.com/path/to/nowhere";
+$m->field("$tid-DependsOn", $not_a_ticket_url);
+$m->field("DependsOn-$tid", $not_a_ticket_url);
+$m->field("$tid-MemberOf", $not_a_ticket_url);
+$m->field("MemberOf-$tid", $not_a_ticket_url);
+$m->field("$tid-RefersTo", $not_a_ticket_url);
+$m->field("RefersTo-$tid", $not_a_ticket_url);
+$m->submit;
+
+foreach my $type ("depends on", "member of", "refers to") {
+ $m->content_like(qr/$type.+$not_a_ticket_url/,"base for $type");
+ $m->content_like(qr/$not_a_ticket_url.+$type/,"target for $type");
+}
+
+$m->goto_ticket($tid);
diff --git a/rt/t/ticket/batch-upload-csv.t b/rt/t/ticket/batch-upload-csv.t
new file mode 100644
index 000000000..41dc78696
--- /dev/null
+++ b/rt/t/ticket/batch-upload-csv.t
@@ -0,0 +1,48 @@
+#!/usr/bin/perl -w
+use strict; use warnings;
+
+use RT::Test tests => 12;
+use_ok('RT');
+
+use_ok('RT::Action::CreateTickets');
+
+my $QUEUE = 'uploadtest-'.$$;
+
+my $queue_obj = RT::Queue->new($RT::SystemUser);
+$queue_obj->Create(Name => $QUEUE);
+
+my $cf = RT::CustomField->new($RT::SystemUser);
+my ($val,$msg) = $cf->Create(Name => 'Work Package-'.$$, Type => 'Freeform', LookupType => RT::Ticket->CustomFieldLookupType, MaxValues => 1);
+ok($cf->id);
+ok($val,$msg);
+($val, $msg) = $cf->AddToObject($queue_obj);
+ok($val,$msg);
+ok($queue_obj->TicketCustomFields()->Count, "We have a custom field, at least");
+
+
+my $data = <<EOF;
+id,Queue,Subject,Status,Requestor,@{[$cf->Name]}
+create-1,$QUEUE,hi,new,root,2.0
+create-2,$QUEUE,hello,new,root,3.0
+EOF
+
+my $action = RT::Action::CreateTickets->new(CurrentUser => RT::CurrentUser->new('root'));
+ok ($action->CurrentUser->id , "WE have a current user");
+
+$action->Parse(Content => $data);
+my @results = $action->CreateByTemplate();
+
+my $tix = RT::Tickets->new($RT::SystemUser);
+$tix->FromSQL ("Queue = '". $QUEUE."'");
+$tix->OrderBy( FIELD => 'id', ORDER => 'ASC' );
+is($tix->Count, 2, '2 tickets');
+
+my $first = $tix->First();
+
+is($first->Subject(), 'hi');
+is($first->FirstCustomFieldValue($cf->id), '2.0');
+
+my $second = $tix->Next;
+is($second->Subject(), 'hello');
+is($second->FirstCustomFieldValue($cf->id), '3.0');
+1;
diff --git a/rt/t/ticket/cfsort-freeform-multiple.t b/rt/t/ticket/cfsort-freeform-multiple.t
new file mode 100644
index 000000000..f8f5950ef
--- /dev/null
+++ b/rt/t/ticket/cfsort-freeform-multiple.t
@@ -0,0 +1,137 @@
+#!/usr/bin/perl
+
+use RT::Test tests => 24;
+
+use strict;
+use warnings;
+
+use RT::Tickets;
+use RT::Queue;
+use RT::CustomField;
+
+# Test Sorting by custom fields.
+
+diag "Create a queue to test with." if $ENV{TEST_VERBOSE};
+my $queue_name = "CFSortQueue-$$";
+my $queue;
+{
+ $queue = RT::Queue->new( $RT::SystemUser );
+ my ($ret, $msg) = $queue->Create(
+ Name => $queue_name,
+ Description => 'queue for custom field sort testing'
+ );
+ ok($ret, "$queue_name - test queue creation. $msg");
+}
+
+diag "create a CF\n" if $ENV{TEST_VERBOSE};
+my $cf_name = "Order$$";
+my $cf;
+{
+ $cf = RT::CustomField->new( $RT::SystemUser );
+ my ($ret, $msg) = $cf->Create(
+ Name => $cf_name,
+ Queue => $queue->id,
+ Type => 'FreeformMultiple',
+ );
+ ok($ret, "Custom Field Order created");
+}
+
+my ($total, @data, @tickets, @test) = (0, ());
+
+sub add_tix_from_data {
+ my @res = ();
+ @data = sort { rand(100) <=> rand(100) } @data;
+ while (@data) {
+ my $t = RT::Ticket->new($RT::SystemUser);
+ my %args = %{ shift(@data) };
+ my @values = ();
+ if ( exists $args{'CF'} && ref $args{'CF'} ) {
+ @values = @{ delete $args{'CF'} };
+ } elsif ( exists $args{'CF'} ) {
+ @values = (delete $args{'CF'});
+ }
+ $args{ 'CustomField-'. $cf->id } = \@values
+ if @values;
+ my $subject = join(",", sort @values) || '-';
+ my ( $id, undef $msg ) = $t->Create(
+ %args,
+ Queue => $queue->id,
+ Subject => $subject,
+ );
+ ok( $id, "ticket created" ) or diag("error: $msg");
+ push @res, $t;
+ $total++;
+ }
+ return @res;
+}
+
+sub run_tests {
+ my $query_prefix = join ' OR ', map 'id = '. $_->id, @tickets;
+ foreach my $test ( @test ) {
+ my $query = join " AND ", map "( $_ )", grep defined && length,
+ $query_prefix, $test->{'Query'};
+
+ foreach my $order (qw(ASC DESC)) {
+ my $error = 0;
+ my $tix = RT::Tickets->new( $RT::SystemUser );
+ $tix->FromSQL( $query );
+ $tix->OrderBy( FIELD => $test->{'Order'}, ORDER => $order );
+
+ ok($tix->Count, "found ticket(s)")
+ or $error = 1;
+
+ my ($order_ok, $last) = (1, $order eq 'ASC'? '-': 'zzzzzz');
+ my $last_id = $tix->Last->id;
+ while ( my $t = $tix->Next ) {
+ my $tmp;
+ next if $t->id == $last_id and $t->Subject eq "-"; # Nulls are allowed to come last, in Pg
+
+ if ( $order eq 'ASC' ) {
+ $tmp = ((split( /,/, $last))[0] cmp (split( /,/, $t->Subject))[0]);
+ } else {
+ $tmp = -((split( /,/, $last))[-1] cmp (split( /,/, $t->Subject))[-1]);
+ }
+ if ( $tmp > 0 ) {
+ $order_ok = 0; last;
+ }
+ $last = $t->Subject;
+ }
+
+ ok( $order_ok, "$order order of tickets is good" )
+ or $error = 1;
+
+ if ( $error ) {
+ diag "Wrong SQL query:". $tix->BuildSelectQuery;
+ $tix->GotoFirstItem;
+ while ( my $t = $tix->Next ) {
+ diag sprintf "%02d - %s", $t->id, $t->Subject;
+ }
+ }
+ }
+ }
+}
+
+@data = (
+ { },
+ { CF => ['b', 'd'] },
+ { CF => ['a', 'c'] },
+);
+@tickets = add_tix_from_data();
+@test = (
+ { Order => "CF.{$cf_name}" },
+ { Order => "CF.$queue_name.{$cf_name}" },
+);
+run_tests();
+
+@data = (
+ { CF => ['m', 'a'] },
+ { CF => ['m'] },
+ { CF => ['m', 'o'] },
+);
+@tickets = add_tix_from_data();
+@test = (
+ { Order => "CF.{$cf_name}", Query => "CF.{$cf_name} = 'm'" },
+ { Order => "CF.$queue_name.{$cf_name}", Query => "CF.{$cf_name} = 'm'" },
+);
+run_tests();
+
diff --git a/rt/t/ticket/cfsort-freeform-single.t b/rt/t/ticket/cfsort-freeform-single.t
new file mode 100644
index 000000000..f1f506bea
--- /dev/null
+++ b/rt/t/ticket/cfsort-freeform-single.t
@@ -0,0 +1,191 @@
+#!/usr/bin/perl
+
+use RT::Test tests => 57;
+
+use strict;
+use warnings;
+
+use RT::Tickets;
+use RT::Queue;
+use RT::CustomField;
+
+# Test Sorting by FreeformSingle custom field.
+
+diag "Create a queue to test with." if $ENV{TEST_VERBOSE};
+my $queue_name = "CFSortQueue-$$";
+my $queue;
+{
+ $queue = RT::Queue->new( $RT::SystemUser );
+ my ($ret, $msg) = $queue->Create(
+ Name => $queue_name,
+ Description => 'queue for custom field sort testing'
+ );
+ ok($ret, "$queue test queue creation. $msg");
+}
+
+# CFs for testing, later we create another one
+my %CF;
+my $cf_name;
+
+diag "create a CF\n" if $ENV{TEST_VERBOSE};
+{
+ $cf_name = $CF{'CF'}{'name'} = "Order$$";
+ $CF{'CF'}{'obj'} = RT::CustomField->new( $RT::SystemUser );
+ my ($ret, $msg) = $CF{'CF'}{'obj'}->Create(
+ Name => $CF{'CF'}{'name'},
+ Queue => $queue->id,
+ Type => 'FreeformSingle',
+ );
+ ok($ret, "Custom Field $CF{'CF'}{'name'} created");
+}
+
+my ($total, @data, @tickets, @test) = (0, ());
+
+sub add_tix_from_data {
+ my @res = ();
+ @data = sort { rand(100) <=> rand(100) } @data;
+ while (@data) {
+ my $t = RT::Ticket->new($RT::SystemUser);
+ my %args = %{ shift(@data) };
+
+ my $subject = '-';
+ foreach my $e ( grep exists $CF{$_} && defined $CF{$_}, keys %args ) {
+ my @values = ();
+ if ( ref $args{ $e } ) {
+ @values = @{ delete $args{ $e } };
+ } else {
+ @values = (delete $args{ $e });
+ }
+ $args{ 'CustomField-'. $CF{ $e }{'obj'}->id } = \@values
+ if @values;
+ $subject = join(",", sort @values) || '-'
+ if $e eq 'CF';
+ }
+
+ my ( $id, undef $msg ) = $t->Create(
+ %args,
+ Queue => $queue->id,
+ Subject => $subject,
+ );
+ ok( $id, "ticket created" ) or diag("error: $msg");
+ push @res, $t;
+ $total++;
+ }
+ return @res;
+}
+
+sub run_tests {
+ my $query_prefix = join ' OR ', map 'id = '. $_->id, @tickets;
+ foreach my $test ( @test ) {
+ my $query = join " AND ", map "( $_ )", grep defined && length,
+ $query_prefix, $test->{'Query'};
+
+ foreach my $order (qw(ASC DESC)) {
+ my $error = 0;
+ my $tix = RT::Tickets->new( $RT::SystemUser );
+ $tix->FromSQL( $query );
+ $tix->OrderBy( FIELD => $test->{'Order'}, ORDER => $order );
+
+ ok($tix->Count, "found ticket(s)")
+ or $error = 1;
+
+ my ($order_ok, $last) = (1, $order eq 'ASC'? '-': 'zzzzzz');
+ my $last_id = $tix->Last->id;
+ while ( my $t = $tix->Next ) {
+ my $tmp;
+ next if $t->id == $last_id and $t->Subject eq "-"; # Nulls are allowed to come last, in Pg
+
+ if ( $order eq 'ASC' ) {
+ $tmp = ((split( /,/, $last))[0] cmp (split( /,/, $t->Subject))[0]);
+ } else {
+ $tmp = -((split( /,/, $last))[-1] cmp (split( /,/, $t->Subject))[-1]);
+ }
+ if ( $tmp > 0 ) {
+ $order_ok = 0; last;
+ }
+ $last = $t->Subject;
+ }
+
+ ok( $order_ok, "$order order of tickets is good" )
+ or $error = 1;
+
+ if ( $error ) {
+ diag "Wrong SQL query:". $tix->BuildSelectQuery;
+ $tix->GotoFirstItem;
+ while ( my $t = $tix->Next ) {
+ diag sprintf "%02d - %s", $t->id, $t->Subject;
+ }
+ }
+ }
+ }
+}
+
+@data = (
+ { },
+ { CF => 'a' },
+ { CF => 'b' },
+);
+@tickets = add_tix_from_data();
+@test = (
+ { Order => "CF.{$cf_name}" },
+ { Order => "CF.$queue_name.{$cf_name}" },
+);
+run_tests();
+
+@data = (
+ { },
+ { CF => 'aa' },
+ { CF => 'ab' },
+);
+@tickets = add_tix_from_data();
+@test = (
+ { Query => "CF.{$cf_name} LIKE 'a'", Order => "CF.{$cf_name}" },
+ { Query => "CF.{$cf_name} LIKE 'a'", Order => "CF.$queue_name.{$cf_name}" },
+);
+run_tests();
+
+@data = (
+ { Subject => '-', },
+ { Subject => 'a', CF => 'a' },
+ { Subject => 'b', CF => 'b' },
+ { Subject => 'c', CF => 'c' },
+);
+@tickets = add_tix_from_data();
+@test = (
+ { Query => "CF.{$cf_name} != 'c'", Order => "CF.{$cf_name}" },
+ { Query => "CF.{$cf_name} != 'c'", Order => "CF.$queue_name.{$cf_name}" },
+);
+run_tests();
+
+
+
+diag "create another CF\n" if $ENV{TEST_VERBOSE};
+{
+ $CF{'AnotherCF'}{'name'} = "OrderAnother$$";
+ $CF{'AnotherCF'}{'obj'} = RT::CustomField->new( $RT::SystemUser );
+ my ($ret, $msg) = $CF{'AnotherCF'}{'obj'}->Create(
+ Name => $CF{'AnotherCF'}{'name'},
+ Queue => $queue->id,
+ Type => 'FreeformSingle',
+ );
+ ok($ret, "Custom Field $CF{'AnotherCF'}{'name'} created");
+}
+
+# test that order is not affect by other fields (had such problem)
+@data = (
+ { Subject => '-', },
+ { Subject => 'a', CF => 'a', AnotherCF => 'za' },
+ { Subject => 'b', CF => 'b', AnotherCF => 'ya' },
+ { Subject => 'c', CF => 'c', AnotherCF => 'xa' },
+);
+@tickets = add_tix_from_data();
+@test = (
+ { Order => "CF.{$cf_name}" },
+ { Order => "CF.$queue_name.{$cf_name}" },
+ { Query => "CF.{$cf_name} != 'c'", Order => "CF.{$cf_name}" },
+ { Query => "CF.{$cf_name} != 'c'", Order => "CF.$queue_name.{$cf_name}" },
+);
+run_tests();
+
+
+
diff --git a/rt/t/ticket/deferred_owner.t b/rt/t/ticket/deferred_owner.t
new file mode 100644
index 000000000..40172caf9
--- /dev/null
+++ b/rt/t/ticket/deferred_owner.t
@@ -0,0 +1,120 @@
+
+use strict;
+use warnings;
+
+use RT::Test tests => 18;
+use_ok('RT');
+use_ok('RT::Ticket');
+use Test::Warn;
+
+
+my $tester = RT::Test->load_or_create_user(
+ EmailAddress => 'tester@localhost',
+);
+ok $tester && $tester->id, 'loaded or created user';
+
+my $queue = RT::Test->load_or_create_queue( Name => 'General' );
+ok $queue && $queue->id, 'loaded or created queue';
+
+my $owner_role_group = RT::Group->new( $RT::SystemUser );
+$owner_role_group->LoadQueueRoleGroup( Type => 'Owner', Queue => $queue->id );
+ok $owner_role_group->id, 'loaded owners role group of the queue';
+
+diag "check that deffering owner doesn't regress" if $ENV{'TEST_VERBOSE'};
+{
+ RT::Test->set_rights(
+ { Principal => $tester->PrincipalObj,
+ Right => [qw(SeeQueue ShowTicket CreateTicket OwnTicket)],
+ },
+ { Principal => $owner_role_group->PrincipalObj,
+ Object => $queue,
+ Right => [qw(ModifyTicket)],
+ },
+ );
+ my $ticket = RT::Ticket->new( $tester );
+ # tester is owner, owner has right to modify owned tickets,
+ # this right is required to set somebody as AdminCc
+ my ($tid, $txn_id, $msg) = $ticket->Create(
+ Queue => $queue->id,
+ Owner => $tester->id,
+ AdminCc => 'root@localhost',
+ );
+ diag $msg if $msg && $ENV{'TEST_VERBOSE'};
+ ok $tid, "created a ticket";
+ is $ticket->Owner, $tester->id, 'correct owner';
+ like $ticket->AdminCcAddresses, qr/root\@localhost/, 'root is there';
+}
+
+diag "check that previous trick doesn't work without sufficient rights"
+ if $ENV{'TEST_VERBOSE'};
+{
+ RT::Test->set_rights(
+ { Principal => $tester->PrincipalObj,
+ Right => [qw(SeeQueue ShowTicket CreateTicket OwnTicket)],
+ },
+ );
+ my $ticket = RT::Ticket->new( $tester );
+ # tester is owner, owner has right to modify owned tickets,
+ # this right is required to set somebody as AdminCc
+ my ($tid, $txn_id, $msg) = $ticket->Create(
+ Queue => $queue->id,
+ Owner => $tester->id,
+ AdminCc => 'root@localhost',
+ );
+ diag $msg if $msg && $ENV{'TEST_VERBOSE'};
+ ok $tid, "created a ticket";
+ is $ticket->Owner, $tester->id, 'correct owner';
+ unlike $ticket->AdminCcAddresses, qr/root\@localhost/, 'root is there';
+}
+
+diag "check that deffering owner really works" if $ENV{'TEST_VERBOSE'};
+{
+ RT::Test->set_rights(
+ { Principal => $tester->PrincipalObj,
+ Right => [qw(SeeQueue ShowTicket CreateTicket)],
+ },
+ { Principal => $queue->Cc->PrincipalObj,
+ Object => $queue,
+ Right => [qw(OwnTicket TakeTicket)],
+ },
+ );
+ my $ticket = RT::Ticket->new( $tester );
+ # set tester as Cc, Cc role group has right to own and take tickets
+ my ($tid, $txn_id, $msg) = $ticket->Create(
+ Queue => $queue->id,
+ Owner => $tester->id,
+ Cc => 'tester@localhost',
+ );
+ diag $msg if $msg && $ENV{'TEST_VERBOSE'};
+ ok $tid, "created a ticket";
+ like $ticket->CcAddresses, qr/tester\@localhost/, 'tester is in the cc list';
+ is $ticket->Owner, $tester->id, 'tester is also owner';
+}
+
+diag "check that deffering doesn't work without correct rights" if $ENV{'TEST_VERBOSE'};
+{
+ RT::Test->set_rights(
+ { Principal => $tester->PrincipalObj,
+ Right => [qw(SeeQueue ShowTicket CreateTicket)],
+ },
+ );
+
+ my $ticket = RT::Ticket->new( $tester );
+ # set tester as Cc, Cc role group has right to own and take tickets
+ my ($tid, $txn_id, $msg);
+ warning_like {
+ ($tid, $txn_id, $msg) = $ticket->Create(
+ Queue => $queue->id,
+ Owner => $tester->id,
+ Cc => 'tester@localhost',
+ );
+ } qr/User .* was proposed as a ticket owner but has no rights to own tickets in General/;
+
+ diag $msg if $msg && $ENV{'TEST_VERBOSE'};
+ ok $tid, "created a ticket";
+ like $ticket->CcAddresses, qr/tester\@localhost/, 'tester is in the cc list';
+ isnt $ticket->Owner, $tester->id, 'tester is also owner';
+}
+
+
+
diff --git a/rt/t/ticket/link_search.t b/rt/t/ticket/link_search.t
new file mode 100644
index 000000000..1bf7dc6dc
--- /dev/null
+++ b/rt/t/ticket/link_search.t
@@ -0,0 +1,246 @@
+#!/usr/bin/perl -w
+
+use strict;
+use RT;
+
+# Load the config file
+use RT::Test tests => 63;
+
+#Connect to the database and get RT::SystemUser and RT::Nobody loaded
+
+
+#Get the current user all loaded
+my $CurrentUser = $RT::SystemUser;
+
+my $queue = new RT::Queue($CurrentUser);
+$queue->Load('General') || Abort(loc("Queue could not be loaded."));
+
+my $child_ticket = new RT::Ticket( $CurrentUser );
+my ($childid) = $child_ticket->Create(
+ Subject => 'test child',
+ Queue => $queue->Id,
+);
+ok($childid, "We created a child ticket");
+
+my $parent_ticket = new RT::Ticket( $CurrentUser );
+my ($parentid) = $parent_ticket->Create(
+ Subject => 'test parent',
+ Children => [ $childid ],
+ Queue => $queue->Id,
+);
+ok($parentid, "We created a parent ticket");
+
+
+my $Collection = RT::Tickets->new($CurrentUser);
+$Collection->LimitMemberOf( $parentid );
+is($Collection->Count,1, "We found only one result");
+ok($Collection->First);
+is($Collection->First->id, $childid, "We found the collection of all children of $parentid with Limit");
+
+$Collection = RT::Tickets->new($CurrentUser);
+$Collection->FromSQL("MemberOf = $parentid");
+is($Collection->Count, 1, "We found only one result");
+ok($Collection->First);
+is($Collection->First->id, $childid, "We found the collection of all children of $parentid with TicketSQL");
+
+
+$Collection = RT::Tickets->new($CurrentUser);
+$Collection->LimitHasMember ($childid);
+is($Collection->Count,1, "We found only one result");
+ok($Collection->First);
+is($Collection->First->id, $parentid, "We found the collection of all parents of $childid with Limit");
+
+
+$Collection = RT::Tickets->new($CurrentUser);
+$Collection->FromSQL("HasMember = $childid");
+is($Collection->Count,1, "We found only one result");
+ok($Collection->First);
+is($Collection->First->id, $parentid, "We found the collection of all parents of $childid with TicketSQL");
+
+
+# Now we find a collection of all the tickets which have no members. they should have no children.
+$Collection = RT::Tickets->new($CurrentUser);
+$Collection->LimitHasMember('');
+# must contain child; must not contain parent
+my %has;
+while (my $t = $Collection->Next) {
+ ++$has{$t->id};
+}
+ok( $has{$childid}, "The collection has our child - $childid");
+ok( !$has{$parentid}, "The collection doesn't have our parent - $parentid");
+
+
+# Now we find a collection of all the tickets which are not members of anything. they should have no parents.
+$Collection = RT::Tickets->new($CurrentUser);
+$Collection->LimitMemberOf('');
+# must contain parent; must not contain child
+%has = ();
+while (my $t = $Collection->Next) {
+ ++$has{$t->id};
+}
+ok ($has{$parentid} , "The collection has our parent - $parentid");
+ok( !$has{$childid}, "The collection doesn't have our child - $childid");
+
+
+# Do it all over with TicketSQL
+#
+
+
+
+# Now we find a collection of all the tickets which have no members. they should have no children.
+$Collection = RT::Tickets->new($CurrentUser);
+$Collection->FromSQL ("HasMember IS NULL");
+# must contain parent; must not contain child
+%has = ();
+while (my $t = $Collection->Next) {
+ ++$has{$t->id};
+}
+ok( !$has{$parentid}, "The collection doesn't have our parent - $parentid");
+ok( $has{$childid}, "The collection has our child - $childid");
+
+
+# Now we find a collection of all the tickets which have no members. they should have no children.
+# Alternate syntax
+$Collection = RT::Tickets->new($CurrentUser);
+$Collection->FromSQL("HasMember = ''");
+# must contain parent; must not contain child
+%has = ();
+while (my $t = $Collection->Next) {
+ ++$has{$t->id};
+}
+ok( !$has{$parentid}, "The collection doesn't have our parent - $parentid");
+ok( $has{$childid}, "The collection has our child - $childid");
+
+
+# Now we find a collection of all the tickets which are not members of anything. they should have no parents.
+$Collection = RT::Tickets->new($CurrentUser);
+$Collection->FromSQL("MemberOf IS NULL");
+# must not contain parent; must contain parent
+%has = ();
+while (my $t = $Collection->Next) {
+ ++$has{$t->id};
+}
+ok( $has{$parentid}, "The collection has our parent - $parentid");
+ok( !$has{$childid}, "The collection doesn't have our child - $childid");
+
+
+# Now we find a collection of all the tickets which are not members of anything. they should have no parents.
+$Collection = RT::Tickets->new($CurrentUser);
+$Collection->FromSQL("MemberOf = ''");
+# must not contain parent; must contain parent
+%has = ();
+while (my $t = $Collection->Next) {
+ ++$has{$t->id};
+}
+ok( $has{$parentid}, "The collection has our parent - $parentid");
+ok( !$has{$childid}, "The collection doesn't have our child - $childid");
+
+
+# Now we find a collection of all the tickets which are not members of the parent ticket
+$Collection = RT::Tickets->new($CurrentUser);
+$Collection->FromSQL("MemberOf != $parentid");
+%has = ();
+while (my $t = $Collection->Next) {
+ ++$has{$t->id};
+}
+ok( $has{$parentid}, "The collection has our parent - $parentid");
+ok( !$has{$childid}, "The collection doesn't have our child - $childid");
+
+$Collection = RT::Tickets->new($CurrentUser);
+$Collection->LimitMemberOf($parentid, OPERATOR => '!=');
+%has = ();
+while (my $t = $Collection->Next) {
+ ++$has{$t->id};
+}
+ok( $has{$parentid}, "The collection has our parent - $parentid");
+ok( !$has{$childid}, "The collection doesn't have our child - $childid");
+
+my $grand_child_ticket = new RT::Ticket( $CurrentUser );
+my ($grand_childid) = $child_ticket->Create(
+ Subject => 'test child',
+ Queue => $queue->Id,
+ MemberOf => $childid,
+);
+ok($childid, "We created a grand child ticket");
+
+my $unlinked_ticket = new RT::Ticket( $CurrentUser );
+my ($unlinked_id) = $child_ticket->Create(
+ Subject => 'test unlinked',
+ Queue => $queue->Id,
+);
+ok($unlinked_id, "We created a grand child ticket");
+
+$Collection = RT::Tickets->new($CurrentUser);
+$Collection->FromSQL( "LinkedTo = $childid" );
+is($Collection->Count,1, "We found only one result");
+ok($Collection->First);
+is($Collection->First->id, $grand_childid, "We found all tickets linked to ticket #$childid");
+
+$Collection = RT::Tickets->new($CurrentUser);
+$Collection->FromSQL( "LinkedFrom = $childid" );
+is($Collection->Count,1, "We found only one result");
+ok($Collection->First);
+is($Collection->First->id, $parentid, "We found all tickets linked from ticket #$childid");
+
+$Collection = RT::Tickets->new($CurrentUser);
+$Collection->FromSQL( "LinkedTo IS NULL" );
+ok($Collection->Count, "Result is set is not empty");
+%has = ();
+while (my $t = $Collection->Next) {
+ ++$has{$t->id};
+}
+ok( $has{$parentid}, "parent is in collection");
+ok( $has{$unlinked_id}, "unlinked is in collection");
+ok( !$has{$childid}, "child is NOT in collection");
+ok( !$has{$grand_childid}, "grand child too is not in collection");
+
+$Collection = RT::Tickets->new($CurrentUser);
+$Collection->FromSQL( "LinkedTo IS NOT NULL" );
+ok($Collection->Count, "Result set is not empty");
+%has = ();
+while (my $t = $Collection->Next) {
+ ++$has{$t->id};
+}
+ok( !$has{$parentid}, "The collection has no our parent - $parentid");
+ok( !$has{$unlinked_id}, "unlinked is not in collection");
+ok( $has{$childid}, "The collection have our child - $childid");
+ok( $has{$grand_childid}, "The collection have our grand child - $grand_childid");
+
+$Collection = RT::Tickets->new($CurrentUser);
+$Collection->FromSQL( "LinkedFrom IS NULL" );
+ok($Collection->Count, "Result is set is not empty");
+%has = ();
+while (my $t = $Collection->Next) {
+ ++$has{$t->id};
+}
+ok( !$has{$parentid}, "parent is NOT in collection");
+ok( !$has{$childid}, "child is NOT in collection");
+ok( $has{$grand_childid}, "grand child is in collection");
+ok( $has{$unlinked_id}, "unlinked is in collection");
+
+$Collection = RT::Tickets->new($CurrentUser);
+$Collection->FromSQL( "LinkedFrom IS NOT NULL" );
+ok($Collection->Count, "Result set is not empty");
+%has = ();
+while (my $t = $Collection->Next) {
+ ++$has{$t->id};
+}
+ok( $has{$parentid}, "The collection has our parent - $parentid");
+ok( $has{$childid}, "The collection have our child - $childid");
+ok( !$has{$grand_childid}, "The collection have no our grand child - $grand_childid");
+ok( !$has{$unlinked_id}, "unlinked is not in collection");
+
+$Collection = RT::Tickets->new($CurrentUser);
+$Collection->FromSQL( "Linked = $childid" );
+is($Collection->Count, 2, "We found two tickets: parent and child");
+%has = ();
+while (my $t = $Collection->Next) {
+ ++$has{$t->id};
+}
+ok( !$has{$childid}, "Ticket is not linked to itself");
+ok( $has{$parentid}, "The collection has our parent");
+ok( $has{$grand_childid}, "The collection have our child");
+ok( !$has{$unlinked_id}, "unlinked is not in collection");
+
+
+1;
diff --git a/rt/t/ticket/linking.t b/rt/t/ticket/linking.t
new file mode 100644
index 000000000..2ea3d58da
--- /dev/null
+++ b/rt/t/ticket/linking.t
@@ -0,0 +1,385 @@
+
+use strict;
+use warnings;
+
+use RT::Test tests => '101';
+use_ok('RT');
+use_ok('RT::Ticket');
+use_ok('RT::ScripConditions');
+use_ok('RT::ScripActions');
+use_ok('RT::Template');
+use_ok('RT::Scrips');
+use_ok('RT::Scrip');
+
+
+use File::Temp qw/tempfile/;
+my ($fh, $filename) = tempfile( UNLINK => 1, SUFFIX => '.rt');
+my $link_scrips_orig = RT->Config->Get( 'LinkTransactionsRun1Scrip' );
+RT->Config->Set( 'LinkTransactionsRun1Scrip', 1 );
+
+my $link_acl_checks_orig = RT->Config->Get( 'StrictLinkACL' );
+RT->Config->Set( 'StrictLinkACL', 1);
+
+my $condition = RT::ScripCondition->new( $RT::SystemUser );
+$condition->Load('User Defined');
+ok($condition->id);
+my $action = RT::ScripAction->new( $RT::SystemUser );
+$action->Load('User Defined');
+ok($action->id);
+my $template = RT::Template->new( $RT::SystemUser );
+$template->Load('Blank');
+ok($template->id);
+
+my $q1 = RT::Queue->new($RT::SystemUser);
+my ($id,$msg) = $q1->Create(Name => "LinkTest1.$$");
+ok ($id,$msg);
+my $q2 = RT::Queue->new($RT::SystemUser);
+($id,$msg) = $q2->Create(Name => "LinkTest2.$$");
+ok ($id,$msg);
+
+my $commit_code = <<END;
+open my \$file, "<$filename" or die "couldn't open $filename";
+my \$data = <\$file>;
+chomp \$data;
+\$data += 0;
+close \$file;
+\$RT::Logger->debug("Data is \$data");
+
+open \$file, ">$filename" or die "couldn't open $filename";
+if (\$self->TransactionObj->Type eq 'AddLink') {
+ \$RT::Logger->debug("AddLink");
+ print \$file \$data+1, "\n";
+}
+elsif (\$self->TransactionObj->Type eq 'DeleteLink') {
+ \$RT::Logger->debug("DeleteLink");
+ print \$file \$data-1, "\n";
+}
+else {
+ \$RT::Logger->error("THIS SHOULDN'T HAPPEN");
+ print \$file "666\n";
+}
+close \$file;
+1;
+END
+
+my $Scrips = RT::Scrips->new( $RT::SystemUser );
+$Scrips->UnLimit;
+while ( my $Scrip = $Scrips->Next ) {
+ $Scrip->Delete if $Scrip->Description and $Scrip->Description =~ /Add or Delete Link \d+/;
+}
+
+
+my $scrip = RT::Scrip->new($RT::SystemUser);
+($id,$msg) = $scrip->Create( Description => "Add or Delete Link $$",
+ ScripCondition => $condition->id,
+ ScripAction => $action->id,
+ Template => $template->id,
+ Stage => 'TransactionCreate',
+ Queue => 0,
+ CustomIsApplicableCode => '$self->TransactionObj->Type =~ /(Add|Delete)Link/;',
+ CustomPrepareCode => '1;',
+ CustomCommitCode => $commit_code,
+ );
+ok($id, "Scrip created");
+
+my $u1 = RT::User->new($RT::SystemUser);
+($id,$msg) = $u1->Create(Name => "LinkTestUser.$$");
+ok ($id,$msg);
+
+# grant ShowTicket right to allow count transactions
+($id,$msg) = $u1->PrincipalObj->GrantRight ( Object => $q1, Right => 'ShowTicket');
+ok ($id,$msg);
+($id,$msg) = $u1->PrincipalObj->GrantRight ( Object => $q2, Right => 'ShowTicket');
+ok ($id,$msg);
+($id,$msg) = $u1->PrincipalObj->GrantRight ( Object => $q1, Right => 'CreateTicket');
+ok ($id,$msg);
+
+my $creator = RT::CurrentUser->new($u1->id);
+
+diag('Create tickets without rights to link') if $ENV{'TEST_VERBOSE'};
+{
+ # on q2 we have no rights, yet
+ my $parent = RT::Ticket->new( $RT::SystemUser );
+ my ($id,$tid,$msg) = $parent->Create( Subject => 'Link test 1', Queue => $q2->id );
+ ok($id,$msg);
+ my $child = RT::Ticket->new( $creator );
+ ($id,$tid,$msg) = $child->Create( Subject => 'Link test 1', Queue => $q1->id, MemberOf => $parent->id );
+ ok($id,$msg);
+ $child->CurrentUser( $RT::SystemUser );
+ is($child->_Links('Base')->Count, 0, 'link was not created, no permissions');
+ is($child->_Links('Target')->Count, 0, 'link was not create, no permissions');
+}
+
+diag('Create tickets with rights checks on one end of a link') if $ENV{'TEST_VERBOSE'};
+{
+ # on q2 we have no rights, but use checking one only on thing
+ RT->Config->Set( StrictLinkACL => 0 );
+ my $parent = RT::Ticket->new( $RT::SystemUser );
+ my ($id,$tid,$msg) = $parent->Create( Subject => 'Link test 1', Queue => $q2->id );
+ ok($id,$msg);
+ my $child = RT::Ticket->new( $creator );
+ ($id,$tid,$msg) = $child->Create( Subject => 'Link test 1', Queue => $q1->id, MemberOf => $parent->id );
+ ok($id,$msg);
+ $child->CurrentUser( $RT::SystemUser );
+ is($child->_Links('Base')->Count, 1, 'link was created');
+ is($child->_Links('Target')->Count, 0, 'link was created only one');
+ # no scrip run on second ticket accroding to config option
+ is(link_count($filename), undef, "scrips ok");
+ RT->Config->Set( StrictLinkACL => 1 );
+}
+
+($id,$msg) = $u1->PrincipalObj->GrantRight ( Object => $q1, Right => 'ModifyTicket');
+ok ($id,$msg);
+
+diag('try to add link without rights') if $ENV{'TEST_VERBOSE'};
+{
+ # on q2 we have no rights, yet
+ my $parent = RT::Ticket->new( $RT::SystemUser );
+ my ($id,$tid,$msg) = $parent->Create( Subject => 'Link test 1', Queue => $q2->id );
+ ok($id,$msg);
+ my $child = RT::Ticket->new( $creator );
+ ($id,$tid,$msg) = $child->Create( Subject => 'Link test 1', Queue => $q1->id );
+ ok($id,$msg);
+ ($id, $msg) = $child->AddLink(Type => 'MemberOf', Target => $parent->id);
+ ok(!$id, $msg);
+ is(link_count($filename), undef, "scrips ok");
+ $child->CurrentUser( $RT::SystemUser );
+ is($child->_Links('Base')->Count, 0, 'link was not created, no permissions');
+ is($child->_Links('Target')->Count, 0, 'link was not create, no permissions');
+}
+
+diag('add link with rights only on base') if $ENV{'TEST_VERBOSE'};
+{
+ # on q2 we have no rights, but use checking one only on thing
+ RT->Config->Set( StrictLinkACL => 0 );
+ my $parent = RT::Ticket->new( $RT::SystemUser );
+ my ($id,$tid,$msg) = $parent->Create( Subject => 'Link test 1', Queue => $q2->id );
+ ok($id,$msg);
+ my $child = RT::Ticket->new( $creator );
+ ($id,$tid,$msg) = $child->Create( Subject => 'Link test 1', Queue => $q1->id );
+ ok($id,$msg);
+ ($id, $msg) = $child->AddLink(Type => 'MemberOf', Target => $parent->id);
+ ok($id, $msg);
+ is(link_count($filename), 1, "scrips ok");
+ $child->CurrentUser( $RT::SystemUser );
+ is($child->_Links('Base')->Count, 1, 'link was created');
+ is($child->_Links('Target')->Count, 0, 'link was created only one');
+ $child->CurrentUser( $creator );
+
+ # turn off feature and try to delete link, we should fail
+ RT->Config->Set( StrictLinkACL => 1 );
+ ($id, $msg) = $child->AddLink(Type => 'MemberOf', Target => $parent->id);
+ ok(!$id, $msg);
+ is(link_count($filename), 1, "scrips ok");
+ $child->CurrentUser( $RT::SystemUser );
+ $child->_Links('Base')->_DoCount;
+ is($child->_Links('Base')->Count, 1, 'link was not deleted');
+ $child->CurrentUser( $creator );
+
+ # try to delete link, we should success as feature is active
+ RT->Config->Set( StrictLinkACL => 0 );
+ ($id, $msg) = $child->DeleteLink(Type => 'MemberOf', Target => $parent->id);
+ ok($id, $msg);
+ is(link_count($filename), 0, "scrips ok");
+ $child->CurrentUser( $RT::SystemUser );
+ $child->_Links('Base')->_DoCount;
+ is($child->_Links('Base')->Count, 0, 'link was deleted');
+ RT->Config->Set( StrictLinkACL => 1 );
+}
+
+my $tid;
+my $ticket = RT::Ticket->new( $creator);
+ok($ticket->isa('RT::Ticket'));
+($id,$tid, $msg) = $ticket->Create(Subject => 'Link test 1', Queue => $q1->id);
+ok ($id,$msg);
+
+diag('try link to itself') if $ENV{'TEST_VERBOSE'};
+{
+ my ($id, $msg) = $ticket->AddLink(Type => 'RefersTo', Target => $ticket->id);
+ ok(!$id, $msg);
+ is(link_count($filename), 0, "scrips ok");
+}
+
+my $ticket2 = RT::Ticket->new($RT::SystemUser);
+($id, $tid, $msg) = $ticket2->Create(Subject => 'Link test 2', Queue => $q2->id);
+ok ($id, $msg);
+($id,$msg) =$ticket->AddLink(Type => 'RefersTo', Target => $ticket2->id);
+ok(!$id,$msg);
+is(link_count($filename), 0, "scrips ok");
+
+($id,$msg) = $u1->PrincipalObj->GrantRight ( Object => $q2, Right => 'CreateTicket');
+ok ($id,$msg);
+($id,$msg) = $u1->PrincipalObj->GrantRight ( Object => $q2, Right => 'ModifyTicket');
+ok ($id,$msg);
+($id,$msg) = $ticket->AddLink(Type => 'RefersTo', Target => $ticket2->id);
+ok($id,$msg);
+is(link_count($filename), 1, "scrips ok");
+($id,$msg) = $ticket->AddLink(Type => 'RefersTo', Target => -1);
+ok(!$id,$msg);
+($id,$msg) = $ticket->AddLink(Type => 'RefersTo', Target => $ticket2->id);
+ok($id,$msg);
+is(link_count($filename), 1, "scrips ok");
+
+my $transactions = $ticket2->Transactions;
+$transactions->Limit( FIELD => 'Type', VALUE => 'AddLink' );
+is( $transactions->Count, 1, "Transaction found in other ticket" );
+is( $transactions->First->Field , 'ReferredToBy');
+is( $transactions->First->NewValue , $ticket->URI );
+
+($id,$msg) = $ticket->DeleteLink(Type => 'RefersTo', Target => $ticket2->id);
+ok($id,$msg);
+is(link_count($filename), 0, "scrips ok");
+$transactions = $ticket2->Transactions;
+$transactions->Limit( FIELD => 'Type', VALUE => 'DeleteLink' );
+is( $transactions->Count, 1, "Transaction found in other ticket" );
+is( $transactions->First->Field , 'ReferredToBy');
+is( $transactions->First->OldValue , $ticket->URI );
+
+RT->Config->Set( LinkTransactionsRun1Scrip => 0 );
+
+($id,$msg) =$ticket->AddLink(Type => 'RefersTo', Target => $ticket2->id);
+ok($id,$msg);
+is(link_count($filename), 2, "scrips ok");
+($id,$msg) =$ticket->DeleteLink(Type => 'RefersTo', Target => $ticket2->id);
+ok($id,$msg);
+is(link_count($filename), 0, "scrips ok");
+
+# tests for silent behaviour
+($id,$msg) = $ticket->AddLink(Type => 'RefersTo', Target => $ticket2->id, Silent => 1);
+ok($id,$msg);
+is(link_count($filename), 0, "scrips ok");
+{
+ my $transactions = $ticket->Transactions;
+ $transactions->Limit( FIELD => 'Type', VALUE => 'AddLink' );
+ is( $transactions->Count, 2, "Still two txns on the base" );
+
+ $transactions = $ticket2->Transactions;
+ $transactions->Limit( FIELD => 'Type', VALUE => 'AddLink' );
+ is( $transactions->Count, 2, "Still two txns on the target" );
+
+}
+($id,$msg) =$ticket->DeleteLink(Type => 'RefersTo', Target => $ticket2->id, Silent => 1);
+ok($id,$msg);
+is(link_count($filename), 0, "scrips ok");
+
+($id,$msg) = $ticket->AddLink(Type => 'RefersTo', Target => $ticket2->id, SilentBase => 1);
+ok($id,$msg);
+is(link_count($filename), 1, "scrips ok");
+{
+ my $transactions = $ticket->Transactions;
+ $transactions->Limit( FIELD => 'Type', VALUE => 'AddLink' );
+ is( $transactions->Count, 2, "still five txn on the base" );
+
+ $transactions = $ticket2->Transactions;
+ $transactions->Limit( FIELD => 'Type', VALUE => 'AddLink' );
+ is( $transactions->Count, 3, "+1 txn on the target" );
+
+}
+($id,$msg) =$ticket->DeleteLink(Type => 'RefersTo', Target => $ticket2->id, SilentBase => 1);
+ok($id,$msg);
+is(link_count($filename), 0, "scrips ok");
+
+($id,$msg) = $ticket->AddLink(Type => 'RefersTo', Target => $ticket2->id, SilentTarget => 1);
+ok($id,$msg);
+is(link_count($filename), 1, "scrips ok");
+{
+ my $transactions = $ticket->Transactions;
+ $transactions->Limit( FIELD => 'Type', VALUE => 'AddLink' );
+ is( $transactions->Count, 3, "+1 txn on the base" );
+
+ $transactions = $ticket2->Transactions;
+ $transactions->Limit( FIELD => 'Type', VALUE => 'AddLink' );
+ is( $transactions->Count, 3, "three txns on the target" );
+}
+($id,$msg) =$ticket->DeleteLink(Type => 'RefersTo', Target => $ticket2->id, SilentTarget => 1);
+ok($id,$msg);
+is(link_count($filename), 0, "scrips ok");
+
+
+# restore
+RT->Config->Set( LinkTransactionsRun1Scrip => $link_scrips_orig );
+RT->Config->Set( StrictLinkACL => $link_acl_checks_orig );
+
+{
+ my $Scrips = RT::Scrips->new( $RT::SystemUser );
+ $Scrips->Limit( FIELD => 'Description', OPERATOR => 'STARTSWITH', VALUE => 'Add or Delete Link ');
+ while ( my $s = $Scrips->Next ) { $s->Delete };
+}
+
+
+my $link = RT::Link->new( $RT::SystemUser );
+($id,$msg) = $link->Create( Base => $ticket->URI, Target => $ticket2->URI, Type => 'MyLinkType' );
+ok($id, $msg);
+ok($link->LocalBase == $ticket->id, "LocalBase set correctly");
+ok($link->LocalTarget == $ticket2->id, "LocalTarget set correctly");
+
+{
+ no warnings 'once';
+ *RT::NotTicket::Id = sub { return $$ };
+ *RT::NotTicket::id = \&RT::NotTicket::Id;
+}
+
+{
+ package RT::URI::not_ticket;
+ use RT::URI::base;
+ use vars qw(@ISA);
+ @ISA = qw/RT::URI::base/;
+ sub IsLocal { 1; }
+ sub Object { return bless {}, 'RT::NotTicket'; }
+}
+
+my $orig_getresolver = \&RT::URI::_GetResolver;
+{
+ no warnings 'redefine';
+ *RT::URI::_GetResolver = sub {
+ my $self = shift;
+ my $scheme = shift;
+
+ $scheme =~ s/(\.|-)/_/g;
+ my $resolver;
+ my $module = "RT::URI::$scheme";
+ $resolver = $module->new($self->CurrentUser);
+
+ if ($resolver) {
+ $self->{'resolver'} = $resolver;
+ } else {
+ $self->{'resolver'} = RT::URI::base->new($self->CurrentUser);
+ }
+ };
+}
+
+($id,$msg) = $link->Create( Base => "not_ticket::$RT::Organization/notticket/$$", Target => $ticket2->URI, Type => 'MyLinkType' );
+ok($id, $msg);
+ok($link->LocalBase == 0, "LocalBase set correctly");
+ok($link->LocalTarget == $ticket2->id, "LocalTarget set correctly");
+
+($id,$msg) = $link->Create( Target => "not_ticket::$RT::Organization/notticket/$$", Base => $ticket->URI, Type => 'MyLinkType' );
+ok($id, $msg);
+ok($link->LocalTarget == 0, "LocalTarget set correctly");
+ok($link->LocalBase == $ticket->id, "LocalBase set correctly");
+
+($id,$msg) = $link->Create(
+ Target => "not_ticket::$RT::Organization/notticket/1$$",
+ Base => "not_ticket::$RT::Organization/notticket/$$",
+ Type => 'MyLinkType' );
+
+ok($id, $msg);
+ok($link->LocalTarget == 0, "LocalTarget set correctly");
+ok($link->LocalBase == 0, "LocalBase set correctly");
+
+# restore _GetResolver
+{
+ no warnings 'redefine';
+ *RT::URI::_GetResolver = $orig_getresolver;
+}
+
+sub link_count {
+ my $file = shift;
+ open my $fh, "<$file" or die "couldn't open $file";
+ my $data = <$fh>;
+ close $fh;
+
+ return undef unless $data;
+ chomp $data;
+ return $data + 0;
+}
diff --git a/rt/t/ticket/merge.t b/rt/t/ticket/merge.t
new file mode 100644
index 000000000..a714cb6cc
--- /dev/null
+++ b/rt/t/ticket/merge.t
@@ -0,0 +1,92 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+
+use RT;
+use RT::Test tests => '17';
+
+
+# validate that when merging two tickets, the comments from both tickets
+# are integrated into the new ticket
+{
+ my $queue = RT::Queue->new($RT::SystemUser);
+ my ($id,$msg) = $queue->Create(Name => 'MergeTest-'.rand(25));
+ ok ($id,$msg);
+
+ my $t1 = RT::Ticket->new($RT::SystemUser);
+ my ($tid,$transid, $t1msg) =$t1->Create ( Queue => $queue->Name, Subject => 'Merge test. orig');
+ ok ($tid, $t1msg);
+ ($id, $msg) = $t1->Comment(Content => 'This is a Comment on the original');
+ ok($id,$msg);
+
+ my $txns = $t1->Transactions;
+ my $Comments = 0;
+ while (my $txn = $txns->Next) {
+ $Comments++ if ($txn->Type eq 'Comment');
+ }
+ is($Comments,1, "our first ticket has only one Comment");
+
+ my $t2 = RT::Ticket->new($RT::SystemUser);
+ my ($t2id,$t2transid, $t2msg) =$t2->Create ( Queue => $queue->Name, Subject => 'Merge test. duplicate');
+ ok ($t2id, $t2msg);
+
+
+
+ ($id, $msg) = $t2->Comment(Content => 'This is a commet on the duplicate');
+ ok($id,$msg);
+
+
+ $txns = $t2->Transactions;
+ $Comments = 0;
+ while (my $txn = $txns->Next) {
+ $Comments++ if ($txn->Type eq 'Comment');
+ }
+ is($Comments,1, "our second ticket has only one Comment");
+
+ ($id, $msg) = $t1->Comment(Content => 'This is a second Comment on the original');
+ ok($id,$msg);
+
+ $txns = $t1->Transactions;
+ $Comments = 0;
+ while (my $txn = $txns->Next) {
+ $Comments++ if ($txn->Type eq 'Comment');
+ }
+ is($Comments,2, "our first ticket now has two Comments");
+
+ ($id,$msg) = $t2->MergeInto($t1->id);
+
+ ok($id,$msg);
+ $txns = $t1->Transactions;
+ $Comments = 0;
+ while (my $txn = $txns->Next) {
+ $Comments++ if ($txn->Type eq 'Comment');
+ }
+ is($Comments,3, "our first ticket now has three Comments - we merged safely");
+}
+
+# when you try to merge duplicate links on postgres, eveyrything goes to hell due to referential integrity constraints.
+{
+ my $t = RT::Ticket->new($RT::SystemUser);
+ $t->Create(Subject => 'Main', Queue => 'general');
+
+ ok ($t->id);
+ my $t2 = RT::Ticket->new($RT::SystemUser);
+ $t2->Create(Subject => 'Second', Queue => 'general');
+ ok ($t2->id);
+
+ my $t3 = RT::Ticket->new($RT::SystemUser);
+ $t3->Create(Subject => 'Third', Queue => 'general');
+
+ ok ($t3->id);
+
+ my ($id,$val);
+ ($id,$val) = $t->AddLink(Type => 'DependsOn', Target => $t3->id);
+ ok($id,$val);
+ ($id,$val) = $t2->AddLink(Type => 'DependsOn', Target => $t3->id);
+ ok($id,$val);
+
+ ($id,$val) = $t->MergeInto($t2->id);
+ ok($id,$val);
+}
diff --git a/rt/t/ticket/quicksearch.t b/rt/t/ticket/quicksearch.t
new file mode 100644
index 000000000..9ab9f21e4
--- /dev/null
+++ b/rt/t/ticket/quicksearch.t
@@ -0,0 +1,41 @@
+
+#!/usr/bin/perl -w
+
+use strict;
+use warnings;
+
+use RT::Test tests => 10;
+use_ok('RT');
+
+
+my $q = RT::Queue->new($RT::SystemUser);
+my $queue = 'SearchTests-'.$$;
+$q->Create(Name => $queue);
+ok ($q->id, "Created the queue");
+
+my $t1 = RT::Ticket->new($RT::SystemUser);
+my ( $id, undef, $msg ) = $t1->Create(
+ Queue => $q->id,
+ Subject => 'SearchTest1',
+ Requestor => ['search2@example.com'],
+);
+ok( $id, $msg );
+
+use_ok("RT::Search::Googleish");
+
+my $active_statuses = join( " OR ", map "Status = '$_'", RT::Queue->ActiveStatusArray());
+
+my $tickets = RT::Tickets->new($RT::SystemUser);
+my $quick = RT::Search::Googleish->new(Argument => "",
+ TicketsObj => $tickets);
+my @tests = (
+ "General new open root" => "( Owner = 'root' ) AND ( Queue = 'General' ) AND ( Status = 'new' OR Status = 'open' )",
+ "fulltext:jesse" => "( Content LIKE 'jesse' ) AND ( $active_statuses )",
+ $queue => "( Queue = '$queue' ) AND ( $active_statuses )",
+ "root $queue" => "( Owner = 'root' ) AND ( Queue = '$queue' ) AND ( $active_statuses )",
+ "notauser $queue" => "( Queue = '$queue' ) AND ( $active_statuses ) AND ( Subject LIKE 'notauser' )",
+ "notauser $queue root" => "( Owner = 'root' ) AND ( Queue = '$queue' ) AND ( $active_statuses ) AND ( Subject LIKE 'notauser' )");
+
+while (my ($from, $to) = splice @tests, 0, 2) {
+ is($quick->QueryToSQL($from), $to, "<$from> -> <$to>");
+}
diff --git a/rt/t/ticket/requestor-order.t b/rt/t/ticket/requestor-order.t
new file mode 100644
index 000000000..4539fbdc6
--- /dev/null
+++ b/rt/t/ticket/requestor-order.t
@@ -0,0 +1,142 @@
+#!/usr/bin/perl -w
+use strict; use warnings;
+
+use RT::Test tests => 58;
+use_ok('RT');
+
+use RT::Ticket;
+
+my $q = RT::Queue->new($RT::SystemUser);
+my $queue = 'SearchTests-'.rand(200);
+$q->Create(Name => $queue);
+
+my @requestors = ( ('bravo@example.com') x 6, ('alpha@example.com') x 6,
+ ('delta@example.com') x 6, ('charlie@example.com') x 6,
+ (undef) x 6);
+my @subjects = ("first test", "second test", "third test", "fourth test", "fifth test") x 6;
+while (@requestors) {
+ my $t = RT::Ticket->new($RT::SystemUser);
+ my ( $id, undef $msg ) = $t->Create(
+ Queue => $q->id,
+ Subject => shift @subjects,
+ Requestor => [ shift @requestors ]
+ );
+ ok( $id, $msg );
+}
+
+{
+ my $tix = RT::Tickets->new($RT::SystemUser);
+ $tix->FromSQL("Queue = '$queue'");
+ is($tix->Count, 30, "found thirty tickets");
+}
+
+{
+ my $tix = RT::Tickets->new($RT::SystemUser);
+ $tix->FromSQL("Queue = '$queue' AND requestor = 'alpha\@example.com'");
+ $tix->OrderByCols({ FIELD => "Subject" });
+ my @subjects;
+ while (my $t = $tix->Next) { push @subjects, $t->Subject; }
+ is(@subjects, 6, "found six tickets");
+ is_deeply( \@subjects, [ sort @subjects ], "Subjects are sorted");
+}
+
+sub check_emails_order
+{
+ my ($tix,$count,$order) = (@_);
+ my @mails;
+ while (my $t = $tix->Next) { push @mails, $t->RequestorAddresses; }
+ is(@mails, $count, "found $count tickets for ". $tix->Query);
+ my @required_order;
+ if( $order =~ /asc/i ) {
+ @required_order = sort { $a? ($b? ($a cmp $b) : -1) : 1} @mails;
+ } else {
+ @required_order = sort { $a? ($b? ($b cmp $a) : -1) : 1} @mails;
+ }
+ foreach( reverse splice @mails ) {
+ if( $_ ) { unshift @mails, $_ }
+ else { push @mails, $_ }
+ }
+ is_deeply( \@mails, \@required_order, "Addresses are sorted");
+}
+
+{
+ my $tix = RT::Tickets->new($RT::SystemUser);
+ $tix->FromSQL("Queue = '$queue' AND subject = 'first test' AND Requestor.EmailAddress LIKE 'example.com'");
+ $tix->OrderByCols({ FIELD => "Requestor.EmailAddress" });
+ check_emails_order($tix, 5, 'ASC');
+ $tix->OrderByCols({ FIELD => "Requestor.EmailAddress", ORDER => 'DESC' });
+ check_emails_order($tix, 5, 'DESC');
+}
+
+{
+ my $tix = RT::Tickets->new($RT::SystemUser);
+ $tix->FromSQL("Queue = '$queue' AND Subject = 'first test'");
+ $tix->OrderByCols({ FIELD => "Requestor.EmailAddress" });
+ check_emails_order($tix, 6, 'ASC');
+ $tix->OrderByCols({ FIELD => "Requestor.EmailAddress", ORDER => 'DESC' });
+ check_emails_order($tix, 6, 'DESC');
+}
+
+
+{
+ my $tix = RT::Tickets->new($RT::SystemUser);
+ $tix->FromSQL("Queue = '$queue' AND Subject = 'first test'");
+ $tix->OrderByCols({ FIELD => "Requestor.EmailAddress" });
+ check_emails_order($tix, 6, 'ASC');
+ $tix->OrderByCols({ FIELD => "Requestor.EmailAddress", ORDER => 'DESC' });
+ check_emails_order($tix, 6, 'DESC');
+}
+
+{
+ # create ticket with group as member of the requestors group
+ my $t = RT::Ticket->new($RT::SystemUser);
+ my ( $id, $msg ) = $t->Create(
+ Queue => $q->id,
+ Subject => "first test",
+ Requestor => 'badaboom@example.com',
+ );
+ ok( $id, "ticket created" ) or diag( "error: $msg" );
+
+ my $g = RT::Group->new($RT::SystemUser);
+
+ my ($gid);
+ ($gid, $msg) = $g->CreateUserDefinedGroup(Name => '20-sort-by-requestor.t-'.rand(200));
+ ok($gid, "created group") or diag("error: $msg");
+
+ ($id, $msg) = $t->Requestors->AddMember( $gid );
+ ok($id, "added group to requestors group") or diag("error: $msg");
+}
+
+ my $tix = RT::Tickets->new($RT::SystemUser);
+ $tix->FromSQL("Queue = '$queue' AND Subject = 'first test'");
+
+ $tix->OrderByCols({ FIELD => "Requestor.EmailAddress" });
+ check_emails_order($tix, 7, 'ASC');
+
+ $tix->OrderByCols({ FIELD => "Requestor.EmailAddress", ORDER => 'DESC' });
+ check_emails_order($tix, 7, 'DESC');
+
+{
+ my $tix = RT::Tickets->new($RT::SystemUser);
+ $tix->FromSQL("Queue = '$queue'");
+ $tix->OrderByCols({ FIELD => "Requestor.EmailAddress" });
+ $tix->RowsPerPage(30);
+ my @mails;
+ while (my $t = $tix->Next) { push @mails, $t->RequestorAddresses; }
+ is(@mails, 30, "found thirty tickets");
+ is_deeply( [grep {$_} @mails], [ sort grep {$_} @mails ], "Paging works (exclude nulls, which are db-dependant)");
+}
+
+{
+ my $tix = RT::Tickets->new($RT::SystemUser);
+ $tix->FromSQL("Queue = '$queue'");
+ $tix->OrderByCols({ FIELD => "Requestor.EmailAddress" });
+ $tix->RowsPerPage(30);
+ my @mails;
+ while (my $t = $tix->Next) { push @mails, $t->RequestorAddresses; }
+ is(@mails, 30, "found thirty tickets");
+ is_deeply( [grep {$_} @mails], [ sort grep {$_} @mails ], "Paging works (exclude nulls, which are db-dependant)");
+}
+RT::Test->mailsent_ok(25);
+
+# vim:ft=perl:
diff --git a/rt/t/ticket/scrips_batch.t b/rt/t/ticket/scrips_batch.t
new file mode 100644
index 000000000..f558d3bd4
--- /dev/null
+++ b/rt/t/ticket/scrips_batch.t
@@ -0,0 +1,100 @@
+
+use strict;
+use warnings;
+
+use RT::Test tests => '19';
+use_ok('RT');
+use_ok('RT::Ticket');
+
+my $queue = RT::Test->load_or_create_queue( Name => 'Regression' );
+ok $queue && $queue->id, 'loaded or created queue';
+
+RT->Config->Set( UseTransactionBatch => 1 );
+
+my ($baseurl, $m) = RT::Test->started_ok;
+ok $m->login, 'logged in as root';
+
+my $sid;
+{
+ $m->follow_link_ok( { text => 'Configuration' } );
+ $m->follow_link_ok( { text => 'Queues' } );
+ $m->follow_link_ok( { text => $queue->Name } );
+ $m->follow_link_ok( { text => 'Scrips' } );
+ $m->follow_link_ok( { text => 'New scrip' } );
+ $m->form_number(3);
+ $m->field('Scrip-new-Description' => 'test');
+ $m->select('Scrip-new-ScripCondition' => 'On Transaction');
+ $m->select('Scrip-new-ScripAction' => 'User Defined');
+ $m->select('Scrip-new-Template' => 'Global template: Blank');
+ $m->select('Scrip-new-Stage' => 'TransactionBatch');
+ $m->field('Scrip-new-CustomPrepareCode' => 'return 1;');
+ $m->field('Scrip-new-CustomCommitCode' => 'return 1;');
+ $m->submit;
+ $m->content_like( qr/Scrip Created/ );
+
+ ($sid) = ($m->content =~ /Scrip\s*#(\d+)/);
+
+ my $form = $m->form_number(3);
+ is $m->value("Scrip-$sid-Description"), 'test', 'correct description';
+ is value_name($form, "Scrip-$sid-ScripCondition"), 'On Transaction', 'correct condition';
+ is value_name($form, "Scrip-$sid-ScripAction"), 'User Defined', 'correct action';
+ is value_name($form, "Scrip-$sid-Template"), 'Global template: Blank', 'correct template';
+ is value_name($form, "Scrip-$sid-Stage"), 'TransactionBatch', 'correct stage';
+
+ use File::Temp qw(tempfile);
+ my ($tmp_fh, $tmp_fn) = tempfile();
+
+ my $code = <<END;
+open my \$fh, '>', '$tmp_fn' or die "Couldn't open '$tmp_fn':\$!";
+
+my \$batch = \$self->TicketObj->TransactionBatch;
+unless ( \$batch && \@\$batch ) {
+ print \$fh "no batch\n";
+ return 1;
+}
+foreach my \$txn ( \@\$batch ) {
+ print \$fh \$txn->Type ."\n";
+}
+return 1;
+END
+
+ $m->field( "Scrip-$sid-CustomCommitCode" => $code );
+ $m->submit;
+
+ $m->goto_create_ticket( $queue );
+ $m->form_number(3);
+ $m->submit;
+
+ is_deeply parse_handle($tmp_fh), ['Create'], 'Create';
+
+ $m->follow_link_ok( { text => 'Resolve' } );
+ $m->form_number(3);
+ $m->field( "UpdateContent" => 'resolve it' );
+ $m->click('SubmitTicket');
+
+ is_deeply parse_handle($tmp_fh), ['Comment', 'Status'], 'Comment + Resolve';
+}
+
+sub value_name {
+ my $form = shift;
+ my $field = shift;
+
+ my $input = $form->find_input( $field );
+
+ my @names = $input->value_names;
+ my @values = $input->possible_values;
+ for ( my $i = 0; $i < @values; $i++ ) {
+ return $names[ $i ] if $values[ $i ] eq $input->value;
+ }
+ return undef;
+}
+
+sub parse_handle {
+ my $fh = shift;
+ seek $fh, 0, 0;
+ my @lines = <$fh>;
+ foreach ( @lines ) { s/^\s+//gms; s/\s+$//gms }
+ truncate $fh, 0;
+ return \@lines;
+}
+
diff --git a/rt/t/ticket/search.t b/rt/t/ticket/search.t
new file mode 100644
index 000000000..9cec4f753
--- /dev/null
+++ b/rt/t/ticket/search.t
@@ -0,0 +1,278 @@
+#!/opt/perl/bin/perl -w
+
+# tests relating to searching. Especially around custom fields, and
+# corner cases.
+
+use strict;
+use warnings;
+
+use RT::Test tests => 43;
+
+# setup the queue
+
+my $q = RT::Queue->new($RT::SystemUser);
+my $queue = 'SearchTests-'.$$;
+$q->Create(Name => $queue);
+ok ($q->id, "Created the queue");
+
+
+# and setup the CFs
+# we believe the Type shouldn't matter.
+
+my $cf = RT::CustomField->new($RT::SystemUser);
+$cf->Create(Name => 'SearchTest', Type => 'Freeform', MaxValues => 0, Queue => $q->id);
+ok($cf->id, "Created the SearchTest CF");
+my $cflabel = "CustomField-".$cf->id;
+
+my $cf2 = RT::CustomField->new($RT::SystemUser);
+$cf2->Create(Name => 'SearchTest2', Type => 'Freeform', MaxValues => 0, Queue => $q->id);
+ok($cf2->id, "Created the SearchTest2 CF");
+my $cflabel2 = "CustomField-".$cf2->id;
+
+my $cf3 = RT::CustomField->new($RT::SystemUser);
+$cf3->Create(Name => 'SearchTest3', Type => 'Freeform', MaxValues => 0, Queue => $q->id);
+ok($cf3->id, "Created the SearchTest3 CF");
+my $cflabel3 = "CustomField-".$cf3->id;
+
+
+# There was a bug involving a missing join to ObjectCustomFields that
+# caused spurious results on negative searches if another custom field
+# with the same name existed on a different queue. Hence, we make
+# duplicate CFs on a different queue here
+my $dup = RT::Queue->new($RT::SystemUser);
+$dup->Create(Name => $queue . "-Copy");
+ok ($dup->id, "Created the duplicate queue");
+my $dupcf = RT::CustomField->new($RT::SystemUser);
+$dupcf->Create(Name => 'SearchTest', Type => 'Freeform', MaxValues => 0, Queue => $dup->id);
+ok($dupcf->id, "Created the duplicate SearchTest CF");
+$dupcf = RT::CustomField->new($RT::SystemUser);
+$dupcf->Create(Name => 'SearchTest2', Type => 'Freeform', MaxValues => 0, Queue => $dup->id);
+ok($dupcf->id, "Created the SearchTest2 CF");
+$dupcf = RT::CustomField->new($RT::SystemUser);
+$dupcf->Create(Name => 'SearchTest3', Type => 'Freeform', MaxValues => 0, Queue => $dup->id);
+ok($dupcf->id, "Created the SearchTest3 CF");
+
+
+# setup some tickets
+# we'll need a small pile of them, to test various combinations and nulls.
+# there's probably a way to think harder and do this with fewer
+
+
+my $t1 = RT::Ticket->new($RT::SystemUser);
+my ( $id, undef $msg ) = $t1->Create(
+ Queue => $q->id,
+ Subject => 'SearchTest1',
+ Requestor => ['search1@example.com'],
+ $cflabel => 'foo1',
+ $cflabel2 => 'bar1',
+ $cflabel3 => 'qux1',
+);
+ok( $id, $msg );
+
+
+my $t2 = RT::Ticket->new($RT::SystemUser);
+( $id, undef, $msg ) = $t2->Create(
+ Queue => $q->id,
+ Subject => 'SearchTest2',
+ Requestor => ['search2@example.com'],
+# $cflabel => 'foo2',
+ $cflabel2 => 'bar2',
+ $cflabel3 => 'qux2',
+);
+ok( $id, $msg );
+
+my $t3 = RT::Ticket->new($RT::SystemUser);
+( $id, undef, $msg ) = $t3->Create(
+ Queue => $q->id,
+ Subject => 'SearchTest3',
+ Requestor => ['search3@example.com'],
+ $cflabel => 'foo3',
+# $cflabel2 => 'bar3',
+ $cflabel3 => 'qux3',
+);
+ok( $id, $msg );
+
+my $t4 = RT::Ticket->new($RT::SystemUser);
+( $id, undef, $msg ) = $t4->Create(
+ Queue => $q->id,
+ Subject => 'SearchTest4',
+ Requestor => ['search4@example.com'],
+ $cflabel => 'foo4',
+ $cflabel2 => 'bar4',
+# $cflabel3 => 'qux4',
+);
+ok( $id, $msg );
+
+my $t5 = RT::Ticket->new($RT::SystemUser);
+( $id, undef, $msg ) = $t5->Create(
+ Queue => $q->id,
+# Subject => 'SearchTest5',
+ Requestor => ['search5@example.com'],
+ $cflabel => 'foo5',
+ $cflabel2 => 'bar5',
+ $cflabel3 => 'qux5',
+);
+ok( $id, $msg );
+
+my $t6 = RT::Ticket->new($RT::SystemUser);
+( $id, undef, $msg ) = $t6->Create(
+ Queue => $q->id,
+ Subject => 'SearchTest6',
+# Requestor => ['search6@example.com'],
+ $cflabel => 'foo6',
+ $cflabel2 => 'bar6',
+ $cflabel3 => 'qux6',
+);
+ok( $id, $msg );
+
+my $t7 = RT::Ticket->new($RT::SystemUser);
+( $id, undef, $msg ) = $t7->Create(
+ Queue => $q->id,
+ Subject => 'SearchTest7',
+ Requestor => ['search7@example.com'],
+# $cflabel => 'foo7',
+# $cflabel2 => 'bar7',
+ $cflabel3 => 'qux7',
+);
+ok( $id, $msg );
+
+# we have tickets. start searching
+my $tix = RT::Tickets->new($RT::SystemUser);
+$tix->FromSQL("Queue = '$queue'");
+is($tix->Count, 7, "found all the tickets")
+ or diag "wrong results from SQL:\n". $tix->BuildSelectCountQuery;
+
+
+# very simple searches. both CF and normal
+
+$tix = RT::Tickets->new($RT::SystemUser);
+$tix->FromSQL("Queue = '$queue' AND CF.SearchTest = 'foo1'");
+is($tix->Count, 1, "matched identical subject")
+ or diag "wrong results from SQL:\n". $tix->BuildSelectCountQuery;
+
+$tix = RT::Tickets->new($RT::SystemUser);
+$tix->FromSQL("Queue = '$queue' AND CF.SearchTest LIKE 'foo1'");
+is($tix->Count, 1, "matched LIKE subject")
+ or diag "wrong results from SQL:\n". $tix->BuildSelectCountQuery;
+
+$tix = RT::Tickets->new($RT::SystemUser);
+$tix->FromSQL("Queue = '$queue' AND CF.SearchTest = 'foo'");
+is($tix->Count, 0, "IS a regexp match")
+ or diag "wrong results from SQL:\n". $tix->BuildSelectCountQuery;
+
+$tix = RT::Tickets->new($RT::SystemUser);
+$tix->FromSQL("Queue = '$queue' AND CF.SearchTest LIKE 'foo'");
+is($tix->Count, 5, "matched LIKE subject")
+ or diag "wrong results from SQL:\n". $tix->BuildSelectCountQuery;
+
+
+$tix = RT::Tickets->new($RT::SystemUser);
+$tix->FromSQL("Queue = '$queue' AND CF.SearchTest IS NULL");
+is($tix->Count, 2, "IS null CF")
+ or diag "wrong results from SQL:\n". $tix->BuildSelectCountQuery;
+
+$tix = RT::Tickets->new($RT::SystemUser);
+$tix->FromSQL("Queue = '$queue' AND Requestors LIKE 'search1'");
+is($tix->Count, 1, "LIKE requestor")
+ or diag "wrong results from SQL:\n". $tix->BuildSelectCountQuery;
+
+$tix = RT::Tickets->new($RT::SystemUser);
+$tix->FromSQL("Queue = '$queue' AND Requestors = 'search1\@example.com'");
+is($tix->Count, 1, "IS requestor")
+ or diag "wrong results from SQL:\n". $tix->BuildSelectCountQuery;
+
+$tix = RT::Tickets->new($RT::SystemUser);
+$tix->FromSQL("Queue = '$queue' AND Requestors LIKE 'search'");
+is($tix->Count, 6, "LIKE requestor")
+ or diag "wrong results from SQL:\n". $tix->BuildSelectCountQuery;
+
+$tix = RT::Tickets->new($RT::SystemUser);
+$tix->FromSQL("Queue = '$queue' AND Requestors IS NULL");
+is($tix->Count, 1, "Search for no requestor")
+ or diag "wrong results from SQL:\n". $tix->BuildSelectCountQuery;
+
+$tix = RT::Tickets->new($RT::SystemUser);
+$tix->FromSQL("Queue = '$queue' AND Subject = 'SearchTest1'");
+is($tix->Count, 1, "IS subject")
+ or diag "wrong results from SQL:\n". $tix->BuildSelectCountQuery;
+
+$tix = RT::Tickets->new($RT::SystemUser);
+$tix->FromSQL("Queue = '$queue' AND Subject LIKE 'SearchTest1'");
+is($tix->Count, 1, "LIKE subject")
+ or diag "wrong results from SQL:\n". $tix->BuildSelectCountQuery;
+
+$tix = RT::Tickets->new($RT::SystemUser);
+$tix->FromSQL("Queue = '$queue' AND Subject = ''");
+is($tix->Count, 1, "found one ticket")
+ or diag "wrong results from SQL:\n". $tix->BuildSelectCountQuery;
+
+$tix = RT::Tickets->new($RT::SystemUser);
+$tix->FromSQL("Queue = '$queue' AND Subject LIKE 'SearchTest'");
+is($tix->Count, 6, "found two ticket")
+ or diag "wrong results from SQL:\n". $tix->BuildSelectCountQuery;
+
+$tix = RT::Tickets->new($RT::SystemUser);
+$tix->FromSQL("Queue = '$queue' AND Subject LIKE 'qwerty'");
+is($tix->Count, 0, "found zero ticket")
+ or diag "wrong results from SQL:\n". $tix->BuildSelectCountQuery;
+
+
+
+
+# various combinations
+
+$tix = RT::Tickets->new($RT::SystemUser);
+$tix->FromSQL("CF.SearchTest LIKE 'foo' AND CF.SearchTest2 LIKE 'bar1'");
+is($tix->Count, 1, "LIKE cf and LIKE cf");
+
+$tix = RT::Tickets->new($RT::SystemUser);
+$tix->FromSQL("CF.SearchTest = 'foo1' AND CF.SearchTest2 = 'bar1'");
+is($tix->Count, 1, "is cf and is cf");
+
+$tix = RT::Tickets->new($RT::SystemUser);
+$tix->FromSQL("CF.SearchTest = 'foo' AND CF.SearchTest2 LIKE 'bar1'");
+is($tix->Count, 0, "is cf and like cf");
+
+$tix = RT::Tickets->new($RT::SystemUser);
+$tix->FromSQL("CF.SearchTest LIKE 'foo' AND CF.SearchTest2 LIKE 'bar' AND CF.SearchTest3 LIKE 'qux'");
+is($tix->Count, 3, "like cf and like cf and like cf");
+
+$tix = RT::Tickets->new($RT::SystemUser);
+$tix->FromSQL("CF.SearchTest LIKE 'foo' AND CF.SearchTest2 LIKE 'bar' AND CF.SearchTest3 LIKE 'qux6'");
+is($tix->Count, 1, "like cf and like cf and is cf");
+
+$tix = RT::Tickets->new($RT::SystemUser);
+$tix->FromSQL("CF.SearchTest LIKE 'foo' AND Subject LIKE 'SearchTest'");
+is($tix->Count, 4, "like cf and like subject");
+
+$tix = RT::Tickets->new($RT::SystemUser);
+$tix->FromSQL("CF.SearchTest IS NULL AND CF.SearchTest2 = 'bar2'");
+is($tix->Count, 1, "null cf and is cf");
+
+
+$tix = RT::Tickets->new($RT::SystemUser);
+$tix->FromSQL("Queue = '$queue' AND CF.SearchTest IS NULL AND CF.SearchTest2 IS NULL");
+is($tix->Count, 1, "null cf and null cf");
+
+# tests with the same CF listed twice
+
+$tix = RT::Tickets->new($RT::SystemUser);
+$tix->FromSQL("CF.{SearchTest} = 'foo1'");
+is($tix->Count, 1, "is cf.{name} format");
+
+$tix = RT::Tickets->new($RT::SystemUser);
+$tix->FromSQL("CF.SearchTest = 'foo1' OR CF.SearchTest = 'foo3'");
+is($tix->Count, 2, "is cf1 or is cf1");
+
+$tix = RT::Tickets->new($RT::SystemUser);
+$tix->FromSQL("CF.SearchTest = 'foo1' OR CF.SearchTest IS NULL");
+is($tix->Count, 3, "is cf1 or null cf1");
+
+$tix = RT::Tickets->new($RT::SystemUser);
+$tix->FromSQL("(CF.SearchTest = 'foo1' OR CF.SearchTest = 'foo3') AND (CF.SearchTest2 = 'bar1' OR CF.SearchTest2 = 'bar2')");
+is($tix->Count, 1, "(is cf1 or is cf1) and (is cf2 or is cf2)");
+
+$tix = RT::Tickets->new($RT::SystemUser);
+$tix->FromSQL("CF.SearchTest = 'foo1' OR CF.SearchTest = 'foo3' OR CF.SearchTest2 = 'bar1' OR CF.SearchTest2 = 'bar2'");
+is($tix->Count, 3, "is cf1 or is cf1 or is cf2 or is cf2");
+
diff --git a/rt/t/ticket/search_by_cf_freeform_multiple.t b/rt/t/ticket/search_by_cf_freeform_multiple.t
new file mode 100644
index 000000000..be5130651
--- /dev/null
+++ b/rt/t/ticket/search_by_cf_freeform_multiple.t
@@ -0,0 +1,153 @@
+#!/usr/bin/perl -w
+
+use strict;
+use warnings;
+
+use RT::Test tests => 105;
+use RT::Ticket;
+
+my $q = RT::Test->load_or_create_queue( Name => 'Regression' );
+ok $q && $q->id, 'loaded or created queue';
+my $queue = $q->Name;
+
+diag "create a CF\n" if $ENV{TEST_VERBOSE};
+my ($cf_name, $cf_id, $cf) = ("Test", 0, undef);
+{
+ $cf = RT::CustomField->new( $RT::SystemUser );
+ my ($ret, $msg) = $cf->Create(
+ Name => $cf_name,
+ Queue => $q->id,
+ Type => 'FreeformMultiple',
+ );
+ ok($ret, "Custom Field Order created");
+ $cf_id = $cf->id;
+}
+
+my ($total, @data, @tickets, %test) = (0, ());
+
+sub add_tix_from_data {
+ my @res = ();
+ while (@data) {
+ my %args = %{ shift(@data) };
+ my @cf_value = $args{'Subject'} ne '-'? (split /(?=.)/, $args{'Subject'}) : ();
+ diag "vals: ". join ', ', @cf_value;
+ my $t = RT::Ticket->new($RT::SystemUser);
+ my ( $id, undef $msg ) = $t->Create(
+ Queue => $q->id,
+ %args,
+ "CustomField-$cf_id" => \@cf_value,
+ );
+ ok( $id, "ticket created" ) or diag("error: $msg");
+
+ my $got = join ',', sort do {
+ my $vals = $t->CustomFieldValues( $cf_name );
+ my @tmp;
+ while (my $v = $vals->Next ) { push @tmp, $v->Content }
+ @tmp;
+ };
+
+ is( $got, join( ',', sort @cf_value), 'correct CF values' );
+ push @res, $t;
+ $total++;
+ }
+ return @res;
+}
+
+sub run_tests {
+ my $query_prefix = join ' OR ', map 'id = '. $_->id, @tickets;
+ foreach my $key ( sort keys %test ) {
+ my $tix = RT::Tickets->new($RT::SystemUser);
+ $tix->FromSQL( "( $query_prefix ) AND ( $key )" );
+
+ my $error = 0;
+
+ my $count = 0;
+ $count++ foreach grep $_, values %{ $test{$key} };
+ is($tix->Count, $count, "found correct number of ticket(s) by '$key'") or $error = 1;
+
+ my $good_tickets = ($tix->Count == $count);
+ while ( my $ticket = $tix->Next ) {
+ next if $test{$key}->{ $ticket->Subject };
+ diag $ticket->Subject ." ticket has been found when it's not expected";
+ $good_tickets = 0;
+ }
+ ok( $good_tickets, "all tickets are good with '$key'" ) or $error = 1;
+
+ diag "Wrong SQL query for '$key':". $tix->BuildSelectQuery if $error;
+ }
+}
+
+@data = (
+ { Subject => '-' },
+ { Subject => 'x' },
+ { Subject => 'y' },
+ { Subject => 'z' },
+ { Subject => 'xy' },
+ { Subject => 'xz' },
+ { Subject => 'yz' },
+);
+%test = (
+ "CF.{$cf_id} IS NULL" => { '-' => 1, x => 0, y => 0, z => 0, xy => 0, xz => 0, yz => 0 },
+ "'CF.{$cf_name}' IS NULL" => { '-' => 1, x => 0, y => 0, z => 0, xy => 0, xz => 0, yz => 0 },
+ "'CF.$queue.{$cf_id}' IS NULL" => { '-' => 1, x => 0, y => 0, z => 0, xy => 0, xz => 0, yz => 0 },
+ "'CF.$queue.{$cf_name}' IS NULL" => { '-' => 1, x => 0, y => 0, z => 0, xy => 0, xz => 0, yz => 0 },
+
+ "CF.{$cf_id} IS NOT NULL" => { '-' => 0, x => 1, y => 1, z => 1, xy => 1, xz => 1, yz => 1 },
+ "'CF.{$cf_name}' IS NOT NULL" => { '-' => 0, x => 1, y => 1, z => 1, xy => 1, xz => 1, yz => 1 },
+ "'CF.$queue.{$cf_id}' IS NOT NULL" => { '-' => 0, x => 1, y => 1, z => 1, xy => 1, xz => 1, yz => 1 },
+ "'CF.$queue.{$cf_name}' IS NOT NULL" => { '-' => 0, x => 1, y => 1, z => 1, xy => 1, xz => 1, yz => 1 },
+
+ "CF.{$cf_id} = 'x'" => { '-' => 0, x => 1, y => 0, z => 0, xy => 1, xz => 1, yz => 0 },
+ "'CF.{$cf_name}' = 'x'" => { '-' => 0, x => 1, y => 0, z => 0, xy => 1, xz => 1, yz => 0 },
+ "'CF.$queue.{$cf_id}' = 'x'" => { '-' => 0, x => 1, y => 0, z => 0, xy => 1, xz => 1, yz => 0 },
+ "'CF.$queue.{$cf_name}' = 'x'" => { '-' => 0, x => 1, y => 0, z => 0, xy => 1, xz => 1, yz => 0 },
+
+ "CF.{$cf_id} != 'x'" => { '-' => 1, x => 0, y => 1, z => 1, xy => 0, xz => 0, yz => 1 },
+ "'CF.{$cf_name}' != 'x'" => { '-' => 1, x => 0, y => 1, z => 1, xy => 0, xz => 0, yz => 1 },
+ "'CF.$queue.{$cf_id}' != 'x'" => { '-' => 1, x => 0, y => 1, z => 1, xy => 0, xz => 0, yz => 1 },
+ "'CF.$queue.{$cf_name}' != 'x'" => { '-' => 1, x => 0, y => 1, z => 1, xy => 0, xz => 0, yz => 1 },
+
+ "CF.{$cf_id} = 'x' OR CF.{$cf_id} = 'y'" => { '-' => 0, x => 1, y => 1, z => 0, xy => 1, xz => 1, yz => 1 },
+ "'CF.{$cf_name}' = 'x' OR 'CF.{$cf_name}' = 'y'" => { '-' => 0, x => 1, y => 1, z => 0, xy => 1, xz => 1, yz => 1 },
+ "'CF.$queue.{$cf_id}' = 'x' OR 'CF.$queue.{$cf_id}' = 'y'" => { '-' => 0, x => 1, y => 1, z => 0, xy => 1, xz => 1, yz => 1 },
+ "'CF.$queue.{$cf_name}' = 'x' OR 'CF.$queue.{$cf_name}' = 'y'" => { '-' => 0, x => 1, y => 1, z => 0, xy => 1, xz => 1, yz => 1 },
+
+ "CF.{$cf_id} = 'x' AND CF.{$cf_id} = 'y'" => { '-' => 0, x => 0, y => 0, z => 0, xy => 1, xz => 0, yz => 0 },
+ "'CF.{$cf_name}' = 'x' AND 'CF.{$cf_name}' = 'y'" => { '-' => 0, x => 0, y => 0, z => 0, xy => 1, xz => 0, yz => 0 },
+ "'CF.$queue.{$cf_id}' = 'x' AND 'CF.$queue.{$cf_id}' = 'y'" => { '-' => 0, x => 0, y => 0, z => 0, xy => 1, xz => 0, yz => 0 },
+ "'CF.$queue.{$cf_name}' = 'x' AND 'CF.$queue.{$cf_name}' = 'y'" => { '-' => 0, x => 0, y => 0, z => 0, xy => 1, xz => 0, yz => 0 },
+
+ "CF.{$cf_id} != 'x' AND CF.{$cf_id} != 'y'" => { '-' => 1, x => 0, y => 0, z => 1, xy => 0, xz => 0, yz => 0 },
+ "'CF.{$cf_name}' != 'x' AND 'CF.{$cf_name}' != 'y'" => { '-' => 1, x => 0, y => 0, z => 1, xy => 0, xz => 0, yz => 0 },
+ "'CF.$queue.{$cf_id}' != 'x' AND 'CF.$queue.{$cf_id}' != 'y'" => { '-' => 1, x => 0, y => 0, z => 1, xy => 0, xz => 0, yz => 0 },
+ "'CF.$queue.{$cf_name}' != 'x' AND 'CF.$queue.{$cf_name}' != 'y'" => { '-' => 1, x => 0, y => 0, z => 1, xy => 0, xz => 0, yz => 0 },
+
+ "CF.{$cf_id} = 'x' AND CF.{$cf_id} IS NULL" => { '-' => 0, x => 0, y => 0, z => 0, xy => 0, xz => 0, yz => 0 },
+ "'CF.{$cf_name}' = 'x' AND 'CF.{$cf_name}' IS NULL" => { '-' => 0, x => 0, y => 0, z => 0, xy => 0, xz => 0, yz => 0 },
+ "'CF.$queue.{$cf_id}' = 'x' AND 'CF.$queue.{$cf_id}' IS NULL" => { '-' => 0, x => 0, y => 0, z => 0, xy => 0, xz => 0, yz => 0 },
+ "'CF.$queue.{$cf_name}' = 'x' AND 'CF.$queue.{$cf_name}' IS NULL" => { '-' => 0, x => 0, y => 0, z => 0, xy => 0, xz => 0, yz => 0 },
+
+ "CF.{$cf_id} = 'x' OR CF.{$cf_id} IS NULL" => { '-' => 1, x => 1, y => 0, z => 0, xy => 1, xz => 1, yz => 0 },
+ "'CF.{$cf_name}' = 'x' OR 'CF.{$cf_name}' IS NULL" => { '-' => 1, x => 1, y => 0, z => 0, xy => 1, xz => 1, yz => 0 },
+ "'CF.$queue.{$cf_id}' = 'x' OR 'CF.$queue.{$cf_id}' IS NULL" => { '-' => 1, x => 1, y => 0, z => 0, xy => 1, xz => 1, yz => 0 },
+ "'CF.$queue.{$cf_name}' = 'x' OR 'CF.$queue.{$cf_name}' IS NULL" => { '-' => 1, x => 1, y => 0, z => 0, xy => 1, xz => 1, yz => 0 },
+
+ "CF.{$cf_id} = 'x' AND CF.{$cf_id} IS NOT NULL" => { '-' => 0, x => 1, y => 0, z => 0, xy => 1, xz => 1, yz => 0 },
+ "'CF.{$cf_name}' = 'x' AND 'CF.{$cf_name}' IS NOT NULL" => { '-' => 0, x => 1, y => 0, z => 0, xy => 1, xz => 1, yz => 0 },
+ "'CF.$queue.{$cf_id}' = 'x' AND 'CF.$queue.{$cf_id}' IS NOT NULL" => { '-' => 0, x => 1, y => 0, z => 0, xy => 1, xz => 1, yz => 0 },
+ "'CF.$queue.{$cf_name}' = 'x' AND 'CF.$queue.{$cf_name}' IS NOT NULL" => { '-' => 0, x => 1, y => 0, z => 0, xy => 1, xz => 1, yz => 0 },
+
+ "CF.{$cf_id} = 'x' OR CF.{$cf_id} IS NOT NULL" => { '-' => 0, x => 1, y => 1, z => 1, xy => 1, xz => 1, yz => 1 },
+ "'CF.{$cf_name}' = 'x' OR 'CF.{$cf_name}' IS NOT NULL" => { '-' => 0, x => 1, y => 1, z => 1, xy => 1, xz => 1, yz => 1 },
+ "'CF.$queue.{$cf_id}' = 'x' OR 'CF.$queue.{$cf_id}' IS NOT NULL" => { '-' => 0, x => 1, y => 1, z => 1, xy => 1, xz => 1, yz => 1 },
+ "'CF.$queue.{$cf_name}' = 'x' OR 'CF.$queue.{$cf_name}' IS NOT NULL" => { '-' => 0, x => 1, y => 1, z => 1, xy => 1, xz => 1, yz => 1 },
+);
+@tickets = add_tix_from_data();
+{
+ my $tix = RT::Tickets->new($RT::SystemUser);
+ $tix->FromSQL("Queue = '$queue'");
+ is($tix->Count, $total, "found $total tickets");
+}
+run_tests();
+
+exit 0;
diff --git a/rt/t/ticket/search_by_cf_freeform_single.t b/rt/t/ticket/search_by_cf_freeform_single.t
new file mode 100644
index 000000000..d5ff7ec0d
--- /dev/null
+++ b/rt/t/ticket/search_by_cf_freeform_single.t
@@ -0,0 +1,142 @@
+#!/usr/bin/perl -w
+
+use strict;
+use warnings;
+
+use RT::Test tests => 99;
+use RT::Ticket;
+
+my $q = RT::Test->load_or_create_queue( Name => 'Regression' );
+ok $q && $q->id, 'loaded or created queue';
+my $queue = $q->Name;
+
+diag "create a CF\n" if $ENV{TEST_VERBOSE};
+my ($cf_name, $cf_id, $cf) = ("Test", 0, undef);
+{
+ $cf = RT::CustomField->new( $RT::SystemUser );
+ my ($ret, $msg) = $cf->Create(
+ Name => $cf_name,
+ Queue => $q->id,
+ Type => 'FreeformSingle',
+ );
+ ok($ret, "Custom Field Order created");
+ $cf_id = $cf->id;
+}
+
+my ($total, @data, @tickets, %test) = (0, ());
+
+sub add_tix_from_data {
+ my @res = ();
+ while (@data) {
+ my %args = %{ shift(@data) };
+ my $cf_value = $args{'Subject'} ne '-'? $args{'Subject'} : undef;
+ my $t = RT::Ticket->new($RT::SystemUser);
+ my ( $id, undef $msg ) = $t->Create(
+ Queue => $q->id,
+ %args,
+ "CustomField-$cf_id" => $cf_value,
+ );
+ ok( $id, "ticket created" ) or diag("error: $msg");
+ is( $t->FirstCustomFieldValue( $cf_name ), $cf_value, 'correct value' );
+ push @res, $t;
+ $total++;
+ }
+ return @res;
+}
+
+sub run_tests {
+ my $query_prefix = join ' OR ', map 'id = '. $_->id, @tickets;
+ foreach my $key ( sort keys %test ) {
+ my $tix = RT::Tickets->new($RT::SystemUser);
+ $tix->FromSQL( "( $query_prefix ) AND ( $key )" );
+
+ my $error = 0;
+
+ my $count = 0;
+ $count++ foreach grep $_, values %{ $test{$key} };
+ is($tix->Count, $count, "found correct number of ticket(s) by '$key'") or $error = 1;
+
+ my $good_tickets = ($tix->Count == $count);
+ while ( my $ticket = $tix->Next ) {
+ next if $test{$key}->{ $ticket->Subject };
+ diag $ticket->Subject ." ticket has been found when it's not expected";
+ $good_tickets = 0;
+ }
+ ok( $good_tickets, "all tickets are good with '$key'" ) or $error = 1;
+
+ diag "Wrong SQL query for '$key':". $tix->BuildSelectQuery if $error;
+ }
+}
+
+@data = (
+ { Subject => '-' },
+ { Subject => 'x' },
+ { Subject => 'y' },
+ { Subject => 'z' },
+);
+%test = (
+ "CF.{$cf_id} IS NULL" => { '-' => 1, x => 0, y => 0, z => 0 },
+ "'CF.{$cf_name}' IS NULL" => { '-' => 1, x => 0, y => 0, z => 0 },
+ "'CF.$queue.{$cf_id}' IS NULL" => { '-' => 1, x => 0, y => 0, z => 0 },
+ "'CF.$queue.{$cf_name}' IS NULL" => { '-' => 1, x => 0, y => 0, z => 0 },
+
+ "CF.{$cf_id} IS NOT NULL" => { '-' => 0, x => 1, y => 1, z => 1 },
+ "'CF.{$cf_name}' IS NOT NULL" => { '-' => 0, x => 1, y => 1, z => 1 },
+ "'CF.$queue.{$cf_id}' IS NOT NULL" => { '-' => 0, x => 1, y => 1, z => 1 },
+ "'CF.$queue.{$cf_name}' IS NOT NULL" => { '-' => 0, x => 1, y => 1, z => 1 },
+
+ "CF.{$cf_id} = 'x'" => { '-' => 0, x => 1, y => 0, z => 0 },
+ "'CF.{$cf_name}' = 'x'" => { '-' => 0, x => 1, y => 0, z => 0 },
+ "'CF.$queue.{$cf_id}' = 'x'" => { '-' => 0, x => 1, y => 0, z => 0 },
+ "'CF.$queue.{$cf_name}' = 'x'" => { '-' => 0, x => 1, y => 0, z => 0 },
+
+ "CF.{$cf_id} != 'x'" => { '-' => 1, x => 0, y => 1, z => 1 },
+ "'CF.{$cf_name}' != 'x'" => { '-' => 1, x => 0, y => 1, z => 1 },
+ "'CF.$queue.{$cf_id}' != 'x'" => { '-' => 1, x => 0, y => 1, z => 1 },
+ "'CF.$queue.{$cf_name}' != 'x'" => { '-' => 1, x => 0, y => 1, z => 1 },
+
+ "CF.{$cf_id} = 'x' OR CF.{$cf_id} = 'y'" => { '-' => 0, x => 1, y => 1, z => 0 },
+ "'CF.{$cf_name}' = 'x' OR 'CF.{$cf_name}' = 'y'" => { '-' => 0, x => 1, y => 1, z => 0 },
+ "'CF.$queue.{$cf_id}' = 'x' OR 'CF.$queue.{$cf_id}' = 'y'" => { '-' => 0, x => 1, y => 1, z => 0 },
+ "'CF.$queue.{$cf_name}' = 'x' OR 'CF.$queue.{$cf_name}' = 'y'" => { '-' => 0, x => 1, y => 1, z => 0 },
+
+ "CF.{$cf_id} = 'x' AND CF.{$cf_id} = 'y'" => { '-' => 0, x => 0, y => 0, z => 0 },
+ "'CF.{$cf_name}' = 'x' AND 'CF.{$cf_name}' = 'y'" => { '-' => 0, x => 0, y => 0, z => 0 },
+ "'CF.$queue.{$cf_id}' = 'x' AND 'CF.$queue.{$cf_id}' = 'y'" => { '-' => 0, x => 0, y => 0, z => 0 },
+ "'CF.$queue.{$cf_name}' = 'x' AND 'CF.$queue.{$cf_name}' = 'y'" => { '-' => 0, x => 0, y => 0, z => 0 },
+
+ "CF.{$cf_id} != 'x' AND CF.{$cf_id} != 'y'" => { '-' => 1, x => 0, y => 0, z => 1 },
+ "'CF.{$cf_name}' != 'x' AND 'CF.{$cf_name}' != 'y'" => { '-' => 1, x => 0, y => 0, z => 1 },
+ "'CF.$queue.{$cf_id}' != 'x' AND 'CF.$queue.{$cf_id}' != 'y'" => { '-' => 1, x => 0, y => 0, z => 1 },
+ "'CF.$queue.{$cf_name}' != 'x' AND 'CF.$queue.{$cf_name}' != 'y'" => { '-' => 1, x => 0, y => 0, z => 1 },
+
+ "CF.{$cf_id} = 'x' AND CF.{$cf_id} IS NULL" => { '-' => 0, x => 0, y => 0, z => 0 },
+ "'CF.{$cf_name}' = 'x' AND 'CF.{$cf_name}' IS NULL" => { '-' => 0, x => 0, y => 0, z => 0 },
+ "'CF.$queue.{$cf_id}' = 'x' AND 'CF.$queue.{$cf_id}' IS NULL" => { '-' => 0, x => 0, y => 0, z => 0 },
+ "'CF.$queue.{$cf_name}' = 'x' AND 'CF.$queue.{$cf_name}' IS NULL" => { '-' => 0, x => 0, y => 0, z => 0 },
+
+ "CF.{$cf_id} = 'x' OR CF.{$cf_id} IS NULL" => { '-' => 1, x => 1, y => 0, z => 0 },
+ "'CF.{$cf_name}' = 'x' OR 'CF.{$cf_name}' IS NULL" => { '-' => 1, x => 1, y => 0, z => 0 },
+ "'CF.$queue.{$cf_id}' = 'x' OR 'CF.$queue.{$cf_id}' IS NULL" => { '-' => 1, x => 1, y => 0, z => 0 },
+ "'CF.$queue.{$cf_name}' = 'x' OR 'CF.$queue.{$cf_name}' IS NULL" => { '-' => 1, x => 1, y => 0, z => 0 },
+
+ "CF.{$cf_id} = 'x' AND CF.{$cf_id} IS NOT NULL" => { '-' => 0, x => 1, y => 0, z => 0 },
+ "'CF.{$cf_name}' = 'x' AND 'CF.{$cf_name}' IS NOT NULL" => { '-' => 0, x => 1, y => 0, z => 0 },
+ "'CF.$queue.{$cf_id}' = 'x' AND 'CF.$queue.{$cf_id}' IS NOT NULL" => { '-' => 0, x => 1, y => 0, z => 0 },
+ "'CF.$queue.{$cf_name}' = 'x' AND 'CF.$queue.{$cf_name}' IS NOT NULL" => { '-' => 0, x => 1, y => 0, z => 0 },
+
+ "CF.{$cf_id} = 'x' OR CF.{$cf_id} IS NOT NULL" => { '-' => 0, x => 1, y => 1, z => 1 },
+ "'CF.{$cf_name}' = 'x' OR 'CF.{$cf_name}' IS NOT NULL" => { '-' => 0, x => 1, y => 1, z => 1 },
+ "'CF.$queue.{$cf_id}' = 'x' OR 'CF.$queue.{$cf_id}' IS NOT NULL" => { '-' => 0, x => 1, y => 1, z => 1 },
+ "'CF.$queue.{$cf_name}' = 'x' OR 'CF.$queue.{$cf_name}' IS NOT NULL" => { '-' => 0, x => 1, y => 1, z => 1 },
+
+);
+@tickets = add_tix_from_data();
+{
+ my $tix = RT::Tickets->new($RT::SystemUser);
+ $tix->FromSQL("Queue = '$queue'");
+ is($tix->Count, $total, "found $total tickets");
+}
+run_tests();
+
+exit 0;
diff --git a/rt/t/ticket/search_by_links.t b/rt/t/ticket/search_by_links.t
new file mode 100644
index 000000000..a8e955c8b
--- /dev/null
+++ b/rt/t/ticket/search_by_links.t
@@ -0,0 +1,132 @@
+#!/usr/bin/perl -w
+
+use strict;
+use warnings;
+
+use RT::Test tests => 80;
+use RT::Ticket;
+
+my $q = RT::Test->load_or_create_queue( Name => 'Regression' );
+ok $q && $q->id, 'loaded or created queue';
+
+my ($total, @data, @tickets, %test) = (0, ());
+
+sub add_tix_from_data {
+ my @res = ();
+ while (@data) {
+ my $t = RT::Ticket->new($RT::SystemUser);
+ my %args = %{ shift(@data) };
+ $args{$_} = $res[ $args{$_} ]->id foreach grep $args{$_}, keys %RT::Ticket::LINKTYPEMAP;
+ my ( $id, undef $msg ) = $t->Create(
+ Queue => $q->id,
+ %args,
+ );
+ ok( $id, "ticket created" ) or diag("error: $msg");
+ push @res, $t;
+ $total++;
+ }
+ return @res;
+}
+
+sub run_tests {
+ my $query_prefix = join ' OR ', map 'id = '. $_->id, @tickets;
+ foreach my $key ( sort keys %test ) {
+ my $tix = RT::Tickets->new($RT::SystemUser);
+ $tix->FromSQL( "( $query_prefix ) AND ( $key )" );
+
+ my $error = 0;
+
+ my $count = 0;
+ $count++ foreach grep $_, values %{ $test{$key} };
+ is($tix->Count, $count, "found correct number of ticket(s) by '$key'") or $error = 1;
+
+ my $good_tickets = 1;
+ while ( my $ticket = $tix->Next ) {
+ next if $test{$key}->{ $ticket->Subject };
+ diag $ticket->Subject ." ticket has been found when it's not expected";
+ $good_tickets = 0;
+ }
+ ok( $good_tickets, "all tickets are good with '$key'" ) or $error = 1;
+
+ diag "Wrong SQL query for '$key':". $tix->BuildSelectQuery if $error;
+ }
+}
+
+# simple set with "no links", "parent and child"
+@data = (
+ { Subject => '-', },
+ { Subject => 'p', },
+ { Subject => 'c', MemberOf => -1 },
+);
+@tickets = add_tix_from_data();
+%test = (
+ 'Linked IS NOT NULL' => { '-' => 0, c => 1, p => 1 },
+ 'Linked IS NULL' => { '-' => 1, c => 0, p => 0 },
+ 'LinkedTo IS NOT NULL' => { '-' => 0, c => 1, p => 0 },
+ 'LinkedTo IS NULL' => { '-' => 1, c => 0, p => 1 },
+ 'LinkedFrom IS NOT NULL' => { '-' => 0, c => 0, p => 1 },
+ 'LinkedFrom IS NULL' => { '-' => 1, c => 1, p => 0 },
+
+ 'HasMember IS NOT NULL' => { '-' => 0, c => 0, p => 1 },
+ 'HasMember IS NULL' => { '-' => 1, c => 1, p => 0 },
+ 'MemberOf IS NOT NULL' => { '-' => 0, c => 1, p => 0 },
+ 'MemberOf IS NULL' => { '-' => 1, c => 0, p => 1 },
+
+ 'RefersTo IS NOT NULL' => { '-' => 0, c => 0, p => 0 },
+ 'RefersTo IS NULL' => { '-' => 1, c => 1, p => 1 },
+
+ 'Linked = '. $tickets[0]->id => { '-' => 0, c => 0, p => 0 },
+ 'Linked != '. $tickets[0]->id => { '-' => 1, c => 1, p => 1 },
+
+ 'MemberOf = '. $tickets[1]->id => { '-' => 0, c => 1, p => 0 },
+ 'MemberOf != '. $tickets[1]->id => { '-' => 1, c => 0, p => 1 },
+);
+{
+ my $tix = RT::Tickets->new($RT::SystemUser);
+ $tix->FromSQL("Queue = '". $q->id ."'");
+ is($tix->Count, $total, "found $total tickets");
+}
+run_tests();
+
+# another set with tests of combinations searches
+@data = (
+ { Subject => '-', },
+ { Subject => 'p', },
+ { Subject => 'rp', RefersTo => -1 },
+ { Subject => 'c', MemberOf => -2 },
+ { Subject => 'rc1', RefersTo => -1 },
+ { Subject => 'rc2', RefersTo => -2 },
+);
+@tickets = add_tix_from_data();
+my $pid = $tickets[1]->id;
+%test = (
+ 'RefersTo IS NOT NULL' => { '-' => 0, c => 0, p => 0, rp => 1, rc1 => 1, rc2 => 1 },
+ 'RefersTo IS NULL' => { '-' => 1, c => 1, p => 1, rp => 0, rc1 => 0, rc2 => 0 },
+
+ 'RefersTo IS NOT NULL AND MemberOf IS NOT NULL' => { '-' => 0, c => 0, p => 0, rp => 0, rc1 => 0, rc2 => 0 },
+ 'RefersTo IS NOT NULL AND MemberOf IS NULL' => { '-' => 0, c => 0, p => 0, rp => 1, rc1 => 1, rc2 => 1 },
+ 'RefersTo IS NULL AND MemberOf IS NOT NULL' => { '-' => 0, c => 1, p => 0, rp => 0, rc1 => 0, rc2 => 0 },
+ 'RefersTo IS NULL AND MemberOf IS NULL' => { '-' => 1, c => 0, p => 1, rp => 0, rc1 => 0, rc2 => 0 },
+
+ 'RefersTo IS NOT NULL OR MemberOf IS NOT NULL' => { '-' => 0, c => 1, p => 0, rp => 1, rc1 => 1, rc2 => 1 },
+ 'RefersTo IS NOT NULL OR MemberOf IS NULL' => { '-' => 1, c => 0, p => 1, rp => 1, rc1 => 1, rc2 => 1 },
+ 'RefersTo IS NULL OR MemberOf IS NOT NULL' => { '-' => 1, c => 1, p => 1, rp => 0, rc1 => 0, rc2 => 0 },
+ 'RefersTo IS NULL OR MemberOf IS NULL' => { '-' => 1, c => 1, p => 1, rp => 1, rc1 => 1, rc2 => 1 },
+
+ "RefersTo = $pid AND MemberOf = $pid" => { '-' => 0, c => 0, p => 0, rp => 0, rc1 => 0, rc2 => 0 },
+ "RefersTo = $pid AND MemberOf != $pid" => { '-' => 0, c => 0, p => 0, rp => 1, rc1 => 0, rc2 => 0 },
+ "RefersTo != $pid AND MemberOf = $pid" => { '-' => 0, c => 1, p => 0, rp => 0, rc1 => 0, rc2 => 0 },
+ "RefersTo != $pid AND MemberOf != $pid" => { '-' => 1, c => 0, p => 1, rp => 0, rc1 => 1, rc2 => 1 },
+
+ "RefersTo = $pid OR MemberOf = $pid" => { '-' => 0, c => 1, p => 0, rp => 1, rc1 => 0, rc2 => 0 },
+ "RefersTo = $pid OR MemberOf != $pid" => { '-' => 1, c => 0, p => 1, rp => 1, rc1 => 1, rc2 => 1 },
+ "RefersTo != $pid OR MemberOf = $pid" => { '-' => 1, c => 1, p => 1, rp => 0, rc1 => 1, rc2 => 1 },
+ "RefersTo != $pid OR MemberOf != $pid" => { '-' => 1, c => 1, p => 1, rp => 1, rc1 => 1, rc2 => 1 },
+);
+{
+ my $tix = RT::Tickets->new($RT::SystemUser);
+ $tix->FromSQL("Queue = '". $q->id ."'");
+ is($tix->Count, $total, "found $total tickets");
+}
+run_tests();
+
diff --git a/rt/t/ticket/search_by_txn.t b/rt/t/ticket/search_by_txn.t
new file mode 100644
index 000000000..1be6916ef
--- /dev/null
+++ b/rt/t/ticket/search_by_txn.t
@@ -0,0 +1,35 @@
+#!/usr/bin/perl
+
+use warnings;
+use strict;
+
+
+BEGIN{ $ENV{'TZ'} = 'GMT'};
+
+use RT::Test tests => 10;
+
+my $SUBJECT = "Search test - ".$$;
+
+use_ok('RT::Tickets');
+my $tix = RT::Tickets->new($RT::SystemUser);
+can_ok($tix, 'FromSQL');
+$tix->FromSQL('Updated = "2005-08-05" AND Subject = "$SUBJECT"');
+
+ok(! $tix->Count, "Searching for tickets updated on a random date finds nothing" . $tix->Count);
+
+my $ticket = RT::Ticket->new($RT::SystemUser);
+$ticket->Create(Queue => 'General', Subject => $SUBJECT);
+ok ($ticket->id, "We created a ticket");
+my ($id, $txnid, $txnobj) = $ticket->Comment( Content => 'A comment that happend on 2004-01-01');
+
+isa_ok($txnobj, 'RT::Transaction');
+
+ok($txnobj->CreatedObj->ISO);
+my ( $sid,$smsg) = $txnobj->__Set(Field => 'Created', Value => '2005-08-05 20:00:56');
+ok($sid,$smsg);
+is($txnobj->Created,'2005-08-05 20:00:56');
+is($txnobj->CreatedObj->ISO,'2005-08-05 20:00:56');
+
+$tix->FromSQL(qq{Updated = "2005-08-05" AND Subject = "$SUBJECT"});
+is( $tix->Count, 1);
+
diff --git a/rt/t/ticket/search_by_watcher.t b/rt/t/ticket/search_by_watcher.t
new file mode 100644
index 000000000..9d94432d2
--- /dev/null
+++ b/rt/t/ticket/search_by_watcher.t
@@ -0,0 +1,280 @@
+#!/usr/bin/perl -w
+
+use strict;
+use warnings;
+
+use RT::Test tests => 119;
+use RT::Ticket;
+
+my $q = RT::Test->load_or_create_queue( Name => 'Regression' );
+ok $q && $q->id, 'loaded or created queue';
+my $queue = $q->Name;
+
+my ($total, @data, @tickets, %test) = (0, ());
+
+sub add_tix_from_data {
+ my @res = ();
+ while (@data) {
+ my $t = RT::Ticket->new($RT::SystemUser);
+ my ( $id, undef $msg ) = $t->Create(
+ Queue => $q->id,
+ %{ shift(@data) },
+ );
+ ok( $id, "ticket created" ) or diag("error: $msg");
+ push @res, $t;
+ $total++;
+ }
+ return @res;
+}
+
+sub run_tests {
+ my $query_prefix = join ' OR ', map 'id = '. $_->id, @tickets;
+ foreach my $key ( sort keys %test ) {
+ my $tix = RT::Tickets->new($RT::SystemUser);
+ $tix->FromSQL( "( $query_prefix ) AND ( $key )" );
+
+ my $error = 0;
+
+ my $count = 0;
+ $count++ foreach grep $_, values %{ $test{$key} };
+ is($tix->Count, $count, "found correct number of ticket(s) by '$key'") or $error = 1;
+
+ my $good_tickets = ($tix->Count == $count);
+ while ( my $ticket = $tix->Next ) {
+ next if $test{$key}->{ $ticket->Subject };
+ diag $ticket->Subject ." ticket has been found when it's not expected";
+ $good_tickets = 0;
+ }
+ ok( $good_tickets, "all tickets are good with '$key'" ) or $error = 1;
+
+ diag "Wrong SQL query for '$key':". $tix->BuildSelectQuery if $error;
+ }
+}
+
+@data = (
+ { Subject => 'xy', Requestor => ['x@example.com', 'y@example.com'] },
+ { Subject => 'x', Requestor => 'x@example.com' },
+ { Subject => 'y', Requestor => 'y@example.com' },
+ { Subject => '-', },
+ { Subject => 'z', Requestor => 'z@example.com' },
+);
+%test = (
+ 'Requestor = "x@example.com"' => { xy => 1, x => 1, y => 0, '-' => 0, z => 0 },
+ 'Requestor != "x@example.com"' => { xy => 0, x => 0, y => 1, '-' => 1, z => 1 },
+
+ 'Requestor = "y@example.com"' => { xy => 1, x => 0, y => 1, '-' => 0, z => 0 },
+ 'Requestor != "y@example.com"' => { xy => 0, x => 1, y => 0, '-' => 1, z => 1 },
+
+ 'Requestor LIKE "@example.com"' => { xy => 1, x => 1, y => 1, '-' => 0, z => 1 },
+ 'Requestor NOT LIKE "@example.com"' => { xy => 0, x => 0, y => 0, '-' => 1, z => 0 },
+
+ 'Requestor IS NULL' => { xy => 0, x => 0, y => 0, '-' => 1, z => 0 },
+ 'Requestor IS NOT NULL' => { xy => 1, x => 1, y => 1, '-' => 0, z => 1 },
+
+# this test is a todo, we run it later
+# 'Requestor = "x@example.com" AND Requestor = "y@example.com"' => { xy => 1, x => 0, y => 0, '-' => 0, z => 0 },
+ 'Requestor = "x@example.com" OR Requestor = "y@example.com"' => { xy => 1, x => 1, y => 1, '-' => 0, z => 0 },
+
+ 'Requestor != "x@example.com" AND Requestor != "y@example.com"' => { xy => 0, x => 0, y => 0, '-' => 1, z => 1 },
+ 'Requestor != "x@example.com" OR Requestor != "y@example.com"' => { xy => 0, x => 1, y => 1, '-' => 1, z => 1 },
+
+ 'Requestor = "x@example.com" AND Requestor != "y@example.com"' => { xy => 0, x => 1, y => 0, '-' => 0, z => 0 },
+ 'Requestor = "x@example.com" OR Requestor != "y@example.com"' => { xy => 1, x => 1, y => 0, '-' => 1, z => 1 },
+
+ 'Requestor != "x@example.com" AND Requestor = "y@example.com"' => { xy => 0, x => 0, y => 1, '-' => 0, z => 0 },
+ 'Requestor != "x@example.com" OR Requestor = "y@example.com"' => { xy => 1, x => 0, y => 1, '-' => 1, z => 1 },
+);
+@tickets = add_tix_from_data();
+{
+ my $tix = RT::Tickets->new($RT::SystemUser);
+ $tix->FromSQL("Queue = '$queue'");
+ is($tix->Count, $total, "found $total tickets");
+}
+run_tests();
+
+# mixing searches by watchers with other conditions
+# http://rt3.fsck.com/Ticket/Display.html?id=9322
+%test = (
+ 'Subject LIKE "x" AND Requestor = "y@example.com"' =>
+ { xy => 1, x => 0, y => 0, '-' => 0, z => 0 },
+ 'Subject NOT LIKE "x" AND Requestor = "y@example.com"' =>
+ { xy => 0, x => 0, y => 1, '-' => 0, z => 0 },
+ 'Subject LIKE "x" AND Requestor != "y@example.com"' =>
+ { xy => 0, x => 1, y => 0, '-' => 0, z => 0 },
+ 'Subject NOT LIKE "x" AND Requestor != "y@example.com"' =>
+ { xy => 0, x => 0, y => 0, '-' => 1, z => 1 },
+
+ 'Subject LIKE "x" OR Requestor = "y@example.com"' =>
+ { xy => 1, x => 1, y => 1, '-' => 0, z => 0 },
+ 'Subject NOT LIKE "x" OR Requestor = "y@example.com"' =>
+ { xy => 1, x => 0, y => 1, '-' => 1, z => 1 },
+ 'Subject LIKE "x" OR Requestor != "y@example.com"' =>
+ { xy => 1, x => 1, y => 0, '-' => 1, z => 1 },
+ 'Subject NOT LIKE "x" OR Requestor != "y@example.com"' =>
+ { xy => 0, x => 1, y => 1, '-' => 1, z => 1 },
+
+# group of cases when user doesn't exist in DB at all
+ 'Subject LIKE "x" AND Requestor = "not-exist@example.com"' =>
+ { xy => 0, x => 0, y => 0, '-' => 0, z => 0 },
+ 'Subject NOT LIKE "x" AND Requestor = "not-exist@example.com"' =>
+ { xy => 0, x => 0, y => 0, '-' => 0, z => 0 },
+ 'Subject LIKE "x" AND Requestor != "not-exist@example.com"' =>
+ { xy => 1, x => 1, y => 0, '-' => 0, z => 0 },
+ 'Subject NOT LIKE "x" AND Requestor != "not-exist@example.com"' =>
+ { xy => 0, x => 0, y => 1, '-' => 1, z => 1 },
+# 'Subject LIKE "x" OR Requestor = "not-exist@example.com"' =>
+# { xy => 1, x => 1, y => 0, '-' => 0, z => 0 },
+# 'Subject NOT LIKE "x" OR Requestor = "not-exist@example.com"' =>
+# { xy => 0, x => 0, y => 1, '-' => 1, z => 1 },
+ 'Subject LIKE "x" OR Requestor != "not-exist@example.com"' =>
+ { xy => 1, x => 1, y => 1, '-' => 1, z => 1 },
+ 'Subject NOT LIKE "x" OR Requestor != "not-exist@example.com"' =>
+ { xy => 1, x => 1, y => 1, '-' => 1, z => 1 },
+
+ 'Subject LIKE "z" AND (Requestor = "x@example.com" OR Requestor = "y@example.com")' =>
+ { xy => 0, x => 0, y => 0, '-' => 0, z => 0 },
+ 'Subject NOT LIKE "z" AND (Requestor = "x@example.com" OR Requestor = "y@example.com")' =>
+ { xy => 1, x => 1, y => 1, '-' => 0, z => 0 },
+ 'Subject LIKE "z" OR (Requestor = "x@example.com" OR Requestor = "y@example.com")' =>
+ { xy => 1, x => 1, y => 1, '-' => 0, z => 1 },
+ 'Subject NOT LIKE "z" OR (Requestor = "x@example.com" OR Requestor = "y@example.com")' =>
+ { xy => 1, x => 1, y => 1, '-' => 1, z => 0 },
+);
+run_tests();
+
+TODO: {
+ local $TODO = "we can't generate this query yet";
+ %test = (
+ 'Requestor = "x@example.com" AND Requestor = "y@example.com"'
+ => { xy => 1, x => 0, y => 0, '-' => 0, z => 0 },
+ 'Subject LIKE "x" OR Requestor = "not-exist@example.com"' =>
+ { xy => 1, x => 1, y => 0, '-' => 0, z => 0 },
+ 'Subject NOT LIKE "x" OR Requestor = "not-exist@example.com"' =>
+ { xy => 0, x => 0, y => 1, '-' => 1, z => 1 },
+ );
+ run_tests();
+}
+
+@data = (
+ { Subject => 'xy', Cc => ['x@example.com'], Requestor => [ 'y@example.com' ] },
+ { Subject => 'x-', Cc => ['x@example.com'], Requestor => [] },
+ { Subject => '-y', Cc => [], Requestor => [ 'y@example.com' ] },
+ { Subject => '-', },
+ { Subject => 'zz', Cc => ['z@example.com'], Requestor => [ 'z@example.com' ] },
+ { Subject => 'z-', Cc => ['z@example.com'], Requestor => [] },
+ { Subject => '-z', Cc => [], Requestor => [ 'z@example.com' ] },
+);
+%test = (
+ 'Cc = "x@example.com" AND Requestor = "y@example.com"' =>
+ { xy => 1, 'x-' => 0, '-y' => 0, '-' => 0, zz => 0, 'z-' => 0, '-z' => 0 },
+ 'Cc = "x@example.com" OR Requestor = "y@example.com"' =>
+ { xy => 1, 'x-' => 1, '-y' => 1, '-' => 0, zz => 0, 'z-' => 0, '-z' => 0 },
+
+ 'Cc != "x@example.com" AND Requestor = "y@example.com"' =>
+ { xy => 0, 'x-' => 0, '-y' => 1, '-' => 0, zz => 0, 'z-' => 0, '-z' => 0 },
+ 'Cc != "x@example.com" OR Requestor = "y@example.com"' =>
+ { xy => 1, 'x-' => 0, '-y' => 1, '-' => 1, zz => 1, 'z-' => 1, '-z' => 1 },
+
+ 'Cc IS NULL AND Requestor = "y@example.com"' =>
+ { xy => 0, 'x-' => 0, '-y' => 1, '-' => 0, zz => 0, 'z-' => 0, '-z' => 0 },
+ 'Cc IS NULL OR Requestor = "y@example.com"' =>
+ { xy => 1, 'x-' => 0, '-y' => 1, '-' => 1, zz => 0, 'z-' => 0, '-z' => 1 },
+
+ 'Cc IS NOT NULL AND Requestor = "y@example.com"' =>
+ { xy => 1, 'x-' => 0, '-y' => 0, '-' => 0, zz => 0, 'z-' => 0, '-z' => 0 },
+ 'Cc IS NOT NULL OR Requestor = "y@example.com"' =>
+ { xy => 1, 'x-' => 1, '-y' => 1, '-' => 0, zz => 1, 'z-' => 1, '-z' => 0 },
+);
+@tickets = add_tix_from_data();
+{
+ my $tix = RT::Tickets->new($RT::SystemUser);
+ $tix->FromSQL("Queue = '$queue'");
+ is($tix->Count, $total, "found $total tickets");
+}
+run_tests();
+
+
+# owner is special watcher because reference is duplicated in two places,
+# owner was an ENUM field now it's WATCHERFIELD, but should support old
+# style ENUM searches for backward compatibility
+my $nobody = RT::Nobody();
+{
+ my $tix = RT::Tickets->new($RT::SystemUser);
+ $tix->FromSQL("Queue = '$queue' AND Owner = '". $nobody->id ."'");
+ ok($tix->Count, "found ticket(s)");
+}
+{
+ my $tix = RT::Tickets->new($RT::SystemUser);
+ $tix->FromSQL("Queue = '$queue' AND Owner = '". $nobody->Name ."'");
+ ok($tix->Count, "found ticket(s)");
+}
+{
+ my $tix = RT::Tickets->new($RT::SystemUser);
+ $tix->FromSQL("Queue = '$queue' AND Owner != '". $nobody->id ."'");
+ is($tix->Count, 0, "found ticket(s)");
+}
+{
+ my $tix = RT::Tickets->new($RT::SystemUser);
+ $tix->FromSQL("Queue = '$queue' AND Owner != '". $nobody->Name ."'");
+ is($tix->Count, 0, "found ticket(s)");
+}
+
+{
+ my $tix = RT::Tickets->new($RT::SystemUser);
+ $tix->FromSQL("Queue = '$queue' AND Owner.Name LIKE 'nob'");
+ ok($tix->Count, "found ticket(s)");
+}
+
+{
+ # create ticket and force type to not a 'ticket' value
+ # bug #6898@rt3.fsck.com
+ # and http://marc.theaimsgroup.com/?l=rt-devel&m=112662934627236&w=2
+ @data = ( { Subject => 'not a ticket' } );
+ my($t) = add_tix_from_data();
+ $t->_Set( Field => 'Type',
+ Value => 'not a ticket',
+ CheckACL => 0,
+ RecordTransaction => 0,
+ );
+ $total--;
+
+ my $tix = RT::Tickets->new($RT::SystemUser);
+ $tix->FromSQL("Queue = '$queue' AND Owner = 'Nobody'");
+ is($tix->Count, $total, "found ticket(s)");
+}
+
+{
+ my $everyone = RT::Group->new( $RT::SystemUser );
+ $everyone->LoadSystemInternalGroup('Everyone');
+ ok($everyone->id, "loaded 'everyone' group");
+ my($id, $msg) = $everyone->PrincipalObj->GrantRight( Right => 'OwnTicket',
+ Object => $q
+ );
+ ok($id, "granted OwnTicket right to Everyone on '$queue'") or diag("error: $msg");
+
+ my $u = RT::User->new( $RT::SystemUser );
+ $u->LoadOrCreateByEmail('alpha@example.com');
+ ok($u->id, "loaded user");
+ @data = ( { Subject => '4', Owner => $u->id } );
+ my($t) = add_tix_from_data();
+ is( $t->Owner, $u->id, "created ticket with custom owner" );
+ my $u_alpha_id = $u->id;
+
+ $u = RT::User->new( $RT::SystemUser );
+ $u->LoadOrCreateByEmail('bravo@example.com');
+ ok($u->id, "loaded user");
+ @data = ( { Subject => '5', Owner => $u->id } );
+ ($t) = add_tix_from_data();
+ is( $t->Owner, $u->id, "created ticket with custom owner" );
+ my $u_bravo_id = $u->id;
+
+ my $tix = RT::Tickets->new($RT::SystemUser);
+ $tix->FromSQL("Queue = '$queue' AND
+ ( Owner = '$u_alpha_id' OR
+ Owner = '$u_bravo_id' )"
+ );
+ is($tix->Count, 2, "found ticket(s)");
+}
+
+
+exit(0)
diff --git a/rt/t/ticket/search_long_cf_values.t b/rt/t/ticket/search_long_cf_values.t
new file mode 100644
index 000000000..f9cc7b5a2
--- /dev/null
+++ b/rt/t/ticket/search_long_cf_values.t
@@ -0,0 +1,79 @@
+#!/opt/perl/bin/perl -w
+
+# tests relating to searching. Especially around custom fields with long values
+# (> 255 chars)
+
+use strict;
+use warnings;
+
+use RT::Test tests => 10;
+
+# setup the queue
+
+my $q = RT::Queue->new($RT::SystemUser);
+my $queue = 'SearchTests-'.$$;
+$q->Create(Name => $queue);
+ok ($q->id, "Created the queue");
+
+
+# setup the CF
+my $cf = RT::CustomField->new($RT::SystemUser);
+$cf->Create(Name => 'SearchTest', Type => 'Freeform', MaxValues => 0, Queue => $q->id);
+ok($cf->id, "Created the SearchTest CF");
+my $cflabel = "CustomField-".$cf->id;
+
+# setup some tickets
+my $t1 = RT::Ticket->new($RT::SystemUser);
+my ( $id, undef $msg ) = $t1->Create(
+ Queue => $q->id,
+ Subject => 'SearchTest1',
+ Requestor => ['search@example.com'],
+ $cflabel => 'foo',
+);
+ok( $id, $msg );
+
+
+my $t2 = RT::Ticket->new($RT::SystemUser);
+( $id, undef, $msg ) = $t2->Create(
+ Queue => $q->id,
+ Subject => 'SearchTest2',
+ Requestor => ['searchlong@example.com'],
+ $cflabel => 'bar' x 150,
+);
+ok( $id, $msg );
+
+my $t3 = RT::Ticket->new($RT::SystemUser);
+( $id, undef, $msg ) = $t3->Create(
+ Queue => $q->id,
+ Subject => 'SearchTest3',
+ Requestor => ['searchlong@example.com'],
+ $cflabel => 'bar',
+);
+ok( $id, $msg );
+
+# we have tickets. start searching
+my $tix = RT::Tickets->new($RT::SystemUser);
+$tix->FromSQL("Queue = '$queue' AND CF.SearchTest LIKE 'foo'");
+is($tix->Count, 1, "matched short string foo")
+ or diag "wrong results from SQL:\n". $tix->BuildSelectCountQuery;
+
+$tix = RT::Tickets->new($RT::SystemUser);
+$tix->FromSQL("Queue = '$queue' AND CF.SearchTest LIKE 'bar'");
+is($tix->Count, 2, "matched long+short string bar")
+ or diag "wrong results from SQL:\n". $tix->BuildSelectCountQuery;
+
+$tix = RT::Tickets->new($RT::SystemUser);
+$tix->FromSQL("Queue = '$queue' AND ( CF.SearchTest LIKE 'foo' OR CF.SearchTest LIKE 'bar' )");
+is($tix->Count, 3, "matched short string foo or long+short string bar")
+ or diag "wrong results from SQL:\n". $tix->BuildSelectCountQuery;
+
+$tix = RT::Tickets->new($RT::SystemUser);
+$tix->FromSQL("Queue = '$queue' AND CF.SearchTest NOT LIKE 'foo' AND CF.SearchTest LIKE 'bar'");
+is($tix->Count, 2, "not matched short string foo and matched long+short string bar")
+ or diag "wrong results from SQL:\n". $tix->BuildSelectCountQuery;
+
+$tix = RT::Tickets->new($RT::SystemUser);
+$tix->FromSQL("Queue = '$queue' AND CF.SearchTest LIKE 'foo' AND CF.SearchTest NOT LIKE 'bar'");
+is($tix->Count, 1, "matched short string foo and not matched long+short string bar")
+ or diag "wrong results from SQL:\n". $tix->BuildSelectCountQuery;
+
diff --git a/rt/t/ticket/sort-by-custom-ownership.t b/rt/t/ticket/sort-by-custom-ownership.t
new file mode 100644
index 000000000..9739c5aec
--- /dev/null
+++ b/rt/t/ticket/sort-by-custom-ownership.t
@@ -0,0 +1,103 @@
+#!/usr/bin/perl
+
+use RT;
+use RT::Test tests => 7;
+
+
+use strict;
+use warnings;
+
+use RT::Tickets;
+use RT::Queue;
+use RT::CustomField;
+
+my($ret,$msg);
+
+# Test Paw Sort
+
+
+
+# ---- Create a queue to test with.
+my $queue = "PAWSortQueue-$$";
+my $queue_obj = RT::Queue->new($RT::SystemUser);
+($ret, $msg) = $queue_obj->Create(Name => $queue,
+ Description => 'queue for custom field sort testing');
+ok($ret, "$queue test queue creation. $msg");
+
+
+# ---- Create some users
+
+my $me = RT::User->new($RT::SystemUser);
+($ret, $msg) = $me->Create(Name => "Me$$", EmailAddress => $$.'create-me-1@example.com');
+($ret, $msg) = $me->PrincipalObj->GrantRight(Object =>$queue_obj, Right => 'OwnTicket');
+($ret, $msg) = $me->PrincipalObj->GrantRight(Object =>$queue_obj, Right => 'SeeQueue');
+($ret, $msg) = $me->PrincipalObj->GrantRight(Object =>$queue_obj, Right => 'ShowTicket');
+my $you = RT::User->new($RT::SystemUser);
+($ret, $msg) = $you->Create(Name => "You$$", EmailAddress => $$.'create-you-1@example.com');
+($ret, $msg) = $you->PrincipalObj->GrantRight(Object =>$queue_obj, Right => 'OwnTicket');
+($ret, $msg) = $you->PrincipalObj->GrantRight(Object =>$queue_obj, Right => 'SeeQueue');
+($ret, $msg) = $you->PrincipalObj->GrantRight(Object =>$queue_obj, Right => 'ShowTicket');
+
+my $nobody = RT::User->new($RT::SystemUser);
+$nobody->Load('nobody');
+
+
+# ----- Create some tickets to test with. Assign them some values to
+# make it easy to sort with.
+
+my @tickets = (
+ [qw[1 10], $me],
+ [qw[2 20], $me],
+ [qw[3 20], $you],
+ [qw[4 30], $you],
+ [qw[5 5], $nobody],
+ [qw[6 55], $nobody],
+ );
+for (@tickets) {
+ my $t = RT::Ticket->new($RT::SystemUser);
+ $t->Create( Queue => $queue_obj->Id,
+ Subject => $_->[0],
+ Owner => $_->[2]->Id,
+ Priority => $_->[1],
+ );
+}
+
+sub check_order {
+ my ($tx, @order) = @_;
+ my @results;
+ while (my $t = $tx->Next) {
+ push @results, $t->Subject;
+ }
+ my $results = join (" ",@results);
+ my $order = join(" ",@order);
+ is( $results, $order );
+}
+
+
+# The real tests start here
+
+my $cme = new RT::CurrentUser( $me );
+my $metx = new RT::Tickets( $cme );
+# Make sure we can sort in both directions on a queue specific field.
+$metx->FromSQL(qq[queue="$queue"] );
+$metx->OrderBy( FIELD => "Custom.Ownership", ORDER => 'ASC' );
+is($metx->Count,6);
+check_order( $metx, qw[2 1 6 5 4 3]);
+
+$metx->OrderBy( FIELD => "Custom.Ownership", ORDER => 'DESC' );
+is($metx->Count,6);
+check_order( $metx, reverse qw[2 1 6 5 4 3]);
+
+
+
+my $cyou = new RT::CurrentUser( $you );
+my $youtx = new RT::Tickets( $cyou );
+# Make sure we can sort in both directions on a queue specific field.
+$youtx->FromSQL(qq[queue="$queue"] );
+$youtx->OrderBy( FIELD => "Custom.Ownership", ORDER => 'ASC' );
+is($youtx->Count,6);
+check_order( $youtx, qw[4 3 6 5 2 1]);
+
+__END__
+
+
diff --git a/rt/t/ticket/sort-by-queue.t b/rt/t/ticket/sort-by-queue.t
new file mode 100644
index 000000000..df6e1ad0f
--- /dev/null
+++ b/rt/t/ticket/sort-by-queue.t
@@ -0,0 +1,100 @@
+#!/usr/bin/perl
+
+use RT::Test tests => 8;
+
+use strict;
+use warnings;
+
+use RT::Tickets;
+use RT::Queue;
+use RT::CustomField;
+
+#########################################################
+# Test sorting by Queue, we sort by its name
+#########################################################
+
+
+diag "Create queues to test with." if $ENV{TEST_VERBOSE};
+my @qids;
+my @queues;
+# create them in reverse order to avoid false positives
+foreach my $name ( qw(sort-by-queue-Z sort-by-queue-A) ) {
+ my $queue = RT::Queue->new( $RT::SystemUser );
+ my ($ret, $msg) = $queue->Create(
+ Name => $name ."-$$",
+ Description => 'queue to test sorting by queue'
+ );
+ ok($ret, "test queue creation. $msg");
+ push @queues, $queue;
+ push @qids, $queue->id;
+}
+
+my ($total, @data, @tickets, @test) = (0, ());
+
+sub add_tix_from_data {
+ my @res = ();
+ @data = sort { rand(100) <=> rand(100) } @data;
+ while (@data) {
+ my $t = RT::Ticket->new($RT::SystemUser);
+ my %args = %{ shift(@data) };
+ my ( $id, undef, $msg ) = $t->Create( %args );
+ ok( $id, "ticket created" ) or diag("error: $msg");
+ push @res, $t;
+ $total++;
+ }
+ return @res;
+}
+
+sub run_tests {
+ my $query_prefix = join ' OR ', map 'id = '. $_->id, @tickets;
+ foreach my $test ( @test ) {
+ my $query = join " AND ", map "( $_ )", grep defined && length,
+ $query_prefix, $test->{'Query'};
+
+ foreach my $order (qw(ASC DESC)) {
+ my $error = 0;
+ my $tix = RT::Tickets->new( $RT::SystemUser );
+ $tix->FromSQL( $query );
+ $tix->OrderBy( FIELD => $test->{'Order'}, ORDER => $order );
+
+ ok($tix->Count, "found ticket(s)")
+ or $error = 1;
+
+ my ($order_ok, $last) = (1, $order eq 'ASC'? '-': 'zzzzzz');
+ while ( my $t = $tix->Next ) {
+ my $tmp;
+ if ( $order eq 'ASC' ) {
+ $tmp = ((split( /,/, $last))[0] cmp (split( /,/, $t->Subject))[0]);
+ } else {
+ $tmp = -((split( /,/, $last))[-1] cmp (split( /,/, $t->Subject))[-1]);
+ }
+ if ( $tmp > 0 ) {
+ $order_ok = 0; last;
+ }
+ $last = $t->Subject;
+ }
+
+ ok( $order_ok, "$order order of tickets is good" )
+ or $error = 1;
+
+ if ( $error ) {
+ diag "Wrong SQL query:". $tix->BuildSelectQuery;
+ $tix->GotoFirstItem;
+ while ( my $t = $tix->Next ) {
+ diag sprintf "%02d - %s", $t->id, $t->Subject;
+ }
+ }
+ }
+ }
+}
+
+@data = (
+ { Queue => $qids[0], Subject => 'z' },
+ { Queue => $qids[1], Subject => 'a' },
+);
+@tickets = add_tix_from_data();
+@test = (
+ { Order => "Queue" },
+);
+run_tests();
+
diff --git a/rt/t/ticket/sort-by-user.t b/rt/t/ticket/sort-by-user.t
new file mode 100644
index 000000000..f9b1602f1
--- /dev/null
+++ b/rt/t/ticket/sort-by-user.t
@@ -0,0 +1,152 @@
+#!/usr/bin/perl
+
+use RT::Test tests => 32;
+
+use strict;
+use warnings;
+
+use RT::Tickets;
+use RT::Queue;
+use RT::CustomField;
+
+#########################################################
+# Test sorting by Owner, Creator and LastUpdatedBy
+# we sort by user name
+#########################################################
+
+diag "Create a queue to test with." if $ENV{TEST_VERBOSE};
+my $queue_name = "OwnerSortQueue$$";
+my $queue;
+{
+ $queue = RT::Queue->new( $RT::SystemUser );
+ my ($ret, $msg) = $queue->Create(
+ Name => $queue_name,
+ Description => 'queue for custom field sort testing'
+ );
+ ok($ret, "$queue test queue creation. $msg");
+}
+
+my @uids;
+my @users;
+# create them in reverse order to avoid false positives
+foreach my $u (qw(Z A)) {
+ my $name = $u ."-user-to-test-ordering-$$";
+ my $user = RT::User->new( $RT::SystemUser );
+ my ($uid) = $user->Create(
+ Name => $name,
+ Privileged => 1,
+ );
+ ok $uid, "created user #$uid";
+
+ my ($status, $msg) = $user->PrincipalObj->GrantRight( Right => 'OwnTicket', Object => $queue );
+ ok $status, "granted right";
+ ($status, $msg) = $user->PrincipalObj->GrantRight( Right => 'CreateTicket', Object => $queue );
+ ok $status, "granted right";
+
+ push @users, $user;
+ push @uids, $user->id;
+}
+
+my ($total, @data, @tickets, @test) = (0, ());
+
+sub add_tix_from_data {
+ my @res = ();
+ @data = sort { rand(100) <=> rand(100) } @data;
+ while (@data) {
+ my $t = RT::Ticket->new($RT::SystemUser);
+ my %args = %{ shift(@data) };
+
+ my ( $id, undef, $msg ) = $t->Create( %args, Queue => $queue->id );
+ if ( $args{'Owner'} ) {
+ is $t->Owner, $args{'Owner'}, "owner is correct";
+ }
+ if ( $args{'Creator'} ) {
+ is $t->Creator, $args{'Creator'}, "creator is correct";
+ }
+ # hackish, but simpler
+ if ( $args{'LastUpdatedBy'} ) {
+ $t->__Set( Field => 'LastUpdatedBy', Value => $args{'LastUpdatedBy'} );
+ }
+ ok( $id, "ticket created" ) or diag("error: $msg");
+ push @res, $t;
+ $total++;
+ }
+ return @res;
+}
+
+sub run_tests {
+ my $query_prefix = join ' OR ', map 'id = '. $_->id, @tickets;
+ foreach my $test ( @test ) {
+ my $query = join " AND ", map "( $_ )", grep defined && length,
+ $query_prefix, $test->{'Query'};
+
+ foreach my $order (qw(ASC DESC)) {
+ my $error = 0;
+ my $tix = RT::Tickets->new( $RT::SystemUser );
+ $tix->FromSQL( $query );
+ $tix->OrderBy( FIELD => $test->{'Order'}, ORDER => $order );
+
+ ok($tix->Count, "found ticket(s)")
+ or $error = 1;
+
+ my ($order_ok, $last) = (1, $order eq 'ASC'? '-': 'zzzzzz');
+ while ( my $t = $tix->Next ) {
+ my $tmp;
+ if ( $order eq 'ASC' ) {
+ $tmp = ((split( /,/, $last))[0] cmp (split( /,/, $t->Subject))[0]);
+ } else {
+ $tmp = -((split( /,/, $last))[-1] cmp (split( /,/, $t->Subject))[-1]);
+ }
+ if ( $tmp > 0 ) {
+ $order_ok = 0; last;
+ }
+ $last = $t->Subject;
+ }
+
+ ok( $order_ok, "$order order of tickets is good" )
+ or $error = 1;
+
+ if ( $error ) {
+ diag "Wrong SQL query:". $tix->BuildSelectQuery;
+ $tix->GotoFirstItem;
+ while ( my $t = $tix->Next ) {
+ diag sprintf "%02d - %s", $t->id, $t->Subject;
+ }
+ }
+ }
+ }
+}
+
+@data = (
+ { Subject => 'Nobody' },
+ { Subject => 'Z', Owner => $uids[0] },
+ { Subject => 'A', Owner => $uids[1] },
+);
+@tickets = add_tix_from_data();
+@test = (
+ { Order => "Owner" },
+);
+run_tests();
+
+@data = (
+ { Subject => 'RT' },
+ { Subject => 'Z', Creator => $uids[0] },
+ { Subject => 'A', Creator => $uids[1] },
+);
+@tickets = add_tix_from_data();
+@test = (
+ { Order => "Creator" },
+);
+run_tests();
+
+@data = (
+ { Subject => 'RT' },
+ { Subject => 'Z', LastUpdatedBy => $uids[0] },
+ { Subject => 'A', LastUpdatedBy => $uids[1] },
+);
+@tickets = add_tix_from_data();
+@test = (
+ { Order => "LastUpdatedBy" },
+);
+run_tests();
+
diff --git a/rt/t/ticket/sort_by_cf.t b/rt/t/ticket/sort_by_cf.t
new file mode 100644
index 000000000..69274add9
--- /dev/null
+++ b/rt/t/ticket/sort_by_cf.t
@@ -0,0 +1,172 @@
+#!/usr/bin/perl
+
+use RT::Test tests => 21;
+RT::Init();
+
+use strict;
+use warnings;
+
+use RT::Tickets;
+use RT::Queue;
+use RT::CustomField;
+
+my($ret,$msg);
+
+
+# Test Sorting by custom fields.
+# TODO: it's hard to read this file, conver to new style,
+# for example look at 23cfsort-freeform-single.t
+
+# ---- Create a queue to test with.
+my $queue = "CFSortQueue-$$";
+my $queue_obj = RT::Queue->new( $RT::SystemUser );
+($ret, $msg) = $queue_obj->Create(
+ Name => $queue,
+ Description => 'queue for custom field sort testing'
+);
+ok($ret, "$queue test queue creation. $msg");
+
+# ---- Create some custom fields. We're not currently using all of
+# them to test with, but the more the merrier.
+my $cfO = RT::CustomField->new($RT::SystemUser);
+my $cfA = RT::CustomField->new($RT::SystemUser);
+my $cfB = RT::CustomField->new($RT::SystemUser);
+my $cfC = RT::CustomField->new($RT::SystemUser);
+
+($ret, $msg) = $cfO->Create( Name => 'Order',
+ Queue => 0,
+ SortOrder => 1,
+ Description => q{Something to compare results for, since we can't guarantee ticket ID},
+ Type=> 'FreeformSingle');
+ok($ret, "Custom Field Order created");
+
+($ret, $msg) = $cfA->Create( Name => 'Alpha',
+ Queue => $queue_obj->id,
+ SortOrder => 1,
+ Description => 'A Testing custom field',
+ Type=> 'FreeformSingle');
+ok($ret, "Custom Field Alpha created");
+
+($ret, $msg) = $cfB->Create( Name => 'Beta',
+ Queue => $queue_obj->id,
+ Description => 'A Testing custom field',
+ Type=> 'FreeformSingle');
+ok($ret, "Custom Field Beta created");
+
+($ret, $msg) = $cfC->Create( Name => 'Charlie',
+ Queue => $queue_obj->id,
+ Description => 'A Testing custom field',
+ Type=> 'FreeformSingle');
+ok($ret, "Custom Field Charlie created");
+
+# ----- Create some tickets to test with. Assign them some values to
+# make it easy to sort with.
+my $t1 = RT::Ticket->new($RT::SystemUser);
+$t1->Create( Queue => $queue_obj->Id,
+ Subject => 'One',
+ );
+$t1->AddCustomFieldValue(Field => $cfO->Id, Value => '1');
+$t1->AddCustomFieldValue(Field => $cfA->Id, Value => '2');
+$t1->AddCustomFieldValue(Field => $cfB->Id, Value => '1');
+$t1->AddCustomFieldValue(Field => $cfC->Id, Value => 'BBB');
+
+my $t2 = RT::Ticket->new($RT::SystemUser);
+$t2->Create( Queue => $queue_obj->Id,
+ Subject => 'Two',
+ );
+$t2->AddCustomFieldValue(Field => $cfO->Id, Value => '2');
+$t2->AddCustomFieldValue(Field => $cfA->Id, Value => '1');
+$t2->AddCustomFieldValue(Field => $cfB->Id, Value => '2');
+$t2->AddCustomFieldValue(Field => $cfC->Id, Value => 'AAA');
+
+# helper
+sub check_order {
+ my ($tx, @order) = @_;
+ my @results;
+ while (my $t = $tx->Next) {
+ push @results, $t->CustomFieldValues($cfO->Id)->First->Content;
+ }
+ my $results = join (" ",@results);
+ my $order = join(" ",@order);
+ @_ = ($results, $order , "Ordered correctly: $order");
+ goto \&is;
+}
+
+# The real tests start here
+my $tx = new RT::Tickets( $RT::SystemUser );
+
+
+# Make sure we can sort in both directions on a queue specific field.
+$tx->FromSQL(qq[queue="$queue"] );
+$tx->OrderBy( FIELD => "CF.${queue}.{Charlie}", ORDER => 'DES' );
+is($tx->Count,2 ,"We found 2 tickets when looking for cf charlie");
+check_order( $tx, 1, 2);
+
+$tx = new RT::Tickets( $RT::SystemUser );
+$tx->FromSQL(qq[queue="$queue"] );
+$tx->OrderBy( FIELD => "CF.${queue}.{Charlie}", ORDER => 'ASC' );
+is($tx->Count,2, "We found two tickets when sorting by cf charlie without limiting to it" );
+check_order( $tx, 2, 1);
+
+# When ordering by _global_ CustomFields, if more than one queue has a
+# CF named Charlie, things will go bad. So, these results are uniqued
+# in Tickets_Overlay.
+$tx = new RT::Tickets( $RT::SystemUser );
+$tx->FromSQL(qq[queue="$queue"] );
+$tx->OrderBy( FIELD => "CF.{Charlie}", ORDER => 'DESC' );
+is($tx->Count,2);
+check_order( $tx, 1, 2);
+
+$tx = new RT::Tickets( $RT::SystemUser );
+$tx->FromSQL(qq[queue="$queue"] );
+$tx->OrderBy( FIELD => "CF.{Charlie}", ORDER => 'ASC' );
+is($tx->Count,2);
+check_order( $tx, 2, 1);
+
+# Add a new ticket, to test sorting on multiple columns.
+my $t3 = RT::Ticket->new($RT::SystemUser);
+$t3->Create( Queue => $queue_obj->Id,
+ Subject => 'Three',
+ );
+$t3->AddCustomFieldValue(Field => $cfO->Id, Value => '3');
+$t3->AddCustomFieldValue(Field => $cfA->Id, Value => '3');
+$t3->AddCustomFieldValue(Field => $cfB->Id, Value => '2');
+$t3->AddCustomFieldValue(Field => $cfC->Id, Value => 'AAA');
+
+$tx = new RT::Tickets( $RT::SystemUser );
+$tx->FromSQL(qq[queue="$queue"] );
+$tx->OrderByCols(
+ { FIELD => "CF.${queue}.{Charlie}", ORDER => 'ASC' },
+ { FIELD => "CF.${queue}.{Alpha}", ORDER => 'DES' },
+);
+is($tx->Count,3);
+check_order( $tx, 3, 2, 1);
+
+$tx = new RT::Tickets( $RT::SystemUser );
+$tx->FromSQL(qq[queue="$queue"] );
+$tx->OrderByCols(
+ { FIELD => "CF.${queue}.{Charlie}", ORDER => 'DES' },
+ { FIELD => "CF.${queue}.{Alpha}", ORDER => 'ASC' },
+);
+is($tx->Count,3);
+check_order( $tx, 1, 2, 3);
+
+# Reverse the order of the secondary column, which changes the order
+# of the first two tickets.
+$tx = new RT::Tickets( $RT::SystemUser );
+$tx->FromSQL(qq[queue="$queue"] );
+$tx->OrderByCols(
+ { FIELD => "CF.${queue}.{Charlie}", ORDER => 'ASC' },
+ { FIELD => "CF.${queue}.{Alpha}", ORDER => 'ASC' },
+);
+is($tx->Count,3);
+check_order( $tx, 2, 3, 1);
+
+$tx = new RT::Tickets( $RT::SystemUser );
+$tx->FromSQL(qq[queue="$queue"] );
+$tx->OrderByCols(
+ { FIELD => "CF.${queue}.{Charlie}", ORDER => 'DES' },
+ { FIELD => "CF.${queue}.{Alpha}", ORDER => 'DES' },
+);
+is($tx->Count,3);
+check_order( $tx, 1, 3, 2);
diff --git a/rt/t/validator/group_members.t b/rt/t/validator/group_members.t
new file mode 100644
index 000000000..f27a1f177
--- /dev/null
+++ b/rt/t/validator/group_members.t
@@ -0,0 +1,178 @@
+#!/usr/bin/perl -w
+
+use strict;
+use warnings;
+
+use RT::Test tests => 60;
+
+sub load_or_create_group {
+ my $name = shift;
+ my %args = (@_);
+
+ my $group = RT::Group->new( $RT::SystemUser );
+ $group->LoadUserDefinedGroup( $name );
+ unless ( $group->id ) {
+ my ($id, $msg) = $group->CreateUserDefinedGroup(
+ Name => $name,
+ );
+ die "$msg" unless $id;
+ }
+
+ if ( $args{Members} ) {
+ my $cur = $group->MembersObj;
+ while ( my $entry = $cur->Next ) {
+ my ($status, $msg) = $entry->Delete;
+ die "$msg" unless $status;
+ }
+
+ foreach my $new ( @{ $args{Members} } ) {
+ my ($status, $msg) = $group->AddMember(
+ ref($new)? $new->id : $new,
+ );
+ die "$msg" unless $status;
+ }
+ }
+
+ return $group;
+}
+
+my $validator_path = "$RT::SbinPath/rt-validator";
+sub run_validator {
+ my %args = (check => 1, resolve => 0, force => 1, @_ );
+
+ my $cmd = $validator_path;
+ die "Couldn't find $cmd command" unless -f $cmd;
+
+ while( my ($k,$v) = each %args ) {
+ next unless $v;
+ $cmd .= " --$k '$v'";
+ }
+ $cmd .= ' 2>&1';
+
+ require IPC::Open2;
+ my ($child_out, $child_in);
+ my $pid = IPC::Open2::open2($child_out, $child_in, $cmd);
+ close $child_in;
+
+ my $result = do { local $/; <$child_out> };
+ close $child_out;
+ waitpid $pid, 0;
+
+ DBIx::SearchBuilder::Record::Cachable->FlushCache
+ if $args{'resolve'};
+
+ return ($?, $result);
+}
+
+{
+ my ($ecode, $res) = run_validator();
+ is $res, '', 'empty result';
+}
+
+{
+ my $group = load_or_create_group('test', Members => [] );
+ ok $group, "loaded or created a group";
+
+ my ($ecode, $res) = run_validator();
+ is $res, '', 'empty result';
+}
+
+# G1 -> G2
+{
+ my $group1 = load_or_create_group( 'test1', Members => [] );
+ ok $group1, "loaded or created a group";
+
+ my $group2 = load_or_create_group( 'test2', Members => [ $group1 ]);
+ ok $group2, "loaded or created a group";
+
+ ok $group2->HasMember( $group1->id ), "has member";
+ ok $group2->HasMemberRecursively( $group1->id ), "has member";
+
+ my ($ecode, $res) = run_validator();
+ is $res, '', 'empty result';
+
+ $RT::Handle->dbh->do("DELETE FROM CachedGroupMembers");
+ DBIx::SearchBuilder::Record::Cachable->FlushCache;
+ ok !$group2->HasMemberRecursively( $group1->id ), "has no member, broken DB";
+
+ ($ecode, $res) = run_validator(resolve => 1);
+
+ ok $group2->HasMember( $group1->id ), "has member";
+ ok $group2->HasMemberRecursively( $group1->id ), "has member";
+
+ ($ecode, $res) = run_validator();
+ is $res, '', 'empty result';
+}
+
+# G1 <- G2 <- G3 <- G4 <- G5
+{
+ my @groups;
+ for (1..5) {
+ my $child = @groups? $groups[-1]: undef;
+
+ my $group = load_or_create_group( 'test'. $_, Members => [ $child? ($child): () ] );
+ ok $group, "loaded or created a group";
+
+ ok $group->HasMember( $child->id ), "has member"
+ if $child;
+ ok $group->HasMemberRecursively( $_->id ), "has member"
+ foreach @groups;
+
+ push @groups, $group;
+ }
+
+ my ($ecode, $res) = run_validator();
+ is $res, '', 'empty result';
+
+ $RT::Handle->dbh->do("DELETE FROM CachedGroupMembers");
+ DBIx::SearchBuilder::Record::Cachable->FlushCache;
+
+ ok !$groups[1]->HasMemberRecursively( $groups[0]->id ), "has no member, broken DB";
+
+ ($ecode, $res) = run_validator(resolve => 1);
+
+ for ( my $i = 1; $i < @groups; $i++ ) {
+ ok $groups[$i]->HasMember( $groups[$i-1]->id ), "has member";
+ ok $groups[$i]->HasMemberRecursively( $groups[$_]->id ), "has member"
+ foreach 0..$i-1;
+ }
+
+ ($ecode, $res) = run_validator();
+ is $res, '', 'empty result';
+}
+
+# G1 <- (G2, G3, G4, G5)
+{
+ my @groups;
+ for (2..5) {
+ my $group = load_or_create_group( 'test'. $_, Members => [] );
+ ok $group, "loaded or created a group";
+ push @groups, $group;
+ }
+
+ my $parent = load_or_create_group( 'test1', Members => \@groups );
+ ok $parent, "loaded or created a group";
+
+ my ($ecode, $res) = run_validator();
+ is $res, '', 'empty result';
+}
+
+# G1 <- (G2, G3, G4) <- G5
+{
+ my $gchild = load_or_create_group( 'test5', Members => [] );
+ ok $gchild, "loaded or created a group";
+
+ my @groups;
+ for (2..4) {
+ my $group = load_or_create_group( 'test'. $_, Members => [ $gchild ] );
+ ok $group, "loaded or created a group";
+ push @groups, $group;
+ }
+
+ my $parent = load_or_create_group( 'test1', Members => \@groups );
+ ok $parent, "loaded or created a group";
+
+ my ($ecode, $res) = run_validator();
+ is $res, '', 'empty result';
+}
+
diff --git a/rt/t/web/attachments.t b/rt/t/web/attachments.t
new file mode 100644
index 000000000..e827b2f02
--- /dev/null
+++ b/rt/t/web/attachments.t
@@ -0,0 +1,47 @@
+#!/usr/bin/perl -w
+use strict;
+
+use RT::Test tests => 14;
+
+use constant LogoFile => $RT::MasonComponentRoot .'/NoAuth/images/bplogo.gif';
+use constant FaviconFile => $RT::MasonComponentRoot .'/NoAuth/images/favicon.png';
+
+my ($baseurl, $m) = RT::Test->started_ok;
+ok $m->login, 'logged in';
+
+my $queue = RT::Queue->new($RT::Nobody);
+my $qid = $queue->Load('General');
+ok( $qid, "Loaded General queue" );
+
+$m->form_name('CreateTicketInQueue');
+$m->field('Queue', $qid);
+$m->submit;
+is($m->status, 200, "request successful");
+$m->content_like(qr/Create a new ticket/, 'ticket create page');
+
+$m->form_name('TicketCreate');
+$m->field('Subject', 'Attachments test');
+$m->field('Attach', LogoFile);
+$m->field('Content', 'Some content');
+$m->submit;
+is($m->status, 200, "request successful");
+
+$m->content_like(qr/Attachments test/, 'we have subject on the page');
+$m->content_like(qr/Some content/, 'and content');
+$m->content_like(qr/Download bplogo\.gif/, 'page has file name');
+
+$m->follow_link_ok({text => 'Reply'}, "reply to the ticket");
+$m->form_name('TicketUpdate');
+$m->field('Attach', LogoFile);
+$m->click('AddMoreAttach');
+is($m->status, 200, "request successful");
+
+$m->form_name('TicketUpdate');
+$m->field('Attach', FaviconFile);
+$m->field('UpdateContent', 'Message');
+$m->click('SubmitTicket');
+is($m->status, 200, "request successful");
+
+$m->content_like(qr/Download bplogo\.gif/, 'page has file name');
+$m->content_like(qr/Download favicon\.png/, 'page has file name');
+
diff --git a/rt/t/web/basic.t b/rt/t/web/basic.t
new file mode 100644
index 000000000..bc4d65587
--- /dev/null
+++ b/rt/t/web/basic.t
@@ -0,0 +1,146 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+use Encode;
+
+use RT::Test tests => 24;
+$RT::Test::SKIP_REQUEST_WORK_AROUND = 1;
+
+my ($baseurl, $agent) = RT::Test->started_ok;
+
+my $url = $agent->rt_base_url;
+diag $url if $ENV{TEST_VERBOSE};
+
+# get the top page
+{
+ $agent->get($url);
+ is ($agent->{'status'}, 200, "Loaded a page");
+}
+
+# test a login
+{
+ ok($agent->{form}->find_input('user'));
+ ok($agent->{form}->find_input('pass'));
+
+ ok($agent->{'content'} =~ /username:/i);
+ $agent->field( 'user' => 'root' );
+ $agent->field( 'pass' => 'password' );
+
+ # the field isn't named, so we have to click link 0
+ $agent->click(0);
+ is( $agent->{'status'}, 200, "Fetched the page ok");
+ ok( $agent->{'content'} =~ /Logout/i, "Found a logout link");
+}
+
+{
+ $agent->get($url."Ticket/Create.html?Queue=1");
+ is ($agent->{'status'}, 200, "Loaded Create.html");
+ $agent->form_number(3);
+ my $string = Encode::decode_utf8("I18N Web Testing æøå");
+ $agent->field('Subject' => "Ticket with utf8 body");
+ $agent->field('Content' => $string);
+ ok($agent->submit, "Created new ticket with $string as Content");
+ $agent->content_like( qr{$string} , "Found the content");
+ ok($agent->{redirected_uri}, "Did redirection");
+
+ {
+ my $ticket = RT::Test->last_ticket;
+ my $content = $ticket->Transactions->First->Content;
+ like(
+ $content, qr{$string},
+ 'content is there, API check'
+ );
+ }
+}
+
+{
+ $agent->get($url."Ticket/Create.html?Queue=1");
+ is ($agent->{'status'}, 200, "Loaded Create.html");
+ $agent->form_number(3);
+
+ my $string = Encode::decode_utf8("I18N Web Testing æøå");
+ $agent->field('Subject' => $string);
+ $agent->field('Content' => "Ticket with utf8 subject");
+ ok($agent->submit, "Created new ticket with $string as Content");
+ $agent->content_like( qr{$string} , "Found the content");
+ ok($agent->{redirected_uri}, "Did redirection");
+
+ {
+ my $ticket = RT::Test->last_ticket;
+ is(
+ $ticket->Subject, $string,
+ 'subject is correct, API check'
+ );
+ }
+}
+
+# Update time worked in hours
+{
+ $agent->follow_link( text_regex => qr/Basics/ );
+ $agent->submit_form( form_number => 3,
+ fields => { TimeWorked => 5, 'TimeWorked-TimeUnits' => "hours" }
+ );
+
+ like ($agent->{'content'}, qr/to &#39;300&#39;/, "5 hours is 300 minutes");
+}
+
+# {{{ test an image
+
+TODO: {
+ todo_skip("Need to handle mason trying to compile images",1);
+$agent->get( $url."NoAuth/images/test.png" );
+my $file = RT::Test::get_relocatable_file(
+ File::Spec->catfile(
+ qw(.. .. share html NoAuth images test.png)
+ )
+);
+is(
+ length($agent->content),
+ -s $file,
+ "got a file of the correct size ($file)",
+);
+}
+# }}}
+
+# {{{ Query Builder tests
+#
+# XXX: hey-ho, we have these tests in t/web/query-builder
+# TODO: move everything about QB there
+
+my $response = $agent->get($url."Search/Build.html");
+ok( $response->is_success, "Fetched " . $url."Search/Build.html" );
+
+# Parsing TicketSQL
+#
+# Adding items
+
+# set the first value
+ok($agent->form_name('BuildQuery'));
+$agent->field("AttachmentField", "Subject");
+$agent->field("AttachmentOp", "LIKE");
+$agent->field("ValueOfAttachment", "aaa");
+$agent->submit("AddClause");
+
+# set the next value
+ok($agent->form_name('BuildQuery'));
+$agent->field("AttachmentField", "Subject");
+$agent->field("AttachmentOp", "LIKE");
+$agent->field("ValueOfAttachment", "bbb");
+$agent->submit("AddClause");
+
+ok($agent->form_name('BuildQuery'));
+
+# get the query
+my $query = $agent->current_form->find_input("Query")->value;
+# strip whitespace from ends
+$query =~ s/^\s*//g;
+$query =~ s/\s*$//g;
+
+# collapse other whitespace
+$query =~ s/\s+/ /g;
+
+is ($query, "Subject LIKE 'aaa' AND Subject LIKE 'bbb'");
+
+
+1;
diff --git a/rt/t/web/cf_access.t b/rt/t/web/cf_access.t
new file mode 100644
index 000000000..1022c6da6
--- /dev/null
+++ b/rt/t/web/cf_access.t
@@ -0,0 +1,191 @@
+#!/usr/bin/perl -w
+use strict;
+
+use RT::Test tests => 26;
+$RT::Test::SKIP_REQUEST_WORK_AROUND = 1;
+
+my ($baseurl, $m) = RT::Test->started_ok;
+
+use constant ImageFile => $RT::MasonComponentRoot .'/NoAuth/images/bplogo.gif';
+use constant ImageFileContent => RT::Test->file_content(ImageFile);
+
+ok $m->login, 'logged in';
+
+diag "Create a CF" if $ENV{'TEST_VERBOSE'};
+{
+ $m->follow_link( text => 'Configuration' );
+ $m->title_is(q/RT Administration/, 'admin screen');
+ $m->follow_link( text => 'Custom Fields' );
+ $m->title_is(q/Select a Custom Field/, 'admin-cf screen');
+ $m->follow_link( text => 'Create' );
+ $m->submit_form(
+ form_name => "ModifyCustomField",
+ fields => {
+ TypeComposite => 'Image-0',
+ LookupType => 'RT::Queue-RT::Ticket',
+ Name => 'img',
+ Description => 'img',
+ },
+ );
+}
+
+diag "apply the CF to General queue" if $ENV{'TEST_VERBOSE'};
+my ( $cf, $cfid, $tid );
+{
+ $m->title_is(q/Created CustomField img/, 'admin-cf created');
+ $m->follow_link( text => 'Queues' );
+ $m->title_is(q/Admin queues/, 'admin-queues screen');
+ $m->follow_link( text => 'General' );
+ $m->title_is(q/Editing Configuration for queue General/, 'admin-queue: general');
+ $m->follow_link( text => 'Ticket Custom Fields' );
+
+ $m->title_is(q/Edit Custom Fields for General/, 'admin-queue: general cfid');
+ $m->form_name('EditCustomFields');
+
+ # Sort by numeric IDs in names
+ my @names = map { $_->[1] }
+ sort { $a->[0] <=> $b->[0] }
+ map { /Object-1-CF-(\d+)/ ? [ $1 => $_ ] : () }
+ grep defined, map $_->name, $m->current_form->inputs;
+ $cf = pop(@names);
+ $cf =~ /(\d+)$/ or die "Hey this is impossible dude";
+ $cfid = $1;
+ $m->field( $cf => 1 ); # Associate the new CF with this queue
+ $m->field( $_ => undef ) for @names; # ...and not any other. ;-)
+ $m->submit;
+
+ $m->content_like( qr/Object created/, 'TCF added to the queue' );
+}
+
+my $tester = RT::Test->load_or_create_user( Name => 'tester', Password => '123456' );
+RT::Test->set_rights(
+ { Principal => $tester->PrincipalObj,
+ Right => [qw(SeeQueue ShowTicket CreateTicket)],
+ },
+);
+ok $m->login( $tester->Name, 123456), 'logged in';
+
+diag "check that we have no the CF on the create"
+ ." ticket page when user has no SeeCustomField right"
+ if $ENV{'TEST_VERBOSE'};
+{
+ $m->submit_form(
+ form_name => "CreateTicketInQueue",
+ fields => { Queue => 'General' },
+ );
+ $m->content_unlike(qr/Upload multiple images/, 'has no upload image field');
+
+ my $form = $m->form_name("TicketCreate");
+ my $upload_field = "Object-RT::Ticket--CustomField-$cfid-Upload";
+ ok !$form->find_input( $upload_field ), 'no form field on the page';
+
+ $m->submit_form(
+ form_name => "TicketCreate",
+ fields => { Subject => 'test' },
+ );
+ $m->content_like(qr/Ticket \d+ created/, "a ticket is created succesfully");
+
+ $m->content_unlike(qr/img:/, 'has no img field on the page');
+ $m->follow_link( text => 'Custom Fields');
+ $m->content_unlike(qr/Upload multiple images/, 'has no upload image field');
+}
+
+RT::Test->set_rights(
+ { Principal => $tester->PrincipalObj,
+ Right => [qw(SeeQueue ShowTicket CreateTicket SeeCustomField)],
+ },
+);
+
+diag "check that we have no the CF on the create"
+ ." ticket page when user has no ModifyCustomField right"
+ if $ENV{'TEST_VERBOSE'};
+{
+ $m->submit_form(
+ form_name => "CreateTicketInQueue",
+ fields => { Queue => 'General' },
+ );
+ $m->content_unlike(qr/Upload multiple images/, 'has no upload image field');
+
+ my $form = $m->form_name("TicketCreate");
+ my $upload_field = "Object-RT::Ticket--CustomField-$cfid-Upload";
+ ok !$form->find_input( $upload_field ), 'no form field on the page';
+
+ $m->submit_form(
+ form_name => "TicketCreate",
+ fields => { Subject => 'test' },
+ );
+ $tid = $1 if $m->content =~ /Ticket (\d+) created/i;
+ ok $tid, "a ticket is created succesfully";
+
+ $m->follow_link( text => 'Custom Fields' );
+ $m->content_unlike(qr/Upload multiple images/, 'has no upload image field');
+ $form = $m->form_number(3);
+ $upload_field = "Object-RT::Ticket-$tid-CustomField-$cfid-Upload";
+ ok !$form->find_input( $upload_field ), 'no form field on the page';
+}
+
+RT::Test->set_rights(
+ { Principal => $tester->PrincipalObj,
+ Right => [qw(SeeQueue ShowTicket CreateTicket SeeCustomField ModifyCustomField)],
+ },
+);
+
+diag "create a ticket with an image" if $ENV{'TEST_VERBOSE'};
+{
+ $m->submit_form(
+ form_name => "CreateTicketInQueue",
+ fields => { Queue => 'General' },
+ );
+ $m->content_like(qr/Upload multiple images/, 'has a upload image field');
+
+ $cf =~ /(\d+)$/ or die "Hey this is impossible dude";
+ my $upload_field = "Object-RT::Ticket--CustomField-$1-Upload";
+
+ $m->submit_form(
+ form_name => "TicketCreate",
+ fields => {
+ $upload_field => ImageFile,
+ Subject => 'testing img cf creation',
+ },
+ );
+
+ $m->content_like(qr/Ticket \d+ created/, "a ticket is created succesfully");
+
+ $tid = $1 if $m->content =~ /Ticket (\d+) created/;
+
+ $m->title_like(qr/testing img cf creation/, "its title is the Subject");
+
+ $m->follow_link( text => 'bplogo.gif' );
+ $m->content_is(ImageFileContent, "it links to the uploaded image");
+}
+
+$m->get( $m->rt_base_url );
+$m->follow_link( text => 'Tickets' );
+$m->follow_link( text => 'New Query' );
+
+$m->title_is(q/Query Builder/, 'Query building');
+$m->submit_form(
+ form_name => "BuildQuery",
+ fields => {
+ idOp => '=',
+ ValueOfid => $tid,
+ ValueOfQueue => 'General',
+ },
+ button => 'AddClause',
+);
+
+$m->form_name('BuildQuery');
+
+my $col = ($m->current_form->find_input('SelectDisplayColumns'))[-1];
+$col->value( ($col->possible_values)[-1] );
+
+$m->click('AddCol');
+
+$m->form_name('BuildQuery');
+$m->click('DoSearch');
+
+$m->follow_link( text_regex => qr/bplogo\.gif/ );
+$m->content_is(ImageFileContent, "it links to the uploaded image");
+
+__END__
+[FC] Bulk Update does not have custom fields.
diff --git a/rt/t/web/cf_onqueue.t b/rt/t/web/cf_onqueue.t
new file mode 100644
index 000000000..dcd585277
--- /dev/null
+++ b/rt/t/web/cf_onqueue.t
@@ -0,0 +1,66 @@
+#!/usr/bin/perl -w
+use strict;
+
+use RT::Test tests => 14;
+my ($baseurl, $m) = RT::Test->started_ok;
+
+ok $m->login, 'logged in';
+
+diag "Create a queue CF" if $ENV{'TEST_VERBOSE'};
+{
+ $m->follow_link( text => 'Configuration' );
+ $m->title_is(q/RT Administration/, 'admin screen');
+ $m->follow_link( text => 'Custom Fields' );
+ $m->title_is(q/Select a Custom Field/, 'admin-cf screen');
+ $m->follow_link( text => 'Create' );
+ $m->submit_form(
+ form_name => "ModifyCustomField",
+ fields => {
+ TypeComposite => 'Freeform-1',
+ LookupType => 'RT::Queue',
+ Name => 'QueueCFTest',
+ Description => 'QueueCFTest',
+ },
+ );
+ $m->content_like( qr/Object created/, 'CF QueueCFTest created' );
+}
+
+diag "Apply the new CF globally" if $ENV{'TEST_VERBOSE'};
+{
+ $m->follow_link( text => 'Global' );
+ $m->title_is(q!Admin/Global configuration!, 'global configuration screen');
+ $m->follow_link( url_regex => qr!Admin/Global/CustomFields/index! );
+ $m->title_is(q/Global custom field configuration/, 'global custom field configuration screen');
+ $m->follow_link( url => 'Queues.html' );
+ $m->title_is(q/Edit Custom Fields for all queues/, 'global custom field for all queues configuration screen');
+ $m->content_like( qr/QueueCFTest/, 'CF QueueCFTest displayed on page' );
+ $m->submit_form(
+ form_name => "EditCustomFields",
+ fields => {
+ 'Object--CF-1' => '1',
+ },
+ );
+ $m->content_like( qr/Object created/, 'CF QueueCFTest enabled globally' );
+}
+
+diag "Edit the CF value for default queue" if $ENV{'TEST_VERBOSE'};
+{
+ $m->follow_link( url => '/Admin/Queues/' );
+ $m->title_is(q/Admin queues/, 'queues configuration screen');
+ $m->follow_link( text => "1" );
+ $m->title_is(q/Editing Configuration for queue General/, 'default queue configuration screen');
+ $m->content_like( qr/QueueCFTest/, 'CF QueueCFTest displayed on default queue' );
+ $m->submit_form(
+ form_number => 3,
+ # The following doesn't want to works :(
+ #with_fields => { 'Object-RT::Queue-1-CustomField-1-Value' },
+ fields => {
+ 'Object-RT::Queue-1-CustomField-1-Value' => 'QueueCFTest content',
+ },
+ );
+ $m->content_like( qr/QueueCFTest QueueCFTest content added/, 'Content filed in CF QueueCFTest for default queue' );
+
+}
+
+
+__END__
diff --git a/rt/t/web/cf_select_one.t b/rt/t/web/cf_select_one.t
new file mode 100644
index 000000000..e009af7cf
--- /dev/null
+++ b/rt/t/web/cf_select_one.t
@@ -0,0 +1,159 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use RT::Test tests => 41;
+
+my ($baseurl, $m) = RT::Test->started_ok;
+ok $m->login, 'logged in as root';
+
+my $cf_name = 'test select one value';
+
+my $cfid;
+diag "Create a CF" if $ENV{'TEST_VERBOSE'};
+{
+ $m->follow_link( text => 'Configuration' );
+ $m->title_is(q/RT Administration/, 'admin screen');
+ $m->follow_link( text => 'Custom Fields' );
+ $m->title_is(q/Select a Custom Field/, 'admin-cf screen');
+ $m->follow_link( text => 'Create' );
+ $m->submit_form(
+ form_name => "ModifyCustomField",
+ fields => {
+ Name => $cf_name,
+ TypeComposite => 'Select-1',
+ LookupType => 'RT::Queue-RT::Ticket',
+ },
+ );
+ $m->content_like( qr/Object created/, 'created CF sucessfully' );
+ $cfid = $m->form_name('ModifyCustomField')->value('id');
+ ok $cfid, "found id of the CF in the form, it's #$cfid";
+}
+
+diag "add 'qwe', 'ASD' and '0' as values to the CF" if $ENV{'TEST_VERBOSE'};
+{
+ foreach my $value(qw(qwe ASD 0)) {
+ $m->submit_form(
+ form_name => "ModifyCustomField",
+ fields => {
+ "CustomField-". $cfid ."-Value-new-Name" => $value,
+ },
+ button => 'Update',
+ );
+ $m->content_like( qr/Object created/, 'added a value to the CF' ); # or diag $m->content;
+ }
+}
+
+my $queue = RT::Test->load_or_create_queue( Name => 'General' );
+ok $queue && $queue->id, 'loaded or created queue';
+
+diag "apply the CF to General queue" if $ENV{'TEST_VERBOSE'};
+{
+ $m->follow_link( text => 'Queues' );
+ $m->title_is(q/Admin queues/, 'admin-queues screen');
+ $m->follow_link( text => 'General' );
+ $m->title_is(q/Editing Configuration for queue General/, 'admin-queue: general');
+ $m->follow_link( text => 'Ticket Custom Fields' );
+ $m->title_is(q/Edit Custom Fields for General/, 'admin-queue: general cfid');
+
+ $m->form_name('EditCustomFields');
+ $m->field( "Object-". $queue->id ."-CF-$cfid" => 1 );
+ $m->submit;
+
+ $m->content_like( qr/Object created/, 'TCF added to the queue' );
+}
+
+my $tid;
+diag "create a ticket using API with 'asd'(not 'ASD') as value of the CF"
+ if $ENV{'TEST_VERBOSE'};
+{
+ my $ticket = RT::Ticket->new( $RT::SystemUser );
+ my ($txnid, $msg);
+ ($tid, $txnid, $msg) = $ticket->Create(
+ Subject => 'test',
+ Queue => $queue->id,
+ "CustomField-$cfid" => 'asd',
+ );
+ ok $tid, "created ticket";
+ diag $msg if $msg && $ENV{'TEST_VERBOSE'};
+
+ # we use lc as we really don't care about case
+ # so if later we'll add canonicalization of value
+ # test should work
+ is lc $ticket->FirstCustomFieldValue( $cf_name ),
+ 'asd', 'assigned value of the CF';
+}
+
+diag "check that values of the CF are case insensetive(asd vs. ASD)"
+ if $ENV{'TEST_VERBOSE'};
+{
+ ok $m->goto_ticket( $tid ), "opened ticket's page";
+ $m->follow_link( text => 'Custom Fields' );
+ $m->title_like(qr/Modify ticket/i, 'modify ticket');
+ $m->content_like(qr/\Q$cf_name/, 'CF on the page');
+
+ my $value = $m->form_number(3)->value("Object-RT::Ticket-$tid-CustomField-$cfid-Values");
+ is lc $value, 'asd', 'correct value is selected';
+ $m->submit;
+ $m->content_unlike(qr/\Q$cf_name\E.*?changed/mi, 'field is not changed');
+
+ $value = $m->form_number(3)->value("Object-RT::Ticket-$tid-CustomField-$cfid-Values");
+ is lc $value, 'asd', 'the same value is still selected';
+
+ my $ticket = RT::Ticket->new( $RT::SystemUser );
+ $ticket->Load( $tid );
+ ok $ticket->id, 'loaded the ticket';
+ is lc $ticket->FirstCustomFieldValue( $cf_name ),
+ 'asd', 'value is still the same';
+}
+
+diag "check that 0 is ok value of the CF"
+ if $ENV{'TEST_VERBOSE'};
+{
+ ok $m->goto_ticket( $tid ), "opened ticket's page";
+ $m->follow_link( text => 'Custom Fields' );
+ $m->title_like(qr/Modify ticket/i, 'modify ticket');
+ $m->content_like(qr/\Q$cf_name/, 'CF on the page');
+
+ my $value = $m->form_number(3)->value("Object-RT::Ticket-$tid-CustomField-$cfid-Values");
+ is lc $value, 'asd', 'correct value is selected';
+ $m->select("Object-RT::Ticket-$tid-CustomField-$cfid-Values" => 0 );
+ $m->submit;
+ $m->content_like(qr/\Q$cf_name\E.*?changed/mi, 'field is changed');
+ $m->content_unlike(qr/0 is no longer a value for custom field/mi, 'no bad message in results');
+
+ $value = $m->form_number(3)->value("Object-RT::Ticket-$tid-CustomField-$cfid-Values");
+ is lc $value, '0', 'new value is selected';
+
+ my $ticket = RT::Ticket->new( $RT::SystemUser );
+ $ticket->Load( $tid );
+ ok $ticket->id, 'loaded the ticket';
+ is lc $ticket->FirstCustomFieldValue( $cf_name ),
+ '0', 'API returns correct value';
+}
+
+diag "check that we can set empty value when the current is 0"
+ if $ENV{'TEST_VERBOSE'};
+{
+ ok $m->goto_ticket( $tid ), "opened ticket's page";
+ $m->follow_link( text => 'Custom Fields' );
+ $m->title_like(qr/Modify ticket/i, 'modify ticket');
+ $m->content_like(qr/\Q$cf_name/, 'CF on the page');
+
+ my $value = $m->form_number(3)->value("Object-RT::Ticket-$tid-CustomField-$cfid-Values");
+ is lc $value, '0', 'correct value is selected';
+ $m->select("Object-RT::Ticket-$tid-CustomField-$cfid-Values" => '' );
+ $m->submit;
+ $m->content_like(qr/0 is no longer a value for custom field/mi, '0 is no longer a value');
+
+ $value = $m->form_number(3)->value("Object-RT::Ticket-$tid-CustomField-$cfid-Values");
+ is $value, '', '(no value) is selected';
+
+ my $ticket = RT::Ticket->new( $RT::SystemUser );
+ $ticket->Load( $tid );
+ ok $ticket->id, 'loaded the ticket';
+ is $ticket->FirstCustomFieldValue( $cf_name ),
+ undef, 'API returns correct value';
+}
+
diff --git a/rt/t/web/command_line.t b/rt/t/web/command_line.t
new file mode 100644
index 000000000..3fc279bf3
--- /dev/null
+++ b/rt/t/web/command_line.t
@@ -0,0 +1,544 @@
+#!/usr/bin/perl -w
+
+use strict;
+use File::Spec ();
+use Test::Expect;
+use RT::Test tests => 295;
+my ($baseurl, $m) = RT::Test->started_ok;
+
+use RT::User;
+use RT::Queue;
+
+my $rt_tool_path = "$RT::BinPath/rt";
+
+# {{{ test configuration options
+
+# config directives:
+# (in $CWD/.rtrc)
+# - server <URL> URL to RT server.
+# - user <username> RT username.
+# - passwd <passwd> RT user's password.
+# - query <RT Query> Default RT Query for list action
+# - orderby <order> Default RT order for list action
+#
+# Blank and #-commented lines are ignored.
+
+# environment variables
+# The following environment variables override any corresponding
+# values defined in configuration files:
+#
+# - RTUSER
+$ENV{'RTUSER'} = 'root';
+# - RTPASSWD
+$ENV{'RTPASSWD'} = 'password';
+# - RTSERVER
+$RT::Logger->debug("Connecting to server at ".RT->Config->Get('WebBaseURL'));
+$ENV{'RTSERVER'} =RT->Config->Get('WebBaseURL') ;
+# - RTDEBUG Numeric debug level. (Set to 3 for full logs.)
+$ENV{'RTDEBUG'} = '1';
+# - RTCONFIG Specifies a name other than ".rtrc" for the
+# configuration file.
+#
+# - RTQUERY Default RT Query for rt list
+# - RTORDERBY Default order for rt list
+
+
+# }}}
+
+# {{{ test ticket manipulation
+
+# create a ticket
+expect_run(
+ command => "$rt_tool_path shell",
+ prompt => 'rt> ',
+ quit => 'quit',
+);
+expect_send(q{create -t ticket set subject='new ticket' add cc=foo@example.com}, "Creating a ticket...");
+expect_like(qr/Ticket \d+ created/, "Created the ticket");
+expect_handle->before() =~ /Ticket (\d+) created/;
+my $ticket_id = $1;
+ok($ticket_id, "Got ticket id=$ticket_id");
+expect_send(q{create -t ticket set subject='new ticket'}, "Creating a ticket as just a subject...");
+expect_like(qr/Ticket \d+ created/, "Created the ticket");
+
+# make sure we can request things as 'rt foo'
+expect_send(q{rt create -t ticket set subject='rt ticket'}, "Creating a ticket with 'rt create'...");
+expect_like(qr/Ticket \d+ created/, "Created the ticket");
+
+# {{{ test queue manipulation
+
+# creating queues
+expect_send("create -t queue set Name='NewQueue$$'", 'Creating a queue...');
+expect_like(qr/Queue \d+ created/, 'Created the queue');
+expect_handle->before() =~ /Queue (\d+) created/;
+my $queue_id = $1;
+ok($queue_id, "Got queue id=$queue_id");
+# updating users
+expect_send("edit queue/$queue_id set Name='EditedQueue$$'", 'Editing the queue');
+expect_like(qr/Queue $queue_id updated/, 'Edited the queue');
+expect_send("show queue/$queue_id", 'Showing the queue...');
+expect_like(qr/id: queue\/$queue_id/, 'Saw the queue');
+expect_like(qr/Name: EditedQueue$$/, 'Saw the modification');
+TODO: {
+ todo_skip "Listing non-ticket items doesn't work", 2;
+ expect_send("list -t queue 'id > 0'", 'Listing the queues...');
+ expect_like(qr/$queue_id: EditedQueue$$/, 'Found the queue');
+}
+
+# }}}
+
+
+# Set up a custom field for editing tests
+my $cf = RT::CustomField->new($RT::SystemUser);
+my ($val,$msg) = $cf->Create(Name => 'MyCF'.$$, Type => 'FreeformSingle', Queue => $queue_id);
+ok($val,$msg);
+
+my $othercf = RT::CustomField->new($RT::SystemUser);
+($val,$msg) = $othercf->Create(Name => 'My CF'.$$, Type => 'FreeformSingle', Queue => $queue_id);
+ok($val,$msg);
+
+my $multiple_cf = RT::CustomField->new($RT::SystemUser);
+($val,$msg) = $multiple_cf->Create(Name => 'MultipleCF'.$$, Type =>
+ 'FreeformMultiple', Queue => $queue_id);
+ok($val,$msg);
+
+
+# add a comment to ticket
+ expect_send("comment -m 'comment-$$' $ticket_id", "Adding a comment...");
+ expect_like(qr/Message recorded/, "Added the comment");
+ ### should test to make sure it actually got added
+ # add correspondance to ticket (?)
+ expect_send("correspond -m 'correspond-$$' $ticket_id", "Adding correspondence...");
+ expect_like(qr/Message recorded/, "Added the correspondence");
+ ### should test to make sure it actually got added
+
+ my $test_email = RT::Test::get_relocatable_file('lorem-ipsum',
+ (File::Spec->updir(), 'data', 'emails'));
+ # add attachments to a ticket
+ # text attachment
+ check_attachment($test_email);
+ # binary attachment
+ check_attachment($RT::MasonComponentRoot.'/NoAuth/images/bplogo.gif');
+
+# change a ticket's Owner
+expect_send("edit ticket/$ticket_id set owner=root", 'Changing owner...');
+expect_like(qr/Ticket $ticket_id updated/, 'Changed owner');
+expect_send("show ticket/$ticket_id -f owner", 'Verifying change...');
+expect_like(qr/Owner: root/, 'Verified change');
+# change a ticket's Requestor
+expect_send("edit ticket/$ticket_id set requestors=foo\@example.com", 'Changing Requestor...');
+expect_like(qr/Ticket $ticket_id updated/, 'Changed Requestor');
+expect_send("show ticket/$ticket_id -f requestors", 'Verifying change...');
+expect_like(qr/Requestors: foo\@example.com/, 'Verified change');
+# change a ticket's Cc
+expect_send("edit ticket/$ticket_id set cc=bar\@example.com", 'Changing Cc...');
+expect_like(qr/Ticket $ticket_id updated/, 'Changed Cc');
+expect_send("show ticket/$ticket_id -f cc", 'Verifying change...');
+expect_like(qr/Cc: bar\@example.com/, 'Verified change');
+# change a ticket's priority
+expect_send("edit ticket/$ticket_id set priority=10", 'Changing priority...');
+expect_like(qr/Ticket $ticket_id updated/, 'Changed priority');
+expect_send("show ticket/$ticket_id -f priority", 'Verifying change...');
+expect_like(qr/Priority: 10/, 'Verified change');
+# move a ticket to a different queue
+expect_send("edit ticket/$ticket_id set queue=EditedQueue$$", 'Changing queue...');
+expect_like(qr/Ticket $ticket_id updated/, 'Changed queue');
+expect_send("show ticket/$ticket_id -f queue", 'Verifying change...');
+expect_like(qr/Queue: EditedQueue$$/, 'Verified change');
+# cannot move ticket to a nonexistent queue
+expect_send("edit ticket/$ticket_id set queue=nonexistent-$$", 'Changing to nonexistent queue...');
+expect_like(qr/queue does not exist/i, 'Errored out');
+expect_send("show ticket/$ticket_id -f queue", 'Verifying lack of change...');
+expect_like(qr/Queue: EditedQueue$$/, 'Verified lack of change');
+
+# Test reading and setting custom fields without spaces
+expect_send("show ticket/$ticket_id -f CF-myCF$$", 'Checking initial value');
+expect_like(qr/CF\.{myCF$$}:/i, 'Verified initial empty value (CF-x syntax)');
+expect_send("show ticket/$ticket_id -f CF.{myCF$$}", 'Checking initial value');
+expect_like(qr/CF\.{myCF$$}:/i, 'Verified initial empty value (CF.{x} syntax)');
+
+expect_send("edit ticket/$ticket_id set 'CF-myCF$$=VALUE' ", 'Changing CF...');
+expect_like(qr/Ticket $ticket_id updated/, 'Changed cf');
+expect_send("show ticket/$ticket_id -f CF-myCF$$", 'Checking new value');
+expect_like(qr/CF\.{myCF$$}: VALUE/i, 'Verified change');
+# Test setting 0 as value of the custom field
+expect_send("edit ticket/$ticket_id set 'CF-myCF$$=0' ", 'Changing CF...');
+expect_like(qr/Ticket $ticket_id updated/, 'Changed cf');
+expect_send("show ticket/$ticket_id -f CF-myCF$$", 'Checking new value');
+expect_like(qr/CF\.{myCF$$}: 0/i, 'Verified change');
+
+expect_send("edit ticket/$ticket_id set 'CF.{myCF$$}=VALUE' ",'Changing CF...');
+expect_like(qr/Ticket $ticket_id updated/, 'Changed cf');
+expect_send("show ticket/$ticket_id -f CF.{myCF$$}", 'Checking new value');
+expect_like(qr/CF\.{myCF$$}: VALUE/i, 'Verified change');
+# Test setting 0 as value of the custom field
+expect_send("edit ticket/$ticket_id set 'CF.{myCF$$}=0' ", 'Changing CF...');
+expect_like(qr/Ticket $ticket_id updated/, 'Changed cf');
+expect_send("show ticket/$ticket_id -f CF.{myCF$$}", 'Checking new value');
+expect_like(qr/CF\.{myCF$$}: 0/i, 'Verified change');
+
+# Test reading and setting custom fields with spaces
+expect_send("show ticket/$ticket_id -f 'CF-my CF$$'", 'Checking initial value');
+expect_like(qr/CF\.{my CF$$}:/i, 'Verified change');
+expect_send("edit ticket/$ticket_id set 'CF-my CF$$=VALUE' ", 'Changing CF...');
+expect_like(qr/Ticket $ticket_id updated/, 'Changed cf');
+expect_send("show ticket/$ticket_id -f 'CF-my CF$$'", 'Checking new value');
+expect_like(qr/CF\.{my CF$$}: VALUE/i, 'Verified change');
+expect_send("ls -l 'id = $ticket_id' -f 'CF-my CF$$'", 'Checking new value');
+expect_like(qr/CF\.{my CF$$}: VALUE/i, 'Verified change');
+
+expect_send("show ticket/$ticket_id -f 'CF.{my CF$$}'", 'Checking initial value');
+expect_like(qr/CF\.{my CF$$}: VALUE/i, 'Verified change');
+expect_send("edit ticket/$ticket_id set 'CF.{my CF$$}=NEW' ", 'Changing CF...');
+expect_send("show ticket/$ticket_id -f 'CF.{my CF$$}'", 'Checking new value');
+expect_like(qr/CF\.{my CF$$}: NEW/i, 'Verified change');
+expect_send("ls -l 'id = $ticket_id' -f 'CF.{my CF$$}'", 'Checking new value');
+expect_like(qr/CF\.{my CF$$}: NEW/i, 'Verified change');
+
+# Test reading and setting single value custom field with commas or quotes
+expect_send("show ticket/$ticket_id -f CF-myCF$$", 'Checking initial value');
+expect_like(qr/CF\.{myCF$$}:/i, 'Verified change');
+expect_send("edit ticket/$ticket_id set CF-myCF$$=1,2,3", 'Changing CF...');
+expect_like(qr/Ticket $ticket_id updated/, 'Changed cf');
+expect_send("show ticket/$ticket_id -f CF-myCF$$", 'Checking new value');
+expect_like(qr/CF\.{myCF$$}: 1,2,3/i, 'Verified change');
+expect_send("edit ticket/$ticket_id set CF-myCF$$=\"1's,2,3\"", 'Changing CF...');
+expect_like(qr/Ticket $ticket_id updated/, 'Changed cf');
+expect_send("show ticket/$ticket_id -f CF-myCF$$", 'Checking new value');
+expect_like(qr/CF\.{myCF$$}: 1's,2,3/i, 'Verified change');
+
+# Test reading and setting custom fields with multiple values
+expect_send("show ticket/$ticket_id -f CF-MultipleCF$$", 'Checking initial value');
+expect_like(qr/CF\.{MultipleCF$$}:/i, 'Verified multiple cf change');
+expect_send("edit ticket/$ticket_id set CF.{MultipleCF$$}=1,2,3 ", 'Changing CF...');
+expect_like(qr/Ticket $ticket_id updated/, 'Changed multiple cf');
+expect_send("show ticket/$ticket_id -f CF.{MultipleCF$$}", 'Checking new value');
+expect_like(qr/CF\.{MultipleCF$$}: 1,\s*2,\s*3/i, 'Verified multiple cf change');
+expect_send("edit ticket/$ticket_id set CF.{MultipleCF$$}=a,b,c ", 'Changing CF...');
+expect_like(qr/Ticket $ticket_id updated/, 'Changed multiple cf');
+expect_send("show ticket/$ticket_id -f CF.{MultipleCF$$}", 'Checking new value');
+expect_like(qr/CF\.{MultipleCF$$}: a,\s*b,\s*c/i, 'Verified change');
+expect_send("edit ticket/$ticket_id del CF.{MultipleCF$$}=a", 'Changing CF...');
+expect_like(qr/Ticket $ticket_id updated/, 'del multiple cf');
+expect_send("show ticket/$ticket_id -f CF.{MultipleCF$$}", 'Checking new value');
+expect_like(qr/CF\.{MultipleCF$$}: b,\s*c/i, 'Verified multiple cf change');
+expect_send("edit ticket/$ticket_id add CF.{MultipleCF$$}=o", 'Changing CF...');
+expect_like(qr/Ticket $ticket_id updated/, 'Changed multiple cf');
+expect_send("show ticket/$ticket_id -f CF.{MultipleCF$$}", 'Checking new value');
+expect_like(qr/CF\.{MultipleCF$$}: b,\s*c,\s*o/i, 'Verified multiple cf change');
+
+expect_send("edit ticket/$ticket_id set CF.{MultipleCF$$}=\"'a,b,c'\" ", 'Changing CF...');
+expect_like(qr/Ticket $ticket_id updated/, 'Changed multiple cf');
+expect_send("show ticket/$ticket_id -f CF.{MultipleCF$$}", 'Checking new value');
+expect_like(qr/CF\.{MultipleCF$$}: 'a,b,c'/i, 'Verified change');
+expect_send("edit ticket/$ticket_id del CF.{MultipleCF$$}=a", 'Changing CF...');
+expect_like(qr/Ticket $ticket_id updated/, 'Changed multiple cf');
+expect_send("show ticket/$ticket_id -f CF.{MultipleCF$$}", 'Checking new value');
+expect_like(qr/CF\.{MultipleCF$$}: 'a,b,c'/i, 'Verified change');
+
+expect_send("edit ticket/$ticket_id set CF.{MultipleCF$$}=q{a,b,c}", 'Changing CF...');
+expect_like(qr/Ticket $ticket_id updated/, 'Changed multiple cf');
+expect_send("show ticket/$ticket_id -f CF.{MultipleCF$$}", 'Checking new value');
+expect_like(qr/CF\.{MultipleCF$$}: 'a,b,c'/i, 'Verified change');
+expect_send("edit ticket/$ticket_id del CF.{MultipleCF$$}=a", 'Changing CF...');
+expect_like(qr/Ticket $ticket_id updated/, 'Changed multiple cf');
+expect_send("show ticket/$ticket_id -f CF.{MultipleCF$$}", 'Checking new value');
+expect_like(qr/CF\.{MultipleCF$$}: 'a,b,c'/i, 'Verified change');
+expect_send("edit ticket/$ticket_id del CF.{MultipleCF$$}=\"'a,b,c'\"", 'Changing CF...');
+expect_like(qr/Ticket $ticket_id updated/, 'Changed multiple cf');
+expect_send("show ticket/$ticket_id -f CF.{MultipleCF$$}", 'Checking new value');
+expect_like(qr/CF\.{MultipleCF$$}: \s*$/i, 'Verified change');
+
+expect_send("edit ticket/$ticket_id set CF.{MultipleCF$$}=\"q{1,2's,3}\"", 'Changing CF...');
+expect_like(qr/Ticket $ticket_id updated/, 'Changed multiple cf');
+expect_send("show ticket/$ticket_id -f CF.{MultipleCF$$}", 'Checking new value');
+expect_like(qr/CF\.{MultipleCF$$}: '1,2\\'s,3'/i, 'Verified change');
+
+# ...
+# change a ticket's ...[other properties]...
+# ...
+# stall a ticket
+expect_send("edit ticket/$ticket_id set status=stalled", 'Changing status to "stalled"...');
+expect_like(qr/Ticket $ticket_id updated/, 'Changed status');
+expect_send("show ticket/$ticket_id -f status", 'Verifying change...');
+expect_like(qr/Status: stalled/, 'Verified change');
+# resolve a ticket
+expect_send("edit ticket/$ticket_id set status=resolved", 'Changing status to "resolved"...');
+expect_like(qr/Ticket $ticket_id updated/, 'Changed status');
+expect_send("show ticket/$ticket_id -f status", 'Verifying change...');
+expect_like(qr/Status: resolved/, 'Verified change');
+# try to set status to an illegal value
+expect_send("edit ticket/$ticket_id set status=quux", 'Changing status to an illegal value...');
+expect_like(qr/illegal value/i, 'Errored out');
+expect_send("show ticket/$ticket_id -f status", 'Verifying lack of change...');
+expect_like(qr/Status: resolved/, 'Verified change');
+
+# }}}
+
+# {{{ display
+
+# show ticket list
+expect_send("ls -s -t ticket -o +id \"Status='resolved'\"", 'Listing resolved tickets...');
+expect_like(qr/$ticket_id: new ticket/, 'Found our ticket');
+# show ticket list verbosely
+expect_send("ls -l -t ticket -o +id \"Status='resolved'\"", 'Listing resolved tickets verbosely...');
+expect_like(qr/id: ticket\/$ticket_id/, 'Found our ticket');
+# show ticket
+expect_send("show -s -t ticket $ticket_id", 'Showing our ticket...');
+expect_like(qr/id: ticket\/$ticket_id/, 'Got our ticket');
+# show ticket history
+expect_send("show ticket/$ticket_id/history", 'Showing our ticket\'s history...');
+expect_like(qr/Ticket created by root/, 'Got our history');
+
+expect_send("show -v ticket/$ticket_id/history", 'Showing our ticket\'s history verbosely...');
+TODO: {
+ local $TODO = "Cannot show verbose ticket history right now";
+ # show ticket history verbosely
+ expect_like(qr/Ticket created by root/, 'Got our history');
+}
+# get attachments from a ticket
+expect_send("show -s ticket/$ticket_id/attachments", 'Showing ticket attachments...');
+expect_like(qr/id: ticket\/$ticket_id\/attachments/, 'Got our ticket\'s attachments');
+expect_like(qr/Attachments: \d+: \(Unnamed\) \(\S+ \/ \d+\w+\)/, 'Our ticket has an attachment');
+expect_handle->before() =~ /Attachments: (\d+): \(Unnamed\) \((\S+)/;
+my $attachment_id = $1;
+my $attachment_type = $2;
+ok($attachment_id, "Got attachment id=$attachment_id $attachment_type");
+expect_send("show -s ticket/$ticket_id/attachments/$attachment_id", "Showing attachment $attachment_id...");
+expect_like(qr/ContentType: $attachment_type/, 'Got the attachment');
+
+# }}}
+
+# {{{ test user manipulation
+
+# creating users
+expect_send("create -t user set Name='NewUser$$' EmailAddress='fbar$$\@example.com'", 'Creating a user...');
+expect_like(qr/User \d+ created/, 'Created the user');
+expect_handle->before() =~ /User (\d+) created/;
+my $user_id = $1;
+ok($user_id, "Got user id=$user_id");
+# updating users
+expect_send("edit user/$user_id set Name='EditedUser$$'", 'Editing the user');
+expect_like(qr/User $user_id updated/, 'Edited the user');
+expect_send("show user/$user_id", 'Showing the user...');
+expect_like(qr/id: user\/$user_id/, 'Saw the user');
+expect_like(qr/Name: EditedUser$$/, 'Saw the modification');
+TODO: {
+ todo_skip "Listing non-ticket items doesn't work", 2;
+ expect_send("list -t user 'id > 0'", 'Listing the users...');
+ expect_like(qr/$user_id: EditedUser$$/, 'Found the user');
+}
+
+# }}}
+
+# {{{ test group manipulation
+
+TODO: {
+todo_skip "Group manipulation doesn't work right now", 8;
+# creating groups
+expect_send("create -t group set Name='NewGroup$$'", 'Creating a group...');
+expect_like(qr/Group \d+ created/, 'Created the group');
+expect_handle->before() =~ /Group (\d+) created/;
+my $group_id = $1;
+ok($group_id, "Got group id=$group_id");
+# updating groups
+expect_send("edit group/$group_id set Name='EditedGroup$$'", 'Editing the group');
+expect_like(qr/Group $group_id updated/, 'Edited the group');
+expect_send("show group/$group_id", 'Showing the group...');
+expect_like(qr/id: group\/$group_id/, 'Saw the group');
+expect_like(qr/Name: EditedGroup$$/, 'Saw the modification');
+TODO: {
+ local $TODO = "Listing non-ticket items doesn't work";
+ expect_send("list -t group 'id > 0'", 'Listing the groups...');
+ expect_like(qr/$group_id: EditedGroup$$/, 'Found the group');
+}
+}
+
+# }}}
+
+TODO: {
+todo_skip "Custom field manipulation not yet implemented", 8;
+# {{{ test custom field manipulation
+
+# creating custom fields
+expect_send("create -t custom_field set Name='NewCF$$'", 'Creating a custom field...');
+expect_like(qr/Custom Field \d+ created/, 'Created the custom field');
+expect_handle->before() =~ /Custom Field (\d+) created/;
+my $cf_id = $1;
+ok($cf_id, "Got custom field id=$cf_id");
+# updating custom fields
+expect_send("edit cf/$cf_id set Name='EditedCF$$'", 'Editing the custom field');
+expect_like(qr/Custom field $cf_id updated/, 'Edited the custom field');
+expect_send("show cf/$cf_id", 'Showing the queue...');
+expect_like(qr/id: custom_field\/$cf_id/, 'Saw the custom field');
+expect_like(qr/Name: EditedCF$$/, 'Saw the modification');
+TODO: {
+ todo_skip "Listing non-ticket items doesn't work", 2;
+ expect_send("list -t custom_field 'id > 0'", 'Listing the CFs...');
+ expect_like(qr/$cf_id: EditedCF$$/, 'Found the custom field');
+}
+}
+
+# }}}
+
+# {{{ test merging tickets
+expect_send("create -t ticket set subject='CLIMergeTest1-$$'", 'Creating first ticket to merge...');
+expect_like(qr/Ticket \d+ created/, 'Created first ticket');
+expect_handle->before() =~ /Ticket (\d+) created/;
+my $merge_ticket_A = $1;
+ok($merge_ticket_A, "Got first ticket to merge id=$merge_ticket_A");
+expect_send("create -t ticket set subject='CLIMergeTest2-$$'", 'Creating second ticket to merge...');
+expect_like(qr/Ticket \d+ created/, 'Created second ticket');
+expect_handle->before() =~ /Ticket (\d+) created/;
+my $merge_ticket_B = $1;
+ok($merge_ticket_B, "Got second ticket to merge id=$merge_ticket_B");
+expect_send("merge $merge_ticket_B $merge_ticket_A", 'Merging the tickets...');
+expect_like(qr/Merge completed/, 'Merged the tickets');
+
+TODO: {
+ local $TODO = "we generate a spurious warning here";
+ $m->no_warnings_ok;
+}
+
+expect_send("show ticket/$merge_ticket_A/history", 'Checking merge on first ticket');
+expect_like(qr/Merged into ticket #$merge_ticket_A by root/, 'Merge recorded in first ticket');
+expect_send("show ticket/$merge_ticket_B/history", 'Checking merge on second ticket');
+expect_like(qr/Merged into ticket #$merge_ticket_A by root/, 'Merge recorded in second ticket');
+# }}}
+
+# {{{ test taking/stealing tickets
+{
+ # create a user; give them privileges to take and steal
+ ### TODO: implement 'grant' in the CLI tool; use that here instead.
+ ### this breaks the abstraction barrier, like, a lot.
+ my $steal_user = RT::User->new($RT::SystemUser);
+ my ($steal_user_id, $msg) = $steal_user->Create( Name => "fooser$$",
+ EmailAddress => "fooser$$\@localhost",
+ Privileged => 1,
+ Password => 'foobar',
+ );
+ ok($steal_user_id, "Created the user? $msg");
+ my $steal_queue = RT::Queue->new($RT::SystemUser);
+ my $steal_queue_id;
+ ($steal_queue_id, $msg) = $steal_queue->Create( Name => "Steal$$" );
+ ok($steal_queue_id, "Got the queue? $msg");
+ ok($steal_queue->id, "queue obj has id");
+ my $status;
+ ($status, $msg) = $steal_user->PrincipalObj->GrantRight( Right => 'ShowTicket', Object => $steal_queue );
+ ok($status, "Gave 'ShowTicket' to our user? $msg");
+ ($status, $msg) = $steal_user->PrincipalObj->GrantRight( Right => 'OwnTicket', Object => $steal_queue );
+ ok($status, "Gave 'OwnTicket' to our user? $msg");
+ ($status, $msg) = $steal_user->PrincipalObj->GrantRight( Right => 'StealTicket', Object => $steal_queue );
+ ok($status, "Gave 'StealTicket' to our user? $msg");
+ ($status, $msg) = $steal_user->PrincipalObj->GrantRight( Right => 'TakeTicket', Object => $steal_queue );
+ ok($status, "Gave 'TakeTicket' to our user? $msg");
+
+ # create a ticket to take/steal
+ expect_send("create -t ticket set queue=$steal_queue_id subject='CLIStealTest-$$'", 'Creating ticket to steal...');
+ expect_like(qr/Ticket \d+ created/, 'Created ticket');
+ expect_handle->before() =~ /Ticket (\d+) created/;
+ my $steal_ticket_id = $1;
+ ok($steal_ticket_id, "Got ticket to steal id=$steal_ticket_id");
+
+ # root takes the ticket
+ expect_send("take $steal_ticket_id", 'root takes the ticket...');
+ expect_like(qr/Owner changed from Nobody to root/, 'root took the ticket');
+ expect_quit();
+
+ # log in as the non-root user
+ $ENV{'RTUSER'} = "fooser$$";
+ $ENV{'RTPASSWD'} = 'foobar';
+ expect_run( command => "$rt_tool_path shell", prompt => 'rt> ', quit => 'quit',);
+
+ # user tries to take the ticket, fails
+ # shouldn't be able to 'take' a ticket which someone else has taken out from
+ # under you; that should produce an error. should have to explicitly
+ # 'steal' it back from them. 'steal' can automatically 'take' a ticket,
+ # though.
+ expect_send("take $steal_ticket_id", 'user tries to take the ticket...');
+ expect_like(qr/You can only take tickets that are unowned/, '...and fails.');
+ expect_send("show ticket/$steal_ticket_id -f owner", 'Double-checking...');
+ expect_like(qr/Owner: root/, '...no change.');
+
+ # user steals the ticket
+ expect_send("steal $steal_ticket_id", 'user tries to *steal* the ticket...');
+ expect_like(qr/Owner changed from root to fooser$$/, '...and succeeds!');
+ expect_send("show ticket/$steal_ticket_id -f owner", 'Double-checking...');
+ expect_like(qr/Owner: fooser$$/, '...yup, it worked.');
+ expect_quit();
+
+ # log back in as root
+ $ENV{'RTUSER'} = 'root';
+ $ENV{'RTPASSWD'} = 'password';
+ expect_run( command => "$rt_tool_path shell", prompt => 'rt> ', quit => 'quit',);
+
+ # root steals the ticket back
+ expect_send("steal $steal_ticket_id", 'root steals the ticket back...');
+ expect_like(qr/Owner changed from fooser$$ to root/, '...and succeeds.');
+}
+# }}}
+
+# {{{ test ticket linking
+ my @link_relns = ( 'DependsOn', 'DependedOnBy', 'RefersTo', 'ReferredToBy',
+ 'MemberOf', 'HasMember', );
+ my %display_relns = map { $_ => $_ } @link_relns;
+ $display_relns{HasMember} = 'Members';
+
+ my $link1_id = ok_create_ticket( "LinkTicket1-$$" );
+ my $link2_id = ok_create_ticket( "LinkTicket2-$$" );
+
+ foreach my $reln (@link_relns) {
+ # create link
+ expect_send("link $link1_id $reln $link2_id", "Link by $reln...");
+ expect_like(qr/Created link $link1_id $reln $link2_id/, 'Linked');
+ expect_send("show -s ticket/$link1_id/links", "Checking creation of $reln...");
+ expect_like(qr/$display_relns{$reln}: [\w\d\.\-]+:\/\/[\w\d\.]+\/ticket\/$link2_id/, "Created link $reln");
+
+ # delete link
+ expect_send("link -d $link1_id $reln $link2_id", "Delete $reln...");
+ expect_like(qr/Deleted link $link1_id $reln $link2_id/, 'Deleted');
+ expect_send("show ticket/$link1_id/links", "Checking removal of $reln...");
+ ok( expect_handle->before() !~ /\Q$display_relns{$reln}: \E[\w\d\.\-]+:\/\/[w\d\.]+\/ticket\/$link2_id/, "Removed link $reln" );
+ #expect_unlike(qr/\Q$reln: \E[\w\d\.]+\Q://\E[w\d\.]+\/ticket\/$link2_id/, "Removed link $reln");
+
+ }
+# }}}
+
+expect_quit(); # We need to do this ourselves, so that we quit
+ # *before* we tear down the webserver.
+
+# helper function
+sub ok_create_ticket {
+ my $subject = shift;
+
+ expect_send("create -t ticket set subject='$subject'", 'Creating ticket...');
+ expect_like(qr/Ticket \d+ created/, "Created ticket '$subject'");
+ expect_handle->before() =~ /Ticket (\d+) created/;
+ my $id = $1;
+ ok($id, "Got ticket id=$id");
+
+ return $id;
+}
+
+# wrap up all the file handling stuff for attachment testing
+sub check_attachment {
+ my $attachment_path = shift;
+ (my $filename = $attachment_path) =~ s/.*\/(.*)$/$1/;
+ expect_send("comment -m 'attach file' -a $attachment_path $ticket_id", "Adding an attachment ($filename)");
+ expect_like(qr/Message recorded/, "Added the attachment");
+ expect_send("show ticket/$ticket_id/attachments","Finding Attachment");
+ my $attachment_regex = qr/(\d+):\s+$filename/;
+ expect_like($attachment_regex,"Attachment Uploaded");
+ expect_handle->before() =~ $attachment_regex;
+ my $attachment_id = $1;
+ expect_send("show ticket/$ticket_id/attachments/$attachment_id/content","Fetching Attachment");
+ open (my $fh, $attachment_path) or die "Can't open $attachment_path: $!";
+ my $attachment_content = do { local($/); <$fh> };
+ close $fh;
+ chomp $attachment_content;
+ expect_is($attachment_content,"Attachment contains original text");
+}
+
+
+
+1;
diff --git a/rt/t/web/command_line_with_unknown_field.t b/rt/t/web/command_line_with_unknown_field.t
new file mode 100644
index 000000000..9a7ec7acd
--- /dev/null
+++ b/rt/t/web/command_line_with_unknown_field.t
@@ -0,0 +1,34 @@
+#!/usr/bin/perl -w
+
+use strict;
+use File::Spec ();
+use Test::Expect;
+use RT::Test tests => 10;
+my ($baseurl, $m) = RT::Test->started_ok;
+my $rt_tool_path = "$RT::BinPath/rt";
+
+$ENV{'RTUSER'} = 'root';
+$ENV{'RTPASSWD'} = 'password';
+$RT::Logger->debug("Connecting to server at ".RT->Config->Get('WebBaseURL'));
+$ENV{'RTSERVER'} =RT->Config->Get('WebBaseURL') ;
+$ENV{'RTDEBUG'} = '1';
+
+expect_run(
+ command => "$rt_tool_path shell",
+ prompt => 'rt> ',
+ quit => 'quit',
+);
+expect_send(q{create -t ticket set subject='new ticket' add cc=foo@example.com}, "Creating a ticket...");
+expect_like(qr/Ticket \d+ created/, "Created the ticket");
+expect_handle->before() =~ /Ticket (\d+) created/;
+my $ticket_id = $1;
+
+expect_send("edit ticket/$ticket_id set marge=simpson", 'set unknown field');
+expect_like(qr/marge: Unknown field/, 'marge is unknown field');
+expect_like(qr/marge: simpson/, 'the value we set for marge is shown too');
+
+expect_send("edit ticket/$ticket_id set homer=simpson", 'set unknown field');
+expect_like(qr/homer: Unknown field/, 'homer is unknown field');
+expect_like(qr/homer: simpson/, 'the value we set for homer is shown too');
+
+expect_quit();
diff --git a/rt/t/web/compilation_errors.t b/rt/t/web/compilation_errors.t
new file mode 100644
index 000000000..46a862868
--- /dev/null
+++ b/rt/t/web/compilation_errors.t
@@ -0,0 +1,68 @@
+#!/usr/bin/perl
+
+use strict;
+use Test::More;
+use File::Find;
+BEGIN {
+ sub wanted {
+ -f && /\.html$/ && $_ !~ /Logout.html$/;
+ }
+ my $tests = 7;
+ find( sub { wanted() and $tests += 4 }, 'share/html/' );
+ plan tests => $tests;
+}
+
+
+use HTTP::Request::Common;
+use HTTP::Cookies;
+use LWP;
+use Encode;
+
+my $cookie_jar = HTTP::Cookies->new;
+
+use RT::Test;
+my ($baseurl, $agent) = RT::Test->started_ok;
+
+# give the agent a place to stash the cookies
+$agent->cookie_jar($cookie_jar);
+
+# get the top page
+my $url = $agent->rt_base_url;
+diag "Base URL is '$url'" if $ENV{TEST_VERBOSE};
+$agent->get($url);
+
+is ($agent->{'status'}, 200, "Loaded a page");
+
+# {{{ test a login
+
+# follow the link marked "Login"
+
+ok($agent->{form}->find_input('user'));
+
+ok($agent->{form}->find_input('pass'));
+like ($agent->{'content'} , qr/username:/i);
+$agent->field( 'user' => 'root' );
+$agent->field( 'pass' => 'password' );
+# the field isn't named, so we have to click link 0
+$agent->click(0);
+is($agent->{'status'}, 200, "Fetched the page ok");
+like( $agent->{'content'} , qr/Logout/i, "Found a logout link");
+
+
+find ( sub { wanted() and test_get($File::Find::name) } , 'share/html/');
+
+sub test_get {
+ my $file = shift;
+
+ $file =~ s#^share/html/##;
+ diag( "testing $url/$file" ) if $ENV{TEST_VERBOSE};
+ ok ($agent->get("$url/$file", "GET $url/$file"), "Can Get $url/$file");
+ is ($agent->{'status'}, 200, "Loaded $file");
+# ok( $agent->{'content'} =~ /Logout/i, "Found a logout link on $file ");
+ ok( $agent->{'content'} !~ /Not logged in/i, "Still logged in for $file");
+ ok( $agent->{'content'} !~ /raw error/i, "Didn't get a Mason compilation error on $file");
+}
+
+# }}}
+
+1;
diff --git a/rt/t/web/config_tab_right.t b/rt/t/web/config_tab_right.t
new file mode 100644
index 000000000..4dc9ec082
--- /dev/null
+++ b/rt/t/web/config_tab_right.t
@@ -0,0 +1,41 @@
+#!/usr/bin/perl -w
+use strict;
+use warnings;
+
+use RT::Test tests => 8;
+
+my ($uname, $upass, $user) = ('tester', 'tester');
+{
+ $user = RT::User->new($RT::SystemUser);
+ my ($status, $msg) = $user->Create(
+ Name => $uname,
+ Password => $upass,
+ Disabled => 0,
+ Privileged => 1,
+ );
+ ok($status, 'created a user');
+}
+
+my ($baseurl, $m) = RT::Test->started_ok;
+ok $m->login($uname, $upass), "logged in";
+
+{
+ $m->content_unlike(qr/Configuration/, 'no configuration');
+ $m->get('/Admin/');
+ is $m->status, 403, 'no access to /Admin/';
+}
+
+RT::Test->set_rights(
+ { Principal => $user->PrincipalObj,
+ Right => [qw(ShowConfigTab)],
+ },
+);
+
+{
+ $m->get('/');
+ $m->content_like(qr/Configuration/, 'configuration is there');
+
+ $m->follow_link_ok({text => 'Configuration'});
+ is $m->status, 200, 'user has access to /Admin/';
+}
+
diff --git a/rt/t/web/crypt-gnupg.t b/rt/t/web/crypt-gnupg.t
new file mode 100644
index 000000000..fb28c887c
--- /dev/null
+++ b/rt/t/web/crypt-gnupg.t
@@ -0,0 +1,446 @@
+#!/usr/bin/perl -w
+use strict;
+
+use RT::Test tests => 94;
+
+plan skip_all => 'GnuPG required.'
+ unless eval 'use GnuPG::Interface; 1';
+plan skip_all => 'gpg executable is required.'
+ unless RT::Test->find_executable('gpg');
+
+
+use RT::Action::SendEmail;
+
+eval 'use GnuPG::Interface; 1' or plan skip_all => 'GnuPG required.';
+
+RT::Test->set_mail_catcher;
+
+RT->Config->Set( CommentAddress => 'general@example.com');
+RT->Config->Set( CorrespondAddress => 'general@example.com');
+RT->Config->Set( DefaultSearchResultFormat => qq{
+ '<B><A HREF="__WebPath__/Ticket/Display.html?id=__id__">__id__</a></B>/TITLE:#',
+ '<B><A HREF="__WebPath__/Ticket/Display.html?id=__id__">__Subject__</a></B>/TITLE:Subject',
+ 'OO-__OwnerName__-O',
+ 'OR-__Requestors__-O',
+ 'KO-__KeyOwnerName__-K',
+ 'KR-__KeyRequestors__-K',
+ Status});
+
+use File::Spec ();
+use Cwd;
+use File::Temp qw(tempdir);
+my $homedir = tempdir( CLEANUP => 1 );
+
+use_ok('RT::Crypt::GnuPG');
+
+RT->Config->Set( 'GnuPG',
+ Enable => 1,
+ OutgoingMessagesFormat => 'RFC' );
+
+RT->Config->Set( 'GnuPGOptions',
+ homedir => $homedir,
+ passphrase => 'recipient',
+ 'no-permission-warning' => undef,
+ 'trust-model' => 'always');
+RT->Config->Set( 'MailPlugins' => 'Auth::MailFrom', 'Auth::GnuPG' );
+
+RT::Test->import_gnupg_key('recipient@example.com', 'public');
+RT::Test->import_gnupg_key('recipient@example.com', 'secret');
+RT::Test->import_gnupg_key('general@example.com', 'public');
+RT::Test->import_gnupg_key('general@example.com', 'secret');
+RT::Test->import_gnupg_key('general@example.com.2', 'public');
+RT::Test->import_gnupg_key('general@example.com.2', 'secret');
+
+ok(my $user = RT::User->new($RT::SystemUser));
+ok($user->Load('root'), "Loaded user 'root'");
+$user->SetEmailAddress('recipient@example.com');
+
+my $queue = RT::Test->load_or_create_queue(
+ Name => 'General',
+ CorrespondAddress => 'general@example.com',
+);
+ok $queue && $queue->id, 'loaded or created queue';
+my $qid = $queue->id;
+
+RT::Test->set_rights(
+ Principal => 'Everyone',
+ Right => ['CreateTicket', 'ShowTicket', 'SeeQueue', 'ModifyTicket'],
+);
+
+my ($baseurl, $m) = RT::Test->started_ok;
+ok $m->login, 'logged in';
+
+$m->get_ok("/Admin/Queues/Modify.html?id=$qid");
+$m->form_with_fields('Sign', 'Encrypt');
+$m->field(Encrypt => 1);
+$m->submit;
+
+RT::Test->clean_caught_mails;
+
+$m->goto_create_ticket( $queue );
+$m->form_name('TicketCreate');
+$m->field('Subject', 'Encryption test');
+$m->field('Content', 'Some content');
+ok($m->value('Encrypt', 2), "encrypt tick box is checked");
+ok(!$m->value('Sign', 2), "sign tick box is unchecked");
+$m->submit;
+is($m->status, 200, "request successful");
+
+$m->get($baseurl); # ensure that the mail has been processed
+
+my @mail = RT::Test->fetch_caught_mails;
+ok(@mail, "got some mail");
+
+$user->SetEmailAddress('general@example.com');
+for my $mail (@mail) {
+ unlike $mail, qr/Some content/, "outgoing mail was encrypted";
+
+ my ($content_type) = $mail =~ /^(Content-Type: .*)/m;
+ my ($mime_version) = $mail =~ /^(MIME-Version: .*)/m;
+ my $body = strip_headers($mail);
+
+ $mail = << "MAIL";
+Subject: RT mail sent back into RT
+From: general\@example.com
+To: recipient\@example.com
+$mime_version
+$content_type
+
+$body
+MAIL
+
+ my ($status, $id) = RT::Test->send_via_mailgate($mail);
+ is ($status >> 8, 0, "The mail gateway exited normally");
+ ok ($id, "got id of a newly created ticket - $id");
+
+ my $tick = RT::Ticket->new( $RT::SystemUser );
+ $tick->Load( $id );
+ ok ($tick->id, "loaded ticket #$id");
+
+ is ($tick->Subject,
+ "RT mail sent back into RT",
+ "Correct subject"
+ );
+
+ my $txn = $tick->Transactions->First;
+ my ($msg, @attachments) = @{$txn->Attachments->ItemsArrayRef};
+
+ is( $msg->GetHeader('X-RT-Privacy'),
+ 'PGP',
+ "RT's outgoing mail has crypto"
+ );
+ is( $msg->GetHeader('X-RT-Incoming-Encryption'),
+ 'Success',
+ "RT's outgoing mail looks encrypted"
+ );
+
+ like($attachments[0]->Content, qr/Some content/, "RT's mail includes copy of ticket text");
+ like($attachments[0]->Content, qr/$RT::rtname/, "RT's mail includes this instance's name");
+}
+
+$m->get("$baseurl/Admin/Queues/Modify.html?id=$qid");
+$m->form_with_fields('Sign', 'Encrypt');
+$m->field(Encrypt => undef);
+$m->field(Sign => 1);
+$m->submit;
+
+RT::Test->clean_caught_mails;
+
+$m->goto_create_ticket( $queue );
+$m->form_name('TicketCreate');
+$m->field('Subject', 'Signing test');
+$m->field('Content', 'Some other content');
+ok(!$m->value('Encrypt', 2), "encrypt tick box is unchecked");
+ok($m->value('Sign', 2), "sign tick box is checked");
+$m->submit;
+is($m->status, 200, "request successful");
+
+$m->get($baseurl); # ensure that the mail has been processed
+
+@mail = RT::Test->fetch_caught_mails;
+ok(@mail, "got some mail");
+for my $mail (@mail) {
+ like $mail, qr/Some other content/, "outgoing mail was not encrypted";
+ like $mail, qr/-----BEGIN PGP SIGNATURE-----[\s\S]+-----END PGP SIGNATURE-----/, "data has some kind of signature";
+
+ my ($content_type) = $mail =~ /^(Content-Type: .*)/m;
+ my ($mime_version) = $mail =~ /^(MIME-Version: .*)/m;
+ my $body = strip_headers($mail);
+
+ $mail = << "MAIL";
+Subject: More RT mail sent back into RT
+From: general\@example.com
+To: recipient\@example.com
+$mime_version
+$content_type
+
+$body
+MAIL
+
+ my ($status, $id) = RT::Test->send_via_mailgate($mail);
+ is ($status >> 8, 0, "The mail gateway exited normally");
+ ok ($id, "got id of a newly created ticket - $id");
+
+ my $tick = RT::Ticket->new( $RT::SystemUser );
+ $tick->Load( $id );
+ ok ($tick->id, "loaded ticket #$id");
+
+ is ($tick->Subject,
+ "More RT mail sent back into RT",
+ "Correct subject"
+ );
+
+ my $txn = $tick->Transactions->First;
+ my ($msg, @attachments) = @{$txn->Attachments->ItemsArrayRef};
+
+ is( $msg->GetHeader('X-RT-Privacy'),
+ 'PGP',
+ "RT's outgoing mail has crypto"
+ );
+ is( $msg->GetHeader('X-RT-Incoming-Encryption'),
+ 'Not encrypted',
+ "RT's outgoing mail looks unencrypted"
+ );
+ is( $msg->GetHeader('X-RT-Incoming-Signature'),
+ 'general <general@example.com>',
+ "RT's outgoing mail looks signed"
+ );
+
+ like($attachments[0]->Content, qr/Some other content/, "RT's mail includes copy of ticket text");
+ like($attachments[0]->Content, qr/$RT::rtname/, "RT's mail includes this instance's name");
+}
+
+$m->get("$baseurl/Admin/Queues/Modify.html?id=$qid");
+$m->form_with_fields('Sign', 'Encrypt');
+$m->field(Encrypt => 1);
+$m->field(Sign => 1);
+$m->submit;
+
+RT::Test->clean_caught_mails;
+
+
+$m->goto_create_ticket( $queue );
+$m->form_name('TicketCreate');
+$m->field('Subject', 'Crypt+Sign test');
+$m->field('Content', 'Some final? content');
+ok($m->value('Encrypt', 2), "encrypt tick box is checked");
+ok($m->value('Sign', 2), "sign tick box is checked");
+$m->submit;
+is($m->status, 200, "request successful");
+
+$m->get($baseurl); # ensure that the mail has been processed
+
+@mail = RT::Test->fetch_caught_mails;
+ok(@mail, "got some mail");
+for my $mail (@mail) {
+ unlike $mail, qr/Some other content/, "outgoing mail was encrypted";
+
+ my ($content_type) = $mail =~ /^(Content-Type: .*)/m;
+ my ($mime_version) = $mail =~ /^(MIME-Version: .*)/m;
+ my $body = strip_headers($mail);
+
+ $mail = << "MAIL";
+Subject: Final RT mail sent back into RT
+From: general\@example.com
+To: recipient\@example.com
+$mime_version
+$content_type
+
+$body
+MAIL
+
+ my ($status, $id) = RT::Test->send_via_mailgate($mail);
+ is ($status >> 8, 0, "The mail gateway exited normally");
+ ok ($id, "got id of a newly created ticket - $id");
+
+ my $tick = RT::Ticket->new( $RT::SystemUser );
+ $tick->Load( $id );
+ ok ($tick->id, "loaded ticket #$id");
+
+ is ($tick->Subject,
+ "Final RT mail sent back into RT",
+ "Correct subject"
+ );
+
+ my $txn = $tick->Transactions->First;
+ my ($msg, @attachments) = @{$txn->Attachments->ItemsArrayRef};
+
+ is( $msg->GetHeader('X-RT-Privacy'),
+ 'PGP',
+ "RT's outgoing mail has crypto"
+ );
+ is( $msg->GetHeader('X-RT-Incoming-Encryption'),
+ 'Success',
+ "RT's outgoing mail looks encrypted"
+ );
+ is( $msg->GetHeader('X-RT-Incoming-Signature'),
+ 'general <general@example.com>',
+ "RT's outgoing mail looks signed"
+ );
+
+ like($attachments[0]->Content, qr/Some final\? content/, "RT's mail includes copy of ticket text");
+ like($attachments[0]->Content, qr/$RT::rtname/, "RT's mail includes this instance's name");
+}
+
+RT::Test->clean_caught_mails;
+
+$m->goto_create_ticket( $queue );
+$m->form_name('TicketCreate');
+$m->field('Subject', 'Test crypt-off on encrypted queue');
+$m->field('Content', 'Thought you had me figured out didya');
+$m->field(Encrypt => undef, 2); # turn off encryption
+ok(!$m->value('Encrypt', 2), "encrypt tick box is now unchecked");
+ok($m->value('Sign', 2), "sign tick box is still checked");
+$m->submit;
+is($m->status, 200, "request successful");
+
+$m->get($baseurl); # ensure that the mail has been processed
+
+@mail = RT::Test->fetch_caught_mails;
+ok(@mail, "got some mail");
+for my $mail (@mail) {
+ like $mail, qr/Thought you had me figured out didya/, "outgoing mail was unencrypted";
+
+ my ($content_type) = $mail =~ /^(Content-Type: .*)/m;
+ my ($mime_version) = $mail =~ /^(MIME-Version: .*)/m;
+ my $body = strip_headers($mail);
+
+ $mail = << "MAIL";
+Subject: Post-final! RT mail sent back into RT
+From: general\@example.com
+To: recipient\@example.com
+$mime_version
+$content_type
+
+$body
+MAIL
+
+ my ($status, $id) = RT::Test->send_via_mailgate($mail);
+ is ($status >> 8, 0, "The mail gateway exited normally");
+ ok ($id, "got id of a newly created ticket - $id");
+
+ my $tick = RT::Ticket->new( $RT::SystemUser );
+ $tick->Load( $id );
+ ok ($tick->id, "loaded ticket #$id");
+
+ is ($tick->Subject,
+ "Post-final! RT mail sent back into RT",
+ "Correct subject"
+ );
+
+ my $txn = $tick->Transactions->First;
+ my ($msg, @attachments) = @{$txn->Attachments->ItemsArrayRef};
+
+ is( $msg->GetHeader('X-RT-Privacy'),
+ 'PGP',
+ "RT's outgoing mail has crypto"
+ );
+ is( $msg->GetHeader('X-RT-Incoming-Encryption'),
+ 'Not encrypted',
+ "RT's outgoing mail looks unencrypted"
+ );
+ is( $msg->GetHeader('X-RT-Incoming-Signature'),
+ 'general <general@example.com>',
+ "RT's outgoing mail looks signed"
+ );
+
+ like($attachments[0]->Content, qr/Thought you had me figured out didya/, "RT's mail includes copy of ticket text");
+ like($attachments[0]->Content, qr/$RT::rtname/, "RT's mail includes this instance's name");
+}
+
+sub strip_headers
+{
+ my $mail = shift;
+ $mail =~ s/.*?\n\n//s;
+ return $mail;
+}
+
+# now test the OwnerNameKey and RequestorsKey fields
+
+my $nokey = RT::Test->load_or_create_user(Name => 'nokey', EmailAddress => 'nokey@example.com');
+$nokey->PrincipalObj->GrantRight(Right => 'CreateTicket');
+$nokey->PrincipalObj->GrantRight(Right => 'OwnTicket');
+
+my $tick = RT::Ticket->new( $RT::SystemUser );
+$tick->Create(Subject => 'owner lacks pubkey', Queue => 'general',
+ Owner => $nokey);
+ok(my $id = $tick->id, 'created ticket for owner-without-pubkey');
+
+$tick = RT::Ticket->new( $RT::SystemUser );
+$tick->Create(Subject => 'owner has pubkey', Queue => 'general',
+ Owner => 'root');
+ok($id = $tick->id, 'created ticket for owner-with-pubkey');
+
+my $mail = << "MAIL";
+Subject: Nokey requestor
+From: nokey\@example.com
+To: general\@example.com
+
+hello
+MAIL
+
+((my $status), $id) = RT::Test->send_via_mailgate($mail);
+is ($status >> 8, 0, "The mail gateway exited normally");
+ok ($id, "got id of a newly created ticket - $id");
+
+$tick = RT::Ticket->new( $RT::SystemUser );
+$tick->Load( $id );
+ok ($tick->id, "loaded ticket #$id");
+
+is ($tick->Subject,
+ "Nokey requestor",
+ "Correct subject"
+);
+
+# test key selection
+my $key1 = "EC1E81E7DC3DB42788FB0E4E9FA662C06DE22FC2";
+my $key2 = "75E156271DCCF02DDD4A7A8CDF651FA0632C4F50";
+
+ok($user = RT::User->new($RT::SystemUser));
+ok($user->Load('root'), "Loaded user 'root'");
+is($user->PreferredKey, $key1, "preferred key is set correctly");
+$m->get("$baseurl/Prefs/Other.html");
+like($m->content, qr/Preferred key/, "preferred key option shows up in preference");
+
+# XXX: mech doesn't let us see the current value of the select, apparently
+like($m->content, qr/$key1/, "first key shows up in preferences");
+like($m->content, qr/$key2/, "second key shows up in preferences");
+like($m->content, qr/$key1.*?$key2/s, "first key shows up before the second");
+
+$m->form_number(3);
+$m->select("PreferredKey" => $key2);
+$m->submit;
+
+ok($user = RT::User->new($RT::SystemUser));
+ok($user->Load('root'), "Loaded user 'root'");
+is($user->PreferredKey, $key2, "preferred key is set correctly to the new value");
+
+$m->get("$baseurl/Prefs/Other.html");
+like($m->content, qr/Preferred key/, "preferred key option shows up in preference");
+
+# XXX: mech doesn't let us see the current value of the select, apparently
+like($m->content, qr/$key2/, "second key shows up in preferences");
+like($m->content, qr/$key1/, "first key shows up in preferences");
+like($m->content, qr/$key2.*?$key1/s, "second key (now preferred) shows up before the first");
+
+# test that the new fields work
+$m->get("$baseurl/Search/Simple.html?q=General");
+my $content = $m->content;
+$content =~ s/&#40;/(/g;
+$content =~ s/&#41;/)/g;
+
+like($content, qr/OO-Nobody-O/, "original OwnerName untouched");
+like($content, qr/OO-nokey-O/, "original OwnerName untouched");
+like($content, qr/OO-root-O/, "original OwnerName untouched");
+
+like($content, qr/OR-recipient\@example.com-O/, "original Requestors untouched");
+like($content, qr/OR-nokey\@example.com-O/, "original Requestors untouched");
+
+like($content, qr/KO-root-K/, "KeyOwnerName does not issue no-pubkey warning for recipient");
+like($content, qr/KO-nokey \(no pubkey!\)-K/, "KeyOwnerName issues no-pubkey warning for root");
+like($content, qr/KO-Nobody \(no pubkey!\)-K/, "KeyOwnerName issues no-pubkey warning for nobody");
+
+like($content, qr/KR-recipient\@example.com-K/, "KeyRequestors does not issue no-pubkey warning for recipient\@example.com");
+like($content, qr/KR-general\@example.com-K/, "KeyRequestors does not issue no-pubkey warning for general\@example.com");
+like($content, qr/KR-nokey\@example.com \(no pubkey!\)-K/, "KeyRequestors DOES issue no-pubkey warning for nokey\@example.com");
+
diff --git a/rt/t/web/custom_frontpage.t b/rt/t/web/custom_frontpage.t
new file mode 100644
index 000000000..45a390ab0
--- /dev/null
+++ b/rt/t/web/custom_frontpage.t
@@ -0,0 +1,61 @@
+#!/usr/bin/perl -w
+use strict;
+
+use RT::Test tests => 7;
+my ($baseurl, $m) = RT::Test->started_ok;
+
+my $url = $m->rt_base_url;
+
+my $user_obj = RT::User->new($RT::SystemUser);
+my ($ret, $msg) = $user_obj->LoadOrCreateByEmail('customer@example.com');
+ok($ret, 'ACL test user creation');
+$user_obj->SetName('customer');
+$user_obj->SetPrivileged(1);
+($ret, $msg) = $user_obj->SetPassword('customer');
+$user_obj->PrincipalObj->GrantRight(Right => 'LoadSavedSearch');
+$user_obj->PrincipalObj->GrantRight(Right => 'EditSavedSearch');
+$user_obj->PrincipalObj->GrantRight(Right => 'CreateSavedSearch');
+$user_obj->PrincipalObj->GrantRight(Right => 'ModifySelf');
+
+ok $m->login( customer => 'customer' ), "logged in";
+
+$m->get ( $url."Search/Build.html");
+
+#create a saved search
+$m->form_name ('BuildQuery');
+
+$m->field ( "ValueOfAttachment" => 'stupid');
+$m->field ( "SavedSearchDescription" => 'stupid tickets');
+$m->click_button (name => 'SavedSearchSave');
+
+$m->get ( $url.'Prefs/MyRT.html' );
+$m->content_like (qr/stupid tickets/, 'saved search listed in rt at a glance items');
+
+ok $m->login, 'we did log in as root';
+
+$m->get ( $url.'Prefs/MyRT.html' );
+$m->form_name ('SelectionBox-body');
+# can't use submit form for mutli-valued select as it uses set_fields
+$m->field ('body-Selected' => ['component-QuickCreate', 'system-Unowned Tickets', 'system-My Tickets']);
+$m->click_button (name => 'remove');
+$m->form_name ('SelectionBox-body');
+#$m->click_button (name => 'body-Save');
+$m->get ( $url );
+$m->content_lacks ('highest priority tickets', 'remove everything from body pane');
+
+$m->get ( $url.'Prefs/MyRT.html' );
+$m->form_name ('SelectionBox-body');
+$m->field ('body-Available' => ['component-QuickCreate', 'system-Unowned Tickets', 'system-My Tickets']);
+$m->click_button (name => 'add');
+
+$m->form_name ('SelectionBox-body');
+$m->field ('body-Selected' => ['component-QuickCreate']);
+$m->click_button (name => 'movedown');
+
+$m->form_name ('SelectionBox-body');
+$m->click_button (name => 'movedown');
+
+$m->form_name ('SelectionBox-body');
+#$m->click_button (name => 'body-Save');
+$m->get ( $url );
+$m->content_like (qr'highest priority tickets', 'adds them back');
diff --git a/rt/t/web/custom_search.t b/rt/t/web/custom_search.t
new file mode 100644
index 000000000..05cfdab60
--- /dev/null
+++ b/rt/t/web/custom_search.t
@@ -0,0 +1,84 @@
+#!/usr/bin/perl -w
+use strict;
+
+use RT::Test tests => 11;
+my ($baseurl, $m) = RT::Test->started_ok;
+my $url = $m->rt_base_url;
+
+# reset preferences for easier test?
+
+
+
+my $t = RT::Ticket->new($RT::SystemUser);
+$t->Create(Subject => 'for custom search'.$$, Queue => 'general',
+ Owner => 'root', Requestor => 'customsearch@localhost');
+ok(my $id = $t->id, 'created ticket for custom search');
+
+ok $m->login, 'logged in';
+
+my $t_link = $m->find_link( text => "for custom search".$$ );
+like ($t_link->url, qr/$id/, 'link to the ticket we created');
+
+$m->content_lacks ('customsearch@localhost', 'requestor not displayed ');
+$m->get ( $url.'Prefs/MyRT.html' );
+my $cus_hp = $m->find_link( text => "My Tickets" );
+my $cus_qs = $m->find_link( text => "Quick search" );
+$m->get ($cus_hp);
+$m->content_like (qr'highest priority tickets');
+
+# add Requestor to the fields
+$m->form_name ('BuildQuery');
+# can't use submit form for mutli-valued select as it uses set_fields
+$m->field (SelectDisplayColumns => ['Requestors']);
+$m->click_button (name => 'AddCol') ;
+
+$m->form_name ('BuildQuery');
+$m->click_button (name => 'Save');
+
+$m->get( $url );
+$m->content_contains ('customsearch@localhost', 'requestor now displayed ');
+
+
+# now remove Requestor from the fields
+$m->get ($cus_hp);
+
+$m->form_name ('BuildQuery');
+
+my $cdc = $m->current_form->find_input('CurrentDisplayColumns');
+my ($requestor_value) = grep { /Requestor/ } $cdc->possible_values;
+ok($requestor_value, "got the requestor value");
+
+$m->field (CurrentDisplayColumns => $requestor_value);
+$m->click_button (name => 'RemoveCol') ;
+
+$m->form_name ('BuildQuery');
+$m->click_button (name => 'Save');
+
+$m->get( $url );
+$m->content_lacks ('customsearch@localhost', 'requestor not displayed ');
+
+
+# try to disable General from quick search
+
+# Note that there's a small problem in the current implementation,
+# since ticked quese are wanted, we do the invesrsion. So any
+# queue added during the quicksearch setting will be unticked.
+my $nlinks = $#{$m->find_all_links( text => "General" )};
+$m->get ($cus_qs);
+$m->form_name ('Preferences');
+$m->untick('Want-General', '1');
+$m->click_button (name => 'Save');
+
+$m->get( $url );
+is ($#{$m->find_all_links( text => "General" )}, $nlinks - 1,
+ 'General gone from quicksearch list');
+
+# get it back
+$m->get ($cus_qs);
+$m->form_name ('Preferences');
+$m->tick('Want-General', '1');
+$m->click_button (name => 'Save');
+
+$m->get( $url );
+is ($#{$m->find_all_links( text => "General" )}, $nlinks,
+ 'General back in quicksearch list');
diff --git a/rt/t/web/dashboard_with_deleted_saved_search.t b/rt/t/web/dashboard_with_deleted_saved_search.t
new file mode 100644
index 000000000..328095aaf
--- /dev/null
+++ b/rt/t/web/dashboard_with_deleted_saved_search.t
@@ -0,0 +1,89 @@
+#!/usr/bin/env perl
+use strict;
+use warnings;
+
+use RT::Test tests => 18;
+my ( $url, $m ) = RT::Test->started_ok;
+ok( $m->login, 'logged in' );
+
+# create a saved search
+$m->get_ok( $url . "/Search/Build.html?Query=" . 'id=1' );
+
+$m->submit_form(
+ form_name => 'BuildQuery',
+ fields => { SavedSearchDescription => 'foo', },
+ button => 'SavedSearchSave',
+);
+
+my ( $search_uri, $user_id, $search_id ) =
+ $m->content =~ /value="(RT::User-(\d+)-SavedSearch-(\d+))"/;
+$m->submit_form(
+ form_name => 'BuildQuery',
+ fields => { SavedSearchLoad => $search_uri },
+ button => 'SavedSearchSave',
+);
+
+$m->content_like( qr/name="SavedSearchDelete"\s+value="Delete"/,
+ 'found Delete button' );
+$m->content_like(
+ qr/name="SavedSearchDescription"\s+value="foo"/,
+ 'found Description input with the value filled'
+);
+
+# create a dashboard with the created search
+
+$m->get_ok( $url . "/Dashboards/Modify.html?Create=1" );
+$m->submit_form(
+ form_name => 'ModifyDashboard',
+ fields => { Name => 'bar' },
+);
+
+$m->content_like( qr/Saved dashboard bar/i, 'dashboard saved' );
+my $dashboard_queries_link = $m->find_link( text_regex => qr/Queries/ );
+my ( $dashboard_id ) = $dashboard_queries_link->url =~ /id=(\d+)/;
+
+$m->get_ok( $url . "/Dashboards/Queries.html?id=$dashboard_id" );
+
+$m->content_lacks( 'value="Update"', 'no update button' );
+
+$m->submit_form(
+ form_name => 'Dashboard-Searches-body',
+ fields =>
+ { 'Searches-body-Available' => "search-$search_id-RT::User-$user_id" },
+ button => 'add',
+);
+
+$m->content_like( qr/Dashboard updated/i, 'added search foo to dashboard bar' );
+
+# delete the created search
+
+$m->get_ok( $url . "/Search/Build.html?Query=" . 'id=1' );
+$m->submit_form(
+ form_name => 'BuildQuery',
+ fields => { SavedSearchLoad => $search_uri },
+);
+$m->submit_form(
+ form_name => 'BuildQuery',
+ button => 'SavedSearchDelete',
+);
+
+$m->content_lacks( $search_uri, 'deleted search foo' );
+
+# here is what we really want to test
+
+$m->get_ok( $url . "/Dashboards/Queries.html?id=$dashboard_id" );
+$m->content_like( qr/Deleted queries/i, 'found deleted message' );
+
+# Update button shows so we can update the deleted search easily
+$m->content_contains( 'value="Update"', 'found update button' );
+
+$m->submit_form(
+ form_name => 'Dashboard-Searches-body',
+ button => 'update',
+);
+
+$m->content_unlike( qr/Deleted queries/i, 'deleted message is gone' );
+$m->content_lacks( 'value="Update"', 'update button is gone too' );
+
+$m->get_warnings; # we'll get a lot of warnings because the deleted search
+
diff --git a/rt/t/web/dashboards-groups.t b/rt/t/web/dashboards-groups.t
new file mode 100644
index 000000000..cbf1d6a9f
--- /dev/null
+++ b/rt/t/web/dashboards-groups.t
@@ -0,0 +1,102 @@
+#!/usr/bin/perl -w
+use strict;
+
+use RT::Test tests => 40;
+my ($baseurl, $m) = RT::Test->started_ok;
+
+my $url = $m->rt_base_url;
+
+# create user and queue {{{
+my $user_obj = RT::User->new($RT::SystemUser);
+my ($ok, $msg) = $user_obj->LoadOrCreateByEmail('customer@example.com');
+ok($ok, 'ACL test user creation');
+$user_obj->SetName('customer');
+$user_obj->SetPrivileged(1);
+($ok, $msg) = $user_obj->SetPassword('customer');
+$user_obj->PrincipalObj->GrantRight(Right => 'ModifySelf');
+my $currentuser = RT::CurrentUser->new($user_obj);
+
+my $queue = RT::Queue->new($RT::SystemUser);
+$queue->Create(Name => 'SearchQueue'.$$);
+
+$user_obj->PrincipalObj->GrantRight(Right => $_, Object => $queue)
+ for qw/SeeQueue ShowTicket OwnTicket/;
+
+# grant the user all these rights so we can make sure that the group rights
+# are checked and not these as well
+$user_obj->PrincipalObj->GrantRight(Right => $_, Object => $RT::System)
+ for qw/SubscribeDashboard CreateOwnDashboard SeeOwnDashboard ModifyOwnDashboard DeleteOwnDashboard/;
+# }}}
+# create and test groups (outer < inner < user) {{{
+my $inner_group = RT::Group->new($RT::SystemUser);
+($ok, $msg) = $inner_group->CreateUserDefinedGroup(Name => "inner", Description => "inner group");
+ok($ok, "created inner group: $msg");
+
+my $outer_group = RT::Group->new($RT::SystemUser);
+($ok, $msg) = $outer_group->CreateUserDefinedGroup(Name => "outer", Description => "outer group");
+ok($ok, "created outer group: $msg");
+
+($ok, $msg) = $outer_group->AddMember($inner_group->PrincipalId);
+ok($ok, "added inner as a member of outer: $msg");
+
+($ok, $msg) = $inner_group->AddMember($user_obj->PrincipalId);
+ok($ok, "added user as a member of member: $msg");
+
+ok($outer_group->HasMember($inner_group->PrincipalId), "outer has inner");
+ok(!$outer_group->HasMember($user_obj->PrincipalId), "outer doesn't have user directly");
+ok($outer_group->HasMemberRecursively($inner_group->PrincipalId), "outer has inner recursively");
+ok($outer_group->HasMemberRecursively($user_obj->PrincipalId), "outer has user recursively");
+
+ok(!$inner_group->HasMember($outer_group->PrincipalId), "inner doesn't have outer");
+ok($inner_group->HasMember($user_obj->PrincipalId), "inner has user");
+ok(!$inner_group->HasMemberRecursively($outer_group->PrincipalId), "inner doesn't have outer, even recursively");
+ok($inner_group->HasMemberRecursively($user_obj->PrincipalId), "inner has user recursively");
+# }}}
+
+ok $m->login(customer => 'customer'), "logged in";
+
+$m->get_ok("$url/Dashboards");
+
+$m->follow_link_ok({text => "New"});
+$m->form_name('ModifyDashboard');
+is_deeply([$m->current_form->find_input('Privacy')->possible_values], ["RT::User-" . $user_obj->Id], "the only selectable privacy is user");
+$m->content_lacks('Delete', "Delete button hidden because we are creating");
+
+$user_obj->PrincipalObj->GrantRight(Right => 'CreateGroupDashboard', Object => $inner_group);
+
+$m->follow_link_ok({text => "New"});
+$m->form_name('ModifyDashboard');
+is_deeply([$m->current_form->find_input('Privacy')->possible_values], ["RT::User-" . $user_obj->Id, "RT::Group-" . $inner_group->Id], "the only selectable privacies are user and inner group (not outer group)");
+$m->field("Name" => 'inner dashboard');
+$m->field("Privacy" => "RT::Group-" . $inner_group->Id);
+$m->content_lacks('Delete', "Delete button hidden because we are creating");
+
+$m->click_button(value => 'Create');
+$m->content_lacks("No permission to create dashboards");
+$m->content_contains("Saved dashboard inner dashboard");
+$m->content_lacks('Delete', "Delete button hidden because we lack DeleteDashboard");
+
+my $dashboard = RT::Dashboard->new($currentuser);
+my ($id) = $m->content =~ /name="id" value="(\d+)"/;
+ok($id, "got an ID, $id");
+$dashboard->LoadById($id);
+is($dashboard->Name, "inner dashboard");
+
+is($dashboard->Privacy, 'RT::Group-' . $inner_group->Id, "correct privacy");
+is($dashboard->PossibleHiddenSearches, 0, "all searches are visible");
+
+$m->no_warnings_ok;
+
+$m->get_ok("/Dashboards/Modify.html?id=$id");
+$m->content_lacks("inner dashboard", "no SeeGroupDashboard right");
+$m->content_contains("Permission denied");
+
+$m->warning_like(qr/Permission denied/, "got a permission denied warning");
+
+$user_obj->PrincipalObj->GrantRight(Right => 'SeeGroupDashboard', Object => $inner_group);
+$m->get_ok("/Dashboards/Modify.html?id=$id");
+$m->content_contains("inner dashboard", "we now have SeeGroupDashboard right");
+$m->content_lacks("Permission denied");
+
+$m->content_contains('Subscription', "Subscription link not hidden because we have SubscribeDashboard");
+
diff --git a/rt/t/web/dashboards-permissions.t b/rt/t/web/dashboards-permissions.t
new file mode 100644
index 000000000..172404289
--- /dev/null
+++ b/rt/t/web/dashboards-permissions.t
@@ -0,0 +1,38 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+use RT::Test tests => 7;
+my ($baseurl, $m) = RT::Test->started_ok;
+
+my $url = $m->rt_base_url;
+
+# create user and queue {{{
+my $user_obj = RT::User->new($RT::SystemUser);
+my ($ok, $msg) = $user_obj->LoadOrCreateByEmail('customer@example.com');
+ok($ok, 'ACL test user creation');
+$user_obj->SetName('customer');
+$user_obj->SetPrivileged(1);
+($ok, $msg) = $user_obj->SetPassword('customer');
+$user_obj->PrincipalObj->GrantRight(Right => 'ModifySelf');
+my $currentuser = RT::CurrentUser->new($user_obj);
+
+my $queue = RT::Queue->new($RT::SystemUser);
+$queue->Create(Name => 'SearchQueue'.$$);
+
+$user_obj->PrincipalObj->GrantRight(Right => $_, Object => $queue)
+ for qw/SeeQueue ShowTicket OwnTicket/;
+
+$user_obj->PrincipalObj->GrantRight(Right => $_, Object => $RT::System)
+ for qw/SubscribeDashboard CreateOwnDashboard SeeOwnDashboard ModifyOwnDashboard DeleteOwnDashboard/;
+# }}}
+
+ok $m->login(customer => 'customer'), "logged in";
+
+$m->get_ok("$url/Dashboards");
+
+$m->follow_link_ok({text => "New"});
+$m->form_name('ModifyDashboard');
+is_deeply([$m->current_form->find_input('Privacy')->possible_values], ["RT::User-" . $user_obj->Id], "the only selectable privacy is user");
+$m->content_lacks('Delete', "Delete button hidden because we are creating");
+
diff --git a/rt/t/web/dashboards.t b/rt/t/web/dashboards.t
new file mode 100644
index 000000000..9d98ce6e4
--- /dev/null
+++ b/rt/t/web/dashboards.t
@@ -0,0 +1,250 @@
+#!/usr/bin/perl -w
+use strict;
+
+use RT::Test tests => 109;
+my ($baseurl, $m) = RT::Test->started_ok;
+
+my $url = $m->rt_base_url;
+
+my $user_obj = RT::User->new($RT::SystemUser);
+my ($ret, $msg) = $user_obj->LoadOrCreateByEmail('customer@example.com');
+ok($ret, 'ACL test user creation');
+$user_obj->SetName('customer');
+$user_obj->SetPrivileged(1);
+($ret, $msg) = $user_obj->SetPassword('customer');
+$user_obj->PrincipalObj->GrantRight(Right => 'ModifySelf');
+my $currentuser = RT::CurrentUser->new($user_obj);
+
+my $onlooker = RT::User->new($RT::SystemUser);
+($ret, $msg) = $onlooker->LoadOrCreateByEmail('onlooker@example.com');
+ok($ret, 'ACL test user creation');
+$onlooker->SetName('onlooker');
+$onlooker->SetPrivileged(1);
+($ret, $msg) = $onlooker->SetPassword('onlooker');
+
+my $queue = RT::Queue->new($RT::SystemUser);
+$queue->Create(Name => 'SearchQueue'.$$);
+
+for my $user ($user_obj, $onlooker) {
+ $user->PrincipalObj->GrantRight(Right => 'ModifySelf');
+ for my $right (qw/SeeQueue ShowTicket OwnTicket/) {
+ $user->PrincipalObj->GrantRight(Right => $right, Object => $queue);
+ }
+}
+
+ok $m->login(customer => 'customer'), "logged in";
+
+$m->get_ok($url."Dashboards/index.html");
+$m->content_lacks('<a href="/Dashboards/Modify.html?Create=1">New</a>',
+ "No 'new dashboard' link because we have no CreateOwnDashboard");
+
+$m->no_warnings_ok;
+
+$m->get_ok($url."Dashboards/Modify.html?Create=1");
+$m->content_contains("Permission denied");
+$m->content_lacks("Save Changes");
+
+$m->warning_like(qr/Permission denied/, "got a permission denied warning");
+
+$user_obj->PrincipalObj->GrantRight(Right => 'ModifyOwnDashboard', Object => $RT::System);
+
+# Modify itself is no longer good enough, you need Create
+$m->get_ok($url."Dashboards/Modify.html?Create=1");
+$m->content_contains("Permission denied");
+$m->content_lacks("Save Changes");
+
+$m->warning_like(qr/Permission denied/, "got a permission denied warning");
+
+$user_obj->PrincipalObj->GrantRight(Right => 'CreateOwnDashboard', Object => $RT::System);
+
+$m->get_ok($url."Dashboards/Modify.html?Create=1");
+$m->content_lacks("Permission denied");
+$m->content_contains("Create");
+
+$m->get_ok($url."Dashboards/index.html");
+$m->content_contains("New", "'New' link because we now have ModifyOwnDashboard");
+
+$m->follow_link_ok({text => "New"});
+$m->form_name('ModifyDashboard');
+$m->field("Name" => 'different dashboard');
+$m->content_lacks('Delete', "Delete button hidden because we are creating");
+$m->click_button(value => 'Create');
+$m->content_lacks("No permission to create dashboards");
+$m->content_contains("Saved dashboard different dashboard");
+$m->content_lacks('Delete', "Delete button hidden because we lack DeleteOwnDashboard");
+
+$m->get_ok($url."Dashboards/index.html");
+$m->content_lacks("different dashboard", "we lack SeeOwnDashboard");
+
+$user_obj->PrincipalObj->GrantRight(Right => 'SeeOwnDashboard', Object => $RT::System);
+
+$m->get_ok($url."Dashboards/index.html");
+$m->content_contains("different dashboard", "we now have SeeOwnDashboard");
+$m->content_lacks("Permission denied");
+
+$m->follow_link_ok({text => "different dashboard"});
+$m->content_contains("Basics");
+$m->content_contains("Queries");
+$m->content_lacks("Subscription", "we don't have the SubscribeDashboard right");
+
+$m->follow_link_ok({text => "Basics"});
+$m->content_contains("Modify the dashboard different dashboard");
+
+$m->follow_link_ok({text => "Queries"});
+$m->content_contains("Modify the queries of dashboard different dashboard");
+$m->form_name('Dashboard-Searches-body');
+$m->field('Searches-body-Available' => ["search-2-RT::System-1"]);
+$m->click_button(name => 'add');
+$m->content_contains("Dashboard updated");
+
+my $dashboard = RT::Dashboard->new($currentuser);
+my ($id) = $m->content =~ /name="id" value="(\d+)"/;
+ok($id, "got an ID, $id");
+$dashboard->LoadById($id);
+is($dashboard->Name, "different dashboard");
+
+is($dashboard->Privacy, 'RT::User-' . $user_obj->Id, "correct privacy");
+is($dashboard->PossibleHiddenSearches, 0, "all searches are visible");
+
+my @searches = $dashboard->Searches;
+is(@searches, 1, "one saved search in the dashboard");
+like($searches[0]->Name, qr/newest unowned tickets/, "correct search name");
+
+$m->form_name('Dashboard-Searches-body');
+$m->field('Searches-body-Available' => ["search-1-RT::System-1"]);
+$m->click_button(name => 'add');
+$m->content_contains("Dashboard updated");
+
+RT::Record->FlushCache if RT::Record->can('FlushCache');
+$dashboard = RT::Dashboard->new($currentuser);
+$dashboard->LoadById($id);
+
+@searches = $dashboard->Searches;
+is(@searches, 2, "two saved searches in the dashboard");
+like($searches[0]->Name, qr/newest unowned tickets/, "correct existing search name");
+like($searches[1]->Name, qr/highest priority tickets I own/, "correct new search name");
+
+my $ticket = RT::Ticket->new($RT::SystemUser);
+$ticket->Create(
+ Queue => $queue->Id,
+ Requestor => [ $user_obj->Name ],
+ Owner => $user_obj,
+ Subject => 'dashboard test',
+);
+
+$m->follow_link_ok({text => 'different dashboard'});
+$m->content_contains("20 highest priority tickets I own");
+$m->content_contains("20 newest unowned tickets");
+$m->content_lacks("Bookmarked Tickets");
+$m->content_contains("dashboard test", "ticket subject");
+
+$m->get_ok("/Dashboards/$id/This fragment left intentionally blank");
+$m->content_contains("20 highest priority tickets I own");
+$m->content_contains("20 newest unowned tickets");
+$m->content_lacks("Bookmarked Tickets");
+$m->content_contains("dashboard test", "ticket subject");
+
+$m->get_ok("/Dashboards/Subscription.html?DashboardId=$id");
+$m->form_name('SubscribeDashboard');
+$m->click_button(name => 'Save');
+$m->content_contains("Permission denied");
+$m->warning_like(qr/Unable to subscribe to dashboard.*Permission denied/, "got a permission denied warning when trying to subscribe to a dashboard");
+
+RT::Record->FlushCache if RT::Record->can('FlushCache');
+is($user_obj->Attributes->Named('Subscription'), 0, "no subscriptions");
+
+$user_obj->PrincipalObj->GrantRight(Right => 'SubscribeDashboard', Object => $RT::System);
+
+$m->get_ok("/Dashboards/Modify.html?id=$id");
+$m->follow_link_ok({text => "Subscription"});
+$m->content_contains("Subscribe to dashboard different dashboard");
+$m->content_contains("Unowned Tickets");
+$m->content_contains("My Tickets");
+$m->content_lacks("Bookmarked Tickets", "only dashboard queries show up");
+
+$m->form_name('SubscribeDashboard');
+$m->click_button(name => 'Save');
+$m->content_lacks("Permission denied");
+$m->content_contains("Subscribed to dashboard different dashboard");
+
+RT::Record->FlushCache if RT::Record->can('FlushCache');
+TODO: {
+ local $TODO = "some kind of caching is still happening (it works if I remove the check above)";
+ is($user_obj->Attributes->Named('Subscription'), 1, "we have a subscription");
+};
+
+$m->get_ok("/Dashboards/Modify.html?id=$id");
+$m->follow_link_ok({text => "Subscription"});
+$m->content_contains("Modify the subscription to dashboard different dashboard");
+
+$m->get_ok("/Dashboards/Modify.html?id=$id&Delete=1");
+$m->content_contains("Permission denied", "unable to delete dashboard because we lack DeleteOwnDashboard");
+
+$m->warning_like(qr/Couldn't delete dashboard.*Permission denied/, "got a permission denied warning when trying to delete the dashboard");
+
+$user_obj->PrincipalObj->GrantRight(Right => 'DeleteOwnDashboard', Object => $RT::System);
+
+$m->get_ok("/Dashboards/Modify.html?id=$id");
+$m->content_contains('Delete', "Delete button shows because we have DeleteOwnDashboard");
+
+$m->form_name('ModifyDashboard');
+$m->click_button(name => 'Delete');
+$m->content_contains("Deleted dashboard $id");
+
+$m->get("/Dashboards/Modify.html?id=$id");
+$m->content_lacks("different dashboard", "dashboard was deleted");
+$m->content_contains("Failed to load dashboard $id");
+
+$m->warning_like(qr/Failed to load dashboard.*Couldn't find row/, "the dashboard was deleted");
+
+$user_obj->PrincipalObj->GrantRight(Right => "SuperUser", Object => $RT::System);
+
+# now test that we warn about searches others can't see
+# first create a personal saved search...
+$m->get_ok($url."Search/Build.html");
+$m->follow_link_ok({text => 'Advanced'});
+$m->form_with_fields('Query');
+$m->field(Query => "id > 0");
+$m->submit;
+
+$m->form_with_fields('SavedSearchDescription');
+$m->field(SavedSearchDescription => "personal search");
+$m->click_button(name => "SavedSearchSave");
+
+# then the system-wide dashboard
+$m->get_ok($url."Dashboards/Modify.html?Create=1");
+
+$m->form_name('ModifyDashboard');
+$m->field("Name" => 'system dashboard');
+$m->field("Privacy" => 'RT::System-1');
+$m->content_lacks('Delete', "Delete button hidden because we are creating");
+$m->click_button(value => 'Create');
+$m->content_lacks("No permission to create dashboards");
+$m->content_contains("Saved dashboard system dashboard");
+
+$m->follow_link_ok({text => 'Queries'});
+
+$m->form_name('Dashboard-Searches-body');
+$m->field('Searches-body-Available' => ['search-7-RT::User-22']); # XXX: :( :(
+$m->click_button(name => 'add');
+$m->content_contains("Dashboard updated");
+
+$m->content_contains("The following queries may not be visible to all users who can see this dashboard.");
+
+$m->follow_link_ok({text => 'system dashboard'});
+$m->content_contains("personal search", "saved search shows up");
+$m->content_contains("dashboard test", "matched ticket shows up");
+
+# make sure the onlooker can't see the search...
+$onlooker->PrincipalObj->GrantRight(Right => 'SeeDashboard', Object => $RT::System);
+
+my $omech = RT::Test::Web->new;
+ok $omech->login(onlooker => 'onlooker'), "logged in";
+$omech->get_ok("/Dashboards");
+
+$omech->follow_link_ok({text => 'system dashboard'});
+$omech->content_lacks("personal search", "saved search doesn't show up");
+$omech->content_lacks("dashboard test", "matched ticket doesn't show up");
+
+$m->warning_like(qr/User .* tried to load container user /, "can't see other users' personal searches");
+
diff --git a/rt/t/web/gnupg-outgoing.t b/rt/t/web/gnupg-outgoing.t
new file mode 100644
index 000000000..a46833c6c
--- /dev/null
+++ b/rt/t/web/gnupg-outgoing.t
@@ -0,0 +1,363 @@
+#!/usr/bin/perl -w
+use strict;
+use warnings;
+
+use RT::Test tests => 492;
+
+plan skip_all => 'GnuPG required.'
+ unless eval 'use GnuPG::Interface; 1';
+plan skip_all => 'gpg executable is required.'
+ unless RT::Test->find_executable('gpg');
+
+
+use RT::Action::SendEmail;
+use File::Temp qw(tempdir);
+
+RT::Test->set_mail_catcher;
+
+use_ok('RT::Crypt::GnuPG');
+
+RT->Config->Set( GnuPG =>
+ Enable => 1,
+ OutgoingMessagesFormat => 'RFC',
+);
+
+RT->Config->Set( GnuPGOptions =>
+ homedir => scalar tempdir( CLEANUP => 1 ),
+ passphrase => 'rt-test',
+ 'no-permission-warning' => undef,
+ 'trust-model' => 'always',
+);
+RT->Config->Set( 'MailPlugins' => 'Auth::MailFrom', 'Auth::GnuPG' );
+
+RT::Test->import_gnupg_key('rt-recipient@example.com');
+RT::Test->import_gnupg_key('rt-test@example.com', 'public');
+
+my $queue = RT::Test->load_or_create_queue(
+ Name => 'Regression',
+ CorrespondAddress => 'rt-recipient@example.com',
+ CommentAddress => 'rt-recipient@example.com',
+);
+ok $queue && $queue->id, 'loaded or created queue';
+
+RT::Test->set_rights(
+ Principal => 'Everyone',
+ Right => ['CreateTicket', 'ShowTicket', 'SeeQueue', 'ReplyToTicket', 'ModifyTicket'],
+);
+
+my ($baseurl, $m) = RT::Test->started_ok;
+ok $m->login, 'logged in';
+
+my @variants = (
+ {},
+ { Sign => 1 },
+ { Encrypt => 1 },
+ { Sign => 1, Encrypt => 1 },
+);
+
+# collect emails
+my %mail = (
+ plain => [],
+ signed => [],
+ encrypted => [],
+ signed_encrypted => [],
+);
+
+diag "check in read-only mode that queue's props influence create/update ticket pages" if $ENV{TEST_VERBOSE};
+{
+ foreach my $variant ( @variants ) {
+ set_queue_crypt_options( %$variant );
+ $m->goto_create_ticket( $queue );
+ $m->form_name('TicketCreate');
+ if ( $variant->{'Encrypt'} ) {
+ ok $m->value('Encrypt', 2), "encrypt tick box is checked";
+ } else {
+ ok !$m->value('Encrypt', 2), "encrypt tick box is unchecked";
+ }
+ if ( $variant->{'Sign'} ) {
+ ok $m->value('Sign', 2), "sign tick box is checked";
+ } else {
+ ok !$m->value('Sign', 2), "sign tick box is unchecked";
+ }
+ }
+
+ # to avoid encryption/signing during create
+ set_queue_crypt_options();
+
+ my $ticket = RT::Ticket->new( $RT::SystemUser );
+ my ($id) = $ticket->Create(
+ Subject => 'test',
+ Queue => $queue->id,
+ Requestor => 'rt-test@example.com',
+ );
+ ok $id, 'ticket created';
+
+ foreach my $variant ( @variants ) {
+ set_queue_crypt_options( %$variant );
+ $m->goto_ticket( $id );
+ $m->follow_link_ok({text => 'Reply'}, '-> reply');
+ $m->form_number(3);
+ if ( $variant->{'Encrypt'} ) {
+ ok $m->value('Encrypt', 2), "encrypt tick box is checked";
+ } else {
+ ok !$m->value('Encrypt', 2), "encrypt tick box is unchecked";
+ }
+ if ( $variant->{'Sign'} ) {
+ ok $m->value('Sign', 2), "sign tick box is checked";
+ } else {
+ ok !$m->value('Sign', 2), "sign tick box is unchecked";
+ }
+ }
+}
+
+# create a ticket for each combination
+foreach my $queue_set ( @variants ) {
+ set_queue_crypt_options( %$queue_set );
+ foreach my $ticket_set ( @variants ) {
+ create_a_ticket( %$ticket_set );
+ }
+}
+
+my $tid;
+{
+ my $ticket = RT::Ticket->new( $RT::SystemUser );
+ ($tid) = $ticket->Create(
+ Subject => 'test',
+ Queue => $queue->id,
+ Requestor => 'rt-test@example.com',
+ );
+ ok $tid, 'ticket created';
+}
+
+# again for each combination add a reply message
+foreach my $queue_set ( @variants ) {
+ set_queue_crypt_options( %$queue_set );
+ foreach my $ticket_set ( @variants ) {
+ update_ticket( $tid, %$ticket_set );
+ }
+}
+
+
+# ------------------------------------------------------------------------------
+# now delete all keys from the keyring and put back secret/pub pair for rt-test@
+# and only public key for rt-recipient@ so we can verify signatures and decrypt
+# like we are on another side recieve emails
+# ------------------------------------------------------------------------------
+
+unlink $_ foreach glob( RT->Config->Get('GnuPGOptions')->{'homedir'} ."/*" );
+RT::Test->import_gnupg_key('rt-recipient@example.com', 'public');
+RT::Test->import_gnupg_key('rt-test@example.com');
+
+$queue = RT::Test->load_or_create_queue(
+ Name => 'Regression',
+ CorrespondAddress => 'rt-test@example.com',
+ CommentAddress => 'rt-test@example.com',
+);
+ok $queue && $queue->id, 'changed props of the queue';
+
+foreach my $mail ( map cleanup_headers($_), @{ $mail{'plain'} } ) {
+ my ($status, $id) = RT::Test->send_via_mailgate($mail);
+ is ($status >> 8, 0, "The mail gateway exited normally");
+ ok ($id, "got id of a newly created ticket - $id");
+
+ my $tick = RT::Ticket->new( $RT::SystemUser );
+ $tick->Load( $id );
+ ok ($tick->id, "loaded ticket #$id");
+
+ my $txn = $tick->Transactions->First;
+ my ($msg, @attachments) = @{$txn->Attachments->ItemsArrayRef};
+
+ ok !$msg->GetHeader('X-RT-Privacy'), "RT's outgoing mail has no crypto";
+ is $msg->GetHeader('X-RT-Incoming-Encryption'), 'Not encrypted',
+ "RT's outgoing mail looks not encrypted";
+ ok !$msg->GetHeader('X-RT-Incoming-Signature'),
+ "RT's outgoing mail looks not signed";
+
+ like $msg->Content, qr/Some content/, "RT's mail includes copy of ticket text";
+}
+
+foreach my $mail ( map cleanup_headers($_), @{ $mail{'signed'} } ) {
+ my ($status, $id) = RT::Test->send_via_mailgate($mail);
+ is ($status >> 8, 0, "The mail gateway exited normally");
+ ok ($id, "got id of a newly created ticket - $id");
+
+ my $tick = RT::Ticket->new( $RT::SystemUser );
+ $tick->Load( $id );
+ ok ($tick->id, "loaded ticket #$id");
+
+ my $txn = $tick->Transactions->First;
+ my ($msg, @attachments) = @{$txn->Attachments->ItemsArrayRef};
+
+ is $msg->GetHeader('X-RT-Privacy'), 'PGP',
+ "RT's outgoing mail has crypto";
+ is $msg->GetHeader('X-RT-Incoming-Encryption'), 'Not encrypted',
+ "RT's outgoing mail looks not encrypted";
+ like $msg->GetHeader('X-RT-Incoming-Signature'),
+ qr/<rt-recipient\@example.com>/,
+ "RT's outgoing mail looks signed";
+
+ like $attachments[0]->Content, qr/Some content/,
+ "RT's mail includes copy of ticket text";
+}
+
+foreach my $mail ( map cleanup_headers($_), @{ $mail{'encrypted'} } ) {
+ my ($status, $id) = RT::Test->send_via_mailgate($mail);
+ is ($status >> 8, 0, "The mail gateway exited normally");
+ ok ($id, "got id of a newly created ticket - $id");
+
+ my $tick = RT::Ticket->new( $RT::SystemUser );
+ $tick->Load( $id );
+ ok ($tick->id, "loaded ticket #$id");
+
+ my $txn = $tick->Transactions->First;
+ my ($msg, @attachments) = @{$txn->Attachments->ItemsArrayRef};
+
+ is $msg->GetHeader('X-RT-Privacy'), 'PGP',
+ "RT's outgoing mail has crypto";
+ is $msg->GetHeader('X-RT-Incoming-Encryption'), 'Success',
+ "RT's outgoing mail looks encrypted";
+ ok !$msg->GetHeader('X-RT-Incoming-Signature'),
+ "RT's outgoing mail looks not signed";
+
+ like $attachments[0]->Content, qr/Some content/,
+ "RT's mail includes copy of ticket text";
+}
+
+foreach my $mail ( map cleanup_headers($_), @{ $mail{'signed_encrypted'} } ) {
+ my ($status, $id) = RT::Test->send_via_mailgate($mail);
+ is ($status >> 8, 0, "The mail gateway exited normally");
+ ok ($id, "got id of a newly created ticket - $id");
+
+ my $tick = RT::Ticket->new( $RT::SystemUser );
+ $tick->Load( $id );
+ ok ($tick->id, "loaded ticket #$id");
+
+ my $txn = $tick->Transactions->First;
+ my ($msg, @attachments) = @{$txn->Attachments->ItemsArrayRef};
+
+ is $msg->GetHeader('X-RT-Privacy'), 'PGP',
+ "RT's outgoing mail has crypto";
+ is $msg->GetHeader('X-RT-Incoming-Encryption'), 'Success',
+ "RT's outgoing mail looks encrypted";
+ like $msg->GetHeader('X-RT-Incoming-Signature'),
+ qr/<rt-recipient\@example.com>/,
+ "RT's outgoing mail looks signed";
+
+ like $attachments[0]->Content, qr/Some content/,
+ "RT's mail includes copy of ticket text";
+}
+
+sub create_a_ticket {
+ my %args = (@_);
+
+ RT::Test->clean_caught_mails;
+
+ $m->goto_create_ticket( $queue );
+ $m->form_name('TicketCreate');
+ $m->field( Subject => 'test' );
+ $m->field( Requestors => 'rt-test@example.com' );
+ $m->field( Content => 'Some content' );
+
+ foreach ( qw(Sign Encrypt) ) {
+ if ( $args{ $_ } ) {
+ $m->tick( $_ => 1 );
+ } else {
+ $m->untick( $_ => 1 );
+ }
+ }
+
+ $m->submit;
+ is $m->status, 200, "request successful";
+
+ unlike($m->content, qr/unable to sign outgoing email messages/);
+
+ $m->get_ok('/'); # ensure that the mail has been processed
+
+ my @mail = RT::Test->fetch_caught_mails;
+ check_text_emails( \%args, @mail );
+}
+
+sub update_ticket {
+ my $tid = shift;
+ my %args = (@_);
+
+ RT::Test->clean_caught_mails;
+
+ ok $m->goto_ticket( $tid ), "UI -> ticket #$tid";
+ $m->follow_link_ok( { text => 'Reply' }, 'ticket -> reply' );
+ $m->form_number(3);
+ $m->field( UpdateContent => 'Some content' );
+
+ foreach ( qw(Sign Encrypt) ) {
+ if ( $args{ $_ } ) {
+ $m->tick( $_ => 1 );
+ } else {
+ $m->untick( $_ => 1 );
+ }
+ }
+
+ $m->click('SubmitTicket');
+ is $m->status, 200, "request successful";
+ $m->content_like(qr/Message recorded/, 'Message recorded') or diag $m->content;
+
+ $m->get_ok('/'); # ensure that the mail has been processed
+
+ my @mail = RT::Test->fetch_caught_mails;
+ check_text_emails( \%args, @mail );
+}
+
+sub check_text_emails {
+ my %args = %{ shift @_ };
+ my @mail = @_;
+
+ ok scalar @mail, "got some mail";
+ for my $mail (@mail) {
+ if ( $args{'Encrypt'} ) {
+ unlike $mail, qr/Some content/, "outgoing email was encrypted";
+ } else {
+ like $mail, qr/Some content/, "outgoing email was not encrypted";
+ }
+ if ( $args{'Sign'} && $args{'Encrypt'} ) {
+ like $mail, qr/BEGIN PGP MESSAGE/, 'outgoing email was signed';
+ } elsif ( $args{'Sign'} ) {
+ like $mail, qr/SIGNATURE/, 'outgoing email was signed';
+ } else {
+ unlike $mail, qr/SIGNATURE/, 'outgoing email was not signed';
+ }
+ }
+ if ( $args{'Sign'} && $args{'Encrypt'} ) {
+ push @{ $mail{'signed_encrypted'} }, @mail;
+ } elsif ( $args{'Sign'} ) {
+ push @{ $mail{'signed'} }, @mail;
+ } elsif ( $args{'Encrypt'} ) {
+ push @{ $mail{'encrypted'} }, @mail;
+ } else {
+ push @{ $mail{'plain'} }, @mail;
+ }
+}
+
+sub cleanup_headers {
+ my $mail = shift;
+ # strip id from subject to create new ticket
+ $mail =~ s/^(Subject:)\s*\[.*?\s+#\d+\]\s*/$1 /m;
+ # strip several headers
+ foreach my $field ( qw(Message-ID X-RT-Original-Encoding RT-Originator RT-Ticket X-RT-Loop-Prevention) ) {
+ $mail =~ s/^$field:.*?\n(?! |\t)//gmsi;
+ }
+ return $mail;
+}
+
+sub set_queue_crypt_options {
+ my %args = @_;
+ $m->get_ok("/Admin/Queues/Modify.html?id=". $queue->id);
+ $m->form_with_fields('Sign', 'Encrypt');
+ foreach my $opt ('Sign', 'Encrypt') {
+ if ( $args{$opt} ) {
+ $m->tick($opt => 1);
+ } else {
+ $m->untick($opt => 1);
+ }
+ }
+ $m->submit;
+}
+
diff --git a/rt/t/web/gnupg-select-keys-on-create.t b/rt/t/web/gnupg-select-keys-on-create.t
new file mode 100644
index 000000000..deee6b291
--- /dev/null
+++ b/rt/t/web/gnupg-select-keys-on-create.t
@@ -0,0 +1,325 @@
+#!/usr/bin/perl -w
+use strict;
+use warnings;
+
+use RT::Test tests => 60;
+
+plan skip_all => 'GnuPG required.'
+ unless eval 'use GnuPG::Interface; 1';
+plan skip_all => 'gpg executable is required.'
+ unless RT::Test->find_executable('gpg');
+
+
+use RT::Action::SendEmail;
+use File::Temp qw(tempdir);
+
+RT::Test->set_mail_catcher;
+
+use_ok('RT::Crypt::GnuPG');
+
+RT->Config->Set( GnuPG =>
+ Enable => 1,
+ OutgoingMessagesFormat => 'RFC',
+);
+
+RT->Config->Set( GnuPGOptions =>
+ homedir => scalar tempdir( CLEANUP => 0 ),
+ passphrase => 'rt-test',
+ 'no-permission-warning' => undef,
+);
+diag "GnuPG --homedir ". RT->Config->Get('GnuPGOptions')->{'homedir'} if $ENV{TEST_VERBOSE};
+
+RT->Config->Set( 'MailPlugins' => 'Auth::MailFrom', 'Auth::GnuPG' );
+
+my $queue = RT::Test->load_or_create_queue(
+ Name => 'Regression',
+ CorrespondAddress => 'rt-recipient@example.com',
+ CommentAddress => 'rt-recipient@example.com',
+);
+ok $queue && $queue->id, 'loaded or created queue';
+
+RT::Test->set_rights(
+ Principal => 'Everyone',
+ Right => ['CreateTicket', 'ShowTicket', 'SeeQueue', 'ReplyToTicket', 'ModifyTicket'],
+);
+
+my ($baseurl, $m) = RT::Test->started_ok;
+ok $m->login, 'logged in';
+
+diag "check that signing doesn't work if there is no key" if $ENV{TEST_VERBOSE};
+{
+ RT::Test->clean_caught_mails;
+
+ ok $m->goto_create_ticket( $queue ), "UI -> create ticket";
+ $m->form_number(3);
+ $m->tick( Sign => 1 );
+ $m->field( Requestors => 'rt-test@example.com' );
+ $m->field( Content => 'Some content' );
+ $m->submit;
+ $m->content_like(
+ qr/unable to sign outgoing email messages/i,
+ 'problems with passphrase'
+ );
+
+ my @mail = RT::Test->fetch_caught_mails;
+ ok !@mail, 'there are no outgoing emails';
+}
+
+{
+ RT::Test->import_gnupg_key('rt-recipient@example.com');
+ RT::Test->trust_gnupg_key('rt-recipient@example.com');
+ my %res = RT::Crypt::GnuPG::GetKeysInfo('rt-recipient@example.com');
+ is $res{'info'}[0]{'TrustTerse'}, 'ultimate', 'ultimately trusted key';
+}
+
+diag "check that things don't work if there is no key" if $ENV{TEST_VERBOSE};
+{
+ RT::Test->clean_caught_mails;
+
+ ok $m->goto_create_ticket( $queue ), "UI -> create ticket";
+ $m->form_number(3);
+ $m->tick( Encrypt => 1 );
+ $m->field( Requestors => 'rt-test@example.com' );
+ $m->field( Content => 'Some content' );
+ $m->submit;
+ $m->content_like(
+ qr/You are going to encrypt outgoing email messages/i,
+ 'problems with keys'
+ );
+ $m->content_like(
+ qr/There is no key suitable for encryption/i,
+ 'problems with keys'
+ );
+
+ my $form = $m->form_number(3);
+ ok !$form->find_input( 'UseKey-rt-test@example.com' ), 'no key selector';
+
+ my @mail = RT::Test->fetch_caught_mails;
+ ok !@mail, 'there are no outgoing emails';
+}
+
+diag "import first key of rt-test\@example.com" if $ENV{TEST_VERBOSE};
+my $fpr1 = '';
+{
+ RT::Test->import_gnupg_key('rt-test@example.com', 'public');
+ my %res = RT::Crypt::GnuPG::GetKeysInfo('rt-test@example.com');
+ is $res{'info'}[0]{'TrustLevel'}, 0, 'is not trusted key';
+ $fpr1 = $res{'info'}[0]{'Fingerprint'};
+}
+
+diag "check that things still doesn't work if key is not trusted" if $ENV{TEST_VERBOSE};
+{
+ RT::Test->clean_caught_mails;
+
+ ok $m->goto_create_ticket( $queue ), "UI -> create ticket";
+ $m->form_number(3);
+ $m->tick( Encrypt => 1 );
+ $m->field( Requestors => 'rt-test@example.com' );
+ $m->field( Content => 'Some content' );
+ $m->submit;
+ $m->content_like(
+ qr/You are going to encrypt outgoing email messages/i,
+ 'problems with keys'
+ );
+ $m->content_like(
+ qr/There is one suitable key, but trust level is not set/i,
+ 'problems with keys'
+ );
+
+ my $form = $m->form_number(3);
+ ok my $input = $form->find_input( 'UseKey-rt-test@example.com' ), 'found key selector';
+ is scalar $input->possible_values, 1, 'one option';
+
+ $m->select( 'UseKey-rt-test@example.com' => $fpr1 );
+ $m->submit;
+ $m->content_like(
+ qr/You are going to encrypt outgoing email messages/i,
+ 'problems with keys'
+ );
+ $m->content_like(
+ qr/Selected key either is not trusted/i,
+ 'problems with keys'
+ );
+
+ my @mail = RT::Test->fetch_caught_mails;
+ ok !@mail, 'there are no outgoing emails';
+}
+
+diag "import a second key of rt-test\@example.com" if $ENV{TEST_VERBOSE};
+my $fpr2 = '';
+{
+ RT::Test->import_gnupg_key('rt-test@example.com.2', 'public');
+ my %res = RT::Crypt::GnuPG::GetKeysInfo('rt-test@example.com');
+ is $res{'info'}[1]{'TrustLevel'}, 0, 'is not trusted key';
+ $fpr2 = $res{'info'}[2]{'Fingerprint'};
+}
+
+diag "check that things still doesn't work if two keys are not trusted" if $ENV{TEST_VERBOSE};
+{
+ RT::Test->clean_caught_mails;
+
+ ok $m->goto_create_ticket( $queue ), "UI -> create ticket";
+ $m->form_number(3);
+ $m->tick( Encrypt => 1 );
+ $m->field( Requestors => 'rt-test@example.com' );
+ $m->field( Content => 'Some content' );
+ $m->submit;
+ $m->content_like(
+ qr/You are going to encrypt outgoing email messages/i,
+ 'problems with keys'
+ );
+ $m->content_like(
+ qr/There are several keys suitable for encryption/i,
+ 'problems with keys'
+ );
+
+ my $form = $m->form_number(3);
+ ok my $input = $form->find_input( 'UseKey-rt-test@example.com' ), 'found key selector';
+ is scalar $input->possible_values, 2, 'two options';
+
+ $m->select( 'UseKey-rt-test@example.com' => $fpr1 );
+ $m->submit;
+ $m->content_like(
+ qr/You are going to encrypt outgoing email messages/i,
+ 'problems with keys'
+ );
+ $m->content_like(
+ qr/Selected key either is not trusted/i,
+ 'problems with keys'
+ );
+
+ my @mail = RT::Test->fetch_caught_mails;
+ ok !@mail, 'there are no outgoing emails';
+}
+
+{
+ RT::Test->lsign_gnupg_key( $fpr1 );
+ my %res = RT::Crypt::GnuPG::GetKeysInfo('rt-test@example.com');
+ ok $res{'info'}[0]{'TrustLevel'} > 0, 'trusted key';
+ is $res{'info'}[1]{'TrustLevel'}, 0, 'is not trusted key';
+}
+
+diag "check that we see key selector even if only one key is trusted but there are more keys";
+{
+ RT::Test->clean_caught_mails;
+
+ ok $m->goto_create_ticket( $queue ), "UI -> create ticket";
+ $m->form_number(3);
+ $m->tick( Encrypt => 1 );
+ $m->field( Requestors => 'rt-test@example.com' );
+ $m->field( Content => 'Some content' );
+ $m->submit;
+ $m->content_like(
+ qr/You are going to encrypt outgoing email messages/i,
+ 'problems with keys'
+ );
+ $m->content_like(
+ qr/There are several keys suitable for encryption/i,
+ 'problems with keys'
+ );
+
+ my $form = $m->form_number(3);
+ ok my $input = $form->find_input( 'UseKey-rt-test@example.com' ), 'found key selector';
+ is scalar $input->possible_values, 2, 'two options';
+
+ my @mail = RT::Test->fetch_caught_mails;
+ ok !@mail, 'there are no outgoing emails';
+}
+
+diag "check that key selector works and we can select trusted key";
+{
+ RT::Test->clean_caught_mails;
+
+ ok $m->goto_create_ticket( $queue ), "UI -> create ticket";
+ $m->form_number(3);
+ $m->tick( Encrypt => 1 );
+ $m->field( Requestors => 'rt-test@example.com' );
+ $m->field( Content => 'Some content' );
+ $m->submit;
+ $m->content_like(
+ qr/You are going to encrypt outgoing email messages/i,
+ 'problems with keys'
+ );
+ $m->content_like(
+ qr/There are several keys suitable for encryption/i,
+ 'problems with keys'
+ );
+
+ my $form = $m->form_number(3);
+ ok my $input = $form->find_input( 'UseKey-rt-test@example.com' ), 'found key selector';
+ is scalar $input->possible_values, 2, 'two options';
+
+ $m->select( 'UseKey-rt-test@example.com' => $fpr1 );
+ $m->submit;
+ $m->content_like( qr/Ticket \d+ created in queue/i, 'ticket created' );
+
+ my @mail = RT::Test->fetch_caught_mails;
+ ok @mail, 'there are some emails';
+ check_text_emails( { Encrypt => 1 }, @mail );
+}
+
+diag "check encrypting of attachments";
+{
+ RT::Test->clean_caught_mails;
+
+ ok $m->goto_create_ticket( $queue ), "UI -> create ticket";
+ $m->form_number(3);
+ $m->tick( Encrypt => 1 );
+ $m->field( Requestors => 'rt-test@example.com' );
+ $m->field( Content => 'Some content' );
+ $m->field( Attach => $0 );
+ $m->submit;
+ $m->content_like(
+ qr/You are going to encrypt outgoing email messages/i,
+ 'problems with keys'
+ );
+ $m->content_like(
+ qr/There are several keys suitable for encryption/i,
+ 'problems with keys'
+ );
+
+ my $form = $m->form_number(3);
+ ok my $input = $form->find_input( 'UseKey-rt-test@example.com' ), 'found key selector';
+ is scalar $input->possible_values, 2, 'two options';
+
+ $m->select( 'UseKey-rt-test@example.com' => $fpr1 );
+ $m->submit;
+ $m->content_like( qr/Ticket \d+ created in queue/i, 'ticket created' );
+
+ my @mail = RT::Test->fetch_caught_mails;
+ ok @mail, 'there are some emails';
+ check_text_emails( { Encrypt => 1, Attachment => 1 }, @mail );
+}
+
+sub check_text_emails {
+ my %args = %{ shift @_ };
+ my @mail = @_;
+
+ ok scalar @mail, "got some mail";
+ for my $mail (@mail) {
+ for my $type ('email', 'attachment') {
+ next if $type eq 'attachment' && !$args{'Attachment'};
+
+ my $content = $type eq 'email'
+ ? "Some content"
+ : "Attachment content";
+
+ if ( $args{'Encrypt'} ) {
+ unlike $mail, qr/$content/, "outgoing $type was encrypted";
+ } else {
+ like $mail, qr/$content/, "outgoing $type was not encrypted";
+ }
+
+ next unless $type eq 'email';
+
+ if ( $args{'Sign'} && $args{'Encrypt'} ) {
+ like $mail, qr/BEGIN PGP MESSAGE/, 'outgoing email was signed';
+ } elsif ( $args{'Sign'} ) {
+ like $mail, qr/SIGNATURE/, 'outgoing email was signed';
+ } else {
+ unlike $mail, qr/SIGNATURE/, 'outgoing email was not signed';
+ }
+ }
+ }
+}
+
diff --git a/rt/t/web/gnupg-select-keys-on-update.t b/rt/t/web/gnupg-select-keys-on-update.t
new file mode 100644
index 000000000..76817ddf2
--- /dev/null
+++ b/rt/t/web/gnupg-select-keys-on-update.t
@@ -0,0 +1,344 @@
+#!/usr/bin/perl -w
+use strict;
+use warnings;
+
+use RT::Test tests => 68;
+
+plan skip_all => 'GnuPG required.'
+ unless eval 'use GnuPG::Interface; 1';
+plan skip_all => 'gpg executable is required.'
+ unless RT::Test->find_executable('gpg');
+
+
+use RT::Action::SendEmail;
+use File::Temp qw(tempdir);
+
+RT::Test->set_mail_catcher;
+
+use_ok('RT::Crypt::GnuPG');
+
+RT->Config->Set( GnuPG =>
+ Enable => 1,
+ OutgoingMessagesFormat => 'RFC',
+);
+
+RT->Config->Set( GnuPGOptions =>
+ homedir => scalar tempdir( CLEANUP => 0 ),
+ passphrase => 'rt-test',
+ 'no-permission-warning' => undef,
+);
+diag "GnuPG --homedir ". RT->Config->Get('GnuPGOptions')->{'homedir'} if $ENV{TEST_VERBOSE};
+
+RT->Config->Set( 'MailPlugins' => 'Auth::MailFrom', 'Auth::GnuPG' );
+
+my $queue = RT::Test->load_or_create_queue(
+ Name => 'Regression',
+ CorrespondAddress => 'rt-recipient@example.com',
+ CommentAddress => 'rt-recipient@example.com',
+);
+ok $queue && $queue->id, 'loaded or created queue';
+
+RT::Test->set_rights(
+ Principal => 'Everyone',
+ Right => ['CreateTicket', 'ShowTicket', 'SeeQueue', 'ReplyToTicket', 'ModifyTicket'],
+);
+
+my ($baseurl, $m) = RT::Test->started_ok;
+ok $m->login, 'logged in';
+
+
+my $tid;
+{
+ my $ticket = RT::Ticket->new( $RT::SystemUser );
+ ($tid) = $ticket->Create(
+ Subject => 'test',
+ Queue => $queue->id,
+ );
+ ok $tid, 'ticket created';
+}
+
+diag "check that signing doesn't work if there is no key" if $ENV{TEST_VERBOSE};
+{
+ RT::Test->clean_caught_mails;
+
+ ok $m->goto_ticket( $tid ), "UI -> ticket #$tid";
+ $m->follow_link_ok( { text => 'Reply' }, 'ticket -> reply' );
+ $m->form_number(3);
+ $m->tick( Sign => 1 );
+ $m->field( UpdateCc => 'rt-test@example.com' );
+ $m->field( UpdateContent => 'Some content' );
+ $m->click('SubmitTicket');
+ $m->content_like(
+ qr/unable to sign outgoing email messages/i,
+ 'problems with passphrase'
+ );
+
+ my @mail = RT::Test->fetch_caught_mails;
+ ok !@mail, 'there are no outgoing emails';
+}
+
+{
+ RT::Test->import_gnupg_key('rt-recipient@example.com');
+ RT::Test->trust_gnupg_key('rt-recipient@example.com');
+ my %res = RT::Crypt::GnuPG::GetKeysInfo('rt-recipient@example.com');
+ is $res{'info'}[0]{'TrustTerse'}, 'ultimate', 'ultimately trusted key';
+}
+
+diag "check that things don't work if there is no key" if $ENV{TEST_VERBOSE};
+{
+ RT::Test->clean_caught_mails;
+
+ ok $m->goto_ticket( $tid ), "UI -> ticket #$tid";
+ $m->follow_link_ok( { text => 'Reply' }, 'ticket -> reply' );
+ $m->form_number(3);
+ $m->tick( Encrypt => 1 );
+ $m->field( UpdateCc => 'rt-test@example.com' );
+ $m->field( UpdateContent => 'Some content' );
+ $m->click('SubmitTicket');
+ $m->content_like(
+ qr/You are going to encrypt outgoing email messages/i,
+ 'problems with keys'
+ );
+ $m->content_like(
+ qr/There is no key suitable for encryption/i,
+ 'problems with keys'
+ );
+
+ my $form = $m->form_number(3);
+ ok !$form->find_input( 'UseKey-rt-test@example.com' ), 'no key selector';
+
+ my @mail = RT::Test->fetch_caught_mails;
+ ok !@mail, 'there are no outgoing emails';
+}
+
+
+diag "import first key of rt-test\@example.com" if $ENV{TEST_VERBOSE};
+my $fpr1 = '';
+{
+ RT::Test->import_gnupg_key('rt-test@example.com', 'public');
+ my %res = RT::Crypt::GnuPG::GetKeysInfo('rt-test@example.com');
+ is $res{'info'}[0]{'TrustLevel'}, 0, 'is not trusted key';
+ $fpr1 = $res{'info'}[0]{'Fingerprint'};
+}
+
+diag "check that things still doesn't work if key is not trusted" if $ENV{TEST_VERBOSE};
+{
+ RT::Test->clean_caught_mails;
+
+ ok $m->goto_ticket( $tid ), "UI -> ticket #$tid";
+ $m->follow_link_ok( { text => 'Reply' }, 'ticket -> reply' );
+ $m->form_number(3);
+ $m->tick( Encrypt => 1 );
+ $m->field( UpdateCc => 'rt-test@example.com' );
+ $m->field( UpdateContent => 'Some content' );
+ $m->click('SubmitTicket');
+ $m->content_like(
+ qr/You are going to encrypt outgoing email messages/i,
+ 'problems with keys'
+ );
+ $m->content_like(
+ qr/There is one suitable key, but trust level is not set/i,
+ 'problems with keys'
+ );
+
+ my $form = $m->form_number(3);
+ ok my $input = $form->find_input( 'UseKey-rt-test@example.com' ), 'found key selector';
+ is scalar $input->possible_values, 1, 'one option';
+
+ $m->select( 'UseKey-rt-test@example.com' => $fpr1 );
+ $m->click('SubmitTicket');
+ $m->content_like(
+ qr/You are going to encrypt outgoing email messages/i,
+ 'problems with keys'
+ );
+ $m->content_like(
+ qr/Selected key either is not trusted/i,
+ 'problems with keys'
+ );
+
+ my @mail = RT::Test->fetch_caught_mails;
+ ok !@mail, 'there are no outgoing emails';
+}
+
+diag "import a second key of rt-test\@example.com" if $ENV{TEST_VERBOSE};
+my $fpr2 = '';
+{
+ RT::Test->import_gnupg_key('rt-test@example.com.2', 'public');
+ my %res = RT::Crypt::GnuPG::GetKeysInfo('rt-test@example.com');
+ is $res{'info'}[1]{'TrustLevel'}, 0, 'is not trusted key';
+ $fpr2 = $res{'info'}[2]{'Fingerprint'};
+}
+
+diag "check that things still doesn't work if two keys are not trusted" if $ENV{TEST_VERBOSE};
+{
+ RT::Test->clean_caught_mails;
+
+ ok $m->goto_ticket( $tid ), "UI -> ticket #$tid";
+ $m->follow_link_ok( { text => 'Reply' }, 'ticket -> reply' );
+ $m->form_number(3);
+ $m->tick( Encrypt => 1 );
+ $m->field( UpdateCc => 'rt-test@example.com' );
+ $m->field( UpdateContent => 'Some content' );
+ $m->click('SubmitTicket');
+ $m->content_like(
+ qr/You are going to encrypt outgoing email messages/i,
+ 'problems with keys'
+ );
+ $m->content_like(
+ qr/There are several keys suitable for encryption/i,
+ 'problems with keys'
+ );
+
+ my $form = $m->form_number(3);
+ ok my $input = $form->find_input( 'UseKey-rt-test@example.com' ), 'found key selector';
+ is scalar $input->possible_values, 2, 'two options';
+
+ $m->select( 'UseKey-rt-test@example.com' => $fpr1 );
+ $m->click('SubmitTicket');
+ $m->content_like(
+ qr/You are going to encrypt outgoing email messages/i,
+ 'problems with keys'
+ );
+ $m->content_like(
+ qr/Selected key either is not trusted/i,
+ 'problems with keys'
+ );
+
+ my @mail = RT::Test->fetch_caught_mails;
+ ok !@mail, 'there are no outgoing emails';
+}
+
+{
+ RT::Test->lsign_gnupg_key( $fpr1 );
+ my %res = RT::Crypt::GnuPG::GetKeysInfo('rt-test@example.com');
+ ok $res{'info'}[0]{'TrustLevel'} > 0, 'trusted key';
+ is $res{'info'}[1]{'TrustLevel'}, 0, 'is not trusted key';
+}
+
+diag "check that we see key selector even if only one key is trusted but there are more keys" if $ENV{TEST_VERBOSE};
+{
+ RT::Test->clean_caught_mails;
+
+ ok $m->goto_ticket( $tid ), "UI -> ticket #$tid";
+ $m->follow_link_ok( { text => 'Reply' }, 'ticket -> reply' );
+ $m->form_number(3);
+ $m->tick( Encrypt => 1 );
+ $m->field( UpdateCc => 'rt-test@example.com' );
+ $m->field( UpdateContent => 'Some content' );
+ $m->click('SubmitTicket');
+ $m->content_like(
+ qr/You are going to encrypt outgoing email messages/i,
+ 'problems with keys'
+ );
+ $m->content_like(
+ qr/There are several keys suitable for encryption/i,
+ 'problems with keys'
+ );
+
+ my $form = $m->form_number(3);
+ ok my $input = $form->find_input( 'UseKey-rt-test@example.com' ), 'found key selector';
+ is scalar $input->possible_values, 2, 'two options';
+
+ my @mail = RT::Test->fetch_caught_mails;
+ ok !@mail, 'there are no outgoing emails';
+}
+
+diag "check that key selector works and we can select trusted key" if $ENV{TEST_VERBOSE};
+{
+ RT::Test->clean_caught_mails;
+
+ ok $m->goto_ticket( $tid ), "UI -> ticket #$tid";
+ $m->follow_link_ok( { text => 'Reply' }, 'ticket -> reply' );
+ $m->form_number(3);
+ $m->tick( Encrypt => 1 );
+ $m->field( UpdateCc => 'rt-test@example.com' );
+ $m->field( UpdateContent => 'Some content' );
+ $m->click('SubmitTicket');
+ $m->content_like(
+ qr/You are going to encrypt outgoing email messages/i,
+ 'problems with keys'
+ );
+ $m->content_like(
+ qr/There are several keys suitable for encryption/i,
+ 'problems with keys'
+ );
+
+ my $form = $m->form_number(3);
+ ok my $input = $form->find_input( 'UseKey-rt-test@example.com' ), 'found key selector';
+ is scalar $input->possible_values, 2, 'two options';
+
+ $m->select( 'UseKey-rt-test@example.com' => $fpr1 );
+ $m->click('SubmitTicket');
+ $m->content_like( qr/Message recorded/i, 'Message recorded' );
+
+ my @mail = RT::Test->fetch_caught_mails;
+ ok @mail, 'there are some emails';
+ check_text_emails( { Encrypt => 1 }, @mail );
+}
+
+diag "check encrypting of attachments" if $ENV{TEST_VERBOSE};
+{
+ RT::Test->clean_caught_mails;
+
+ ok $m->goto_ticket( $tid ), "UI -> ticket #$tid";
+ $m->follow_link_ok( { text => 'Reply' }, 'ticket -> reply' );
+ $m->form_number(3);
+ $m->tick( Encrypt => 1 );
+ $m->field( UpdateCc => 'rt-test@example.com' );
+ $m->field( UpdateContent => 'Some content' );
+ $m->field( Attach => $0 );
+ $m->click('SubmitTicket');
+ $m->content_like(
+ qr/You are going to encrypt outgoing email messages/i,
+ 'problems with keys'
+ );
+ $m->content_like(
+ qr/There are several keys suitable for encryption/i,
+ 'problems with keys'
+ );
+
+ my $form = $m->form_number(3);
+ ok my $input = $form->find_input( 'UseKey-rt-test@example.com' ), 'found key selector';
+ is scalar $input->possible_values, 2, 'two options';
+
+ $m->select( 'UseKey-rt-test@example.com' => $fpr1 );
+ $m->click('SubmitTicket');
+ $m->content_like( qr/Message recorded/i, 'Message recorded' );
+
+ my @mail = RT::Test->fetch_caught_mails;
+ ok @mail, 'there are some emails';
+ check_text_emails( { Encrypt => 1, Attachment => 1 }, @mail );
+}
+
+sub check_text_emails {
+ my %args = %{ shift @_ };
+ my @mail = @_;
+
+ ok scalar @mail, "got some mail";
+ for my $mail (@mail) {
+ for my $type ('email', 'attachment') {
+ next if $type eq 'attachment' && !$args{'Attachment'};
+
+ my $content = $type eq 'email'
+ ? "Some content"
+ : "Attachment content";
+
+ if ( $args{'Encrypt'} ) {
+ unlike $mail, qr/$content/, "outgoing $type was encrypted";
+ } else {
+ like $mail, qr/$content/, "outgoing $type was not encrypted";
+ }
+
+ next unless $type eq 'email';
+
+ if ( $args{'Sign'} && $args{'Encrypt'} ) {
+ like $mail, qr/BEGIN PGP MESSAGE/, 'outgoing email was signed';
+ } elsif ( $args{'Sign'} ) {
+ like $mail, qr/SIGNATURE/, 'outgoing email was signed';
+ } else {
+ unlike $mail, qr/SIGNATURE/, 'outgoing email was not signed';
+ }
+ }
+ }
+}
+
diff --git a/rt/t/web/offline_messages_utf8.t b/rt/t/web/offline_messages_utf8.t
new file mode 100644
index 000000000..c32e0bc27
--- /dev/null
+++ b/rt/t/web/offline_messages_utf8.t
@@ -0,0 +1,67 @@
+#!/usr/bin/env perl
+use strict;
+use warnings;
+
+use RT::Test tests => 6;
+use File::Temp qw/tempfile/;
+use Encode;
+use RT::Ticket;
+
+my ( $url, $m ) = RT::Test->started_ok;
+$m->default_header( 'Accept-Language' => "zh-cn" );
+ok( $m->login, 'logged in' );
+
+my $ticket_id;
+my $template;
+
+{
+
+ # test create message
+ $template = <<EOF;
+===Create-Ticket: ticket1
+Queue: General
+Subject: test message
+Status: new
+Content:
+ENDOFCONTENT
+Due:
+TimeEstimated: 100
+TimeLeft: 100
+FinalPriority: 90
+EOF
+
+ $m->get_ok( $url . '/Tools/Offline.html' );
+
+ $m->submit_form(
+ form_name => 'TicketUpdate',
+ fields => { string => $template, },
+ button => 'UpdateTickets',
+ );
+ my $content = encode 'utf8', $m->content;
+ ok( $content =~ qr/ç”³è¯·å• #(\d+) æˆåŠŸæ–°å¢žäºŽ &#39;General&#39; 表å•/, 'message is shown right' );
+ $ticket_id = $1;
+}
+
+{
+
+ # test update message
+ $template = <<EOF;
+===Update-Ticket: 1
+Subject: test message update
+EOF
+
+ $m->get_ok( $url . '/Tools/Offline.html' );
+ $m->submit_form(
+ form_name => 'TicketUpdate',
+ fields => { string => $template, },
+ button => 'UpdateTickets',
+ );
+
+ my $content = encode 'utf8', $m->content;
+ ok(
+ $content =~
+qr/主题\s*的值从\s*&#39;test message&#39;\s*改为\s*&#39;test message update&#39;/,
+ 'subject is updated'
+ );
+}
+
diff --git a/rt/t/web/offline_utf8.t b/rt/t/web/offline_utf8.t
new file mode 100644
index 000000000..2a3e64d3c
--- /dev/null
+++ b/rt/t/web/offline_utf8.t
@@ -0,0 +1,54 @@
+#!/usr/bin/env perl
+use strict;
+use warnings;
+
+use RT::Test tests => 8;
+use File::Temp qw/tempfile/;
+use Encode;
+use RT::Ticket;
+my ( $fh, $file ) = tempfile;
+my $template = <<EOF;
+===Create-Ticket: ticket1
+Queue: General
+Subject: 标题
+Status: new
+Content:
+这是正文
+ENDOFCONTENT
+EOF
+
+print $fh $template;
+close $fh;
+
+my ( $url, $m ) = RT::Test->started_ok;
+ok( $m->login, 'logged in' );
+
+$m->get_ok( $url . '/Tools/Offline.html' );
+
+$m->submit_form(
+ form_name => 'TicketUpdate',
+ fields => { Template => $file, },
+ button => 'Parse',
+);
+
+$m->content_contains( '这是正文', 'content is parsed right' );
+
+$m->submit_form(
+ form_name => 'TicketUpdate',
+ button => 'UpdateTickets',
+
+ # mimic what browsers do: they seems decoded $template
+ fields => { string => decode( 'utf8', $template ), },
+);
+
+$m->content_like( qr/Ticket \d+ created/, 'found ticket created message' );
+my ( $ticket_id ) = $m->content =~ /Ticket (\d+) created/;
+
+my $ticket = RT::Ticket->new( $RT::SystemUser );
+$ticket->Load( $ticket_id );
+is( $ticket->Subject, '标题', 'subject in $ticket is right' );
+
+$m->get_ok( $url . "/Ticket/Display.html?id=$ticket_id" );
+$m->content_contains( '这是正文',
+ 'content is right in ticket display page' );
+
diff --git a/rt/t/web/query_builder.t b/rt/t/web/query_builder.t
new file mode 100644
index 000000000..02ed1297f
--- /dev/null
+++ b/rt/t/web/query_builder.t
@@ -0,0 +1,249 @@
+#!/usr/bin/perl
+
+use strict;
+use HTTP::Request::Common;
+use HTTP::Cookies;
+use LWP;
+use Encode;
+use RT::Test tests => 42;
+
+my $cookie_jar = HTTP::Cookies->new;
+my ($baseurl, $agent) = RT::Test->started_ok;
+
+
+# give the agent a place to stash the cookies
+
+$agent->cookie_jar($cookie_jar);
+
+# create a regression queue if it doesn't exist
+my $queue = RT::Test->load_or_create_queue( Name => 'Regression' );
+ok $queue && $queue->id, 'loaded or created queue';
+
+my $url = $agent->rt_base_url;
+ok $agent->login, "logged in";
+
+# {{{ Query Builder tests
+
+my $response = $agent->get($url."Search/Build.html");
+ok $response->is_success, "Fetched ". $url ."Search/Build.html";
+
+sub getQueryFromForm {
+ $agent->form_name('BuildQuery');
+ # This pulls out the "hidden input" query from the page
+ my $q = $agent->current_form->find_input("Query")->value;
+ $q =~ s/^\s+//g;
+ $q =~ s/\s+$//g;
+ $q =~ s/\s+/ /g;
+ return $q;
+}
+
+sub selectedClauses {
+ my @clauses = grep { defined } map { $_->value } $agent->current_form->find_input("clauses");
+ return [ @clauses ];
+}
+
+
+diag "add the first condition" if $ENV{'TEST_VERBOSE'};
+{
+ ok $agent->form_name('BuildQuery'), "found the form once";
+ $agent->field("ActorField", "Owner");
+ $agent->field("ActorOp", "=");
+ $agent->field("ValueOfActor", "Nobody");
+ $agent->submit;
+ is getQueryFromForm, "Owner = 'Nobody'", 'correct query';
+}
+
+diag "set the next condition" if $ENV{'TEST_VERBOSE'};
+{
+ ok($agent->form_name('BuildQuery'), "found the form again");
+ $agent->field("QueueOp", "!=");
+ $agent->field("ValueOfQueue", "Regression");
+ $agent->submit;
+ is getQueryFromForm, "Owner = 'Nobody' AND Queue != 'Regression'",
+ 'correct query';
+}
+
+diag "We're going to delete the owner" if $ENV{'TEST_VERBOSE'};
+{
+ $agent->select("clauses", ["0"] );
+ $agent->click("DeleteClause");
+ ok $agent->form_name('BuildQuery'), "found the form";
+ is getQueryFromForm, "Queue != 'Regression'", 'correct query';
+}
+
+diag "add a cond with OR and se number by the way" if $ENV{'TEST_VERBOSE'};
+{
+ $agent->field("AndOr", "OR");
+ $agent->select("idOp", ">");
+ $agent->field("ValueOfid" => "1234");
+ $agent->click("AddClause");
+ ok $agent->form_name('BuildQuery'), "found the form again";
+ is getQueryFromForm, "Queue != 'Regression' OR id > 1234",
+ "added something as OR, and number not quoted";
+ is_deeply selectedClauses, ["1"], 'the id that we just entered is still selected';
+
+}
+
+diag "Move the second one up a level" if $ENV{'TEST_VERBOSE'};
+{
+ $agent->click("Up");
+ ok $agent->form_name('BuildQuery'), "found the form again";
+ is getQueryFromForm, "id > 1234 OR Queue != 'Regression'", "moved up one";
+ is_deeply selectedClauses, ["0"], 'the one we moved up is selected';
+}
+
+diag "Move the second one right" if $ENV{'TEST_VERBOSE'};
+{
+ $agent->click("Right");
+ ok $agent->form_name('BuildQuery'), "found the form again";
+ is getQueryFromForm, "Queue != 'Regression' OR ( id > 1234 )",
+ "moved over to the right (and down)";
+ is_deeply selectedClauses, ["2"], 'the one we moved right is selected';
+}
+
+diag "Move the block up" if $ENV{'TEST_VERBOSE'};
+{
+ $agent->select("clauses", ["1"]);
+ $agent->click("Up");
+ ok $agent->form_name('BuildQuery'), "found the form again";
+ is getQueryFromForm, "( id > 1234 ) OR Queue != 'Regression'", "moved up";
+ is_deeply selectedClauses, ["0"], 'the one we moved up is selected';
+}
+
+
+diag "Can not move up the top most clause" if $ENV{'TEST_VERBOSE'};
+{
+ $agent->select("clauses", ["0"]);
+ $agent->click("Up");
+ ok $agent->form_name('BuildQuery'), "found the form again";
+ $agent->content_like(qr/error: can\S+t move up/, "i shouldn't have been able to hit up");
+ is_deeply selectedClauses, ["0"], 'the one we tried to move is selected';
+}
+
+diag "Can not move left the left most clause" if $ENV{'TEST_VERBOSE'};
+{
+ $agent->click("Left");
+ ok($agent->form_name('BuildQuery'), "found the form again");
+ $agent->content_like(qr/error: can\S+t move left/, "i shouldn't have been able to hit left");
+ is_deeply selectedClauses, ["0"], 'the one we tried to move is selected';
+}
+
+diag "Add a condition into a nested block" if $ENV{'TEST_VERBOSE'};
+{
+ $agent->select("clauses", ["1"]);
+ $agent->select("ValueOfStatus" => "stalled");
+ $agent->submit;
+ ok $agent->form_name('BuildQuery'), "found the form again";
+ is_deeply selectedClauses, ["2"], 'the one we added is only selected';
+ is getQueryFromForm,
+ "( id > 1234 AND Status = 'stalled' ) OR Queue != 'Regression'",
+ "added new one";
+}
+
+diag "click advanced, enter 'C1 OR ( C2 AND C3 )', apply, aggregators should stay the same."
+ if $ENV{'TEST_VERBOSE'};
+{
+ my $response = $agent->get($url."Search/Edit.html");
+ ok( $response->is_success, "Fetched /Search/Edit.html" );
+ ok($agent->form_number(3), "found the form");
+ $agent->field("Query", "Status = 'new' OR ( Status = 'open' AND Subject LIKE 'office' )");
+ $agent->submit;
+ is( getQueryFromForm,
+ "Status = 'new' OR ( Status = 'open' AND Subject LIKE 'office' )",
+ "no aggregators change"
+ );
+}
+
+# - new items go one level down
+# - add items at currently selected level
+# - if nothing is selected, add at end, one level down
+#
+# move left
+# - error if nothing selected
+# - same item should be selected after move
+# - can't move left if you're at the top level
+#
+# move right
+# - error if nothing selected
+# - same item should be selected after move
+# - can always move right (no max depth...should there be?)
+#
+# move up
+# - error if nothing selected
+# - same item should be selected after move
+# - can't move up if you're first in the list
+#
+# move down
+# - error if nothing selected
+# - same item should be selected after move
+# - can't move down if you're last in the list
+#
+# toggle
+# - error if nothing selected
+# - change all aggregators in the grouping
+# - don't change any others
+#
+# delete
+# - error if nothing selected
+# - delete currently selected item
+# - delete all children of a grouping
+# - if delete leaves a node with no children, delete that, too
+# - what should be selected?
+#
+# Clear
+# - clears entire query
+# - clears it from the session, too
+
+# }}}
+
+# create a custom field with nonascii name and try to add a condition
+{
+ my $cf = RT::CustomField->new( $RT::SystemUser );
+ $cf->LoadByName( Name => "\x{442}", Queue => 0 );
+ if ( $cf->id ) {
+ is($cf->Type, 'Freeform', 'loaded and type is correct');
+ } else {
+ my ($return, $msg) = $cf->Create(
+ Name => "\x{442}",
+ Queue => 0,
+ Type => 'Freeform',
+ );
+ ok($return, 'created CF') or diag "error: $msg";
+ }
+
+ my $response = $agent->get($url."Search/Build.html?NewQuery=1");
+ ok( $response->is_success, "Fetched " . $url."Search/Build.html" );
+
+ ok($agent->form_name('BuildQuery'), "found the form once");
+ $agent->field("ValueOf'CF.{\x{442}}'", "\x{441}");
+ $agent->submit();
+ is( getQueryFromForm,
+ "'CF.{\x{442}}' LIKE '\x{441}'",
+ "no changes, no duplicate condition with badly encoded text"
+ );
+
+}
+
+diag "input a condition, select (several conditions), click delete"
+ if $ENV{'TEST_VERBOSE'};
+{
+ my $response = $agent->get( $url."Search/Edit.html" );
+ ok $response->is_success, "Fetched /Search/Edit.html";
+ ok $agent->form_number(3), "found the form";
+ $agent->field("Query", "( Status = 'new' OR Status = 'open' )");
+ $agent->submit;
+ is( getQueryFromForm,
+ "( Status = 'new' OR Status = 'open' )",
+ "query is the same"
+ );
+ $agent->select("clauses", [qw(0 1 2)]);
+ $agent->field( ValueOfid => 10 );
+ $agent->click("DeleteClause");
+
+ is( getQueryFromForm,
+ "id < 10",
+ "replaced query successfuly"
+ );
+}
+
+1;
diff --git a/rt/t/web/quicksearch.t b/rt/t/web/quicksearch.t
new file mode 100644
index 000000000..cd9a8e76c
--- /dev/null
+++ b/rt/t/web/quicksearch.t
@@ -0,0 +1,51 @@
+#!/usr/bin/env perl
+use strict;
+use warnings;
+
+use RT::Test tests => 7;
+my ($baseurl, $m) = RT::Test->started_ok;
+my $url = $m->rt_base_url;
+
+# merged tickets still show up in search
+my $t1 = RT::Ticket->new($RT::SystemUser);
+$t1->Create(
+ Subject => 'base ticket'.$$,
+ Queue => 'general',
+ Owner => 'root',
+ Requestor => 'customsearch@localhost',
+ MIMEObj => MIME::Entity->build(
+ From => 'customsearch@localhost',
+ To => 'rt@localhost',
+ Subject => 'base ticket'.$$,
+ Data => "DON'T SEARCH FOR ME",
+ ),
+);
+ok(my $id1 = $t1->id, 'created ticket for custom search');
+
+my $t2 = RT::Ticket->new($RT::SystemUser);
+$t2->Create(
+ Subject => 'merged away'.$$,
+ Queue => 'general',
+ Owner => 'root',
+ Requestor => 'customsearch@localhost',
+ MIMEObj => MIME::Entity->build(
+ From => 'customsearch@localhost',
+ To => 'rt@localhost',
+ Subject => 'merged away'.$$,
+ Data => "MERGEDAWAY",
+ ),
+);
+ok(my $id2 = $t2->id, 'created ticket for custom search');
+
+my ($ok, $msg) = $t2->MergeInto($id1);
+ok($ok, "merge: $msg");
+
+ok($m->login, 'logged in');
+
+$m->form_with_fields('q');
+$m->field(q => 'fulltext:MERGEDAWAY');
+TODO: {
+ local $TODO = "We don't yet handle merged ticket content searches right";
+$m->content_contains('Found 1 ticket');
+}
+$m->content_contains('base ticket', "base ticket is found, not the merged-away ticket");
diff --git a/rt/t/web/rest-non-ascii-subject.t b/rt/t/web/rest-non-ascii-subject.t
new file mode 100644
index 000000000..70c910afe
--- /dev/null
+++ b/rt/t/web/rest-non-ascii-subject.t
@@ -0,0 +1,55 @@
+#!/usr/bin/env perl
+# Test ticket creation with REST using non ascii subject
+use strict;
+use warnings;
+use RT::Test tests => 7;
+
+local $RT::Test::SKIP_REQUEST_WORK_AROUND = 1;
+
+use Encode;
+# \x{XX} where XX is less than 255 is not treated as unicode code point
+my $subject = Encode::decode('latin1', "Sujet accentu\x{e9}");
+my $text = Encode::decode('latin1', "Contenu accentu\x{e9}");
+
+my ($baseurl, $m) = RT::Test->started_ok;
+
+my $queue = RT::Test->load_or_create_queue(Name => 'General');
+ok($queue->Id, "loaded the General queue");
+
+my $content = "id: ticket/new
+Queue: General
+Requestor: root
+Subject: $subject
+Cc:
+AdminCc:
+Owner:
+Status: new
+Priority:
+InitialPriority:
+FinalPriority:
+TimeEstimated:
+Starts: 2009-03-10 16:14:55
+Due: 2009-03-10 16:14:55
+Text: $text";
+
+$m->post("$baseurl/REST/1.0/ticket/new", [
+ user => 'root',
+ pass => 'password',
+# error message from HTTP::Message: content must be bytes
+ content => Encode::encode_utf8($content),
+], Content_Type => 'form-data' );
+
+my ($id) = $m->content =~ /Ticket (\d+) created/;
+ok($id, "got ticket #$id");
+
+my $ticket = RT::Ticket->new($RT::SystemUser);
+$ticket->Load($id);
+is($ticket->Id, $id, "loaded the REST-created ticket");
+is($ticket->Subject, $subject, "ticket subject successfully set");
+
+my $attach = $ticket->Transactions->First->Attachments->First;
+is($attach->Subject, $subject, "attachement subject successfully set");
+TODO: {
+ local $TODO = "Not fixed yet, but not a regression";
+ is($attach->GetHeader('Subject'), $subject, "attachement header subject successfully set");
+}
diff --git a/rt/t/web/rest.t b/rt/t/web/rest.t
new file mode 100644
index 000000000..b3a7c558b
--- /dev/null
+++ b/rt/t/web/rest.t
@@ -0,0 +1,71 @@
+#!/usr/bin/env perl
+use strict;
+use warnings;
+use RT::Test tests => 16;
+
+my ($baseurl, $m) = RT::Test->started_ok;
+
+for my $name ("severity", "fu()n:k/") {
+ my $cf = RT::Test->load_or_create_custom_field(
+ Name => $name,
+ Type => 'Freeform',
+ Queue => 'General',
+ );
+ ok($cf->Id, "created a CustomField");
+ is($cf->Name, $name, "correct CF name");
+}
+
+my $queue = RT::Test->load_or_create_queue(Name => 'General');
+ok($queue->Id, "loaded the General queue");
+
+$m->post("$baseurl/REST/1.0/ticket/new", [
+ user => 'root',
+ pass => 'password',
+ format => 'l',
+]);
+
+my $text = $m->content;
+my @lines = $text =~ m{.*}g;
+shift @lines; # header
+
+# CFs aren't in the default ticket form
+push @lines, "CF-fu()n:k/: maximum"; # old style
+push @lines, "CF.{severity}: explosive"; # new style
+
+$text = join "\n", @lines;
+
+ok($text =~ s/Subject:\s*$/Subject: REST interface/m, "successfully replaced subject");
+
+$m->post("$baseurl/REST/1.0/ticket/edit", [
+ user => 'root',
+ pass => 'password',
+
+ content => $text,
+], Content_Type => 'form-data');
+
+my ($id) = $m->content =~ /Ticket (\d+) created/;
+ok($id, "got ticket #$id");
+
+my $ticket = RT::Ticket->new($RT::SystemUser);
+$ticket->Load($id);
+is($ticket->Id, $id, "loaded the REST-created ticket");
+is($ticket->Subject, "REST interface", "subject successfully set");
+is($ticket->FirstCustomFieldValue("fu()n:k/"), "maximum", "CF successfully set");
+
+$m->post("$baseurl/REST/1.0/search/ticket", [
+ user => 'root',
+ pass => 'password',
+ query => "id=$id",
+ fields => "Subject,CF-fu()n:k/,CF.{severity},Status",
+]);
+
+# the fields are interpreted server-side a hash (why?), so we can't depend
+# on order
+for ("id: ticket/1",
+ "Subject: REST interface",
+ "CF.{fu()n:k/}: maximum",
+ "CF.{severity}: explosive",
+ "Status: new") {
+ $m->content_contains($_);
+}
+
diff --git a/rt/t/web/rights.t b/rt/t/web/rights.t
new file mode 100644
index 000000000..b47ba99af
--- /dev/null
+++ b/rt/t/web/rights.t
@@ -0,0 +1,85 @@
+#!/usr/bin/perl -w
+use strict;
+use warnings;
+
+use RT::Test tests => 14;
+
+my ($baseurl, $m) = RT::Test->started_ok;
+ok $m->login, "logged in";
+
+$m->follow_link_ok({ text => 'Configuration' });
+$m->follow_link_ok({ text => 'Global' });
+$m->follow_link_ok({ text => 'Group Rights' });
+
+
+sub get_rights {
+ my $agent = shift;
+ my $principal_id = shift;
+ my $object = shift;
+ $agent->form_number(3);
+ my @inputs = $agent->current_form->find_input("RevokeRight-$principal_id-$object");
+ my @rights = sort grep $_, map $_->possible_values, grep $_, @inputs;
+ return @rights;
+};
+
+diag "load Everyone group" if $ENV{'TEST_VERBOSE'};
+my ($everyone, $everyone_gid);
+{
+ $everyone = RT::Group->new( $RT::SystemUser );
+ $everyone->LoadSystemInternalGroup('Everyone');
+ ok($everyone_gid = $everyone->id, "loaded 'everyone' group");
+}
+
+diag "revoke all global rights from Everyone group" if $ENV{'TEST_VERBOSE'};
+my @has = get_rights( $m, $everyone_gid, 'RT::System-1' );
+if ( @has ) {
+ $m->form_number(3);
+ $m->tick("RevokeRight-$everyone_gid-RT::System-1", $_) foreach @has;
+ $m->submit;
+
+ is_deeply([get_rights( $m, $everyone_gid, 'RT::System-1' )], [], 'deleted all rights' );
+} else {
+ ok(1, 'the group has no global rights');
+}
+
+diag "grant SuperUser right to everyone" if $ENV{'TEST_VERBOSE'};
+{
+ $m->form_number(3);
+ $m->select("GrantRight-$everyone_gid-RT::System-1", ['SuperUser']);
+ $m->submit;
+
+ $m->content_contains('Right Granted', 'got message');
+ RT::Principal::InvalidateACLCache();
+ ok($everyone->PrincipalObj->HasRight( Right => 'SuperUser', Object => $RT::System ), 'group has right');
+ is_deeply( [get_rights( $m, $everyone_gid, 'RT::System-1' )], ['SuperUser'], 'granted SuperUser right' );
+}
+
+diag "revoke the right" if $ENV{'TEST_VERBOSE'};
+{
+ $m->form_number(3);
+ $m->tick("RevokeRight-$everyone_gid-RT::System-1", 'SuperUser');
+ $m->submit;
+
+ $m->content_contains('Right revoked', 'got message');
+ RT::Principal::InvalidateACLCache();
+ ok(!$everyone->PrincipalObj->HasRight( Right => 'SuperUser', Object => $RT::System ), 'group has no right');
+ is_deeply( [get_rights( $m, $everyone_gid, 'RT::System-1' )], [], 'revoked SuperUser right' );
+}
+
+
+diag "return rights the group had in the beginning" if $ENV{'TEST_VERBOSE'};
+if ( @has ) {
+ $m->form_number(3);
+ $m->select("GrantRight-$everyone_gid-RT::System-1", \@has);
+ $m->submit;
+
+ $m->content_contains('Right Granted', 'got message');
+ is_deeply(
+ [ get_rights( $m, $everyone_gid, 'RT::System-1' ) ],
+ [ @has ],
+ 'returned back all rights'
+ );
+} else {
+ ok(1, 'the group had no global rights, so nothing to return');
+}
+
diff --git a/rt/t/web/rights1.t b/rt/t/web/rights1.t
new file mode 100644
index 000000000..6da204cc9
--- /dev/null
+++ b/rt/t/web/rights1.t
@@ -0,0 +1,134 @@
+#!/usr/bin/perl -w
+use strict;
+use HTTP::Cookies;
+
+use RT::Test tests => 35;
+my ($baseurl, $agent) = RT::Test->started_ok;
+
+# Create a user with basically no rights, to start.
+my $user_obj = RT::User->new($RT::SystemUser);
+my ($ret, $msg) = $user_obj->LoadOrCreateByEmail('customer-'.$$.'@example.com');
+ok($ret, 'ACL test user creation');
+$user_obj->SetName('customer-'.$$);
+$user_obj->SetPrivileged(1);
+($ret, $msg) = $user_obj->SetPassword('customer');
+ok($ret, "ACL test password set. $msg");
+
+# Now test the web interface, making sure objects come and go as
+# required.
+
+
+my $cookie_jar = HTTP::Cookies->new;
+
+# give the agent a place to stash the cookies
+
+$agent->cookie_jar($cookie_jar);
+
+no warnings 'once';
+# get the top page
+login($agent, $user_obj);
+
+# Test for absence of Configure and Preferences tabs.
+ok(!$agent->find_link( url => "$RT::WebPath/Admin/",
+ text => 'Configuration'), "No config tab" );
+ok(!$agent->find_link( url => "$RT::WebPath/User/Prefs.html",
+ text => 'Preferences'), "No prefs pane" );
+
+# Now test for their presence, one at a time. Sleep for a bit after
+# ACL changes, thanks to the 10s ACL cache.
+my ($grantid,$grantmsg) =$user_obj->PrincipalObj->GrantRight(Right => 'ShowConfigTab', Object => $RT::System);
+
+ok($grantid,$grantmsg);
+
+$agent->reload;
+
+like($agent->{'content'} , qr/Logout/i, "Reloaded page successfully");
+ok($agent->find_link( url => "$RT::WebPath/Admin/",
+ text => 'Configuration'), "Found config tab" );
+my ($revokeid,$revokemsg) =$user_obj->PrincipalObj->RevokeRight(Right => 'ShowConfigTab');
+ok ($revokeid,$revokemsg);
+($grantid,$grantmsg) =$user_obj->PrincipalObj->GrantRight(Right => 'ModifySelf');
+ok ($grantid,$grantmsg);
+$agent->reload();
+like($agent->{'content'} , qr/Logout/i, "Reloaded page successfully");
+ok($agent->find_link(
+ text => 'Preferences'), "Found prefs pane" );
+($revokeid,$revokemsg) = $user_obj->PrincipalObj->RevokeRight(Right => 'ModifySelf');
+ok ($revokeid,$revokemsg);
+# Good. Now load the search page and test Load/Save Search.
+$agent->follow_link( url => "$RT::WebPath/Search/Build.html",
+ text => 'Tickets');
+is($agent->{'status'}, 200, "Fetched search builder page");
+ok($agent->{'content'} !~ /Load saved search/i, "No search loading box");
+ok($agent->{'content'} !~ /Saved searches/i, "No saved searches box");
+
+($grantid,$grantmsg) = $user_obj->PrincipalObj->GrantRight(Right => 'LoadSavedSearch');
+ok($grantid,$grantmsg);
+$agent->reload();
+like($agent->{'content'} , qr/Load saved search/i, "Search loading box exists");
+ok($agent->{'content'} !~ /input\s+type=['"]submit['"][^>]+name=['"]SavedSearchSave['"]/i,
+ "Still no saved searches box");
+
+($grantid,$grantmsg) =$user_obj->PrincipalObj->GrantRight(Right => 'CreateSavedSearch');
+ok ($grantid,$grantmsg);
+$agent->reload();
+like($agent->{'content'} , qr/Load saved search/i,
+ "Search loading box still exists");
+like($agent->{'content'} , qr/input\s+type=['"]submit['"][^>]+name=['"]SavedSearchSave['"]/i,
+ "Saved searches box exists");
+
+# Create a group, and a queue, so we can test limited user visibility
+# via SelectOwner.
+
+my $queue_obj = RT::Queue->new($RT::SystemUser);
+($ret, $msg) = $queue_obj->Create(Name => 'CustomerQueue-'.$$,
+ Description => 'queue for SelectOwner testing');
+ok($ret, "SelectOwner test queue creation. $msg");
+my $group_obj = RT::Group->new($RT::SystemUser);
+($ret, $msg) = $group_obj->CreateUserDefinedGroup(Name => 'CustomerGroup-'.$$,
+ Description => 'group for SelectOwner testing');
+ok($ret, "SelectOwner test group creation. $msg");
+
+# Add our customer to the customer group, and give it queue rights.
+($ret, $msg) = $group_obj->AddMember($user_obj->PrincipalObj->Id());
+ok($ret, "Added customer to its group. $msg");
+($grantid,$grantmsg) =$group_obj->PrincipalObj->GrantRight(Right => 'OwnTicket',
+ Object => $queue_obj);
+
+ok($grantid,$grantmsg);
+($grantid,$grantmsg) =$group_obj->PrincipalObj->GrantRight(Right => 'SeeQueue',
+ Object => $queue_obj);
+ok ($grantid,$grantmsg);
+# Now. When we look at the search page we should be able to see
+# ourself in the list of possible owners.
+
+$agent->reload();
+ok($agent->form_name('BuildQuery'), "Yep, form is still there");
+my $input = $agent->current_form->find_input('ValueOfActor');
+ok(grep(/customer-$$/, $input->value_names()), "Found self in the actor listing");
+
+sub login {
+ my $agent = shift;
+
+ my $url = "http://localhost:" . RT->Config->Get('WebPort') . RT->Config->Get('WebPath') . "/";
+ $agent->get($url);
+ is( $agent->{'status'}, 200,
+ "Loaded a page - http://localhost" . RT->Config->Get('WebPath') );
+
+ # {{{ test a login
+
+ # follow the link marked "Login"
+
+ ok( $agent->{form}->find_input('user') );
+
+ ok( $agent->{form}->find_input('pass') );
+ like( $agent->{'content'} , qr/username:/i );
+ $agent->field( 'user' => $user_obj->Name );
+ $agent->field( 'pass' => 'customer' );
+
+ # the field isn't named, so we have to click link 0
+ $agent->click(0);
+ is( $agent->{'status'}, 200, "Fetched the page ok" );
+ like( $agent->{'content'} , qr/Logout/i, "Found a logout link" );
+}
+1;
diff --git a/rt/t/web/saved_search_chart.t b/rt/t/web/saved_search_chart.t
new file mode 100644
index 000000000..105166233
--- /dev/null
+++ b/rt/t/web/saved_search_chart.t
@@ -0,0 +1,86 @@
+#!/usr/bin/env perl
+use strict;
+use warnings;
+
+use RT::Test tests => 19;
+my ( $url, $m ) = RT::Test->started_ok;
+use RT::Attribute;
+my $search = RT::Attribute->new($RT::SystemUser);
+my $ticket = RT::Ticket->new($RT::SystemUser);
+my ( $ret, $msg ) = $ticket->Create(
+ Subject => 'base ticket' . $$,
+ Queue => 'general',
+ Owner => 'root',
+ Requestor => 'root@localhost',
+ MIMEObj => MIME::Entity->build(
+ From => 'root@localhost',
+ To => 'rt@localhost',
+ Subject => 'base ticket' . $$,
+ Data => "",
+ ),
+);
+ok( $ret, "ticket created: $msg" );
+
+ok( $m->login, 'logged in' );
+
+$m->get_ok( $url . "/Search/Chart.html?Query=" . 'id=1' );
+my ($owner) = $m->content =~ /value="(RT::User-\d+)"/;
+
+$m->submit_form(
+ form_name => 'SaveSearch',
+ fields => {
+ SavedSearchDescription => 'first chart',
+ SavedSearchOwner => $owner,
+ },
+ button => 'SavedSearchSave',
+);
+
+$m->content_like( qr/Chart first chart saved/, 'saved first chart' );
+
+my ( $search_uri, $id ) = $m->content =~ /value="(RT::User-\d+-SavedSearch-(\d+))"/;
+$m->submit_form(
+ form_name => 'SaveSearch',
+ fields => { SavedSearchLoad => $search_uri },
+);
+
+$m->content_like( qr/name="SavedSearchDelete"\s+value="Delete"/,
+ 'found Delete button' );
+$m->content_like(
+ qr/name="SavedSearchDescription"\s+value="first chart"/,
+ 'found Description input with the value filled'
+);
+$m->content_like( qr/name="SavedSearchSave"\s+value="Update"/,
+ 'found Update button' );
+$m->content_unlike( qr/name="SavedSearchSave"\s+value="Save"/,
+ 'no Save button' );
+
+$m->submit_form(
+ form_name => 'SaveSearch',
+ fields => {
+ Query => 'id=2',
+ PrimaryGroupBy => 'Status',
+ ChartStyle => 'pie',
+ },
+ button => 'SavedSearchSave',
+);
+
+$m->content_like( qr/Chart first chart updated/, 'found updated message' );
+$m->content_like( qr/id=2/, 'Query is updated' );
+$m->content_like( qr/value="Status"\s+selected="selected"/,
+ 'PrimaryGroupBy is updated' );
+$m->content_like( qr/value="pie"\s+selected="selected"/,
+ 'ChartType is updated' );
+ok( $search->Load($id) );
+is( $search->SubValue('Query'), 'id=2', 'Query is indeed updated' );
+is( $search->SubValue('PrimaryGroupBy'),
+ 'Status', 'PrimaryGroupBy is indeed updated' );
+is( $search->SubValue('ChartStyle'), 'pie', 'ChartStyle is indeed updated' );
+
+# finally, let's test delete
+$m->submit_form(
+ form_name => 'SaveSearch',
+ button => 'SavedSearchDelete',
+);
+$m->content_like( qr/Chart first chart deleted/, 'found deleted message' );
+$m->content_unlike( qr/value="RT::User-\d+-SavedSearch-\d+"/,
+ 'no saved search' );
diff --git a/rt/t/web/saved_search_permissions.t b/rt/t/web/saved_search_permissions.t
new file mode 100644
index 000000000..f91ca13c6
--- /dev/null
+++ b/rt/t/web/saved_search_permissions.t
@@ -0,0 +1,34 @@
+#!/usr/bin/env perl
+use strict;
+use warnings;
+
+use RT::Test tests => 10;
+my $user = RT::User->new($RT::SystemUser);
+ok(
+ $user->Create(
+ Name => 'foo',
+ Privileged => 1,
+ Password => 'foobar'
+ )
+);
+
+my ( $url, $m ) = RT::Test->started_ok;
+ok( $m->login, 'root logged in' );
+$m->get_ok( $url . '/Search/Build.html?Query=id<100' );
+$m->submit_form(
+ form_name => 'BuildQuery',
+ fields => { SavedSearchDescription => 'test' },
+ button => 'SavedSearchSave',
+);
+$m->content_contains( q{name="SavedSearchDescription" value="test"},
+ 'saved test search' );
+my ($id) = $m->content =~ /value="(RT::User-\d+-SavedSearch-\d+)"/;
+ok( $m->login( 'foo', 'foobar' ), 'logged in' );
+$m->get_ok( $url . "/Search/Build.html?SavedSearchLoad=$id" );
+
+my $message = qq{Can not load saved search "$id"};
+RT::Interface::Web::EscapeUTF8( \$message );
+$m->content_contains( $message, 'user foo can not load saved search of root' );
+
+$m->warning_like( qr/User #\d+ tried to load container user #\d+/,
+ 'get warning' );
diff --git a/rt/t/web/search_bulk_update_links.t b/rt/t/web/search_bulk_update_links.t
new file mode 100644
index 000000000..d6bfdfd3c
--- /dev/null
+++ b/rt/t/web/search_bulk_update_links.t
@@ -0,0 +1,147 @@
+#!/usr/bin/env perl
+use strict;
+use warnings;
+
+use RT::Test tests => 28;
+my ( $url, $m ) = RT::Test->started_ok;
+ok( $m->login, 'logged in' );
+
+my $rtname = RT->Config->Get('rtname');
+
+# create tickets
+use RT::Ticket;
+
+my ( @link_tickets, @search_tickets );
+for ( 1 .. 3 ) {
+ my $link_ticket = RT::Ticket->new($RT::SystemUser);
+ my ( $ret, $msg ) = $link_ticket->Create(
+ Subject => "link ticket $_",
+ Queue => 'general',
+ Owner => 'root',
+ Requestor => 'root@localhost',
+ );
+ ok( $ret, "link ticket created: $msg" );
+ push @link_tickets, $ret;
+}
+
+for ( 1 .. 3 ) {
+ my $ticket = RT::Ticket->new($RT::SystemUser);
+ my ( $ret, $msg ) = $ticket->Create(
+ Subject => "search ticket $_",
+ Queue => 'general',
+ Owner => 'root',
+ Requestor => 'root@localhost',
+ );
+ ok( $ret, "search ticket created: $msg" );
+ push @search_tickets, $ret;
+}
+
+# let's add link to 1 search ticket first
+$m->get_ok( $url . "/Search/Bulk.html?Query=id=$search_tickets[0]&Rows=10" );
+$m->content_contains( 'Current Links', 'has current links part' );
+$m->content_lacks( 'DeleteLink--', 'no delete link stuff' );
+$m->submit_form(
+ form_number => 3,
+ fields => {
+ 'Ticket-DependsOn' => $link_tickets[0],
+ 'Ticket-MemberOf' => $link_tickets[1],
+ 'Ticket-RefersTo' => $link_tickets[2],
+ },
+);
+$m->content_contains(
+ "Ticket $search_tickets[0] depends on Ticket $link_tickets[0]",
+ 'depends on msg',
+);
+$m->content_contains(
+ "Ticket $search_tickets[0] member of Ticket $link_tickets[1]",
+ 'member of msg',
+);
+$m->content_contains(
+ "Ticket $search_tickets[0] refers to Ticket $link_tickets[2]",
+ 'refers to msg',
+);
+
+$m->content_contains(
+ "DeleteLink--DependsOn-fsck.com-rt://$rtname/ticket/$link_tickets[0]",
+ 'found depends on link' );
+$m->content_contains(
+ "DeleteLink--MemberOf-fsck.com-rt://$rtname/ticket/$link_tickets[1]",
+ 'found member of link' );
+$m->content_contains(
+ "DeleteLink--RefersTo-fsck.com-rt://$rtname/ticket/$link_tickets[2]",
+ 'found refers to link' );
+
+# here we check the *real* bulk update
+my $query = join ' OR ', map { "id=$_" } @search_tickets;
+$m->get_ok( $url . "/Search/Bulk.html?Query=$query&Rows=10" );
+$m->content_contains( 'Current Links', 'has current links part' );
+$m->content_lacks( 'DeleteLink--', 'no delete link stuff' );
+
+# test DependsOn, MemberOf and RefersTo
+$m->submit_form(
+ form_number => 3,
+ fields => {
+ 'Ticket-DependsOn' => $link_tickets[0],
+ 'Ticket-MemberOf' => $link_tickets[1],
+ 'Ticket-RefersTo' => $link_tickets[2],
+ },
+);
+
+$m->content_contains(
+ "DeleteLink--DependsOn-fsck.com-rt://$rtname/ticket/$link_tickets[0]",
+ 'found depends on link' );
+$m->content_contains(
+ "DeleteLink--MemberOf-fsck.com-rt://$rtname/ticket/$link_tickets[1]",
+ 'found member of link' );
+$m->content_contains(
+ "DeleteLink--RefersTo-fsck.com-rt://$rtname/ticket/$link_tickets[2]",
+ 'found refers to link' );
+
+$m->submit_form(
+ form_number => 3,
+ fields => {
+ "DeleteLink--DependsOn-fsck.com-rt://$rtname/ticket/$link_tickets[0]" =>
+ 1,
+ "DeleteLink--MemberOf-fsck.com-rt://$rtname/ticket/$link_tickets[1]" =>
+ 1,
+ "DeleteLink--RefersTo-fsck.com-rt://$rtname/ticket/$link_tickets[2]" =>
+ 1,
+ },
+);
+
+$m->content_lacks( 'DeleteLink--', 'links are all deleted' );
+
+# test DependedOnBy, Members and ReferredToBy
+
+$m->submit_form(
+ form_number => 3,
+ fields => {
+ 'DependsOn-Ticket' => $link_tickets[0],
+ 'MemberOf-Ticket' => $link_tickets[1],
+ 'RefersTo-Ticket' => $link_tickets[2],
+ },
+);
+
+$m->content_contains(
+ "DeleteLink-fsck.com-rt://$rtname/ticket/$link_tickets[0]-DependsOn-",
+ 'found depended on link' );
+$m->content_contains(
+ "DeleteLink-fsck.com-rt://$rtname/ticket/$link_tickets[1]-MemberOf-",
+ 'found members link' );
+$m->content_contains(
+ "DeleteLink-fsck.com-rt://$rtname/ticket/$link_tickets[2]-RefersTo-",
+ 'found referrd to link' );
+
+$m->submit_form(
+ form_number => 3,
+ fields => {
+ "DeleteLink-fsck.com-rt://$rtname/ticket/$link_tickets[0]-DependsOn-" =>
+ 1,
+ "DeleteLink-fsck.com-rt://$rtname/ticket/$link_tickets[1]-MemberOf-" =>
+ 1,
+ "DeleteLink-fsck.com-rt://$rtname/ticket/$link_tickets[2]-RefersTo-" =>
+ 1,
+ },
+);
+$m->content_lacks( 'DeleteLink--', 'links are all deleted' );
+
diff --git a/rt/t/web/ticket-create-utf8.t b/rt/t/web/ticket-create-utf8.t
new file mode 100644
index 000000000..a4d7ae98d
--- /dev/null
+++ b/rt/t/web/ticket-create-utf8.t
@@ -0,0 +1,83 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use RT::Test tests => 14;
+
+$RT::Test::SKIP_REQUEST_WORK_AROUND = 1;
+
+use Encode;
+
+my $ru_test = "\x{442}\x{435}\x{441}\x{442}";
+my $ru_autoreply = "\x{410}\x{432}\x{442}\x{43e}\x{43e}\x{442}\x{432}\x{435}\x{442}";
+my $ru_support = "\x{43f}\x{43e}\x{434}\x{434}\x{435}\x{440}\x{436}\x{43a}\x{430}";
+
+my $q = RT::Test->load_or_create_queue( Name => 'Regression' );
+ok $q && $q->id, 'loaded or created queue';
+
+RT::Test->set_rights(
+ Principal => 'Everyone',
+ Right => ['CreateTicket', 'ShowTicket', 'SeeQueue', 'ReplyToTicket', 'ModifyTicket'],
+);
+
+my ($baseurl, $m) = RT::Test->started_ok;
+ok $m->login, 'logged in';
+
+# create a ticket with a subject only
+{
+ ok $m->goto_create_ticket( $q ), "go to create ticket";
+ $m->form_number(3);
+ $m->field( Subject => $ru_test );
+ $m->submit;
+
+ $m->content_like(
+ qr{<td\s+class="message-header-value"[^>]*>\s*\Q$ru_test\E\s*</td>}i,
+ 'header on the page'
+ );
+
+ my $ticket = RT::Test->last_ticket;
+ is $ticket->Subject, $ru_test, "correct subject";
+}
+
+# create a ticket with a subject and content
+{
+ ok $m->goto_create_ticket( $q ), "go to create ticket";
+ $m->form_number(3);
+ $m->field( Subject => $ru_test );
+ $m->field( Content => $ru_support );
+ $m->submit;
+
+ $m->content_like(
+ qr{<td\s+class="message-header-value"[^>]*>\s*\Q$ru_test\E\s*</td>}i,
+ 'header on the page'
+ );
+ $m->content_like(
+ qr{\Q$ru_support\E}i,
+ 'content on the page'
+ );
+
+ my $ticket = RT::Test->last_ticket;
+ is $ticket->Subject, $ru_test, "correct subject";
+}
+
+# create a ticket with a subject and content
+{
+ ok $m->goto_create_ticket( $q ), "go to create ticket";
+ $m->form_number(3);
+ $m->field( Subject => $ru_test );
+ $m->field( Content => $ru_support );
+ $m->submit;
+
+ $m->content_like(
+ qr{<td\s+class="message-header-value"[^>]*>\s*\Q$ru_test\E\s*</td>}i,
+ 'header on the page'
+ );
+ $m->content_like(
+ qr{\Q$ru_support\E}i,
+ 'content on the page'
+ );
+
+ my $ticket = RT::Test->last_ticket;
+ is $ticket->Subject, $ru_test, "correct subject";
+}
diff --git a/rt/t/web/ticket_owner.t b/rt/t/web/ticket_owner.t
new file mode 100644
index 000000000..0bacaf1bc
--- /dev/null
+++ b/rt/t/web/ticket_owner.t
@@ -0,0 +1,356 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use RT::Test tests => 91;
+
+my $queue = RT::Test->load_or_create_queue( Name => 'Regression' );
+ok $queue && $queue->id, 'loaded or created queue';
+
+my $user_a = RT::Test->load_or_create_user(
+ Name => 'user_a', Password => 'password',
+);
+ok $user_a && $user_a->id, 'loaded or created user';
+
+my $user_b = RT::Test->load_or_create_user(
+ Name => 'user_b', Password => 'password',
+);
+ok $user_b && $user_b->id, 'loaded or created user';
+
+RT::Test->started_ok;
+
+ok( RT::Test->set_rights(
+ { Principal => $user_a, Right => [qw(SeeQueue ShowTicket CreateTicket ReplyToTicket)] },
+ { Principal => $user_b, Right => [qw(SeeQueue ShowTicket OwnTicket)] },
+), 'set rights');
+
+my $agent_a = RT::Test::Web->new;
+ok $agent_a->login('user_a', 'password'), 'logged in as user A';
+
+diag "current user has no right to own, nobody selected as owner on create" if $ENV{TEST_VERBOSE};
+{
+ $agent_a->get_ok('/', 'open home page');
+ $agent_a->form_name('CreateTicketInQueue');
+ $agent_a->select( 'Queue', $queue->id );
+ $agent_a->submit;
+
+ $agent_a->content_like(qr/Create a new ticket/i, 'opened create ticket page');
+ my $form = $agent_a->form_name('TicketCreate');
+ is $form->value('Owner'), $RT::Nobody->id, 'correct owner selected';
+ ok !grep($_ == $user_a->id, $form->find_input('Owner')->possible_values),
+ 'user A can not own tickets';
+ $agent_a->submit;
+
+ $agent_a->content_like(qr/Ticket \d+ created in queue/i, 'created ticket');
+ my ($id) = ($agent_a->content =~ /Ticket (\d+) created in queue/);
+ ok $id, 'found id of the ticket';
+
+ my $ticket = RT::Ticket->new( $RT::SystemUser );
+ $ticket->Load( $id );
+ ok $ticket->id, 'loaded the ticket';
+ is $ticket->Owner, $RT::Nobody->id, 'correct owner';
+}
+
+diag "user can chose owner of a new ticket" if $ENV{TEST_VERBOSE};
+{
+ $agent_a->get_ok('/', 'open home page');
+ $agent_a->form_name('CreateTicketInQueue');
+ $agent_a->select( 'Queue', $queue->id );
+ $agent_a->submit;
+
+ $agent_a->content_like(qr/Create a new ticket/i, 'opened create ticket page');
+ my $form = $agent_a->form_name('TicketCreate');
+ is $form->value('Owner'), $RT::Nobody->id, 'correct owner selected';
+
+ ok grep($_ == $user_b->id, $form->find_input('Owner')->possible_values),
+ 'user B is listed as potential owner';
+ $agent_a->select('Owner', $user_b->id);
+ $agent_a->submit;
+
+ $agent_a->content_like(qr/Ticket \d+ created in queue/i, 'created ticket');
+ my ($id) = ($agent_a->content =~ /Ticket (\d+) created in queue/);
+ ok $id, 'found id of the ticket';
+
+ my $ticket = RT::Ticket->new( $RT::SystemUser );
+ $ticket->Load( $id );
+ ok $ticket->id, 'loaded the ticket';
+ is $ticket->Owner, $user_b->id, 'correct owner';
+}
+
+my $agent_b = RT::Test::Web->new;
+ok $agent_b->login('user_b', 'password'), 'logged in as user B';
+
+diag "user A can not change owner after create" if $ENV{TEST_VERBOSE};
+{
+ my $ticket = RT::Ticket->new( $user_a );
+ my ($id, $txn, $msg) = $ticket->Create(
+ Queue => $queue->id,
+ Owner => $user_b->id,
+ Subject => 'test',
+ );
+ ok $id, 'created a ticket #'. $id or diag "error: $msg";
+ is $ticket->Owner, $user_b->id, 'correct owner';
+
+ # try the following group of tests twice with different agents(logins)
+ my $test_cb = sub {
+ my $agent = shift;
+ $agent->goto_ticket( $id );
+ $agent->follow_link_ok({text => 'Basics'}, 'Ticket -> Basics');
+ my $form = $agent->form_number(3);
+ is $form->value('Owner'), $user_b->id, 'correct owner selected';
+ $agent->select('Owner', $RT::Nobody->id);
+ $agent->submit;
+
+ $agent->content_like(
+ qr/Permission denied/i,
+ 'no way to change owner after create if you have no rights'
+ );
+
+ my $ticket = RT::Ticket->new( $RT::SystemUser );
+ $ticket->Load( $id );
+ ok $ticket->id, 'loaded the ticket';
+ is $ticket->Owner, $user_b->id, 'correct owner';
+ };
+
+ $test_cb->($agent_a);
+ diag "even owner(user B) can not change owner" if $ENV{TEST_VERBOSE};
+ $test_cb->($agent_b);
+}
+
+diag "on reply correct owner is selected" if $ENV{TEST_VERBOSE};
+{
+ my $ticket = RT::Ticket->new( $user_a );
+ my ($id, $txn, $msg) = $ticket->Create(
+ Queue => $queue->id,
+ Owner => $user_b->id,
+ Subject => 'test',
+ );
+ ok $id, 'created a ticket #'. $id or diag "error: $msg";
+ is $ticket->Owner, $user_b->id, 'correct owner';
+
+ $agent_a->goto_ticket( $id );
+ $agent_a->follow_link_ok({text => 'Reply'}, 'Ticket -> Basics');
+
+ my $form = $agent_a->form_number(3);
+ is $form->value('Owner'), '', 'empty value selected';
+ $agent_a->submit;
+
+ $ticket = RT::Ticket->new( $RT::SystemUser );
+ $ticket->Load( $id );
+ ok $ticket->id, 'loaded the ticket';
+ is $ticket->Owner, $user_b->id, 'correct owner';
+}
+
+ok( RT::Test->set_rights(
+ { Principal => $user_a, Right => [qw(SeeQueue ShowTicket CreateTicket OwnTicket)] },
+ { Principal => $user_b, Right => [qw(SeeQueue ShowTicket OwnTicket)] },
+), 'set rights');
+
+diag "Couldn't take without coresponding right" if $ENV{TEST_VERBOSE};
+{
+ my $ticket = RT::Ticket->new( $user_a );
+ my ($id, $txn, $msg) = $ticket->Create(
+ Queue => $queue->id,
+ Subject => 'test',
+ );
+ ok $id, 'created a ticket #'. $id or diag "error: $msg";
+ is $ticket->Owner, $RT::Nobody->id, 'correct owner';
+
+ $agent_a->goto_ticket( $id );
+ ok !($agent_a->find_all_links( text => 'Take' ))[0],
+ 'no Take link';
+ ok !($agent_a->find_all_links( text => 'Steal' ))[0],
+ 'no Steal link as well';
+}
+
+diag "Couldn't steal without coresponding right" if $ENV{TEST_VERBOSE};
+{
+ my $ticket = RT::Ticket->new( $user_a );
+ my ($id, $txn, $msg) = $ticket->Create(
+ Queue => $queue->id,
+ Owner => $user_b->id,
+ Subject => 'test',
+ );
+ ok $id, 'created a ticket #'. $id or diag "error: $msg";
+ is $ticket->Owner, $user_b->id, 'correct owner';
+
+ $agent_a->goto_ticket( $id );
+ ok !($agent_a->find_all_links( text => 'Steal' ))[0],
+ 'no Steal link';
+ ok !($agent_a->find_all_links( text => 'Take' ))[0],
+ 'no Take link as well';
+}
+
+ok( RT::Test->set_rights(
+ { Principal => $user_a, Right => [qw(SeeQueue ShowTicket CreateTicket TakeTicket)] },
+), 'set rights');
+
+diag "TakeTicket require OwnTicket to work" if $ENV{TEST_VERBOSE};
+{
+ my $ticket = RT::Ticket->new( $user_a );
+ my ($id, $txn, $msg) = $ticket->Create(
+ Queue => $queue->id,
+ Subject => 'test',
+ );
+ ok $id, 'created a ticket #'. $id or diag "error: $msg";
+ is $ticket->Owner, $RT::Nobody->id, 'correct owner';
+
+ $agent_a->goto_ticket( $id );
+ ok !($agent_a->find_all_links( text => 'Take' ))[0],
+ 'no Take link';
+ ok !($agent_a->find_all_links( text => 'Steal' ))[0],
+ 'no Steal link as well';
+}
+
+ok( RT::Test->set_rights(
+ { Principal => $user_a, Right => [qw(SeeQueue ShowTicket CreateTicket OwnTicket TakeTicket)] },
+ { Principal => $user_b, Right => [qw(SeeQueue ShowTicket OwnTicket)] },
+), 'set rights');
+
+diag "TakeTicket+OwnTicket work" if $ENV{TEST_VERBOSE};
+{
+ my $ticket = RT::Ticket->new( $user_a );
+ my ($id, $txn, $msg) = $ticket->Create(
+ Queue => $queue->id,
+ Subject => 'test',
+ );
+ ok $id, 'created a ticket #'. $id or diag "error: $msg";
+ is $ticket->Owner, $RT::Nobody->id, 'correct owner';
+
+ $agent_a->goto_ticket( $id );
+ ok !($agent_a->find_all_links( text => 'Steal' ))[0],
+ 'no Steal link';
+ $agent_a->follow_link_ok({text => 'Take'}, 'Ticket -> Take');
+
+ $ticket = RT::Ticket->new( $RT::SystemUser );
+ $ticket->Load( $id );
+ ok $ticket->id, 'loaded the ticket';
+ is $ticket->Owner, $user_a->id, 'correct owner';
+}
+
+diag "TakeTicket+OwnTicket don't work when owner is not nobody" if $ENV{TEST_VERBOSE};
+{
+ my $ticket = RT::Ticket->new( $user_a );
+ my ($id, $txn, $msg) = $ticket->Create(
+ Queue => $queue->id,
+ Owner => $user_b->id,
+ Subject => 'test',
+ );
+ ok $id, 'created a ticket #'. $id or diag "error: $msg";
+ is $ticket->Owner, $user_b->id, 'correct owner';
+
+ $agent_a->goto_ticket( $id );
+ ok !($agent_a->find_all_links( text => 'Take' ))[0],
+ 'no Take link';
+ ok !($agent_a->find_all_links( text => 'Steal' ))[0],
+ 'no Steal link too';
+}
+
+ok( RT::Test->set_rights(
+ { Principal => $user_a, Right => [qw(SeeQueue ShowTicket CreateTicket StealTicket)] },
+ { Principal => $user_b, Right => [qw(SeeQueue ShowTicket OwnTicket)] },
+), 'set rights');
+
+diag "StealTicket require OwnTicket to work" if $ENV{TEST_VERBOSE};
+{
+ my $ticket = RT::Ticket->new( $user_a );
+ my ($id, $txn, $msg) = $ticket->Create(
+ Queue => $queue->id,
+ Owner => $user_b->id,
+ Subject => 'test',
+ );
+ ok $id, 'created a ticket #'. $id or diag "error: $msg";
+ is $ticket->Owner, $user_b->id, 'correct owner';
+
+ $agent_a->goto_ticket( $id );
+ ok !($agent_a->find_all_links( text => 'Steal' ))[0],
+ 'no Steal link';
+ ok !($agent_a->find_all_links( text => 'Take' ))[0],
+ 'no Take link too';
+}
+
+ok( RT::Test->set_rights(
+ { Principal => $user_a, Right => [qw(SeeQueue ShowTicket CreateTicket OwnTicket StealTicket)] },
+ { Principal => $user_b, Right => [qw(SeeQueue ShowTicket OwnTicket)] },
+), 'set rights');
+
+diag "StealTicket+OwnTicket work" if $ENV{TEST_VERBOSE};
+{
+ my $ticket = RT::Ticket->new( $user_a );
+ my ($id, $txn, $msg) = $ticket->Create(
+ Queue => $queue->id,
+ Owner => $user_b->id,
+ Subject => 'test',
+ );
+ ok $id, 'created a ticket #'. $id or diag "error: $msg";
+ is $ticket->Owner, $user_b->id, 'correct owner';
+
+ $agent_a->goto_ticket( $id );
+ ok !($agent_a->find_all_links( text => 'Take' ))[0],
+ 'but no Take link';
+ $agent_a->follow_link_ok({text => 'Steal'}, 'Ticket -> Steal');
+
+ $ticket = RT::Ticket->new( $RT::SystemUser );
+ $ticket->Load( $id );
+ ok $ticket->id, 'loaded the ticket';
+ is $ticket->Owner, $user_a->id, 'correct owner';
+}
+
+diag "StealTicket+OwnTicket don't work when owner is nobody" if $ENV{TEST_VERBOSE};
+{
+ my $ticket = RT::Ticket->new( $user_a );
+ my ($id, $txn, $msg) = $ticket->Create(
+ Queue => $queue->id,
+ Subject => 'test',
+ );
+ ok $id, 'created a ticket #'. $id or diag "error: $msg";
+ is $ticket->Owner, $RT::Nobody->id, 'correct owner';
+
+ $agent_a->goto_ticket( $id );
+ ok !($agent_a->find_all_links( text => 'Steal' ))[0],
+ 'no Steal link';
+ ok !($agent_a->find_all_links( text => 'Take' ))[0],
+ 'no Take link as well (no right)';
+}
+
+ok( RT::Test->set_rights(
+ { Principal => $user_a, Right => [qw(SeeQueue ShowTicket CreateTicket OwnTicket TakeTicket StealTicket)] },
+ { Principal => $user_b, Right => [qw(SeeQueue ShowTicket OwnTicket)] },
+), 'set rights');
+
+diag "no Steal link when owner nobody" if $ENV{TEST_VERBOSE};
+{
+ my $ticket = RT::Ticket->new( $user_a );
+ my ($id, $txn, $msg) = $ticket->Create(
+ Queue => $queue->id,
+ Subject => 'test',
+ );
+ ok $id, 'created a ticket #'. $id or diag "error: $msg";
+ is $ticket->Owner, $RT::Nobody->id, 'correct owner';
+
+ $agent_a->goto_ticket( $id );
+ ok !($agent_a->find_all_links( text => 'Steal' ))[0],
+ 'no Steal link';
+ ok( ($agent_a->find_all_links( text => 'Take' ))[0],
+ 'but have Take link');
+}
+
+diag "no Take link when owner is not nobody" if $ENV{TEST_VERBOSE};
+{
+ my $ticket = RT::Ticket->new( $user_a );
+ my ($id, $txn, $msg) = $ticket->Create(
+ Queue => $queue->id,
+ Owner => $user_b->id,
+ Subject => 'test',
+ );
+ ok $id, 'created a ticket #'. $id or diag "error: $msg";
+ is $ticket->Owner, $user_b->id, 'correct owner';
+
+ $agent_a->goto_ticket( $id );
+ ok !($agent_a->find_all_links( text => 'Take' ))[0],
+ 'no Take link';
+ ok( ($agent_a->find_all_links( text => 'Steal' ))[0],
+ 'but have Steal link');
+}
+
diff --git a/rt/t/web/ticket_seen.t b/rt/t/web/ticket_seen.t
new file mode 100644
index 000000000..00b2632d8
--- /dev/null
+++ b/rt/t/web/ticket_seen.t
@@ -0,0 +1,80 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use RT::Test tests => 16;
+
+my $queue = RT::Test->load_or_create_queue( Name => 'Regression' );
+ok $queue && $queue->id, 'loaded or created queue';
+
+my $user_a = RT::Test->load_or_create_user(
+ Name => 'user_a', Password => 'password',
+);
+ok $user_a && $user_a->id, 'loaded or created user';
+
+my $user_b = RT::Test->load_or_create_user(
+ Name => 'user_b', Password => 'password',
+);
+ok $user_b && $user_b->id, 'loaded or created user';
+
+ok( RT::Test->set_rights(
+ { Principal => $user_a, Right => [qw(SeeQueue ShowTicket CreateTicket OwnTicket ModifyTicket)] },
+ { Principal => $user_b, Right => [qw(SeeQueue ShowTicket ReplyToTicket)] },
+), 'set rights');
+RT::Test->started_ok;
+
+my $agent_a = RT::Test::Web->new;
+ok $agent_a->login('user_a', 'password'), 'logged in as user A';
+
+my $agent_b = RT::Test::Web->new;
+ok $agent_b->login('user_b', 'password'), 'logged in as user B';
+
+diag "create a ticket for testing" if $ENV{TEST_VERBOSE};
+my $tid;
+{
+ my $ticket = RT::Ticket->new( $user_a );
+ my ($txn, $msg);
+ ($tid, $txn, $msg) = $ticket->Create(
+ Queue => $queue->id,
+ Owner => $user_a->id,
+ Subject => 'test',
+ );
+ ok $tid, 'created a ticket #'. $tid or diag "error: $msg";
+ is $ticket->Owner, $user_a->id, 'correct owner';
+}
+
+diag "user B adds a message, we check that user A see notification and can clear it" if $ENV{TEST_VERBOSE};
+{
+ my $ticket = RT::Ticket->new( $user_b );
+ $ticket->Load( $tid );
+ ok $ticket->id, 'loaded the ticket';
+
+ my ($status, $msg) = $ticket->Correspond( Content => 'bla-bla' );
+ ok $status, 'added reply' or diag "error: $msg";
+
+ $agent_a->goto_ticket($tid);
+ $agent_a->content_like(qr/bla-bla/ims, 'the message on the page');
+
+ $agent_a->content_like(
+ qr/unread message/ims,
+ 'we have not seen something'
+ );
+
+ $agent_a->follow_link_ok({text => 'jump to the first unread message and mark all messages as seen'}, 'try to mark all as seen');
+ $agent_a->content_like(
+ qr/Marked all messages as seen/ims,
+ 'see success message'
+ );
+
+ $agent_a->goto_ticket($tid);
+ $agent_a->content_unlike(
+ qr/unread message/ims,
+ 'we have seen everything, so no messages'
+ );
+}
+
+
+
+
+
diff --git a/rt/t/web/ticket_update_without_content.t b/rt/t/web/ticket_update_without_content.t
new file mode 100644
index 000000000..595cb74e9
--- /dev/null
+++ b/rt/t/web/ticket_update_without_content.t
@@ -0,0 +1,52 @@
+#!/usr/bin/env perl
+use strict;
+use warnings;
+
+use RT::Test tests => 10;
+my ( $url, $m ) = RT::Test->started_ok;
+
+# merged tickets still show up in search
+my $ticket = RT::Ticket->new($RT::SystemUser);
+my ( $ret, $msg ) = $ticket->Create(
+ Subject => 'base ticket' . $$,
+ Queue => 'general',
+ Owner => 'root',
+ Requestor => 'root@localhost',
+ MIMEObj => MIME::Entity->build(
+ From => 'root@localhost',
+ To => 'rt@localhost',
+ Subject => 'base ticket' . $$,
+ Data => "",
+ ),
+);
+ok( $ret, "ticket created: $msg" );
+
+ok( $m->login, 'logged in' );
+
+$m->get_ok( $url . "/Ticket/ModifyAll.html?id=" . $ticket->id );
+
+$m->submit_form(
+ form_number => 3,
+ fields => { Priority => '1', }
+);
+
+$m->content_like(qr/priority changed/i);
+$m->content_unlike(qr/message recorded/i);
+
+my $root = RT::User->new( $RT::SystemUser );
+$root->Load('root');
+( $ret, $msg ) = $root->SetSignature(<<EOF);
+best wishes
+foo
+EOF
+
+ok( $ret, $msg );
+
+$m->get_ok( $url . "/Ticket/ModifyAll.html?id=" . $ticket->id );
+
+$m->submit_form(
+ form_number => 3,
+ fields => { Priority => '2', }
+);
+$m->content_like(qr/priority changed/i);
+$m->content_unlike(qr/message recorded/i);
diff --git a/rt/t/web/unlimited_search.t b/rt/t/web/unlimited_search.t
new file mode 100644
index 000000000..d98baaac0
--- /dev/null
+++ b/rt/t/web/unlimited_search.t
@@ -0,0 +1,41 @@
+#!/usr/bin/perl
+
+use strict;
+
+use RT::Test tests => 8;
+RT::Test->started_ok;
+
+my $ticket = RT::Ticket->new($RT::SystemUser);
+for ( 1 .. 75 ) {
+ $ticket->Create(
+ Subject => 'Ticket ' . $_,
+ Queue => 'General',
+ Owner => 'root',
+ Requestor => 'unlimitedsearch@localhost',
+ );
+}
+
+my $agent = RT::Test::Web->new;
+ok $agent->login('root', 'password'), 'logged in as root';
+
+$agent->get_ok('/Search/Build.html');
+$agent->form_name('BuildQuery');
+$agent->field('idOp', '>');
+$agent->field('ValueOfid', '0');
+$agent->submit('AddClause');
+$agent->form_name('BuildQuery');
+$agent->field('RowsPerPage', '0');
+$agent->submit('DoSearch');
+$agent->follow_link_ok({text=>'Show Results'});
+$agent->content_like(qr/Ticket 75/);
+
+$agent->follow_link_ok({text=>'New Search'});
+$agent->form_name('BuildQuery');
+$agent->field('idOp', '>');
+$agent->field('ValueOfid', '0');
+$agent->submit('AddClause');
+$agent->form_name('BuildQuery');
+$agent->field('RowsPerPage', '50');
+$agent->submit('DoSearch');
+$agent->follow_link_ok({text=>'Bulk Update'});
+$agent->content_unlike(qr/Ticket 51/);